summaryrefslogtreecommitdiffstats
path: root/netwerk/test/unit
diff options
context:
space:
mode:
Diffstat (limited to 'netwerk/test/unit')
-rw-r--r--netwerk/test/unit/client-cert.p12bin0 -> 2333 bytes
-rw-r--r--netwerk/test/unit/client-cert.p12.pkcs12spec3
-rw-r--r--netwerk/test/unit/data/cookies_v10.sqlitebin0 -> 131072 bytes
-rw-r--r--netwerk/test/unit/data/image.pngbin0 -> 102591 bytes
-rw-r--r--netwerk/test/unit/data/signed_win.exebin0 -> 61064 bytes
-rw-r--r--netwerk/test/unit/data/system_root.lnkbin0 -> 1677 bytes
-rw-r--r--netwerk/test/unit/data/test_psl.txt98
-rw-r--r--netwerk/test/unit/data/test_readline1.txt0
-rw-r--r--netwerk/test/unit/data/test_readline2.txt1
-rw-r--r--netwerk/test/unit/data/test_readline3.txt3
-rw-r--r--netwerk/test/unit/data/test_readline4.txt3
-rw-r--r--netwerk/test/unit/data/test_readline5.txt1
-rw-r--r--netwerk/test/unit/data/test_readline6.txt1
-rw-r--r--netwerk/test/unit/data/test_readline7.txt2
-rw-r--r--netwerk/test/unit/data/test_readline8.txt1
-rw-r--r--netwerk/test/unit/head_cache.js137
-rw-r--r--netwerk/test/unit/head_cache2.js429
-rw-r--r--netwerk/test/unit/head_channels.js527
-rw-r--r--netwerk/test/unit/head_cookies.js1062
-rw-r--r--netwerk/test/unit/head_http3.js105
-rw-r--r--netwerk/test/unit/head_servers.js925
-rw-r--r--netwerk/test/unit/head_telemetry.js171
-rw-r--r--netwerk/test/unit/head_trr.js550
-rw-r--r--netwerk/test/unit/head_websocket.js71
-rw-r--r--netwerk/test/unit/head_webtransport.js134
-rw-r--r--netwerk/test/unit/http2-ca.pem18
-rw-r--r--netwerk/test/unit/http2-ca.pem.certspec4
-rw-r--r--netwerk/test/unit/http2_test_common.js1504
-rw-r--r--netwerk/test/unit/node_execute/test_node_execute_loop.js22
-rw-r--r--netwerk/test/unit/node_execute/xpcshell.toml5
-rw-r--r--netwerk/test/unit/perftest.toml3
-rw-r--r--netwerk/test/unit/proxy-ca.pem18
-rw-r--r--netwerk/test/unit/proxy-ca.pem.certspec4
-rw-r--r--netwerk/test/unit/socks_client_subprocess.js94
-rw-r--r--netwerk/test/unit/test_1073747.js42
-rw-r--r--netwerk/test/unit/test_304_headers.js91
-rw-r--r--netwerk/test/unit/test_304_responses.js92
-rw-r--r--netwerk/test/unit/test_307_redirect.js95
-rw-r--r--netwerk/test/unit/test_421.js65
-rw-r--r--netwerk/test/unit/test_MIME_params.js798
-rw-r--r--netwerk/test/unit/test_NetUtil.js804
-rw-r--r--netwerk/test/unit/test_SuperfluousAuth.js101
-rw-r--r--netwerk/test/unit/test_URIs.js996
-rw-r--r--netwerk/test/unit/test_URIs2.js881
-rw-r--r--netwerk/test/unit/test_XHR_redirects.js275
-rw-r--r--netwerk/test/unit/test_about_networking.js120
-rw-r--r--netwerk/test/unit/test_about_protocol.js49
-rw-r--r--netwerk/test/unit/test_aboutblank.js32
-rw-r--r--netwerk/test/unit/test_addr_in_use_error.js36
-rw-r--r--netwerk/test/unit/test_alt-data_closeWithStatus.js184
-rw-r--r--netwerk/test/unit/test_alt-data_cross_process.js153
-rw-r--r--netwerk/test/unit/test_alt-data_overwrite.js209
-rw-r--r--netwerk/test/unit/test_alt-data_simple.js212
-rw-r--r--netwerk/test/unit/test_alt-data_stream.js163
-rw-r--r--netwerk/test/unit/test_alt-data_too_big.js113
-rw-r--r--netwerk/test/unit/test_altsvc.js597
-rw-r--r--netwerk/test/unit/test_altsvc_http3.js494
-rw-r--r--netwerk/test/unit/test_altsvc_pref.js136
-rw-r--r--netwerk/test/unit/test_anonymous-coalescing.js179
-rw-r--r--netwerk/test/unit/test_auth_dialog_permission.js280
-rw-r--r--netwerk/test/unit/test_auth_jar.js92
-rw-r--r--netwerk/test/unit/test_auth_multiple.js464
-rw-r--r--netwerk/test/unit/test_auth_proxy.js463
-rw-r--r--netwerk/test/unit/test_authentication.js1400
-rw-r--r--netwerk/test/unit/test_authpromptwrapper.js207
-rw-r--r--netwerk/test/unit/test_backgroundfilesaver.js761
-rw-r--r--netwerk/test/unit/test_be_conservative.js256
-rw-r--r--netwerk/test/unit/test_be_conservative_error_handling.js216
-rw-r--r--netwerk/test/unit/test_bhttp.js220
-rw-r--r--netwerk/test/unit/test_blob_channelname.js42
-rw-r--r--netwerk/test/unit/test_brotli_decoding.js128
-rw-r--r--netwerk/test/unit/test_brotli_http.js122
-rw-r--r--netwerk/test/unit/test_brotli_unknown_content_type.js74
-rw-r--r--netwerk/test/unit/test_bug1064258.js144
-rw-r--r--netwerk/test/unit/test_bug1177909.js251
-rw-r--r--netwerk/test/unit/test_bug1195415.js116
-rw-r--r--netwerk/test/unit/test_bug1218029.js116
-rw-r--r--netwerk/test/unit/test_bug1279246.js100
-rw-r--r--netwerk/test/unit/test_bug1312774_http1.js149
-rw-r--r--netwerk/test/unit/test_bug1312782_http1.js197
-rw-r--r--netwerk/test/unit/test_bug1355539_http1.js206
-rw-r--r--netwerk/test/unit/test_bug1378385_http1.js198
-rw-r--r--netwerk/test/unit/test_bug1411316_http1.js116
-rw-r--r--netwerk/test/unit/test_bug1527293.js94
-rw-r--r--netwerk/test/unit/test_bug1683176.js91
-rw-r--r--netwerk/test/unit/test_bug1725766.js85
-rw-r--r--netwerk/test/unit/test_bug203271.js249
-rw-r--r--netwerk/test/unit/test_bug248970_cache.js144
-rw-r--r--netwerk/test/unit/test_bug248970_cookie.js148
-rw-r--r--netwerk/test/unit/test_bug261425.js29
-rw-r--r--netwerk/test/unit/test_bug263127.js58
-rw-r--r--netwerk/test/unit/test_bug282432.js37
-rw-r--r--netwerk/test/unit/test_bug321706.js10
-rw-r--r--netwerk/test/unit/test_bug331825.js43
-rw-r--r--netwerk/test/unit/test_bug336501.js26
-rw-r--r--netwerk/test/unit/test_bug337744.js126
-rw-r--r--netwerk/test/unit/test_bug368702.js147
-rw-r--r--netwerk/test/unit/test_bug369787.js73
-rw-r--r--netwerk/test/unit/test_bug371473.js36
-rw-r--r--netwerk/test/unit/test_bug376844.js19
-rw-r--r--netwerk/test/unit/test_bug376865.js23
-rw-r--r--netwerk/test/unit/test_bug379034.js19
-rw-r--r--netwerk/test/unit/test_bug380994.js24
-rw-r--r--netwerk/test/unit/test_bug388281.js25
-rw-r--r--netwerk/test/unit/test_bug396389.js63
-rw-r--r--netwerk/test/unit/test_bug401564.js42
-rw-r--r--netwerk/test/unit/test_bug411952.js53
-rw-r--r--netwerk/test/unit/test_bug412457.js79
-rw-r--r--netwerk/test/unit/test_bug412945.js44
-rw-r--r--netwerk/test/unit/test_bug414122.js59
-rw-r--r--netwerk/test/unit/test_bug427957.js100
-rw-r--r--netwerk/test/unit/test_bug429347.js39
-rw-r--r--netwerk/test/unit/test_bug455311.js128
-rw-r--r--netwerk/test/unit/test_bug464591.js94
-rw-r--r--netwerk/test/unit/test_bug468426.js131
-rw-r--r--netwerk/test/unit/test_bug468594.js178
-rw-r--r--netwerk/test/unit/test_bug470716.js171
-rw-r--r--netwerk/test/unit/test_bug477578.js51
-rw-r--r--netwerk/test/unit/test_bug479413.js48
-rw-r--r--netwerk/test/unit/test_bug479485.js65
-rw-r--r--netwerk/test/unit/test_bug482601.js264
-rw-r--r--netwerk/test/unit/test_bug482934.js190
-rw-r--r--netwerk/test/unit/test_bug490095.js160
-rw-r--r--netwerk/test/unit/test_bug504014.js72
-rw-r--r--netwerk/test/unit/test_bug510359.js104
-rw-r--r--netwerk/test/unit/test_bug526789.js289
-rw-r--r--netwerk/test/unit/test_bug528292.js87
-rw-r--r--netwerk/test/unit/test_bug536324_64bit_content_length.js66
-rw-r--r--netwerk/test/unit/test_bug540566.js24
-rw-r--r--netwerk/test/unit/test_bug553970.js50
-rw-r--r--netwerk/test/unit/test_bug561042.js44
-rw-r--r--netwerk/test/unit/test_bug561276.js66
-rw-r--r--netwerk/test/unit/test_bug580508.js31
-rw-r--r--netwerk/test/unit/test_bug586908.js103
-rw-r--r--netwerk/test/unit/test_bug596443.js117
-rw-r--r--netwerk/test/unit/test_bug618835.js124
-rw-r--r--netwerk/test/unit/test_bug633743.js195
-rw-r--r--netwerk/test/unit/test_bug650522.js35
-rw-r--r--netwerk/test/unit/test_bug650995.js187
-rw-r--r--netwerk/test/unit/test_bug652761.js19
-rw-r--r--netwerk/test/unit/test_bug654926.js91
-rw-r--r--netwerk/test/unit/test_bug654926_doom_and_read.js82
-rw-r--r--netwerk/test/unit/test_bug654926_test_seek.js76
-rw-r--r--netwerk/test/unit/test_bug659569.js60
-rw-r--r--netwerk/test/unit/test_bug660066.js53
-rw-r--r--netwerk/test/unit/test_bug667087.js33
-rw-r--r--netwerk/test/unit/test_bug667818.js49
-rw-r--r--netwerk/test/unit/test_bug667907.js88
-rw-r--r--netwerk/test/unit/test_bug669001.js178
-rw-r--r--netwerk/test/unit/test_bug770243.js246
-rw-r--r--netwerk/test/unit/test_bug812167.js143
-rw-r--r--netwerk/test/unit/test_bug826063.js88
-rw-r--r--netwerk/test/unit/test_bug856978.js138
-rw-r--r--netwerk/test/unit/test_bug894586.js135
-rw-r--r--netwerk/test/unit/test_bug935499.js10
-rw-r--r--netwerk/test/unit/test_cache-control_request.js448
-rw-r--r--netwerk/test/unit/test_cache-entry-id.js220
-rw-r--r--netwerk/test/unit/test_cache2-00-service-get.js15
-rw-r--r--netwerk/test/unit/test_cache2-01-basic.js45
-rw-r--r--netwerk/test/unit/test_cache2-01a-basic-readonly.js45
-rw-r--r--netwerk/test/unit/test_cache2-01b-basic-datasize.js51
-rw-r--r--netwerk/test/unit/test_cache2-01c-basic-hasmeta-only.js45
-rw-r--r--netwerk/test/unit/test_cache2-01d-basic-not-wanted.js45
-rw-r--r--netwerk/test/unit/test_cache2-01e-basic-bypass-if-busy.js39
-rw-r--r--netwerk/test/unit/test_cache2-01f-basic-openTruncate.js24
-rw-r--r--netwerk/test/unit/test_cache2-02-open-non-existing.js45
-rw-r--r--netwerk/test/unit/test_cache2-02b-open-non-existing-and-doom.js180
-rw-r--r--netwerk/test/unit/test_cache2-03-oncacheentryavail-throws.js36
-rw-r--r--netwerk/test/unit/test_cache2-04-oncacheentryavail-throws2x.js45
-rw-r--r--netwerk/test/unit/test_cache2-05-visit.js113
-rw-r--r--netwerk/test/unit/test_cache2-06-pb-mode.js50
-rw-r--r--netwerk/test/unit/test_cache2-07-visit-memory.js123
-rw-r--r--netwerk/test/unit/test_cache2-07a-open-memory.js81
-rw-r--r--netwerk/test/unit/test_cache2-08-evict-disk-by-memory-storage.js25
-rw-r--r--netwerk/test/unit/test_cache2-09-evict-disk-by-uri.js32
-rw-r--r--netwerk/test/unit/test_cache2-10-evict-direct.js29
-rw-r--r--netwerk/test/unit/test_cache2-10b-evict-direct-immediate.js21
-rw-r--r--netwerk/test/unit/test_cache2-11-evict-memory.js89
-rw-r--r--netwerk/test/unit/test_cache2-12-evict-disk.js81
-rw-r--r--netwerk/test/unit/test_cache2-13-evict-non-existing.js16
-rw-r--r--netwerk/test/unit/test_cache2-14-concurent-readers.js48
-rw-r--r--netwerk/test/unit/test_cache2-14b-concurent-readers-complete.js76
-rw-r--r--netwerk/test/unit/test_cache2-15-conditional-304.js60
-rw-r--r--netwerk/test/unit/test_cache2-16-conditional-200.js76
-rw-r--r--netwerk/test/unit/test_cache2-17-evict-all.js17
-rw-r--r--netwerk/test/unit/test_cache2-18-not-valid.js38
-rw-r--r--netwerk/test/unit/test_cache2-19-range-206.js65
-rw-r--r--netwerk/test/unit/test_cache2-20-range-200.js72
-rw-r--r--netwerk/test/unit/test_cache2-21-anon-storage.js52
-rw-r--r--netwerk/test/unit/test_cache2-22-anon-visit.js43
-rw-r--r--netwerk/test/unit/test_cache2-23-read-over-chunk.js34
-rw-r--r--netwerk/test/unit/test_cache2-24-exists.js43
-rw-r--r--netwerk/test/unit/test_cache2-25-chunk-memory-limit.js53
-rw-r--r--netwerk/test/unit/test_cache2-26-no-outputstream-open.js36
-rw-r--r--netwerk/test/unit/test_cache2-27-force-valid-for.js35
-rw-r--r--netwerk/test/unit/test_cache2-28-last-access-attrs.js46
-rw-r--r--netwerk/test/unit/test_cache2-28a-OPEN_SECRETLY.js42
-rw-r--r--netwerk/test/unit/test_cache2-29a-concurrent_read_resumable_entry_size_zero.js76
-rw-r--r--netwerk/test/unit/test_cache2-29b-concurrent_read_non-resumable_entry_size_zero.js80
-rw-r--r--netwerk/test/unit/test_cache2-29c-concurrent_read_half-interrupted.js95
-rw-r--r--netwerk/test/unit/test_cache2-29d-concurrent_read_half-corrupted-206.js95
-rw-r--r--netwerk/test/unit/test_cache2-29e-concurrent_read_half-non-206-response.js90
-rw-r--r--netwerk/test/unit/test_cache2-30a-entry-pinning.js39
-rw-r--r--netwerk/test/unit/test_cache2-30b-pinning-storage-clear.js45
-rw-r--r--netwerk/test/unit/test_cache2-30c-pinning-deferred-doom.js185
-rw-r--r--netwerk/test/unit/test_cache2-30d-pinning-WasEvicted-API.js148
-rw-r--r--netwerk/test/unit/test_cache2-31-visit-all.js88
-rw-r--r--netwerk/test/unit/test_cache2-32-clear-origin.js69
-rw-r--r--netwerk/test/unit/test_cache_204_response.js62
-rw-r--r--netwerk/test/unit/test_cache_jar.js105
-rw-r--r--netwerk/test/unit/test_cacheflags.js437
-rw-r--r--netwerk/test/unit/test_captive_portal_service.js325
-rw-r--r--netwerk/test/unit/test_cert_info.js162
-rw-r--r--netwerk/test/unit/test_cert_verification_failure.js66
-rw-r--r--netwerk/test/unit/test_channel_close.js70
-rw-r--r--netwerk/test/unit/test_channel_long_domain.js14
-rw-r--r--netwerk/test/unit/test_channel_priority.js98
-rw-r--r--netwerk/test/unit/test_chunked_responses.js180
-rw-r--r--netwerk/test/unit/test_client_auth_with_proxy.js176
-rw-r--r--netwerk/test/unit/test_coaleasing_h2_and_h3_connection.js109
-rw-r--r--netwerk/test/unit/test_compareURIs.js61
-rw-r--r--netwerk/test/unit/test_compressappend.js99
-rw-r--r--netwerk/test/unit/test_connection_based_auth.js91
-rw-r--r--netwerk/test/unit/test_content_encoding_gzip.js163
-rw-r--r--netwerk/test/unit/test_content_length_underrun.js295
-rw-r--r--netwerk/test/unit/test_content_sniffer.js157
-rw-r--r--netwerk/test/unit/test_cookie_blacklist.js43
-rw-r--r--netwerk/test/unit/test_cookie_header.js112
-rw-r--r--netwerk/test/unit/test_cookie_ipv6.js53
-rw-r--r--netwerk/test/unit/test_cookie_partitioned_attribute.js80
-rw-r--r--netwerk/test/unit/test_cookiejars.js188
-rw-r--r--netwerk/test/unit/test_cookiejars_safebrowsing.js231
-rw-r--r--netwerk/test/unit/test_cookies_async_failure.js514
-rw-r--r--netwerk/test/unit/test_cookies_partition_counting.js183
-rw-r--r--netwerk/test/unit/test_cookies_privatebrowsing.js132
-rw-r--r--netwerk/test/unit/test_cookies_profile_close.js114
-rw-r--r--netwerk/test/unit/test_cookies_purge_counting.js74
-rw-r--r--netwerk/test/unit/test_cookies_purge_counting_per_host.js81
-rw-r--r--netwerk/test/unit/test_cookies_read.js119
-rw-r--r--netwerk/test/unit/test_cookies_sync_failure.js344
-rw-r--r--netwerk/test/unit/test_cookies_thirdparty.js164
-rw-r--r--netwerk/test/unit/test_cookies_thirdparty_session.js77
-rw-r--r--netwerk/test/unit/test_cookies_upgrade_10.js61
-rw-r--r--netwerk/test/unit/test_data_protocol.js90
-rw-r--r--netwerk/test/unit/test_defaultURI.js186
-rw-r--r--netwerk/test/unit/test_dns_by_type_resolve.js83
-rw-r--r--netwerk/test/unit/test_dns_cancel.js119
-rw-r--r--netwerk/test/unit/test_dns_disable_ipv4.js68
-rw-r--r--netwerk/test/unit/test_dns_disable_ipv6.js51
-rw-r--r--netwerk/test/unit/test_dns_disabled.js88
-rw-r--r--netwerk/test/unit/test_dns_localredirect.js59
-rw-r--r--netwerk/test/unit/test_dns_offline.js105
-rw-r--r--netwerk/test/unit/test_dns_onion.js76
-rw-r--r--netwerk/test/unit/test_dns_originAttributes.js93
-rw-r--r--netwerk/test/unit/test_dns_override.js515
-rw-r--r--netwerk/test/unit/test_dns_override_for_localhost.js92
-rw-r--r--netwerk/test/unit/test_dns_proxy_bypass.js95
-rw-r--r--netwerk/test/unit/test_dns_retry.js319
-rw-r--r--netwerk/test/unit/test_dns_service.js123
-rw-r--r--netwerk/test/unit/test_domain_eviction.js182
-rw-r--r--netwerk/test/unit/test_dooh.js354
-rw-r--r--netwerk/test/unit/test_doomentry.js108
-rw-r--r--netwerk/test/unit/test_duplicate_headers.js563
-rw-r--r--netwerk/test/unit/test_early_hint_listener.js170
-rw-r--r--netwerk/test/unit/test_early_hint_listener_http2.js110
-rw-r--r--netwerk/test/unit/test_ech_grease.js270
-rw-r--r--netwerk/test/unit/test_event_sink.js183
-rw-r--r--netwerk/test/unit/test_eviction.js252
-rw-r--r--netwerk/test/unit/test_extract_charset_from_content_type.js238
-rw-r--r--netwerk/test/unit/test_file_protocol.js277
-rw-r--r--netwerk/test/unit/test_filestreams.js300
-rw-r--r--netwerk/test/unit/test_freshconnection.js30
-rw-r--r--netwerk/test/unit/test_getHost.js65
-rw-r--r--netwerk/test/unit/test_gio_protocol.js201
-rw-r--r--netwerk/test/unit/test_gre_resources.js30
-rw-r--r--netwerk/test/unit/test_h2proxy_connection_limit.js77
-rw-r--r--netwerk/test/unit/test_head.js171
-rw-r--r--netwerk/test/unit/test_head_request_no_response_body.js78
-rw-r--r--netwerk/test/unit/test_header_Accept-Language.js99
-rw-r--r--netwerk/test/unit/test_header_Accept-Language_case.js50
-rw-r--r--netwerk/test/unit/test_header_Server_Timing.js64
-rw-r--r--netwerk/test/unit/test_headers.js184
-rw-r--r--netwerk/test/unit/test_hostnameIsLocalIPAddress.js37
-rw-r--r--netwerk/test/unit/test_hostnameIsSharedIPAddress.js17
-rw-r--r--netwerk/test/unit/test_hpke_config_manager.js112
-rw-r--r--netwerk/test/unit/test_http1-proxy.js231
-rw-r--r--netwerk/test/unit/test_http2-proxy-failing.js174
-rw-r--r--netwerk/test/unit/test_http2-proxy.js862
-rw-r--r--netwerk/test/unit/test_http2.js481
-rw-r--r--netwerk/test/unit/test_http2_with_proxy.js425
-rw-r--r--netwerk/test/unit/test_http3.js571
-rw-r--r--netwerk/test/unit/test_http3_0rtt.js96
-rw-r--r--netwerk/test/unit/test_http3_421.js172
-rw-r--r--netwerk/test/unit/test_http3_alt_svc.js136
-rw-r--r--netwerk/test/unit/test_http3_coalescing.js113
-rw-r--r--netwerk/test/unit/test_http3_direct_proxy.js54
-rw-r--r--netwerk/test/unit/test_http3_dns_retry.js357
-rw-r--r--netwerk/test/unit/test_http3_early_hint_listener.js92
-rw-r--r--netwerk/test/unit/test_http3_error_before_connect.js109
-rw-r--r--netwerk/test/unit/test_http3_fast_fallback.js908
-rw-r--r--netwerk/test/unit/test_http3_fatal_stream_error.js135
-rw-r--r--netwerk/test/unit/test_http3_large_post.js165
-rw-r--r--netwerk/test/unit/test_http3_large_post_telemetry.js151
-rw-r--r--netwerk/test/unit/test_http3_perf.js262
-rw-r--r--netwerk/test/unit/test_http3_prio_disabled.js106
-rw-r--r--netwerk/test/unit/test_http3_prio_enabled.js108
-rw-r--r--netwerk/test/unit/test_http3_prio_helpers.js121
-rw-r--r--netwerk/test/unit/test_http3_server.js168
-rw-r--r--netwerk/test/unit/test_http3_server_not_existing.js109
-rw-r--r--netwerk/test/unit/test_http3_trans_close.js84
-rw-r--r--netwerk/test/unit/test_http3_version1.js93
-rw-r--r--netwerk/test/unit/test_httpResponseTimeout.js162
-rw-r--r--netwerk/test/unit/test_http_408_retry.js92
-rw-r--r--netwerk/test/unit/test_http_headers.js75
-rw-r--r--netwerk/test/unit/test_http_server_timing.js97
-rw-r--r--netwerk/test/unit/test_http_sfv.js597
-rw-r--r--netwerk/test/unit/test_httpauth.js204
-rw-r--r--netwerk/test/unit/test_httpcancel.js261
-rw-r--r--netwerk/test/unit/test_https_rr_ech_prefs.js535
-rw-r--r--netwerk/test/unit/test_https_rr_sorted_alpn.js226
-rw-r--r--netwerk/test/unit/test_httpssvc_ech_with_alpn.js246
-rw-r--r--netwerk/test/unit/test_httpssvc_https_upgrade.js352
-rw-r--r--netwerk/test/unit/test_httpssvc_iphint.js350
-rw-r--r--netwerk/test/unit/test_httpssvc_priority.js125
-rw-r--r--netwerk/test/unit/test_httpssvc_retry_with_ech.js511
-rw-r--r--netwerk/test/unit/test_httpssvc_retry_without_ech.js138
-rw-r--r--netwerk/test/unit/test_httpsuspend.js86
-rw-r--r--netwerk/test/unit/test_idn_blacklist.js168
-rw-r--r--netwerk/test/unit/test_idn_spoof.js1052
-rw-r--r--netwerk/test/unit/test_idn_urls.js436
-rw-r--r--netwerk/test/unit/test_idna2008.js65
-rw-r--r--netwerk/test/unit/test_idnservice.js39
-rw-r--r--netwerk/test/unit/test_immutable.js206
-rw-r--r--netwerk/test/unit/test_inhibit_caching.js94
-rw-r--r--netwerk/test/unit/test_ioservice.js19
-rw-r--r--netwerk/test/unit/test_large_port.js65
-rw-r--r--netwerk/test/unit/test_link.desktop3
-rw-r--r--netwerk/test/unit/test_link.lnkbin0 -> 345 bytes
-rw-r--r--netwerk/test/unit/test_link.url5
-rw-r--r--netwerk/test/unit/test_loadgroup_cancel.js96
-rw-r--r--netwerk/test/unit/test_localhost_offline.js77
-rw-r--r--netwerk/test/unit/test_localstreams.js89
-rw-r--r--netwerk/test/unit/test_mismatch_last-modified.js154
-rw-r--r--netwerk/test/unit/test_mozTXTToHTMLConv.js394
-rw-r--r--netwerk/test/unit/test_multipart_byteranges.js143
-rw-r--r--netwerk/test/unit/test_multipart_streamconv-byte-by-byte.js115
-rw-r--r--netwerk/test/unit/test_multipart_streamconv.js100
-rw-r--r--netwerk/test/unit/test_multipart_streamconv_empty.js68
-rw-r--r--netwerk/test/unit/test_multipart_streamconv_missing_boundary_lead_dashes.js92
-rw-r--r--netwerk/test/unit/test_multipart_streamconv_missing_lead_boundary.js92
-rw-r--r--netwerk/test/unit/test_nestedabout_serialize.js39
-rw-r--r--netwerk/test/unit/test_net_addr.js216
-rw-r--r--netwerk/test/unit/test_network_connectivity_service.js215
-rw-r--r--netwerk/test/unit/test_networking_over_socket_process.js168
-rw-r--r--netwerk/test/unit/test_no_cookies_after_last_pb_exit.js135
-rw-r--r--netwerk/test/unit/test_node_execute.js87
-rw-r--r--netwerk/test/unit/test_nojsredir.js67
-rw-r--r--netwerk/test/unit/test_non_ipv4_hostname_ending_in_number_cookie_db.js128
-rw-r--r--netwerk/test/unit/test_nsIBufferedOutputStream_writeFrom_block.js193
-rw-r--r--netwerk/test/unit/test_ntlm_authentication.js268
-rw-r--r--netwerk/test/unit/test_ntlm_proxy_and_web_auth.js362
-rw-r--r--netwerk/test/unit/test_ntlm_proxy_auth.js408
-rw-r--r--netwerk/test/unit/test_ntlm_web_auth.js251
-rw-r--r--netwerk/test/unit/test_oblivious_http.js206
-rw-r--r--netwerk/test/unit/test_obs-fold.js75
-rw-r--r--netwerk/test/unit/test_offline_status.js15
-rw-r--r--netwerk/test/unit/test_ohttp.js41
-rw-r--r--netwerk/test/unit/test_orb_empty_header.js83
-rw-r--r--netwerk/test/unit/test_origin.js323
-rw-r--r--netwerk/test/unit/test_original_sent_received_head.js249
-rw-r--r--netwerk/test/unit/test_pac_reload_after_network_change.js75
-rw-r--r--netwerk/test/unit/test_parse_content_type.js365
-rw-r--r--netwerk/test/unit/test_partial_response_entry_size_smart_shrink.js106
-rw-r--r--netwerk/test/unit/test_permmgr.js125
-rw-r--r--netwerk/test/unit/test_ping_aboutnetworking.js103
-rw-r--r--netwerk/test/unit/test_plaintext_sniff.js211
-rw-r--r--netwerk/test/unit/test_port_remapping.js50
-rw-r--r--netwerk/test/unit/test_post.js141
-rw-r--r--netwerk/test/unit/test_predictor.js852
-rw-r--r--netwerk/test/unit/test_private_cookie_changed.js44
-rw-r--r--netwerk/test/unit/test_private_necko_channel.js60
-rw-r--r--netwerk/test/unit/test_progress.js145
-rw-r--r--netwerk/test/unit/test_progress_no_proxy_and_proxy.js205
-rw-r--r--netwerk/test/unit/test_protocolproxyservice-async-filters.js435
-rw-r--r--netwerk/test/unit/test_protocolproxyservice.js1071
-rw-r--r--netwerk/test/unit/test_proxy-failover_canceled.js57
-rw-r--r--netwerk/test/unit/test_proxy-failover_passing.js45
-rw-r--r--netwerk/test/unit/test_proxy-replace_canceled.js57
-rw-r--r--netwerk/test/unit/test_proxy-replace_passing.js45
-rw-r--r--netwerk/test/unit/test_proxy-slow-upload.js105
-rw-r--r--netwerk/test/unit/test_proxy_cancel.js397
-rw-r--r--netwerk/test/unit/test_proxy_pac.js126
-rw-r--r--netwerk/test/unit/test_proxyconnect.js360
-rw-r--r--netwerk/test/unit/test_psl.js39
-rw-r--r--netwerk/test/unit/test_race_cache_with_network.js273
-rw-r--r--netwerk/test/unit/test_range_requests.js443
-rw-r--r--netwerk/test/unit/test_rcwn_always_cache_new_content.js116
-rw-r--r--netwerk/test/unit/test_rcwn_interrupted.js108
-rw-r--r--netwerk/test/unit/test_readline.js104
-rw-r--r--netwerk/test/unit/test_redirect-caching_canceled.js64
-rw-r--r--netwerk/test/unit/test_redirect-caching_failure.js81
-rw-r--r--netwerk/test/unit/test_redirect-caching_passing.js56
-rw-r--r--netwerk/test/unit/test_redirect_baduri.js44
-rw-r--r--netwerk/test/unit/test_redirect_canceled.js50
-rw-r--r--netwerk/test/unit/test_redirect_different-protocol.js49
-rw-r--r--netwerk/test/unit/test_redirect_failure.js59
-rw-r--r--netwerk/test/unit/test_redirect_from_script.js247
-rw-r--r--netwerk/test/unit/test_redirect_from_script_after-open_passing.js247
-rw-r--r--netwerk/test/unit/test_redirect_history.js75
-rw-r--r--netwerk/test/unit/test_redirect_loop.js88
-rw-r--r--netwerk/test/unit/test_redirect_passing.js55
-rw-r--r--netwerk/test/unit/test_redirect_protocol_telemetry.js65
-rw-r--r--netwerk/test/unit/test_redirect_veto.js102
-rw-r--r--netwerk/test/unit/test_reentrancy.js109
-rw-r--r--netwerk/test/unit/test_referrer.js248
-rw-r--r--netwerk/test/unit/test_referrer_cross_origin.js332
-rw-r--r--netwerk/test/unit/test_referrer_policy.js154
-rw-r--r--netwerk/test/unit/test_reopen.js136
-rw-r--r--netwerk/test/unit/test_reply_without_content_type.js151
-rw-r--r--netwerk/test/unit/test_resumable_channel.js426
-rw-r--r--netwerk/test/unit/test_resumable_truncate.js95
-rw-r--r--netwerk/test/unit/test_retry_0rtt.js123
-rw-r--r--netwerk/test/unit/test_safeoutputstream.js70
-rw-r--r--netwerk/test/unit/test_safeoutputstream_append.js45
-rw-r--r--netwerk/test/unit/test_schema_10_migration.js181
-rw-r--r--netwerk/test/unit/test_schema_12_migration.js181
-rw-r--r--netwerk/test/unit/test_schema_13_db.js87
-rw-r--r--netwerk/test/unit/test_schema_2_migration.js303
-rw-r--r--netwerk/test/unit/test_schema_3_migration.js170
-rw-r--r--netwerk/test/unit/test_separate_connections.js104
-rw-r--r--netwerk/test/unit/test_servers.js324
-rw-r--r--netwerk/test/unit/test_signature_extraction.js203
-rw-r--r--netwerk/test/unit/test_simple.js70
-rw-r--r--netwerk/test/unit/test_sockettransportsvc_available.js11
-rw-r--r--netwerk/test/unit/test_socks.js520
-rw-r--r--netwerk/test/unit/test_speculative_connect.js382
-rw-r--r--netwerk/test/unit/test_stale-while-revalidate_loop.js43
-rw-r--r--netwerk/test/unit/test_stale-while-revalidate_max-age-0.js113
-rw-r--r--netwerk/test/unit/test_stale-while-revalidate_negative.js92
-rw-r--r--netwerk/test/unit/test_stale-while-revalidate_positive.js113
-rw-r--r--netwerk/test/unit/test_standardurl.js1053
-rw-r--r--netwerk/test/unit/test_standardurl_default_port.js58
-rw-r--r--netwerk/test/unit/test_standardurl_port.js53
-rw-r--r--netwerk/test/unit/test_streamcopier.js63
-rw-r--r--netwerk/test/unit/test_substituting_protocol_handler.js64
-rw-r--r--netwerk/test/unit/test_suspend_channel_before_connect.js93
-rw-r--r--netwerk/test/unit/test_suspend_channel_on_authRetry.js264
-rw-r--r--netwerk/test/unit/test_suspend_channel_on_examine.js76
-rw-r--r--netwerk/test/unit/test_suspend_channel_on_examine_merged_response.js208
-rw-r--r--netwerk/test/unit/test_suspend_channel_on_modified.js177
-rw-r--r--netwerk/test/unit/test_synthesized_response.js288
-rw-r--r--netwerk/test/unit/test_throttlechannel.js48
-rw-r--r--netwerk/test/unit/test_throttlequeue.js25
-rw-r--r--netwerk/test/unit/test_throttling.js66
-rw-r--r--netwerk/test/unit/test_tldservice_nextsubdomain.js24
-rw-r--r--netwerk/test/unit/test_tls13_disabled.js93
-rw-r--r--netwerk/test/unit/test_tls_flags.js248
-rw-r--r--netwerk/test/unit/test_tls_flags_separate_connections.js117
-rw-r--r--netwerk/test/unit/test_tls_server.js314
-rw-r--r--netwerk/test/unit/test_tls_server_multiple_clients.js130
-rw-r--r--netwerk/test/unit/test_traceable_channel.js145
-rw-r--r--netwerk/test/unit/test_trackingProtection_annotateChannels.js391
-rw-r--r--netwerk/test/unit/test_trr.js946
-rw-r--r--netwerk/test/unit/test_trr_additional_section.js380
-rw-r--r--netwerk/test/unit/test_trr_af_fallback.js117
-rw-r--r--netwerk/test/unit/test_trr_blocklist.js78
-rw-r--r--netwerk/test/unit/test_trr_cancel.js180
-rw-r--r--netwerk/test/unit/test_trr_case_sensitivity.js153
-rw-r--r--netwerk/test/unit/test_trr_cname_chain.js231
-rw-r--r--netwerk/test/unit/test_trr_confirmation.js401
-rw-r--r--netwerk/test/unit/test_trr_decoding.js56
-rw-r--r--netwerk/test/unit/test_trr_domain.js123
-rw-r--r--netwerk/test/unit/test_trr_enterprise_policy.js93
-rw-r--r--netwerk/test/unit/test_trr_extended_error.js319
-rw-r--r--netwerk/test/unit/test_trr_https_fallback.js1105
-rw-r--r--netwerk/test/unit/test_trr_httpssvc.js728
-rw-r--r--netwerk/test/unit/test_trr_nat64.js120
-rw-r--r--netwerk/test/unit/test_trr_noPrefetch.js174
-rw-r--r--netwerk/test/unit/test_trr_proxy.js156
-rw-r--r--netwerk/test/unit/test_trr_proxy_auth.js122
-rw-r--r--netwerk/test/unit/test_trr_strict_mode.js52
-rw-r--r--netwerk/test/unit/test_trr_telemetry.js118
-rw-r--r--netwerk/test/unit/test_trr_ttl.js60
-rw-r--r--netwerk/test/unit/test_trr_with_proxy.js210
-rw-r--r--netwerk/test/unit/test_udp_multicast.js99
-rw-r--r--netwerk/test/unit/test_udpsocket.js89
-rw-r--r--netwerk/test/unit/test_udpsocket_offline.js144
-rw-r--r--netwerk/test/unit/test_unescapestring.js35
-rw-r--r--netwerk/test/unit/test_unix_domain.js699
-rw-r--r--netwerk/test/unit/test_uri_mutator.js48
-rw-r--r--netwerk/test/unit/test_use_httpssvc.js240
-rw-r--r--netwerk/test/unit/test_verify_traffic.js110
-rw-r--r--netwerk/test/unit/test_websocket_500k.js222
-rw-r--r--netwerk/test/unit/test_websocket_fails.js194
-rw-r--r--netwerk/test/unit/test_websocket_fails_2.js57
-rw-r--r--netwerk/test/unit/test_websocket_offline.js51
-rw-r--r--netwerk/test/unit/test_websocket_server.js317
-rw-r--r--netwerk/test/unit/test_websocket_server_multiclient.js141
-rw-r--r--netwerk/test/unit/test_websocket_with_h3_active.js97
-rw-r--r--netwerk/test/unit/test_webtransport_simple.js460
-rw-r--r--netwerk/test/unit/test_xmlhttprequest.js57
-rw-r--r--netwerk/test/unit/trr_common.js1235
-rw-r--r--netwerk/test/unit/xpcshell.toml1270
503 files changed, 86349 insertions, 0 deletions
diff --git a/netwerk/test/unit/client-cert.p12 b/netwerk/test/unit/client-cert.p12
new file mode 100644
index 0000000000..0f0fd43eba
--- /dev/null
+++ b/netwerk/test/unit/client-cert.p12
Binary files differ
diff --git a/netwerk/test/unit/client-cert.p12.pkcs12spec b/netwerk/test/unit/client-cert.p12.pkcs12spec
new file mode 100644
index 0000000000..548c1a6aa6
--- /dev/null
+++ b/netwerk/test/unit/client-cert.p12.pkcs12spec
@@ -0,0 +1,3 @@
+issuer:Test CA
+subject:Test End-entity
+extension:subjectAlternativeName:example.com
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
--- /dev/null
+++ b/netwerk/test/unit/data/cookies_v10.sqlite
Binary files differ
diff --git a/netwerk/test/unit/data/image.png b/netwerk/test/unit/data/image.png
new file mode 100644
index 0000000000..e0c5d3d6a1
--- /dev/null
+++ b/netwerk/test/unit/data/image.png
Binary files 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
--- /dev/null
+++ b/netwerk/test/unit/data/signed_win.exe
Binary files 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
--- /dev/null
+++ b/netwerk/test/unit/data/system_root.lnk
Binary files 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
--- /dev/null
+++ b/netwerk/test/unit/data/test_readline1.txt
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..7ec0e11f97
--- /dev/null
+++ b/netwerk/test/unit/head_cache.js
@@ -0,0 +1,137 @@
+"use strict";
+
+var { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+function evict_cache_entries(where) {
+ var clearDisk = !where || where == "disk" || where == "all";
+ var clearMem = !where || where == "memory" || where == "all";
+
+ var storage;
+
+ if (clearMem) {
+ storage = Services.cache2.memoryCacheStorage(
+ Services.loadContextInfo.default
+ );
+ storage.asyncEvictStorage(null);
+ }
+
+ if (clearDisk) {
+ storage = Services.cache2.diskCacheStorage(
+ Services.loadContextInfo.default
+ );
+ storage.asyncEvictStorage(null);
+ }
+}
+
+function createURI(urispec) {
+ return Services.io.newURI(urispec);
+}
+
+function getCacheStorage(where, lci) {
+ if (!lci) {
+ lci = Services.loadContextInfo.default;
+ }
+ switch (where) {
+ case "disk":
+ return Services.cache2.diskCacheStorage(lci);
+ case "memory":
+ return Services.cache2.memoryCacheStorage(lci);
+ case "pin":
+ return Services.cache2.pinningCacheStorage(lci);
+ }
+ return null;
+}
+
+function asyncOpenCacheEntry(key, where, flags, lci, callback) {
+ key = createURI(key);
+
+ function CacheListener() {}
+ CacheListener.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["nsICacheEntryOpenCallback"]),
+
+ onCacheEntryCheck(entry) {
+ if (typeof callback === "object") {
+ return callback.onCacheEntryCheck(entry);
+ }
+ return Ci.nsICacheEntryOpenCallback.ENTRY_WANTED;
+ },
+
+ onCacheEntryAvailable(entry, isnew, status) {
+ if (typeof callback === "object") {
+ // Root us at the callback
+ callback.__cache_listener_root = this;
+ callback.onCacheEntryAvailable(entry, isnew, status);
+ } else {
+ callback(status, entry);
+ }
+ },
+
+ run() {
+ var storage = getCacheStorage(where, lci);
+ 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) {
+ 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();
+ }
+ );
+}
diff --git a/netwerk/test/unit/head_cache2.js b/netwerk/test/unit/head_cache2.js
new file mode 100644
index 0000000000..f7c865872a
--- /dev/null
+++ b/netwerk/test/unit/head_cache2.js
@@ -0,0 +1,429 @@
+/* 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);
+ let 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
+ let data = read_stream(inputStream, inputStream.available());
+ goon(data);
+ }
+}
+
+OpenCallback.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["nsICacheEntryOpenCallback"]),
+ onCacheEntryCheck(entry) {
+ 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, 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;
+ }
+
+ let 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 {
+ let 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);
+ let 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);
+ }
+
+ let 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,
+ aAltDataSize,
+ 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.
+ Services.cache2.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..140135339a
--- /dev/null
+++ b/netwerk/test/unit/head_channels.js
@@ -0,0 +1,527 @@
+/**
+ * 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) {
+ 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 CL_IGNORE_DELAYS = 0x200; // don't throw if channel returns after a long delay
+
+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 (
+ !(this._flags & CL_IGNORE_DELAYS) &&
+ 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);
+}
+
+async function asyncStartTLSTestServer(
+ serverBinName,
+ certsPath,
+ addDefaultRoot = true
+) {
+ const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+ );
+ 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 greBinDir = Services.dirsvc.get("GreBinD", Ci.nsIFile);
+ Services.env.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.
+ Services.env.set("LD_LIBRARY_PATH", greBinDir.path + ":/data/local/xpcb");
+ Services.env.set("MOZ_TLS_SERVER_DEBUG_LEVEL", "3");
+ Services.env.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;
+}
+
+function promiseAsyncOpen(chan) {
+ return new Promise(resolve => {
+ chan.asyncOpen(
+ new ChannelListener((req, buf, ctx, isCache, cacheId) => {
+ resolve({ req, buf, ctx, isCache, cacheId });
+ })
+ );
+ });
+}
+
+function hexStringToBytes(hex) {
+ let bytes = [];
+ for (let hexByteStr of hex.split(/(..)/)) {
+ if (hexByteStr.length) {
+ bytes.push(parseInt(hexByteStr, 16));
+ }
+ }
+ return bytes;
+}
+
+function stringToBytes(str) {
+ return Array.from(str, chr => chr.charCodeAt(0));
+}
+
+function BinaryHttpResponse(status, headerNames, headerValues, content) {
+ this.status = status;
+ this.headerNames = headerNames;
+ this.headerValues = headerValues;
+ this.content = content;
+}
+
+BinaryHttpResponse.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["nsIBinaryHttpResponse"]),
+};
+
+function bytesToString(bytes) {
+ return String.fromCharCode.apply(null, bytes);
+}
+
+function check_http_info(request, expected_httpVersion, expected_proxy) {
+ let httpVersion = "";
+ try {
+ httpVersion = request.protocolVersion;
+ } catch (e) {}
+
+ request.QueryInterface(Ci.nsIProxiedChannel);
+ var httpProxyConnectResponseCode = request.httpProxyConnectResponseCode;
+
+ Assert.equal(expected_httpVersion, httpVersion);
+ if (expected_proxy) {
+ Assert.equal(httpProxyConnectResponseCode, 200);
+ } else {
+ Assert.equal(httpProxyConnectResponseCode, -1);
+ }
+}
+
+function makeHTTPChannel(url, with_proxy) {
+ function createPrincipal(uri) {
+ var ssm = Services.scriptSecurityManager;
+ try {
+ return ssm.createContentPrincipal(Services.io.newURI(uri), {});
+ } catch (e) {
+ return null;
+ }
+ }
+
+ if (with_proxy) {
+ return Services.io
+ .newChannelFromURIWithProxyFlags(
+ Services.io.newURI(url),
+ null,
+ Ci.nsIProtocolProxyService.RESOLVE_ALWAYS_TUNNEL,
+ null,
+ createPrincipal(url),
+ createPrincipal(url),
+ Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_INHERITS_SEC_CONTEXT,
+ Ci.nsIContentPolicy.TYPE_OTHER
+ )
+ .QueryInterface(Ci.nsIHttpChannel);
+ }
+ return NetUtil.newChannel({
+ uri: url,
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+}
diff --git a/netwerk/test/unit/head_cookies.js b/netwerk/test/unit/head_cookies.js
new file mode 100644
index 0000000000..037068689f
--- /dev/null
+++ b/netwerk/test/unit/head_cookies.js
@@ -0,0 +1,1062 @@
+/* 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.importESModule(
+ "resource://gre/modules/NetUtil.sys.mjs"
+);
+const { CookieXPCShellUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/CookieXPCShellUtils.sys.mjs"
+);
+
+// Don't pick up default permissions from profile.
+Services.prefs.setCharPref("permissions.manager.defaultsUrl", "");
+
+CookieXPCShellUtils.init(this);
+
+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", null);
+}
+
+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", null);
+
+ 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.cookies.setCookieStringFromHttp(uri, "foo=bar", channel);
+ Assert.equal(Services.cookies.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 function (obj) {
+ await new this.content.Promise(resolve => {
+ let doc = this.content.document;
+ let ifr = doc.createElement("iframe");
+ ifr.src = obj.url;
+ doc.body.appendChild(ifr);
+ ifr.addEventListener("load", async () => {
+ await this.SpecialPowers.spawn(ifr, [obj.cookie], cookie => {
+ this.content.document.cookie = cookie;
+ });
+ resolve();
+ });
+ });
+ }
+ );
+ await contentPage.close();
+
+ Assert.equal(Services.cookies.countCookiesFromHost(uri.host), expected[0]);
+
+ // via http request
+ Services.cookies.setCookieStringFromHttp(uri, "hot=dog" + suffix, channel);
+ Assert.equal(Services.cookies.countCookiesFromHost(uri.host), expected[1]);
+}
+
+function do_count_cookies() {
+ return Services.cookies.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,
+ isPartitioned = false
+) {
+ 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;
+ this.isPartitioned = isPartitioned;
+
+ 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;
+ }
+
+ case 13: {
+ 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, \
+ isPartitionedAttributeSet 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, \
+ isPartitionedAttributeSet \
+ ) VALUES ( \
+ :name, \
+ :value, \
+ :host, \
+ :path, \
+ :expiry, \
+ :lastAccessed, \
+ :creationTime, \
+ :isSecure, \
+ :isHttpOnly, \
+ :inBrowserElement, \
+ :originAttributes, \
+ :sameSite, \
+ :rawSameSite, \
+ :schemeMap, \
+ :isPartitionedAttributeSet)"
+ );
+
+ 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;
+
+ case 13:
+ 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);
+ this.stmtInsert.bindByName(
+ "isPartitionedAttributeSet",
+ cookie.isPartitioned
+ );
+ 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:
+ case 13:
+ 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:
+ case 13:
+ 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..56bc04db14
--- /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(http3version) {
+ let h3Port = Services.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.enable", 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;${http3version}=:${h3Port}`
+ );
+
+ let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(
+ Ci.nsIX509CertDB
+ );
+ // `../unit/` so that unit_ipc tests can use as well
+ addCertFromFile(certdb, "../unit/http2-ca.pem", "CTu,u,u");
+
+ await setup_altsvc("https://foo.example.com/", h3Route, http3version);
+}
+
+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: "",
+ http3version: "",
+
+ 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, this.http3version);
+ this.finish(true);
+ } else {
+ dump("try again to get alt svc mapping\n");
+ this.finish(false);
+ }
+ },
+};
+
+async function setup_altsvc(uri, expectedRoute, http3version) {
+ let result = false;
+ do {
+ let chan = makeChan(uri);
+ let listener = new CheckHttp3Listener();
+ listener.expectedRoute = expectedRoute;
+ listener.http3version = http3version;
+ 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.enable");
+ 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.support_version1");
+ dump("cleanup done\n");
+}
diff --git a/netwerk/test/unit/head_servers.js b/netwerk/test/unit/head_servers.js
new file mode 100644
index 0000000000..d2d449b482
--- /dev/null
+++ b/netwerk/test/unit/head_servers.js
@@ -0,0 +1,925 @@
+/* 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 */
+
+const { NodeServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+
+/* globals require, __dirname, global, Buffer, process */
+
+class BaseNodeHTTPServerCode {
+ static globalHandler(req, resp) {
+ let path = new URL(req.url, "http://example.com").pathname;
+ let handler = global.path_handlers[path];
+ if (handler) {
+ return handler(req, resp);
+ }
+
+ // Didn't find a handler for this path.
+ let response = `<h1> 404 Path not found: ${path}</h1>`;
+ resp.setHeader("Content-Type", "text/html");
+ resp.setHeader("Content-Length", response.length);
+ resp.writeHead(404);
+ resp.end(response);
+ return undefined;
+ }
+}
+
+class ADB {
+ static async stopForwarding(port) {
+ return this.forwardPort(port, true);
+ }
+
+ static async forwardPort(port, remove = false) {
+ if (!process.env.MOZ_ANDROID_DATA_DIR) {
+ // Not android, or we don't know how to do the forwarding
+ return true;
+ }
+ // When creating a server on Android we must make sure that the port
+ // is forwarded from the host machine to the emulator.
+ let adb_path = "adb";
+ if (process.env.MOZ_FETCHES_DIR) {
+ adb_path = `${process.env.MOZ_FETCHES_DIR}/android-sdk-linux/platform-tools/adb`;
+ }
+
+ let command = `${adb_path} reverse tcp:${port} tcp:${port}`;
+ if (remove) {
+ command = `${adb_path} reverse --remove tcp:${port}`;
+ return true;
+ }
+
+ try {
+ await new Promise((resolve, reject) => {
+ const { exec } = require("child_process");
+ exec(command, (error, stdout, stderr) => {
+ if (error) {
+ console.log(`error: ${error.message}`);
+ reject(error);
+ } else if (stderr) {
+ console.log(`stderr: ${stderr}`);
+ reject(stderr);
+ } else {
+ // console.log(`stdout: ${stdout}`);
+ resolve();
+ }
+ });
+ });
+ } catch (error) {
+ console.log(`Command failed: ${error}`);
+ return false;
+ }
+
+ return true;
+ }
+
+ static async listenAndForwardPort(server, port) {
+ let retryCount = 0;
+ const maxRetries = 10;
+
+ while (retryCount < maxRetries) {
+ await server.listen(port);
+ let serverPort = server.address().port;
+ let res = await ADB.forwardPort(serverPort);
+
+ if (res) {
+ return serverPort;
+ }
+
+ retryCount++;
+ console.log(
+ `Port forwarding failed. Retrying (${retryCount}/${maxRetries})...`
+ );
+ server.close();
+ // eslint-disable-next-line no-undef
+ await new Promise(resolve => setTimeout(resolve, 500));
+ }
+
+ return -1;
+ }
+}
+
+class BaseNodeServer {
+ protocol() {
+ return this._protocol;
+ }
+ version() {
+ return this._version;
+ }
+ origin() {
+ return `${this.protocol()}://localhost:${this.port()}`;
+ }
+ port() {
+ return this._port;
+ }
+ domain() {
+ return `localhost`;
+ }
+
+ /// Stops the server
+ async stop() {
+ if (this.processId) {
+ await this.execute(`ADB.stopForwarding(${this.port()})`);
+ await NodeServer.kill(this.processId);
+ this.processId = undefined;
+ }
+ }
+
+ /// Executes a command in the context of the node server
+ async execute(command) {
+ return NodeServer.execute(this.processId, command);
+ }
+
+ /// @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()}`
+ );
+ }
+}
+
+// HTTP
+
+class NodeHTTPServerCode extends BaseNodeHTTPServerCode {
+ static async startServer(port) {
+ const http = require("http");
+ global.server = http.createServer(BaseNodeHTTPServerCode.globalHandler);
+
+ let serverPort = await ADB.listenAndForwardPort(global.server, port);
+ return serverPort;
+ }
+}
+
+class NodeHTTPServer extends BaseNodeServer {
+ _protocol = "http";
+ _version = "http/1.1";
+ /// 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(BaseNodeHTTPServerCode);
+ await this.execute(NodeHTTPServerCode);
+ await this.execute(ADB);
+ this._port = await this.execute(`NodeHTTPServerCode.startServer(${port})`);
+ await this.execute(`global.path_handlers = {};`);
+ }
+}
+
+// HTTPS
+
+class NodeHTTPSServerCode extends BaseNodeHTTPServerCode {
+ 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 https = require("https");
+ global.server = https.createServer(
+ options,
+ BaseNodeHTTPServerCode.globalHandler
+ );
+
+ let serverPort = await ADB.listenAndForwardPort(global.server, port);
+ return serverPort;
+ }
+}
+
+class NodeHTTPSServer extends BaseNodeServer {
+ _protocol = "https";
+ _version = "http/1.1";
+ /// 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(BaseNodeHTTPServerCode);
+ await this.execute(NodeHTTPSServerCode);
+ await this.execute(ADB);
+ this._port = await this.execute(`NodeHTTPSServerCode.startServer(${port})`);
+ await this.execute(`global.path_handlers = {};`);
+ }
+}
+
+// HTTP2
+
+class NodeHTTP2ServerCode extends BaseNodeHTTPServerCode {
+ 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 http2 = require("http2");
+ global.server = http2.createSecureServer(
+ options,
+ BaseNodeHTTPServerCode.globalHandler
+ );
+
+ global.sessionCount = 0;
+ global.server.on("session", () => {
+ global.sessionCount++;
+ });
+
+ let serverPort = await ADB.listenAndForwardPort(global.server, port);
+ return serverPort;
+ }
+
+ static sessionCount() {
+ return global.sessionCount;
+ }
+}
+
+class NodeHTTP2Server extends BaseNodeServer {
+ _protocol = "https";
+ _version = "h2";
+ /// 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(BaseNodeHTTPServerCode);
+ await this.execute(NodeHTTP2ServerCode);
+ await this.execute(ADB);
+ this._port = await this.execute(`NodeHTTP2ServerCode.startServer(${port})`);
+ await this.execute(`global.path_handlers = {};`);
+ }
+
+ async sessionCount() {
+ let count = this.execute(`NodeHTTP2ServerCode.sessionCount()`);
+ return count;
+ }
+}
+
+// Base HTTP proxy
+
+class BaseProxyCode {
+ static proxyHandler(req, res) {
+ if (req.url.startsWith("/")) {
+ res.writeHead(405);
+ res.end();
+ return;
+ }
+
+ let url = new URL(req.url);
+ const http = require("http");
+ let preq = http
+ .request(
+ {
+ method: req.method,
+ path: url.pathname,
+ port: url.port,
+ host: url.hostname,
+ protocol: url.protocol,
+ },
+ proxyresp => {
+ res.writeHead(
+ proxyresp.statusCode,
+ proxyresp.statusMessage,
+ proxyresp.headers
+ );
+ proxyresp.on("data", chunk => {
+ if (!res.writableEnded) {
+ res.write(chunk);
+ }
+ });
+ proxyresp.on("end", () => {
+ res.end();
+ });
+ }
+ )
+ .on("error", e => {
+ console.log(`sock err: ${e}`);
+ });
+ if (req.method != "POST") {
+ preq.end();
+ } else {
+ req.on("data", chunk => {
+ if (!preq.writableEnded) {
+ preq.write(chunk);
+ }
+ });
+ req.on("end", () => preq.end());
+ }
+ }
+
+ static onConnect(req, clientSocket, head) {
+ if (global.connect_handler) {
+ global.connect_handler(req, clientSocket, head);
+ return;
+ }
+ const net = require("net");
+ // Connect to an origin server
+ const { port, hostname } = new URL(`https://${req.url}`);
+ const serverSocket = net
+ .connect(port || 443, hostname, () => {
+ clientSocket.write(
+ "HTTP/1.1 200 Connection Established\r\n" +
+ "Proxy-agent: Node.js-Proxy\r\n" +
+ "\r\n"
+ );
+ serverSocket.write(head);
+ serverSocket.pipe(clientSocket);
+ clientSocket.pipe(serverSocket);
+ })
+ .on("error", e => {
+ // The socket will error out when we kill the connection
+ // just ignore it.
+ });
+ clientSocket.on("error", e => {
+ // Sometimes we got ECONNRESET error on windows platform.
+ // Ignore it for now.
+ });
+ }
+}
+
+class BaseHTTPProxy extends BaseNodeServer {
+ registerFilter() {
+ const pps =
+ Cc["@mozilla.org/network/protocol-proxy-service;1"].getService();
+ this.filter = new NodeProxyFilter(
+ this.protocol(),
+ "localhost",
+ this.port(),
+ 0
+ );
+ pps.registerFilter(this.filter, 10);
+ registerCleanupFunction(() => {
+ this.unregisterFilter();
+ });
+ }
+
+ unregisterFilter() {
+ const pps =
+ Cc["@mozilla.org/network/protocol-proxy-service;1"].getService();
+ if (this.filter) {
+ pps.unregisterFilter(this.filter);
+ this.filter = undefined;
+ }
+ }
+
+ /// Stops the server
+ async stop() {
+ this.unregisterFilter();
+ await super.stop();
+ }
+
+ async registerConnectHandler(handler) {
+ return this.execute(`global.connect_handler = ${handler.toString()}`);
+ }
+}
+
+// HTTP1 Proxy
+
+class NodeProxyFilter {
+ 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) {
+ const pps =
+ Cc["@mozilla.org/network/protocol-proxy-service;1"].getService();
+ cb.onProxyFilterResult(
+ pps.newProxyInfo(
+ this._type,
+ this._host,
+ this._port,
+ "",
+ "",
+ this._flags,
+ 1000,
+ null
+ )
+ );
+ }
+}
+
+class HTTPProxyCode {
+ static async startServer(port) {
+ const http = require("http");
+ global.proxy = http.createServer(BaseProxyCode.proxyHandler);
+ global.proxy.on("connect", BaseProxyCode.onConnect);
+
+ let proxyPort = await ADB.listenAndForwardPort(global.proxy, port);
+ return proxyPort;
+ }
+}
+
+class NodeHTTPProxyServer extends BaseHTTPProxy {
+ _protocol = "http";
+ /// 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(BaseProxyCode);
+ await this.execute(HTTPProxyCode);
+ await this.execute(ADB);
+ await this.execute(`global.connect_handler = null;`);
+ this._port = await this.execute(`HTTPProxyCode.startServer(${port})`);
+
+ this.registerFilter();
+ }
+}
+
+// HTTPS proxy
+
+class HTTPSProxyCode {
+ static async startServer(port) {
+ const fs = require("fs");
+ const options = {
+ key: fs.readFileSync(__dirname + "/proxy-cert.key"),
+ cert: fs.readFileSync(__dirname + "/proxy-cert.pem"),
+ };
+ const https = require("https");
+ global.proxy = https.createServer(options, BaseProxyCode.proxyHandler);
+ global.proxy.on("connect", BaseProxyCode.onConnect);
+
+ let proxyPort = await ADB.listenAndForwardPort(global.proxy, port);
+ return proxyPort;
+ }
+}
+
+class NodeHTTPSProxyServer extends BaseHTTPProxy {
+ _protocol = "https";
+ /// 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(BaseProxyCode);
+ await this.execute(HTTPSProxyCode);
+ await this.execute(ADB);
+ await this.execute(`global.connect_handler = null;`);
+ this._port = await this.execute(`HTTPSProxyCode.startServer(${port})`);
+
+ this.registerFilter();
+ }
+}
+
+// HTTP2 proxy
+
+class HTTP2ProxyCode {
+ static async startServer(port, auth) {
+ const fs = require("fs");
+ const options = {
+ key: fs.readFileSync(__dirname + "/proxy-cert.key"),
+ cert: fs.readFileSync(__dirname + "/proxy-cert.pem"),
+ };
+ const http2 = require("http2");
+ global.proxy = http2.createSecureServer(options);
+ global.socketCounts = {};
+ this.setupProxy(auth);
+
+ let proxyPort = await ADB.listenAndForwardPort(global.proxy, port);
+ return proxyPort;
+ }
+
+ static setupProxy(auth) {
+ if (!global.proxy) {
+ throw new Error("proxy is null");
+ }
+
+ global.proxy.on("stream", (stream, headers) => {
+ if (headers[":scheme"] === "http") {
+ const http = require("http");
+ let url = new URL(
+ `${headers[":scheme"]}://${headers[":authority"]}${headers[":path"]}`
+ );
+ let req = http
+ .request(
+ {
+ method: headers[":method"],
+ path: headers[":path"],
+ port: url.port,
+ host: url.hostname,
+ protocol: url.protocol,
+ },
+ proxyresp => {
+ let proxyheaders = Object.assign({}, proxyresp.headers);
+ // Filter out some prohibited headers.
+ ["connection", "transfer-encoding", "keep-alive"].forEach(
+ prop => {
+ delete proxyheaders[prop];
+ }
+ );
+ try {
+ stream.respond(
+ Object.assign(
+ { ":status": proxyresp.statusCode },
+ proxyheaders
+ )
+ );
+ } catch (e) {
+ // The channel may have been closed already.
+ if (e.message != "The stream has been destroyed") {
+ throw e;
+ }
+ }
+ proxyresp.on("data", chunk => {
+ if (stream.writable) {
+ stream.write(chunk);
+ }
+ });
+ proxyresp.on("end", () => {
+ stream.end();
+ });
+ }
+ )
+ .on("error", e => {
+ console.log(`sock err: ${e}`);
+ });
+
+ if (headers[":method"] != "POST") {
+ req.end();
+ } else {
+ stream.on("data", chunk => {
+ if (!req.writableEnded) {
+ req.write(chunk);
+ }
+ });
+ stream.on("end", () => req.end());
+ }
+ return;
+ }
+ if (headers[":method"] !== "CONNECT") {
+ // Only accept CONNECT requests
+ stream.respond({ ":status": 405 });
+ stream.end();
+ return;
+ }
+
+ const authorization_token = headers["proxy-authorization"];
+ if (auth && !authorization_token) {
+ stream.respond({
+ ":status": 407,
+ "proxy-authenticate": "Basic realm='foo'",
+ });
+ stream.end();
+ return;
+ }
+
+ const target = headers[":authority"];
+ const { port } = new URL(`https://${target}`);
+ const net = require("net");
+ const socket = net.connect(port, "127.0.0.1", () => {
+ try {
+ global.socketCounts[socket.remotePort] =
+ (global.socketCounts[socket.remotePort] || 0) + 1;
+ stream.respond({ ":status": 200 });
+ socket.pipe(stream);
+ stream.pipe(socket);
+ } catch (exception) {
+ console.log(exception);
+ stream.close();
+ }
+ });
+ const http2 = require("http2");
+ socket.on("error", error => {
+ const status = error.errno == "ENOTFOUND" ? 404 : 502;
+ try {
+ // If we already sent headers when the socket connected
+ // then sending the status again would throw.
+ if (!stream.sentHeaders) {
+ stream.respond({ ":status": status });
+ }
+ stream.end();
+ } catch (exception) {
+ stream.close(http2.constants.NGHTTP2_CONNECT_ERROR);
+ }
+ });
+ stream.on("close", () => {
+ socket.end();
+ });
+ socket.on("close", () => {
+ stream.close();
+ });
+ stream.on("end", () => {
+ socket.end();
+ });
+ stream.on("aborted", () => {
+ socket.end();
+ });
+ stream.on("error", error => {
+ console.log("RESPONSE STREAM ERROR", error);
+ });
+ });
+ }
+
+ static socketCount(port) {
+ return global.socketCounts[port];
+ }
+}
+
+class NodeHTTP2ProxyServer extends BaseHTTPProxy {
+ _protocol = "https";
+ /// Starts the server
+ /// @port - default 0
+ /// when provided, will attempt to listen on that port.
+ async start(port = 0, auth) {
+ this.processId = await NodeServer.fork();
+
+ await this.execute(BaseProxyCode);
+ await this.execute(HTTP2ProxyCode);
+ await this.execute(ADB);
+ await this.execute(`global.connect_handler = null;`);
+ this._port = await this.execute(
+ `HTTP2ProxyCode.startServer(${port}, ${auth})`
+ );
+
+ this.registerFilter();
+ }
+
+ async socketCount(port) {
+ let count = this.execute(`HTTP2ProxyCode.socketCount(${port})`);
+ return count;
+ }
+}
+
+// websocket server
+
+class NodeWebSocketServerCode extends BaseNodeHTTPServerCode {
+ static messageHandler(data, ws) {
+ if (global.wsInputHandler) {
+ global.wsInputHandler(data, ws);
+ return;
+ }
+
+ ws.send("test");
+ }
+
+ 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 https = require("https");
+ global.server = https.createServer(
+ options,
+ BaseNodeHTTPServerCode.globalHandler
+ );
+
+ let node_ws_root = `${__dirname}/../node-ws`;
+ const WebSocket = require(`${node_ws_root}/lib/websocket`);
+ WebSocket.Server = require(`${node_ws_root}/lib/websocket-server`);
+ global.webSocketServer = new WebSocket.Server({ server: global.server });
+ global.webSocketServer.on("connection", function connection(ws) {
+ ws.on("message", data =>
+ NodeWebSocketServerCode.messageHandler(data, ws)
+ );
+ });
+
+ let serverPort = await ADB.listenAndForwardPort(global.server, port);
+ return serverPort;
+ }
+}
+
+class NodeWebSocketServer extends BaseNodeServer {
+ _protocol = "wss";
+ /// 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(BaseNodeHTTPServerCode);
+ await this.execute(NodeWebSocketServerCode);
+ await this.execute(ADB);
+ this._port = await this.execute(
+ `NodeWebSocketServerCode.startServer(${port})`
+ );
+ await this.execute(`global.path_handlers = {};`);
+ await this.execute(`global.wsInputHandler = null;`);
+ }
+
+ async registerMessageHandler(handler) {
+ return this.execute(`global.wsInputHandler = ${handler.toString()}`);
+ }
+}
+
+// websocket http2 server
+// This code is inspired by
+// https://github.com/szmarczak/http2-wrapper/blob/master/examples/ws/server.js
+class NodeWebSocketHttp2ServerCode extends BaseNodeHTTPServerCode {
+ static async startServer(port) {
+ const fs = require("fs");
+ const options = {
+ key: fs.readFileSync(__dirname + "/http2-cert.key"),
+ cert: fs.readFileSync(__dirname + "/http2-cert.pem"),
+ settings: {
+ enableConnectProtocol: true,
+ },
+ };
+ const http2 = require("http2");
+ global.h2Server = http2.createSecureServer(options);
+
+ let node_ws_root = `${__dirname}/../node-ws`;
+ const WebSocket = require(`${node_ws_root}/lib/websocket`);
+
+ global.h2Server.on("stream", (stream, headers) => {
+ if (headers[":method"] === "CONNECT") {
+ stream.respond();
+
+ const ws = new WebSocket(null);
+ stream.setNoDelay = () => {};
+ ws.setSocket(stream, Buffer.from(""), 100 * 1024 * 1024);
+
+ ws.on("message", data => {
+ if (global.wsInputHandler) {
+ global.wsInputHandler(data, ws);
+ return;
+ }
+
+ ws.send("test");
+ });
+ } else {
+ stream.respond();
+ stream.end("ok");
+ }
+ });
+
+ let serverPort = await ADB.listenAndForwardPort(global.h2Server, port);
+ return serverPort;
+ }
+}
+
+class NodeWebSocketHttp2Server extends BaseNodeServer {
+ _protocol = "h2ws";
+ /// 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(BaseNodeHTTPServerCode);
+ await this.execute(NodeWebSocketHttp2ServerCode);
+ await this.execute(ADB);
+ this._port = await this.execute(
+ `NodeWebSocketHttp2ServerCode.startServer(${port})`
+ );
+ await this.execute(`global.path_handlers = {};`);
+ await this.execute(`global.wsInputHandler = null;`);
+ }
+
+ async registerMessageHandler(handler) {
+ return this.execute(`global.wsInputHandler = ${handler.toString()}`);
+ }
+}
+
+// Helper functions
+
+async function with_node_servers(arrayOfClasses, asyncClosure) {
+ for (let s of arrayOfClasses) {
+ let server = new s();
+ await server.start();
+ registerCleanupFunction(async () => {
+ await server.stop();
+ });
+ await asyncClosure(server);
+ await server.stop();
+ }
+}
+
+// nsITLSServerSocket needs a certificate with a corresponding private key
+// available. xpcshell tests can import the test file "client-cert.p12" using
+// the password "password", resulting in a certificate with the common name
+// "Test End-entity" being available with a corresponding private key.
+function getTestServerCertificate() {
+ const certDB = Cc["@mozilla.org/security/x509certdb;1"].getService(
+ Ci.nsIX509CertDB
+ );
+ const certFile = do_get_file("client-cert.p12");
+ certDB.importPKCS12File(certFile, "password");
+ for (const cert of certDB.getCerts()) {
+ if (cert.commonName == "Test End-entity") {
+ return cert;
+ }
+ }
+ return null;
+}
+
+class WebSocketConnection {
+ constructor() {
+ this._openPromise = new Promise(resolve => {
+ this._openCallback = resolve;
+ });
+
+ this._stopPromise = new Promise(resolve => {
+ this._stopCallback = resolve;
+ });
+
+ this._msgPromise = new Promise(resolve => {
+ this._msgCallback = resolve;
+ });
+
+ this._proxyAvailablePromise = new Promise(resolve => {
+ this._proxyAvailCallback = resolve;
+ });
+
+ this._messages = [];
+ this._ws = null;
+ }
+
+ get QueryInterface() {
+ return ChromeUtils.generateQI([
+ "nsIWebSocketListener",
+ "nsIProtocolProxyCallback",
+ ]);
+ }
+
+ onAcknowledge(aContext, aSize) {}
+ onBinaryMessageAvailable(aContext, aMsg) {
+ this._messages.push(aMsg);
+ this._msgCallback();
+ }
+ onMessageAvailable(aContext, aMsg) {}
+ onServerClose(aContext, aCode, aReason) {}
+ onWebSocketListenerStart(aContext) {}
+ onStart(aContext) {
+ this._openCallback();
+ }
+ onStop(aContext, aStatusCode) {
+ this._stopCallback({ status: aStatusCode });
+ this._ws = null;
+ }
+ onProxyAvailable(req, chan, proxyInfo, status) {
+ if (proxyInfo) {
+ this._proxyAvailCallback({ type: proxyInfo.type });
+ } else {
+ this._proxyAvailCallback({});
+ }
+ }
+
+ static makeWebSocketChan() {
+ let chan = Cc["@mozilla.org/network/protocol;1?name=wss"].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
+ );
+ return chan;
+ }
+ // Returns a promise that resolves when the websocket channel is opened.
+ open(url) {
+ this._ws = WebSocketConnection.makeWebSocketChan();
+ let uri = Services.io.newURI(url);
+ this._ws.asyncOpen(uri, url, {}, 0, this, null);
+ return this._openPromise;
+ }
+ // Closes the inner websocket. code and reason arguments are optional.
+ close(code, reason) {
+ this._ws.close(code || Ci.nsIWebSocketChannel.CLOSE_NORMAL, reason || "");
+ }
+ // Sends a message to the server.
+ send(msg) {
+ this._ws.sendMsg(msg);
+ }
+ // Returns a promise that resolves when the channel's onStop is called.
+ // Promise resolves with an `{status}` object, where status is the
+ // result passed to onStop.
+ finished() {
+ return this._stopPromise;
+ }
+ getProxyInfo() {
+ return this._proxyAvailablePromise;
+ }
+
+ // Returned promise resolves with an array of received messages
+ // If messages have been received in the the past before calling
+ // receiveMessages, the promise will immediately resolve. Otherwise
+ // it will resolve when the first message is received.
+ async receiveMessages() {
+ await this._msgPromise;
+ this._msgPromise = new Promise(resolve => {
+ this._msgCallback = resolve;
+ });
+ let messages = this._messages;
+ this._messages = [];
+ return messages;
+ }
+}
diff --git a/netwerk/test/unit/head_telemetry.js b/netwerk/test/unit/head_telemetry.js
new file mode 100644
index 0000000000..c3b1ec66aa
--- /dev/null
+++ b/netwerk/test/unit/head_telemetry.js
@@ -0,0 +1,171 @@
+/* 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 { TelemetryTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TelemetryTestUtils.sys.mjs"
+);
+
+var HandshakeTelemetryHelpers = {
+ HISTOGRAMS: ["SSL_HANDSHAKE_RESULT", "SSL_TIME_UNTIL_READY"],
+ FLAVORS: ["", "_FIRST_TRY", "_CONSERVATIVE", "_ECH", "_ECH_GREASE"],
+
+ /**
+ * Prints the Histogram to console.
+ *
+ * @param {*} name The identifier of the Histogram.
+ */
+ dumpHistogram(name) {
+ let values = Services.telemetry.getHistogramById(name).snapshot().values;
+ dump(`${name}: ${JSON.stringify(values)}\n`);
+ },
+
+ /**
+ * Counts the number of entries in the histogram, ignoring the bucket value.
+ * e.g. {0: 1, 1: 2, 3: 3} has 6 entries.
+ *
+ * @param {Object} histObject The histogram to count the entries of.
+ * @returns The count of the number of entries in the histogram.
+ */
+ countHistogramEntries(histObject) {
+ Assert.ok(
+ !mozinfo.socketprocess_networking,
+ "Histograms don't populate on network process"
+ );
+ let count = 0;
+ let m = histObject.snapshot().values;
+ for (let k in m) {
+ count += m[k];
+ }
+ return count;
+ },
+
+ /**
+ * Assert that the histogram index is the right value. It expects that
+ * other indexes are all zero.
+ *
+ * @param {Object} histogram The histogram to check.
+ * @param {Number} index The index to check against the expected value.
+ * @param {Number} expected The expected value of the index.
+ */
+ assertHistogramMap(histogram, expectedEntries) {
+ Assert.ok(
+ !mozinfo.socketprocess_networking,
+ "Histograms don't populate on network process"
+ );
+ let snapshot = JSON.parse(JSON.stringify(histogram));
+ for (let [Tk, Tv] of expectedEntries.entries()) {
+ let found = false;
+ for (let [i, val] of Object.entries(snapshot.values)) {
+ if (i == Tk) {
+ found = true;
+ Assert.equal(val, Tv, `expected counts should match at index ${i}`);
+ snapshot.values[i] = 0; // Reset the value
+ }
+ }
+ Assert.ok(found, `Should have found an entry at index ${Tk}`);
+ }
+ for (let k in snapshot.values) {
+ Assert.equal(
+ snapshot.values[k],
+ 0,
+ `Should NOT have found an entry at index ${k} of value ${snapshot.values[k]}`
+ );
+ }
+ },
+
+ /**
+ * Generates the pairwise concatonation of histograms and flavors.
+ *
+ * @param {Array} histogramList A subset of HISTOGRAMS.
+ * @param {Array} flavorList A subset of FLAVORS.
+ * @returns {Array} Valid TLS Histogram identifiers
+ */
+ getHistogramNames(histogramList, flavorList) {
+ let output = [];
+ for (let h of histogramList) {
+ Assert.ok(this.HISTOGRAMS.includes(h), "Histogram name valid");
+ for (let f of flavorList) {
+ Assert.ok(this.FLAVORS.includes(f), "Histogram flavor valid");
+ output.push(h.concat(f));
+ }
+ }
+ return output;
+ },
+
+ /**
+ * getHistogramNames but mapped to Histogram objects.
+ */
+ getHistograms(histogramList, flavorList) {
+ return this.getHistogramNames(histogramList, flavorList).map(x =>
+ Services.telemetry.getHistogramById(x)
+ );
+ },
+
+ /**
+ * Clears TLS Handshake Histograms.
+ */
+ resetHistograms() {
+ let allHistograms = this.getHistograms(this.HISTOGRAMS, this.FLAVORS);
+ for (let h of allHistograms) {
+ h.clear();
+ }
+ },
+
+ /**
+ * Checks that all TLS Handshake Histograms of a particular flavor have
+ * exactly resultCount entries for the resultCode and no other entries.
+ *
+ * @param {Array} flavors An array of strings corresponding to which types
+ * of histograms should have entries. See
+ * HandshakeTelemetryHelpers.FLAVORS.
+ * @param {number} resultCode The expected result code, see sslerr.h. 0 is success, all others are errors.
+ * @param {number} resultCount The number of handshake results expected.
+ */
+ checkEntry(flavors, resultCode, resultCount) {
+ Assert.ok(
+ !mozinfo.socketprocess_networking,
+ "Histograms don't populate on network process"
+ );
+ // SSL_HANDSHAKE_RESULT_{FLAVOR}
+ for (let h of this.getHistograms(["SSL_HANDSHAKE_RESULT"], flavors)) {
+ TelemetryTestUtils.assertHistogram(h, resultCode, resultCount);
+ }
+
+ // SSL_TIME_UNTIL_READY_{FLAVOR} should only contain values if we expected success.
+ if (resultCode === 0) {
+ for (let h of this.getHistograms(["SSL_TIME_UNTIL_READY"], flavors)) {
+ Assert.ok(
+ this.countHistogramEntries(h) === resultCount,
+ "Timing entry count correct"
+ );
+ }
+ } else {
+ for (let h of this.getHistograms(["SSL_TIME_UNTIL_READY"], flavors)) {
+ Assert.ok(
+ this.countHistogramEntries(h) === 0,
+ "No timing entries expected"
+ );
+ }
+ }
+ },
+
+ checkSuccess(flavors, resultCount = 1) {
+ this.checkEntry(flavors, 0, resultCount);
+ },
+
+ checkEmpty(flavors) {
+ for (let h of this.getHistogramNames(this.HISTOGRAMS, flavors)) {
+ let hObj = Services.telemetry.getHistogramById(h);
+ Assert.ok(
+ this.countHistogramEntries(hObj) === 0,
+ `No entries expected in ${h.name}. Contents: ${JSON.stringify(
+ hObj.snapshot()
+ )}`
+ );
+ }
+ },
+};
diff --git a/netwerk/test/unit/head_trr.js b/netwerk/test/unit/head_trr.js
new file mode 100644
index 0000000000..8262c735de
--- /dev/null
+++ b/netwerk/test/unit/head_trr.js
@@ -0,0 +1,550 @@
+/* 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 */
+/* import-globals-from head_servers.js */
+
+/* globals require, __dirname, global, Buffer, process */
+
+/// Sets the TRR related prefs and adds the certificate we use for the HTTP2
+/// server.
+function trr_test_setup() {
+ dump("start!\n");
+
+ let h2Port = Services.env.get("MOZHTTP2_PORT");
+ Assert.notEqual(h2Port, null);
+ Assert.notEqual(h2Port, "");
+
+ // Set to allow the cert presented by our H2 server
+ do_get_profile();
+
+ Services.prefs.setBoolPref("network.http.http2.enabled", true);
+ // the TRR server is on 127.0.0.1
+ if (AppConstants.platform == "android") {
+ Services.prefs.setCharPref("network.trr.bootstrapAddr", "10.0.2.2");
+ } else {
+ Services.prefs.setCharPref("network.trr.bootstrapAddr", "127.0.0.1");
+ }
+
+ // make all native resolve calls "secretly" resolve localhost instead
+ Services.prefs.setBoolPref("network.dns.native-is-localhost", true);
+
+ Services.prefs.setBoolPref("network.trr.wait-for-portal", false);
+ // 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");
+
+ // Turn off strict fallback mode and TRR retry for most tests,
+ // it is tested specifically.
+ Services.prefs.setBoolPref("network.trr.strict_native_fallback", false);
+ Services.prefs.setBoolPref("network.trr.retry_on_recoverable_errors", false);
+
+ // Turn off temp blocklist feature in tests. When enabled we may issue a
+ // lookup to resolve a parent name when blocklisting, which may bleed into
+ // and interfere with subsequent tasks.
+ Services.prefs.setBoolPref("network.trr.temp_blocklist", false);
+
+ // We intentionally don't set the TRR mode. Each test should set it
+ // after setup in the first test.
+
+ return h2Port;
+}
+
+/// 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.bootstrapAddr");
+ Services.prefs.clearUserPref("network.trr.temp_blocklist_duration_sec");
+ 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.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.http2.enabled");
+ Services.prefs.clearUserPref("network.dns.localDomains");
+ Services.prefs.clearUserPref("network.dns.native-is-localhost");
+ Services.prefs.clearUserPref(
+ "network.trr.send_empty_accept-encoding_headers"
+ );
+ Services.prefs.clearUserPref("network.trr.strict_native_fallback");
+ Services.prefs.clearUserPref("network.trr.temp_blocklist");
+}
+
+/// This class sends a DNS query and can be awaited as a promise to get the
+/// response.
+class TRRDNSListener {
+ constructor(...args) {
+ if (args.length < 2) {
+ Assert.ok(false, "TRRDNSListener requires at least two arguments");
+ }
+ this.name = args[0];
+ if (typeof args[1] == "object") {
+ this.options = args[1];
+ } else {
+ this.options = {
+ expectedAnswer: args[1],
+ expectedSuccess: args[2] ?? true,
+ delay: args[3],
+ trrServer: args[4] ?? "",
+ expectEarlyFail: args[5] ?? "",
+ flags: args[6] ?? 0,
+ type: args[7] ?? Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT,
+ port: args[8] ?? -1,
+ };
+ }
+ this.expectedAnswer = this.options.expectedAnswer ?? undefined;
+ this.expectedSuccess = this.options.expectedSuccess ?? true;
+ this.delay = this.options.delay;
+ this.promise = new Promise(resolve => {
+ this.resolve = resolve;
+ });
+ this.type = this.options.type ?? Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT;
+ let trrServer = this.options.trrServer || "";
+ let port = this.options.port || -1;
+
+ // This may be called in a child process that doesn't have Services available.
+ // eslint-disable-next-line mozilla/use-services
+ const threadManager = Cc["@mozilla.org/thread-manager;1"].getService(
+ Ci.nsIThreadManager
+ );
+ const currentThread = threadManager.currentThread;
+
+ this.additionalInfo =
+ trrServer == "" && port == -1
+ ? null
+ : Services.dns.newAdditionalInfo(trrServer, port);
+ try {
+ this.request = Services.dns.asyncResolve(
+ this.name,
+ this.type,
+ this.options.flags || 0,
+ this.additionalInfo,
+ this,
+ currentThread,
+ this.options.originAttributes || {} // defaultOriginAttributes
+ );
+ Assert.ok(!this.options.expectEarlyFail, "asyncResolve ok");
+ } catch (e) {
+ Assert.ok(this.options.expectEarlyFail, "asyncResolve fail");
+ this.resolve({ error: 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");
+
+ if (this.type != Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT) {
+ this.resolve({ inRequest, inRecord, inStatus });
+ return;
+ }
+
+ 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);
+ }
+
+ cancel(aStatus = Cr.NS_ERROR_ABORT) {
+ Services.dns.cancelAsyncResolve(
+ this.name,
+ this.type,
+ this.options.flags || 0,
+ this.resolverInfo,
+ this,
+ aStatus,
+ {}
+ );
+ }
+}
+
+// This is for reteriiving the raw bytes from a DNS answer.
+function answerHandler(req, resp) {
+ let searchParams = new URL(req.url, "http://example.com").searchParams;
+ console.log("req.searchParams:" + searchParams);
+ if (!searchParams.get("host")) {
+ resp.writeHead(400);
+ resp.end("Missing search parameter");
+ return;
+ }
+
+ function processRequest(req1, resp1) {
+ let domain = searchParams.get("host");
+ let type = searchParams.get("type");
+ let response = global.dns_query_answers[`${domain}/${type}`] || {};
+ let buf = global.dnsPacket.encode({
+ type: "response",
+ id: 0,
+ flags: 0,
+ questions: [],
+ answers: response.answers || [],
+ additionals: response.additionals || [],
+ });
+ let writeResponse = (resp2, buf2, context) => {
+ try {
+ let data = buf2.toString("hex");
+ resp2.setHeader("Content-Length", data.length);
+ resp2.writeHead(200, { "Content-Type": "plain/text" });
+ resp2.write(data);
+ resp2.end("");
+ } catch (e) {}
+ };
+
+ writeResponse(resp1, buf, response);
+ }
+
+ processRequest(req, resp);
+}
+
+/// 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) {
+ 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");
+ processRequest(req, resp, requestBody);
+ } else {
+ // unexpected method.
+ resp.writeHead(405);
+ resp.end("Unexpected method");
+ }
+
+ function processRequest(req1, resp1, payload) {
+ let dnsQuery = global.dnsPacket.decode(payload);
+ let domain = dnsQuery.questions[0].name;
+ let type = dnsQuery.questions[0].type;
+ let response = global.dns_query_answers[`${domain}/${type}`] || {};
+
+ if (!global.dns_query_counts[domain]) {
+ global.dns_query_counts[domain] = {};
+ }
+ global.dns_query_counts[domain][type] =
+ global.dns_query_counts[domain][type] + 1 || 1;
+
+ let flags = global.dnsPacket.RECURSION_DESIRED;
+ if (!response.answers && !response.flags) {
+ flags |= 2; // SERVFAIL
+ }
+ flags |= response.flags || 0;
+ let buf = global.dnsPacket.encode({
+ type: "response",
+ id: dnsQuery.id,
+ flags,
+ questions: dnsQuery.questions,
+ answers: response.answers || [],
+ additionals: response.additionals || [],
+ });
+
+ let writeResponse = (resp2, buf2, context) => {
+ try {
+ if (context.error) {
+ // If the error is a valid HTTP response number just write it out.
+ if (context.error < 600) {
+ resp2.writeHead(context.error);
+ resp2.end("Intentional error");
+ return;
+ }
+
+ // Bigger error means force close the session
+ req1.stream.session.close();
+ return;
+ }
+ resp2.setHeader("Content-Length", buf2.length);
+ resp2.writeHead(200, { "Content-Type": "application/dns-message" });
+ resp2.write(buf2);
+ resp2.end("");
+ } catch (e) {}
+ };
+
+ if (response.delay) {
+ // This function is handled within the httpserver where setTimeout is
+ // available.
+ // eslint-disable-next-line no-undef
+ setTimeout(
+ arg => {
+ writeResponse(arg[0], arg[1], arg[2]);
+ },
+ response.delay,
+ [resp1, buf, response]
+ );
+ return;
+ }
+
+ writeResponse(resp1, buf, response);
+ }
+}
+
+function getRequestCount(domain, type) {
+ if (!global.dns_query_counts[domain]) {
+ return 0;
+ }
+ return global.dns_query_counts[domain][type] || 0;
+}
+
+// A convenient wrapper around NodeServer
+class TRRServer extends NodeHTTP2Server {
+ /// Starts the server
+ /// @port - default 0
+ /// when provided, will attempt to listen on that port.
+ async start(port = 0) {
+ await super.start(port);
+ await this.execute(`( () => {
+ // key: string "name/type"
+ // value: array [answer1, answer2]
+ global.dns_query_answers = {};
+
+ // key: domain
+ // value: a map containing {key: type, value: number of requests}
+ global.dns_query_counts = {};
+
+ global.dnsPacket = require(\`\${__dirname}/../dns-packet\`);
+ global.ip = require(\`\${__dirname}/../node_ip\`);
+ global.http2 = require("http2");
+ })()`);
+ await this.registerPathHandler("/dns-query", trrQueryHandler);
+ await this.registerPathHandler("/dnsAnswer", answerHandler);
+ await this.execute(getRequestCount);
+ }
+
+ /// @name : string - name we're providing answers for. eg: foo.example.com
+ /// @type : string - the DNS query type. eg: "A", "AAAA", "CNAME", etc
+ /// @response : a map containing the response
+ /// answers: array of answers (hashmap) that dnsPacket can parse
+ /// eg: [{
+ /// name: "bar.example.com",
+ /// ttl: 55,
+ /// type: "A",
+ /// flush: false,
+ /// data: "1.2.3.4",
+ /// }]
+ /// additionals - array of answers (hashmap) to be added to the additional section
+ /// delay: int - if not 0 the response will be sent with after `delay` ms.
+ /// flags: int - flags to be set on the answer
+ /// error: int - HTTP status. If truthy then the response will send this status
+ async registerDoHAnswers(name, type, response = {}) {
+ let text = `global.dns_query_answers["${name}/${type}"] = ${JSON.stringify(
+ response
+ )}`;
+ return this.execute(text);
+ }
+
+ async requestCount(domain, type) {
+ return this.execute(`getRequestCount("${domain}", "${type}")`);
+ }
+}
+
+// Implements a basic HTTP2 proxy server
+class TRRProxyCode {
+ static async startServer(endServerPort) {
+ 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();
+ global.endServerPort = endServerPort;
+
+ await global.proxy.listen(0);
+
+ let serverPort = global.proxy.address().port;
+ return serverPort;
+ }
+
+ static closeProxy() {
+ global.proxy.closeSockets();
+ return new Promise(resolve => {
+ global.proxy.close(resolve);
+ });
+ }
+
+ static proxyRequestCount() {
+ return global.proxy_stream_count;
+ }
+
+ static setupProxy() {
+ if (!global.proxy) {
+ throw new Error("proxy is null");
+ }
+
+ global.proxy_stream_count = 0;
+
+ // We need to track active connections so we can forcefully close keep-alive
+ // connections when shutting down the proxy.
+ global.proxy.socketIndex = 0;
+ global.proxy.socketMap = {};
+ global.proxy.on("connection", function (socket) {
+ let index = global.proxy.socketIndex++;
+ global.proxy.socketMap[index] = socket;
+ socket.on("close", function () {
+ delete global.proxy.socketMap[index];
+ });
+ });
+ global.proxy.closeSockets = function () {
+ for (let i in global.proxy.socketMap) {
+ global.proxy.socketMap[i].destroy();
+ }
+ };
+
+ global.proxy.on("stream", (stream, headers) => {
+ if (headers[":method"] !== "CONNECT") {
+ // Only accept CONNECT requests
+ stream.respond({ ":status": 405 });
+ stream.end();
+ return;
+ }
+ global.proxy_stream_count++;
+ const net = require("net");
+ const socket = net.connect(global.endServerPort, "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 new Error(
+ `Unxpected error when conneting the HTTP/2 server from the HTTP/2 proxy during CONNECT handling: '${error}'`
+ );
+ });
+ });
+ }
+}
+
+class TRRProxy {
+ // Starts the proxy
+ async start(port) {
+ info("TRRProxy start!");
+ this.processId = await NodeServer.fork();
+ info("processid=" + this.processId);
+ await this.execute(TRRProxyCode);
+ this.port = await this.execute(`TRRProxyCode.startServer(${port})`);
+ Assert.notEqual(this.port, null);
+ }
+
+ // 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.execute(this.processId, `TRRProxyCode.closeProxy()`);
+ await NodeServer.kill(this.processId);
+ }
+ }
+
+ async request_count() {
+ let data = await NodeServer.execute(
+ this.processId,
+ `TRRProxyCode.proxyRequestCount()`
+ );
+ return parseInt(data);
+ }
+}
diff --git a/netwerk/test/unit/head_websocket.js b/netwerk/test/unit/head_websocket.js
new file mode 100644
index 0000000000..84c5987f38
--- /dev/null
+++ b/netwerk/test/unit/head_websocket.js
@@ -0,0 +1,71 @@
+/* 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 WebSocketListener(closure, ws, sentMsg) {
+ this._closure = closure;
+ this._ws = ws;
+ this._sentMsg = sentMsg;
+}
+
+WebSocketListener.prototype = {
+ _closure: null,
+ _ws: null,
+ _sentMsg: null,
+ _received: null,
+ QueryInterface: ChromeUtils.generateQI(["nsIWebSocketListener"]),
+
+ onAcknowledge(aContext, aSize) {},
+ onBinaryMessageAvailable(aContext, aMsg) {
+ info("WsListener::onBinaryMessageAvailable");
+ this._received = aMsg;
+ this._ws.close(0, null);
+ },
+ onMessageAvailable(aContext, aMsg) {},
+ onServerClose(aContext, aCode, aReason) {},
+ onWebSocketListenerStart(aContext) {},
+ onStart(aContext) {
+ this._ws.sendMsg(this._sentMsg);
+ },
+ onStop(aContext, aStatusCode) {
+ try {
+ this._closure(aStatusCode, this._received);
+ this._ws = null;
+ } catch (ex) {
+ do_throw("Error in closure function: " + ex);
+ }
+ },
+};
+
+function makeWebSocketChan() {
+ let chan = Cc["@mozilla.org/network/protocol;1?name=wss"].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
+ );
+ return chan;
+}
+
+function openWebSocketChannelPromise(chan, url, msg) {
+ let uri = Services.io.newURI(url);
+ return new Promise(resolve => {
+ function finish(status, result) {
+ resolve([status, result]);
+ }
+ chan.asyncOpen(
+ uri,
+ url,
+ {},
+ 0,
+ new WebSocketListener(finish, chan, msg),
+ null
+ );
+ });
+}
diff --git a/netwerk/test/unit/head_webtransport.js b/netwerk/test/unit/head_webtransport.js
new file mode 100644
index 0000000000..99432e950d
--- /dev/null
+++ b/netwerk/test/unit/head_webtransport.js
@@ -0,0 +1,134 @@
+/* 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_cookies.js */
+
+let WebTransportListener = function () {};
+
+WebTransportListener.prototype = {
+ onSessionReady(sessionId) {
+ info("SessionId " + sessionId);
+ this.ready();
+ },
+ onSessionClosed(errorCode, reason) {
+ info("Error: " + errorCode + " reason: " + reason);
+ if (this.closed) {
+ this.closed();
+ }
+ },
+ onIncomingBidirectionalStreamAvailable(stream) {
+ info("got incoming bidirectional stream");
+ this.streamAvailable(stream);
+ },
+ onIncomingUnidirectionalStreamAvailable(stream) {
+ info("got incoming unidirectional stream");
+ this.streamAvailable(stream);
+ },
+ onDatagramReceived(data) {
+ info("got datagram");
+ if (this.onDatagram) {
+ this.onDatagram(data);
+ }
+ },
+ onMaxDatagramSize(size) {
+ info("max datagram size: " + size);
+ if (this.onMaxDatagramSize) {
+ this.onMaxDatagramSize(size);
+ }
+ },
+ onOutgoingDatagramOutCome(id, outcome) {
+ if (this.onDatagramOutcome) {
+ this.onDatagramOutcome({ id, outcome });
+ }
+ },
+
+ QueryInterface: ChromeUtils.generateQI(["WebTransportSessionEventListener"]),
+};
+
+function WebTransportStreamCallback() {}
+
+WebTransportStreamCallback.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["nsIWebTransportStreamCallback"]),
+
+ onBidirectionalStreamReady(aStream) {
+ Assert.ok(aStream != null);
+ this.finish(aStream);
+ },
+ onUnidirectionalStreamReady(aStream) {
+ Assert.ok(aStream != null);
+ this.finish(aStream);
+ },
+ onError(aError) {
+ this.finish(aError);
+ },
+};
+
+function StreamStatsCallback() {}
+
+StreamStatsCallback.prototype = {
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIWebTransportStreamStatsCallback",
+ ]),
+
+ onSendStatsAvailable(aStats) {
+ Assert.ok(aStats != null);
+ this.finish(aStats);
+ },
+ onReceiveStatsAvailable(aStats) {
+ Assert.ok(aStats != null);
+ this.finish(aStats);
+ },
+};
+
+function inputStreamReader() {}
+
+inputStreamReader.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["nsIInputStreamCallback"]),
+
+ onInputStreamReady(input) {
+ let data = NetUtil.readInputStreamToString(input, input.available());
+ this.finish(data);
+ },
+};
+
+function streamCreatePromise(transport, bidi) {
+ return new Promise(resolve => {
+ let listener = new WebTransportStreamCallback().QueryInterface(
+ Ci.nsIWebTransportStreamCallback
+ );
+ listener.finish = resolve;
+
+ if (bidi) {
+ transport.createOutgoingBidirectionalStream(listener);
+ } else {
+ transport.createOutgoingUnidirectionalStream(listener);
+ }
+ });
+}
+
+function sendStreamStatsPromise(stream) {
+ return new Promise(resolve => {
+ let listener = new StreamStatsCallback().QueryInterface(
+ Ci.nsIWebTransportStreamStatsCallback
+ );
+ listener.finish = resolve;
+
+ stream.QueryInterface(Ci.nsIWebTransportSendStream);
+ stream.getSendStreamStats(listener);
+ });
+}
+
+function receiveStreamStatsPromise(stream) {
+ return new Promise(resolve => {
+ let listener = new StreamStatsCallback().QueryInterface(
+ Ci.nsIWebTransportStreamStatsCallback
+ );
+ listener.finish = resolve;
+
+ stream.QueryInterface(Ci.nsIWebTransportReceiveStream);
+ stream.getReceiveStreamStats(listener);
+ });
+}
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/http2_test_common.js b/netwerk/test/unit/http2_test_common.js
new file mode 100644
index 0000000000..341aa191da
--- /dev/null
+++ b/netwerk/test/unit/http2_test_common.js
@@ -0,0 +1,1504 @@
+// test HTTP/2
+
+"use strict";
+
+/* import-globals-from head_channels.js */
+
+// 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));
+ }
+ request.QueryInterface(Ci.nsIProxiedChannel);
+ var httpProxyConnectResponseCode = request.httpProxyConnectResponseCode;
+ this.finish({ httpProxyConnectResponseCode });
+ },
+};
+
+/*
+ * Support for testing valid multiplexing of streams
+ */
+
+var multiplexContent = generateContent(30 * 1024);
+
+/* 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);
+
+ request.QueryInterface(Ci.nsIProxiedChannel);
+ // This is what does most of the hard work for us
+ var httpProxyConnectResponseCode = request.httpProxyConnectResponseCode;
+ var streamID = this.streamID;
+ this.finish({ httpProxyConnectResponseCode, streamID });
+};
+
+// 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:${this.serverPort}/push.js` ||
+ request.originalURI.spec ==
+ `https://localhost:${this.serverPort}/push2.js` ||
+ request.originalURI.spec == `https://localhost:${this.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:${this.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) {
+ request.QueryInterface(Ci.nsIProxiedChannel);
+ var httpProxyConnectResponseCode = request.httpProxyConnectResponseCode;
+ this.finish({ httpProxyConnectResponseCode });
+ }
+};
+
+Http2ContinuedHeaderListener.prototype.onPush = function (
+ associatedChannel,
+ pushChannel
+) {
+ Assert.equal(
+ associatedChannel.originalURI.spec,
+ "https://localhost:" + this.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);
+
+ request.QueryInterface(Ci.nsIProxiedChannel);
+ var httpProxyConnectResponseCode = request.httpProxyConnectResponseCode;
+ this.finish({ httpProxyConnectResponseCode });
+};
+
+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
+ request.QueryInterface(Ci.nsIProxiedChannel);
+ var httpProxyConnectResponseCode = request.httpProxyConnectResponseCode;
+ this.finish({ httpProxyConnectResponseCode });
+};
+
+// 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")
+ );
+};
+
+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
+async function test_http2_blocking_download(serverPort) {
+ var chan = makeHTTPChannel(`https://localhost:${serverPort}/bigdownload`);
+ var internalChannel = chan.QueryInterface(Ci.nsIHttpChannelInternal);
+ internalChannel.initialRwin = 500000; // make the stream.suspend push back in h2
+ var p = new Promise(resolve => {
+ var listener = new Http2CheckListener();
+ listener.finish = resolve;
+ 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 = makeHTTPChannel(`https://localhost:${serverPort}/`);
+ var sl = new ResumeStalledChannelListener();
+ sl.resumable = chan;
+ simpleChannel.asyncOpen(sl);
+ });
+ return p;
+}
+
+// Make sure we make a HTTP2 connection and both us and the server mark it as such
+async function test_http2_basic(serverPort) {
+ var chan = makeHTTPChannel(`https://localhost:${serverPort}/`);
+ var p = new Promise(resolve => {
+ var listener = new Http2CheckListener();
+ listener.finish = resolve;
+ chan.asyncOpen(listener);
+ });
+ return p;
+}
+
+async function test_http2_basic_unblocked_dep(serverPort) {
+ var chan = makeHTTPChannel(
+ `https://localhost:${serverPort}/basic_unblocked_dep`
+ );
+ var cos = chan.QueryInterface(Ci.nsIClassOfService);
+ cos.addClassFlags(Ci.nsIClassOfService.Unblocked);
+ return new Promise(resolve => {
+ var listener = new Http2CheckListener();
+ listener.finish = resolve;
+ chan.asyncOpen(listener);
+ });
+}
+
+// make sure we don't use h2 when disallowed
+async function test_http2_nospdy(serverPort) {
+ var chan = makeHTTPChannel(`https://localhost:${serverPort}/`);
+ return new Promise(resolve => {
+ var listener = new Http2CheckListener();
+ listener.finish = resolve;
+ 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, finish) {
+ if (xhr.readyState != 4) {
+ return;
+ }
+
+ Assert.equal(xhr.status, 200);
+ Assert.equal(checkIsHttp2(xhr), true);
+ finish();
+}
+
+// Fires off an XHR request over h2
+async function test_http2_xhr(serverPort) {
+ return new Promise(resolve => {
+ var req = new XMLHttpRequest();
+ req.open("GET", `https://localhost:${serverPort}/`, true);
+ req.addEventListener("readystatechange", function (evt) {
+ checkXhr(req, resolve);
+ });
+ req.send(null);
+ });
+}
+
+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) {
+ Services.prefs.setIntPref(
+ "network.http.http2.default-concurrent",
+ this.reset
+ );
+ }
+ request.QueryInterface(Ci.nsIProxiedChannel);
+ var httpProxyConnectResponseCode = request.httpProxyConnectResponseCode;
+ this.finish({ httpProxyConnectResponseCode });
+ }
+};
+
+async function test_http2_concurrent(concurrent_channels, serverPort) {
+ var p = new Promise(resolve => {
+ var concurrent_listener = new Http2ConcurrentListener();
+ concurrent_listener.finish = resolve;
+ concurrent_listener.target = 201;
+ concurrent_listener.reset = Services.prefs.getIntPref(
+ "network.http.http2.default-concurrent"
+ );
+ Services.prefs.setIntPref("network.http.http2.default-concurrent", 100);
+
+ for (var i = 0; i < concurrent_listener.target; i++) {
+ concurrent_channels[i] = makeHTTPChannel(
+ `https://localhost:${serverPort}/750ms`
+ );
+ concurrent_channels[i].loadFlags = Ci.nsIRequest.LOAD_BYPASS_CACHE;
+ concurrent_channels[i].asyncOpen(concurrent_listener);
+ }
+ });
+ return p;
+}
+
+async function test_http2_concurrent_post(concurrent_channels, serverPort) {
+ return new Promise(resolve => {
+ var concurrent_listener = new Http2ConcurrentListener();
+ concurrent_listener.finish = resolve;
+ concurrent_listener.target = 8;
+ concurrent_listener.recvdHdr = posts[2].length;
+ concurrent_listener.reset = Services.prefs.getIntPref(
+ "network.http.http2.default-concurrent"
+ );
+ Services.prefs.setIntPref("network.http.http2.default-concurrent", 3);
+
+ for (var i = 0; i < concurrent_listener.target; i++) {
+ concurrent_channels[i] = makeHTTPChannel(
+ `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
+async function test_http2_multiplex(serverPort) {
+ let chan1 = makeHTTPChannel(`https://localhost:${serverPort}/multiplex1`);
+ let chan2 = makeHTTPChannel(`https://localhost:${serverPort}/multiplex2`);
+ let listener1 = new Http2MultiplexListener();
+ let listener2 = new Http2MultiplexListener();
+
+ let promises = [];
+ let p1 = new Promise(resolve => {
+ listener1.finish = resolve;
+ });
+ promises.push(p1);
+ let p2 = new Promise(resolve => {
+ listener2.finish = resolve;
+ });
+ promises.push(p2);
+
+ chan1.asyncOpen(listener1);
+ chan2.asyncOpen(listener2);
+ return Promise.all(promises);
+}
+
+// Test to make sure we gateway non-standard headers properly
+async function test_http2_header(serverPort) {
+ let chan = makeHTTPChannel(`https://localhost:${serverPort}/header`);
+ let hvalue = "Headers are fun";
+ chan.setRequestHeader("X-Test-Header", hvalue, false);
+ return new Promise(resolve => {
+ let listener = new Http2HeaderListener("X-Received-Test-Header", function (
+ received_hvalue
+ ) {
+ Assert.equal(received_hvalue, hvalue);
+ });
+ listener.finish = resolve;
+ chan.asyncOpen(listener);
+ });
+}
+
+// Test to make sure headers with invalid characters in the name are rejected
+async function test_http2_invalid_response_header(serverPort, invalid_kind) {
+ return new Promise(resolve => {
+ var listener = new Http2CheckListener();
+ listener.finish = resolve;
+ listener.shouldSucceed = false;
+ var chan = makeHTTPChannel(
+ `https://localhost:${serverPort}/invalid_response_header/${invalid_kind}`
+ );
+ chan.asyncOpen(listener);
+ });
+}
+
+// Test to make sure cookies are split into separate fields before compression
+async function test_http2_cookie_crumbling(serverPort) {
+ var chan = makeHTTPChannel(
+ `https://localhost:${serverPort}/cookie_crumbling`
+ );
+ var cookiesSent = ["a=b", "c=d01234567890123456789", "e=f"].sort();
+ chan.setRequestHeader("Cookie", cookiesSent.join("; "), false);
+ return new Promise(resolve => {
+ 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);
+ });
+ });
+ listener.finish = resolve;
+ chan.asyncOpen(listener);
+ });
+}
+
+async function test_http2_push1(loadGroup, serverPort) {
+ var chan = makeHTTPChannel(`https://localhost:${serverPort}/push`);
+ chan.loadGroup = loadGroup;
+ return new Promise(resolve => {
+ var listener = new Http2PushListener(true);
+ listener.finish = resolve;
+ listener.serverPort = serverPort;
+ chan.asyncOpen(listener);
+ });
+}
+
+async function test_http2_push2(loadGroup, serverPort) {
+ var chan = makeHTTPChannel(`https://localhost:${serverPort}/push.js`);
+ chan.loadGroup = loadGroup;
+ return new Promise(resolve => {
+ var listener = new Http2PushListener(true);
+ listener.finish = resolve;
+ listener.serverPort = serverPort;
+ chan.asyncOpen(listener);
+ });
+}
+
+async function test_http2_push3(loadGroup, serverPort) {
+ var chan = makeHTTPChannel(`https://localhost:${serverPort}/push2`);
+ chan.loadGroup = loadGroup;
+ return new Promise(resolve => {
+ var listener = new Http2PushListener(true);
+ listener.finish = resolve;
+ listener.serverPort = serverPort;
+ chan.asyncOpen(listener);
+ });
+}
+
+async function test_http2_push4(loadGroup, serverPort) {
+ var chan = makeHTTPChannel(`https://localhost:${serverPort}/push2.js`);
+ chan.loadGroup = loadGroup;
+ return new Promise(resolve => {
+ var listener = new Http2PushListener(true);
+ listener.finish = resolve;
+ listener.serverPort = serverPort;
+ chan.asyncOpen(listener);
+ });
+}
+
+async function test_http2_push5(loadGroup, serverPort) {
+ var chan = makeHTTPChannel(`https://localhost:${serverPort}/push5`);
+ chan.loadGroup = loadGroup;
+ return new Promise(resolve => {
+ var listener = new Http2PushListener(true);
+ listener.finish = resolve;
+ listener.serverPort = serverPort;
+ chan.asyncOpen(listener);
+ });
+}
+
+async function test_http2_push6(loadGroup, serverPort) {
+ var chan = makeHTTPChannel(`https://localhost:${serverPort}/push5.js`);
+ chan.loadGroup = loadGroup;
+ return new Promise(resolve => {
+ var listener = new Http2PushListener(true);
+ listener.finish = resolve;
+ listener.serverPort = serverPort;
+ chan.asyncOpen(listener);
+ });
+}
+
+// this is a basic test where the server sends a simple document with 2 header
+// blocks. bug 1027364
+async function test_http2_doubleheader(serverPort) {
+ var chan = makeHTTPChannel(`https://localhost:${serverPort}/doubleheader`);
+ return new Promise(resolve => {
+ var listener = new Http2CheckListener();
+ listener.finish = resolve;
+ chan.asyncOpen(listener);
+ });
+}
+
+// Make sure we handle GETs that cover more than 2 frames properly
+async function test_http2_big(serverPort) {
+ var chan = makeHTTPChannel(`https://localhost:${serverPort}/big`);
+ return new Promise(resolve => {
+ var listener = new Http2BigListener();
+ listener.finish = resolve;
+ chan.asyncOpen(listener);
+ });
+}
+
+async function test_http2_huge_suspended(serverPort) {
+ var chan = makeHTTPChannel(`https://localhost:${serverPort}/huge`);
+ return new Promise(resolve => {
+ var listener = new Http2HugeSuspendedListener();
+ listener.finish = resolve;
+ 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
+async function test_http2_post(serverPort) {
+ var chan = makeHTTPChannel(`https://localhost:${serverPort}/post`);
+ var p = new Promise(resolve => {
+ var listener = new Http2PostListener(md5s[0]);
+ listener.finish = resolve;
+ do_post(posts[0], chan, listener, "POST");
+ });
+ return p;
+}
+
+async function test_http2_empty_post(serverPort) {
+ var chan = makeHTTPChannel(`https://localhost:${serverPort}/post`);
+ var p = new Promise(resolve => {
+ var listener = new Http2PostListener("0");
+ listener.finish = resolve;
+ do_post("", chan, listener, "POST");
+ });
+ return p;
+}
+
+// Make sure we can do a simple PATCH
+async function test_http2_patch(serverPort) {
+ var chan = makeHTTPChannel(`https://localhost:${serverPort}/patch`);
+ return new Promise(resolve => {
+ var listener = new Http2PostListener(md5s[0]);
+ listener.finish = resolve;
+ do_post(posts[0], chan, listener, "PATCH");
+ });
+}
+
+// Make sure we can do a POST that covers more than 2 frames
+async function test_http2_post_big(serverPort) {
+ var chan = makeHTTPChannel(`https://localhost:${serverPort}/post`);
+ return new Promise(resolve => {
+ var listener = new Http2PostListener(md5s[1]);
+ listener.finish = resolve;
+ do_post(posts[1], chan, listener, "POST");
+ });
+}
+
+// When a http proxy is used alt-svc is disable. Therefore if withProxy is true,
+// try numberOfTries times to connect and make sure that alt-svc is not use and we never
+// connect to the HTTP/2 server.
+var altsvcClientListener = function (
+ finish,
+ httpserv,
+ httpserv2,
+ withProxy,
+ numberOfTries
+) {
+ this.finish = finish;
+ this.httpserv = httpserv;
+ this.httpserv2 = httpserv2;
+ this.withProxy = withProxy;
+ this.numberOfTries = numberOfTries;
+};
+
+altsvcClientListener.prototype = {
+ 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");
+ if (this.withProxy && this.numberOfTries == 0) {
+ request.QueryInterface(Ci.nsIProxiedChannel);
+ var httpProxyConnectResponseCode = request.httpProxyConnectResponseCode;
+ this.finish({ httpProxyConnectResponseCode });
+ return;
+ }
+ let chan = makeHTTPChannel(
+ `http://foo.example.com:${this.httpserv}/altsvc1`,
+ this.withProxy
+ ).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:${this.httpserv2}`,
+ false
+ );
+ chan.loadFlags = Ci.nsIRequest.LOAD_BYPASS_CACHE;
+ chan.asyncOpen(
+ new altsvcClientListener(
+ this.finish,
+ this.httpserv,
+ this.httpserv2,
+ this.withProxy,
+ this.numberOfTries - 1
+ )
+ );
+ } else {
+ Assert.ok(isHttp2Connection);
+ let chan = makeHTTPChannel(
+ `http://foo.example.com:${this.httpserv2}/altsvc2`
+ ).QueryInterface(Ci.nsIHttpChannel);
+ chan.loadFlags = Ci.nsIRequest.LOAD_BYPASS_CACHE;
+ chan.asyncOpen(
+ new altsvcClientListener2(this.finish, this.httpserv, this.httpserv2)
+ );
+ }
+ },
+};
+
+var altsvcClientListener2 = function (finish, httpserv, httpserv2) {
+ this.finish = finish;
+ this.httpserv = httpserv;
+ this.httpserv2 = httpserv2;
+};
+
+altsvcClientListener2.prototype = {
+ 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 = makeHTTPChannel(
+ `http://foo.example.com:${this.httpserv2}/altsvc2`
+ ).QueryInterface(Ci.nsIHttpChannel);
+ chan.loadFlags = Ci.nsIRequest.LOAD_BYPASS_CACHE;
+ chan.asyncOpen(
+ new altsvcClientListener2(this.finish, this.httpserv, this.httpserv2)
+ );
+ } else {
+ Assert.ok(isHttp2Connection);
+ request.QueryInterface(Ci.nsIProxiedChannel);
+ var httpProxyConnectResponseCode = request.httpProxyConnectResponseCode;
+ this.finish({ httpProxyConnectResponseCode });
+ }
+ },
+};
+
+async function test_http2_altsvc(httpserv, httpserv2, withProxy) {
+ var chan = makeHTTPChannel(
+ `http://foo.example.com:${httpserv}/altsvc1`,
+ withProxy
+ ).QueryInterface(Ci.nsIHttpChannel);
+ return new Promise(resolve => {
+ var numberOfTries = 0;
+ if (withProxy) {
+ numberOfTries = 20;
+ }
+ chan.asyncOpen(
+ new altsvcClientListener(
+ resolve,
+ httpserv,
+ httpserv2,
+ withProxy,
+ numberOfTries
+ )
+ );
+ });
+}
+
+var Http2PushApiListener = function (finish, serverPort) {
+ this.finish = finish;
+ this.serverPort = serverPort;
+};
+
+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:" + this.serverPort + "/pushapi1"
+ );
+ Assert.equal(pushChannel.getRequestHeader("x-pushed-request"), "true");
+
+ pushChannel.asyncOpen(this);
+ if (
+ pushChannel.originalURI.spec ==
+ "https://localhost:" + this.serverPort + "/pushapi1/2"
+ ) {
+ pushChannel.cancel(Cr.NS_ERROR_ABORT);
+ } else if (
+ pushChannel.originalURI.spec ==
+ "https://localhost:" + this.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:${this.serverPort}/pushapi1/2`
+ );
+
+ var data = read_stream(stream, cnt);
+
+ if (
+ request.originalURI.spec ==
+ `https://localhost:${this.serverPort}/pushapi1`
+ ) {
+ Assert.equal(data[0], "0");
+ --this.checksPending;
+ } else if (
+ request.originalURI.spec ==
+ `https://localhost:${this.serverPort}/pushapi1/1`
+ ) {
+ Assert.equal(data[0], "1");
+ --this.checksPending; // twice
+ } else if (
+ request.originalURI.spec ==
+ `https://localhost:${this.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:${this.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) {
+ request.QueryInterface(Ci.nsIProxiedChannel);
+ var httpProxyConnectResponseCode = request.httpProxyConnectResponseCode;
+ this.finish({ httpProxyConnectResponseCode });
+ }
+ },
+};
+
+// 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
+
+async function test_http2_pushapi_1(loadGroup, serverPort) {
+ var chan = makeHTTPChannel(`https://localhost:${serverPort}/pushapi1`);
+ chan.loadGroup = loadGroup;
+ return new Promise(resolve => {
+ var listener = new Http2PushApiListener(resolve, serverPort);
+ chan.notificationCallbacks = listener;
+ chan.asyncOpen(listener);
+ });
+}
+
+var WrongSuiteListener = function () {};
+
+WrongSuiteListener.prototype = new Http2CheckListener();
+WrongSuiteListener.prototype.shouldBeHttp2 = false;
+WrongSuiteListener.prototype.onStopRequest = function (request, status) {
+ Services.prefs.setBoolPref(
+ "security.ssl3.ecdhe_rsa_aes_128_gcm_sha256",
+ true
+ );
+ Services.prefs.clearUserPref("security.tls.version.max");
+ Http2CheckListener.prototype.onStopRequest.call(this, request, status);
+};
+
+// test that we use h1 without the mandatory cipher suite available when
+// offering at most tls1.2
+async function test_http2_wrongsuite_tls12(serverPort) {
+ Services.prefs.setBoolPref(
+ "security.ssl3.ecdhe_rsa_aes_128_gcm_sha256",
+ false
+ );
+ Services.prefs.setIntPref("security.tls.version.max", 3);
+ var chan = makeHTTPChannel(`https://localhost:${serverPort}/wrongsuite`);
+ chan.loadFlags =
+ Ci.nsIRequest.LOAD_FRESH_CONNECTION |
+ Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI;
+ return new Promise(resolve => {
+ var listener = new WrongSuiteListener();
+ listener.finish = resolve;
+ chan.asyncOpen(listener);
+ });
+}
+
+// test that we use h2 when offering tls1.3 or higher regardless of if the
+// mandatory cipher suite is available
+async function test_http2_wrongsuite_tls13(serverPort) {
+ Services.prefs.setBoolPref(
+ "security.ssl3.ecdhe_rsa_aes_128_gcm_sha256",
+ false
+ );
+ var chan = makeHTTPChannel(`https://localhost:${serverPort}/wrongsuite`);
+ chan.loadFlags =
+ Ci.nsIRequest.LOAD_FRESH_CONNECTION |
+ Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI;
+ return new Promise(resolve => {
+ var listener = new WrongSuiteListener();
+ listener.finish = resolve;
+ listener.shouldBeHttp2 = true;
+ chan.asyncOpen(listener);
+ });
+}
+
+async function test_http2_h11required_stream(serverPort) {
+ var chan = makeHTTPChannel(
+ `https://localhost:${serverPort}/h11required_stream`
+ );
+ return new Promise(resolve => {
+ var listener = new Http2CheckListener();
+ listener.finish = resolve;
+ 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);
+
+ request.QueryInterface(Ci.nsIProxiedChannel);
+ var httpProxyConnectResponseCode = request.httpProxyConnectResponseCode;
+ this.finish({ httpProxyConnectResponseCode });
+};
+
+async function test_http2_h11required_session(serverPort) {
+ var chan = makeHTTPChannel(
+ `https://localhost:${serverPort}/h11required_session`
+ );
+ return new Promise(resolve => {
+ var listener = new H11RequiredSessionListener();
+ listener.finish = resolve;
+ listener.shouldBeHttp2 = false;
+ chan.asyncOpen(listener);
+ });
+}
+
+async function test_http2_retry_rst(serverPort) {
+ var chan = makeHTTPChannel(`https://localhost:${serverPort}/rstonce`);
+ return new Promise(resolve => {
+ var listener = new Http2CheckListener();
+ listener.finish = resolve;
+ chan.asyncOpen(listener);
+ });
+}
+
+async function test_http2_continuations(loadGroup, serverPort) {
+ var chan = makeHTTPChannel(
+ `https://localhost:${serverPort}/continuedheaders`
+ );
+ chan.loadGroup = loadGroup;
+ return new Promise(resolve => {
+ var listener = new Http2ContinuedHeaderListener();
+ listener.finish = resolve;
+ listener.serverPort = serverPort;
+ 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);
+
+ request.QueryInterface(Ci.nsIProxiedChannel);
+ var httpProxyConnectResponseCode = request.httpProxyConnectResponseCode;
+ this.finish({ httpProxyConnectResponseCode });
+};
+
+function Http2IllegalHpackListener() {}
+Http2IllegalHpackListener.prototype = new Http2CheckListener();
+Http2IllegalHpackListener.prototype.shouldGoAway = false;
+
+Http2IllegalHpackListener.prototype.onStopRequest = function (request, status) {
+ var chan = makeHTTPChannel(
+ `https://localhost:${this.serverPort}/illegalhpack_validate`
+ );
+ var listener = new Http2IllegalHpackValidationListener();
+ listener.finish = this.finish;
+ listener.shouldGoAway = this.shouldGoAway;
+ chan.asyncOpen(listener);
+};
+
+async function test_http2_illegalhpacksoft(serverPort) {
+ var chan = makeHTTPChannel(
+ `https://localhost:${serverPort}/illegalhpacksoft`
+ );
+ return new Promise(resolve => {
+ var listener = new Http2IllegalHpackListener();
+ listener.finish = resolve;
+ listener.serverPort = serverPort;
+ listener.shouldGoAway = false;
+ listener.shouldSucceed = false;
+ chan.asyncOpen(listener);
+ });
+}
+
+async function test_http2_illegalhpackhard(serverPort) {
+ var chan = makeHTTPChannel(
+ `https://localhost:${serverPort}/illegalhpackhard`
+ );
+ return new Promise(resolve => {
+ var listener = new Http2IllegalHpackListener();
+ listener.finish = resolve;
+ listener.serverPort = serverPort;
+ listener.shouldGoAway = true;
+ listener.shouldSucceed = false;
+ chan.asyncOpen(listener);
+ });
+}
+
+async function test_http2_folded_header(loadGroup, serverPort) {
+ var chan = makeHTTPChannel(`https://localhost:${serverPort}/foldedheader`);
+ chan.loadGroup = loadGroup;
+ return new Promise(resolve => {
+ var listener = new Http2CheckListener();
+ listener.finish = resolve;
+ listener.shouldSucceed = false;
+ chan.asyncOpen(listener);
+ });
+}
+
+async function test_http2_empty_data(serverPort) {
+ var chan = makeHTTPChannel(`https://localhost:${serverPort}/emptydata`);
+ return new Promise(resolve => {
+ var listener = new Http2CheckListener();
+ listener.finish = resolve;
+ chan.asyncOpen(listener);
+ });
+}
+
+async function test_http2_push_firstparty1(loadGroup, serverPort) {
+ var chan = makeHTTPChannel(`https://localhost:${serverPort}/push`);
+ chan.loadGroup = loadGroup;
+ chan.loadInfo.originAttributes = { firstPartyDomain: "foo.com" };
+ return new Promise(resolve => {
+ var listener = new Http2PushListener(true);
+ listener.finish = resolve;
+ listener.serverPort = serverPort;
+ chan.asyncOpen(listener);
+ });
+}
+
+async function test_http2_push_firstparty2(loadGroup, serverPort) {
+ var chan = makeHTTPChannel(`https://localhost:${serverPort}/push.js`);
+ chan.loadGroup = loadGroup;
+ chan.loadInfo.originAttributes = { firstPartyDomain: "bar.com" };
+ return new Promise(resolve => {
+ var listener = new Http2PushListener(false);
+ listener.finish = resolve;
+ listener.serverPort = serverPort;
+ chan.asyncOpen(listener);
+ });
+}
+
+async function test_http2_push_firstparty3(loadGroup, serverPort) {
+ var chan = makeHTTPChannel(`https://localhost:${serverPort}/push.js`);
+ chan.loadGroup = loadGroup;
+ chan.loadInfo.originAttributes = { firstPartyDomain: "foo.com" };
+ return new Promise(resolve => {
+ var listener = new Http2PushListener(true);
+ listener.finish = resolve;
+ listener.serverPort = serverPort;
+ chan.asyncOpen(listener);
+ });
+}
+
+async function test_http2_push_userContext1(loadGroup, serverPort) {
+ var chan = makeHTTPChannel(`https://localhost:${serverPort}/push`);
+ chan.loadGroup = loadGroup;
+ chan.loadInfo.originAttributes = { userContextId: 1 };
+ return new Promise(resolve => {
+ var listener = new Http2PushListener(true);
+ listener.finish = resolve;
+ listener.serverPort = serverPort;
+ chan.asyncOpen(listener);
+ });
+}
+
+async function test_http2_push_userContext2(loadGroup, serverPort) {
+ var chan = makeHTTPChannel(`https://localhost:${serverPort}/push.js`);
+ chan.loadGroup = loadGroup;
+ chan.loadInfo.originAttributes = { userContextId: 2 };
+ return new Promise(resolve => {
+ var listener = new Http2PushListener(false);
+ listener.finish = resolve;
+ listener.serverPort = serverPort;
+ chan.asyncOpen(listener);
+ });
+}
+
+async function test_http2_push_userContext3(loadGroup, serverPort) {
+ var chan = makeHTTPChannel(`https://localhost:${serverPort}/push.js`);
+ chan.loadGroup = loadGroup;
+ chan.loadInfo.originAttributes = { userContextId: 1 };
+ return new Promise(resolve => {
+ var listener = new Http2PushListener(true);
+ listener.finish = resolve;
+ listener.serverPort = serverPort;
+ chan.asyncOpen(listener);
+ });
+}
+
+async function test_http2_status_phrase(serverPort) {
+ var chan = makeHTTPChannel(`https://localhost:${serverPort}/statusphrase`);
+ return new Promise(resolve => {
+ var listener = new Http2CheckListener();
+ listener.finish = resolve;
+ 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 (finish, loadGroup, serverPort) {
+ this.finish = finish;
+ this.loadGroup = loadGroup;
+ this.serverPort = serverPort;
+};
+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 = makeHTTPChannel(
+ `https://localhost:${this.serverPort}/diskcache`
+ );
+ var listener = new PulledDiskCacheListener();
+ listener.finish = this.finish;
+ chan.loadGroup = this.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 = makeHTTPChannel(`https://localhost:${this.serverPort}/diskcache`);
+ var listener = new FromDiskCacheListener(
+ this.finish,
+ this.loadGroup,
+ this.serverPort
+ );
+ chan.loadGroup = this.loadGroup;
+ chan.asyncOpen(listener);
+};
+
+function continue_test_http2_disk_cache_push(
+ status,
+ entry,
+ finish,
+ loadGroup,
+ serverPort
+) {
+ // 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 = makeHTTPChannel(`https://localhost:${serverPort}/pushindisk`);
+ var listener = new Http2DiskCachePushListener();
+ listener.finish = finish;
+ listener.loadGroup = loadGroup;
+ listener.serverPort = serverPort;
+ chan.loadGroup = loadGroup;
+ chan.asyncOpen(listener);
+}
+
+async function test_http2_disk_cache_push(loadGroup, serverPort) {
+ return new Promise(resolve => {
+ asyncOpenCacheEntry(
+ `https://localhost:${serverPort}/diskcache`,
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ function (status, entry) {
+ continue_test_http2_disk_cache_push(
+ status,
+ entry,
+ resolve,
+ loadGroup,
+ serverPort
+ );
+ },
+ false
+ );
+ });
+}
+
+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 = makeHTTPChannel(
+ `https://localhost:${this.serverPort}/doublypushed`
+ );
+ var listener = new Http2DoublypushedListener();
+ listener.finish = this.finish;
+ chan.loadGroup = this.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");
+
+ request.QueryInterface(Ci.nsIProxiedChannel);
+ let httpProxyConnectResponseCode = request.httpProxyConnectResponseCode;
+ this.finish({ httpProxyConnectResponseCode });
+};
+
+function test_http2_doublepush(loadGroup, serverPort) {
+ var chan = makeHTTPChannel(`https://localhost:${serverPort}/doublepush`);
+ return new Promise(resolve => {
+ var listener = new Http2DoublepushListener();
+ listener.finish = resolve;
+ listener.loadGroup = loadGroup;
+ listener.serverPort = serverPort;
+ chan.loadGroup = loadGroup;
+ chan.asyncOpen(listener);
+ });
+}
diff --git a/netwerk/test/unit/node_execute/test_node_execute_loop.js b/netwerk/test/unit/node_execute/test_node_execute_loop.js
new file mode 100644
index 0000000000..10400b8b54
--- /dev/null
+++ b/netwerk/test/unit/node_execute/test_node_execute_loop.js
@@ -0,0 +1,22 @@
+// This test checks that the interaction between NodeServer.execute defined in
+// httpd.js and the node server that we're interacting with defined in
+// moz-http2.js is working properly.
+// This test spawns a node server that loops on while true and makes sure
+// the the process group is killed by runxpcshelltests.py at exit.
+// See bug 1855174
+
+"use strict";
+
+const { NodeServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+add_task(async function killOnEnd() {
+ let id = await NodeServer.fork();
+ await NodeServer.execute(id, `console.log("hello");`);
+ await NodeServer.execute(id, `console.error("hello");`);
+ // Make the forked subprocess hang forever.
+ NodeServer.execute(id, "while (true) {}").catch(e => {});
+ await new Promise(resolve => do_timeout(10, resolve));
+ // Should get killed at the end of the test by the harness.
+});
diff --git a/netwerk/test/unit/node_execute/xpcshell.toml b/netwerk/test/unit/node_execute/xpcshell.toml
new file mode 100644
index 0000000000..7d881a3bda
--- /dev/null
+++ b/netwerk/test/unit/node_execute/xpcshell.toml
@@ -0,0 +1,5 @@
+[DEFAULT]
+
+["test_node_execute_loop.js"]
+run-sequentially = "node server exceptions dont replay well"
+skip-if = ["verify"] # running it once hangs forever so don't run it in a loop.
diff --git a/netwerk/test/unit/perftest.toml b/netwerk/test/unit/perftest.toml
new file mode 100644
index 0000000000..709f6ce7b4
--- /dev/null
+++ b/netwerk/test/unit/perftest.toml
@@ -0,0 +1,3 @@
+[DEFAULT]
+
+["test_http3_perf.js"]
diff --git a/netwerk/test/unit/proxy-ca.pem b/netwerk/test/unit/proxy-ca.pem
new file mode 100644
index 0000000000..5325d8cbd2
--- /dev/null
+++ b/netwerk/test/unit/proxy-ca.pem
@@ -0,0 +1,18 @@
+-----BEGIN CERTIFICATE-----
+MIIC1DCCAbygAwIBAgIUW4p+/QPIt/MX8PWl1HdqbTSfjakwDQYJKoZIhvcNAQEL
+BQAwGTEXMBUGA1UEAwwOIFByb3h5IFRlc3QgQ0EwIhgPMjAyMjAxMDEwMDAwMDBa
+GA8yMDMyMDEwMTAwMDAwMFowGTEXMBUGA1UEAwwOIFByb3h5IFRlc3QgQ0EwggEi
+MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC6iFGoRI4W1kH9braIBjYQPTwT
+2erkNUq07PVoV2wke8HHJajg2B+9sZwGm24ahvJr4q9adWtqZHEIeqVap0WH9xzV
+JJwCfs1D/B5p0DggKZOrIMNJ5Nu5TMJrbA7tFYIP8X6taRqx0wI6iypB7qdw4A8N
+jf1mCyuwJJKkfbmIYXmQsVeQPdI7xeC4SB+oN9OIQ+8nFthVt2Zaqn4CkC86exCA
+BiTMHGyXrZZhW7filhLAdTGjDJHdtMr3/K0dJdMJ77kXDqdo4bN7LyJvaeO0ipVh
+He4m1iWdq5EITjbLHCQELL8Wiy/l8Y+ZFzG4s/5JI/pyUcQx1QOs2hgKNe2NAgMB
+AAGjEDAOMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAIeZDX+A8ZhI
+NU+wg2vTMKr5hyd0cOVUgOOWUGmATrgAMuo9Asn29SNcI61nGTrUpDBuDCy8OTQf
+J7Gva5eLmS/c+INpRtLzcCKvkd06bexSKk8naUTuFwtwtS0WQwSV20yUf9mR+UcO
+60U9F2r7dHfgFqXlNhQ1AngXkfMOlrCWw50CyMj7y9fOeJ22Q0JDXzK2UU64tWhi
+e22fSfFCdjRcIPiqdG+BHNQe2M6DYPMgrrEnRqq/3mJsf886FxR1AhqkhYS/tkLZ
+TGSrETajIK62v3qY9LNB8iWv2e0lj0pZlPsTmUWysIXc3fAF6RIu3T8Ypxm5i6sw
+OfmaaMxPV20=
+-----END CERTIFICATE-----
diff --git a/netwerk/test/unit/proxy-ca.pem.certspec b/netwerk/test/unit/proxy-ca.pem.certspec
new file mode 100644
index 0000000000..4e0279143d
--- /dev/null
+++ b/netwerk/test/unit/proxy-ca.pem.certspec
@@ -0,0 +1,4 @@
+issuer: Proxy Test CA
+subject: Proxy Test CA
+validity:20220101-20320101
+extension:basicConstraints:cA,
diff --git a/netwerk/test/unit/socks_client_subprocess.js b/netwerk/test/unit/socks_client_subprocess.js
new file mode 100644
index 0000000000..7bd5dc120e
--- /dev/null
+++ b/netwerk/test/unit/socks_client_subprocess.js
@@ -0,0 +1,94 @@
+/* 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(resolve, 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, null);
+ 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..dd8c597113
--- /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_headers.js b/netwerk/test/unit/test_304_headers.js
new file mode 100644
index 0000000000..b3583663bf
--- /dev/null
+++ b/netwerk/test/unit/test_304_headers.js
@@ -0,0 +1,91 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+ChromeUtils.defineLazyGetter(this, "URL", function () {
+ return `http://localhost:${httpServer.identity.primaryPort}/test`;
+});
+
+let httpServer = null;
+
+function make_channel(url) {
+ return NetUtil.newChannel({
+ uri: url,
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+}
+
+function contentHandler(metadata, response) {
+ response.seizePower();
+ let etag = "";
+ try {
+ etag = metadata.getHeader("If-None-Match");
+ } catch (ex) {}
+
+ if (etag == "test-etag1") {
+ response.write("HTTP/1.1 304 Not Modified\r\n");
+
+ response.write("Link: <ref>; param1=value1\r\n");
+ response.write("Link: <ref2>; param2=value2\r\n");
+ response.write("Link: <ref3>; param1=value1\r\n");
+ response.write("\r\n");
+ response.finish();
+ return;
+ }
+
+ response.write("HTTP/1.1 200 OK\r\n");
+
+ response.write("ETag: test-etag1\r\n");
+ response.write("Link: <ref>; param1=value1\r\n");
+ response.write("Link: <ref2>; param2=value2\r\n");
+ response.write("Link: <ref3>; param1=value1\r\n");
+ response.write("\r\n");
+ response.finish();
+}
+
+add_task(async function test() {
+ httpServer = new HttpServer();
+ httpServer.registerPathHandler("/test", contentHandler);
+ httpServer.start(-1);
+ registerCleanupFunction(async () => {
+ await httpServer.stop();
+ });
+
+ let chan = make_channel(Services.io.newURI(URL));
+ chan.requestMethod = "HEAD";
+ await new Promise(resolve => {
+ chan.asyncOpen({
+ onStopRequest(req, status) {
+ equal(status, Cr.NS_OK);
+ equal(req.QueryInterface(Ci.nsIHttpChannel).responseStatus, 200);
+ equal(
+ req.QueryInterface(Ci.nsIHttpChannel).getResponseHeader("Link"),
+ "<ref>; param1=value1, <ref2>; param2=value2, <ref3>; param1=value1"
+ );
+ resolve();
+ },
+ onStartRequest(req) {},
+ onDataAvailable() {},
+ });
+ });
+
+ chan = make_channel(Services.io.newURI(URL));
+ chan.requestMethod = "HEAD";
+ await new Promise(resolve => {
+ chan.asyncOpen({
+ onStopRequest(req, status) {
+ equal(status, Cr.NS_OK);
+ equal(req.QueryInterface(Ci.nsIHttpChannel).responseStatus, 200);
+ equal(
+ req.QueryInterface(Ci.nsIHttpChannel).getResponseHeader("Link"),
+ "<ref>; param1=value1, <ref2>; param2=value2, <ref3>; param1=value1"
+ );
+ resolve();
+ },
+ onStartRequest(req) {},
+ onDataAvailable() {},
+ });
+ });
+});
diff --git a/netwerk/test/unit/test_304_responses.js b/netwerk/test/unit/test_304_responses.js
new file mode 100644
index 0000000000..a468eec191
--- /dev/null
+++ b/netwerk/test/unit/test_304_responses.js
@@ -0,0 +1,92 @@
+"use strict";
+// https://bugzilla.mozilla.org/show_bug.cgi?id=761228
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+ChromeUtils.defineLazyGetter(this, "URL", function () {
+ return "http://localhost:" + httpServer.identity.primaryPort;
+});
+
+var httpServer = null;
+const testFileName = "test_customConditionalRequest_304";
+const basePath = "/" + testFileName + "/";
+
+ChromeUtils.defineLazyGetter(this, "baseURI", function () {
+ return URL + basePath;
+});
+
+const unexpected304 = "unexpected304";
+const existingCached304 = "existingCached304";
+
+function make_channel(url) {
+ return NetUtil.newChannel({
+ uri: url,
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+}
+
+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 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..983884c134
--- /dev/null
+++ b/netwerk/test/unit/test_307_redirect.js
@@ -0,0 +1,95 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+ChromeUtils.defineLazyGetter(this, "URL", function () {
+ return "http://localhost:" + httpserver.identity.primaryPort;
+});
+
+ChromeUtils.defineLazyGetter(this, "uri", function () {
+ return URL + "/redirect";
+});
+
+ChromeUtils.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);
+
+ Services.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..680973384e
--- /dev/null
+++ b/netwerk/test/unit/test_421.js
@@ -0,0 +1,65 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+ChromeUtils.defineLazyGetter(this, "URL", function () {
+ return "http://localhost:" + httpserver.identity.primaryPort;
+});
+
+var httpserver = new HttpServer();
+var testpath = "/421";
+var httpbody = "0123456789";
+var channel;
+
+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..bebd5cb39b
--- /dev/null
+++ b/netwerk/test/unit/test_MIME_params.js
@@ -0,0 +1,798 @@
+/**
+ * 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", ""],
+
+ // Bug 1784348
+ [
+ "attachment; filename=foo.exe\0.pdf",
+ Cr.NS_ERROR_ILLEGAL_VALUE,
+ Cr.NS_ERROR_INVALID_ARG,
+ ],
+ [
+ "attachment; filename=\0\0foo\0",
+ Cr.NS_ERROR_ILLEGAL_VALUE,
+ Cr.NS_ERROR_INVALID_ARG,
+ ],
+ ["attachment; filename=foo\0\0\0", "attachment", "foo"],
+ ["attachment; filename=\0\0\0", "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 {
+ let 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 (e1) {}
+ }
+ continue;
+ }
+
+ // check filename parameter
+ var expectedFn =
+ tests[i].length == 3 || whichRFC == 0 ? tests[i][2] : tests[i][4];
+
+ try {
+ let 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 (e1) {}
+ }
+ 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..624e35f30e
--- /dev/null
+++ b/netwerk/test/unit/test_NetUtil.js
@@ -0,0 +1,804 @@
+/* -*- 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.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+// 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 = Services.dirsvc.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 = Services.dirsvc.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 = Services.dirsvc.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() {
+ // Check that we get the same URI back from the IO service and the utility
+ // method.
+ const TEST_URI = "http://mozilla.org";
+ let iosURI = Services.io.newURI(TEST_URI);
+ let NetUtilURI = NetUtil.newURI(TEST_URI);
+ Assert.ok(iosURI.equals(NetUtilURI));
+
+ run_next_test();
+}
+
+function test_newURI_takes_nsIFile() {
+ // Create a test file that we can pass into NetUtil.newURI
+ let file = Services.dirsvc.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 = Services.io.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 = Services.dirsvc.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 iosChannel = Services.io.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));
diff --git a/netwerk/test/unit/test_SuperfluousAuth.js b/netwerk/test/unit/test_SuperfluousAuth.js
new file mode 100644
index 0000000000..2766b00c4c
--- /dev/null
+++ b/netwerk/test/unit/test_SuperfluousAuth.js
@@ -0,0 +1,101 @@
+/*
+
+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.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+const { MockRegistrar } = ChromeUtils.importESModule(
+ "resource://testing-common/MockRegistrar.sys.mjs"
+);
+
+var httpProtocolHandler = Cc[
+ "@mozilla.org/network/protocol;1?name=http"
+].getService(Ci.nsIHttpProtocolHandler);
+
+ChromeUtils.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/prompter;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_SUPERFLUOS_AUTH);
+ 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..548ecd5535
--- /dev/null
+++ b/netwerk/test/unit/test_URIs.js
@@ -0,0 +1,996 @@
+/* -*- 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";
+
+// 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/
+
+Services.prefs.setBoolPref("network.url.useDefaultURI", true);
+
+// 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,<html></html>",
+ scheme: "data",
+ prePath: "data:",
+ pathQueryRef: "text/html;charset=utf-8,<html></html>",
+ ref: "",
+ nsIURL: false,
+ nsINestedURI: false,
+ },
+ {
+ spec: "data:text/html;charset=utf-8,<html>\r\n\t</html>",
+ scheme: "data",
+ prePath: "data:",
+ pathQueryRef: "text/html;charset=utf-8,<html></html>",
+ 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://mozilla.org",
+ pathQueryRef: "/",
+ 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 = Services.io.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");
+
+ do_info("testing hasRef");
+ Assert.equal(URI.hasRef, !!aTest.ref, "URI.hasRef is correct");
+ do_info("testing hasUserPass");
+ Assert.equal(
+ URI.hasUserPass,
+ !!aTest.username || !!aTest.password,
+ "URI.hasUserPass is correct"
+ );
+}
+
+// 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 = Services.io.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 = Services.io.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 = Services.io.newURI("about:blank#");
+ let uri2 = Services.io.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 = Services.io.newURI("about:blank?something");
+ uri2 = Services.io.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 = Services.io.newURI("about:blank?query#ref");
+ uri2 = Services.io.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 = Services.io.newURI("view-source:http://example.com/path#");
+ uri2 = Services.io.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 = Services.io.newURI("view-source:http://example.com/path?something");
+ uri2 = Services.io.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 = Services.io.newURI("view-source:http://example.com/path?query#ref");
+ uri2 = Services.io.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 = Services.io.newURI("view-source:about:blank#");
+ uri2 = Services.io.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 = Services.io.newURI("view-source:about:blank?something");
+ uri2 = Services.io.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 = Services.io.newURI("view-source:about:blank?query#ref");
+ uri2 = Services.io.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 = Services.io.newURI("data:text/plain,hello%20world#space hash");
+ Assert.equal(uri.spec, "data:text/plain,hello%20world#space%20hash");
+ uri = Services.io.newURI("data:text/plain,hello%20world#space%20hash");
+ Assert.equal(uri.spec, "data:text/plain,hello%20world#space%20hash");
+ uri = Services.io.newURI("data:text/plain,hello world#space%20hash");
+ Assert.equal(uri.spec, "data:text/plain,hello world#space%20hash");
+ uri = Services.io.newURI("data:text/plain,hello world#space hash");
+ Assert.equal(uri.spec, "data:text/plain,hello world#space%20hash");
+ uri = Services.io.newURI("http://example.com/test path#test path");
+ uri = Services.io.newURI("http://example.com/test%20path#test%20path");
+});
+
+add_task(function check_schemeIsNull() {
+ let uri = Services.io.newURI("data:text/plain,aaa");
+ Assert.ok(!uri.schemeIs(null));
+ uri = Services.io.newURI("http://example.com");
+ Assert.ok(!uri.schemeIs(null));
+ uri = Services.io.newURI("dummyscheme://example.com");
+ Assert.ok(!uri.schemeIs(null));
+ uri = Services.io.newURI("jar:resource://gre/chrome.toolkit.jar!/");
+ Assert.ok(!uri.schemeIs(null));
+ uri = Services.io.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 = Services.io.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 = Services.io.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 = Services.io.newURI("http://example.com");
+ let uri = Services.io.newURI("tel::+371 27028456", "utf-8", base);
+ Assert.equal(uri.spec, "tel::+371 27028456");
+});
+
+add_task(function test_extra_protocols() {
+ // dweb://
+ let url = Services.io.newURI("dweb://example.com/test");
+ Assert.equal(url.host, "example.com");
+
+ // dat://
+ url = Services.io.newURI(
+ "dat://41f8a987cfeba80a037e51cc8357d513b62514de36f2f9b3d3eeec7a8fb3b5a5/"
+ );
+ Assert.equal(
+ url.host,
+ "41f8a987cfeba80a037e51cc8357d513b62514de36f2f9b3d3eeec7a8fb3b5a5"
+ );
+ url = Services.io.newURI("dat://example.com/test");
+ Assert.equal(url.host, "example.com");
+
+ // ipfs://
+ url = Services.io.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 = Services.io.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 = Services.io.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 = Services.io.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 = Services.io.newURI("http://example.org/xenia?");
+ let resolved = Services.io.newURI("?x", null, base);
+ let expected = Services.io.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 = Services.io.newURI("http://example.org/xènia?");
+ resolved = Services.io.newURI("?x", null, base);
+ expected = Services.io.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 = Services.io.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!/");
+});
+
+add_task(async function round_trip_invalid_ace_label() {
+ // This is well-formed punycode, but an invalid ACE label due to hyphens in
+ // positions 3 & 4 and trailing hyphen. (Punycode-decode yields "xn--d淾-")
+ let uri = Services.io.newURI("http://xn--xn--d--fg4n/");
+ Assert.equal(uri.spec, "http://xn--xn--d--fg4n/");
+
+ // Entirely invalid punycode will throw a MALFORMED error.
+ Assert.throws(() => {
+ uri = Services.io.newURI("http://a.b.c.XN--pokxncvks");
+ }, /NS_ERROR_MALFORMED_URI/);
+});
+
+add_task(async function test_bug1875119() {
+ let uri1 = Services.io.newURI("file:///path");
+ let uri2 = Services.io.newURI("resource://test/bla");
+ // type of uri2 is still SubstitutingURL which overrides the implementation of EnsureFile,
+ // but it's scheme is now file.
+ // See https://bugzilla.mozilla.org/show_bug.cgi?id=1876483 to disallow this
+ uri2 = uri2.mutate().setSpec("file:///path2").finalize();
+ Assert.throws(
+ () => uri1.equals(uri2),
+ /(NS_NOINTERFACE)|(NS_ERROR_FILE_UNRECOGNIZED_PATH)/,
+ "uri2 is in an invalid state and should throw"
+ );
+});
+
+add_task(async function test_bug1843717() {
+ // Make sure file path normalization on windows
+ // doesn't affect the hash of the URL.
+ let base = Services.io.newURI("file:///abc\\def/");
+ let uri = Services.io.newURI("foo\\bar#x\\y", null, base);
+ Assert.equal(uri.spec, "file:///abc/def/foo/bar#x\\y");
+ uri = Services.io.newURI("foo\\bar#xy", null, base);
+ Assert.equal(uri.spec, "file:///abc/def/foo/bar#xy");
+ uri = Services.io.newURI("foo\\bar#", null, base);
+ Assert.equal(uri.spec, "file:///abc/def/foo/bar#");
+});
diff --git a/netwerk/test/unit/test_URIs2.js b/netwerk/test/unit/test_URIs2.js
new file mode 100644
index 0000000000..3ca9706543
--- /dev/null
+++ b/netwerk/test/unit/test_URIs2.js
@@ -0,0 +1,881 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+"use strict";
+
+// 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,
+ hasQuery: true,
+ 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",
+ hasQuery: false,
+ 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 = Services.io.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);
+ }
+ if ("hasQuery" in aTest) {
+ do_info("testing hasQuery: " + aTest.hasQuery + " vs " + URI.hasQuery);
+ Assert.equal(aTest.hasQuery, URI.hasQuery);
+ }
+}
+
+// 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 = Services.io.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 = Services.io.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 = Services.io.newURI("http://example.org/xenia?");
+ let resolved = Services.io.newURI("?x", null, base);
+ let expected = Services.io.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 = Services.io.newURI("http://example.org/xènia?");
+ resolved = Services.io.newURI("?x", null, base);
+ expected = Services.io.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..e3bee4c76f
--- /dev/null
+++ b/netwerk/test/unit/test_XHR_redirects.js
@@ -0,0 +1,275 @@
+// 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.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+const { Preferences } = ChromeUtils.importESModule(
+ "resource://gre/modules/Preferences.sys.mjs"
+);
+
+var sSame;
+var sOther;
+var sRedirectPromptPref;
+
+const BUGID = "676059";
+const OTHERBUGID = "696849";
+
+ChromeUtils.defineLazyGetter(this, "pSame", function () {
+ return sSame.identity.primaryPort;
+});
+ChromeUtils.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 (let 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 (let 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..2cbae65058
--- /dev/null
+++ b/netwerk/test/unit/test_about_networking.js
@@ -0,0 +1,120 @@
+/* -*- 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.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+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,
+ 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);
+
+ gHttpServer.start(-1);
+
+ let uri = Services.io.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..7fc6d63c1e
--- /dev/null
+++ b/netwerk/test/unit/test_about_protocol.js
@@ -0,0 +1,49 @@
+/* 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(aIID) {
+ return unsafeAboutModule.QueryInterface(aIID);
+ },
+ QueryInterface: ChromeUtils.generateQI(["nsIFactory"]),
+};
+
+function run_test() {
+ let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
+ let classID = Services.uuid.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..df81419cc2
--- /dev/null
+++ b/netwerk/test/unit/test_alt-data_closeWithStatus.js
@@ -0,0 +1,184 @@
+/**
+ * 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.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+ChromeUtils.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 Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT;
+}
+
+const responseContent = "response body";
+const responseContent2 = "response body 2";
+const altContent = "!@#$%^&*()";
+const altContentType = "text/binary";
+
+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");
+
+ let etag;
+ try {
+ etag = metadata.getHeader("If-None-Match");
+ } catch (ex) {
+ etag = "";
+ }
+
+ if (etag == "test-etag1" && shouldPassRevalidation) {
+ response.setStatusLine(metadata.httpVersion, 304, "Not Modified");
+ } 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,
+ "",
+ Ci.nsICacheInfoChannel.ASYNC
+ );
+
+ 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",
+ Ci.nsICacheInfoChannel.ASYNC
+ );
+ cc.preferAlternativeDataType(
+ altContentType,
+ "text/plain",
+ Ci.nsICacheInfoChannel.ASYNC
+ );
+ cc.preferAlternativeDataType("dummy2", "", Ci.nsICacheInfoChannel.ASYNC);
+
+ 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..e46120f312
--- /dev/null
+++ b/netwerk/test/unit/test_alt-data_cross_process.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 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.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+ChromeUtils.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 Services.appinfo.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");
+
+ let etag;
+ try {
+ etag = metadata.getHeader("If-None-Match");
+ } catch (ex) {
+ 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);
+}
+
+// This file is loaded as part of test_alt-data_cross_process_wrap.js.
+// eslint-disable-next-line no-unused-vars
+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,
+ "",
+ Ci.nsICacheInfoChannel.ASYNC
+ );
+
+ 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,
+ "",
+ Ci.nsICacheInfoChannel.ASYNC
+ );
+
+ 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..3a3c1964d2
--- /dev/null
+++ b/netwerk/test/unit/test_alt-data_overwrite.js
@@ -0,0 +1,209 @@
+/**
+ * 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.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+ChromeUtils.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,
+ "",
+ Ci.nsICacheInfoChannel.ASYNC
+ );
+ }
+ 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..d648884e7a
--- /dev/null
+++ b/netwerk/test/unit/test_alt-data_simple.js
@@ -0,0 +1,212 @@
+/**
+ * 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.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+ChromeUtils.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 Services.appinfo.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");
+
+ let etag;
+ try {
+ etag = metadata.getHeader("If-None-Match");
+ } catch (ex) {
+ 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,
+ "",
+ Ci.nsICacheInfoChannel.ASYNC
+ );
+
+ 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",
+ Ci.nsICacheInfoChannel.ASYNC
+ );
+ cc.preferAlternativeDataType(
+ altContentType,
+ "text/plain",
+ Ci.nsICacheInfoChannel.ASYNC
+ );
+ cc.preferAlternativeDataType("dummy2", "", Ci.nsICacheInfoChannel.ASYNC);
+
+ 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,
+ "",
+ Ci.nsICacheInfoChannel.ASYNC
+ );
+ 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..9b6bfd050a
--- /dev/null
+++ b/netwerk/test/unit/test_alt-data_stream.js
@@ -0,0 +1,163 @@
+/**
+ * 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.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+ChromeUtils.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,
+ "",
+ Ci.nsICacheInfoChannel.ASYNC
+ );
+
+ 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,
+ "",
+ Ci.nsICacheInfoChannel.ASYNC
+ );
+
+ 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,
+ "",
+ Ci.nsICacheInfoChannel.SERIALIZE
+ );
+
+ 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) {
+ Assert.ok(!!cc.alternativeDataInputStream);
+ 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..fc9ca337fa
--- /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..ea9ac3ade0
--- /dev/null
+++ b/netwerk/test/unit/test_altsvc.js
@@ -0,0 +1,597 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+var h2Port;
+var prefs;
+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() {
+ h2Port = Services.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 = Services.prefs;
+
+ http2pref = prefs.getBoolPref("network.http.http2.enabled");
+ altsvcpref1 = prefs.getBoolPref("network.http.altsvc.enabled");
+ altsvcpref2 = prefs.getBoolPref("network.http.altsvc.oe", true);
+
+ prefs.setBoolPref("network.http.http2.enabled", 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.http2.enabled", 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 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();
+ },
+ });
+ nextTest = doTest19;
+ do_test_pending();
+ doTest();
+}
+
+// Check we don't connect to blocked ports
+function doTest19() {
+ dump("doTest19()\n");
+ origin = httpFooOrigin;
+ nextTest = testsDone;
+ otherServer = Cc["@mozilla.org/network/server-socket;1"].createInstance(
+ Ci.nsIServerSocket
+ );
+ const BAD_PORT_U32 = 6667 + 65536;
+ otherServer.init(BAD_PORT_U32, true, -1);
+ Assert.ok(otherServer.port == 6667, "Trying to listen on port 6667");
+ xaltsvc = "localhost:" + BAD_PORT_U32;
+ 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();
+ },
+ });
+ nextTest = doTest20;
+ do_test_pending();
+ doTest();
+}
+function doTest20() {
+ dump("doTest20()\n");
+ origin = httpFooOrigin;
+ nextTest = testsDone;
+ otherServer = Cc["@mozilla.org/network/server-socket;1"].createInstance(
+ Ci.nsIServerSocket
+ );
+ const BAD_PORT_U64 = 6666 + 429496729;
+ otherServer.init(6666, true, -1);
+ Assert.ok(otherServer.port == 6666, "Trying to listen on port 6666");
+ xaltsvc = "localhost:" + BAD_PORT_U64;
+ 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();
+ },
+ });
+ nextTest = doTest21;
+ do_test_pending();
+ doTest();
+}
+// Port 65535 should be OK
+function doTest21() {
+ dump("doTest21()\n");
+ origin = httpFooOrigin;
+ nextTest = testsDone;
+ otherServer = Cc["@mozilla.org/network/server-socket;1"].createInstance(
+ Ci.nsIServerSocket
+ );
+ const GOOD_PORT = 65535;
+ otherServer.init(65535, true, -1);
+ Assert.ok(otherServer.port == 65535, "Trying to listen on port 65535");
+ xaltsvc = "localhost:" + GOOD_PORT;
+ dump("Allowed port: " + otherServer.port);
+ waitFor = 500;
+ otherServer.asyncListen({
+ onSocketAccepted() {
+ Assert.ok(true, "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..c80afdf116
--- /dev/null
+++ b/netwerk/test/unit/test_altsvc_http3.js
@@ -0,0 +1,494 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+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() {
+ h3Port = Services.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.enable", 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-29=" + 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.enable");
+ 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 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-29=:port
+function doTest1() {
+ dump("doTest1()\n");
+ origin = httpFooOrigin;
+ xaltsvc = h3Route;
+ nextTest = doTest2;
+ do_test_pending();
+ doTest();
+ xaltsvc = h3FooRoute;
+}
+
+// http://foo served from h3-29=foo:port
+function doTest2() {
+ dump("doTest2()\n");
+ origin = httpFooOrigin;
+ xaltsvc = h3FooRoute;
+ nextTest = doTest3;
+ do_test_pending();
+ doTest();
+}
+
+// http://foo served from h3-29=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-29=: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-29=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-29=: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..3e6d3289f4
--- /dev/null
+++ b/netwerk/test/unit/test_altsvc_pref.js
@@ -0,0 +1,136 @@
+"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() {
+ h3Port = Services.env.get("MOZHTTP3_PORT");
+ Assert.notEqual(h3Port, null);
+ Assert.notEqual(h3Port, "");
+ h3AltSvc = ":" + h3Port;
+
+ h3Route = "foo.example.com:" + h3Port;
+ do_get_profile();
+ prefs = Services.prefs;
+
+ prefs.setBoolPref("network.http.http3.enable", 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-29=" + h3AltSvc
+ );
+
+ let chan = makeChan(httpsOrigin + "http3-test");
+ let listener = new Http3CheckListener();
+ listener.expectedRoute = h3Route;
+ chan.asyncOpen(listener);
+}
+
+function testsDone() {
+ prefs.clearUserPref("network.http.http3.enable");
+ 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..c46dbfe52b
--- /dev/null
+++ b/netwerk/test/unit/test_anonymous-coalescing.js
@@ -0,0 +1,179 @@
+/*
+- 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 http2pref;
+var extpref;
+
+function run_test() {
+ h2Port = Services.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 = Services.prefs;
+
+ http2pref = prefs.getBoolPref("network.http.http2.enabled");
+ extpref = prefs.getBoolPref("network.http.originextension");
+
+ prefs.setBoolPref("network.http.http2.enabled", 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.http2.enabled", 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..a45b651588
--- /dev/null
+++ b/netwerk/test/unit/test_auth_dialog_permission.js
@@ -0,0 +1,280 @@
+// 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.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+var prefs = Services.prefs;
+
+// 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);
+ response.setHeader("Content-Type", "text/javascript", false);
+
+ body = "success";
+ } else {
+ // didn't know guest:guest, failure
+ response.setStatusLine(metadata.httpVersion, 401, "Unauthorized");
+ response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false);
+ response.setHeader("Content-Type", "text/javascript", false);
+
+ body = "failed";
+ }
+
+ response.bodyOutputStream.write(body, body.length);
+}
+
+var httpserv = new HttpServer();
+httpserv.registerPathHandler("/auth", authHandler);
+httpserv.start(-1);
+
+ChromeUtils.defineLazyGetter(this, "URL", function () {
+ return "http://localhost:" + 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) {
+ return Services.io.newURI(url);
+}
+
+function makeChan(loadingUrl, url, contentPolicy) {
+ var uri = make_uri(loadingUrl);
+ var principal = Services.scriptSecurityManager.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..a6f1ea257c
--- /dev/null
+++ b/netwerk/test/unit/test_auth_jar.js
@@ -0,0 +1,92 @@
+"use strict";
+
+function createURI(s) {
+ return Services.io.newURI(s);
+}
+
+function run_test() {
+ // Set up a profile.
+ do_get_profile();
+
+ var secMan = Services.scriptSecurityManager;
+ 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_multiple.js b/netwerk/test/unit/test_auth_multiple.js
new file mode 100644
index 0000000000..3cf039ea7d
--- /dev/null
+++ b/netwerk/test/unit/test_auth_multiple.js
@@ -0,0 +1,464 @@
+// 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.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+// Turn off the authentication dialog blocking for this test.
+var prefs = Services.prefs;
+prefs.setIntPref("network.auth.subresource-http-auth-allow", 2);
+
+function URL(domain, path = "") {
+ if (path.startsWith("/")) {
+ path = path.substring(1);
+ }
+ return `http://${domain}:${httpserv.identity.primaryPort}/${path}`;
+}
+
+ChromeUtils.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;
+
+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 & 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) {
+ authInfo.username = this.user;
+ authInfo.password = this.pass;
+ 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);
+ },
+};
+
+function makeChan(url) {
+ let loadingUrl = Services.io
+ .newURI(url)
+ .mutate()
+ .setPathQueryRef("")
+ .finalize();
+ var principal = Services.scriptSecurityManager.createContentPrincipal(
+ 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 ntlm_auth(metadata, response) {
+ let challenge = metadata.getHeader("Authorization");
+ if (!challenge.startsWith("NTLM ")) {
+ response.setStatusLine(metadata.httpVersion, 401, "Unauthorized");
+ return;
+ }
+
+ let decoded = atob(challenge.substring(5));
+ info(decoded);
+
+ if (!decoded.startsWith("NTLMSSP\0")) {
+ response.setStatusLine(metadata.httpVersion, 401, "Unauthorized");
+ return;
+ }
+
+ let isNegotiate = decoded.substring(8).startsWith("\x01\x00\x00\x00");
+ let isAuthenticate = decoded.substring(8).startsWith("\x03\x00\x00\x00");
+
+ if (isNegotiate) {
+ response.setStatusLine(metadata.httpVersion, 401, "Unauthorized");
+ response.setHeader(
+ "WWW-Authenticate",
+ "NTLM TlRMTVNTUAACAAAAAAAAAAAoAAABggAAASNFZ4mrze8AAAAAAAAAAAAAAAAAAAAA",
+ false
+ );
+ return;
+ }
+
+ if (isAuthenticate) {
+ let body = "OK";
+ response.bodyOutputStream.write(body, body.length);
+ return;
+ }
+
+ // Something else went wrong.
+ response.setStatusLine(metadata.httpVersion, 401, "Unauthorized");
+}
+
+function basic_auth(metadata, response) {
+ let challenge = metadata.getHeader("Authorization");
+ if (!challenge.startsWith("Basic ")) {
+ response.setStatusLine(metadata.httpVersion, 401, "Unauthorized");
+ return;
+ }
+
+ if (challenge == "Basic Z3Vlc3Q6Z3Vlc3Q=") {
+ response.setStatusLine(metadata.httpVersion, 200, "OK, authorized");
+ response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false);
+
+ let body = "success";
+ response.bodyOutputStream.write(body, body.length);
+ return;
+ }
+
+ response.setStatusLine(metadata.httpVersion, 401, "Unauthorized");
+ response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false);
+}
+
+//
+// Digest functions
+//
+function bytesFromString(str) {
+ const encoder = new TextEncoder();
+ return encoder.encode(str);
+}
+
+// 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("");
+}
+
+const nonce = "6f93719059cf8d568005727f3250e798";
+const opaque = "1234opaque1234";
+const digestChallenge = `Digest realm="secret", domain="/", qop=auth,algorithm=MD5, nonce="${nonce}" opaque="${opaque}"`;
+//
+// Digest handler
+//
+// /auth/digest
+function authDigest(metadata, response) {
+ var cnonceRE = /cnonce="(\w+)"/;
+ var responseRE = /response="(\w+)"/;
+ var usernameRE = /username="(\w+)"/;
+ 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:/path";
+ 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 = "digest";
+ } else {
+ info(clientDigest);
+ info(digest);
+ handle_unauthorized(metadata, response);
+ return;
+ }
+ }
+ } else {
+ // no header, send one
+ handle_unauthorized(metadata, response);
+ return;
+ }
+
+ response.bodyOutputStream.write(body, body.length);
+}
+
+let challenges = ["NTLM", `Basic realm="secret"`, digestChallenge];
+
+function handle_unauthorized(metadata, response) {
+ response.setStatusLine(metadata.httpVersion, 401, "Unauthorized");
+
+ for (let ch of challenges) {
+ response.setHeader("WWW-Authenticate", ch, true);
+ }
+}
+
+// /path
+function auth_handler(metadata, response) {
+ if (!metadata.hasHeader("Authorization")) {
+ handle_unauthorized(metadata, response);
+ return;
+ }
+
+ let challenge = metadata.getHeader("Authorization");
+ if (challenge.startsWith("NTLM ")) {
+ ntlm_auth(metadata, response);
+ return;
+ }
+
+ if (challenge.startsWith("Basic ")) {
+ basic_auth(metadata, response);
+ return;
+ }
+
+ if (challenge.startsWith("Digest ")) {
+ authDigest(metadata, response);
+ return;
+ }
+
+ handle_unauthorized(metadata, response);
+}
+
+let httpserv;
+add_setup(() => {
+ Services.prefs.setBoolPref("network.auth.force-generic-ntlm", true);
+ Services.prefs.setBoolPref("network.auth.force-generic-ntlm-v1", true);
+ Services.prefs.setBoolPref("network.dns.native-is-localhost", true);
+ Services.prefs.setBoolPref("network.http.sanitize-headers-in-logs", false);
+
+ httpserv = new HttpServer();
+ httpserv.registerPathHandler("/path", auth_handler);
+ httpserv.start(-1);
+
+ registerCleanupFunction(async () => {
+ Services.prefs.clearUserPref("network.auth.force-generic-ntlm");
+ Services.prefs.clearUserPref("network.auth.force-generic-ntlm-v1");
+ Services.prefs.clearUserPref("network.dns.native-is-localhost");
+ Services.prefs.clearUserPref("network.http.sanitize-headers-in-logs");
+
+ await httpserv.stop();
+ });
+});
+
+add_task(async function test_ntlm_first() {
+ Services.prefs.setBoolPref(
+ "network.auth.choose_most_secure_challenge",
+ false
+ );
+ challenges = ["NTLM", `Basic realm="secret"`, digestChallenge];
+ httpserv.identity.add("http", "ntlm.com", httpserv.identity.primaryPort);
+ let chan = makeChan(URL("ntlm.com", "/path"));
+
+ chan.notificationCallbacks = new Requestor(FLAG_RETURN_FALSE, 2);
+ let [req, buf] = await new Promise(resolve => {
+ chan.asyncOpen(
+ new ChannelListener((request, buffer) => resolve([request, buffer]), null)
+ );
+ });
+ Assert.equal(buf, "OK");
+ Assert.equal(req.QueryInterface(Ci.nsIHttpChannel).responseStatus, 200);
+});
+
+add_task(async function test_basic_first() {
+ Services.prefs.setBoolPref(
+ "network.auth.choose_most_secure_challenge",
+ false
+ );
+ challenges = [`Basic realm="secret"`, "NTLM", digestChallenge];
+ httpserv.identity.add("http", "basic.com", httpserv.identity.primaryPort);
+ let chan = makeChan(URL("basic.com", "/path"));
+
+ chan.notificationCallbacks = new Requestor(FLAG_RETURN_FALSE, 2);
+ let [req, buf] = await new Promise(resolve => {
+ chan.asyncOpen(
+ new ChannelListener((request, buffer) => resolve([request, buffer]), null)
+ );
+ });
+ Assert.equal(buf, "success");
+ Assert.equal(req.QueryInterface(Ci.nsIHttpChannel).responseStatus, 200);
+});
+
+add_task(async function test_digest_first() {
+ Services.prefs.setBoolPref(
+ "network.auth.choose_most_secure_challenge",
+ false
+ );
+ challenges = [digestChallenge, `Basic realm="secret"`, "NTLM"];
+ httpserv.identity.add("http", "digest.com", httpserv.identity.primaryPort);
+ let chan = makeChan(URL("digest.com", "/path"));
+
+ chan.notificationCallbacks = new Requestor(FLAG_RETURN_FALSE, 2);
+ let [req, buf] = await new Promise(resolve => {
+ chan.asyncOpen(
+ new ChannelListener((request, buffer) => resolve([request, buffer]), null)
+ );
+ });
+ Assert.equal(req.QueryInterface(Ci.nsIHttpChannel).responseStatus, 200);
+ Assert.equal(buf, "digest");
+});
+
+add_task(async function test_choose_most_secure() {
+ // When the pref is true, we rank the challenges by how secure they are.
+ // In this case, NTLM should be the most secure.
+ Services.prefs.setBoolPref("network.auth.choose_most_secure_challenge", true);
+ challenges = [digestChallenge, `Basic realm="secret"`, "NTLM"];
+ httpserv.identity.add(
+ "http",
+ "ntlmstrong.com",
+ httpserv.identity.primaryPort
+ );
+ let chan = makeChan(URL("ntlmstrong.com", "/path"));
+
+ chan.notificationCallbacks = new Requestor(FLAG_RETURN_FALSE, 2);
+ let [req, buf] = await new Promise(resolve => {
+ chan.asyncOpen(
+ new ChannelListener((request, buffer) => resolve([request, buffer]), null)
+ );
+ });
+ Assert.equal(req.QueryInterface(Ci.nsIHttpChannel).responseStatus, 200);
+ Assert.equal(buf, "OK");
+});
diff --git a/netwerk/test/unit/test_auth_proxy.js b/netwerk/test/unit/test_auth_proxy.js
new file mode 100644
index 0000000000..d49e230922
--- /dev/null
+++ b/netwerk/test/unit/test_auth_proxy.js
@@ -0,0 +1,463 @@
+/* -*- 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.
+ *
+ * <copied from="test_authentication.js"/>
+ */
+
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+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;
+ }
+ } catch (e) {
+ do_throw(e);
+ }
+ return true;
+ },
+
+ asyncPromptAuth: function ap2_async(
+ channel,
+ callback,
+ context,
+ encryptionLevel,
+ authInfo
+ ) {
+ var me = this;
+ var allOverAndDead = false;
+ executeSoon(function () {
+ try {
+ if (allOverAndDead) {
+ throw new Error("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 new Error("can't cancel, already ran");
+ }
+ callback.onAuthAvailable(context, authInfo);
+ allOverAndDead = true;
+ });
+ },
+};
+
+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..823e2cb36b
--- /dev/null
+++ b/netwerk/test/unit/test_authentication.js
@@ -0,0 +1,1400 @@
+// 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.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+// Turn off the authentication dialog blocking for this test.
+Services.prefs.setIntPref("network.auth.subresource-http-auth-allow", 2);
+
+ChromeUtils.defineLazyGetter(this, "URL", function () {
+ return "http://localhost:" + httpserv.identity.primaryPort;
+});
+
+ChromeUtils.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;
+}
+
+var initialChannelId = -1;
+
+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);
+
+ // eslint-disable-next-line no-nested-ternary
+ 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) {
+ let self = this;
+ executeSoon(function () {
+ let ret = self.promptAuth(chan, lvl, info);
+ if (ret) {
+ cb.onAuthAvailable(ctx, info);
+ } else {
+ cb.onAuthCancelled(ctx, true);
+ }
+ });
+ },
+};
+
+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() {
+ this.promptRealm = "";
+}
+
+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) {
+ this.promptRealm = authInfo.realm;
+
+ return false;
+ },
+
+ asyncPromptAuth: function realmtest_async(chan, cb, ctx, lvl, info) {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
+ },
+};
+
+var listener = {
+ expectedCode: -1, // Uninitialized
+ nextTest: undefined,
+ expectRequestFail: false,
+ onStartRequest: function test_onStartR(request) {
+ try {
+ if (
+ !this.expectRequestFail &&
+ !Components.isSuccessCode(request.status)
+ ) {
+ do_throw("Channel should have a success code!");
+ }
+
+ if (!(request instanceof Ci.nsIHttpChannel)) {
+ do_throw("Expecting an HTTP channel");
+ }
+
+ if (
+ Services.prefs.getBoolPref("network.auth.use_redirect_for_retries") &&
+ // we should skip redirect check if we do not expect to succeed
+ this.expectedCode == 200
+ ) {
+ // ensure channel ids are initialized
+ Assert.notEqual(initialChannelId, -1);
+
+ // for each request we must use a unique channel ID.
+ // See Bug 1820807
+ var chan = request.QueryInterface(Ci.nsIIdentChannel);
+ Assert.notEqual(initialChannelId, chan.channelId);
+ }
+
+ Assert.equal(request.responseStatus, this.expectedCode);
+ // The request should be succeeded if 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);
+ initialChannelId = -1;
+ this.nextTest();
+ },
+};
+
+let ChannelEventSink1 = {
+ _classDescription: "WebRequest channel event sink",
+ _classID: Components.ID("115062f8-92f1-11e5-8b7f-08001110f7ec"),
+ _contractID: "@mozilla.org/webrequest/channel-event-sink;1",
+
+ QueryInterface: ChromeUtils.generateQI(["nsIChannelEventSink", "nsIFactory"]),
+
+ init() {
+ Components.manager
+ .QueryInterface(Ci.nsIComponentRegistrar)
+ .registerFactory(
+ this._classID,
+ this._classDescription,
+ this._contractID,
+ this
+ );
+ },
+
+ register() {
+ Services.catMan.addCategoryEntry(
+ "net-channel-event-sinks",
+ this._contractID,
+ this._contractID,
+ false,
+ true
+ );
+ },
+
+ unregister() {
+ Services.catMan.deleteCategoryEntry(
+ "net-channel-event-sinks",
+ this._contractID,
+ false
+ );
+ },
+
+ // nsIChannelEventSink implementation
+ asyncOnChannelRedirect(oldChannel, newChannel, flags, redirectCallback) {
+ // Abort the redirection
+ redirectCallback.onRedirectVerifyCallback(Cr.NS_ERROR_ABORT);
+ },
+
+ // nsIFactory implementation
+ createInstance(iid) {
+ return this.QueryInterface(iid);
+ },
+};
+
+function makeChan(
+ url,
+ loadingUrl,
+ securityFlags = Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ contentPolicyType = Ci.nsIContentPolicy.TYPE_OTHER
+) {
+ var principal = Services.scriptSecurityManager.createContentPrincipal(
+ Services.io.newURI(loadingUrl),
+ {}
+ );
+ return NetUtil.newChannel({
+ uri: url,
+ loadingPrincipal: principal,
+ securityFlags,
+ contentPolicyType,
+ });
+}
+
+var ChannelCreationObserver = {
+ QueryInterface: ChromeUtils.generateQI(["nsIObserver"]),
+ observe(aSubject, aTopic, aData) {
+ if (aTopic == "http-on-opening-request") {
+ initialChannelId = aSubject.QueryInterface(Ci.nsIIdentChannel).channelId;
+ }
+ },
+};
+
+var httpserv = null;
+
+function setup() {
+ httpserv = new HttpServer();
+
+ httpserv.registerPathHandler("/auth", authHandler);
+ httpserv.registerPathHandler(
+ "/auth/stored/wrong/credentials/",
+ authHandlerWrongStoredCredentials
+ );
+ httpserv.registerPathHandler("/auth/ntlm/simple", authNtlmSimple);
+ httpserv.registerPathHandler("/auth/realm", authRealm);
+ httpserv.registerPathHandler("/auth/non_ascii", authNonascii);
+ httpserv.registerPathHandler("/auth/digest_md5", authDigestMD5);
+ httpserv.registerPathHandler("/auth/digest_md5sess", authDigestMD5sess);
+ httpserv.registerPathHandler("/auth/digest_sha256", authDigestSHA256);
+ httpserv.registerPathHandler("/auth/digest_sha256sess", authDigestSHA256sess);
+ httpserv.registerPathHandler("/auth/digest_sha256_md5", authDigestSHA256_MD5);
+ httpserv.registerPathHandler("/auth/digest_md5_sha256", authDigestMD5_SHA256);
+ httpserv.registerPathHandler(
+ "/auth/digest_md5_sha256_oneline",
+ authDigestMD5_SHA256_oneline
+ );
+ httpserv.registerPathHandler("/auth/short_digest", authShortDigest);
+ httpserv.registerPathHandler("/largeRealm", largeRealm);
+ httpserv.registerPathHandler("/largeDomain", largeDomain);
+
+ httpserv.registerPathHandler("/corp-coep", corpAndCoep);
+
+ httpserv.start(-1);
+
+ registerCleanupFunction(async () => {
+ await httpserv.stop();
+ });
+ Services.obs.addObserver(ChannelCreationObserver, "http-on-opening-request");
+}
+setup();
+
+async function openAndListen(chan) {
+ await new Promise(resolve => {
+ listener.nextTest = resolve;
+ chan.asyncOpen(listener);
+ });
+ Cc["@mozilla.org/network/http-auth-manager;1"]
+ .getService(Ci.nsIHttpAuthManager)
+ .clearAll();
+}
+
+async function test_noauth() {
+ var chan = makeChan(URL + "/auth", URL);
+
+ listener.expectedCode = 401; // Unauthorized
+ await openAndListen(chan);
+}
+
+async function test_returnfalse1() {
+ var chan = makeChan(URL + "/auth", URL);
+
+ chan.notificationCallbacks = new Requestor(FLAG_RETURN_FALSE, 1);
+ listener.expectedCode = 401; // Unauthorized
+ await openAndListen(chan);
+}
+
+async function test_wrongpw1() {
+ var chan = makeChan(URL + "/auth", URL);
+
+ chan.notificationCallbacks = new Requestor(FLAG_WRONG_PASSWORD, 1);
+ listener.expectedCode = 200; // OK
+ await openAndListen(chan);
+}
+
+async function test_prompt1() {
+ var chan = makeChan(URL + "/auth", URL);
+
+ chan.notificationCallbacks = new Requestor(0, 1);
+ listener.expectedCode = 200; // OK
+ await openAndListen(chan);
+}
+
+async function test_prompt1CrossOrigin() {
+ var chan = makeChan(URL + "/auth", "http://example.org");
+
+ chan.notificationCallbacks = new Requestor(16, 1);
+ listener.expectedCode = 200; // OK
+ await openAndListen(chan);
+}
+
+async function test_prompt2CrossOrigin() {
+ var chan = makeChan(URL + "/auth", "http://example.org");
+
+ chan.notificationCallbacks = new Requestor(16, 2);
+ listener.expectedCode = 200; // OK
+ await openAndListen(chan);
+}
+
+async function test_returnfalse2() {
+ var chan = makeChan(URL + "/auth", URL);
+
+ chan.notificationCallbacks = new Requestor(FLAG_RETURN_FALSE, 2);
+ listener.expectedCode = 401; // Unauthorized
+ await openAndListen(chan);
+}
+
+async function test_wrongpw2() {
+ var chan = makeChan(URL + "/auth", URL);
+
+ chan.notificationCallbacks = new Requestor(FLAG_WRONG_PASSWORD, 2);
+ listener.expectedCode = 200; // OK
+ await openAndListen(chan);
+}
+
+var requestNum = 0;
+var expectedRequestNum = 0;
+async function test_wrong_stored_passwd() {
+ // tests that we don't retry auth requests for incorrect custom credentials passed during channel creation
+ requestNum = 0;
+ expectedRequestNum = 1;
+ var chan = makeChan(URL + "/auth/stored/wrong/credentials/", URL);
+ chan.nsIHttpChannel.setRequestHeader("Authorization", "wrong_cred", false);
+ chan.notificationCallbacks = new Requestor(0, 1);
+ listener.expectedCode = 401; // Unauthorized
+
+ await openAndListen(chan);
+}
+
+async function test_prompt2() {
+ var chan = makeChan(URL + "/auth", URL);
+
+ chan.notificationCallbacks = new Requestor(0, 2);
+ listener.expectedCode = 200; // OK
+ await openAndListen(chan);
+}
+
+async function test_ntlm() {
+ var chan = makeChan(URL + "/auth/ntlm/simple", URL);
+
+ chan.notificationCallbacks = new Requestor(FLAG_RETURN_FALSE, 2);
+ listener.expectedCode = 401; // Unauthorized
+ await openAndListen(chan);
+}
+
+async function test_basicrealm() {
+ var chan = makeChan(URL + "/auth/realm", URL);
+
+ let requestor = new RealmTestRequestor();
+ chan.notificationCallbacks = requestor;
+ listener.expectedCode = 401; // Unauthorized
+ await openAndListen(chan);
+ Assert.equal(requestor.promptRealm, '"foo_bar');
+}
+
+async 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
+ await openAndListen(chan);
+}
+
+async function test_digest_noauth() {
+ var chan = makeChan(URL + "/auth/digest_md5", URL);
+
+ // chan.notificationCallbacks = new Requestor(FLAG_RETURN_FALSE, 2);
+ listener.expectedCode = 401; // Unauthorized
+ await openAndListen(chan);
+}
+
+async function test_digest_md5() {
+ var chan = makeChan(URL + "/auth/digest_md5", URL);
+
+ chan.notificationCallbacks = new Requestor(0, 2);
+ listener.expectedCode = 200; // OK
+ await openAndListen(chan);
+}
+
+add_task(
+ { pref_set: [["network.auth.use_redirect_for_retries", true]] },
+ async function test_digest_md5_redirect_veto() {
+ ChannelEventSink1.init();
+ ChannelEventSink1.register();
+ var chan = makeChan(URL + "/auth/digest_md5", URL);
+
+ chan.notificationCallbacks = new Requestor(0, 1);
+ listener.expectedCode = 401; // Unauthorized
+ listener.expectRequestFail = true;
+ await openAndListen(chan);
+ ChannelEventSink1.unregister();
+ listener.expectRequestFail = false;
+ }
+);
+
+async function test_digest_md5sess() {
+ var chan = makeChan(URL + "/auth/digest_md5sess", URL);
+
+ chan.notificationCallbacks = new Requestor(0, 2);
+ listener.expectedCode = 200; // OK
+ await openAndListen(chan);
+}
+
+async function test_digest_sha256() {
+ var chan = makeChan(URL + "/auth/digest_sha256", URL);
+
+ chan.notificationCallbacks = new Requestor(0, 2);
+ listener.expectedCode = 200; // OK
+ await openAndListen(chan);
+}
+
+async function test_digest_sha256sess() {
+ var chan = makeChan(URL + "/auth/digest_sha256sess", URL);
+
+ chan.notificationCallbacks = new Requestor(0, 2);
+ listener.expectedCode = 200; // OK
+ await openAndListen(chan);
+}
+
+async function test_digest_sha256_md5() {
+ var chan = makeChan(URL + "/auth/digest_sha256_md5", URL);
+
+ chan.notificationCallbacks = new Requestor(0, 2);
+ listener.expectedCode = 200; // OK
+ await openAndListen(chan);
+}
+
+async function test_digest_md5_sha256() {
+ var chan = makeChan(URL + "/auth/digest_md5_sha256", URL);
+
+ chan.notificationCallbacks = new Requestor(0, 2);
+ listener.expectedCode = 200; // OK
+ await openAndListen(chan);
+}
+
+async function test_digest_md5_sha256_oneline() {
+ var chan = makeChan(URL + "/auth/digest_md5_sha256_oneline", URL);
+
+ chan.notificationCallbacks = new Requestor(0, 2);
+ listener.expectedCode = 200; // OK
+ await openAndListen(chan);
+}
+
+async function test_digest_bogus_user() {
+ var chan = makeChan(URL + "/auth/digest_md5", URL);
+ chan.notificationCallbacks = new Requestor(FLAG_BOGUS_USER, 2);
+ listener.expectedCode = 401; // unauthorized
+ await openAndListen(chan);
+}
+
+// Test header "WWW-Authenticate: Digest" - bug 1338876.
+async function test_short_digest() {
+ var chan = makeChan(URL + "/auth/short_digest", URL);
+ chan.notificationCallbacks = new Requestor(FLAG_NO_REALM, 2);
+ listener.expectedCode = 401; // OK
+ await openAndListen(chan);
+}
+
+// Test that COOP/COEP are processed even though asyncPromptAuth is cancelled.
+async function test_corp_coep() {
+ var chan = makeChan(
+ URL + "/corp-coep",
+ URL,
+ Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_INHERITS_SEC_CONTEXT,
+ Ci.nsIContentPolicy.TYPE_DOCUMENT
+ );
+
+ chan.notificationCallbacks = new Requestor(FLAG_RETURN_FALSE, 2);
+ listener.expectedCode = 401; // OK
+ await openAndListen(chan);
+
+ Assert.equal(
+ chan.getResponseHeader("cross-origin-embedder-policy"),
+ "require-corp"
+ );
+ Assert.equal(
+ chan.getResponseHeader("cross-origin-opener-policy"),
+ "same-origin"
+ );
+}
+
+// XXX(valentin): this makes tests fail if it's not run last. Why?
+async function test_nonascii_xhr() {
+ await new Promise(resolve => {
+ let xhr = new XMLHttpRequest();
+ xhr.open("GET", URL + "/auth/non_ascii", true, "é", "é");
+ xhr.onreadystatechange = function (event) {
+ if (xhr.readyState == 4) {
+ Assert.equal(xhr.status, 200);
+ resolve();
+ xhr.onreadystatechange = null;
+ }
+ };
+ xhr.send(null);
+ });
+}
+
+let auth_tests = [
+ test_noauth,
+ test_returnfalse1,
+ test_wrongpw1,
+ test_wrong_stored_passwd,
+ test_prompt1,
+ test_prompt1CrossOrigin,
+ test_prompt2CrossOrigin,
+ test_returnfalse2,
+ test_wrongpw2,
+ test_prompt2,
+ test_ntlm,
+ test_basicrealm,
+ test_nonascii,
+ test_digest_noauth,
+ test_digest_md5,
+ test_digest_md5sess,
+ test_digest_sha256,
+ test_digest_sha256sess,
+ test_digest_sha256_md5,
+ test_digest_md5_sha256,
+ test_digest_md5_sha256_oneline,
+ test_digest_bogus_user,
+ test_short_digest,
+ test_corp_coep,
+ test_nonascii_xhr,
+];
+
+for (let auth_test of auth_tests) {
+ add_task(
+ { pref_set: [["network.auth.use_redirect_for_retries", false]] },
+ auth_test
+ );
+}
+
+for (let auth_test of auth_tests) {
+ add_task(
+ { pref_set: [["network.auth.use_redirect_for_retries", true]] },
+ auth_test
+ );
+}
+
+// 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);
+}
+
+function authHandlerWrongStoredCredentials(metadata, response) {
+ var body;
+ if (++requestNum > expectedRequestNum) {
+ response.setStatusLine(metadata.httpVersion, 500, "");
+ } else {
+ response.setStatusLine(
+ metadata.httpVersion,
+ 401,
+ "Unauthorized" + requestNum
+ );
+ 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 = "<?xml version='1.0' ?><root>success</root>";
+ } else {
+ // didn't know é:é, failure
+ response.setStatusLine(metadata.httpVersion, 401, "Unauthorized");
+ response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false);
+
+ body = "<?xml version='1.0' ?><root>failed</root>";
+ }
+
+ response.bodyOutputStream.write(body, body.length);
+}
+
+function corpAndCoep(metadata, response) {
+ response.setStatusLine(metadata.httpVersion, 401, "Unauthorized");
+ response.setHeader("cross-origin-embedder-policy", "require-corp");
+ response.setHeader("cross-origin-opener-policy", "same-origin");
+ response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false);
+}
+
+//
+// Digest functions
+//
+function bytesFromString(str) {
+ return new TextEncoder().encode(str);
+}
+
+// return the two-digit hexadecimal code for a byte
+function toHexString(charCode) {
+ return ("0" + charCode.toString(16)).slice(-2);
+}
+
+function HMD5(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("");
+}
+
+function HSHA256(str) {
+ var data = bytesFromString(str);
+ var ch = Cc["@mozilla.org/security/hash;1"].createInstance(Ci.nsICryptoHash);
+ ch.init(Ci.nsICryptoHash.SHA256);
+ 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 authDigestMD5_helper(metadata, response, test_name) {
+ var nonce = "6f93719059cf8d568005727f3250e798";
+ var opaque = "1234opaque1234";
+ var body;
+ var send_401 = 0;
+ // check creds if we have them
+ if (metadata.hasHeader("Authorization")) {
+ var cnonceRE = /cnonce="(\w+)"/;
+ var responseRE = /response="(\w+)"/;
+ var usernameRE = /username="(\w+)"/;
+ var algorithmRE = /algorithm=([\w-]+)/;
+ var auth = metadata.getHeader("Authorization");
+ var cnonce = auth.match(cnonceRE)[1];
+ var clientDigest = auth.match(responseRE)[1];
+ var username = auth.match(usernameRE)[1];
+ var algorithm = auth.match(algorithmRE)[1];
+ var nc = "00000001";
+
+ if (username != "guest") {
+ response.setStatusLine(metadata.httpVersion, 400, "bad request");
+ body = "should never get here";
+ } else if (
+ algorithm != null &&
+ algorithm != "MD5" &&
+ algorithm != "MD5-sess"
+ ) {
+ response.setStatusLine(metadata.httpVersion, 400, "bad request");
+ body = "Algorithm must be same as provided in WWW-Authenticate header";
+ } else {
+ // see RFC2617 for the description of this calculation
+ var A1 = "guest:secret:guest";
+ if (algorithm == "MD5-sess") {
+ A1 = [HMD5(A1), nonce, cnonce].join(":");
+ }
+ var A2 = "GET:/auth/" + test_name;
+ var noncebits = [nonce, nc, cnonce, "auth", HMD5(A2)].join(":");
+ var digest = HMD5([HMD5(A1), noncebits].join(":"));
+
+ if (clientDigest == digest) {
+ response.setStatusLine(metadata.httpVersion, 200, "OK, authorized");
+ body = "success";
+ } else {
+ send_401 = 1;
+ body = "auth failed";
+ }
+ }
+ } else {
+ // no header, send one
+ send_401 = 1;
+ body = "failed, no header";
+ }
+
+ if (send_401) {
+ var authenticate_md5 =
+ 'Digest realm="secret", domain="/", qop=auth,' +
+ 'algorithm=MD5, nonce="' +
+ nonce +
+ '" opaque="' +
+ opaque +
+ '"';
+ var authenticate_md5sess =
+ 'Digest realm="secret", domain="/", qop=auth,' +
+ 'algorithm=MD5, nonce="' +
+ nonce +
+ '" opaque="' +
+ opaque +
+ '"';
+ if (test_name == "digest_md5") {
+ response.setHeader("WWW-Authenticate", authenticate_md5, false);
+ } else if (test_name == "digest_md5sess") {
+ response.setHeader("WWW-Authenticate", authenticate_md5sess, false);
+ }
+ response.setStatusLine(metadata.httpVersion, 401, "Unauthorized");
+ }
+
+ response.bodyOutputStream.write(body, body.length);
+}
+
+function authDigestMD5(metadata, response) {
+ authDigestMD5_helper(metadata, response, "digest_md5");
+}
+
+function authDigestMD5sess(metadata, response) {
+ authDigestMD5_helper(metadata, response, "digest_md5sess");
+}
+
+function authDigestSHA256_helper(metadata, response, test_name) {
+ var nonce = "6f93719059cf8d568005727f3250e798";
+ var opaque = "1234opaque1234";
+ var body;
+ var send_401 = 0;
+ // check creds if we have them
+ if (metadata.hasHeader("Authorization")) {
+ var cnonceRE = /cnonce="(\w+)"/;
+ var responseRE = /response="(\w+)"/;
+ var usernameRE = /username="(\w+)"/;
+ var algorithmRE = /algorithm=([\w-]+)/;
+ var auth = metadata.getHeader("Authorization");
+ var cnonce = auth.match(cnonceRE)[1];
+ var clientDigest = auth.match(responseRE)[1];
+ var username = auth.match(usernameRE)[1];
+ var algorithm = auth.match(algorithmRE)[1];
+ var nc = "00000001";
+
+ if (username != "guest") {
+ response.setStatusLine(metadata.httpVersion, 400, "bad request");
+ body = "should never get here";
+ } else if (algorithm != "SHA-256" && algorithm != "SHA-256-sess") {
+ response.setStatusLine(metadata.httpVersion, 400, "bad request");
+ body = "Algorithm must be same as provided in WWW-Authenticate header";
+ } else {
+ // see RFC7616 for the description of this calculation
+ var A1 = "guest:secret:guest";
+ if (algorithm == "SHA-256-sess") {
+ A1 = [HSHA256(A1), nonce, cnonce].join(":");
+ }
+ var A2 = "GET:/auth/" + test_name;
+ var noncebits = [nonce, nc, cnonce, "auth", HSHA256(A2)].join(":");
+ var digest = HSHA256([HSHA256(A1), noncebits].join(":"));
+
+ if (clientDigest == digest) {
+ response.setStatusLine(metadata.httpVersion, 200, "OK, authorized");
+ body = "success";
+ } else {
+ send_401 = 1;
+ body = "auth failed";
+ }
+ }
+ } else {
+ // no header, send one
+ send_401 = 1;
+ body = "failed, no header";
+ }
+
+ if (send_401) {
+ var authenticate_sha256 =
+ 'Digest realm="secret", domain="/", qop=auth, ' +
+ 'algorithm=SHA-256, nonce="' +
+ nonce +
+ '", opaque="' +
+ opaque +
+ '"';
+ var authenticate_sha256sess =
+ 'Digest realm="secret", domain="/", qop=auth, ' +
+ 'algorithm=SHA-256-sess, nonce="' +
+ nonce +
+ '", opaque="' +
+ opaque +
+ '"';
+ var authenticate_md5 =
+ 'Digest realm="secret", domain="/", qop=auth, ' +
+ 'algorithm=MD5, nonce="' +
+ nonce +
+ '", opaque="' +
+ opaque +
+ '"';
+ if (test_name == "digest_sha256") {
+ response.setHeader("WWW-Authenticate", authenticate_sha256, false);
+ } else if (test_name == "digest_sha256sess") {
+ response.setHeader("WWW-Authenticate", authenticate_sha256sess, false);
+ } else if (test_name == "digest_md5_sha256") {
+ response.setHeader("WWW-Authenticate", authenticate_md5, false);
+ response.setHeader("WWW-Authenticate", authenticate_sha256, true);
+ } else if (test_name == "digest_md5_sha256_oneline") {
+ response.setHeader(
+ "WWW-Authenticate",
+ authenticate_md5 + " " + authenticate_sha256,
+ false
+ );
+ } else if (test_name == "digest_sha256_md5") {
+ response.setHeader("WWW-Authenticate", authenticate_sha256, false);
+ response.setHeader("WWW-Authenticate", authenticate_md5, true);
+ }
+ response.setStatusLine(metadata.httpVersion, 401, "Unauthorized");
+ }
+
+ response.bodyOutputStream.write(body, body.length);
+}
+
+function authDigestSHA256(metadata, response) {
+ authDigestSHA256_helper(metadata, response, "digest_sha256");
+}
+
+function authDigestSHA256sess(metadata, response) {
+ authDigestSHA256_helper(metadata, response, "digest_sha256sess");
+}
+
+function authDigestSHA256_MD5(metadata, response) {
+ authDigestSHA256_helper(metadata, response, "digest_sha256_md5");
+}
+
+function authDigestMD5_SHA256(metadata, response) {
+ authDigestSHA256_helper(metadata, response, "digest_md5_sha256");
+}
+
+function authDigestMD5_SHA256_oneline(metadata, response) {
+ authDigestSHA256_helper(metadata, response, "digest_md5_sha256_oneline");
+}
+
+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) {
+ 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);
+}
+
+add_task(async function test_large_realm() {
+ var chan = makeChan(URL + "/largeRealm", URL);
+
+ listener.expectedCode = 401; // Unauthorized
+ await openAndListen(chan);
+});
+
+add_task(async function test_large_domain() {
+ var chan = makeChan(URL + "/largeDomain", URL);
+
+ listener.expectedCode = 401; // Unauthorized
+ await openAndListen(chan);
+});
+
+async function add_parse_realm_testcase(testcase) {
+ httpserv.registerPathHandler("/parse_realm", (metadata, response) => {
+ response.setStatusLine(metadata.httpVersion, 401, "Unauthorized");
+ response.setHeader("WWW-Authenticate", testcase.input, false);
+
+ let body = "failed";
+ response.bodyOutputStream.write(body, body.length);
+ });
+
+ let chan = makeChan(URL + "/parse_realm", URL);
+ let requestor = new RealmTestRequestor();
+ chan.notificationCallbacks = requestor;
+
+ listener.expectedCode = 401;
+ await openAndListen(chan);
+ Assert.equal(requestor.promptRealm, testcase.realm);
+}
+
+add_task(async function simplebasic() {
+ await add_parse_realm_testcase({
+ input: `Basic realm="foo"`,
+ scheme: `Basic`,
+ realm: `foo`,
+ });
+});
+
+add_task(async function simplebasiclf() {
+ await add_parse_realm_testcase({
+ input: `Basic\r\n realm="foo"`,
+ scheme: `Basic`,
+ realm: `foo`,
+ });
+});
+
+add_task(async function simplebasicucase() {
+ await add_parse_realm_testcase({
+ input: `BASIC REALM="foo"`,
+ scheme: `Basic`,
+ realm: `foo`,
+ });
+});
+
+add_task(async function simplebasictok() {
+ await add_parse_realm_testcase({
+ input: `Basic realm=foo`,
+ scheme: `Basic`,
+ realm: `foo`,
+ });
+});
+
+add_task(async function simplebasictokbs() {
+ await add_parse_realm_testcase({
+ input: `Basic realm=\\f\\o\\o`,
+ scheme: `Basic`,
+ realm: `\\foo`,
+ });
+});
+
+add_task(async function simplebasicsq() {
+ await add_parse_realm_testcase({
+ input: `Basic realm='foo'`,
+ scheme: `Basic`,
+ realm: `'foo'`,
+ });
+});
+
+add_task(async function simplebasicpct() {
+ await add_parse_realm_testcase({
+ input: `Basic realm="foo%20bar"`,
+ scheme: `Basic`,
+ realm: `foo%20bar`,
+ });
+});
+
+add_task(async function simplebasiccomma() {
+ await add_parse_realm_testcase({
+ input: `Basic , realm="foo"`,
+ scheme: `Basic`,
+ realm: `foo`,
+ });
+});
+
+add_task(async function simplebasiccomma2() {
+ await add_parse_realm_testcase({
+ input: `Basic, realm="foo"`,
+ scheme: `Basic`,
+ realm: ``,
+ });
+});
+
+add_task(async function simplebasicnorealm() {
+ await add_parse_realm_testcase({
+ input: `Basic`,
+ scheme: `Basic`,
+ realm: ``,
+ });
+});
+
+add_task(async function simplebasic2realms() {
+ await add_parse_realm_testcase({
+ input: `Basic realm="foo", realm="bar"`,
+ scheme: `Basic`,
+ realm: `foo`,
+ });
+});
+
+add_task(async function simplebasicwsrealm() {
+ await add_parse_realm_testcase({
+ input: `Basic realm = "foo"`,
+ scheme: `Basic`,
+ realm: `foo`,
+ });
+});
+
+add_task(async function simplebasicrealmsqc() {
+ await add_parse_realm_testcase({
+ input: `Basic realm="\\f\\o\\o"`,
+ scheme: `Basic`,
+ realm: `foo`,
+ });
+});
+
+add_task(async function simplebasicrealmsqc2() {
+ await add_parse_realm_testcase({
+ input: `Basic realm="\\"foo\\""`,
+ scheme: `Basic`,
+ realm: `"foo"`,
+ });
+});
+
+add_task(async function simplebasicnewparam1() {
+ await add_parse_realm_testcase({
+ input: `Basic realm="foo", bar="xyz",, a=b,,,c=d`,
+ scheme: `Basic`,
+ realm: `foo`,
+ });
+});
+
+add_task(async function simplebasicnewparam2() {
+ await add_parse_realm_testcase({
+ input: `Basic bar="xyz", realm="foo"`,
+ scheme: `Basic`,
+ realm: `foo`,
+ });
+});
+
+add_task(async function simplebasicrealmiso88591() {
+ await add_parse_realm_testcase({
+ input: `Basic realm="foo-ä"`,
+ scheme: `Basic`,
+ realm: `foo-ä`,
+ });
+});
+
+add_task(async function simplebasicrealmutf8() {
+ await add_parse_realm_testcase({
+ input: `Basic realm="foo-ä"`,
+ scheme: `Basic`,
+ realm: `foo-ä`,
+ });
+});
+
+add_task(async function simplebasicrealmrfc2047() {
+ await add_parse_realm_testcase({
+ input: `Basic realm="=?ISO-8859-1?Q?foo-=E4?="`,
+ scheme: `Basic`,
+ realm: `=?ISO-8859-1?Q?foo-=E4?=`,
+ });
+});
+
+add_task(async function multibasicunknown() {
+ await add_parse_realm_testcase({
+ input: `Basic realm="basic", Newauth realm="newauth"`,
+ scheme: `Basic`,
+ realm: `basic`,
+ });
+});
+
+add_task(async function multibasicunknownnoparam() {
+ await add_parse_realm_testcase({
+ input: `Basic realm="basic", Newauth`,
+ scheme: `Basic`,
+ realm: `basic`,
+ });
+});
+
+add_task(async function multibasicunknown2() {
+ await add_parse_realm_testcase({
+ input: `Newauth realm="newauth", Basic realm="basic"`,
+ scheme: `Basic`,
+ realm: `basic`,
+ });
+});
+
+add_task(async function multibasicunknown2np() {
+ await add_parse_realm_testcase({
+ input: `Newauth, Basic realm="basic"`,
+ scheme: `Basic`,
+ realm: `basic`,
+ });
+});
+
+add_task(async function multibasicunknown2mf() {
+ httpserv.registerPathHandler("/parse_realm", (metadata, response) => {
+ response.setStatusLine(metadata.httpVersion, 401, "Unauthorized");
+ response.setHeader("WWW-Authenticate", `Newauth realm="newauth"`, false);
+ response.setHeader("WWW-Authenticate", `Basic realm="basic"`, false);
+
+ let body = "failed";
+ response.bodyOutputStream.write(body, body.length);
+ });
+
+ let chan = makeChan(URL + "/parse_realm", URL);
+ let requestor = new RealmTestRequestor();
+ chan.notificationCallbacks = requestor;
+
+ listener.expectedCode = 401;
+ await openAndListen(chan);
+ Assert.equal(requestor.promptRealm, "basic");
+});
+
+add_task(async function multibasicempty() {
+ await add_parse_realm_testcase({
+ input: `,Basic realm="basic"`,
+ scheme: `Basic`,
+ realm: `basic`,
+ });
+});
+
+add_task(async function multibasicqs() {
+ await add_parse_realm_testcase({
+ input: `Newauth realm="apps", type=1, title="Login to \"apps\"", Basic realm="simple"`,
+ scheme: `Basic`,
+ realm: `simple`,
+ });
+});
+
+add_task(async function multidisgscheme() {
+ await add_parse_realm_testcase({
+ input: `Newauth realm="Newauth Realm", basic=foo, Basic realm="Basic Realm"`,
+ scheme: `Basic`,
+ realm: `Basic Realm`,
+ });
+});
+
+add_task(async function unknown() {
+ await add_parse_realm_testcase({
+ input: `Newauth param="value"`,
+ scheme: `Basic`,
+ realm: ``,
+ });
+});
+
+add_task(async function parametersnotrequired() {
+ await add_parse_realm_testcase({ input: `A, B`, scheme: `Basic`, realm: `` });
+});
+
+add_task(async function disguisedrealm() {
+ await add_parse_realm_testcase({
+ input: `Basic foo="realm=nottherealm", realm="basic"`,
+ scheme: `Basic`,
+ realm: `basic`,
+ });
+});
+
+add_task(async function disguisedrealm2() {
+ await add_parse_realm_testcase({
+ input: `Basic nottherealm="nottherealm", realm="basic"`,
+ scheme: `Basic`,
+ realm: `basic`,
+ });
+});
+
+add_task(async function missingquote() {
+ await add_parse_realm_testcase({
+ input: `Basic realm="basic`,
+ scheme: `Basic`,
+ realm: `basic`,
+ });
+});
diff --git a/netwerk/test/unit/test_authpromptwrapper.js b/netwerk/test/unit/test_authpromptwrapper.js
new file mode 100644
index 0000000000..69680354ab
--- /dev/null
+++ b/netwerk/test/unit/test_authpromptwrapper.js
@@ -0,0 +1,207 @@
+// 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 = "";
+ }
+ 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..eeceab9bf8
--- /dev/null
+++ b/netwerk/test/unit/test_backgroundfilesaver.js
@@ -0,0 +1,761 @@
+/* -*- 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.defineESModuleGetters(this, {
+ FileTestUtils: "resource://testing-common/FileTestUtils.sys.mjs",
+});
+
+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",
+};
+
+// 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(saver, aTarget) {
+ if (aOnTargetChangeFn) {
+ aOnTargetChangeFn(aTarget);
+ }
+ },
+ onSaveComplete: function BFSO_onSaveComplete(saver, 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..af8cf23976
--- /dev/null
+++ b/netwerk/test/unit/test_be_conservative.js
@@ -0,0 +1,256 @@
+// -*- 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";
+
+// Allow telemetry probes which may otherwise be disabled for some
+// applications (e.g. Thunderbird).
+Services.prefs.setBoolPref(
+ "toolkit.telemetry.testing.overrideProductsCheck",
+ true
+);
+
+// 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);
+
+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.securityCallbacks.getInterface(
+ 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);
+ certOverrideService.rememberValidityOverride(hostname, port, {}, cert, true);
+}
+
+function startClient(port, beConservative, expectSuccess) {
+ HandshakeTelemetryHelpers.resetHistograms();
+ let flavors = ["", "_FIRST_TRY"];
+ let nonflavors = [];
+ if (beConservative) {
+ flavors.push("_CONSERVATIVE");
+ nonflavors.push("_ECH");
+ nonflavors.push("_ECH_GREASE");
+ } else {
+ nonflavors.push("_CONSERVATIVE");
+ }
+
+ 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'");
+
+ // Only check telemetry if network process is disabled.
+ if (!mozinfo.socketprocess_networking) {
+ HandshakeTelemetryHelpers.checkSuccess(flavors);
+ HandshakeTelemetryHelpers.checkEmpty(nonflavors);
+ }
+
+ resolve();
+ };
+ req.onerror = () => {
+ ok(
+ !expectSuccess,
+ `should ${!expectSuccess ? "" : "not "}have gotten an error`
+ );
+
+ // Only check telemetry if network process is disabled.
+ if (!mozinfo.socketprocess_networking) {
+ // 98 is SSL_ERROR_PROTOCOL_VERSION_ALERT (see sslerr.h)
+ HandshakeTelemetryHelpers.checkEntry(flavors, 98, 1);
+ HandshakeTelemetryHelpers.checkEmpty(nonflavors);
+ }
+
+ resolve();
+ };
+
+ req.send();
+ });
+}
+
+add_task(async function () {
+ Services.prefs.setIntPref("security.tls.version.max", 4);
+ Services.prefs.setCharPref("network.dns.localDomains", hostname);
+ Services.prefs.setIntPref("network.http.speculative-parallel-limit", 0);
+ let cert = getTestServerCertificate();
+
+ // 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");
+ Services.prefs.clearUserPref("network.http.speculative-parallel-limit");
+});
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..eae3042592
--- /dev/null
+++ b/netwerk/test/unit/test_be_conservative_error_handling.js
@@ -0,0 +1,216 @@
+// -*- 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);
+
+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.securityCallbacks.getInterface(
+ 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);
+ certOverrideService.rememberValidityOverride(hostname, port, {}, cert, 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 = getTestServerCertificate();
+
+ // 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_bhttp.js b/netwerk/test/unit/test_bhttp.js
new file mode 100644
index 0000000000..61c8066a79
--- /dev/null
+++ b/netwerk/test/unit/test_bhttp.js
@@ -0,0 +1,220 @@
+/* Any copyright is dedicated to the Public Domain.
+ * https://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Unit tests for the binary http bindings.
+// Tests basic encoding and decoding of requests and responses.
+
+function BinaryHttpRequest(
+ method,
+ scheme,
+ authority,
+ path,
+ headerNames,
+ headerValues,
+ content
+) {
+ this.method = method;
+ this.scheme = scheme;
+ this.authority = authority;
+ this.path = path;
+ this.headerNames = headerNames;
+ this.headerValues = headerValues;
+ this.content = content;
+}
+
+BinaryHttpRequest.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["nsIBinaryHttpRequest"]),
+};
+
+function test_encode_request() {
+ let bhttp = Cc["@mozilla.org/network/binary-http;1"].getService(
+ Ci.nsIBinaryHttp
+ );
+ let request = new BinaryHttpRequest(
+ "GET",
+ "https",
+ "",
+ "/hello.txt",
+ ["user-agent", "host", "accept-language"],
+ [
+ "curl/7.16.3 libcurl/7.16.3 OpenSSL/0.9.7l zlib/1.2.3",
+ "www.example.com",
+ "en, mi",
+ ],
+ []
+ );
+ let encoded = bhttp.encodeRequest(request);
+ // This example is from RFC 9292.
+ let expected = hexStringToBytes(
+ "0003474554056874747073000a2f6865" +
+ "6c6c6f2e747874406c0a757365722d61" +
+ "67656e74346375726c2f372e31362e33" +
+ "206c69626375726c2f372e31362e3320" +
+ "4f70656e53534c2f302e392e376c207a" +
+ "6c69622f312e322e3304686f73740f77" +
+ "77772e6578616d706c652e636f6d0f61" +
+ "63636570742d6c616e67756167650665" +
+ "6e2c206d690000"
+ );
+ deepEqual(encoded, expected);
+
+ let mismatchedHeaders = new BinaryHttpRequest(
+ "GET",
+ "https",
+ "",
+ "",
+ ["whoops-only-one-header-name"],
+ ["some-header-value", "some-other-header-value"],
+ []
+ );
+ // The implementation uses "NS_ERROR_INVALID_ARG", because that's an
+ // appropriate description for the error. However, that is an alias to
+ // "NS_ERROR_ILLEGAL_VALUE", which is what the actual exception uses, so
+ // that's what is tested for here.
+ Assert.throws(
+ () => bhttp.encodeRequest(mismatchedHeaders),
+ /NS_ERROR_ILLEGAL_VALUE/
+ );
+}
+
+function test_decode_request() {
+ let bhttp = Cc["@mozilla.org/network/binary-http;1"].getService(
+ Ci.nsIBinaryHttp
+ );
+
+ // From RFC 9292.
+ let encoded = hexStringToBytes(
+ "0003474554056874747073000a2f6865" +
+ "6c6c6f2e747874406c0a757365722d61" +
+ "67656e74346375726c2f372e31362e33" +
+ "206c69626375726c2f372e31362e3320" +
+ "4f70656e53534c2f302e392e376c207a" +
+ "6c69622f312e322e3304686f73740f77" +
+ "77772e6578616d706c652e636f6d0f61" +
+ "63636570742d6c616e67756167650665" +
+ "6e2c206d690000"
+ );
+ let request = bhttp.decodeRequest(encoded);
+ equal(request.method, "GET");
+ equal(request.scheme, "https");
+ equal(request.authority, "");
+ equal(request.path, "/hello.txt");
+ let expectedHeaderNames = ["user-agent", "host", "accept-language"];
+ deepEqual(request.headerNames, expectedHeaderNames);
+ let expectedHeaderValues = [
+ "curl/7.16.3 libcurl/7.16.3 OpenSSL/0.9.7l zlib/1.2.3",
+ "www.example.com",
+ "en, mi",
+ ];
+ deepEqual(request.headerValues, expectedHeaderValues);
+ deepEqual(request.content, []);
+
+ let garbage = hexStringToBytes("115f00ab64c0fa783fe4cb723eaa87fa78900a0b00");
+ Assert.throws(() => bhttp.decodeRequest(garbage), /NS_ERROR_UNEXPECTED/);
+}
+
+function test_decode_response() {
+ let bhttp = Cc["@mozilla.org/network/binary-http;1"].getService(
+ Ci.nsIBinaryHttp
+ );
+ // From RFC 9292.
+ let encoded = hexStringToBytes(
+ "0340660772756e6e696e670a22736c65" +
+ "657020313522004067046c696e6b233c" +
+ "2f7374796c652e6373733e3b2072656c" +
+ "3d7072656c6f61643b2061733d737479" +
+ "6c65046c696e6b243c2f736372697074" +
+ "2e6a733e3b2072656c3d7072656c6f61" +
+ "643b2061733d7363726970740040c804" +
+ "646174651d4d6f6e2c203237204a756c" +
+ "20323030392031323a32383a35332047" +
+ "4d540673657276657206417061636865" +
+ "0d6c6173742d6d6f6469666965641d57" +
+ "65642c203232204a756c203230303920" +
+ "31393a31353a353620474d5404657461" +
+ "671422333461613338372d642d313536" +
+ "3865623030220d6163636570742d7261" +
+ "6e6765730562797465730e636f6e7465" +
+ "6e742d6c656e67746802353104766172" +
+ "790f4163636570742d456e636f64696e" +
+ "670c636f6e74656e742d747970650a74" +
+ "6578742f706c61696e003348656c6c6f" +
+ "20576f726c6421204d7920636f6e7465" +
+ "6e7420696e636c756465732061207472" +
+ "61696c696e672043524c462e0d0a0000"
+ );
+ let response = bhttp.decodeResponse(encoded);
+ equal(response.status, 200);
+ deepEqual(
+ response.content,
+ stringToBytes("Hello World! My content includes a trailing CRLF.\r\n")
+ );
+ let expectedHeaderNames = [
+ "date",
+ "server",
+ "last-modified",
+ "etag",
+ "accept-ranges",
+ "content-length",
+ "vary",
+ "content-type",
+ ];
+ deepEqual(response.headerNames, expectedHeaderNames);
+ let expectedHeaderValues = [
+ "Mon, 27 Jul 2009 12:28:53 GMT",
+ "Apache",
+ "Wed, 22 Jul 2009 19:15:56 GMT",
+ '"34aa387-d-1568eb00"',
+ "bytes",
+ "51",
+ "Accept-Encoding",
+ "text/plain",
+ ];
+ deepEqual(response.headerValues, expectedHeaderValues);
+
+ let garbage = hexStringToBytes(
+ "0367890084cb0ab03115fa0b4c2ea0fa783f7a87fa00"
+ );
+ Assert.throws(() => bhttp.decodeResponse(garbage), /NS_ERROR_UNEXPECTED/);
+}
+
+function test_encode_response() {
+ let response = new BinaryHttpResponse(
+ 418,
+ ["content-type"],
+ ["text/plain"],
+ stringToBytes("I'm a teapot")
+ );
+ let bhttp = Cc["@mozilla.org/network/binary-http;1"].getService(
+ Ci.nsIBinaryHttp
+ );
+ let encoded = bhttp.encodeResponse(response);
+ let expected = hexStringToBytes(
+ "0141a2180c636f6e74656e742d747970650a746578742f706c61696e0c49276d206120746561706f7400"
+ );
+ deepEqual(encoded, expected);
+
+ let mismatchedHeaders = new BinaryHttpResponse(
+ 500,
+ ["some-header", "some-other-header"],
+ ["whoops-only-one-header-value"],
+ []
+ );
+ // The implementation uses "NS_ERROR_INVALID_ARG", because that's an
+ // appropriate description for the error. However, that is an alias to
+ // "NS_ERROR_ILLEGAL_VALUE", which is what the actual exception uses, so
+ // that's what is tested for here.
+ Assert.throws(
+ () => bhttp.encodeResponse(mismatchedHeaders),
+ /NS_ERROR_ILLEGAL_VALUE/
+ );
+}
+
+function run_test() {
+ test_encode_request();
+ test_decode_request();
+ test_encode_response();
+ test_decode_response();
+}
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_decoding.js b/netwerk/test/unit/test_brotli_decoding.js
new file mode 100644
index 0000000000..55e2869401
--- /dev/null
+++ b/netwerk/test/unit/test_brotli_decoding.js
@@ -0,0 +1,128 @@
+/* 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 */
+/* import-globals-from head_servers.js */
+
+let endChunk2ReceivedInTime = false;
+
+function makeChan(uri) {
+ let chan = NetUtil.newChannel({
+ uri,
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+ chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI;
+ return chan;
+}
+
+let channelListener = function (closure) {
+ this._closure = closure;
+ this._start = Date.now();
+};
+
+channelListener.prototype = {
+ onStartRequest: function testOnStartRequest(request) {},
+
+ onDataAvailable: function testOnDataAvailable(request, stream, off, cnt) {
+ let data = read_stream(stream, cnt);
+ let current = Date.now();
+ let elapsed = current - this._start;
+ dump("data:" + data.slice(-10) + "\n");
+ dump("elapsed=" + elapsed + "\n");
+ if (elapsed < 2500 && data[data.length - 1] == "E") {
+ endChunk2ReceivedInTime = true;
+ }
+ },
+
+ onStopRequest: function testOnStopRequest(request, status) {
+ this._closure();
+ },
+};
+
+add_task(async function test_http2() {
+ let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(
+ Ci.nsIX509CertDB
+ );
+ addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u");
+
+ let server = new NodeHTTP2Server();
+ await server.start();
+ registerCleanupFunction(async () => {
+ await server.stop();
+ });
+ let chan = makeChan(`https://localhost:${server.port()}/test`);
+ let req = await new Promise(resolve => {
+ chan.asyncOpen(new ChannelListener(resolve, null, CL_ALLOW_UNKNOWN_CL));
+ });
+ equal(req.status, Cr.NS_OK);
+ equal(req.QueryInterface(Ci.nsIHttpChannel).responseStatus, 404);
+ await server.registerPathHandler("/test", (req1, resp) => {
+ resp.writeHead(200, {
+ "content-type": "text/html; charset=utf-8",
+ "content-encoding": "br",
+ });
+ resp.write(
+ Buffer.from([
+ 0x8b, 0x2a, 0x80, 0x3c, 0x64, 0x69, 0x76, 0x3e, 0x73, 0x74, 0x61, 0x72,
+ 0x74, 0x3c, 0x2f, 0x64, 0x69, 0x76, 0x3e, 0x3c, 0x64, 0x69, 0x76, 0x20,
+ 0x73, 0x74, 0x79, 0x6c, 0x65, 0x3d, 0x22, 0x74, 0x65, 0x78, 0x74, 0x2d,
+ 0x6f, 0x76, 0x65, 0x72, 0x66, 0x6c, 0x6f, 0x77, 0x3a, 0x20, 0x65, 0x6c,
+ 0x6c, 0x69, 0x70, 0x73, 0x69, 0x73, 0x3b, 0x6f, 0x76, 0x65, 0x72, 0x66,
+ 0x6c, 0x6f, 0x77, 0x3a, 0x20, 0x68, 0x69, 0x64, 0x64, 0x65, 0x6e, 0x3b,
+ 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x3a, 0x20, 0x72,
+ 0x74, 0x6c, 0x3b, 0x22, 0x3e,
+ ])
+ );
+
+ // This function is handled within the httpserver where setTimeout is
+ // available.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout, no-undef
+ setTimeout(function () {
+ resp.write(
+ Buffer.from([
+ 0xfa, 0xff, 0x0b, 0x00, 0x80, 0xaa, 0xaa, 0xaa, 0xea, 0x3f, 0x72,
+ 0x59, 0xd6, 0x05, 0x73, 0x5b, 0xb6, 0x75, 0xea, 0xe6, 0xfd, 0xa8,
+ 0x54, 0xc7, 0x62, 0xd8, 0x18, 0x86, 0x61, 0x18, 0x86, 0x63, 0xa9,
+ 0x86, 0x61, 0x18, 0x86, 0xe1, 0x63, 0x8e, 0x63, 0xa9, 0x86, 0x61,
+ 0x18, 0x86, 0x61, 0xd8, 0xe0, 0xbc, 0x85, 0x48, 0x1f, 0xa0, 0x05,
+ 0xda, 0x6f, 0xef, 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0xaa, 0xaa,
+ 0xaa, 0xaa, 0xff, 0xc8, 0x65, 0x59, 0x17, 0xcc, 0x6d, 0xd9, 0xd6,
+ 0xa9, 0x9b, 0xf7, 0xff, 0x0d, 0xd5, 0xb1, 0x18, 0xe6, 0x63, 0x18,
+ 0x86, 0x61, 0x18, 0x8e, 0xa5, 0x1a, 0x86, 0x61, 0x18, 0x86, 0x61,
+ 0x8e, 0xed, 0x57, 0x0d, 0xc3, 0x30, 0x0c, 0xc3, 0xb0, 0xc1, 0x79,
+ 0x0b, 0x91, 0x3e, 0x40, 0x6c, 0x1c, 0x00, 0x90, 0xd6, 0x3b, 0x00,
+ 0x50, 0x96, 0x31, 0x53, 0xe6, 0x2c, 0x59, 0xb3, 0x65, 0xcf, 0x91,
+ 0x33, 0x57, 0x31, 0x03,
+ ])
+ );
+ }, 100);
+
+ // This function is handled within the httpserver where setTimeout is
+ // available.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout, no-undef
+ setTimeout(function () {
+ resp.end(
+ Buffer.from([
+ 0x98, 0x00, 0x08, 0x3c, 0x2f, 0x64, 0x69, 0x76, 0x3e, 0x3c, 0x64,
+ 0x69, 0x76, 0x3e, 0x65, 0x6e, 0x64, 0x3c, 0x2f, 0x64, 0x69, 0x76,
+ 0x3e, 0x03,
+ ])
+ );
+ }, 2500);
+ });
+ chan = makeChan(`https://localhost:${server.port()}/test`);
+ await new Promise(resolve => {
+ chan.asyncOpen(new channelListener(() => resolve()));
+ });
+
+ equal(
+ endChunk2ReceivedInTime,
+ true,
+ "End of chunk 2 not received before chunk 3 was sent"
+ );
+});
diff --git a/netwerk/test/unit/test_brotli_http.js b/netwerk/test/unit/test_brotli_http.js
new file mode 100644
index 0000000000..1207c95012
--- /dev/null
+++ b/netwerk/test/unit/test_brotli_http.js
@@ -0,0 +1,122 @@
+// 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.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+ChromeUtils.defineLazyGetter(this, "URL", function () {
+ return "http://localhost:" + httpServer.identity.primaryPort + "/content";
+});
+
+var httpServer = null;
+
+add_task(async function check_brotli() {
+ httpServer = new HttpServer();
+ httpServer.registerPathHandler("/content", contentHandler);
+ httpServer.start(-1);
+
+ async function test() {
+ let chan = NetUtil.newChannel({ uri: URL, loadUsingSystemPrincipal: true });
+ let [, buff] = await new Promise(resolve => {
+ chan.asyncOpen(
+ new ChannelListener(
+ (req, buff1) => {
+ resolve([req, buff1]);
+ },
+ null,
+ CL_IGNORE_CL
+ )
+ );
+ });
+ return buff;
+ }
+
+ Services.prefs.setBoolPref(
+ "network.http.encoding.trustworthy_is_https",
+ true
+ );
+ equal(
+ await test(),
+ "hello",
+ "Should decode brotli when trustworthy_is_https=true"
+ );
+ Services.prefs.setBoolPref(
+ "network.http.encoding.trustworthy_is_https",
+ false
+ );
+ equal(
+ await test(),
+ "\x0b\x02\x80hello\x03",
+ "Should not decode brotli when trustworthy_is_https=false"
+ );
+ Services.prefs.setCharPref(
+ "network.http.accept-encoding",
+ "gzip, deflate, br"
+ );
+ equal(
+ await test(),
+ "hello",
+ "Should decode brotli if we set the HTTP accept encoding to include brotli"
+ );
+ Services.prefs.clearUserPref("network.http.accept-encoding");
+ Services.prefs.clearUserPref("network.http.encoding.trustworthy_is_https");
+ await httpServer.stop();
+});
+
+// Make sure we still decode brotli on HTTPS
+// Node server doesn't work on Android yet.
+add_task(
+ { skip_if: () => AppConstants.platform == "android" },
+ async function check_https() {
+ Services.prefs.setBoolPref(
+ "network.http.encoding.trustworthy_is_https",
+ true
+ );
+ let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(
+ Ci.nsIX509CertDB
+ );
+ addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u");
+
+ let server = new NodeHTTPSServer();
+ await server.start();
+ registerCleanupFunction(async () => {
+ await server.stop();
+ });
+ await server.registerPathHandler("/brotli", (req, resp) => {
+ resp.setHeader("Content-Type", "text/plain");
+ resp.setHeader("Content-Encoding", "br");
+ let output = "\x0b\x02\x80hello\x03";
+ resp.writeHead(200);
+ resp.end(output, "binary");
+ });
+ equal(
+ Services.prefs.getCharPref("network.http.accept-encoding.secure"),
+ "gzip, deflate, br"
+ );
+ let { req, buff } = await new Promise(resolve => {
+ let chan = NetUtil.newChannel({
+ uri: `${server.origin()}/brotli`,
+ loadUsingSystemPrincipal: true,
+ });
+ chan.asyncOpen(
+ new ChannelListener(
+ (req1, buff1) => resolve({ req: req1, buff: buff1 }),
+ null,
+ CL_ALLOW_UNKNOWN_CL
+ )
+ );
+ });
+ equal(req.status, Cr.NS_OK);
+ equal(req.QueryInterface(Ci.nsIHttpChannel).responseStatus, 200);
+ equal(buff, "hello");
+ }
+);
diff --git a/netwerk/test/unit/test_brotli_unknown_content_type.js b/netwerk/test/unit/test_brotli_unknown_content_type.js
new file mode 100644
index 0000000000..7df0d4f847
--- /dev/null
+++ b/netwerk/test/unit/test_brotli_unknown_content_type.js
@@ -0,0 +1,74 @@
+/* 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 file makes sure that we can decode brotli files when the
+// Content-Type header is missing (Bug 1715401)
+
+"use strict";
+
+function emptyBrotli(metadata, response) {
+ response.setHeader("Content-Encoding", "br", false);
+ response.write("\x01\x03\x06\x03");
+}
+
+function largeEmptyBrotli(metadata, response) {
+ response.setHeader("Content-Encoding", "br", false);
+ response.write("\x01\x03" + "\x06".repeat(600) + "\x03");
+}
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+ChromeUtils.defineLazyGetter(this, "URL_EMPTY_BROTLI", function () {
+ return (
+ "http://localhost:" + httpServer.identity.primaryPort + "/empty-brotli"
+ );
+});
+
+ChromeUtils.defineLazyGetter(this, "URL_LARGE_EMPTY_BROTLI", function () {
+ return (
+ "http://localhost:" +
+ httpServer.identity.primaryPort +
+ "/large-empty-brotli"
+ );
+});
+
+var httpServer = null;
+
+add_task(async function check_brotli() {
+ httpServer = new HttpServer();
+ httpServer.registerPathHandler("/empty-brotli", emptyBrotli);
+ httpServer.registerPathHandler("/large-empty-brotli", largeEmptyBrotli);
+ httpServer.start(-1);
+
+ async function test(url) {
+ let chan = NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true });
+ let [, response] = await new Promise(resolve => {
+ chan.asyncOpen(
+ new ChannelListener(
+ (req, buff) => {
+ resolve([req, buff]);
+ },
+ null,
+ CL_IGNORE_CL
+ )
+ );
+ });
+ return response;
+ }
+ equal(
+ await test(URL_EMPTY_BROTLI),
+ "",
+ "Should decode brotli even when brotli output is empty"
+ );
+ equal(
+ await test(URL_LARGE_EMPTY_BROTLI),
+ "",
+ "Should decode brotli even when the nsUnknownDecoder can't get any decoded output"
+ );
+ Services.prefs.clearUserPref("network.http.accept-encoding");
+ Services.prefs.clearUserPref("network.http.encoding.trustworthy_is_https");
+ 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..9be44254b5
--- /dev/null
+++ b/netwerk/test/unit/test_bug1064258.js
@@ -0,0 +1,144 @@
+/**
+ * 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.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+ChromeUtils.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 new Error("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..0fc94f8a2c
--- /dev/null
+++ b/netwerk/test/unit/test_bug1177909.js
@@ -0,0 +1,251 @@
+"use strict";
+
+const { MockRegistrar } = ChromeUtils.importESModule(
+ "resource://testing-common/MockRegistrar.sys.mjs"
+);
+
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "gProxyService",
+ "@mozilla.org/network/protocol-proxy-service;1",
+ "nsIProtocolProxyService"
+);
+
+ChromeUtils.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") {
+ 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) {
+ Services.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");
+});
+
+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 chan = Services.io.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 chan = Services.io.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 chan = Services.io.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");
+});
+
+add_task(async function testProxyHttpsToHttpIsBlocked() {
+ // Ensure that regressions of bug 1702417 will be detected by the next test
+ const turnUri = Services.io.newURI("http://turn.example.com/");
+ const proxyFlags =
+ Ci.nsIProtocolProxyService.RESOLVE_PREFER_HTTPS_PROXY |
+ Ci.nsIProtocolProxyService.RESOLVE_ALWAYS_TUNNEL;
+
+ const fakeContentPrincipal =
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ "https://example.com"
+ );
+
+ const chan = Services.io.newChannelFromURIWithProxyFlags(
+ turnUri,
+ null,
+ proxyFlags,
+ null,
+ fakeContentPrincipal,
+ fakeContentPrincipal,
+ Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ Ci.nsIContentPolicy.TYPE_OTHER
+ );
+
+ const 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");
+
+ const csm = Cc["@mozilla.org/contentsecuritymanager;1"].getService(
+ Ci.nsIContentSecurityManager
+ );
+
+ try {
+ csm.performSecurityCheck(chan, null);
+ Assert.ok(
+ false,
+ "performSecurityCheck should fail (due to mixed content blocking)"
+ );
+ } catch (e) {
+ Assert.equal(
+ e.result,
+ Cr.NS_ERROR_CONTENT_BLOCKED,
+ "performSecurityCheck should throw NS_ERROR_CONTENT_BLOCKED"
+ );
+ }
+});
+
+add_task(async function testProxyHttpsToTurnTcpWorks() {
+ // Test for bug 1702417
+ const turnUri = Services.io.newURI("http://turn.example.com/");
+ const proxyFlags =
+ Ci.nsIProtocolProxyService.RESOLVE_PREFER_HTTPS_PROXY |
+ Ci.nsIProtocolProxyService.RESOLVE_ALWAYS_TUNNEL;
+
+ const fakeContentPrincipal =
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ "https://example.com"
+ );
+
+ const chan = Services.io.newChannelFromURIWithProxyFlags(
+ turnUri,
+ null,
+ proxyFlags,
+ null,
+ fakeContentPrincipal,
+ fakeContentPrincipal,
+ Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ // This is what allows this to avoid mixed content blocking
+ Ci.nsIContentPolicy.TYPE_PROXIED_WEBRTC_MEDIA
+ );
+
+ const 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");
+
+ const csm = Cc["@mozilla.org/contentsecuritymanager;1"].getService(
+ Ci.nsIContentSecurityManager
+ );
+
+ csm.performSecurityCheck(chan, null);
+ Assert.ok(true, "performSecurityCheck should succeed");
+});
diff --git a/netwerk/test/unit/test_bug1195415.js b/netwerk/test/unit/test_bug1195415.js
new file mode 100644
index 0000000000..eb312d27be
--- /dev/null
+++ b/netwerk/test/unit/test_bug1195415.js
@@ -0,0 +1,116 @@
+// Test for bug 1195415
+
+"use strict";
+
+function run_test() {
+ var ios = Services.io;
+ var ssm = Services.scriptSecurityManager;
+
+ // 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..48165807bf
--- /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..be73dbee14
--- /dev/null
+++ b/netwerk/test/unit/test_bug1279246.js
@@ -0,0 +1,100 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+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 = Services.prefs;
+ 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..6f100295ae
--- /dev/null
+++ b/netwerk/test/unit/test_bug1312774_http1.js
@@ -0,0 +1,149 @@
+// 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.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+var server = new HttpServer();
+server.start(-1);
+var baseURL = "http://localhost:" + server.identity.primaryPort + "/";
+var maxConnections = 0;
+var urgentRequests = 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;
+}
+
+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");
+ maxConnections = Services.prefs.getIntPref(
+ "network.http.max-persistent-connections-per-server"
+ );
+ urgentRequests = 2;
+ 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..cf820a4f56
--- /dev/null
+++ b/netwerk/test/unit/test_bug1312782_http1.js
@@ -0,0 +1,197 @@
+// 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.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+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(browserId, requestId) {
+ let uri = baseURL;
+ var chan = make_channel(uri);
+ chan.browserId = browserId;
+ 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");
+ maxConnections = Services.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.
+ Services.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 = Services.obs;
+ obsvc.notifyObservers(windowIdWrapper, "net:current-browser-id");
+}
diff --git a/netwerk/test/unit/test_bug1355539_http1.js b/netwerk/test/unit/test_bug1355539_http1.js
new file mode 100644
index 0000000000..17b2930388
--- /dev/null
+++ b/netwerk/test/unit/test_bug1355539_http1.js
@@ -0,0 +1,206 @@
+// 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.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+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");
+ maxConnections = Services.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..0e21db3478
--- /dev/null
+++ b/netwerk/test/unit/test_bug1378385_http1.js
@@ -0,0 +1,198 @@
+// 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.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+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(browserId, requestId, priority) {
+ let uri = baseURL;
+ var chan = make_channel(uri);
+ chan.browserId = browserId;
+ 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, browserId) {
+ for (var i = 0; i < responses.length; i++) {
+ var id = responses[i].getHeader("X-ID");
+ log("response id=" + id + " browserId=" + browserId);
+ Assert.equal(id, browserId);
+ }
+}
+
+var responseQueue = [];
+function setup_http_server() {
+ log("setup_http_server");
+ maxConnections = Services.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.
+ Services.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..d1df42afdc
--- /dev/null
+++ b/netwerk/test/unit/test_bug1411316_http1.js
@@ -0,0 +1,116 @@
+// 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.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+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");
+ maxConnections = Services.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..bed2bddec6
--- /dev/null
+++ b/netwerk/test/unit/test_bug1527293.js
@@ -0,0 +1,94 @@
+// 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.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+ChromeUtils.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..9b0fddf7bd
--- /dev/null
+++ b/netwerk/test/unit/test_bug1683176.js
@@ -0,0 +1,91 @@
+// Test bug 1683176
+//
+// Summary:
+// Test the case when a channel is cancelled when "negotiate" authentication
+// is processing.
+//
+
+"use strict";
+
+let prefs;
+let httpserv;
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+ChromeUtils.defineLazyGetter(this, "URL", function () {
+ return "http://localhost:" + httpserv.identity.primaryPort;
+});
+
+function makeChan(url, loadingUrl) {
+ var principal = Services.scriptSecurityManager.createContentPrincipal(
+ Services.io.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 = Services.prefs;
+
+ 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 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_bug1725766.js b/netwerk/test/unit/test_bug1725766.js
new file mode 100644
index 0000000000..7b50a7cbd4
--- /dev/null
+++ b/netwerk/test/unit/test_bug1725766.js
@@ -0,0 +1,85 @@
+/* -*- 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";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+let hserv = Cc["@mozilla.org/uriloader/handler-service;1"].getService(
+ Ci.nsIHandlerService
+);
+let handlerInfo;
+const testScheme = "x-moz-test";
+
+function setup() {
+ var handler = Cc["@mozilla.org/uriloader/web-handler-app;1"].createInstance(
+ Ci.nsIWebHandlerApp
+ );
+ handler.name = testScheme;
+ handler.uriTemplate = "http://test.mozilla.org/%s";
+
+ var extps = Cc[
+ "@mozilla.org/uriloader/external-protocol-service;1"
+ ].getService(Ci.nsIExternalProtocolService);
+ handlerInfo = extps.getProtocolHandlerInfo(testScheme);
+ handlerInfo.possibleApplicationHandlers.appendElement(handler);
+
+ hserv.store(handlerInfo);
+ Assert.ok(extps.externalProtocolHandlerExists(testScheme));
+}
+
+setup();
+registerCleanupFunction(() => {
+ hserv.remove(handlerInfo);
+});
+
+function makeChan(url) {
+ let chan = NetUtil.newChannel({
+ uri: url,
+ loadUsingSystemPrincipal: true,
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT,
+ }).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]);
+ }
+ let internal = chan.QueryInterface(Ci.nsIHttpChannelInternal);
+ internal.setWaitForHTTPSSVCRecord();
+ chan.asyncOpen(new ChannelListener(finish, null, flags));
+ });
+}
+
+add_task(async function viewsourceExternalProtocol() {
+ Assert.throws(
+ () => makeChan(`view-source:${testScheme}:foo.example.com`),
+ /NS_ERROR_MALFORMED_URI/
+ );
+});
+
+add_task(async function viewsourceExternalProtocolRedirect() {
+ let httpserv = new HttpServer();
+ httpserv.registerPathHandler("/", function handler(metadata, response) {
+ response.setStatusLine(metadata.httpVersion, 301, "Moved Permanently");
+ response.setHeader("Location", `${testScheme}:foo@bar.com`, false);
+
+ var body = "Moved\n";
+ response.bodyOutputStream.write(body, body.length);
+ });
+ httpserv.start(-1);
+
+ let chan = makeChan(
+ `view-source:http://127.0.0.1:${httpserv.identity.primaryPort}/`
+ );
+ let [req] = await channelOpenPromise(chan, CL_EXPECT_FAILURE);
+ Assert.equal(req.status, Cr.NS_ERROR_MALFORMED_URI);
+ await httpserv.stop();
+});
diff --git a/netwerk/test/unit/test_bug203271.js b/netwerk/test/unit/test_bug203271.js
new file mode 100644
index 0000000000..da43a1b4aa
--- /dev/null
+++ b/netwerk/test/unit/test_bug203271.js
@@ -0,0 +1,249 @@
+//
+// 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.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+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..d52d3b88d9
--- /dev/null
+++ b/netwerk/test/unit/test_bug248970_cache.js
@@ -0,0 +1,144 @@
+/* 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 kCacheA = "http://cache/A";
+const kCacheA2 = "http://cache/A2";
+const kCacheB = "http://cache/B";
+const kTestContent = "test content";
+
+const entries = [
+ // key content device should exist after leaving PB
+ [kCacheA, kTestContent, kMemoryDevice, true],
+ [kCacheA2, kTestContent, kDiskDevice, false],
+ [kCacheB, kTestContent, kDiskDevice, true],
+];
+
+var store_idx;
+var store_cb = 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
+ );
+}
+
+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
+ );
+}
+
+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();
+
+ // 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
+ Services.obs.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..0dfe34e8e0
--- /dev/null
+++ b/netwerk/test/unit/test_bug248970_cookie.js
@@ -0,0 +1,148 @@
+/* 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.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+var httpserver;
+
+function inChildProcess() {
+ return Services.appinfo.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
+ Services.obs.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..f58ad62e93
--- /dev/null
+++ b/netwerk/test/unit/test_bug261425.js
@@ -0,0 +1,29 @@
+"use strict";
+
+function run_test() {
+ var newURI = Services.io.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..75dc650980
--- /dev/null
+++ b/netwerk/test/unit/test_bug263127.js
@@ -0,0 +1,58 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+var server;
+const BUGID = "263127";
+
+var listener = {
+ QueryInterface: ChromeUtils.generateQI(["nsIDownloadObserver"]),
+
+ onDownloadComplete(downloader, request, 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 = Services.dirsvc.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..5e6e7a19b6
--- /dev/null
+++ b/netwerk/test/unit/test_bug282432.js
@@ -0,0 +1,37 @@
+"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();
+
+ // This file does not exist.
+ let file = do_get_file("_NOT_EXIST_.txt", true);
+ Assert.ok(!file.exists());
+ let channel = NetUtil.newChannel({
+ uri: Services.io.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..a267d6586d
--- /dev/null
+++ b/netwerk/test/unit/test_bug321706.js
@@ -0,0 +1,10 @@
+"use strict";
+
+const url = "http://foo.com/folder/file?/.";
+
+function run_test() {
+ var newURI = Services.io.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..5a389c74a4
--- /dev/null
+++ b/netwerk/test/unit/test_bug331825.js
@@ -0,0 +1,43 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+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..69a99b8765
--- /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 (let 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 (let spec of error_specs) {
+ check_resolution_error(spec);
+ }
+}
diff --git a/netwerk/test/unit/test_bug368702.js b/netwerk/test/unit/test_bug368702.js
new file mode 100644
index 0000000000..c77f40a71b
--- /dev/null
+++ b/netwerk/test/unit/test_bug368702.js
@@ -0,0 +1,147 @@
+"use strict";
+
+function run_test() {
+ var tld = Services.eTLD;
+ 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 uri = Services.io.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..36cf2472e1
--- /dev/null
+++ b/netwerk/test/unit/test_bug369787.js
@@ -0,0 +1,73 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+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..999a6c00cf
--- /dev/null
+++ b/netwerk/test/unit/test_bug371473.js
@@ -0,0 +1,36 @@
+"use strict";
+
+function test_not_too_long() {
+ var spec = "jar:http://example.com/bar.jar!/";
+ try {
+ Services.io.newURI(spec);
+ } catch (e) {
+ do_throw("newURI threw even though it wasn't passed a large nested URI?");
+ }
+}
+
+function test_too_long() {
+ 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.
+ Services.io.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..5184a30e54
--- /dev/null
+++ b/netwerk/test/unit/test_bug376844.js
@@ -0,0 +1,19 @@
+"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() {
+ for (var i = 0; i < testURLs.length; i++) {
+ var uri = Services.io.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..eb28daf51e
--- /dev/null
+++ b/netwerk/test/unit/test_bug379034.js
@@ -0,0 +1,19 @@
+"use strict";
+
+function run_test() {
+ const ios = Services.io;
+
+ 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..c46d9b29ed
--- /dev/null
+++ b/netwerk/test/unit/test_bug380994.js
@@ -0,0 +1,24 @@
+/* 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() {
+ for (var spec of specs) {
+ var uri = Services.io.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..5ded2ad2b5
--- /dev/null
+++ b/netwerk/test/unit/test_bug388281.js
@@ -0,0 +1,25 @@
+"use strict";
+
+function run_test() {
+ const ios = Services.io;
+
+ 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..cb71f549a6
--- /dev/null
+++ b/netwerk/test/unit/test_bug396389.js
@@ -0,0 +1,63 @@
+"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,
+ },
+];
+
+function run_test() {
+ var uri1 = Services.io.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 = Services.prefs;
+ for (let 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 = Services.io.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 (let 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..7cc52f3965
--- /dev/null
+++ b/netwerk/test/unit/test_bug401564.js
@@ -0,0 +1,42 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+var httpserver = null;
+const noRedirectURI = "/content";
+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);
+
+ Services.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..4584957188
--- /dev/null
+++ b/netwerk/test/unit/test_bug411952.js
@@ -0,0 +1,53 @@
+"use strict";
+
+function run_test() {
+ try {
+ var cm = Services.cookies;
+ 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..30fd2ed3fc
--- /dev/null
+++ b/netwerk/test/unit/test_bug412457.js
@@ -0,0 +1,79 @@
+"use strict";
+
+function run_test() {
+ // check if hostname is unescaped before applying IDNA
+ var newURI = Services.io.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_MALFORMED_URI/,
+ "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..78c48bbf22
--- /dev/null
+++ b/netwerk/test/unit/test_bug412945.js
@@ -0,0 +1,44 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+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..66c2bdf4bd
--- /dev/null
+++ b/netwerk/test/unit/test_bug414122.js
@@ -0,0 +1,59 @@
+"use strict";
+
+const PR_RDONLY = 0x1;
+
+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 = Services.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..33a2444c9d
--- /dev/null
+++ b/netwerk/test/unit/test_bug427957.js
@@ -0,0 +1,100 @@
+/**
+ * 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() {
+ 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..ad6c508eb6
--- /dev/null
+++ b/netwerk/test/unit/test_bug429347.js
@@ -0,0 +1,39 @@
+"use strict";
+
+function run_test() {
+ var ios = Services.io;
+
+ 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..36e000a174
--- /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 = Services.io;
+
+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..bc2f481a0e
--- /dev/null
+++ b/netwerk/test/unit/test_bug464591.js
@@ -0,0 +1,94 @@
+// 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,
+ },
+];
+
+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 = Services.prefs;
+ 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..def7041ad8
--- /dev/null
+++ b/netwerk/test/unit/test_bug468426.js
@@ -0,0 +1,131 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+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..abab980f48
--- /dev/null
+++ b/netwerk/test/unit/test_bug468594.js
@@ -0,0 +1,178 @@
+//
+// 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.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+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..009e7eee11
--- /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..c28f3da412
--- /dev/null
+++ b/netwerk/test/unit/test_bug479413.js
@@ -0,0 +1,48 @@
+/**
+ * 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() {
+ 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");
+}
diff --git a/netwerk/test/unit/test_bug479485.js b/netwerk/test/unit/test_bug479485.js
new file mode 100644
index 0000000000..30e2e0428d
--- /dev/null
+++ b/netwerk/test/unit/test_bug479485.js
@@ -0,0 +1,65 @@
+"use strict";
+
+function run_test() {
+ var ios = Services.io;
+
+ 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..ae4e848dbf
--- /dev/null
+++ b/netwerk/test/unit/test_bug482601.js
@@ -0,0 +1,264 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+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..ee2579b1bb
--- /dev/null
+++ b/netwerk/test/unit/test_bug482934.js
@@ -0,0 +1,190 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+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) {
+ return Services.io.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_bug490095.js b/netwerk/test/unit/test_bug490095.js
new file mode 100644
index 0000000000..4e0a37e450
--- /dev/null
+++ b/netwerk/test/unit/test_bug490095.js
@@ -0,0 +1,160 @@
+//
+// 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.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+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..f7e2ed2452
--- /dev/null
+++ b/netwerk/test/unit/test_bug504014.js
@@ -0,0 +1,72 @@
+"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() {
+ for (let i = 0; i < valid_URIs.length; i++) {
+ try {
+ Services.io.newURI(valid_URIs[i]);
+ } catch (e) {
+ do_throw("cannot create URI:" + valid_URIs[i]);
+ }
+ }
+
+ for (let i = 0; i < invalid_URIs.length; i++) {
+ try {
+ Services.io.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..a273cc4710
--- /dev/null
+++ b/netwerk/test/unit/test_bug510359.js
@@ -0,0 +1,104 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+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_bug526789.js b/netwerk/test/unit/test_bug526789.js
new file mode 100644
index 0000000000..ec80249a3c
--- /dev/null
+++ b/netwerk/test/unit/test_bug526789.js
@@ -0,0 +1,289 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async () => {
+ var cm = Services.cookies;
+ var expiry = (Date.now() + 1000) * 1000;
+
+ cm.removeAll();
+
+ // Allow all cookies.
+ Services.prefs.setIntPref("network.cookie.cookieBehavior", 0);
+ Services.prefs.setBoolPref("dom.security.https_first", false);
+
+ // 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 = Services.cookies;
+ return cm.cookies.length;
+}
+
+async function testDomainCookie(uriString, domain) {
+ var cm = Services.cookies;
+
+ 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 = Services.cookies;
+
+ cm.removeAll();
+
+ await CookieXPCShellUtils.setCookieToDocument(
+ uriString,
+ "foo=bar; domain=" + domain + "/"
+ );
+
+ Assert.equal(cm.countCookiesFromHost(domain), 0);
+ Assert.equal(cm.countCookiesFromHost(domain + "."), 0);
+ cm.removeAll();
+ Services.prefs.clearUserPref("dom.security.https_first");
+}
diff --git a/netwerk/test/unit/test_bug528292.js b/netwerk/test/unit/test_bug528292.js
new file mode 100644
index 0000000000..84449aff71
--- /dev/null
+++ b/netwerk/test/unit/test_bug528292.js
@@ -0,0 +1,87 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+const sentCookieVal = "foo=bar";
+const responseBody = "response body";
+
+ChromeUtils.defineLazyGetter(this, "baseURL", function () {
+ return "http://localhost:" + httpServer.identity.primaryPort;
+});
+
+const preRedirectPath = "/528292/pre-redirect";
+
+ChromeUtils.defineLazyGetter(this, "preRedirectURL", function () {
+ return baseURL + preRedirectPath;
+});
+
+const postRedirectPath = "/528292/post-redirect";
+
+ChromeUtils.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 Services.appinfo.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.
+ Services.prefs.setIntPref("network.cookie.cookieBehavior", 1);
+ Services.prefs.setBoolPref(
+ "network.cookieJarSettings.unblocked_for_testing",
+ true
+ );
+ }
+
+ // 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 = Services.io.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..7ed8ceca6f
--- /dev/null
+++ b/netwerk/test/unit/test_bug536324_64bit_content_length.js
@@ -0,0 +1,66 @@
+/* Test to ensure our 64-bit content length implementation works, at least for
+ a simple HTTP case */
+
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+// 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_bug553970.js b/netwerk/test/unit/test_bug553970.js
new file mode 100644
index 0000000000..adb45a2ee9
--- /dev/null
+++ b/netwerk/test/unit/test_bug553970.js
@@ -0,0 +1,50 @@
+"use strict";
+
+function makeURL(spec) {
+ return Services.io.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..6bdf4d59a6
--- /dev/null
+++ b/netwerk/test/unit/test_bug561042.js
@@ -0,0 +1,44 @@
+/* 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.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+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..9176b7c702
--- /dev/null
+++ b/netwerk/test/unit/test_bug561276.js
@@ -0,0 +1,66 @@
+//
+// Verify that we hit the net if we discover a cycle of redirects
+// coming from cache.
+//
+
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+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..a17f59b334
--- /dev/null
+++ b/netwerk/test/unit/test_bug580508.js
@@ -0,0 +1,31 @@
+"use strict";
+
+var ioService = Services.io;
+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..1d3ab20347
--- /dev/null
+++ b/netwerk/test/unit/test_bug586908.js
@@ -0,0 +1,103 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+const { MockRegistrar } = ChromeUtils.importESModule(
+ "resource://testing-common/MockRegistrar.sys.mjs"
+);
+
+var httpserv = null;
+
+ChromeUtils.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
+ Services.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;
+ pac(metadata, response);
+ return;
+ }
+
+ 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..55f9922e07
--- /dev/null
+++ b/netwerk/test/unit/test_bug596443.js
@@ -0,0 +1,117 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+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..4f4ea8b776
--- /dev/null
+++ b/netwerk/test/unit/test_bug618835.js
@@ -0,0 +1,124 @@
+//
+// 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.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+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..6e49751c78
--- /dev/null
+++ b/netwerk/test/unit/test_bug633743.js
@@ -0,0 +1,195 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+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) {
+ 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..2ffcb5438a
--- /dev/null
+++ b/netwerk/test/unit/test_bug650522.js
@@ -0,0 +1,35 @@
+/* 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);
+ Services.prefs.setBoolPref("dom.security.https_first", false);
+
+ var expiry = (Date.now() + 1000) * 1000;
+
+ // Test our handling of host names with a single character at the beginning
+ // followed by a dot.
+ Services.cookies.add(
+ "e.com",
+ "/",
+ "foo",
+ "bar",
+ false,
+ false,
+ true,
+ expiry,
+ {},
+ Ci.nsICookie.SAMESITE_NONE,
+ Ci.nsICookie.SCHEME_HTTP
+ );
+ Assert.equal(Services.cookies.countCookiesFromHost("e.com"), 1);
+
+ CookieXPCShellUtils.createServer({ hosts: ["e.com"] });
+ const cookies = await CookieXPCShellUtils.getCookieStringFromDocument(
+ "http://e.com/"
+ );
+ Assert.equal(cookies, "foo=bar");
+ Services.prefs.clearUserPref("dom.security.https_first");
+});
diff --git a/netwerk/test/unit/test_bug650995.js b/netwerk/test/unit/test_bug650995.js
new file mode 100644
index 0000000000..d9ccab6540
--- /dev/null
+++ b/netwerk/test/unit/test_bug650995.js
@@ -0,0 +1,187 @@
+//
+// Test that "max_entry_size" prefs for disk- and memory-cache prevents
+// caching resources with size out of bounds
+//
+
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+do_get_profile();
+
+const prefService = Services.prefs;
+
+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 () {
+ Services.cache2.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("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..0f29a3bcf4
--- /dev/null
+++ b/netwerk/test/unit/test_bug654926.js
@@ -0,0 +1,91 @@
+"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 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
+ Services.prefs.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..1ed618e04a
--- /dev/null
+++ b/netwerk/test/unit/test_bug654926_doom_and_read.js
@@ -0,0 +1,82 @@
+"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();
+
+ // 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..1bdc61b618
--- /dev/null
+++ b/netwerk/test/unit/test_bug659569.js
@@ -0,0 +1,60 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+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..2e7c060135
--- /dev/null
+++ b/netwerk/test/unit/test_bug660066.js
@@ -0,0 +1,53 @@
+/* -*- 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..79589799e7
--- /dev/null
+++ b/netwerk/test/unit/test_bug667087.js
@@ -0,0 +1,33 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async () => {
+ Services.prefs.setBoolPref("dom.security.https_first", false);
+ var expiry = (Date.now() + 1000) * 1000;
+
+ // Test our handling of host names with a single character consisting only
+ // of a single character
+ Services.cookies.add(
+ "a",
+ "/",
+ "foo",
+ "bar",
+ false,
+ false,
+ true,
+ expiry,
+ {},
+ Ci.nsICookie.SAMESITE_NONE,
+ Ci.nsICookie.SCHEME_HTTP
+ );
+ Assert.equal(Services.cookies.countCookiesFromHost("a"), 1);
+
+ CookieXPCShellUtils.createServer({ hosts: ["a"] });
+ const cookies = await CookieXPCShellUtils.getCookieStringFromDocument(
+ "http://a/"
+ );
+ Assert.equal(cookies, "foo=bar");
+ Services.prefs.clearUserPref("dom.security.https_first");
+});
diff --git a/netwerk/test/unit/test_bug667818.js b/netwerk/test/unit/test_bug667818.js
new file mode 100644
index 0000000000..9c6e495da0
--- /dev/null
+++ b/netwerk/test/unit/test_bug667818.js
@@ -0,0 +1,49 @@
+"use strict";
+
+function makeURI(str) {
+ return Services.io.newURI(str);
+}
+
+add_task(async () => {
+ // Allow all cookies.
+ Services.prefs.setIntPref("network.cookie.cookieBehavior", 0);
+ Services.prefs.setBoolPref(
+ "network.cookieJarSettings.unblocked_for_testing",
+ true
+ );
+ Services.prefs.setBoolPref("dom.security.https_first", false);
+
+ 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
+ Services.cookies.setCookieStringFromHttp(
+ uri,
+ "test2=test2; path=/; domain=example.com;",
+ channel
+ );
+
+ Assert.equal(
+ await CookieXPCShellUtils.getCookieStringFromDocument(uri.spec),
+ "test2=test2"
+ );
+ Services.prefs.clearUserPref("dom.security.https_first");
+});
diff --git a/netwerk/test/unit/test_bug667907.js b/netwerk/test/unit/test_bug667907.js
new file mode 100644
index 0000000000..5c6b13d538
--- /dev/null
+++ b/netwerk/test/unit/test_bug667907.js
@@ -0,0 +1,88 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+var httpserver = null;
+var simplePath = "/simple";
+var normalPath = "/normal";
+var httpbody = "<html></html>";
+
+ChromeUtils.defineLazyGetter(this, "uri1", function () {
+ return "http://localhost:" + httpserver.identity.primaryPort + simplePath;
+});
+
+ChromeUtils.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..ebd5998cf1
--- /dev/null
+++ b/netwerk/test/unit/test_bug669001.js
@@ -0,0 +1,178 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+var httpServer = null;
+var path = "/bug699001";
+
+ChromeUtils.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) {
+ 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) {
+ Services.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_bug770243.js b/netwerk/test/unit/test_bug770243.js
new file mode 100644
index 0000000000..60aa6a07bb
--- /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.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+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 = Services.prefs;
+ 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..8f4d1310a0
--- /dev/null
+++ b/netwerk/test/unit/test_bug812167.js
@@ -0,0 +1,143 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+/*
+- 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();
+
+ChromeUtils.defineLazyGetter(this, "randomURI1", function () {
+ return "http://localhost:" + httpserver.identity.primaryPort + randomPath1;
+});
+
+var randomPath2 = "/redirect-expires-past/" + Math.random();
+
+ChromeUtils.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 (request1, buffer1) {
+ Assert.equal(buffer1, 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..3e17df6461
--- /dev/null
+++ b/netwerk/test/unit/test_bug826063.js
@@ -0,0 +1,88 @@
+/* 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"];
+
+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..607b392473
--- /dev/null
+++ b/netwerk/test/unit/test_bug856978.js
@@ -0,0 +1,138 @@
+/* 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.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+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);
+ Services.obs.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;
+
+// Must create a RequestObserver for the test to pass, we keep it in memory
+// to avoid garbage collection.
+// eslint-disable-next-line no-unused-vars
+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..bc25731d36
--- /dev/null
+++ b/netwerk/test/unit/test_bug894586.js
@@ -0,0 +1,135 @@
+/*
+ * 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";
+ },
+ 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,
+
+ /** nsISupports */
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIProtocolHandler",
+ "nsIRequest",
+ "nsIChannel",
+ ]),
+};
+
+/**
+ * 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();
+
+ Services.io.registerProtocolHandler(
+ handler.scheme,
+ handler,
+ 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,
+ -1
+ );
+ 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 {
+ Services.io.unregisterProtocolHandler(handler.scheme);
+ }
+}
+
+// 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..f0873cba24
--- /dev/null
+++ b/netwerk/test/unit/test_cache-control_request.js
@@ -0,0 +1,448 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+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) {
+ return Services.io.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..f0011de47f
--- /dev/null
+++ b/netwerk/test/unit/test_cache-entry-id.js
@@ -0,0 +1,220 @@
+/**
+ * Test for the "CacheEntryId" under several cases.
+ */
+
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+ChromeUtils.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 ||
+ Services.appinfo.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,
+ "",
+ Ci.nsICacheInfoChannel.ASYNC
+ );
+ }
+
+ 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..0d348a81de
--- /dev/null
+++ b/netwerk/test/unit/test_cache2-00-service-get.js
@@ -0,0 +1,15 @@
+"use strict";
+
+function run_test() {
+ // Just check the contract ID alias works well.
+ try {
+ var serviceA = Services.cache2;
+ Assert.ok(serviceA);
+ var serviceB = Services.cache2;
+ 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..6b02bd0d91
--- /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 () {
+ // Open for read and check
+ asyncOpenCacheEntry(
+ "http://a/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NORMAL, "a1m", "a1d", function () {
+ // Open for rewrite (truncate), write different meta and data
+ asyncOpenCacheEntry(
+ "http://a/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_TRUNCATE,
+ null,
+ new OpenCallback(NEW, "a2m", "a2d", function () {
+ // Open for read and check
+ asyncOpenCacheEntry(
+ "http://a/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NORMAL, "a2m", "a2d", function () {
+ 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..2988651c61
--- /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 () {
+ // Open for read and check
+ asyncOpenCacheEntry(
+ "http://ro/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_READONLY,
+ null,
+ new OpenCallback(NORMAL, "a1m", "a1d", function () {
+ // Open for rewrite (truncate), write different meta and data
+ asyncOpenCacheEntry(
+ "http://ro/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_TRUNCATE,
+ null,
+ new OpenCallback(NEW, "a2m", "a2d", function () {
+ // Open for read and check
+ asyncOpenCacheEntry(
+ "http://ro/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_READONLY,
+ null,
+ new OpenCallback(NORMAL, "a2m", "a2d", function () {
+ 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..56b3b8d5f2
--- /dev/null
+++ b/netwerk/test/unit/test_cache2-01b-basic-datasize.js
@@ -0,0 +1,51 @@
+"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 (entry1) {
+ // Open for read and check
+ Assert.equal(entry1.dataSize, 3);
+ asyncOpenCacheEntry(
+ "http://a/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NORMAL, "a1m", "a1d", function (entry2) {
+ // Open for rewrite (truncate), write different meta and data
+ Assert.equal(entry2.dataSize, 3);
+ asyncOpenCacheEntry(
+ "http://a/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_TRUNCATE,
+ null,
+ new OpenCallback(NEW | WAITFORWRITE, "a2m", "a2d", function (
+ entry3
+ ) {
+ // Open for read and check
+ Assert.equal(entry3.dataSize, 3);
+ asyncOpenCacheEntry(
+ "http://a/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NORMAL, "a2m", "a2d", function (entry4) {
+ Assert.equal(entry4.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..33adb6206c
--- /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 () {
+ // Open for read and check
+ asyncOpenCacheEntry(
+ "http://mt/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NORMAL, "a1m", "", function () {
+ // Open for rewrite (truncate), write different meta and data
+ asyncOpenCacheEntry(
+ "http://mt/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_TRUNCATE,
+ null,
+ new OpenCallback(NEW, "a2m", "a2d", function () {
+ // Open for read and check
+ asyncOpenCacheEntry(
+ "http://mt/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NORMAL, "a2m", "a2d", function () {
+ 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..9d2b3f8458
--- /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 () {
+ // Open for read and check
+ asyncOpenCacheEntry(
+ "http://a/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NORMAL, "a1m", "a1d", function () {
+ // Open but don't want the entry
+ asyncOpenCacheEntry(
+ "http://a/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NOTWANTED, "a1m", "a1d", function () {
+ // 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 () {
+ 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..e77e7afdfc
--- /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 () {
+ var bypassed = false;
+
+ // Open and bypass
+ asyncOpenCacheEntry(
+ "http://a/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_BYPASS_IF_BUSY,
+ null,
+ new OpenCallback(NOTFOUND, "", "", function () {
+ 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..9d514f7cc7
--- /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, 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..2ee0efa687
--- /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 () {
+ // 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 () {
+ // Try it again normally, should go
+ asyncOpenCacheEntry(
+ "http://b/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NEW, "b1m", "b1d", function () {
+ // ...and check
+ asyncOpenCacheEntry(
+ "http://b/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NORMAL, "b1m", "b1d", function () {
+ finish_cache2_test();
+ })
+ );
+ })
+ );
+ })
+ );
+ })
+ );
+
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_cache2-02b-open-non-existing-and-doom.js b/netwerk/test/unit/test_cache2-02b-open-non-existing-and-doom.js
new file mode 100644
index 0000000000..d54aeeb9eb
--- /dev/null
+++ b/netwerk/test/unit/test_cache2-02b-open-non-existing-and-doom.js
@@ -0,0 +1,180 @@
+"use strict";
+
+add_task(async function test() {
+ do_get_profile();
+ do_test_pending();
+
+ await new Promise(resolve => {
+ // Open non-existing for read, should fail
+ asyncOpenCacheEntry(
+ "http://b/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_READONLY,
+ null,
+ new OpenCallback(NOTFOUND, null, null, function (entry) {
+ resolve(entry);
+ })
+ );
+ });
+
+ await new Promise(resolve => {
+ // 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) {
+ resolve(entry);
+ })
+ );
+ });
+
+ await new Promise(resolve => {
+ // Try it again normally, should go
+ asyncOpenCacheEntry(
+ "http://b/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NEW, "b1m", "b1d", function (entry) {
+ resolve(entry);
+ })
+ );
+ });
+
+ await new Promise(resolve => {
+ // ...and check
+ asyncOpenCacheEntry(
+ "http://b/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NORMAL, "b1m", "b1d", function (entry) {
+ resolve(entry);
+ })
+ );
+ });
+
+ Services.prefs.setBoolPref("network.cache.bug1708673", true);
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("network.cache.bug1708673");
+ });
+
+ let asyncDoomVisitor = new Promise(resolve => {
+ let doomTasks = [];
+ let visitor = {
+ onCacheStorageInfo() {},
+ async onCacheEntryInfo(
+ aURI,
+ aIdEnhance,
+ aDataSize,
+ aAltDataSize,
+ aFetchCount,
+ aLastModifiedTime,
+ aExpirationTime,
+ aPinned,
+ aInfo
+ ) {
+ doomTasks.push(
+ new Promise(resolve1 => {
+ Services.cache2
+ .diskCacheStorage(aInfo, false)
+ .asyncDoomURI(aURI, aIdEnhance, {
+ onCacheEntryDoomed() {
+ info("doomed");
+ resolve1();
+ },
+ });
+ })
+ );
+ },
+ onCacheEntryVisitCompleted() {
+ Promise.allSettled(doomTasks).then(resolve);
+ },
+ QueryInterface: ChromeUtils.generateQI(["nsICacheStorageVisitor"]),
+ };
+ Services.cache2.asyncVisitAllStorages(visitor, true);
+ });
+
+ let asyncOpenVisitor = new Promise(resolve => {
+ let openTasks = [];
+ let visitor = {
+ onCacheStorageInfo() {},
+ async onCacheEntryInfo(
+ aURI,
+ aIdEnhance,
+ aDataSize,
+ aAltDataSize,
+ aFetchCount,
+ aLastModifiedTime,
+ aExpirationTime,
+ aPinned,
+ aInfo
+ ) {
+ info(`found ${aURI.spec}`);
+ openTasks.push(
+ new Promise(r2 => {
+ Services.cache2
+ .diskCacheStorage(aInfo, false)
+ .asyncOpenURI(
+ aURI,
+ "",
+ Ci.nsICacheStorage.OPEN_READONLY |
+ Ci.nsICacheStorage.OPEN_SECRETLY,
+ {
+ onCacheEntryCheck() {
+ return Ci.nsICacheEntryOpenCallback.ENTRY_WANTED;
+ },
+ onCacheEntryAvailable(entry, isnew, status) {
+ info("opened");
+ r2();
+ },
+ QueryInterface: ChromeUtils.generateQI([
+ "nsICacheEntryOpenCallback",
+ ]),
+ }
+ );
+ })
+ );
+ },
+ onCacheEntryVisitCompleted() {
+ Promise.all(openTasks).then(resolve);
+ },
+ QueryInterface: ChromeUtils.generateQI(["nsICacheStorageVisitor"]),
+ };
+ Services.cache2.asyncVisitAllStorages(visitor, true);
+ });
+
+ await Promise.all([asyncDoomVisitor, asyncOpenVisitor]);
+
+ info("finished visiting");
+
+ await new Promise(resolve => {
+ let entryCount = 0;
+ let visitor = {
+ onCacheStorageInfo() {},
+ async onCacheEntryInfo(
+ aURI,
+ aIdEnhance,
+ aDataSize,
+ aAltDataSize,
+ aFetchCount,
+ aLastModifiedTime,
+ aExpirationTime,
+ aPinned,
+ aInfo
+ ) {
+ entryCount++;
+ },
+ onCacheEntryVisitCompleted() {
+ Assert.equal(entryCount, 0);
+ resolve();
+ },
+ QueryInterface: ChromeUtils.generateQI(["nsICacheStorageVisitor"]),
+ };
+ Services.cache2.asyncVisitAllStorages(visitor, true);
+ });
+
+ finish_cache2_test();
+});
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..2b1e230b1a
--- /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 () {
+ // Try it again, should go
+ asyncOpenCacheEntry(
+ "http://c/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NEW, "c1m", "c1d", function () {
+ // ...and check
+ asyncOpenCacheEntry(
+ "http://c/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(false, "c1m", "c1d", function () {
+ 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..aea4cfdbf3
--- /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 () {
+ // Open but let OCEA throw ones again
+ asyncOpenCacheEntry(
+ "http://d/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NEW | THROWAVAIL, null, null, function () {
+ // Try it again, should go
+ asyncOpenCacheEntry(
+ "http://d/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NEW, "d1m", "d1d", function () {
+ // ...and check
+ asyncOpenCacheEntry(
+ "http://d/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NORMAL, "d1m", "d1d", function () {
+ 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..b4ed3ea26e
--- /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 () {
+ asyncOpenCacheEntry(
+ "http://a/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NORMAL, "a1m", "a1d", function () {
+ mc.fired();
+ })
+ );
+ })
+ );
+
+ asyncOpenCacheEntry(
+ "http://b/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NEW, "b1m", "b1d", function () {
+ asyncOpenCacheEntry(
+ "http://b/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NORMAL, "b1m", "b1d", function () {
+ mc.fired();
+ })
+ );
+ })
+ );
+
+ asyncOpenCacheEntry(
+ "http://c/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NEW, "c1m", "c1d", function () {
+ asyncOpenCacheEntry(
+ "http://c/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NORMAL, "c1m", "c1d", function () {
+ mc.fired();
+ })
+ );
+ })
+ );
+
+ asyncOpenCacheEntry(
+ "http://d/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NEW, "d1m", "d1d", function () {
+ asyncOpenCacheEntry(
+ "http://d/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NORMAL, "d1m", "d1d", function () {
+ 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..ddece34276
--- /dev/null
+++ b/netwerk/test/unit/test_cache2-06-pb-mode.js
@@ -0,0 +1,50 @@
+"use strict";
+
+function exitPB() {
+ Services.obs.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 () {
+ asyncOpenCacheEntry(
+ "http://p1/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ Services.loadContextInfo.private,
+ new OpenCallback(NORMAL, "p1m", "p1d", function () {
+ // 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..11f0f6a2e7
--- /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 () {
+ asyncOpenCacheEntry(
+ "http://mem1/",
+ "memory",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NORMAL, "m1m", "m1d", function () {
+ mc.fired();
+ })
+ );
+ })
+ );
+
+ asyncOpenCacheEntry(
+ "http://a/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NEW, "a1m", "a1d", function () {
+ asyncOpenCacheEntry(
+ "http://a/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NORMAL, "a1m", "a1d", function () {
+ mc.fired();
+ })
+ );
+ })
+ );
+
+ asyncOpenCacheEntry(
+ "http://b/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NEW, "a1m", "a1d", function () {
+ asyncOpenCacheEntry(
+ "http://b/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NORMAL, "a1m", "a1d", function () {
+ mc.fired();
+ })
+ );
+ })
+ );
+
+ asyncOpenCacheEntry(
+ "http://c/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NEW, "a1m", "a1d", function () {
+ asyncOpenCacheEntry(
+ "http://c/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NORMAL, "a1m", "a1d", function () {
+ mc.fired();
+ })
+ );
+ })
+ );
+
+ asyncOpenCacheEntry(
+ "http://d/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NEW, "a1m", "a1d", function () {
+ asyncOpenCacheEntry(
+ "http://d/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NORMAL, "a1m", "a1d", function () {
+ 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..3392ee33fc
--- /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..395c41dd7c
--- /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..5b9caa9cfb
--- /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 () {
+ asyncOpenCacheEntry(
+ "http://a/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NORMAL, "a1m", "a1d", function () {
+ 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..dc278e17d4
--- /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 () {
+ 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..83728af20d
--- /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..b73bf2f5cc
--- /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 memoryStorage = getCacheStorage("memory");
+ var mc = new MultipleCallbacks(3, function () {
+ memoryStorage.asyncEvictStorage(
+ new EvictionCallback(true, function () {
+ memoryStorage.asyncVisitStorage(
+ new VisitCallback(0, 0, [], function () {
+ var diskStorage = getCacheStorage("disk");
+
+ var expectedConsumption = 2048;
+
+ diskStorage.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 () {
+ asyncOpenCacheEntry(
+ "http://mem1/",
+ "memory",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NORMAL, "m2m", "m2d", function () {
+ mc.fired();
+ })
+ );
+ })
+ );
+
+ asyncOpenCacheEntry(
+ "http://a/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NEW, "a1m", "a1d", function () {
+ asyncOpenCacheEntry(
+ "http://a/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NORMAL, "a1m", "a1d", function () {
+ mc.fired();
+ })
+ );
+ })
+ );
+
+ asyncOpenCacheEntry(
+ "http://b/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NEW, "a1m", "a1d", function () {
+ asyncOpenCacheEntry(
+ "http://b/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NORMAL, "a1m", "a1d", function () {
+ 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..6b0d31a27b
--- /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 diskStorage = getCacheStorage("disk");
+ diskStorage.asyncEvictStorage(
+ new EvictionCallback(true, function () {
+ diskStorage.asyncVisitStorage(
+ new VisitCallback(0, 0, [], function () {
+ var memoryStorage = getCacheStorage("memory");
+ memoryStorage.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 () {
+ asyncOpenCacheEntry(
+ "http://mem1/",
+ "memory",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NORMAL, "m2m", "m2d", function () {
+ mc.fired();
+ })
+ );
+ })
+ );
+
+ asyncOpenCacheEntry(
+ "http://a/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NEW, "a1m", "a1d", function () {
+ asyncOpenCacheEntry(
+ "http://a/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NORMAL, "a1m", "a1d", function () {
+ mc.fired();
+ })
+ );
+ })
+ );
+
+ asyncOpenCacheEntry(
+ "http://b/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NEW, "b1m", "b1d", function () {
+ asyncOpenCacheEntry(
+ "http://b/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NORMAL, "b1m", "b1d", function () {
+ 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..a2d40fd153
--- /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..d2e80582dc
--- /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..244dca9a3b
--- /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..7b672e69a2
--- /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 () {
+ // 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..beaa3f0dae
--- /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 () {
+ asyncOpenCacheEntry(
+ "http://200/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NORMAL, "21m", "21d", function () {
+ // 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, 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..83829c631e
--- /dev/null
+++ b/netwerk/test/unit/test_cache2-17-evict-all.js
@@ -0,0 +1,17 @@
+"use strict";
+
+function run_test() {
+ do_get_profile();
+
+ Services.cache2.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..8cbe37be4a
--- /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 () {
+ // 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 () {
+ // And check...
+ asyncOpenCacheEntry(
+ "http://nv/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NORMAL, "v2m", "v2d", function () {
+ 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..f3f9491932
--- /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 () {
+ // 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 (entry1) {
+ entry1.setValid();
+ }
+ ).onCacheEntryAvailable(entry, true, 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..31b8223c35
--- /dev/null
+++ b/netwerk/test/unit/test_cache2-20-range-200.js
@@ -0,0 +1,72 @@
+"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 () {
+ // 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 (entry1) {
+ entry1.setValid();
+ }
+ ).onCacheEntryAvailable(entry, true, 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..1db0047475
--- /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 () {
+ asyncOpenCacheEntry(
+ "http://anon1/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ Services.loadContextInfo.anonymous,
+ new OpenCallback(NORMAL, "an1", "an1", function () {
+ // 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 () {
+ asyncOpenCacheEntry(
+ "http://anon1/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ Services.loadContextInfo.default,
+ new OpenCallback(NORMAL, "na1", "na1", function () {
+ // 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 () {
+ 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..1768f142bd
--- /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..89bdb2d963
--- /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..7f7c50e9f0
--- /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..8c5a383ba6
--- /dev/null
+++ b/netwerk/test/unit/test_cache2-25-chunk-memory-limit.js
@@ -0,0 +1,53 @@
+"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();
+
+ // set max chunks memory so that only one full chunk fits within the limit
+ Services.prefs.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 (status1, entry1) {
+ Assert.equal(status1, Cr.NS_OK);
+ var oStr2 = entry1.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..bdb116436c
--- /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 () {
+ // 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 () {
+ 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..78ac7eb847
--- /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..7450e1998f
--- /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 (entry1) {
+ var secondOpen = NowSeconds();
+ Assert.equal(entry1.fetchCount, 2);
+ do_check_time(entry1.lastFetched, firstOpen, secondOpen);
+ do_check_time(entry1.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..d30e13af22
--- /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 (entry1) {
+ Assert.equal(entry1.fetchCount, 1);
+ do_check_time(entry1.lastFetched, now1);
+ do_check_time(entry1.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..b97c4b113c
--- /dev/null
+++ b/netwerk/test/unit/test_cache2-29a-concurrent_read_resumable_entry_size_zero.js
@@ -0,0 +1,76 @@
+/*
+
+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.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+var httpProtocolHandler = Cc[
+ "@mozilla.org/network/protocol;1?name=http"
+].getService(Ci.nsIHttpProtocolHandler);
+
+ChromeUtils.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..e221be7661
--- /dev/null
+++ b/netwerk/test/unit/test_cache2-29b-concurrent_read_non-resumable_entry_size_zero.js
@@ -0,0 +1,80 @@
+/*
+
+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.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+var httpProtocolHandler = Cc[
+ "@mozilla.org/network/protocol;1?name=http"
+].getService(Ci.nsIHttpProtocolHandler);
+
+ChromeUtils.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..706ad894ca
--- /dev/null
+++ b/netwerk/test/unit/test_cache2-29c-concurrent_read_half-interrupted.js
@@ -0,0 +1,95 @@
+/*
+
+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.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+var httpProtocolHandler = Cc[
+ "@mozilla.org/network/protocol;1?name=http"
+].getService(Ci.nsIHttpProtocolHandler);
+
+ChromeUtils.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..d9886a6fbf
--- /dev/null
+++ b/netwerk/test/unit/test_cache2-29d-concurrent_read_half-corrupted-206.js
@@ -0,0 +1,95 @@
+/*
+
+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.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+var httpProtocolHandler = Cc[
+ "@mozilla.org/network/protocol;1?name=http"
+].getService(Ci.nsIHttpProtocolHandler);
+
+ChromeUtils.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..7d517d518d
--- /dev/null
+++ b/netwerk/test/unit/test_cache2-29e-concurrent_read_half-non-206-response.js
@@ -0,0 +1,90 @@
+/*
+
+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.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+var httpProtocolHandler = Cc[
+ "@mozilla.org/network/protocol;1?name=http"
+].getService(Ci.nsIHttpProtocolHandler);
+
+ChromeUtils.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..b2541cbf86
--- /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 () {
+ // Open for read and check
+ asyncOpenCacheEntry(
+ "http://a/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ Services.loadContextInfo.default,
+ new OpenCallback(NORMAL, "a1m", "a1d", function () {
+ // Now clear the whole cache
+ Services.cache2.clear();
+
+ // The pinned entry should be intact
+ asyncOpenCacheEntry(
+ "http://a/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ Services.loadContextInfo.default,
+ new OpenCallback(NORMAL, "a1m", "a1d", function () {
+ 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..21f7f02e45
--- /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 () {
+ // 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 () {
+ // 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 () {
+ 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..73dfee0f6b
--- /dev/null
+++ b/netwerk/test/unit/test_cache2-30c-pinning-deferred-doom.js
@@ -0,0 +1,185 @@
+/*
+
+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 = Services.cache2.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.
+ Services.cache2.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)
+
+ Services.obs.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
+ Services.cache2.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..fd4622f3f4
--- /dev/null
+++ b/netwerk/test/unit/test_cache2-30d-pinning-WasEvicted-API.js
@@ -0,0 +1,148 @@
+/*
+
+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 = Services.cache2.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.
+ Services.cache2.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)
+
+ Services.obs.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.
+ Services.cache2.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..3fed10881f
--- /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
+
+ Services.cache2.asyncVisitAllStorages(
+ // Test should store 8 entries across 4 originAttributes
+ new VisitCallback(8, expectedConsumption, entries, function () {
+ Services.cache2.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 () {
+ asyncOpenCacheEntry(
+ "http://a/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ lcis[i],
+ new OpenCallback(NORMAL, "a1m", "a1d", function () {
+ mc.fired();
+ })
+ );
+ })
+ );
+
+ asyncOpenCacheEntry(
+ "http://b/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ lcis[i],
+ new OpenCallback(NEW, "b1m", "b1d", function () {
+ asyncOpenCacheEntry(
+ "http://b/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ lcis[i],
+ new OpenCallback(NORMAL, "b1m", "b1d", function () {
+ 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..2234ccd13d
--- /dev/null
+++ b/netwerk/test/unit/test_cache2-32-clear-origin.js
@@ -0,0 +1,69 @@
+"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 () {
+ asyncOpenCacheEntry(
+ URL + "/a",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NORMAL, "e1m", "e1d", function () {
+ asyncOpenCacheEntry(
+ URL2 + "/a",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NEW, "f1m", "f1d", function () {
+ asyncOpenCacheEntry(
+ URL2 + "/a",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NORMAL, "f1m", "f1d", function () {
+ var url = Services.io.newURI(URL);
+ var principal =
+ Services.scriptSecurityManager.createContentPrincipal(
+ url,
+ {}
+ );
+
+ Services.cache2.clearOrigin(principal);
+
+ asyncOpenCacheEntry(
+ URL + "/a",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NEW, "e1m", "e1d", function () {
+ asyncOpenCacheEntry(
+ URL2 + "/a",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NORMAL, "f1m", "f1d", function () {
+ finish_cache2_test();
+ })
+ );
+ })
+ );
+ })
+ );
+ })
+ );
+ })
+ );
+ })
+ );
+
+ do_test_pending();
+}
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..3038b7ed71
--- /dev/null
+++ b/netwerk/test/unit/test_cache_204_response.js
@@ -0,0 +1,62 @@
+/* -*- 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.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+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..bedde668b4
--- /dev/null
+++ b/netwerk/test/unit/test_cache_jar.js
@@ -0,0 +1,105 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+ChromeUtils.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 = Services.appinfo.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..614f420a5e
--- /dev/null
+++ b/netwerk/test/unit/test_cacheflags.js
@@ -0,0 +1,437 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+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 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) {
+ httpserver.stop(do_test_finished);
+ return;
+ }
+
+ var test = gTests.shift();
+ test.run();
+}
+
+function handler(httpStatus, metadata, response) {
+ gHitServer = true;
+ let etag;
+ try {
+ etag = metadata.getHeader("If-None-Match");
+ } catch (ex) {
+ 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..d5c951d16c
--- /dev/null
+++ b/netwerk/test/unit/test_captive_portal_service.js
@@ -0,0 +1,325 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+let httpserver = null;
+ChromeUtils.defineLazyGetter(this, "cpURI", function () {
+ return (
+ "http://localhost:" + httpserver.identity.primaryPort + "/captive.html"
+ );
+});
+
+const SUCCESS_STRING =
+ '<meta http-equiv="refresh" content="0;url=https://support.mozilla.org/kb/captive-portal"/>';
+let cpResponse = SUCCESS_STRING;
+function contentHandler(metadata, response) {
+ response.setHeader("Content-Type", "text/html");
+ 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.html", 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/html");
+ response.bodyOutputStream.write(cpResponse, cpResponse.length);
+ });
+ httpserver.registerPathHandler("/captive.html", (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/html");
+ response.bodyOutputStream.write("bad", "bad".length);
+ });
+
+ httpserver.registerPathHandler("/captive.html", (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.html", (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 = Services.env.get("MOZHTTP2_PORT");
+ Assert.notEqual(h2Port, null);
+ Assert.notEqual(h2Port, "");
+
+ // Any kind of redirection should trigger the captive portal login.
+ httpserver.registerPathHandler("/captive.html", (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);
+});
+
+// This test uses a 511 status code to request a captive portal login
+// We check that it triggers a captive portal login
+// See RFC 6585 for details
+add_task(async function test_511_error() {
+ Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, false);
+ equal(cps.state, Ci.nsICaptivePortalService.UNKNOWN);
+
+ httpserver.registerPathHandler("/captive.html", (metadata, response) => {
+ response.setStatusLine(
+ metadata.httpVersion,
+ 511,
+ "Network Authentication Required"
+ );
+ cpResponse = '<meta http-equiv="refresh" content="0;url=/login">';
+ contentHandler(metadata, response);
+ });
+
+ let notification = observerPromise("captive-portal-login");
+ Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, true);
+
+ await notification;
+ equal(cps.state, Ci.nsICaptivePortalService.LOCKED_PORTAL);
+});
+
+// Any other 5xx HTTP error, is assumed to be an issue with the
+// canonical web server, and should not trigger a captive portal login
+add_task(async function test_generic_5xx_error() {
+ Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, false);
+ equal(cps.state, Ci.nsICaptivePortalService.UNKNOWN);
+
+ let requests = 0;
+ httpserver.registerPathHandler("/captive.html", (metadata, response) => {
+ if (requests++ === 0) {
+ // on first attempt, send 503 error
+ response.setStatusLine(
+ metadata.httpVersion,
+ 503,
+ "Internal Server Error"
+ );
+ cpResponse = "<h1>Internal Server Error</h1>";
+ } else {
+ // on retry, send canonical reply
+ cpResponse = SUCCESS_STRING;
+ }
+ contentHandler(metadata, response);
+ });
+
+ let notification = observerPromise("network:captive-portal-connectivity");
+ Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, true);
+
+ await notification;
+ equal(requests, 2);
+ equal(cps.state, Ci.nsICaptivePortalService.NOT_CAPTIVE);
+});
+
+add_task(async function test_changed_notification() {
+ Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, false);
+ equal(cps.state, Ci.nsICaptivePortalService.UNKNOWN);
+
+ httpserver.registerPathHandler("/captive.html", contentHandler);
+ cpResponse = SUCCESS_STRING;
+
+ let changedNotificationCount = 0;
+ let observer = {
+ QueryInterface: ChromeUtils.generateQI(["nsIObserver"]),
+ observe(aSubject, aTopic, aData) {
+ changedNotificationCount += 1;
+ },
+ };
+ Services.obs.addObserver(
+ observer,
+ "network:captive-portal-connectivity-changed"
+ );
+
+ let notification = observerPromise(
+ "network:captive-portal-connectivity-changed"
+ );
+ Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, true);
+ await notification;
+ equal(changedNotificationCount, 1);
+ equal(cps.state, Ci.nsICaptivePortalService.NOT_CAPTIVE);
+
+ notification = observerPromise("network:captive-portal-connectivity");
+ cps.recheckCaptivePortal();
+ await notification;
+ equal(changedNotificationCount, 1);
+ equal(cps.state, Ci.nsICaptivePortalService.NOT_CAPTIVE);
+
+ notification = observerPromise("captive-portal-login");
+ cpResponse = "you are captive";
+ cps.recheckCaptivePortal();
+ await notification;
+ equal(changedNotificationCount, 1);
+ equal(cps.state, Ci.nsICaptivePortalService.LOCKED_PORTAL);
+
+ notification = observerPromise("captive-portal-login-success");
+ cpResponse = SUCCESS_STRING;
+ cps.recheckCaptivePortal();
+ await notification;
+ equal(changedNotificationCount, 2);
+ equal(cps.state, Ci.nsICaptivePortalService.UNLOCKED_PORTAL);
+
+ notification = observerPromise("captive-portal-login");
+ cpResponse = "you are captive";
+ cps.recheckCaptivePortal();
+ await notification;
+ equal(changedNotificationCount, 2);
+ equal(cps.state, Ci.nsICaptivePortalService.LOCKED_PORTAL);
+
+ Services.obs.removeObserver(
+ observer,
+ "network:captive-portal-connectivity-changed"
+ );
+});
diff --git a/netwerk/test/unit/test_cert_info.js b/netwerk/test/unit/test_cert_info.js
new file mode 100644
index 0000000000..abf5a1d634
--- /dev/null
+++ b/netwerk/test/unit/test_cert_info.js
@@ -0,0 +1,162 @@
+/* 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 */
+/* import-globals-from head_servers.js */
+
+function makeChan(uri) {
+ let chan = NetUtil.newChannel({
+ uri,
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+ chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI;
+ return chan;
+}
+
+async function test_cert_failure(server_creator, https_proxy) {
+ let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(
+ Ci.nsIX509CertDB
+ );
+ addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u");
+ addCertFromFile(certdb, "proxy-ca.pem", "CTu,u,u");
+
+ let server = new server_creator();
+ await server.start();
+ registerCleanupFunction(async () => {
+ await server.stop();
+ });
+
+ await server.registerPathHandler("/test", (req, resp) => {
+ resp.writeHead(200);
+ resp.end("done");
+ });
+ let url;
+ if (server_creator == NodeHTTPServer) {
+ url = `http://localhost:${server.port()}/test`;
+ } else {
+ url = `https://localhost:${server.port()}/test`;
+ }
+ let chan = makeChan(url);
+ let req = await new Promise(resolve => {
+ chan.asyncOpen(new ChannelListener(resolve, null, CL_ALLOW_UNKNOWN_CL));
+ });
+ equal(req.status, Cr.NS_OK);
+ equal(req.QueryInterface(Ci.nsIHttpChannel).responseStatus, 200);
+ let secinfo = req.securityInfo;
+ if (server_creator == NodeHTTPServer) {
+ if (!https_proxy) {
+ Assert.equal(secinfo, null);
+ } else {
+ // In the case we are connecting to an insecure HTTP server
+ // through a secure proxy, nsHttpChannel will have the security
+ // info from the proxy.
+ // We will discuss this behavir in bug 1785777.
+ secinfo.QueryInterface(Ci.nsITransportSecurityInfo);
+ Assert.equal(secinfo.serverCert.commonName, " Proxy Test Cert");
+ }
+ } else {
+ secinfo.QueryInterface(Ci.nsITransportSecurityInfo);
+ Assert.equal(secinfo.serverCert.commonName, " HTTP2 Test Cert");
+ }
+}
+
+add_task(async function test_http() {
+ await test_cert_failure(NodeHTTPServer, false);
+});
+
+add_task(async function test_https() {
+ await test_cert_failure(NodeHTTPSServer, false);
+});
+
+add_task(async function test_http2() {
+ await test_cert_failure(NodeHTTP2Server, false);
+});
+
+add_task(async function test_http_proxy_http_server() {
+ let proxy = new NodeHTTPProxyServer();
+ await proxy.start();
+ registerCleanupFunction(() => {
+ proxy.stop();
+ });
+ await test_cert_failure(NodeHTTPServer, false);
+});
+
+add_task(async function test_http_proxy_https_server() {
+ let proxy = new NodeHTTPProxyServer();
+ await proxy.start();
+ registerCleanupFunction(() => {
+ proxy.stop();
+ });
+ await test_cert_failure(NodeHTTPSServer, false);
+});
+
+add_task(async function test_http_proxy_http2_server() {
+ let proxy = new NodeHTTPProxyServer();
+ await proxy.start();
+ registerCleanupFunction(() => {
+ proxy.stop();
+ });
+ await test_cert_failure(NodeHTTP2Server, false);
+});
+
+add_task(async function test_https_proxy_http_server() {
+ let proxy = new NodeHTTPSProxyServer();
+ await proxy.start();
+ registerCleanupFunction(() => {
+ proxy.stop();
+ });
+ await test_cert_failure(NodeHTTPServer, true);
+});
+
+add_task(async function test_https_proxy_https_server() {
+ let proxy = new NodeHTTPSProxyServer();
+ await proxy.start();
+ registerCleanupFunction(() => {
+ proxy.stop();
+ });
+ await test_cert_failure(NodeHTTPSServer, true);
+});
+
+add_task(async function test_https_proxy_http2_server() {
+ let proxy = new NodeHTTPSProxyServer();
+ await proxy.start();
+ registerCleanupFunction(() => {
+ proxy.stop();
+ });
+ await test_cert_failure(NodeHTTP2Server, true);
+});
+
+add_task(async function test_http2_proxy_http_server() {
+ let proxy = new NodeHTTP2ProxyServer();
+ await proxy.start();
+ registerCleanupFunction(() => {
+ proxy.stop();
+ });
+
+ await test_cert_failure(NodeHTTPServer, true);
+});
+
+add_task(async function test_http2_proxy_https_server() {
+ let proxy = new NodeHTTP2ProxyServer();
+ await proxy.start();
+ registerCleanupFunction(() => {
+ proxy.stop();
+ });
+
+ await test_cert_failure(NodeHTTPSServer, true);
+});
+
+add_task(async function test_http2_proxy_http2_server() {
+ let proxy = new NodeHTTP2ProxyServer();
+ await proxy.start();
+ registerCleanupFunction(() => {
+ proxy.stop();
+ });
+
+ await test_cert_failure(NodeHTTP2Server, true);
+});
diff --git a/netwerk/test/unit/test_cert_verification_failure.js b/netwerk/test/unit/test_cert_verification_failure.js
new file mode 100644
index 0000000000..11eb575dc1
--- /dev/null
+++ b/netwerk/test/unit/test_cert_verification_failure.js
@@ -0,0 +1,66 @@
+/* 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 */
+/* import-globals-from head_servers.js */
+
+function makeChan(uri) {
+ let chan = NetUtil.newChannel({
+ uri,
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+ chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI;
+ return chan;
+}
+
+async function test_cert_failure(server_or_proxy, server_cert) {
+ let server = new server_or_proxy();
+ await server.start();
+ registerCleanupFunction(async () => {
+ await server.stop();
+ });
+ let chan = makeChan(`https://localhost:${server.port()}/test`);
+ let req = await new Promise(resolve => {
+ chan.asyncOpen(new ChannelListener(resolve, null, CL_EXPECT_FAILURE));
+ });
+ equal(req.status, 0x805a1ff3); // SEC_ERROR_UNKNOWN_ISSUER
+ let secinfo = req.securityInfo;
+ secinfo.QueryInterface(Ci.nsITransportSecurityInfo);
+ if (server_cert) {
+ Assert.equal(secinfo.serverCert.commonName, " HTTP2 Test Cert");
+ } else {
+ Assert.equal(secinfo.serverCert.commonName, " Proxy Test Cert");
+ }
+}
+
+add_task(async function test_https() {
+ await test_cert_failure(NodeHTTPSServer, true);
+});
+
+add_task(async function test_http2() {
+ await test_cert_failure(NodeHTTP2Server, true);
+});
+
+add_task(async function test_https_proxy() {
+ let proxy = new NodeHTTPSProxyServer();
+ await proxy.start();
+ registerCleanupFunction(() => {
+ proxy.stop();
+ });
+ await test_cert_failure(NodeHTTPSServer, false);
+});
+
+add_task(async function test_http2_proxy() {
+ let proxy = new NodeHTTP2ProxyServer();
+ await proxy.start();
+ registerCleanupFunction(() => {
+ proxy.stop();
+ });
+
+ await test_cert_failure(NodeHTTPSServer, 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..f8f78fd211
--- /dev/null
+++ b/netwerk/test/unit/test_channel_close.js
@@ -0,0 +1,70 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+var httpProtocolHandler = Cc[
+ "@mozilla.org/network/protocol;1?name=http"
+].getService(Ci.nsIHttpProtocolHandler);
+
+ChromeUtils.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_long_domain.js b/netwerk/test/unit/test_channel_long_domain.js
new file mode 100644
index 0000000000..86dbbed3e3
--- /dev/null
+++ b/netwerk/test/unit/test_channel_long_domain.js
@@ -0,0 +1,14 @@
+// Tests that domains longer than 253 characters fail to load when pref is true
+
+add_task(async function test_long_domain_fails() {
+ let domain = "http://" + "a".repeat(254);
+
+ let req = await new Promise(resolve => {
+ let chan = NetUtil.newChannel({
+ uri: domain,
+ loadUsingSystemPrincipal: true,
+ });
+ chan.asyncOpen(new ChannelListener(resolve, null, CL_EXPECT_FAILURE));
+ });
+ Assert.equal(req.status, Cr.NS_ERROR_UNKNOWN_HOST, "Request should fail");
+});
diff --git a/netwerk/test/unit/test_channel_priority.js b/netwerk/test/unit/test_channel_priority.js
new file mode 100644
index 0000000000..b230042890
--- /dev/null
+++ b/netwerk/test/unit/test_channel_priority.js
@@ -0,0 +1,98 @@
+/* -*- 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";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+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
+/* exported configPort */
+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..c8dee14efe
--- /dev/null
+++ b/netwerk/test/unit/test_chunked_responses.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/. */
+
+/*
+ * Test Chunked-Encoded response parsing.
+ */
+
+////////////////////////////////////////////////////////////////////////////////
+// Test infrastructure
+
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+ChromeUtils.defineLazyGetter(this, "URL", function () {
+ return "http://localhost:" + httpserver.identity.primaryPort;
+});
+
+var httpserver = new HttpServer();
+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;
+ // eslint-disable-next-line no-eval
+ httpserver.registerPathHandler(testPath, eval("handler" + num));
+
+ var channel = setupChannel(testPath);
+ var flags = test_flags[num]; // OK if flags undefined for test
+ channel.asyncOpen(
+ // eslint-disable-next-line no-eval
+ 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;
+
+// eslint-disable-next-line no-unused-vars
+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();
+}
+
+// eslint-disable-next-line no-unused-vars
+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;
+
+// eslint-disable-next-line no-unused-vars
+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();
+}
+
+// eslint-disable-next-line no-unused-vars
+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;
+
+// eslint-disable-next-line no-unused-vars
+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();
+}
+
+// eslint-disable-next-line no-unused-vars
+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;
+
+// eslint-disable-next-line no-unused-vars
+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();
+}
+
+// eslint-disable-next-line no-unused-vars
+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;
+
+// eslint-disable-next-line no-unused-vars
+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();
+}
+
+// eslint-disable-next-line no-unused-vars
+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_client_auth_with_proxy.js b/netwerk/test/unit/test_client_auth_with_proxy.js
new file mode 100644
index 0000000000..5a205f4db1
--- /dev/null
+++ b/netwerk/test/unit/test_client_auth_with_proxy.js
@@ -0,0 +1,176 @@
+/* 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 */
+/* import-globals-from head_servers.js */
+
+const { MockRegistrar } = ChromeUtils.importESModule(
+ "resource://testing-common/MockRegistrar.sys.mjs"
+);
+
+const certOverrideService = Cc[
+ "@mozilla.org/security/certoverride;1"
+].getService(Ci.nsICertOverrideService);
+
+function makeChan(uri) {
+ let chan = NetUtil.newChannel({
+ uri,
+ 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]);
+ }
+ chan.asyncOpen(new ChannelListener(finish, null, flags));
+ });
+}
+
+class SecurityObserver {
+ constructor(input, output) {
+ this.input = input;
+ this.output = output;
+ }
+
+ onHandshakeDone(socket, status) {
+ info("TLS handshake done");
+
+ let output = this.output;
+ this.input.asyncWait(
+ {
+ onInputStreamReady(readyInput) {
+ let request = NetUtil.readInputStreamToString(
+ readyInput,
+ readyInput.available()
+ );
+ ok(
+ request.startsWith("GET /") && request.includes("HTTP/1.1"),
+ "expecting an HTTP/1.1 GET request"
+ );
+ let response =
+ "HTTP/1.1 200 OK\r\nContent-Type:text/plain\r\n" +
+ "Connection:Close\r\nContent-Length:2\r\n\r\nOK";
+ output.write(response, response.length);
+ },
+ },
+ 0,
+ 0,
+ Services.tm.currentThread
+ );
+ }
+}
+
+function startServer(cert) {
+ let tlsServer = Cc["@mozilla.org/network/tls-server-socket;1"].createInstance(
+ Ci.nsITLSServerSocket
+ );
+ tlsServer.init(-1, true, -1);
+ tlsServer.serverCert = cert;
+
+ let securityObservers = [];
+
+ let listener = {
+ onSocketAccepted(socket, transport) {
+ info("Accepted TLS client connection");
+ let connectionInfo = transport.securityCallbacks.getInterface(
+ Ci.nsITLSServerConnectionInfo
+ );
+ let input = transport.openInputStream(0, 0, 0);
+ let output = transport.openOutputStream(0, 0, 0);
+ connectionInfo.setSecurityObserver(new SecurityObserver(input, output));
+ },
+
+ onStopListening() {
+ info("onStopListening");
+ for (let securityObserver of securityObservers) {
+ securityObserver.input.close();
+ securityObserver.output.close();
+ }
+ },
+ };
+
+ tlsServer.setSessionTickets(false);
+ tlsServer.setRequestClientCertificate(Ci.nsITLSServerSocket.REQUEST_ALWAYS);
+
+ tlsServer.asyncListen(listener);
+
+ return tlsServer;
+}
+
+// Replace the UI dialog that prompts the user to pick a client certificate.
+const clientAuthDialogService = {
+ chooseCertificate(hostname, certArray, loadContext, callback) {
+ callback.certificateChosen(certArray[0], false);
+ },
+ QueryInterface: ChromeUtils.generateQI(["nsIClientAuthDialogService"]),
+};
+
+let server;
+add_setup(async function setup() {
+ do_get_profile();
+
+ let clientAuthDialogServiceCID = MockRegistrar.register(
+ "@mozilla.org/security/ClientAuthDialogService;1",
+ clientAuthDialogService
+ );
+
+ let cert = getTestServerCertificate();
+ ok(!!cert, "Got self-signed cert");
+ server = startServer(cert);
+
+ certOverrideService.rememberValidityOverride(
+ "localhost",
+ server.port,
+ {},
+ cert,
+ true
+ );
+
+ registerCleanupFunction(async function () {
+ MockRegistrar.unregister(clientAuthDialogServiceCID);
+ certOverrideService.clearValidityOverride("localhost", server.port, {});
+ server.close();
+ });
+});
+
+add_task(async function test_client_auth_with_proxy() {
+ let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(
+ Ci.nsIX509CertDB
+ );
+ addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u");
+ addCertFromFile(certdb, "proxy-ca.pem", "CTu,u,u");
+
+ let proxies = [
+ NodeHTTPProxyServer,
+ NodeHTTPSProxyServer,
+ NodeHTTP2ProxyServer,
+ ];
+
+ for (let p of proxies) {
+ info(`Test with proxy:${p.name}`);
+ let proxy = new p();
+ await proxy.start();
+ registerCleanupFunction(async () => {
+ await proxy.stop();
+ });
+
+ let chan = makeChan(`https://localhost:${server.port}`);
+ let [req, buff] = await channelOpenPromise(chan, CL_ALLOW_UNKNOWN_CL);
+ equal(req.status, Cr.NS_OK);
+ equal(req.QueryInterface(Ci.nsIHttpChannel).responseStatus, 200);
+ equal(buff, "OK");
+ req.QueryInterface(Ci.nsIProxiedChannel);
+ ok(!!req.proxyInfo);
+ notEqual(req.proxyInfo.type, "direct");
+ await proxy.stop();
+ }
+});
diff --git a/netwerk/test/unit/test_coaleasing_h2_and_h3_connection.js b/netwerk/test/unit/test_coaleasing_h2_and_h3_connection.js
new file mode 100644
index 0000000000..3c998ffe29
--- /dev/null
+++ b/netwerk/test/unit/test_coaleasing_h2_and_h3_connection.js
@@ -0,0 +1,109 @@
+/* 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 { setTimeout } = ChromeUtils.importESModule(
+ "resource://gre/modules/Timer.sys.mjs"
+);
+
+let h2Port;
+let h3Port;
+
+add_setup(async function setup() {
+ h2Port = Services.env.get("MOZHTTP2_PORT");
+ Assert.notEqual(h2Port, null);
+ Assert.notEqual(h2Port, "");
+
+ h3Port = Services.env.get("MOZHTTP3_PORT");
+ Assert.notEqual(h3Port, null);
+ Assert.notEqual(h3Port, "");
+
+ Services.prefs.setBoolPref("network.http.http3.enable", true);
+ Services.prefs.setIntPref("network.http.speculative-parallel-limit", 6);
+ Services.prefs.setBoolPref("network.http.altsvc.oe", true);
+
+ // Set to allow the cert presented by our H2 server
+ do_get_profile();
+
+ let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(
+ Ci.nsIX509CertDB
+ );
+ addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u");
+});
+
+registerCleanupFunction(async () => {
+ Services.prefs.clearUserPref("network.http.http3.enable");
+ Services.prefs.clearUserPref("network.dns.localDomains");
+ Services.prefs.clearUserPref("network.http.speculative-parallel-limit");
+ Services.prefs.clearUserPref("network.http.altsvc.oe");
+});
+
+function makeChan(url) {
+ let chan = NetUtil.newChannel({
+ uri: url,
+ loadUsingSystemPrincipal: true,
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT,
+ }).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]);
+ }
+ chan.asyncOpen(new ChannelListener(finish, null, flags));
+ });
+}
+
+add_task(async function testNotCoaleasingH2Connection() {
+ const host = "foo.example.com";
+ Services.prefs.setCharPref("network.dns.localDomains", host);
+
+ let server = new NodeHTTPSServer();
+ await server.start();
+ registerCleanupFunction(async () => {
+ await server.stop();
+ });
+
+ await server.execute(`global.h3Port = "${h3Port}";`);
+ await server.registerPathHandler("/altsvc", (req, resp) => {
+ const body = "done";
+ resp.setHeader("Content-Length", body.length);
+ resp.setHeader("Alt-Svc", `h3-29=:${global.h3Port}`);
+ resp.writeHead(200);
+ resp.write(body);
+ resp.end("");
+ });
+
+ let chan = makeChan(`https://${host}:${server.port()}/altsvc`);
+ let [req] = await channelOpenPromise(chan);
+ Assert.equal(req.protocolVersion, "http/1.1");
+
+ // Some delay to make sure the H3 speculative connection is created.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => setTimeout(resolve, 1000));
+
+ // To clear the altsvc cache.
+ Services.obs.notifyObservers(null, "last-pb-context-exited");
+
+ // Add another alt-svc header to route to moz-http2.js.
+ Services.prefs.setCharPref(
+ "network.http.http3.alt-svc-mapping-for-testing",
+ `${host};h2=:${h2Port}`
+ );
+
+ let start = new Date().getTime();
+ chan = makeChan(`https://${host}:${server.port()}/server-timing`);
+ chan.QueryInterface(Ci.nsIHttpChannelInternal).beConservative = true;
+ [req] = await channelOpenPromise(chan);
+ Assert.equal(req.protocolVersion, "h2");
+
+ // The time this request takes should be way more less than the
+ // neqo idle timeout (30s).
+ let duration = (new Date().getTime() - start) / 1000;
+ Assert.less(duration, 10);
+});
diff --git a/netwerk/test/unit/test_compareURIs.js b/netwerk/test/unit/test_compareURIs.js
new file mode 100644
index 0000000000..e460de1c29
--- /dev/null
+++ b/netwerk/test/unit/test_compareURIs.js
@@ -0,0 +1,61 @@
+"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],
+ ];
+
+ 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 {
+ Services.scriptSecurityManager.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..05f19be4b5
--- /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_connection_based_auth.js b/netwerk/test/unit/test_connection_based_auth.js
new file mode 100644
index 0000000000..3a21ffcb77
--- /dev/null
+++ b/netwerk/test/unit/test_connection_based_auth.js
@@ -0,0 +1,91 @@
+/* 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 */
+/* import-globals-from head_servers.js */
+
+function makeChan(uri) {
+ let chan = NetUtil.newChannel({
+ uri,
+ 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]);
+ }
+ chan.asyncOpen(new ChannelListener(finish, null, flags));
+ });
+}
+
+add_task(async function test_connection_based_auth() {
+ let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(
+ Ci.nsIX509CertDB
+ );
+ addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u");
+ addCertFromFile(certdb, "proxy-ca.pem", "CTu,u,u");
+
+ let proxy = new NodeHTTPSProxyServer();
+ await proxy.start();
+
+ await proxy.registerConnectHandler((req, clientSocket, head) => {
+ if (!req.headers["proxy-authorization"]) {
+ clientSocket.write(
+ "HTTP/1.1 407 Unauthorized\r\n" +
+ "Proxy-agent: Node.js-Proxy\r\n" +
+ "Connection: keep-alive\r\n" +
+ "Proxy-Authenticate: mock_auth\r\n" +
+ "Content-Length: 0\r\n" +
+ "\r\n"
+ );
+
+ clientSocket.on("data", data => {
+ let array = data.toString().split("\r\n");
+ let proxyAuthorization = "";
+ for (let line of array) {
+ let pair = line.split(":").map(element => element.trim());
+ if (pair[0] === "Proxy-Authorization") {
+ proxyAuthorization = pair[1];
+ }
+ }
+
+ if (proxyAuthorization === "moz_test_credentials") {
+ // We don't return 200 OK here, because we don't have a server
+ // to connect to.
+ clientSocket.write(
+ "HTTP/1.1 404 Not Found\r\nProxy-agent: Node.js-Proxy\r\n\r\n"
+ );
+ } else {
+ clientSocket.write(
+ "HTTP/1.1 502 Error\r\nProxy-agent: Node.js-Proxy\r\n\r\n"
+ );
+ }
+ clientSocket.destroy();
+ });
+ return;
+ }
+
+ // We should not reach here.
+ clientSocket.write(
+ "HTTP/1.1 502 Error\r\nProxy-agent: Node.js-Proxy\r\n\r\n"
+ );
+ clientSocket.destroy();
+ });
+
+ let chan = makeChan(`https://example.ntlm.com/test`);
+ let [req] = await channelOpenPromise(chan, CL_EXPECT_FAILURE);
+ Assert.equal(req.status, Cr.NS_ERROR_UNKNOWN_HOST);
+ req.QueryInterface(Ci.nsIProxiedChannel);
+ Assert.equal(req.httpProxyConnectResponseCode, 404);
+
+ await proxy.stop();
+});
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..48479034b8
--- /dev/null
+++ b/netwerk/test/unit/test_content_encoding_gzip.js
@@ -0,0 +1,163 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+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 a truncated brotli document, producing no output bytes
+ {
+ url: "/test/cebrotli2",
+ flags: CL_EXPECT_GZIP,
+ ce: "br",
+ body: [0x0b, 0x0a, 0x09],
+ datalen: 0,
+ },
+
+ // 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,
+ },
+
+ // this is not a brotli document
+ {
+ url: "/test/cebrotli4",
+ flags: CL_EXPECT_GZIP | CL_EXPECT_FAILURE,
+ ce: "br",
+ body: [
+ 0x01, 0xe6, 0x00, 0x76, 0x42, 0x10, 0x01, 0x1c, 0x24, 0x24, 0x3c, 0xd7,
+ 0xd7, 0xd7, 0x01, 0x1c,
+ ],
+ datalen: 16,
+ },
+
+ // this is a brotli document
+ {
+ url: "/test/cebrotli5",
+ flags: CL_EXPECT_GZIP,
+ ce: "br",
+ body: [0x0b, 0x00, 0x80, 0x4e, 0x03],
+ datalen: 1,
+ },
+
+ // this a truncated brotli document (missing the end marker)
+ // producing one output byte
+ {
+ url: "/test/cebrotli6",
+ flags: CL_EXPECT_GZIP,
+ ce: "br",
+ body: [0x0b, 0x00, 0x80, 0x4e],
+ datalen: 1,
+ },
+];
+
+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");
+ } else {
+ prefs.setCharPref("network.http.accept-encoding", "gzip, deflate, br");
+ }
+ 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, "test " + index);
+ }
+ 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 = Services.prefs;
+ cePref = prefs.getCharPref("network.http.accept-encoding");
+ prefs.setBoolPref("network.http.encoding.trustworthy_is_https", false);
+
+ 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.registerPathHandler("/test/cebrotli4", handler);
+ httpserver.registerPathHandler("/test/cebrotli5", handler);
+ httpserver.registerPathHandler("/test/cebrotli6", 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..f5f8e40a8a
--- /dev/null
+++ b/netwerk/test/unit/test_content_length_underrun.js
@@ -0,0 +1,295 @@
+/*
+ * Test Content-Length underrun behavior
+ */
+
+////////////////////////////////////////////////////////////////////////////////
+// Test infrastructure
+
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+ChromeUtils.defineLazyGetter(this, "URL", function () {
+ return "http://localhost:" + httpserver.identity.primaryPort;
+});
+
+var httpserver = new HttpServer();
+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 = Services.prefs;
+ 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;
+ // eslint-disable-next-line no-eval
+ httpserver.registerPathHandler(testPath, eval("handler" + num));
+
+ var channel = setupChannel(testPath);
+ let flags = test_flags[num]; // OK if flags undefined for test
+ channel.asyncOpen(
+ // eslint-disable-next-line no-eval
+ new ChannelListener(eval("completeTest" + num), channel, flags)
+ );
+}
+
+function run_gzip_test(num) {
+ let testPath = testPathBase + num;
+ // eslint-disable-next-line no-eval
+ 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;
+
+// eslint-disable-next-line no-unused-vars
+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();
+}
+
+// eslint-disable-next-line no-unused-vars
+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;
+
+// eslint-disable-next-line no-unused-vars
+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();
+}
+
+// eslint-disable-next-line no-unused-vars
+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;
+
+// eslint-disable-next-line no-unused-vars
+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();
+}
+
+// eslint-disable-next-line no-unused-vars
+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;
+
+// eslint-disable-next-line no-unused-vars
+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();
+}
+
+// eslint-disable-next-line no-unused-vars
+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;
+
+// eslint-disable-next-line no-unused-vars
+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();
+}
+
+// eslint-disable-next-line no-unused-vars
+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.
+
+// eslint-disable-next-line no-unused-vars
+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..26fb55cece
--- /dev/null
+++ b/netwerk/test/unit/test_content_sniffer.js
@@ -0,0 +1,157 @@
+// This file tests nsIContentSniffer, introduced in bug 324985
+
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+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(iid) {
+ return this.QueryInterface(iid);
+ },
+
+ 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 = Services.catMan;
+ 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..d6d25927e9
--- /dev/null
+++ b/netwerk/test/unit/test_cookie_blacklist.js
@@ -0,0 +1,43 @@
+"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
+ );
+ Services.prefs.setBoolPref("dom.security.https_first", false);
+
+ var cookieURI = Services.io.newURI(
+ "http://mozilla.org/test_cookie_blacklist.js"
+ );
+ const channel = NetUtil.newChannel({
+ uri: cookieURI,
+ loadUsingSystemPrincipal: true,
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT,
+ });
+
+ Services.cookies.setCookieStringFromHttp(
+ cookieURI,
+ "BadCookie1=\x01",
+ channel
+ );
+ Services.cookies.setCookieStringFromHttp(cookieURI, "BadCookie2=\v", channel);
+ Services.cookies.setCookieStringFromHttp(
+ cookieURI,
+ "Bad\x07Name=illegal",
+ channel
+ );
+ Services.cookies.setCookieStringFromHttp(cookieURI, GOOD_COOKIE, channel);
+ Services.cookies.setCookieStringFromHttp(cookieURI, SPACEY_COOKIE, channel);
+
+ CookieXPCShellUtils.createServer({ hosts: ["mozilla.org"] });
+
+ const storedCookie = await CookieXPCShellUtils.getCookieStringFromDocument(
+ cookieURI.spec
+ );
+ Assert.equal(storedCookie, GOOD_COOKIE + "; " + SPACEY_COOKIE);
+ Services.prefs.clearUserPref("dom.security.https_first");
+});
diff --git a/netwerk/test/unit/test_cookie_header.js b/netwerk/test/unit/test_cookie_header.js
new file mode 100644
index 0000000000..a5326ef25c
--- /dev/null
+++ b/netwerk/test/unit/test_cookie_header.js
@@ -0,0 +1,112 @@
+// This file tests bug 250375
+
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+ChromeUtils.defineLazyGetter(this, "URL", function () {
+ return "http://localhost:" + httpserv.identity.primaryPort + "/";
+});
+
+function inChildProcess() {
+ return Services.appinfo.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..a91b8a6848
--- /dev/null
+++ b/netwerk/test/unit/test_cookie_ipv6.js
@@ -0,0 +1,53 @@
+/* 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]";
+ChromeUtils.defineLazyGetter(this, "URL", function () {
+ return `http://${ip}:${httpserver.identity.primaryPort}/`;
+});
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+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));
+ });
+ equal(Services.cookies.cookies.length, 1);
+});
diff --git a/netwerk/test/unit/test_cookie_partitioned_attribute.js b/netwerk/test/unit/test_cookie_partitioned_attribute.js
new file mode 100644
index 0000000000..f1f3744831
--- /dev/null
+++ b/netwerk/test/unit/test_cookie_partitioned_attribute.js
@@ -0,0 +1,80 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function count_IsPartitioned(bool) {
+ var cookieCountIsPartitioned = 0;
+ Services.cookies.cookies.forEach(cookie => {
+ if (cookie.isPartitioned === bool) {
+ cookieCountIsPartitioned += 1;
+ }
+ });
+ return cookieCountIsPartitioned;
+}
+
+add_task(async function test_IsPartitioned() {
+ let profile = do_get_profile();
+ let dbFile = do_get_cookie_file(profile);
+ Assert.ok(!dbFile.exists());
+
+ let schema13db = new CookieDatabaseConnection(dbFile, 13);
+ let now = Date.now() * 1000; // date in microseconds
+
+ // add some non-partitioned cookies for key
+ let nUnpartitioned = 5;
+ let hostNonPartitioned = "cookie-host-non-partitioned.com";
+ for (let i = 0; i < nUnpartitioned; i++) {
+ let cookie = new Cookie(
+ "cookie-name" + i,
+ "cookie-value" + i,
+ hostNonPartitioned,
+ "/", // path
+ now, // expiry
+ now, // last accessed
+ now, // creation time
+ false, // session
+ false, // secure
+ false, // http-only
+ false, // inBrowserElement
+ {} // OA
+ );
+ schema13db.insertCookie(cookie);
+ }
+
+ // add some partitioned cookies
+ let nPartitioned = 5;
+ let hostPartitioned = "host-partitioned.com";
+ for (let i = 0; i < nPartitioned; i++) {
+ let cookie = new Cookie(
+ "cookie-name" + i,
+ "cookie-value" + i,
+ hostPartitioned,
+ "/", // path
+ now, // expiry
+ now, // last accessed
+ now, // creation time
+ false, // session
+ false, // secure
+ false, // http-only
+ false, // inBrowserElement
+ { partitionKey: "(https,example.com)" }
+ );
+ schema13db.insertCookie(cookie);
+ }
+
+ Assert.equal(do_count_cookies_in_db(schema13db.db), 10);
+
+ // Startup the cookie service and check the cookie counts by OA
+ let cookieCountNonPart =
+ Services.cookies.countCookiesFromHost(hostNonPartitioned); // includes expired cookies
+ Assert.equal(cookieCountNonPart, nUnpartitioned);
+ let cookieCountPart = Services.cookies.getCookiesFromHost(hostPartitioned, {
+ partitionKey: "(https,example.com)",
+ }).length; // includes expired cookies
+ Assert.equal(cookieCountPart, nPartitioned);
+
+ // Startup the cookie service and check the cookie counts by isPartitioned (IsPartitioned())
+ Assert.equal(count_IsPartitioned(false), nUnpartitioned);
+ Assert.equal(count_IsPartitioned(true), nPartitioned);
+
+ schema13db.close();
+});
diff --git a/netwerk/test/unit/test_cookiejars.js b/netwerk/test/unit/test_cookiejars.js
new file mode 100644
index 0000000000..1d78893f8c
--- /dev/null
+++ b/netwerk/test/unit/test_cookiejars.js
@@ -0,0 +1,188 @@
+/* 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";
+
+ChromeUtils.defineLazyGetter(this, "URL", function () {
+ return "http://localhost:" + httpserver.identity.primaryPort;
+});
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+var httpserver = new HttpServer();
+
+var cookieSetPath = "/setcookie";
+var cookieCheckPath = "/checkcookie";
+
+function inChildProcess() {
+ return Services.appinfo.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);
+
+ let loadGroup = Cc["@mozilla.org/network/load-group;1"].createInstance(
+ Ci.nsILoadGroup
+ );
+
+ if (chan.loadInfo.originAttributes.privateBrowsingId == 0) {
+ loadGroup.notificationCallbacks = Cu.createLoadContext();
+ chan.loadGroup = loadGroup;
+
+ chan.notificationCallbacks = Cu.createLoadContext();
+ } else {
+ loadGroup.notificationCallbacks = Cu.createPrivateLoadContext();
+ chan.loadGroup = loadGroup;
+
+ chan.notificationCallbacks = Cu.createPrivateLoadContext();
+ }
+
+ 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..855bacef42
--- /dev/null
+++ b/netwerk/test/unit/test_cookiejars_safebrowsing.js
@@ -0,0 +1,231 @@
+/* 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.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+ChromeUtils.defineLazyGetter(this, "URL", function () {
+ return "http://localhost:" + httpserver.identity.primaryPort;
+});
+
+var setCookiePath = "/setcookie";
+var checkCookiePath = "/checkcookie";
+var safebrowsingUpdatePath = "/safebrowsingUpdate";
+var safebrowsingGethashPath = "/safebrowsingGethash";
+var httpserver;
+
+function inChildProcess() {
+ return Services.appinfo.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..c61da23f99
--- /dev/null
+++ b/netwerk/test/unit/test_cookies_async_failure.js
@@ -0,0 +1,514 @@
+/* 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();
+ Services.prefs.setBoolPref("dom.security.https_first", false);
+
+ // Allow all cookies.
+ Services.prefs.setIntPref("network.cookie.cookieBehavior", 0);
+ Services.prefs.setBoolPref(
+ "network.cookieJarSettings.unblocked_for_testing",
+ true
+ );
+
+ // Bug 1617611 - Fix all the tests broken by "cookies SameSite=Lax by default"
+ Services.prefs.setBoolPref("network.cookie.sameSite.laxByDefault", false);
+
+ // 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().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();
+ Services.prefs.clearUserPref("dom.security.https_first");
+ Services.prefs.clearUserPref("network.cookie.sameSite.laxByDefault");
+});
+
+function do_get_backup_file() {
+ let file = profile.clone();
+ file.append("cookies.sqlite.bak");
+ return file;
+}
+
+function do_get_rebuild_backup_file() {
+ 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.cookies.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.cookies.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.cookies.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.cookies.countCookiesFromHost("foo.com"), 1);
+ Assert.equal(Services.cookies.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().exists());
+ let backupdb = Services.storage.openDatabase(do_get_backup_file());
+ 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.cookies.countCookiesFromHost("foo.com"), 1);
+ let cookies = Services.cookies.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().remove(false);
+ Assert.ok(!do_get_cookie_file(profile).exists());
+ Assert.ok(!do_get_backup_file().exists());
+}
+
+async function run_test_2() {
+ // Load the profile and populate it.
+ do_load_profile();
+
+ Services.cookies.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) {
+ uri = NetUtil.newURI("http://" + i + ".com/");
+ Services.cookies.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().exists());
+
+ // Recreate a new database since it was corrupted
+ Assert.equal(Services.cookies.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().exists());
+ Assert.equal(do_get_backup_file().fileSize, size);
+ let db = Services.storage.openDatabase(do_get_cookie_file(profile));
+ db.close();
+
+ do_load_profile();
+ Assert.equal(Services.cookies.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().remove(false);
+ Assert.ok(!do_get_cookie_file(profile).exists());
+ Assert.ok(!do_get_backup_file().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.cookies.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.cookies.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.cookies.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().exists());
+
+ // Recreate a new database since it was corrupted
+ Assert.equal(Services.cookies.countCookiesFromHost("hither.com"), 0);
+ Assert.equal(Services.cookies.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().exists());
+ Assert.equal(do_get_backup_file().fileSize, size);
+
+ // Rename it back, and try loading the entire database synchronously.
+ do_get_backup_file().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().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().exists());
+ Assert.equal(do_get_backup_file().fileSize, size);
+
+ // Clean up.
+ do_get_cookie_file(profile).remove(false);
+ do_get_backup_file().remove(false);
+ Assert.ok(!do_get_cookie_file(profile).exists());
+ Assert.ok(!do_get_backup_file().exists());
+}
+
+async function run_test_4() {
+ // Load the profile and populate it.
+ do_load_profile();
+ Services.cookies.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) {
+ uri = NetUtil.newURI("http://" + i + ".com/");
+ Services.cookies.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().exists());
+
+ // Recreate a new database since it was corrupted
+ Assert.equal(Services.cookies.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.cookies.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().exists());
+ Assert.equal(do_get_backup_file().fileSize, size);
+
+ // Load the profile, and check that it contains the new cookie.
+ do_load_profile();
+ Assert.equal(Services.cookies.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().remove(false);
+ Assert.ok(!do_get_cookie_file(profile).exists());
+ Assert.ok(!do_get_backup_file().exists());
+}
+
+async function run_test_5() {
+ // Load the profile and populate it.
+ do_load_profile();
+ Services.cookies.runInTransaction(_ => {
+ let uri = NetUtil.newURI("http://bar.com/");
+ const channel = NetUtil.newChannel({
+ uri,
+ loadUsingSystemPrincipal: true,
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT,
+ });
+ Services.cookies.setCookieStringFromHttp(
+ uri,
+ "oh=hai; path=/; max-age=1000",
+ channel
+ );
+ for (let i = 0; i < 3000; ++i) {
+ uri = NetUtil.newURI("http://" + i + ".com/");
+ Services.cookies.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().exists());
+
+ // Recreate a new database since it was corrupted
+ Assert.equal(Services.cookies.countCookiesFromHost("bar.com"), 0);
+ Assert.equal(Services.cookies.countCookiesFromHost("0.com"), 0);
+ Assert.equal(do_count_cookies(), 0);
+ Assert.ok(do_get_backup_file().exists());
+ Assert.equal(do_get_backup_file().fileSize, size);
+ Assert.ok(!do_get_rebuild_backup_file().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().exists());
+ Assert.equal(do_get_backup_file().fileSize, size);
+
+ Assert.equal(Services.cookies.countCookiesFromHost("bar.com"), 0);
+ Assert.equal(Services.cookies.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().remove(false);
+ Assert.ok(!do_get_cookie_file(profile).exists());
+ Assert.ok(!do_get_backup_file().exists());
+}
diff --git a/netwerk/test/unit/test_cookies_partition_counting.js b/netwerk/test/unit/test_cookies_partition_counting.js
new file mode 100644
index 0000000000..26bd56e1f6
--- /dev/null
+++ b/netwerk/test/unit/test_cookies_partition_counting.js
@@ -0,0 +1,183 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_setup(function test_setup() {
+ // FOG needs a profile directory to put its data in.
+ do_get_profile();
+
+ // FOG needs to be initialized in order for data to flow.
+ Services.fog.initializeFOG();
+});
+
+add_task(async function test_purge_counting() {
+ let profile = do_get_profile();
+ let dbFile = do_get_cookie_file(profile);
+ Assert.ok(!dbFile.exists());
+
+ let schema12db = new CookieDatabaseConnection(dbFile, 12);
+ let now = Date.now() * 1000; // date in microseconds
+
+ // add some non-partitioned cookies for key
+ let cookieNum1 = 1;
+ let hostNonPartitioned = "cookie-host-non-partitioned.com";
+ for (let i = 0; i < cookieNum1; i++) {
+ let cookie = new Cookie(
+ "cookie-name" + i,
+ "cookie-value" + i,
+ hostNonPartitioned,
+ "/", // path
+ now, // expiry
+ now, // needed to get the cookie by the db init
+ now, // creation time
+ false, // session
+ false, // secure
+ false, // http-only
+ false, // inBrowserElement
+ {} // OA
+ );
+ schema12db.insertCookie(cookie);
+ }
+
+ // add some non-partitioned cookies different key
+ let cookieNum2 = 10;
+ for (let i = 0; i < cookieNum2; i++) {
+ let cookie = new Cookie(
+ "cookie-name" + i,
+ "cookie-value" + i,
+ hostNonPartitioned,
+ "/", // path
+ now, // expiry
+ now, // needed to get the cookie by the db init
+ now, // creation time
+ false, // session
+ false, // secure
+ false, // http-only
+ false, // inBrowserElement
+ { userContextId: 8 } // OA
+ );
+ schema12db.insertCookie(cookie);
+ }
+
+ // add some partitioned cookies
+ let hostPartitioned = "host-partitioned.com";
+ let cookieNum3 = 3;
+ for (let i = 0; i < cookieNum3; i++) {
+ let cookie = new Cookie(
+ "cookie-name" + i,
+ "cookie-value" + i,
+ hostPartitioned,
+ "/", // path
+ now, // expiry
+ now, // needed to get the cookie by the db init
+ now, // creation time
+ false, // session
+ false, // secure
+ false, // http-only
+ false, // inBrowserElement
+ { partitionKey: "(https,example.com)" }
+ );
+ schema12db.insertCookie(cookie);
+ }
+
+ // add some partitioned with different OA
+ let cookieNum4 = 35;
+ for (let i = 0; i < cookieNum4; i++) {
+ let cookie = new Cookie(
+ "cookie-name" + i,
+ "cookie-value" + i,
+ hostPartitioned,
+ "/", // path
+ now, // expiry
+ now, // needed to get the cookie by the db init
+ now, // creation time
+ false, // session
+ false, // secure
+ false, // http-only
+ false, // inBrowserElement
+ { partitionKey: "(https,example.com)", userContextId: 7 }
+ );
+ schema12db.insertCookie(cookie);
+ }
+
+ let allCookieCount = cookieNum1 + cookieNum2 + cookieNum3 + cookieNum4;
+ Assert.equal(do_count_cookies_in_db(schema12db.db), allCookieCount);
+
+ // startup the cookie service and check the cookie counts
+ let cookieCountNonPart =
+ Services.cookies.countCookiesFromHost(hostNonPartitioned); // includes expired cookies
+ Assert.equal(cookieCountNonPart, cookieNum1);
+ let cookieCountNonPartOA = Services.cookies.getCookiesFromHost(
+ hostNonPartitioned,
+ { userContextId: 8 }
+ ).length; // includes expired cookies
+ Assert.equal(cookieCountNonPartOA, cookieNum2);
+ let cookieCountPart = Services.cookies.getCookiesFromHost(hostPartitioned, {
+ partitionKey: "(https,example.com)",
+ }).length; // includes expired cookies
+ Assert.equal(cookieCountPart, cookieNum3);
+ let cookieCountPartOA = Services.cookies.getCookiesFromHost(hostPartitioned, {
+ partitionKey: "(https,example.com)",
+ userContextId: 7,
+ }).length; // includes expired cookies
+ Assert.equal(cookieCountPartOA, cookieNum4);
+
+ // trigger the collection
+ Services.obs.notifyObservers(null, "idle-daily");
+
+ // check telem fired for all cookie count
+ let cct = await Glean.networking.cookieCountTotal.testGetValue();
+ Assert.equal(cct.sum, allCookieCount, "All cookies telem");
+
+ // check telem for all un/partitioned counts
+ let ccp = await Glean.networking.cookieCountPartitioned.testGetValue();
+ Assert.equal(
+ ccp.sum,
+ cookieNum3 + cookieNum4,
+ "All partitioned cookies telem"
+ );
+
+ let ccu = await Glean.networking.cookieCountUnpartitioned.testGetValue();
+ Assert.equal(
+ ccu.sum,
+ cookieNum1 + cookieNum2,
+ "All unpartitioned cookies telem"
+ );
+
+ // check telem for part by key (host+OA)
+ // Note: With the decided histogram layout we see the buckets
+ // (used for indexing the histogram's buckets)
+ let histPartByKey =
+ await Glean.networking.cookieCountPartByKey.testGetValue();
+ Assert.equal(histPartByKey.values[2], 1, "Partitioned bucket 2 has 1 value");
+ Assert.equal(
+ histPartByKey.values[31],
+ 1,
+ "Partitioned bucket 31 has 1 value"
+ );
+ Assert.equal(
+ histPartByKey.sum,
+ cookieNum3 + cookieNum4,
+ "Partitioned bucket sums correctly"
+ );
+
+ // check telem for unpart by key (host+OA)
+ let histUnpartByKey =
+ await Glean.networking.cookieCountUnpartByKey.testGetValue();
+ Assert.equal(
+ histUnpartByKey.values[1],
+ 1,
+ "Unpartitioned bucket 1 has 1 value"
+ );
+ Assert.equal(
+ histUnpartByKey.values[8],
+ 1,
+ "Unpartitioned bucket 8 has 1 value"
+ );
+ Assert.equal(
+ histUnpartByKey.sum,
+ cookieNum1 + cookieNum2,
+ "Unpartitioned bucket sums correctly"
+ );
+
+ schema12db.close();
+});
diff --git a/netwerk/test/unit/test_cookies_privatebrowsing.js b/netwerk/test/unit/test_cookies_privatebrowsing.js
new file mode 100644
index 0000000000..9d3528440a
--- /dev/null
+++ b/netwerk/test/unit/test_cookies_privatebrowsing.js
@@ -0,0 +1,132 @@
+/* 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);
+ Services.prefs.setBoolPref("dom.security.https_first", false);
+
+ // Test with https-first-mode disabled in PBM
+ Services.prefs.setBoolPref("dom.security.https_first_pbm", false);
+
+ 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.cookies.setCookieStringFromHttp(
+ uri1,
+ "oh=hai; max-age=1000",
+ make_channel(uri1.spec)
+ );
+ Assert.equal(Services.cookies.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.cookies.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.cookies.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.cookies.countCookiesFromHost(uri1.host), 1);
+ Assert.equal(Services.cookies.countCookiesFromHost(uri2.host), 0);
+
+ // Fake a profile change.
+ await promise_close_profile();
+ do_load_profile();
+
+ // Check that the right cookie persisted.
+ Assert.equal(Services.cookies.countCookiesFromHost(uri1.host), 1);
+ Assert.equal(Services.cookies.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.cookies.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.cookies.countCookiesFromHost(uri1.host), 1);
+ Assert.equal(Services.cookies.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.cookies.countCookiesFromHost(uri1.host), 1);
+ Assert.equal(Services.cookies.countCookiesFromHost(uri2.host), 0);
+
+ // Let's release the last PB window.
+ privateBrowsingHolder.close();
+ Services.prefs.clearUserPref("dom.security.https_first");
+});
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..6ea9ab23f3
--- /dev/null
+++ b/netwerk/test/unit/test_cookies_profile_close.js
@@ -0,0 +1,114 @@
+/* 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
+ );
+ Services.prefs.setBoolPref("dom.security.https_first", false);
+
+ // 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.cookies.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.cookies.getCookieStringFromHttp(uri, channel), "");
+
+ await CookieXPCShellUtils.setCookieToDocument(uri.spec, "oh2=hai");
+
+ Services.cookies.setCookieStringFromHttp(uri, "oh3=hai", channel);
+ Assert.equal(
+ await CookieXPCShellUtils.getCookieStringFromDocument("http://foo.com/"),
+ ""
+ );
+
+ do_check_throws(function () {
+ Services.cookies.removeAll();
+ }, Cr.NS_ERROR_NOT_AVAILABLE);
+
+ do_check_throws(function () {
+ Services.cookies.cookies;
+ }, Cr.NS_ERROR_NOT_AVAILABLE);
+
+ do_check_throws(function () {
+ Services.cookies.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.cookies.remove("foo.com", "", "oh4", {});
+ }, Cr.NS_ERROR_NOT_AVAILABLE);
+
+ do_check_throws(function () {
+ Services.cookies.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.cookies.cookieExists(cookie.host, cookie.path, cookie.name, {})
+ );
+ Services.prefs.clearUserPref("dom.security.https_first");
+});
diff --git a/netwerk/test/unit/test_cookies_purge_counting.js b/netwerk/test/unit/test_cookies_purge_counting.js
new file mode 100644
index 0000000000..d92fabfbba
--- /dev/null
+++ b/netwerk/test/unit/test_cookies_purge_counting.js
@@ -0,0 +1,74 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_setup(function test_setup() {
+ // FOG needs a profile directory to put its data in.
+ do_get_profile();
+
+ // FOG needs to be initialized in order for data to flow.
+ Services.fog.initializeFOG();
+});
+
+add_task(async function test_purge_counting() {
+ let profile = do_get_profile();
+ let dbFile = do_get_cookie_file(profile);
+ Assert.ok(!dbFile.exists());
+
+ let schema12db = new CookieDatabaseConnection(dbFile, 12);
+
+ let now = Date.now() * 1000; // date in microseconds
+ let pastExpiry = Math.round(now / 1e6 - 1000);
+ let futureExpiry = Math.round(now / 1e6 + 1000);
+
+ let manyHosts = 50;
+ let manyCookies = 140;
+ let totalCookies = manyHosts * manyCookies;
+
+ // add many expired cookies for each host
+ for (let hostNum = 0; hostNum < manyHosts; hostNum++) {
+ let host = "cookie-host" + hostNum + ".com";
+ for (let i = 0; i < manyCookies; i++) {
+ let cookie = new Cookie(
+ "cookie-name" + i,
+ "cookie-value" + i,
+ host,
+ "/", // path
+ pastExpiry,
+ pastExpiry, // needed to get the cookie by the db init
+ now,
+ false,
+ false,
+ false
+ );
+ schema12db.insertCookie(cookie);
+ }
+ }
+
+ let validCookies = Services.cookies.cookies.length; // includes expired cookies
+ Assert.equal(validCookies, totalCookies);
+
+ // add a valid cookie - this triggers the purge
+ Services.cookies.add(
+ "cookie-host0.com", // any host
+ "/", // path
+ "cookie-name-x",
+ "cookie-value-x",
+ false, // secure
+ true, // http-only
+ true, // isSession
+ futureExpiry,
+ {}, // OA
+ Ci.nsICookie.SAMESITE_NONE, // SameSite
+ Ci.nsICookie.SCHEME_HTTPS
+ );
+
+ // check that we purge all the expired cookies and not the unexpired
+ validCookies = Services.cookies.cookies.length;
+ Assert.equal(validCookies, 1);
+
+ // check that the telemetry fired
+ let cpm = await Glean.networking.cookiePurgeMax.testGetValue();
+ Assert.equal(cpm.sum, totalCookies, "Purge the expected number of cookies");
+
+ schema12db.close();
+});
diff --git a/netwerk/test/unit/test_cookies_purge_counting_per_host.js b/netwerk/test/unit/test_cookies_purge_counting_per_host.js
new file mode 100644
index 0000000000..83a10c75ac
--- /dev/null
+++ b/netwerk/test/unit/test_cookies_purge_counting_per_host.js
@@ -0,0 +1,81 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_setup(function test_setup() {
+ // FOG needs a profile directory to put its data in.
+ do_get_profile();
+
+ // FOG needs to be initialized in order for data to flow.
+ Services.fog.initializeFOG();
+});
+
+add_task(async function test_purge_counting_per_host() {
+ let profile = do_get_profile();
+ let dbFile = do_get_cookie_file(profile);
+ Assert.ok(!dbFile.exists());
+
+ let schema12db = new CookieDatabaseConnection(dbFile, 12);
+
+ let now = Date.now() * 1000; // date in microseconds
+ let pastExpiry = Math.round(now / 1e6 - 1000);
+ let futureExpiry = Math.round(now / 1e6 + 1000);
+
+ let host = "cookie-host1.com";
+
+ let manyCookies = 180;
+ let cookieMax = 150;
+ let cookiesPurged = manyCookies - cookieMax;
+
+ // add many expired cookies for a single host
+ for (let i = 0; i < manyCookies; i++) {
+ let cookie = new Cookie(
+ "cookie-name" + i,
+ "cookie-value" + i,
+ host,
+ "/", // path
+ pastExpiry,
+ now, // last accessed
+ now, // creation time
+ false,
+ false,
+ false
+ );
+ schema12db.insertCookie(cookie);
+ }
+
+ // check that the cookies were added to the db
+ Assert.equal(do_count_cookies_in_db(schema12db.db), manyCookies);
+ Assert.equal(
+ do_count_cookies_in_db(schema12db.db, "cookie-host1.com"),
+ manyCookies
+ );
+
+ // startup the cookie service and check the cookie count
+ let validCookies = Services.cookies.countCookiesFromHost(host); // includes expired cookies
+ Assert.equal(validCookies, manyCookies);
+
+ // add a cookie - this will trigger the purge
+ Services.cookies.add(
+ host,
+ "/", // path
+ "cookie-name-x",
+ "cookie-value-x",
+ false, // secure
+ true, // http-only
+ true, // isSession
+ futureExpiry,
+ {}, // OA
+ Ci.nsICookie.SAMESITE_NONE, // SameSite
+ Ci.nsICookie.SCHEME_HTTPS
+ );
+
+ // check that we purge down to the cookieMax (plus the cookie added)
+ validCookies = Services.cookies.countCookiesFromHost(host);
+ Assert.equal(validCookies, cookieMax + 1);
+
+ // check that the telemetry fired
+ let cpem = await Glean.networking.cookiePurgeEntryMax.testGetValue();
+ Assert.equal(cpem.sum, cookiesPurged, "Purge the expected number of cookies");
+
+ schema12db.close();
+});
diff --git a/netwerk/test/unit/test_cookies_read.js b/netwerk/test/unit/test_cookies_read.js
new file mode 100644
index 0000000000..e7e9e84b3e
--- /dev/null
+++ b/netwerk/test/unit/test_cookies_read.js
@@ -0,0 +1,119 @@
+/* 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
+ );
+
+ // Bug 1617611 - Fix all the tests broken by "cookies SameSite=Lax by default"
+ Services.prefs.setBoolPref("network.cookie.sameSite.laxByDefault", false);
+
+ // Start the cookieservice, to force creation of a database.
+ // Get the sessionCookies to join the initialization in cookie thread
+ Services.cookies.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) {
+ uri = NetUtil.newURI("http://" + i + ".com/");
+ Services.cookies.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.cookies.countCookiesFromHost("999.com"), 1);
+ Assert.equal(Services.cookies.countCookiesFromHost("abc.com"), 0);
+ Assert.equal(Services.cookies.countCookiesFromHost("100.com"), 1);
+ Assert.equal(Services.cookies.countCookiesFromHost("400.com"), 1);
+ Assert.equal(Services.cookies.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.cookies.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.cookies.remove(host, "oh", "/", {});
+ }
+ for (let i = CMAX - 100; i < CMAX; ++i) {
+ let host = i.toString() + ".com";
+ Services.cookies.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.cookies.countCookiesFromHost(host), 1);
+ }
+
+ Services.prefs.clearUserPref("network.cookie.sameSite.laxByDefault");
+});
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..6c420d1cb0
--- /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 = 13;
+
+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.cookies.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.cookies.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..54d03bd709
--- /dev/null
+++ b/netwerk/test/unit/test_cookies_thirdparty.js
@@ -0,0 +1,164 @@
+/* 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("dom.security.https_first", false);
+
+ // Bug 1617611 - Fix all the tests broken by "cookies SameSite=Lax by default"
+ Services.prefs.setBoolPref("network.cookie.sameSite.laxByDefault", 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();
+ }
+ Services.prefs.clearUserPref("dom.security.https_first");
+ Services.prefs.clearUserPref("network.cookie.sameSite.laxByDefault");
+});
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..eefd5d87f9
--- /dev/null
+++ b/netwerk/test/unit/test_cookies_thirdparty_session.js
@@ -0,0 +1,77 @@
+/* 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
+ );
+ Services.prefs.setBoolPref("dom.security.https_first", false);
+
+ // Bug 1617611 - Fix all the tests broken by "cookies SameSite=Lax by default"
+ Services.prefs.setBoolPref("network.cookie.sameSite.laxByDefault", false);
+
+ 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);
+ Services.prefs.clearUserPref("dom.security.https_first");
+ Services.prefs.clearUserPref("network.cookie.sameSite.laxByDefault");
+});
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..8d9e595a28
--- /dev/null
+++ b/netwerk/test/unit/test_cookies_upgrade_10.js
@@ -0,0 +1,61 @@
+/* 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.startup.advanceShutdownPhase(
+ Services.startup.SHUTDOWN_PHASE_APPSHUTDOWN
+ );
+
+ // check for upgraded schema.
+ Assert.equal(13, 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..484ad469de
--- /dev/null
+++ b/netwerk/test/unit/test_data_protocol.js
@@ -0,0 +1,90 @@
+/* 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",
+ "foo bar",
+ ],
+ [
+ "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",
+ ],
+ ["data:text/plain;base64;x=y,dGVzdA==", "text/plain", "dGVzdA=="],
+ ["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..36f6432db4
--- /dev/null
+++ b/netwerk/test/unit/test_defaultURI.js
@@ -0,0 +1,186 @@
+"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.hasUserPass, true);
+ 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_dns_by_type_resolve.js b/netwerk/test/unit/test_dns_by_type_resolve.js
new file mode 100644
index 0000000000..26b087f301
--- /dev/null
+++ b/netwerk/test/unit/test_dns_by_type_resolve.js
@@ -0,0 +1,83 @@
+/* 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 h2Port;
+
+const { TestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TestUtils.sys.mjs"
+);
+
+add_setup(async function setup() {
+ h2Port = Services.env.get("MOZHTTP2_PORT");
+ Assert.notEqual(h2Port, null);
+ Assert.notEqual(h2Port, "");
+
+ trr_test_setup();
+ registerCleanupFunction(() => {
+ trr_clear_prefs();
+ });
+
+ if (mozinfo.socketprocess_networking) {
+ Services.dns; // Needed to trigger socket process.
+ await TestUtils.waitForCondition(() => Services.io.socketProcessLaunched);
+ }
+
+ Services.prefs.setIntPref("network.trr.mode", Ci.nsIDNSService.MODE_TRRFIRST);
+});
+
+let test_answer = "bXkgdm9pY2UgaXMgbXkgcGFzc3dvcmQ=";
+let test_answer_addr = "127.0.0.1";
+
+add_task(async function testTXTResolve() {
+ // use the h2 server as DOH provider
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ "https://foo.example.com:" + h2Port + "/doh"
+ );
+
+ let { inRecord } = await new TRRDNSListener("_esni.example.com", {
+ type: Ci.nsIDNSService.RESOLVE_TYPE_TXT,
+ });
+
+ 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() {
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ "https://foo.example.com:" + h2Port + "/txt-dns-push"
+ );
+ let { inRecord } = await new TRRDNSListener("_esni_push.example.com", {
+ type: Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT,
+ expectedAnswer: "127.0.0.1",
+ });
+
+ 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.
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ "https://foo.example.com:" + h2Port + "/404"
+ );
+ let { inRecord } = await new TRRDNSListener("_esni_push.example.com", {
+ type: Ci.nsIDNSService.RESOLVE_TYPE_TXT,
+ });
+
+ 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..c20e63ae4c
--- /dev/null
+++ b/netwerk/test/unit/test_dns_cancel.js
@@ -0,0 +1,119 @@
+"use strict";
+
+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 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 mainThread = Services.tm.currentThread;
+
+ var flags = Ci.nsIDNSService.RESOLVE_BYPASS_CACHE;
+
+ // This one will be canceled with cancelAsyncResolve.
+ Services.dns.asyncResolve(
+ hostname2,
+ Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT,
+ flags,
+ null, // resolverInfo
+ listener1,
+ mainThread,
+ defaultOriginAttributes
+ );
+ Services.dns.cancelAsyncResolve(
+ hostname2,
+ Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT,
+ flags,
+ null, // resolverInfo
+ listener1,
+ Cr.NS_ERROR_ABORT,
+ defaultOriginAttributes
+ );
+
+ // This one will not be canceled.
+ requestList1NotCanceled = Services.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 = Services.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 = Services.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 = Services.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..286964341e
--- /dev/null
+++ b/netwerk/test/unit/test_dns_disable_ipv4.js
@@ -0,0 +1,68 @@
+//
+// Tests that calling asyncResolve with the RESOLVE_DISABLE_IPV4 flag doesn't
+// return any IPv4 addresses.
+//
+
+"use strict";
+
+const gOverride = Cc["@mozilla.org/network/native-dns-override;1"].getService(
+ Ci.nsINativeDNSResolverOverride
+);
+
+const defaultOriginAttributes = {};
+
+add_task(async function test_none() {
+ let [, inRecord] = await new Promise(resolve => {
+ let listener = {
+ onLookupComplete(inRequest, inRecord1, inStatus) {
+ resolve([inRequest, inRecord1, inStatus]);
+ },
+ QueryInterface: ChromeUtils.generateQI(["nsIDNSListener"]),
+ };
+
+ Services.dns.asyncResolve(
+ "example.org",
+ Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT,
+ Ci.nsIDNSService.RESOLVE_DISABLE_IPV4,
+ null, // resolverInfo
+ listener,
+ null,
+ defaultOriginAttributes
+ );
+ });
+
+ if (inRecord && inRecord.QueryInterface(Ci.nsIDNSAddrRecord)) {
+ while (inRecord.hasMore()) {
+ let nextIP = inRecord.getNextAddrAsString();
+ ok(nextIP.includes(":"), `${nextIP} should be IPv6`);
+ }
+ }
+});
+
+add_task(async function test_some() {
+ Services.dns.clearCache(true);
+ gOverride.addIPOverride("example.com", "1.1.1.1");
+ gOverride.addIPOverride("example.org", "::1:2:3");
+ let [, inRecord] = await new Promise(resolve => {
+ let listener = {
+ onLookupComplete(inRequest, inRecord1, inStatus) {
+ resolve([inRequest, inRecord1, inStatus]);
+ },
+ QueryInterface: ChromeUtils.generateQI(["nsIDNSListener"]),
+ };
+
+ Services.dns.asyncResolve(
+ "example.org",
+ Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT,
+ Ci.nsIDNSService.RESOLVE_DISABLE_IPV4,
+ null, // resolverInfo
+ listener,
+ null,
+ defaultOriginAttributes
+ );
+ });
+
+ ok(inRecord.QueryInterface(Ci.nsIDNSAddrRecord));
+ equal(inRecord.getNextAddrAsString(), "::1:2:3");
+ equal(inRecord.hasMore(), false);
+});
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..d05c56091f
--- /dev/null
+++ b/netwerk/test/unit/test_dns_disable_ipv6.js
@@ -0,0 +1,51 @@
+//
+// Tests that calling asyncResolve with the RESOLVE_DISABLE_IPV6 flag doesn't
+// return any IPv6 addresses.
+//
+
+"use strict";
+
+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 {
+ Services.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..cfffd5530f
--- /dev/null
+++ b/netwerk/test/unit/test_dns_disabled.js
@@ -0,0 +1,88 @@
+"use strict";
+
+const override = Cc["@mozilla.org/network/native-dns-override;1"].getService(
+ Ci.nsINativeDNSResolverOverride
+);
+const mainThread = Services.tm.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 {
+ Services.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..3ca432f477
--- /dev/null
+++ b/netwerk/test/unit/test_dns_localredirect.js
@@ -0,0 +1,59 @@
+"use strict";
+
+var prefs = Services.prefs;
+
+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 mainThread = Services.tm.currentThread;
+ nextTest = do_test_2;
+ Services.dns.asyncResolve(
+ "local.vingtetun.org",
+ Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT,
+ 0,
+ null, // resolverInfo
+ listener,
+ mainThread,
+ defaultOriginAttributes
+ );
+
+ do_test_pending();
+}
+
+function do_test_2() {
+ var mainThread = Services.tm.currentThread;
+ nextTest = testsDone;
+ prefs.setCharPref("network.dns.forceResolve", "localhost");
+ Services.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..db9c436292
--- /dev/null
+++ b/netwerk/test/unit/test_dns_offline.js
@@ -0,0 +1,105 @@
+"use strict";
+
+var ioService = Services.io;
+var prefs = Services.prefs;
+var mainThread = Services.tm.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 {
+ Services.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() {
+ Services.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() {
+ Services.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..928753f71f
--- /dev/null
+++ b/netwerk/test/unit/test_dns_onion.js
@@ -0,0 +1,76 @@
+"use strict";
+
+var mainThread = Services.tm.currentThread;
+
+var onionPref;
+var localdomainPref;
+var prefs = Services.prefs;
+
+// 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);
+ Services.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 {
+ Services.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..39e2a4f0f1
--- /dev/null
+++ b/netwerk/test/unit/test_dns_originAttributes.js
@@ -0,0 +1,93 @@
+"use strict";
+
+var prefs = Services.prefs;
+var mainThread = Services.tm.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);
+ Services.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();
+ Services.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 {
+ Services.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..f092dd531c
--- /dev/null
+++ b/netwerk/test/unit/test_dns_override.js
@@ -0,0 +1,515 @@
+"use strict";
+
+const override = Cc["@mozilla.org/network/native-dns-override;1"].getService(
+ Ci.nsINativeDNSResolverOverride
+);
+const defaultOriginAttributes = {};
+const mainThread = Services.tm.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) {
+ 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_setup(async function setup() {
+ trr_test_setup();
+
+ registerCleanupFunction(async () => {
+ trr_clear_prefs();
+ });
+});
+
+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");
+ Services.dns.asyncResolve(
+ DOMAIN,
+ Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT,
+ 0,
+ null,
+ listener,
+ mainThread,
+ defaultOriginAttributes
+ );
+ Assert.equal(await listener.firstAddress(), "1.2.3.4");
+
+ Services.dns.clearCache(false);
+ override.clearOverrides();
+});
+
+add_task(async function test_ipv6() {
+ let listener = new Listener();
+ override.addIPOverride(DOMAIN, "fe80::6a99:9b2b:6ccc:6e1b");
+ Services.dns.asyncResolve(
+ DOMAIN,
+ Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT,
+ 0,
+ null,
+ listener,
+ mainThread,
+ defaultOriginAttributes
+ );
+ Assert.equal(await listener.firstAddress(), "fe80::6a99:9b2b:6ccc:6e1b");
+
+ Services.dns.clearCache(false);
+ override.clearOverrides();
+});
+
+add_task(async function test_clearOverrides() {
+ let listener = new Listener();
+ override.addIPOverride(DOMAIN, "1.2.3.4");
+ Services.dns.asyncResolve(
+ DOMAIN,
+ Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT,
+ 0,
+ null,
+ listener,
+ mainThread,
+ defaultOriginAttributes
+ );
+ Assert.equal(await listener.firstAddress(), "1.2.3.4");
+
+ Services.dns.clearCache(false);
+ override.clearOverrides();
+
+ listener = new Listener();
+ Services.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));
+ Services.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();
+ Services.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();
+ Services.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));
+ Services.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();
+ Services.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",
+ ]);
+
+ Services.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();
+ Services.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();
+ Services.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"]);
+
+ Services.dns.clearCache(false);
+ override.clearOverrides();
+});
+
+add_task(async function test_cname_flag() {
+ override.addIPOverride(DOMAIN, "2.2.2.2");
+ let listener = new Listener();
+ Services.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();
+ Services.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");
+
+ Services.dns.clearCache(false);
+ override.clearOverrides();
+
+ override.addIPOverride(DOMAIN, "2.2.2.2");
+ override.setCnameOverride(DOMAIN, OTHER);
+ listener = new Listener();
+ Services.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");
+
+ Services.dns.clearCache(false);
+ override.clearOverrides();
+});
+
+add_task(async function test_nxdomain() {
+ override.addIPOverride(DOMAIN, "N/A");
+ let listener = new Listener();
+ Services.dns.asyncResolve(
+ DOMAIN,
+ Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT,
+ Ci.nsIDNSService.RESOLVE_CANONICAL_NAME,
+ null,
+ listener,
+ mainThread,
+ defaultOriginAttributes
+ );
+
+ let [, , inStatus] = await listener;
+ equal(inStatus, Cr.NS_ERROR_UNKNOWN_HOST);
+});
+
+function makeChan(url) {
+ let chan = NetUtil.newChannel({
+ uri: url,
+ loadUsingSystemPrincipal: true,
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT,
+ }).QueryInterface(Ci.nsIHttpChannel);
+ return chan;
+}
+
+function channelOpenPromise(chan, flags) {
+ return new Promise(resolve => {
+ function finish(req, buffer) {
+ resolve([req, buffer]);
+ }
+ chan.asyncOpen(new ChannelListener(finish, null, flags));
+ });
+}
+
+function hexToUint8Array(hex) {
+ return new Uint8Array(hex.match(/.{1,2}/g).map(byte => parseInt(byte, 16)));
+}
+
+add_task(
+ {
+ skip_if: () => mozinfo.os == "win" || mozinfo.os == "android",
+ },
+ async function test_https_record_override() {
+ let trrServer = new TRRServer();
+ await trrServer.start();
+ registerCleanupFunction(async () => {
+ await trrServer.stop();
+ });
+
+ await trrServer.registerDoHAnswers("service.com", "HTTPS", {
+ answers: [
+ {
+ name: "service.com",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 1,
+ name: ".",
+ values: [
+ { key: "alpn", value: ["h2", "h3"] },
+ { key: "no-default-alpn" },
+ { key: "port", value: 8888 },
+ { key: "ipv4hint", value: "1.2.3.4" },
+ { key: "echconfig", value: "123..." },
+ { key: "ipv6hint", value: "::1" },
+ { key: "odoh", value: "456..." },
+ ],
+ },
+ },
+ {
+ name: "service.com",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 2,
+ name: "test.com",
+ values: [
+ { key: "alpn", value: "h2" },
+ { key: "ipv4hint", value: ["1.2.3.4", "5.6.7.8"] },
+ { key: "echconfig", value: "abc..." },
+ { key: "ipv6hint", value: ["::1", "fe80::794f:6d2c:3d5e:7836"] },
+ { key: "odoh", value: "def..." },
+ ],
+ },
+ },
+ ],
+ });
+
+ let chan = makeChan(
+ `https://foo.example.com:${trrServer.port()}/dnsAnswer?host=service.com&type=HTTPS`
+ );
+ let [, buf] = await channelOpenPromise(chan);
+ let rawBuffer = hexToUint8Array(buf);
+
+ override.addHTTPSRecordOverride("service.com", rawBuffer, rawBuffer.length);
+
+ Services.prefs.setBoolPref("network.dns.native_https_query", true);
+ registerCleanupFunction(async () => {
+ Services.prefs.clearUserPref("network.dns.native_https_query");
+ });
+
+ let listener = new Listener();
+ Services.dns.asyncResolve(
+ "service.com",
+ Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC,
+ 0,
+ null,
+ listener,
+ mainThread,
+ defaultOriginAttributes
+ );
+
+ let [, inRecord, inStatus] = await listener;
+ equal(inStatus, Cr.NS_OK);
+ let answer = inRecord.QueryInterface(Ci.nsIDNSHTTPSSVCRecord).records;
+ equal(answer[0].priority, 1);
+ equal(answer[0].name, "service.com");
+ Assert.deepEqual(
+ answer[0].values[0].QueryInterface(Ci.nsISVCParamAlpn).alpn,
+ ["h2", "h3"],
+ "got correct answer"
+ );
+ Assert.ok(
+ answer[0].values[1].QueryInterface(Ci.nsISVCParamNoDefaultAlpn),
+ "got correct answer"
+ );
+ Assert.equal(
+ answer[0].values[2].QueryInterface(Ci.nsISVCParamPort).port,
+ 8888,
+ "got correct answer"
+ );
+ Assert.equal(
+ answer[0].values[3].QueryInterface(Ci.nsISVCParamIPv4Hint).ipv4Hint[0]
+ .address,
+ "1.2.3.4",
+ "got correct answer"
+ );
+ Assert.equal(
+ answer[0].values[4].QueryInterface(Ci.nsISVCParamEchConfig).echconfig,
+ "123...",
+ "got correct answer"
+ );
+ Assert.equal(
+ answer[0].values[5].QueryInterface(Ci.nsISVCParamIPv6Hint).ipv6Hint[0]
+ .address,
+ "::1",
+ "got correct answer"
+ );
+ Assert.equal(
+ answer[0].values[6].QueryInterface(Ci.nsISVCParamODoHConfig).ODoHConfig,
+ "456...",
+ "got correct answer"
+ );
+
+ Assert.equal(answer[1].priority, 2);
+ Assert.equal(answer[1].name, "test.com");
+ Assert.equal(answer[1].values.length, 5);
+ Assert.deepEqual(
+ answer[1].values[0].QueryInterface(Ci.nsISVCParamAlpn).alpn,
+ ["h2"],
+ "got correct answer"
+ );
+ Assert.equal(
+ answer[1].values[1].QueryInterface(Ci.nsISVCParamIPv4Hint).ipv4Hint[0]
+ .address,
+ "1.2.3.4",
+ "got correct answer"
+ );
+ Assert.equal(
+ answer[1].values[1].QueryInterface(Ci.nsISVCParamIPv4Hint).ipv4Hint[1]
+ .address,
+ "5.6.7.8",
+ "got correct answer"
+ );
+ Assert.equal(
+ answer[1].values[2].QueryInterface(Ci.nsISVCParamEchConfig).echconfig,
+ "abc...",
+ "got correct answer"
+ );
+ Assert.equal(
+ answer[1].values[3].QueryInterface(Ci.nsISVCParamIPv6Hint).ipv6Hint[0]
+ .address,
+ "::1",
+ "got correct answer"
+ );
+ Assert.equal(
+ answer[1].values[3].QueryInterface(Ci.nsISVCParamIPv6Hint).ipv6Hint[1]
+ .address,
+ "fe80::794f:6d2c:3d5e:7836",
+ "got correct answer"
+ );
+ Assert.equal(
+ answer[1].values[4].QueryInterface(Ci.nsISVCParamODoHConfig).ODoHConfig,
+ "def...",
+ "got correct answer"
+ );
+ }
+);
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..ecd708ee53
--- /dev/null
+++ b/netwerk/test/unit/test_dns_override_for_localhost.js
@@ -0,0 +1,92 @@
+"use strict";
+
+const override = Cc["@mozilla.org/network/native-dns-override;1"].getService(
+ Ci.nsINativeDNSResolverOverride
+);
+const defaultOriginAttributes = {};
+const mainThread = Services.tm.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"]);
+
+const DOMAINS = [
+ "localhost",
+ "localhost.",
+ "vhost.localhost",
+ "vhost.localhost.",
+];
+DOMAINS.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.
+ Services.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();
+ Services.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");
+
+ Services.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..d53efc3dd3
--- /dev/null
+++ b/netwerk/test/unit/test_dns_proxy_bypass.js
@@ -0,0 +1,95 @@
+/* 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 prefs = Services.prefs;
+
+function setup() {
+ 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);
+}
+
+setup();
+registerCleanupFunction(async () => {
+ 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");
+});
+
+var url = "ws://dnsleak.example.com";
+
+var dnsRequestObserver = {
+ register() {
+ this.obs = Services.obs;
+ 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") {
+ Assert.ok(!data.includes("dnsleak.example.com"), `no dnsleak: ${data}`);
+ }
+ },
+};
+
+function WSListener(closure) {
+ this._closure = closure;
+}
+WSListener.prototype = {
+ onAcknowledge(aContext, aSize) {},
+ onBinaryMessageAvailable(aContext, aMsg) {},
+ onMessageAvailable(aContext, aMsg) {},
+ onServerClose(aContext, aCode, aReason) {},
+ onStart(aContext) {},
+ onStop(aContext, aStatusCode) {
+ dnsRequestObserver.unregister();
+ this._closure();
+ },
+};
+
+add_task(async function test_dns_websocket_channel() {
+ dnsRequestObserver.register();
+
+ var chan = Cc["@mozilla.org/network/protocol;1?name=ws"].createInstance(
+ Ci.nsIWebSocketChannel
+ );
+
+ var uri = Services.io.newURI(url);
+ chan.initLoadInfo(
+ null, // aLoadingNode
+ Services.scriptSecurityManager.createContentPrincipal(uri, {}),
+ null, // aTriggeringPrincipal
+ Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ Ci.nsIContentPolicy.TYPE_WEBSOCKET
+ );
+
+ await new Promise(resolve =>
+ chan.asyncOpen(uri, url, {}, 0, new WSListener(resolve), null)
+ );
+});
+
+add_task(async function test_dns_resolve_proxy() {
+ dnsRequestObserver.register();
+
+ let { error } = await new TRRDNSListener("dnsleak.example.com", {
+ expectEarlyFail: true,
+ });
+ Assert.equal(
+ error.result,
+ Cr.NS_ERROR_UNKNOWN_PROXY_HOST,
+ "error is NS_ERROR_UNKNOWN_PROXY_HOST"
+ );
+ dnsRequestObserver.unregister();
+});
diff --git a/netwerk/test/unit/test_dns_retry.js b/netwerk/test/unit/test_dns_retry.js
new file mode 100644
index 0000000000..24688417fa
--- /dev/null
+++ b/netwerk/test/unit/test_dns_retry.js
@@ -0,0 +1,319 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+trr_test_setup();
+let httpServerIPv4 = new HttpServer();
+let httpServerIPv6 = new HttpServer();
+let trrServer;
+let testpath = "/simple";
+let httpbody = "0123456789";
+let CC_IPV4 = "example_cc_ipv4.com";
+let CC_IPV6 = "example_cc_ipv6.com";
+Services.prefs.clearUserPref("network.dns.native-is-localhost");
+
+ChromeUtils.defineLazyGetter(this, "URL_CC_IPV4", function () {
+ return `http://${CC_IPV4}:${httpServerIPv4.identity.primaryPort}${testpath}`;
+});
+ChromeUtils.defineLazyGetter(this, "URL_CC_IPV6", function () {
+ return `http://${CC_IPV6}:${httpServerIPv6.identity.primaryPort}${testpath}`;
+});
+ChromeUtils.defineLazyGetter(this, "URL6a", function () {
+ return `http://example6a.com:${httpServerIPv6.identity.primaryPort}${testpath}`;
+});
+ChromeUtils.defineLazyGetter(this, "URL6b", function () {
+ return `http://example6b.com:${httpServerIPv6.identity.primaryPort}${testpath}`;
+});
+
+const ncs = Cc[
+ "@mozilla.org/network/network-connectivity-service;1"
+].getService(Ci.nsINetworkConnectivityService);
+const { TestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TestUtils.sys.mjs"
+);
+
+registerCleanupFunction(async () => {
+ Services.prefs.clearUserPref("network.http.speculative-parallel-limit");
+ Services.prefs.clearUserPref("network.captive-portal-service.testMode");
+ Services.prefs.clearUserPref("network.connectivity-service.IPv6.url");
+ Services.prefs.clearUserPref("network.connectivity-service.IPv4.url");
+ Services.prefs.clearUserPref("network.dns.localDomains");
+
+ trr_clear_prefs();
+ await httpServerIPv4.stop();
+ await httpServerIPv6.stop();
+ await trrServer.stop();
+});
+
+function makeChan(url) {
+ let chan = NetUtil.newChannel({
+ uri: url,
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+ chan.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE;
+ chan.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING;
+ chan.setTRRMode(Ci.nsIRequest.TRR_DEFAULT_MODE);
+ return chan;
+}
+
+function serverHandler(metadata, response) {
+ response.setHeader("Content-Type", "text/plain", false);
+ response.bodyOutputStream.write(httpbody, httpbody.length);
+}
+
+add_task(async function test_setup() {
+ httpServerIPv4.registerPathHandler(testpath, serverHandler);
+ httpServerIPv4.start(-1);
+ httpServerIPv6.registerPathHandler(testpath, serverHandler);
+ httpServerIPv6.start_ipv6(-1);
+ Services.prefs.setCharPref(
+ "network.dns.localDomains",
+ `foo.example.com, ${CC_IPV4}, ${CC_IPV6}`
+ );
+
+ trrServer = new TRRServer();
+ await trrServer.start();
+
+ if (mozinfo.socketprocess_networking) {
+ Services.dns; // Needed to trigger socket process.
+ await TestUtils.waitForCondition(() => Services.io.socketProcessLaunched);
+ }
+
+ Services.prefs.setIntPref("network.trr.mode", 3);
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${trrServer.port()}/dns-query`
+ );
+
+ await registerDoHAnswers(true, true);
+});
+
+async function registerDoHAnswers(ipv4, ipv6) {
+ let hosts = ["example6a.com", "example6b.com"];
+ for (const host of hosts) {
+ let ipv4answers = [];
+ if (ipv4) {
+ ipv4answers = [
+ {
+ name: host,
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "127.0.0.1",
+ },
+ ];
+ }
+ await trrServer.registerDoHAnswers(host, "A", {
+ answers: ipv4answers,
+ });
+
+ let ipv6answers = [];
+ if (ipv6) {
+ ipv6answers = [
+ {
+ name: host,
+ ttl: 55,
+ type: "AAAA",
+ flush: false,
+ data: "::1",
+ },
+ ];
+ }
+
+ await trrServer.registerDoHAnswers(host, "AAAA", {
+ answers: ipv6answers,
+ });
+ }
+
+ Services.dns.clearCache(true);
+}
+
+let StatusCounter = function () {
+ this._statusCount = {};
+};
+StatusCounter.prototype = {
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIInterfaceRequestor",
+ "nsIProgressEventSink",
+ ]),
+
+ getInterface(iid) {
+ return this.QueryInterface(iid);
+ },
+
+ onProgress(request, progress, progressMax) {},
+ onStatus(request, status, statusArg) {
+ this._statusCount[status] = 1 + (this._statusCount[status] || 0);
+ },
+};
+
+let HttpListener = function (finish, succeeded) {
+ this.finish = finish;
+ this.succeeded = succeeded;
+};
+
+HttpListener.prototype = {
+ onStartRequest: function testOnStartRequest(request) {},
+
+ onDataAvailable: function testOnDataAvailable(request, stream, off, cnt) {
+ read_stream(stream, cnt);
+ },
+
+ onStopRequest: function testOnStopRequest(request, status) {
+ equal(this.succeeded, status == Cr.NS_OK);
+ this.finish();
+ },
+};
+
+function promiseObserverNotification(aTopic, matchFunc) {
+ return new Promise((resolve, reject) => {
+ Services.obs.addObserver(function observe(subject, topic, data) {
+ let matches = typeof matchFunc != "function" || matchFunc(subject, data);
+ if (!matches) {
+ return;
+ }
+ Services.obs.removeObserver(observe, topic);
+ resolve({ subject, data });
+ }, aTopic);
+ });
+}
+
+async function make_request(uri, check_events, succeeded) {
+ let chan = makeChan(uri);
+ let statusCounter = new StatusCounter();
+ chan.notificationCallbacks = statusCounter;
+ await new Promise(resolve =>
+ chan.asyncOpen(new HttpListener(resolve, succeeded))
+ );
+
+ if (check_events) {
+ equal(
+ statusCounter._statusCount[0x4b000b] || 0,
+ 1,
+ "Expecting only one instance of NS_NET_STATUS_RESOLVED_HOST"
+ );
+ equal(
+ statusCounter._statusCount[0x4b0007] || 0,
+ 1,
+ "Expecting only one instance of NS_NET_STATUS_CONNECTING_TO"
+ );
+ }
+}
+
+async function setup_connectivity(ipv6, ipv4) {
+ Services.prefs.setBoolPref("network.captive-portal-service.testMode", true);
+
+ if (ipv6) {
+ Services.prefs.setCharPref(
+ "network.connectivity-service.IPv6.url",
+ URL_CC_IPV6 + testpath
+ );
+ } else {
+ Services.prefs.setCharPref(
+ "network.connectivity-service.IPv6.url",
+ "http://donotexist.example.com"
+ );
+ }
+
+ if (ipv4) {
+ Services.prefs.setCharPref(
+ "network.connectivity-service.IPv4.url",
+ URL_CC_IPV4 + testpath
+ );
+ } else {
+ Services.prefs.setCharPref(
+ "network.connectivity-service.IPv4.url",
+ "http://donotexist.example.com"
+ );
+ }
+
+ let topic = "network:connectivity-service:ip-checks-complete";
+ if (mozinfo.socketprocess_networking) {
+ topic += "-from-socket-process";
+ }
+ let observerNotification = promiseObserverNotification(topic);
+ ncs.recheckIPConnectivity();
+ await observerNotification;
+
+ if (!ipv6) {
+ equal(
+ ncs.IPv6,
+ Ci.nsINetworkConnectivityService.NOT_AVAILABLE,
+ "Check IPv6 support"
+ );
+ } else {
+ equal(ncs.IPv6, Ci.nsINetworkConnectivityService.OK, "Check IPv6 support");
+ }
+
+ if (!ipv4) {
+ equal(
+ ncs.IPv4,
+ Ci.nsINetworkConnectivityService.NOT_AVAILABLE,
+ "Check IPv4 support"
+ );
+ } else {
+ equal(ncs.IPv4, Ci.nsINetworkConnectivityService.OK, "Check IPv4 support");
+ }
+}
+
+// This test that we retry to connect using IPv4 when IPv6 connecivity is not
+// present, but a ConnectionEntry have IPv6 prefered set.
+// Speculative connections are disabled.
+add_task(async function test_prefer_address_version_fail_trr3_1() {
+ Services.prefs.setIntPref("network.http.speculative-parallel-limit", 0);
+ await registerDoHAnswers(true, true);
+
+ // Make a request to setup the address version preference to a ConnectionEntry.
+ await make_request(URL6a, true, true);
+
+ // connect again using the address version preference from the ConnectionEntry.
+ await make_request(URL6a, true, true);
+
+ // Make IPv6 connectivity check fail
+ await setup_connectivity(false, true);
+
+ Services.dns.clearCache(true);
+
+ // This will succeed as we query both DNS records
+ await make_request(URL6a, true, true);
+
+ // Now make the DNS server only return IPv4 records
+ await registerDoHAnswers(true, false);
+ // This will fail, because the server is not lisenting to IPv4 address as well,
+ // We should still get NS_NET_STATUS_RESOLVED_HOST and
+ // NS_NET_STATUS_CONNECTING_TO notification.
+ await make_request(URL6a, true, false);
+
+ // Make IPv6 connectivity check succeed again
+ await setup_connectivity(true, true);
+});
+
+// This test that we retry to connect using IPv4 when IPv6 connecivity is not
+// present, but a ConnectionEntry have IPv6 prefered set.
+// Speculative connections are enabled.
+add_task(async function test_prefer_address_version_fail_trr3_2() {
+ Services.prefs.setIntPref("network.http.speculative-parallel-limit", 6);
+ await registerDoHAnswers(true, true);
+
+ // Make a request to setup the address version preference to a ConnectionEntry.
+ await make_request(URL6b, false, true);
+
+ // connect again using the address version preference from the ConnectionEntry.
+ await make_request(URL6b, false, true);
+
+ // Make IPv6 connectivity check fail
+ await setup_connectivity(false, true);
+
+ Services.dns.clearCache(true);
+
+ // This will succeed as we query both DNS records
+ await make_request(URL6b, false, true);
+
+ // Now make the DNS server only return IPv4 records
+ await registerDoHAnswers(true, false);
+ // This will fail, because the server is not lisenting to IPv4 address as well,
+ // We should still get NS_NET_STATUS_RESOLVED_HOST and
+ // NS_NET_STATUS_CONNECTING_TO notification.
+ await make_request(URL6b, true, false);
+});
diff --git a/netwerk/test/unit/test_dns_service.js b/netwerk/test/unit/test_dns_service.js
new file mode 100644
index 0000000000..da404c1e7d
--- /dev/null
+++ b/netwerk/test/unit/test_dns_service.js
@@ -0,0 +1,123 @@
+"use strict";
+
+const defaultOriginAttributes = {};
+const mainThread = Services.tm.currentThread;
+
+const overrideService = Cc[
+ "@mozilla.org/network/native-dns-override;1"
+].getService(Ci.nsINativeDNSResolverOverride);
+
+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 ADDR1 = "127.0.0.1";
+const ADDR2 = "::1";
+
+add_task(async function test_dns_localhost() {
+ let listener = new Listener();
+ Services.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();
+ Services.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");
+});
+
+add_task(
+ {
+ skip_if: () =>
+ Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT,
+ },
+ async function test_long_domain() {
+ let listener = new Listener();
+ let domain = "a".repeat(253);
+ overrideService.addIPOverride(domain, "1.2.3.4");
+ Services.dns.asyncResolve(
+ domain,
+ Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT,
+ Ci.nsIDNSService.RESOLVE_CANONICAL_NAME,
+ null, // resolverInfo
+ listener,
+ mainThread,
+ defaultOriginAttributes
+ );
+ let [, , inStatus] = await listener;
+ Assert.equal(inStatus, Cr.NS_OK);
+
+ listener = new Listener();
+ domain = "a".repeat(254);
+ overrideService.addIPOverride(domain, "1.2.3.4");
+
+ if (mozinfo.socketprocess_networking) {
+ // When using the socket process, the call fails asynchronously.
+ Services.dns.asyncResolve(
+ domain,
+ Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT,
+ Ci.nsIDNSService.RESOLVE_CANONICAL_NAME,
+ null, // resolverInfo
+ listener,
+ mainThread,
+ defaultOriginAttributes
+ );
+ let [, , inStatus1] = await listener;
+ Assert.equal(inStatus1, Cr.NS_ERROR_UNKNOWN_HOST);
+ } else {
+ Assert.throws(
+ () => {
+ Services.dns.asyncResolve(
+ domain,
+ Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT,
+ Ci.nsIDNSService.RESOLVE_CANONICAL_NAME,
+ null, // resolverInfo
+ listener,
+ mainThread,
+ defaultOriginAttributes
+ );
+ },
+ /NS_ERROR_UNKNOWN_HOST/,
+ "Should throw for large domains"
+ );
+ }
+ }
+);
diff --git a/netwerk/test/unit/test_domain_eviction.js b/netwerk/test/unit/test_domain_eviction.js
new file mode 100644
index 0000000000..58520c2daa
--- /dev/null
+++ b/netwerk/test/unit/test_domain_eviction.js
@@ -0,0 +1,182 @@
+/* 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.cookies.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.cookies.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.cookies.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.cookies.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.cookies.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.cookies.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.cookies.countCookiesFromHost(aBaseDomain),
+ cookies.length
+ );
+ Assert.equal(Services.cookies.countCookiesFromHost(aHost), cookies.length);
+
+ for (let cookie of Services.cookies.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_dooh.js b/netwerk/test/unit/test_dooh.js
new file mode 100644
index 0000000000..cab4b3bb02
--- /dev/null
+++ b/netwerk/test/unit/test_dooh.js
@@ -0,0 +1,354 @@
+/* Any copyright is dedicated to the Public Domain.
+ * https://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from trr_common.js */
+
+const { setTimeout } = ChromeUtils.importESModule(
+ "resource://gre/modules/Timer.sys.mjs"
+);
+
+let httpServer;
+let ohttpServer;
+let ohttpEncodedConfig = "not a valid config";
+
+// Decapsulate the request, send it to the actual TRR, receive the response,
+// encapsulate it, and send it back through `response`.
+async function forwardToTRR(request, response) {
+ let inputStream = Cc["@mozilla.org/scriptableinputstream;1"].createInstance(
+ Ci.nsIScriptableInputStream
+ );
+ inputStream.init(request.bodyInputStream);
+ let requestBody = inputStream.readBytes(inputStream.available());
+ let ohttpResponse = ohttpServer.decapsulate(stringToBytes(requestBody));
+ let bhttp = Cc["@mozilla.org/network/binary-http;1"].getService(
+ Ci.nsIBinaryHttp
+ );
+ let decodedRequest = bhttp.decodeRequest(ohttpResponse.request);
+ let headers = {};
+ for (
+ let i = 0;
+ i < decodedRequest.headerNames.length && decodedRequest.headerValues.length;
+ i++
+ ) {
+ headers[decodedRequest.headerNames[i]] = decodedRequest.headerValues[i];
+ }
+ let uri = `${decodedRequest.scheme}://${decodedRequest.authority}${decodedRequest.path}`;
+ let body = new Uint8Array(decodedRequest.content.length);
+ for (let i = 0; i < decodedRequest.content.length; i++) {
+ body[i] = decodedRequest.content[i];
+ }
+ try {
+ // Timeout after 10 seconds.
+ let fetchInProgress = true;
+ let controller = new AbortController();
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ setTimeout(() => {
+ if (fetchInProgress) {
+ controller.abort();
+ }
+ }, 10000);
+ let trrResponse = await fetch(uri, {
+ method: decodedRequest.method,
+ headers,
+ body: decodedRequest.method == "POST" ? body : undefined,
+ credentials: "omit",
+ signal: controller.signal,
+ });
+ fetchInProgress = false;
+ let data = new Uint8Array(await trrResponse.arrayBuffer());
+ let trrResponseContent = [];
+ for (let i = 0; i < data.length; i++) {
+ trrResponseContent.push(data[i]);
+ }
+ let trrResponseHeaderNames = [];
+ let trrResponseHeaderValues = [];
+ for (let header of trrResponse.headers) {
+ trrResponseHeaderNames.push(header[0]);
+ trrResponseHeaderValues.push(header[1]);
+ }
+ let binaryResponse = new BinaryHttpResponse(
+ trrResponse.status,
+ trrResponseHeaderNames,
+ trrResponseHeaderValues,
+ trrResponseContent
+ );
+ let responseBytes = bhttp.encodeResponse(binaryResponse);
+ let encResponse = ohttpResponse.encapsulate(responseBytes);
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "message/ohttp-res", false);
+ response.write(bytesToString(encResponse));
+ } catch (e) {
+ // Some tests involve the responder either timing out or closing the
+ // connection unexpectedly.
+ }
+}
+
+add_setup(async function setup() {
+ h2Port = trr_test_setup();
+ runningOHTTPTests = true;
+
+ if (mozinfo.socketprocess_networking) {
+ Services.dns; // Needed to trigger socket process.
+ await TestUtils.waitForCondition(() => Services.io.socketProcessLaunched);
+ }
+
+ Services.prefs.setIntPref("network.trr.mode", Ci.nsIDNSService.MODE_TRRONLY);
+
+ let ohttp = Cc["@mozilla.org/network/oblivious-http;1"].getService(
+ Ci.nsIObliviousHttp
+ );
+ ohttpServer = ohttp.server();
+
+ httpServer = new HttpServer();
+ httpServer.registerPathHandler("/relay", function (request, response) {
+ response.processAsync();
+ forwardToTRR(request, response).then(() => {
+ response.finish();
+ });
+ });
+ httpServer.registerPathHandler("/config", function (request, response) {
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "application/ohttp-keys", false);
+ response.write(ohttpEncodedConfig);
+ });
+ httpServer.start(-1);
+
+ Services.prefs.setBoolPref("network.trr.use_ohttp", true);
+ // On windows the TTL fetch will race with clearing the cache
+ // to refresh the cache entry.
+ Services.prefs.setBoolPref("network.dns.get-ttl", false);
+
+ registerCleanupFunction(async () => {
+ trr_clear_prefs();
+ Services.prefs.clearUserPref("network.trr.use_ohttp");
+ Services.prefs.clearUserPref("network.trr.ohttp.config_uri");
+ Services.prefs.clearUserPref("network.trr.ohttp.relay_uri");
+ Services.prefs.clearUserPref("network.trr.ohttp.uri");
+ Services.prefs.clearUserPref("network.dns.get-ttl");
+ await new Promise((resolve, reject) => {
+ httpServer.stop(resolve);
+ });
+ });
+});
+
+// Test that if DNS-over-OHTTP isn't configured, the implementation falls back
+// to platform resolution.
+add_task(async function test_ohttp_not_configured() {
+ Services.dns.clearCache(true);
+ setModeAndURI(2, "doh?responseIP=2.2.2.2");
+ await new TRRDNSListener("example.com", "127.0.0.1");
+});
+
+add_task(async function set_ohttp_invalid_prefs() {
+ let configPromise = TestUtils.topicObserved("ohttp-service-config-loaded");
+ Services.prefs.setCharPref(
+ "network.trr.ohttp.relay_uri",
+ "http://nonexistent.test"
+ );
+ Services.prefs.setCharPref(
+ "network.trr.ohttp.config_uri",
+ "http://nonexistent.test"
+ );
+
+ Cc["@mozilla.org/network/oblivious-http-service;1"].getService(
+ Ci.nsIObliviousHttpService
+ );
+ await configPromise;
+});
+
+// Test that if DNS-over-OHTTP has an invalid configuration, the implementation
+// falls back to platform resolution.
+add_task(async function test_ohttp_invalid_prefs_fallback() {
+ Services.dns.clearCache(true);
+ setModeAndURI(2, "doh?responseIP=2.2.2.2");
+ await new TRRDNSListener("example.com", "127.0.0.1");
+});
+
+add_task(async function set_ohttp_prefs_500_error() {
+ let configPromise = TestUtils.topicObserved("ohttp-service-config-loaded");
+ Services.prefs.setCharPref(
+ "network.trr.ohttp.relay_uri",
+ `http://localhost:${httpServer.identity.primaryPort}/relay`
+ );
+ Services.prefs.setCharPref(
+ "network.trr.ohttp.config_uri",
+ `http://localhost:${httpServer.identity.primaryPort}/500error`
+ );
+ await configPromise;
+});
+
+// Test that if DNS-over-OHTTP has an invalid configuration, the implementation
+// falls back to platform resolution.
+add_task(async function test_ohttp_500_error_fallback() {
+ Services.dns.clearCache(true);
+ setModeAndURI(2, "doh?responseIP=2.2.2.2");
+ await new TRRDNSListener("example.com", "127.0.0.1");
+});
+
+add_task(async function retryConfigOnConnectivityChange() {
+ Services.prefs.setCharPref("network.trr.confirmationNS", "skip");
+ // First we make sure the config is properly loaded
+ setModeAndURI(2, "doh?responseIP=2.2.2.2");
+ let ohttpService = Cc[
+ "@mozilla.org/network/oblivious-http-service;1"
+ ].getService(Ci.nsIObliviousHttpService);
+ ohttpService.clearTRRConfig();
+ ohttpEncodedConfig = bytesToString(ohttpServer.encodedConfig);
+ let configPromise = TestUtils.topicObserved("ohttp-service-config-loaded");
+ Services.prefs.setCharPref(
+ "network.trr.ohttp.relay_uri",
+ `http://localhost:${httpServer.identity.primaryPort}/relay`
+ );
+ Services.prefs.setCharPref(
+ "network.trr.ohttp.config_uri",
+ `http://localhost:${httpServer.identity.primaryPort}/config`
+ );
+ let [, status] = await configPromise;
+ equal(status, "success");
+ info("retryConfigOnConnectivityChange setup complete");
+
+ ohttpService.clearTRRConfig();
+
+ let port = httpServer.identity.primaryPort;
+ // Stop the server so getting the config fails.
+ await httpServer.stop();
+
+ configPromise = TestUtils.topicObserved("ohttp-service-config-loaded");
+ Services.obs.notifyObservers(
+ null,
+ "network:captive-portal-connectivity-changed"
+ );
+ [, status] = await configPromise;
+ equal(status, "failed");
+
+ // Should fallback to native DNS since the config is empty
+ Services.dns.clearCache(true);
+ await new TRRDNSListener("example.com", "127.0.0.1");
+
+ // Start the server back again.
+ httpServer.start(port);
+ Assert.equal(
+ port,
+ httpServer.identity.primaryPort,
+ "server should get the same port"
+ );
+
+ // Still the config hasn't been reloaded.
+ await new TRRDNSListener("example2.com", "127.0.0.1");
+
+ // Signal a connectivity change so we reload the config
+ configPromise = TestUtils.topicObserved("ohttp-service-config-loaded");
+ Services.obs.notifyObservers(
+ null,
+ "network:captive-portal-connectivity-changed"
+ );
+ [, status] = await configPromise;
+ equal(status, "success");
+
+ await new TRRDNSListener("example3.com", "2.2.2.2");
+
+ // Now check that we also reload a missing config if a TRR confirmation fails.
+ ohttpService.clearTRRConfig();
+ configPromise = TestUtils.topicObserved("ohttp-service-config-loaded");
+ Services.obs.notifyObservers(
+ null,
+ "network:trr-confirmation",
+ "CONFIRM_FAILED"
+ );
+ [, status] = await configPromise;
+ equal(status, "success");
+ await new TRRDNSListener("example4.com", "2.2.2.2");
+
+ // set the config to an invalid value and check that as long as the URL
+ // doesn't change, we dont reload it again on connectivity notifications.
+ ohttpEncodedConfig = "not a valid config";
+ configPromise = TestUtils.topicObserved("ohttp-service-config-loaded");
+ Services.obs.notifyObservers(
+ null,
+ "network:captive-portal-connectivity-changed"
+ );
+
+ await new TRRDNSListener("example5.com", "2.2.2.2");
+
+ // The change should not cause any config reload because we already have a config.
+ [, status] = await configPromise;
+ equal(status, "no-changes");
+
+ await new TRRDNSListener("example6.com", "2.2.2.2");
+ // Clear the config_uri pref so it gets set to the proper value in the next test.
+ configPromise = TestUtils.topicObserved("ohttp-service-config-loaded");
+ Services.prefs.setCharPref("network.trr.ohttp.config_uri", ``);
+ await configPromise;
+});
+
+add_task(async function set_ohttp_prefs_valid() {
+ let ohttpService = Cc[
+ "@mozilla.org/network/oblivious-http-service;1"
+ ].getService(Ci.nsIObliviousHttpService);
+ ohttpService.clearTRRConfig();
+ let configPromise = TestUtils.topicObserved("ohttp-service-config-loaded");
+ ohttpEncodedConfig = bytesToString(ohttpServer.encodedConfig);
+ Services.prefs.setCharPref(
+ "network.trr.ohttp.config_uri",
+ `http://localhost:${httpServer.identity.primaryPort}/config`
+ );
+ await configPromise;
+});
+
+add_task(test_A_record);
+
+add_task(test_AAAA_records);
+
+add_task(test_RFC1918);
+
+add_task(test_GET_ECS);
+
+add_task(test_timeout_mode3);
+
+add_task(test_strict_native_fallback);
+
+add_task(test_no_answers_fallback);
+
+add_task(test_404_fallback);
+
+add_task(test_mode_1_and_4);
+
+add_task(test_CNAME);
+
+add_task(test_name_mismatch);
+
+add_task(test_mode_2);
+
+add_task(test_excluded_domains);
+
+add_task(test_captiveportal_canonicalURL);
+
+add_task(test_parentalcontrols);
+
+// TRR-first check that DNS result is used if domain is part of the builtin-excluded-domains pref
+add_task(test_builtin_excluded_domains);
+
+add_task(test_excluded_domains_mode3);
+
+add_task(test25e);
+
+add_task(test_parentalcontrols_mode3);
+
+add_task(test_builtin_excluded_domains_mode3);
+
+add_task(count_cookies);
+
+// This test doesn't work with having a JS httpd server as a relay.
+// add_task(test_connection_closed);
+
+add_task(test_fetch_time);
+
+add_task(test_fqdn);
+
+add_task(test_ipv6_trr_fallback);
+
+add_task(test_ipv4_trr_fallback);
+
+add_task(test_no_retry_without_doh);
diff --git a/netwerk/test/unit/test_doomentry.js b/netwerk/test/unit/test_doomentry.js
new file mode 100644
index 0000000000..07e17a1080
--- /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) {
+ Services.cache2
+ .diskCacheStorage(Services.loadContextInfo.default)
+ .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 (stat, 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..78d2355d1e
--- /dev/null
+++ b/netwerk/test/unit/test_duplicate_headers.js
@@ -0,0 +1,563 @@
+/*
+ * Tests bugs 597706, 655389: prevent duplicate headers with differing values
+ * for some headers like Content-Length, Location, etc.
+ */
+
+////////////////////////////////////////////////////////////////////////////////
+// Test infrastructure
+
+"use strict";
+
+// The tests in this file use number indexes to run, which can't be detected
+// via ESLint.
+/* eslint-disable no-unused-vars */
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+ChromeUtils.defineLazyGetter(this, "URL", function () {
+ return "http://localhost:" + httpserver.identity.primaryPort;
+});
+
+var httpserver = new HttpServer();
+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_early_hint_listener.js b/netwerk/test/unit/test_early_hint_listener.js
new file mode 100644
index 0000000000..73b95067fe
--- /dev/null
+++ b/netwerk/test/unit/test_early_hint_listener.js
@@ -0,0 +1,170 @@
+/* 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.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+var httpserver = new HttpServer();
+var earlyhintspath = "/earlyhints";
+var multipleearlyhintspath = "/multipleearlyhintspath";
+var otherearlyhintspath = "/otherearlyhintspath";
+var noearlyhintspath = "/noearlyhints";
+var httpbody = "0123456789";
+var hint1 = "</style.css>; rel=preload; as=style";
+var hint2 = "</img.png>; rel=preload; as=image";
+
+function earlyHintsResponse(metadata, response) {
+ response.setInformationalResponseStatusLine(
+ metadata.httpVersion,
+ 103,
+ "EarlyHints"
+ );
+ response.setInformationalResponseHeader("Link", hint1, false);
+
+ response.setHeader("Content-Type", "text/plain", false);
+ response.bodyOutputStream.write(httpbody, httpbody.length);
+}
+
+function multipleEarlyHintsResponse(metadata, response) {
+ response.setInformationalResponseStatusLine(
+ metadata.httpVersion,
+ 103,
+ "EarlyHints"
+ );
+ response.setInformationalResponseHeader("Link", hint1, false);
+ response.setInformationalHeaderNoCheck("Link", hint2);
+
+ response.setHeader("Content-Type", "text/plain", false);
+ response.bodyOutputStream.write(httpbody, httpbody.length);
+}
+
+function otherHeadersEarlyHintsResponse(metadata, response) {
+ response.setInformationalResponseStatusLine(
+ metadata.httpVersion,
+ 103,
+ "EarlyHints"
+ );
+ response.setInformationalResponseHeader("Link", hint1, false);
+ response.setInformationalResponseHeader("Something", "something", false);
+
+ response.setHeader("Content-Type", "text/plain", false);
+ response.bodyOutputStream.write(httpbody, httpbody.length);
+}
+
+function noEarlyHintsResponse(metadata, response) {
+ response.setHeader("Content-Type", "text/plain", false);
+ response.bodyOutputStream.write(httpbody, httpbody.length);
+}
+
+let EarlyHintsListener = function () {};
+
+EarlyHintsListener.prototype = {
+ _expected_hints: "",
+ earlyHintsReceived: false,
+
+ earlyHint: function testEarlyHint(header) {
+ Assert.equal(header, this._expected_hints);
+ this.earlyHintsReceived = true;
+ },
+};
+
+function chanPromise(uri, listener) {
+ return new Promise(resolve => {
+ var principal = Services.scriptSecurityManager.createContentPrincipal(
+ NetUtil.newURI(uri),
+ {}
+ );
+ var chan = NetUtil.newChannel({
+ uri,
+ loadingPrincipal: principal,
+ securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT,
+ });
+
+ chan
+ .QueryInterface(Ci.nsIHttpChannelInternal)
+ .setEarlyHintObserver(listener);
+ chan.asyncOpen(new ChannelListener(resolve));
+ });
+}
+
+add_task(async function setup() {
+ httpserver.registerPathHandler(earlyhintspath, earlyHintsResponse);
+ httpserver.registerPathHandler(
+ multipleearlyhintspath,
+ multipleEarlyHintsResponse
+ );
+ httpserver.registerPathHandler(
+ otherearlyhintspath,
+ otherHeadersEarlyHintsResponse
+ );
+ httpserver.registerPathHandler(noearlyhintspath, noEarlyHintsResponse);
+ httpserver.start(-1);
+});
+
+add_task(async function early_hints() {
+ let earlyHints = new EarlyHintsListener();
+ earlyHints._expected_hints = hint1;
+
+ await chanPromise(
+ "http://localhost:" + httpserver.identity.primaryPort + earlyhintspath,
+ earlyHints
+ );
+ Assert.ok(earlyHints.earlyHintsReceived);
+});
+
+add_task(async function no_early_hints() {
+ let earlyHints = new EarlyHintsListener("");
+
+ await chanPromise(
+ "http://localhost:" + httpserver.identity.primaryPort + noearlyhintspath,
+ earlyHints
+ );
+ Assert.ok(!earlyHints.earlyHintsReceived);
+});
+
+add_task(async function early_hints_multiple() {
+ let earlyHints = new EarlyHintsListener();
+ earlyHints._expected_hints = hint1 + ", " + hint2;
+
+ await chanPromise(
+ "http://localhost:" +
+ httpserver.identity.primaryPort +
+ multipleearlyhintspath,
+ earlyHints
+ );
+ Assert.ok(earlyHints.earlyHintsReceived);
+});
+
+add_task(async function early_hints_other() {
+ let earlyHints = new EarlyHintsListener();
+ earlyHints._expected_hints = hint1;
+
+ await chanPromise(
+ "http://localhost:" + httpserver.identity.primaryPort + otherearlyhintspath,
+ earlyHints
+ );
+ Assert.ok(earlyHints.earlyHintsReceived);
+});
+
+add_task(async function early_hints_only_secure_context() {
+ Services.prefs.setBoolPref("network.proxy.allow_hijacking_localhost", true);
+ let earlyHints2 = new EarlyHintsListener();
+ earlyHints2._expected_hints = "";
+
+ await chanPromise(
+ "http://localhost:" + httpserver.identity.primaryPort + earlyhintspath,
+ earlyHints2
+ );
+ Assert.ok(!earlyHints2.earlyHintsReceived);
+});
+
+add_task(async function clean_up() {
+ Services.prefs.clearUserPref("network.proxy.allow_hijacking_localhost");
+ await new Promise(resolve => {
+ httpserver.stop(resolve);
+ });
+});
diff --git a/netwerk/test/unit/test_early_hint_listener_http2.js b/netwerk/test/unit/test_early_hint_listener_http2.js
new file mode 100644
index 0000000000..3ea32f0bcc
--- /dev/null
+++ b/netwerk/test/unit/test_early_hint_listener_http2.js
@@ -0,0 +1,110 @@
+/* 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";
+
+// The server will always respond with a 103 EarlyHint followed by a
+// 200 response.
+// 103 response contains:
+// 1) a non-link header
+// 2) a link header if a request has a "link-to-set" header. If the
+// request header is not set, the response will not have Link headers.
+// A "link-to-set" header may contain multiple link headers
+// separated with a comma.
+
+var earlyhintspath = "/103_response";
+var hint1 = "</style.css>; rel=preload; as=style";
+var hint2 = "</img.png>; rel=preload; as=image";
+
+let EarlyHintsListener = function () {};
+
+EarlyHintsListener.prototype = {
+ _expected_hints: "",
+ earlyHintsReceived: false,
+
+ QueryInterface: ChromeUtils.generateQI(["nsIEarlyHintObserver"]),
+
+ earlyHint: function testEarlyHint(header) {
+ Assert.equal(header, this._expected_hints);
+ this.earlyHintsReceived = true;
+ },
+};
+
+function chanPromise(uri, listener, headers) {
+ return new Promise(resolve => {
+ var principal = Services.scriptSecurityManager.createContentPrincipal(
+ NetUtil.newURI(uri),
+ {}
+ );
+ var chan = NetUtil.newChannel({
+ uri,
+ loadingPrincipal: principal,
+ securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT,
+ });
+
+ chan
+ .QueryInterface(Ci.nsIHttpChannel)
+ .setRequestHeader("link-to-set", headers, false);
+ chan
+ .QueryInterface(Ci.nsIHttpChannelInternal)
+ .setEarlyHintObserver(listener);
+ chan.asyncOpen(new ChannelListener(resolve));
+ });
+}
+
+let http2Port;
+
+add_task(async function setup() {
+ Services.prefs.setCharPref("network.dns.localDomains", "foo.example.com");
+ http2Port = Services.env.get("MOZHTTP2_PORT");
+ Assert.notEqual(http2Port, null);
+ Assert.notEqual(http2Port, "");
+
+ do_get_profile();
+ let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(
+ Ci.nsIX509CertDB
+ );
+ addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u");
+});
+
+registerCleanupFunction(async () => {
+ Services.prefs.clearUserPref("network.dns.localDomains");
+});
+
+add_task(async function early_hints() {
+ let earlyHints = new EarlyHintsListener();
+ earlyHints._expected_hints = hint1;
+
+ await chanPromise(
+ `https://foo.example.com:${http2Port}${earlyhintspath}`,
+ earlyHints,
+ hint1
+ );
+ Assert.ok(earlyHints.earlyHintsReceived);
+});
+
+// Test when there is no Link header in a 103 response.
+// 103 response will contain non-link headers.
+add_task(async function no_early_hints() {
+ let earlyHints = new EarlyHintsListener("");
+
+ await chanPromise(
+ `https://foo.example.com:${http2Port}${earlyhintspath}`,
+ earlyHints,
+ ""
+ );
+ Assert.ok(!earlyHints.earlyHintsReceived);
+});
+
+add_task(async function early_hints_multiple() {
+ let earlyHints = new EarlyHintsListener();
+ earlyHints._expected_hints = hint1 + ", " + hint2;
+
+ await chanPromise(
+ `https://foo.example.com:${http2Port}${earlyhintspath}`,
+ earlyHints,
+ hint1 + ", " + hint2
+ );
+ Assert.ok(earlyHints.earlyHintsReceived);
+});
diff --git a/netwerk/test/unit/test_ech_grease.js b/netwerk/test/unit/test_ech_grease.js
new file mode 100644
index 0000000000..bbe9c027b5
--- /dev/null
+++ b/netwerk/test/unit/test_ech_grease.js
@@ -0,0 +1,270 @@
+// -*- 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";
+
+// Allow telemetry probes which may otherwise be disabled for some
+// applications (e.g. Thunderbird).
+Services.prefs.setBoolPref(
+ "toolkit.telemetry.testing.overrideProductsCheck",
+ true
+);
+
+// Get a profile directory and ensure PSM initializes NSS.
+do_get_profile();
+Cc["@mozilla.org/psm;1"].getService(Ci.nsISupports);
+
+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.securityCallbacks.getInterface(
+ 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(
+ minServerVersion = Ci.nsITLSClientStatus.TLS_VERSION_1_2,
+ maxServerVersion = Ci.nsITLSClientStatus.TLS_VERSION_1_3
+) {
+ let tlsServer = Cc["@mozilla.org/network/tls-server-socket;1"].createInstance(
+ Ci.nsITLSServerSocket
+ );
+ tlsServer.init(-1, true, -1);
+ tlsServer.serverCert = getTestServerCertificate();
+ tlsServer.setVersionRange(minServerVersion, maxServerVersion);
+ tlsServer.setSessionTickets(false);
+ tlsServer.asyncListen(new ServerSocketListener());
+ storeCertOverride(tlsServer.port, tlsServer.serverCert);
+ return tlsServer;
+}
+
+const hostname = "example.com";
+
+function storeCertOverride(port, cert) {
+ let certOverrideService = Cc[
+ "@mozilla.org/security/certoverride;1"
+ ].getService(Ci.nsICertOverrideService);
+ certOverrideService.rememberValidityOverride(hostname, port, {}, cert, true);
+}
+
+function startClient(port, useGREASE, beConservative) {
+ HandshakeTelemetryHelpers.resetHistograms();
+ let flavors = ["", "_FIRST_TRY"];
+ let nonflavors = ["_ECH"];
+
+ if (useGREASE) {
+ Services.prefs.setIntPref("security.tls.ech.grease_probability", 100);
+ } else {
+ Services.prefs.setIntPref("security.tls.ech.grease_probability", 0);
+ }
+
+ let req = new XMLHttpRequest();
+ req.open("GET", `https://${hostname}:${port}`);
+
+ if (beConservative) {
+ // We don't have a way to set DONT_TRY_ECH at the moment.
+ let internalChannel = req.channel.QueryInterface(Ci.nsIHttpChannelInternal);
+ internalChannel.beConservative = beConservative;
+ flavors.push("_CONSERVATIVE");
+ } else {
+ nonflavors.push("_CONSERVATIVE");
+ }
+
+ //GREASE is only used if enabled and not in conservative mode.
+ if (useGREASE && !beConservative) {
+ flavors.push("_ECH_GREASE");
+ } else {
+ nonflavors.push("_ECH_GREASE");
+ }
+
+ return new Promise((resolve, reject) => {
+ req.onload = () => {
+ equal(req.responseText, "OK", "response text should be 'OK'");
+
+ // Only check telemetry if network process is disabled.
+ if (!mozinfo.socketprocess_networking) {
+ HandshakeTelemetryHelpers.checkSuccess(flavors);
+ HandshakeTelemetryHelpers.checkEmpty(nonflavors);
+ }
+
+ resolve();
+ };
+ req.onerror = () => {
+ ok(false, `should not have gotten an error`);
+ resolve();
+ };
+
+ req.send();
+ });
+}
+
+function setup() {
+ Services.prefs.setIntPref("security.tls.version.max", 4);
+ Services.prefs.setCharPref("network.dns.localDomains", hostname);
+ Services.prefs.setIntPref("network.http.speculative-parallel-limit", 0);
+}
+setup();
+
+add_task(async function GreaseYConservativeN() {
+ // First run a server that accepts TLS 1.2 and 1.3. A conservative client
+ // should succeed in connecting.
+ let server = startServer();
+
+ await startClient(
+ server.port,
+ true /*be conservative*/,
+ false /*should succeed*/
+ );
+ server.close();
+});
+
+add_task(async function GreaseNConservativeY() {
+ // First run a server that accepts TLS 1.2 and 1.3. A conservative client
+ // should succeed in connecting.
+ let server = startServer();
+
+ await startClient(
+ server.port,
+ false /*be conservative*/,
+ true /*should succeed*/
+ );
+ server.close();
+});
+
+add_task(async function GreaseYConservativeY() {
+ // First run a server that accepts TLS 1.2 and 1.3. A conservative client
+ // should succeed in connecting.
+ let server = startServer();
+
+ await startClient(
+ server.port,
+ true /*be conservative*/,
+ true /*should succeed*/
+ );
+ server.close();
+});
+
+add_task(async function GreaseNConservativeN() {
+ // First run a server that accepts TLS 1.2 and 1.3. A conservative client
+ // should succeed in connecting.
+ let server = startServer();
+
+ await startClient(
+ server.port,
+ false /*be conservative*/,
+ false /*should succeed*/
+ );
+ server.close();
+});
+
+registerCleanupFunction(function () {
+ Services.prefs.clearUserPref("security.tls.version.max");
+ Services.prefs.clearUserPref("network.dns.localDomains");
+ Services.prefs.clearUserPref("security.tls.ech.grease_probability");
+ Services.prefs.clearUserPref("network.http.speculative-parallel-limit");
+});
diff --git a/netwerk/test/unit/test_event_sink.js b/netwerk/test/unit/test_event_sink.js
new file mode 100644
index 0000000000..e49080c88c
--- /dev/null
+++ b/netwerk/test/unit/test_event_sink.js
@@ -0,0 +1,183 @@
+// This file tests channel event sinks (bug 315598 et al)
+
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+ChromeUtils.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(iid) {
+ return this.QueryInterface(iid);
+ },
+
+ 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 chan;
+ if (listener._iteration == 1) {
+ // Step 2: Category entry
+ Services.catMan.addCategoryEntry(
+ categoryName,
+ "unit test",
+ sinkContract,
+ false,
+ true
+ );
+ chan = makeChan(URL + "/redirect");
+ } else {
+ // Step 3: Global contract id
+ Services.catMan.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..eac7ece5be
--- /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.cookies.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.cookies.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.cookies.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.cookies.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.cookies.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.cookies.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.cookies.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.cookies.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.cookies.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..a90e646048
--- /dev/null
+++ b/netwerk/test/unit/test_extract_charset_from_content_type.js
@@ -0,0 +1,238 @@
+/* -*- 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 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 = Services.io;
+ 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_file_protocol.js b/netwerk/test/unit/test_file_protocol.js
new file mode 100644
index 0000000000..707bddef24
--- /dev/null
+++ b/netwerk/test/unit/test_file_protocol.js
@@ -0,0 +1,277 @@
+/* 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 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) {
+ let uri = Services.io.newFileURI(file);
+ return NetUtil.newChannel({
+ uri,
+ loadingPrincipal: Services.scriptSecurityManager.createContentPrincipal(
+ uri,
+ {}
+ ),
+ securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_INHERITS_SEC_CONTEXT,
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT,
+ });
+}
+
+/*
+ * 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) {
+ 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
+ Assert.equal(
+ chan.originalURI.pathQueryRef,
+ Services.io.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..b1edf3d27b
--- /dev/null
+++ b/netwerk/test/unit/test_filestreams.js
@@ -0,0 +1,300 @@
+/* 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 = Services.dirsvc.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 = Services.dirsvc.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 = Services.dirsvc.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..c27fc0c028
--- /dev/null
+++ b/netwerk/test/unit/test_getHost.js
@@ -0,0 +1,65 @@
+// Test getLocalHost/getLocalPort and getRemoteHost/getRemotePort.
+
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+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_gio_protocol.js b/netwerk/test/unit/test_gio_protocol.js
new file mode 100644
index 0000000000..37ce37abab
--- /dev/null
+++ b/netwerk/test/unit/test_gio_protocol.js
@@ -0,0 +1,201 @@
+/* run some tests on the gvfs/gio protocol handler */
+
+"use strict";
+
+function inChildProcess() {
+ return Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT;
+}
+
+const PR_RDONLY = 0x1; // see prio.h
+
+[
+ do_test_read_data_dir,
+ do_test_read_recent,
+ test_read_file,
+ do_test_finished,
+].forEach(f => add_test(f));
+
+function setup() {
+ // Allowing some protocols to get a channel
+ if (!inChildProcess()) {
+ Services.prefs.setCharPref(
+ "network.gio.supported-protocols",
+ "localtest:,recent:"
+ );
+ } else {
+ do_send_remote_message("gio-allow-test-protocols");
+ do_await_remote_message("gio-allow-test-protocols-done");
+ }
+}
+
+setup();
+
+registerCleanupFunction(() => {
+ // Resetting the protocols to None
+ if (!inChildProcess()) {
+ Services.prefs.clearUserPref("network.gio.supported-protocols");
+ }
+});
+
+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 chan = NetUtil.newChannel({
+ uri: file,
+ loadUsingSystemPrincipal: true,
+ });
+
+ return chan;
+}
+
+/*
+ * 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,
+
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIStreamListener",
+ "nsIRequestObserver",
+ ]),
+
+ onStartRequest(request) {
+ if (this._got_onstartrequest) {
+ do_throw("Got second onStartRequest event!");
+ }
+ this._got_onstartrequest = true;
+ },
+
+ 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");
+ // Going via parent path, because this is opended from test/unit/ and test/unit_ipc/
+ var file = do_get_file("../unit/data/test_readline4.txt");
+ var chan = new_file_channel("localtest://" + file.path);
+
+ function on_read_complete(data) {
+ dump("*** test_read_file.on_read_complete()\n");
+ /* 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.asyncOpen(new FileStreamListener(on_read_complete));
+}
+
+function do_test_read_data_dir() {
+ dump('*** test_read_data_dir("../data/")\n');
+
+ var dir = do_get_file("../unit/data/");
+ var chan = new_file_channel("localtest://" + dir.path);
+
+ function on_read_complete(data) {
+ dump("*** test_read_data_dir.on_read_complete()\n");
+
+ // The data-directory should be listed, containing a header-line and the files therein
+ if (
+ !(
+ data.includes("200: filename content-length last-modified file-type") &&
+ data.includes("201: test_readline1.txt") &&
+ data.includes("201: test_readline2.txt")
+ )
+ ) {
+ do_throw(
+ "test_read_data_dir() - Bad data! Does not contain needles! Is <" +
+ data +
+ ">"
+ );
+ }
+ run_next_test();
+ }
+ chan.asyncOpen(new FileStreamListener(on_read_complete));
+}
+
+function do_test_read_recent() {
+ dump('*** test_read_recent("recent://")\n');
+
+ var chan = new_file_channel("recent:///");
+
+ function on_read_complete(data) {
+ dump("*** test_read_recent.on_read_complete()\n");
+
+ // The data-directory should be listed, containing a header-line and the files therein
+ if (
+ !data.includes("200: filename content-length last-modified file-type")
+ ) {
+ do_throw(
+ "do_test_read_recent() - Bad data! Does not contain header! Is <" +
+ data +
+ ">"
+ );
+ }
+ run_next_test();
+ }
+ chan.asyncOpen(new FileStreamListener(on_read_complete));
+}
diff --git a/netwerk/test/unit/test_gre_resources.js b/netwerk/test/unit/test_gre_resources.js
new file mode 100644
index 0000000000..4ea8d04b95
--- /dev/null
+++ b/netwerk/test/unit/test_gre_resources.js
@@ -0,0 +1,30 @@
+// test that things that are expected to be in gre-resources are still there
+
+"use strict";
+
+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);
+ } 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_h2proxy_connection_limit.js b/netwerk/test/unit/test_h2proxy_connection_limit.js
new file mode 100644
index 0000000000..f326b48b40
--- /dev/null
+++ b/netwerk/test/unit/test_h2proxy_connection_limit.js
@@ -0,0 +1,77 @@
+/* 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/. */
+
+// Summary:
+// Test whether the connection limit is honored when http2 proxy is used.
+//
+// Test step:
+// 1. Create 30 http requests.
+// 2. Check if the count of all sockets created by proxy is less than 6.
+
+"use strict";
+
+/* import-globals-from head_cache.js */
+/* import-globals-from head_cookies.js */
+/* import-globals-from head_channels.js */
+/* import-globals-from head_servers.js */
+
+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_connection_limit() {
+ let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(
+ Ci.nsIX509CertDB
+ );
+ addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u");
+ addCertFromFile(certdb, "proxy-ca.pem", "CTu,u,u");
+
+ let proxy = new NodeHTTP2ProxyServer();
+ await proxy.start();
+ registerCleanupFunction(async () => {
+ await proxy.stop();
+ });
+
+ const maxConnections = 6;
+ Services.prefs.setIntPref(
+ "network.http.max-persistent-connections-per-server",
+ maxConnections
+ );
+ registerCleanupFunction(async () => {
+ Services.prefs.clearUserPref(
+ "network.http.max-persistent-connections-per-server"
+ );
+ });
+
+ await with_node_servers([NodeHTTP2Server], async server => {
+ await server.registerPathHandler("/test", (req, resp) => {
+ resp.writeHead(200);
+ resp.end("All good");
+ });
+
+ let promises = [];
+ for (let i = 0; i < 30; ++i) {
+ let chan = makeChan(`${server.origin()}/test`);
+ promises.push(
+ new Promise(resolve => {
+ chan.asyncOpen(
+ new ChannelListener(resolve, null, CL_ALLOW_UNKNOWN_CL)
+ );
+ })
+ );
+ }
+ await Promise.all(promises);
+ let count = await proxy.socketCount(server.port());
+ Assert.lessOrEqual(
+ count,
+ maxConnections,
+ "socket count should be less than maxConnections"
+ );
+ });
+});
diff --git a/netwerk/test/unit/test_head.js b/netwerk/test/unit/test_head.js
new file mode 100644
index 0000000000..8a6b2a5cd7
--- /dev/null
+++ b/netwerk/test/unit/test_head.js
@@ -0,0 +1,171 @@
+//
+// HTTP headers test
+//
+
+// Note: sets Cc and Ci variables
+
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+const ReferrerInfo = Components.Constructor(
+ "@mozilla.org/referrer-info;1",
+ "nsIReferrerInfo",
+ "init"
+);
+
+ChromeUtils.defineLazyGetter(this, "URL", function () {
+ return "http://localhost:" + httpserver.identity.primaryPort;
+});
+
+var httpserver = new HttpServer();
+var testpath = "/simple";
+var httpbody = "0123456789";
+var channel;
+
+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");
+ // No triggering URI inloadInfo, assume load is cross-origin.
+ Assert.equal(setOK, "http://foo2.invalid:90/");
+
+ // 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/");
+
+ 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..852a03040f
--- /dev/null
+++ b/netwerk/test/unit/test_head_request_no_response_body.js
@@ -0,0 +1,78 @@
+/* -*- 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.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+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..b00e02d13b
--- /dev/null
+++ b/netwerk/test/unit/test_header_Accept-Language.js
@@ -0,0 +1,99 @@
+/* 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 = Services.prefs.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..69d936d74a
--- /dev/null
+++ b/netwerk/test/unit/test_header_Accept-Language_case.js
@@ -0,0 +1,50 @@
+"use strict";
+
+var testpath = "/bug1054739";
+
+function run_test() {
+ let intlPrefs = Services.prefs.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..0e65cf3ccf
--- /dev/null
+++ b/netwerk/test/unit/test_header_Server_Timing.js
@@ -0,0 +1,64 @@
+/* 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 serverPort = Services.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..e1439c6c43
--- /dev/null
+++ b/netwerk/test/unit/test_headers.js
@@ -0,0 +1,184 @@
+//
+// 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.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+ChromeUtils.defineLazyGetter(this, "URL", function () {
+ return "http://localhost:" + httpserver.identity.primaryPort;
+});
+
+var httpserver = new HttpServer();
+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
+// eslint-disable-next-line no-unused-vars
+function handler1(metadata, response) {
+ response.setStatusLine(metadata.httpVersion, 200, "OK");
+ response.setHeader("Content-Disposition", "attachment; filename=foo");
+ response.setHeader("Content-Type", "text/plain", false);
+}
+
+// eslint-disable-next-line no-unused-vars
+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
+// eslint-disable-next-line no-unused-vars
+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);
+}
+
+// eslint-disable-next-line no-unused-vars
+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
+// eslint-disable-next-line no-unused-vars
+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);
+}
+
+// eslint-disable-next-line no-unused-vars
+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
+// eslint-disable-next-line no-unused-vars
+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);
+}
+
+// eslint-disable-next-line no-unused-vars
+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..64d246c633
--- /dev/null
+++ b/netwerk/test/unit/test_hostnameIsLocalIPAddress.js
@@ -0,0 +1,37 @@
+"use strict";
+
+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 = Services.io.newURI(uri);
+ equal(isLocal, Services.io.hostnameIsLocalIPAddress(nsuri));
+ }
+}
diff --git a/netwerk/test/unit/test_hostnameIsSharedIPAddress.js b/netwerk/test/unit/test_hostnameIsSharedIPAddress.js
new file mode 100644
index 0000000000..c3eaeabc0e
--- /dev/null
+++ b/netwerk/test/unit/test_hostnameIsSharedIPAddress.js
@@ -0,0 +1,17 @@
+"use strict";
+
+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 = Services.io.newURI(uri);
+ equal(isShared, Services.io.hostnameIsSharedIPAddress(nsuri));
+ }
+}
diff --git a/netwerk/test/unit/test_hpke_config_manager.js b/netwerk/test/unit/test_hpke_config_manager.js
new file mode 100644
index 0000000000..66d06c3464
--- /dev/null
+++ b/netwerk/test/unit/test_hpke_config_manager.js
@@ -0,0 +1,112 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+let { HPKEConfigManager } = ChromeUtils.importESModule(
+ "resource://gre/modules/HPKEConfigManager.sys.mjs"
+);
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+let gHttpServer;
+let gValidRequestCount = 0;
+let gFickleIsWorking = true;
+
+add_setup(async function () {
+ gHttpServer = new HttpServer();
+ let invalidHandler = (req, res) => {
+ res.setStatusLine(req.httpVersion, 500, "Oh no, it broke");
+ res.write("Uh oh, it broke.");
+ };
+ let validHandler = (req, res) => {
+ res.setHeader("Content-Type", "application/ohttp-keys");
+ res.write("1234");
+ gValidRequestCount++;
+ };
+
+ gHttpServer.registerPathHandler("/.wellknown/invalid", invalidHandler);
+ gHttpServer.registerPathHandler("/.wellknown/valid", validHandler);
+
+ gHttpServer.registerPathHandler("/.wellknown/fickle", (req, res) => {
+ if (gFickleIsWorking) {
+ return validHandler(req, res);
+ }
+ return invalidHandler(req, res);
+ });
+
+ gHttpServer.start(-1);
+});
+
+function getLocalURL(path) {
+ return `http://localhost:${gHttpServer.identity.primaryPort}/.wellknown/${path}`;
+}
+
+add_task(async function test_broken_url_returns_null() {
+ Assert.equal(await HPKEConfigManager.get(getLocalURL("invalid")), null);
+});
+
+add_task(async function test_working_url_returns_data() {
+ Assert.deepEqual(
+ await HPKEConfigManager.get(getLocalURL("valid")),
+ new TextEncoder().encode("1234")
+ );
+});
+
+add_task(async function test_we_only_request_once() {
+ Assert.deepEqual(
+ await HPKEConfigManager.get(getLocalURL("valid")),
+ new TextEncoder().encode("1234")
+ );
+ let oldRequestCount = gValidRequestCount;
+
+ Assert.deepEqual(
+ await HPKEConfigManager.get(getLocalURL("valid")),
+ new TextEncoder().encode("1234")
+ );
+ Assert.equal(
+ oldRequestCount,
+ gValidRequestCount,
+ "Shouldn't have made another request."
+ );
+});
+
+add_task(async function test_maxAge_forces_refresh() {
+ Assert.deepEqual(
+ await HPKEConfigManager.get(getLocalURL("valid")),
+ new TextEncoder().encode("1234")
+ );
+ let oldRequestCount = gValidRequestCount;
+
+ Assert.deepEqual(
+ await HPKEConfigManager.get(getLocalURL("valid"), { maxAge: 0 }),
+ new TextEncoder().encode("1234")
+ );
+ Assert.equal(
+ oldRequestCount + 1,
+ gValidRequestCount,
+ "Should have made another request due to maxAge."
+ );
+});
+
+add_task(async function test_maxAge_handling_of_invalid_requests() {
+ Assert.deepEqual(
+ await HPKEConfigManager.get(getLocalURL("fickle")),
+ new TextEncoder().encode("1234")
+ );
+
+ gFickleIsWorking = false;
+
+ Assert.deepEqual(
+ await HPKEConfigManager.get(getLocalURL("fickle"), { maxAge: 0 }),
+ null
+ );
+
+ Assert.deepEqual(
+ await HPKEConfigManager.get(getLocalURL("fickle")),
+ new TextEncoder().encode("1234"),
+ "Should still have the cached config if no max age is passed."
+ );
+});
diff --git a/netwerk/test/unit/test_http1-proxy.js b/netwerk/test/unit/test_http1-proxy.js
new file mode 100644
index 0000000000..7daa37e368
--- /dev/null
+++ b/netwerk/test/unit/test_http1-proxy.js
@@ -0,0 +1,231 @@
+/* -*- 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.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+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-failing.js b/netwerk/test/unit/test_http2-proxy-failing.js
new file mode 100644
index 0000000000..8530f55373
--- /dev/null
+++ b/netwerk/test/unit/test_http2-proxy-failing.js
@@ -0,0 +1,174 @@
+/* -*- 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/. */
+
+/* Test stream failure on the session to the proxy:
+ * - Test the case the error closes the affected stream only
+ * - Test the case the error closes the whole session and cancels existing
+ * streams.
+ */
+
+/* eslint-env node */
+
+"use strict";
+
+const pps = Cc["@mozilla.org/network/protocol-proxy-service;1"].getService();
+
+let filter;
+
+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) {
+ cb.onProxyFilterResult(
+ pps.newProxyInfo(
+ this._type,
+ this._host,
+ this._port,
+ null,
+ null,
+ this._flags,
+ 1000,
+ null
+ )
+ );
+ }
+}
+
+function createPrincipal(url) {
+ var ssm = Services.scriptSecurityManager;
+ try {
+ return ssm.createContentPrincipal(Services.io.newURI(url), {});
+ } catch (e) {
+ return null;
+ }
+}
+
+function make_channel(url) {
+ return Services.io.newChannelFromURIWithProxyFlags(
+ Services.io.newURI(url),
+ null,
+ 16,
+ null,
+ createPrincipal(url),
+ createPrincipal(url),
+ Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_INHERITS_SEC_CONTEXT,
+ Ci.nsIContentPolicy.TYPE_OTHER
+ );
+}
+
+function get_response(channel, flags = CL_ALLOW_UNKNOWN_CL, delay = 0) {
+ return new Promise(resolve => {
+ var listener = 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
+ );
+ if (delay > 0) {
+ do_timeout(delay, function () {
+ channel.asyncOpen(listener);
+ });
+ } else {
+ channel.asyncOpen(listener);
+ }
+ });
+}
+
+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");
+
+ let proxy_port = Services.env.get("MOZHTTP2_PORT");
+ Assert.notEqual(proxy_port, null);
+
+ Services.prefs.setBoolPref("network.http.http2.enabled", 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, 16);
+ pps.registerFilter(filter, 10);
+});
+
+registerCleanupFunction(async () => {
+ Services.prefs.clearUserPref("network.http.http2.enabled");
+ Services.prefs.clearUserPref("network.dns.native-is-localhost");
+
+ pps.unregisterFilter(filter);
+});
+
+add_task(
+ async function proxy_server_stream_soft_failure_multiple_streams_not_affected() {
+ let should_succeed = get_response(make_channel(`http://750.example.com`));
+ const failed = await get_response(
+ make_channel(`http://illegalhpacksoft.example.com`),
+ CL_EXPECT_FAILURE,
+ 20
+ );
+
+ const succeeded = await should_succeed;
+
+ Assert.equal(failed.status, Cr.NS_ERROR_ILLEGAL_VALUE);
+ Assert.equal(failed.proxy_connect_response_code, 0);
+ Assert.equal(failed.http_code, undefined);
+ Assert.equal(succeeded.status, Cr.NS_OK);
+ Assert.equal(succeeded.proxy_connect_response_code, 200);
+ Assert.equal(succeeded.http_code, 200);
+ }
+);
+
+add_task(
+ async function proxy_server_stream_hard_failure_multiple_streams_affected() {
+ let should_failed = get_response(
+ make_channel(`http://750.example.com`),
+ CL_EXPECT_FAILURE
+ );
+ const failed1 = await get_response(
+ make_channel(`http://illegalhpackhard.example.com`),
+ CL_EXPECT_FAILURE
+ );
+
+ const failed2 = await should_failed;
+
+ Assert.equal(failed1.status, 0x804b0053);
+ Assert.equal(failed1.proxy_connect_response_code, 0);
+ Assert.equal(failed1.http_code, undefined);
+ Assert.equal(failed2.status, 0x804b0053);
+ Assert.equal(failed2.proxy_connect_response_code, 0);
+ Assert.equal(failed2.http_code, undefined);
+ }
+);
+
+add_task(async function test_http2_h11required_stream() {
+ let should_failed = await get_response(
+ make_channel(`http://h11required.com`),
+ CL_EXPECT_FAILURE
+ );
+
+ // See HTTP/1.1 connect handler in moz-http2.js. The handler returns
+ // "404 Not Found", so the expected error code is NS_ERROR_UNKNOWN_HOST.
+ Assert.equal(should_failed.status, Cr.NS_ERROR_UNKNOWN_HOST);
+ Assert.equal(should_failed.proxy_connect_response_code, 404);
+ Assert.equal(should_failed.http_code, undefined);
+});
diff --git a/netwerk/test/unit/test_http2-proxy.js b/netwerk/test/unit/test_http2-proxy.js
new file mode 100644
index 0000000000..d8aa0019fe
--- /dev/null
+++ b/netwerk/test/unit/test_http2-proxy.js
@@ -0,0 +1,862 @@
+/* -*- 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
+ * - a stream reset for a connect stream to the proxy does not cause session to
+ * be closed and the request through the proxy will failed.
+ * - a "soft" stream error on a connection to the origin server will close the
+ * stream, but it will not close niether the HTTP/2 session to the proxy nor
+ * to the origin server.
+ * - a "hard" stream error on a connection to the origin server will close the
+ * HTTP/2 session to the origin server, but it will not close the HTTP/2
+ * session to the proxy.
+ */
+
+/* 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) {
+ 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 createPrincipal(url) {
+ var ssm = Services.scriptSecurityManager;
+ try {
+ return ssm.createContentPrincipal(Services.io.newURI(url), {});
+ } catch (e) {
+ return null;
+ }
+}
+
+function make_channel(url) {
+ return NetUtil.newChannel({
+ uri: url,
+ loadingPrincipal: createPrincipal(url),
+ securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_INHERITS_SEC_CONTEXT,
+ // 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, delay = 0) {
+ return new Promise(resolve => {
+ var listener = 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
+ );
+ if (delay > 0) {
+ do_timeout(delay, function () {
+ channel.asyncOpen(listener);
+ });
+ } else {
+ channel.asyncOpen(listener);
+ }
+ });
+}
+
+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 proxySessionToOriginServersCount() {
+ if (!proxy) {
+ return 0;
+ }
+ return proxy.sessionToOriginServersCount;
+ }
+
+ static setupProxy() {
+ if (!proxy) {
+ throw new Error("proxy is null");
+ }
+ proxy.proxy_session_count = 0;
+ proxy.sessionToOriginServersCount = 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;
+ }
+ if (target == "reset.example.com:443") {
+ // always reset the stream.
+ stream.close(0x0);
+ return;
+ }
+
+ ++proxy.sessionToOriginServersCount;
+ 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 new Error(
+ `Unexpected 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;
+}
+async function proxy_session_to_origin_server_counter() {
+ let data = await NodeServer.execute(
+ processId,
+ `http2ProxyCode.proxySessionToOriginServersCount()`
+ );
+ 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");
+
+ let server_port = Services.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 newProxy = await NodeServer.execute(
+ processId,
+ `http2ProxyCode.startNewProxy()`
+ );
+ proxy_port = newProxy.port;
+ Assert.notEqual(proxy_port, null);
+
+ Services.prefs.setStringPref(
+ "services.settings.server",
+ `data:,#remote-settings-dummy/v1`
+ );
+
+ Services.prefs.setBoolPref("network.http.http2.enabled", 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.http2.enabled");
+ 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"
+ );
+});
+
+add_task(async function proxy_stream_reset_failure() {
+ const { status, http_code, proxy_connect_response_code } = await get_response(
+ make_channel(`https://reset.example.com/`),
+ CL_EXPECT_FAILURE
+ );
+
+ Assert.equal(status, Cr.NS_ERROR_NET_INTERRUPT);
+ Assert.equal(proxy_connect_response_code, 0);
+ Assert.equal(http_code, undefined);
+ Assert.equal(
+ await proxy_session_counter(),
+ 1,
+ "No new session created by 429 after 504"
+ );
+});
+
+// The soft errors are not closing the session.
+add_task(async function origin_server_stream_soft_failure() {
+ var current_num_sessions_to_origin_server =
+ await proxy_session_to_origin_server_counter();
+
+ const { status, http_code, proxy_connect_response_code } = await get_response(
+ make_channel(`https://foo.example.com/illegalhpacksoft`),
+ CL_EXPECT_FAILURE
+ );
+
+ Assert.equal(status, Cr.NS_ERROR_ILLEGAL_VALUE);
+ Assert.equal(proxy_connect_response_code, 200);
+ Assert.equal(http_code, undefined);
+ Assert.equal(
+ await proxy_session_counter(),
+ 1,
+ "No session to the proxy closed by soft stream errors"
+ );
+ Assert.equal(
+ await proxy_session_to_origin_server_counter(),
+ current_num_sessions_to_origin_server,
+ "No session to the origin server closed by soft stream errors"
+ );
+});
+
+// The soft errors are not closing the session.
+add_task(
+ async function origin_server_stream_soft_failure_multiple_streams_not_affected() {
+ var current_num_sessions_to_origin_server =
+ await proxy_session_to_origin_server_counter();
+
+ let should_succeed = get_response(
+ make_channel(`https://foo.example.com/750ms`)
+ );
+
+ const failed = await get_response(
+ make_channel(`https://foo.example.com/illegalhpacksoft`),
+ CL_EXPECT_FAILURE,
+ 20
+ );
+
+ const succeeded = await should_succeed;
+
+ Assert.equal(failed.status, Cr.NS_ERROR_ILLEGAL_VALUE);
+ Assert.equal(failed.proxy_connect_response_code, 200);
+ Assert.equal(failed.http_code, undefined);
+ Assert.equal(succeeded.status, Cr.NS_OK);
+ Assert.equal(succeeded.proxy_connect_response_code, 200);
+ Assert.equal(succeeded.http_code, 200);
+ Assert.equal(
+ await proxy_session_counter(),
+ 1,
+ "No session to the proxy closed by soft stream errors"
+ );
+ Assert.equal(
+ await proxy_session_to_origin_server_counter(),
+ current_num_sessions_to_origin_server,
+ "No session to the origin server closed by soft stream errors"
+ );
+ }
+);
+
+// Make sure that the above error codes don't kill the session to the proxy.
+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 to the proxy created after stream 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"
+ );
+});
+
+add_task(async function proxy_success_check_number_of_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`)
+ );
+ 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,
+ "The number of sessions has not changed"
+ );
+});
+
+// The hard errors are closing the session.
+add_task(async function origin_server_stream_hard_failure() {
+ var current_num_sessions_to_origin_server =
+ await proxy_session_to_origin_server_counter();
+ const { status, http_code, proxy_connect_response_code } = await get_response(
+ make_channel(`https://foo.example.com/illegalhpackhard`),
+ CL_EXPECT_FAILURE
+ );
+
+ Assert.equal(status, 0x804b0053);
+ Assert.equal(proxy_connect_response_code, 200);
+ Assert.equal(http_code, undefined);
+ Assert.equal(
+ await proxy_session_counter(),
+ 2,
+ "No new session to the proxy."
+ );
+ Assert.equal(
+ await proxy_session_to_origin_server_counter(),
+ current_num_sessions_to_origin_server,
+ "No new session to the origin server yet."
+ );
+
+ // Check the a new session ill be opened.
+ const foo = await get_response(
+ make_channel(`https://foo.example.com/random-request-1`)
+ );
+
+ 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(
+ await proxy_session_counter(),
+ 2,
+ "No new session to the proxy is created after a hard stream failure on the session to the origin server."
+ );
+ Assert.equal(
+ await proxy_session_to_origin_server_counter(),
+ current_num_sessions_to_origin_server + 1,
+ "A new session to the origin server after a hard stream error"
+ );
+});
+
+// The hard errors are closing the session.
+add_task(
+ async function origin_server_stream_hard_failure_multiple_streams_affected() {
+ var current_num_sessions_to_origin_server =
+ await proxy_session_to_origin_server_counter();
+ let should_fail = get_response(
+ make_channel(`https://foo.example.com/750msNoData`),
+ CL_EXPECT_FAILURE
+ );
+ const failed1 = await get_response(
+ make_channel(`https://foo.example.com/illegalhpackhard`),
+ CL_EXPECT_FAILURE,
+ 10
+ );
+
+ const failed2 = await should_fail;
+
+ Assert.equal(failed1.status, 0x804b0053);
+ Assert.equal(failed1.proxy_connect_response_code, 200);
+ Assert.equal(failed1.http_code, undefined);
+ Assert.equal(failed2.status, 0x804b0053);
+ Assert.equal(failed2.proxy_connect_response_code, 200);
+ Assert.equal(failed2.http_code, undefined);
+ Assert.equal(
+ await proxy_session_counter(),
+ 2,
+ "No new session to the proxy"
+ );
+ Assert.equal(
+ await proxy_session_to_origin_server_counter(),
+ current_num_sessions_to_origin_server,
+ "No session to the origin server yet."
+ );
+ // Check the a new session ill be opened.
+ const foo = await get_response(
+ make_channel(`https://foo.example.com/random-request-1`)
+ );
+
+ 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(
+ await proxy_session_counter(),
+ 2,
+ "No new session to the proxy is created after a hard stream failure on the session to the origin server."
+ );
+
+ Assert.equal(
+ await proxy_session_to_origin_server_counter(),
+ current_num_sessions_to_origin_server + 1,
+ "A new session to the origin server after a hard stream error"
+ );
+ }
+);
diff --git a/netwerk/test/unit/test_http2.js b/netwerk/test/unit/test_http2.js
new file mode 100644
index 0000000000..0b80e99dcc
--- /dev/null
+++ b/netwerk/test/unit/test_http2.js
@@ -0,0 +1,481 @@
+/* import-globals-from http2_test_common.js */
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+var concurrent_channels = [];
+var httpserv = null;
+var httpserv2 = null;
+
+var loadGroup;
+var serverPort;
+
+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);
+}
+
+add_setup(async function setup() {
+ serverPort = Services.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();
+
+ Services.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");
+
+ Services.prefs.setBoolPref("network.http.http2.enabled", true);
+ Services.prefs.setBoolPref("network.http.http2.allow-push", 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"
+ );
+ Services.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
+ );
+});
+
+registerCleanupFunction(async () => {
+ Services.prefs.clearUserPref("network.http.speculative-parallel-limit");
+ Services.prefs.clearUserPref("network.http.http2.enabled");
+ Services.prefs.clearUserPref("network.http.http2.allow-push");
+ Services.prefs.clearUserPref("network.http.altsvc.enabled");
+ Services.prefs.clearUserPref("network.http.altsvc.oe");
+ Services.prefs.clearUserPref("network.dns.localDomains");
+ Services.prefs.clearUserPref(
+ "network.cookieJarSettings.unblocked_for_testing"
+ );
+ await httpserv.stop();
+ await httpserv2.stop();
+});
+
+// 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
+add_task(async function do_test_http2_post_big() {
+ const { httpProxyConnectResponseCode } = await test_http2_post_big(
+ serverPort
+ );
+ Assert.equal(httpProxyConnectResponseCode, -1);
+});
+
+add_task(async function do_test_http2_basic() {
+ const { httpProxyConnectResponseCode } = await test_http2_basic(serverPort);
+ Assert.equal(httpProxyConnectResponseCode, -1);
+});
+
+add_task(async function do_test_http2_concurrent() {
+ const { httpProxyConnectResponseCode } = await test_http2_concurrent(
+ concurrent_channels,
+ serverPort
+ );
+ Assert.equal(httpProxyConnectResponseCode, -1);
+});
+
+add_task(async function do_test_http2_concurrent_post() {
+ const { httpProxyConnectResponseCode } = await test_http2_concurrent_post(
+ concurrent_channels,
+ serverPort
+ );
+ Assert.equal(httpProxyConnectResponseCode, -1);
+});
+
+add_task(async function do_test_http2_basic_unblocked_dep() {
+ const { httpProxyConnectResponseCode } = await test_http2_basic_unblocked_dep(
+ serverPort
+ );
+ Assert.equal(httpProxyConnectResponseCode, -1);
+});
+
+add_task(async function do_test_http2_nospdy() {
+ const { httpProxyConnectResponseCode } = await test_http2_nospdy(serverPort);
+ Assert.equal(httpProxyConnectResponseCode, -1);
+});
+
+add_task(async function do_test_http2_push1() {
+ const { httpProxyConnectResponseCode } = await test_http2_push1(
+ loadGroup,
+ serverPort
+ );
+ Assert.equal(httpProxyConnectResponseCode, -1);
+});
+
+add_task(async function do_test_http2_push2() {
+ const { httpProxyConnectResponseCode } = await test_http2_push2(
+ loadGroup,
+ serverPort
+ );
+ Assert.equal(httpProxyConnectResponseCode, -1);
+});
+
+add_task(async function do_test_http2_push3() {
+ const { httpProxyConnectResponseCode } = await test_http2_push3(
+ loadGroup,
+ serverPort
+ );
+ Assert.equal(httpProxyConnectResponseCode, -1);
+});
+
+add_task(async function do_test_http2_push4() {
+ const { httpProxyConnectResponseCode } = await test_http2_push4(
+ loadGroup,
+ serverPort
+ );
+ Assert.equal(httpProxyConnectResponseCode, -1);
+});
+
+add_task(async function do_test_http2_push5() {
+ const { httpProxyConnectResponseCode } = await test_http2_push5(
+ loadGroup,
+ serverPort
+ );
+ Assert.equal(httpProxyConnectResponseCode, -1);
+});
+
+add_task(async function do_test_http2_push6() {
+ const { httpProxyConnectResponseCode } = await test_http2_push6(
+ loadGroup,
+ serverPort
+ );
+ Assert.equal(httpProxyConnectResponseCode, -1);
+});
+
+add_task(async function do_test_http2_altsvc() {
+ const { httpProxyConnectResponseCode } = await test_http2_altsvc(
+ httpserv.identity.primaryPort,
+ httpserv2.identity.primaryPort,
+ false
+ );
+ Assert.equal(httpProxyConnectResponseCode, -1);
+});
+
+add_task(async function do_test_http2_doubleheader() {
+ const { httpProxyConnectResponseCode } = await test_http2_doubleheader(
+ serverPort
+ );
+ Assert.equal(httpProxyConnectResponseCode, -1);
+});
+
+add_task(async function do_test_http2_xhr() {
+ await test_http2_xhr(serverPort);
+});
+
+add_task(async function do_test_http2_header() {
+ const { httpProxyConnectResponseCode } = await test_http2_header(serverPort);
+ Assert.equal(httpProxyConnectResponseCode, -1);
+});
+
+add_task(async function do_test_http2_invalid_response_header_name_spaces() {
+ const { httpProxyConnectResponseCode } =
+ await test_http2_invalid_response_header(serverPort, "name_spaces");
+ Assert.equal(httpProxyConnectResponseCode, -1);
+});
+
+add_task(
+ async function do_test_http2_invalid_response_header_value_line_feed() {
+ const { httpProxyConnectResponseCode } =
+ await test_http2_invalid_response_header(serverPort, "value_line_feed");
+ Assert.equal(httpProxyConnectResponseCode, -1);
+ }
+);
+
+add_task(
+ async function do_test_http2_invalid_response_header_value_carriage_return() {
+ const { httpProxyConnectResponseCode } =
+ await test_http2_invalid_response_header(
+ serverPort,
+ "value_carriage_return"
+ );
+ Assert.equal(httpProxyConnectResponseCode, -1);
+ }
+);
+
+add_task(async function do_test_http2_invalid_response_header_value_null() {
+ const { httpProxyConnectResponseCode } =
+ await test_http2_invalid_response_header(serverPort, "value_null");
+ Assert.equal(httpProxyConnectResponseCode, -1);
+});
+
+add_task(async function do_test_http2_cookie_crumbling() {
+ const { httpProxyConnectResponseCode } = await test_http2_cookie_crumbling(
+ serverPort
+ );
+ Assert.equal(httpProxyConnectResponseCode, -1);
+});
+
+add_task(async function do_test_http2_multiplex() {
+ var values = await test_http2_multiplex(serverPort);
+ Assert.equal(values[0].httpProxyConnectResponseCode, -1);
+ Assert.equal(values[1].httpProxyConnectResponseCode, -1);
+ Assert.notEqual(values[0].streamID, values[1].streamID);
+});
+
+add_task(async function do_test_http2_big() {
+ const { httpProxyConnectResponseCode } = await test_http2_big(serverPort);
+ Assert.equal(httpProxyConnectResponseCode, -1);
+});
+
+add_task(async function do_test_http2_huge_suspended() {
+ const { httpProxyConnectResponseCode } = await test_http2_huge_suspended(
+ serverPort
+ );
+ Assert.equal(httpProxyConnectResponseCode, -1);
+});
+
+add_task(async function do_test_http2_post() {
+ const { httpProxyConnectResponseCode } = await test_http2_post(serverPort);
+ Assert.equal(httpProxyConnectResponseCode, -1);
+});
+
+add_task(async function do_test_http2_empty_post() {
+ const { httpProxyConnectResponseCode } = await test_http2_empty_post(
+ serverPort
+ );
+ Assert.equal(httpProxyConnectResponseCode, -1);
+});
+
+add_task(async function do_test_http2_patch() {
+ const { httpProxyConnectResponseCode } = await test_http2_patch(serverPort);
+ Assert.equal(httpProxyConnectResponseCode, -1);
+});
+
+add_task(async function do_test_http2_pushapi_1() {
+ const { httpProxyConnectResponseCode } = await test_http2_pushapi_1(
+ loadGroup,
+ serverPort
+ );
+ Assert.equal(httpProxyConnectResponseCode, -1);
+});
+
+add_task(async function do_test_http2_continuations() {
+ const { httpProxyConnectResponseCode } = await test_http2_continuations(
+ loadGroup,
+ serverPort
+ );
+ Assert.equal(httpProxyConnectResponseCode, -1);
+});
+
+add_task(async function do_test_http2_blocking_download() {
+ const { httpProxyConnectResponseCode } = await test_http2_blocking_download(
+ serverPort
+ );
+ Assert.equal(httpProxyConnectResponseCode, -1);
+});
+
+add_task(async function do_test_http2_illegalhpacksoft() {
+ const { httpProxyConnectResponseCode } = await test_http2_illegalhpacksoft(
+ serverPort
+ );
+ Assert.equal(httpProxyConnectResponseCode, -1);
+});
+
+add_task(async function do_test_http2_illegalhpackhard() {
+ const { httpProxyConnectResponseCode } = await test_http2_illegalhpackhard(
+ serverPort
+ );
+ Assert.equal(httpProxyConnectResponseCode, -1);
+});
+
+add_task(async function do_test_http2_folded_header() {
+ const { httpProxyConnectResponseCode } = await test_http2_folded_header(
+ loadGroup,
+ serverPort
+ );
+ Assert.equal(httpProxyConnectResponseCode, -1);
+});
+
+add_task(async function do_test_http2_empty_data() {
+ const { httpProxyConnectResponseCode } = await test_http2_empty_data(
+ serverPort
+ );
+ Assert.equal(httpProxyConnectResponseCode, -1);
+});
+
+add_task(async function do_test_http2_status_phrase() {
+ const { httpProxyConnectResponseCode } = await test_http2_status_phrase(
+ serverPort
+ );
+ Assert.equal(httpProxyConnectResponseCode, -1);
+});
+
+add_task(async function do_test_http2_doublepush() {
+ const { httpProxyConnectResponseCode } = await test_http2_doublepush(
+ loadGroup,
+ serverPort
+ );
+ Assert.equal(httpProxyConnectResponseCode, -1);
+});
+
+add_task(async function do_test_http2_disk_cache_push() {
+ const { httpProxyConnectResponseCode } = await test_http2_disk_cache_push(
+ loadGroup,
+ serverPort
+ );
+ Assert.equal(httpProxyConnectResponseCode, -1);
+});
+
+add_task(async function do_test_http2_h11required_stream() {
+ // 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
+ const { httpProxyConnectResponseCode } = await test_http2_h11required_stream(
+ serverPort
+ );
+ Assert.equal(httpProxyConnectResponseCode, -1);
+});
+
+add_task(async function do_test_http2_h11required_session() {
+ const { httpProxyConnectResponseCode } = await test_http2_h11required_session(
+ serverPort
+ );
+ Assert.equal(httpProxyConnectResponseCode, -1);
+});
+
+add_task(async function do_test_http2_retry_rst() {
+ const { httpProxyConnectResponseCode } = await test_http2_retry_rst(
+ serverPort
+ );
+ Assert.equal(httpProxyConnectResponseCode, -1);
+});
+
+add_task(async function do_test_http2_wrongsuite_tls12() {
+ const { httpProxyConnectResponseCode } = await test_http2_wrongsuite_tls12(
+ serverPort
+ );
+ Assert.equal(httpProxyConnectResponseCode, -1);
+});
+
+add_task(async function do_test_http2_wrongsuite_tls13() {
+ const { httpProxyConnectResponseCode } = await test_http2_wrongsuite_tls13(
+ serverPort
+ );
+ Assert.equal(httpProxyConnectResponseCode, -1);
+});
+
+add_task(async function do_test_http2_push_firstparty1() {
+ const { httpProxyConnectResponseCode } = await test_http2_push_firstparty1(
+ loadGroup,
+ serverPort
+ );
+ Assert.equal(httpProxyConnectResponseCode, -1);
+});
+
+add_task(async function do_test_http2_push_firstparty2() {
+ const { httpProxyConnectResponseCode } = await test_http2_push_firstparty2(
+ loadGroup,
+ serverPort
+ );
+ Assert.equal(httpProxyConnectResponseCode, -1);
+});
+
+add_task(async function do_test_http2_push_firstparty3() {
+ const { httpProxyConnectResponseCode } = await test_http2_push_firstparty3(
+ loadGroup,
+ serverPort
+ );
+ Assert.equal(httpProxyConnectResponseCode, -1);
+});
+
+add_task(async function do_test_http2_push_userContext1() {
+ const { httpProxyConnectResponseCode } = await test_http2_push_userContext1(
+ loadGroup,
+ serverPort
+ );
+ Assert.equal(httpProxyConnectResponseCode, -1);
+});
+
+add_task(async function do_test_http2_push_userContext2() {
+ const { httpProxyConnectResponseCode } = await test_http2_push_userContext2(
+ loadGroup,
+ serverPort
+ );
+ Assert.equal(httpProxyConnectResponseCode, -1);
+});
+
+add_task(async function do_test_http2_push_userContext3() {
+ const { httpProxyConnectResponseCode } = await test_http2_push_userContext3(
+ loadGroup,
+ serverPort
+ );
+ Assert.equal(httpProxyConnectResponseCode, -1);
+});
diff --git a/netwerk/test/unit/test_http2_with_proxy.js b/netwerk/test/unit/test_http2_with_proxy.js
new file mode 100644
index 0000000000..858a0da570
--- /dev/null
+++ b/netwerk/test/unit/test_http2_with_proxy.js
@@ -0,0 +1,425 @@
+// test HTTP/2 with a HTTP/2 prooxy
+
+"use strict";
+
+/* import-globals-from http2_test_common.js */
+/* import-globals-from head_servers.js */
+
+var concurrent_channels = [];
+
+var loadGroup;
+var serverPort;
+var proxy;
+
+add_setup(async function setup() {
+ serverPort = Services.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();
+ Services.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");
+ addCertFromFile(certdb, "proxy-ca.pem", "CTu,u,u");
+
+ Services.prefs.setBoolPref("network.http.http2.enabled", true);
+ Services.prefs.setBoolPref("network.http.http2.allow-push", 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"
+ );
+ Services.prefs.setBoolPref(
+ "network.cookieJarSettings.unblocked_for_testing",
+ true
+ );
+
+ loadGroup = Cc["@mozilla.org/network/load-group;1"].createInstance(
+ Ci.nsILoadGroup
+ );
+
+ Services.prefs.setStringPref(
+ "services.settings.server",
+ `data:,#remote-settings-dummy/v1`
+ );
+
+ proxy = new NodeHTTP2ProxyServer();
+ await proxy.start();
+});
+
+registerCleanupFunction(async () => {
+ Services.prefs.clearUserPref("network.http.speculative-parallel-limit");
+ Services.prefs.clearUserPref("network.http.http2.enabled");
+ Services.prefs.clearUserPref("network.http.http2.allow-push");
+ Services.prefs.clearUserPref("network.http.altsvc.enabled");
+ Services.prefs.clearUserPref("network.http.altsvc.oe");
+ Services.prefs.clearUserPref("network.dns.localDomains");
+ Services.prefs.clearUserPref(
+ "network.cookieJarSettings.unblocked_for_testing"
+ );
+
+ await proxy.stop();
+});
+
+// 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
+add_task(async function do_test_http2_post_big() {
+ const { httpProxyConnectResponseCode } = await test_http2_post_big(
+ serverPort
+ );
+ Assert.equal(httpProxyConnectResponseCode, 200);
+});
+
+add_task(async function do_test_http2_basic() {
+ const { httpProxyConnectResponseCode } = await test_http2_basic(serverPort);
+ Assert.equal(httpProxyConnectResponseCode, 200);
+});
+
+add_task(async function do_test_http2_concurrent() {
+ const { httpProxyConnectResponseCode } = await test_http2_concurrent(
+ concurrent_channels,
+ serverPort
+ );
+ Assert.equal(httpProxyConnectResponseCode, 200);
+});
+
+add_task(async function do_test_http2_concurrent_post() {
+ const { httpProxyConnectResponseCode } = await test_http2_concurrent_post(
+ concurrent_channels,
+ serverPort
+ );
+ Assert.equal(httpProxyConnectResponseCode, 200);
+});
+
+add_task(async function do_test_http2_basic_unblocked_dep() {
+ const { httpProxyConnectResponseCode } = await test_http2_basic_unblocked_dep(
+ serverPort
+ );
+ Assert.equal(httpProxyConnectResponseCode, 200);
+});
+
+add_task(async function do_test_http2_nospdy() {
+ const { httpProxyConnectResponseCode } = await test_http2_nospdy(serverPort);
+ Assert.equal(httpProxyConnectResponseCode, 200);
+});
+
+add_task(async function do_test_http2_push1() {
+ const { httpProxyConnectResponseCode } = await test_http2_push1(
+ loadGroup,
+ serverPort
+ );
+ Assert.equal(httpProxyConnectResponseCode, 200);
+});
+
+add_task(async function do_test_http2_push2() {
+ const { httpProxyConnectResponseCode } = await test_http2_push2(
+ loadGroup,
+ serverPort
+ );
+ Assert.equal(httpProxyConnectResponseCode, 200);
+});
+
+add_task(async function do_test_http2_push3() {
+ const { httpProxyConnectResponseCode } = await test_http2_push3(
+ loadGroup,
+ serverPort
+ );
+ Assert.equal(httpProxyConnectResponseCode, 200);
+});
+
+add_task(async function do_test_http2_push4() {
+ const { httpProxyConnectResponseCode } = await test_http2_push4(
+ loadGroup,
+ serverPort
+ );
+ Assert.equal(httpProxyConnectResponseCode, 200);
+});
+
+add_task(async function do_test_http2_push5() {
+ const { httpProxyConnectResponseCode } = await test_http2_push5(
+ loadGroup,
+ serverPort
+ );
+ Assert.equal(httpProxyConnectResponseCode, 200);
+});
+
+add_task(async function do_test_http2_push6() {
+ const { httpProxyConnectResponseCode } = await test_http2_push6(
+ loadGroup,
+ serverPort
+ );
+ Assert.equal(httpProxyConnectResponseCode, 200);
+});
+
+add_task(async function do_test_http2_doubleheader() {
+ const { httpProxyConnectResponseCode } = await test_http2_doubleheader(
+ serverPort
+ );
+ Assert.equal(httpProxyConnectResponseCode, 200);
+});
+
+add_task(async function do_test_http2_xhr() {
+ await test_http2_xhr(serverPort);
+});
+
+add_task(async function do_test_http2_header() {
+ const { httpProxyConnectResponseCode } = await test_http2_header(serverPort);
+ Assert.equal(httpProxyConnectResponseCode, 200);
+});
+
+add_task(async function do_test_http2_invalid_response_header_name_spaces() {
+ const { httpProxyConnectResponseCode } =
+ await test_http2_invalid_response_header(serverPort, "name_spaces");
+ Assert.equal(httpProxyConnectResponseCode, 200);
+});
+
+add_task(
+ async function do_test_http2_invalid_response_header_value_line_feed() {
+ const { httpProxyConnectResponseCode } =
+ await test_http2_invalid_response_header(serverPort, "value_line_feed");
+ Assert.equal(httpProxyConnectResponseCode, 200);
+ }
+);
+
+add_task(
+ async function do_test_http2_invalid_response_header_value_carriage_return() {
+ const { httpProxyConnectResponseCode } =
+ await test_http2_invalid_response_header(
+ serverPort,
+ "value_carriage_return"
+ );
+ Assert.equal(httpProxyConnectResponseCode, 200);
+ }
+);
+
+add_task(async function do_test_http2_invalid_response_header_value_null() {
+ const { httpProxyConnectResponseCode } =
+ await test_http2_invalid_response_header(serverPort, "value_null");
+ Assert.equal(httpProxyConnectResponseCode, 200);
+});
+
+add_task(async function do_test_http2_cookie_crumbling() {
+ const { httpProxyConnectResponseCode } = await test_http2_cookie_crumbling(
+ serverPort
+ );
+ Assert.equal(httpProxyConnectResponseCode, 200);
+});
+
+add_task(async function do_test_http2_multiplex() {
+ var values = await test_http2_multiplex(serverPort);
+ Assert.equal(values[0].httpProxyConnectResponseCode, 200);
+ Assert.equal(values[1].httpProxyConnectResponseCode, 200);
+ Assert.notEqual(values[0].streamID, values[1].streamID);
+});
+
+add_task(async function do_test_http2_big() {
+ const { httpProxyConnectResponseCode } = await test_http2_big(serverPort);
+ Assert.equal(httpProxyConnectResponseCode, 200);
+});
+
+add_task(async function do_test_http2_huge_suspended() {
+ const { httpProxyConnectResponseCode } = await test_http2_huge_suspended(
+ serverPort
+ );
+ Assert.equal(httpProxyConnectResponseCode, 200);
+});
+
+add_task(async function do_test_http2_post() {
+ const { httpProxyConnectResponseCode } = await test_http2_post(serverPort);
+ Assert.equal(httpProxyConnectResponseCode, 200);
+});
+
+add_task(async function do_test_http2_empty_post() {
+ const { httpProxyConnectResponseCode } = await test_http2_empty_post(
+ serverPort
+ );
+ Assert.equal(httpProxyConnectResponseCode, 200);
+});
+
+add_task(async function do_test_http2_patch() {
+ const { httpProxyConnectResponseCode } = await test_http2_patch(serverPort);
+ Assert.equal(httpProxyConnectResponseCode, 200);
+});
+
+add_task(async function do_test_http2_pushapi_1() {
+ const { httpProxyConnectResponseCode } = await test_http2_pushapi_1(
+ loadGroup,
+ serverPort
+ );
+ Assert.equal(httpProxyConnectResponseCode, 0);
+});
+
+add_task(async function do_test_http2_continuations() {
+ const { httpProxyConnectResponseCode } = await test_http2_continuations(
+ loadGroup,
+ serverPort
+ );
+ Assert.equal(httpProxyConnectResponseCode, 0);
+});
+
+add_task(async function do_test_http2_blocking_download() {
+ const { httpProxyConnectResponseCode } = await test_http2_blocking_download(
+ serverPort
+ );
+ Assert.equal(httpProxyConnectResponseCode, 200);
+});
+
+add_task(async function do_test_http2_illegalhpacksoft() {
+ const { httpProxyConnectResponseCode } = await test_http2_illegalhpacksoft(
+ serverPort
+ );
+ Assert.equal(httpProxyConnectResponseCode, 200);
+});
+
+add_task(async function do_test_http2_illegalhpackhard() {
+ const { httpProxyConnectResponseCode } = await test_http2_illegalhpackhard(
+ serverPort
+ );
+ Assert.equal(httpProxyConnectResponseCode, 200);
+});
+
+add_task(async function do_test_http2_folded_header() {
+ const { httpProxyConnectResponseCode } = await test_http2_folded_header(
+ loadGroup,
+ serverPort
+ );
+ Assert.equal(httpProxyConnectResponseCode, 200);
+});
+
+add_task(async function do_test_http2_empty_data() {
+ const { httpProxyConnectResponseCode } = await test_http2_empty_data(
+ serverPort
+ );
+ Assert.equal(httpProxyConnectResponseCode, 200);
+});
+
+add_task(async function do_test_http2_status_phrase() {
+ const { httpProxyConnectResponseCode } = await test_http2_status_phrase(
+ serverPort
+ );
+ Assert.equal(httpProxyConnectResponseCode, 200);
+});
+
+add_task(async function do_test_http2_doublepush() {
+ const { httpProxyConnectResponseCode } = await test_http2_doublepush(
+ loadGroup,
+ serverPort
+ );
+ Assert.equal(httpProxyConnectResponseCode, 200);
+});
+
+add_task(async function do_test_http2_disk_cache_push() {
+ const { httpProxyConnectResponseCode } = await test_http2_disk_cache_push(
+ loadGroup,
+ serverPort
+ );
+ Assert.equal(httpProxyConnectResponseCode, 200);
+});
+
+add_task(async function do_test_http2_h11required_stream() {
+ // 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
+ const { httpProxyConnectResponseCode } = await test_http2_h11required_stream(
+ serverPort
+ );
+ Assert.equal(httpProxyConnectResponseCode, 200);
+});
+
+add_task(async function do_test_http2_h11required_session() {
+ const { httpProxyConnectResponseCode } = await test_http2_h11required_session(
+ serverPort
+ );
+ Assert.equal(httpProxyConnectResponseCode, 200);
+});
+
+add_task(async function do_test_http2_retry_rst() {
+ const { httpProxyConnectResponseCode } = await test_http2_retry_rst(
+ serverPort
+ );
+ Assert.equal(httpProxyConnectResponseCode, 200);
+});
+
+add_task(async function do_test_http2_wrongsuite_tls12() {
+ // For this test we need to start HTTPS 1.1 proxy because HTTP/2 proxy cannot be used.
+ proxy.unregisterFilter();
+ let proxyHttp1 = new NodeHTTPSProxyServer();
+ await proxyHttp1.start();
+ proxyHttp1.registerFilter();
+ registerCleanupFunction(() => {
+ proxyHttp1.stop();
+ });
+ const { httpProxyConnectResponseCode } = await test_http2_wrongsuite_tls12(
+ serverPort
+ );
+ Assert.equal(httpProxyConnectResponseCode, 200);
+ proxyHttp1.unregisterFilter();
+ proxy.registerFilter();
+});
+
+add_task(async function do_test_http2_wrongsuite_tls13() {
+ const { httpProxyConnectResponseCode } = await test_http2_wrongsuite_tls13(
+ serverPort
+ );
+ Assert.equal(httpProxyConnectResponseCode, 200);
+});
+
+add_task(async function do_test_http2_push_firstparty1() {
+ const { httpProxyConnectResponseCode } = await test_http2_push_firstparty1(
+ loadGroup,
+ serverPort
+ );
+ Assert.equal(httpProxyConnectResponseCode, 200);
+});
+
+add_task(async function do_test_http2_push_firstparty2() {
+ const { httpProxyConnectResponseCode } = await test_http2_push_firstparty2(
+ loadGroup,
+ serverPort
+ );
+ Assert.equal(httpProxyConnectResponseCode, 200);
+});
+
+add_task(async function do_test_http2_push_firstparty3() {
+ const { httpProxyConnectResponseCode } = await test_http2_push_firstparty3(
+ loadGroup,
+ serverPort
+ );
+ Assert.equal(httpProxyConnectResponseCode, 200);
+});
+
+add_task(async function do_test_http2_push_userContext1() {
+ const { httpProxyConnectResponseCode } = await test_http2_push_userContext1(
+ loadGroup,
+ serverPort
+ );
+ Assert.equal(httpProxyConnectResponseCode, 200);
+});
+
+add_task(async function do_test_http2_push_userContext2() {
+ const { httpProxyConnectResponseCode } = await test_http2_push_userContext2(
+ loadGroup,
+ serverPort
+ );
+ Assert.equal(httpProxyConnectResponseCode, 200);
+});
+
+add_task(async function do_test_http2_push_userContext3() {
+ const { httpProxyConnectResponseCode } = await test_http2_push_userContext3(
+ loadGroup,
+ serverPort
+ );
+ Assert.equal(httpProxyConnectResponseCode, 200);
+});
diff --git a/netwerk/test/unit/test_http3.js b/netwerk/test/unit/test_http3.js
new file mode 100644
index 0000000000..7f3f5b118d
--- /dev/null
+++ b/netwerk/test/unit/test_http3.js
@@ -0,0 +1,571 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+// 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 h2Port = Services.env.get("MOZHTTP2_PORT");
+ Assert.notEqual(h2Port, null);
+ Assert.notEqual(h2Port, "");
+ let h3Port = Services.env.get("MOZHTTP3_PORT");
+ Assert.notEqual(h3Port, null);
+ Assert.notEqual(h3Port, "");
+ h3AltSvc = ":" + h3Port;
+
+ h3Route = "foo.example.com:" + h3Port;
+ do_get_profile();
+ prefs = Services.prefs;
+
+ prefs.setBoolPref("network.http.http3.enable", 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);
+ prefs.setBoolPref("network.http.altsvc.oe", 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-29=" + 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-29");
+ Assert.equal(this.onDataAvailableFired, true);
+ Assert.equal(request.getResponseHeader("X-Firefox-Http3"), "h3-29");
+ }
+ 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-29");
+ 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-29=: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-29");
+ }
+
+ 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-29");
+ 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.enable");
+ prefs.clearUserPref("network.dns.localDomains");
+ prefs.clearUserPref("network.proxy.allow_hijacking_localhost");
+ prefs.clearUserPref("network.http.altsvc.oe");
+ dump("testDone\n");
+ do_test_pending();
+ h1Server.stop(do_test_finished);
+}
diff --git a/netwerk/test/unit/test_http3_0rtt.js b/netwerk/test/unit/test_http3_0rtt.js
new file mode 100644
index 0000000000..1002263ed5
--- /dev/null
+++ b/netwerk/test/unit/test_http3_0rtt.js
@@ -0,0 +1,96 @@
+/* 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/. */
+
+var { setTimeout } = ChromeUtils.importESModule(
+ "resource://gre/modules/Timer.sys.mjs"
+);
+
+registerCleanupFunction(async () => {
+ http3_clear_prefs();
+});
+
+add_task(async function setup() {
+ await http3_setup_tests("h3-29");
+});
+
+let Http3Listener = function () {};
+
+Http3Listener.prototype = {
+ resumed: false,
+
+ onStartRequest: function testOnStartRequest(request) {
+ Assert.equal(request.status, Cr.NS_OK);
+ Assert.equal(request.responseStatus, 200);
+
+ let secinfo = request.securityInfo;
+ Assert.equal(secinfo.resumed, this.resumed);
+ Assert.ok(secinfo.serverCert != null);
+ },
+
+ 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-29");
+
+ 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;
+}
+
+async function test_first_conn_no_resumed() {
+ let listener = new Http3Listener();
+ listener.resumed = false;
+ let chan = makeChan("https://foo.example.com/30");
+ await chanPromise(chan, listener);
+}
+
+async function test_0RTT(enable_0rtt, resumed) {
+ info(`enable_0rtt=${enable_0rtt} resumed=${resumed}`);
+ Services.prefs.setBoolPref("network.http.http3.enable_0rtt", enable_0rtt);
+
+ // Make sure the h3 connection created by the previous test is cleared.
+ Services.obs.notifyObservers(null, "net:cancel-all-connections");
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => setTimeout(resolve, 1000));
+
+ // This connecion should be resumed.
+ let listener = new Http3Listener();
+ listener.resumed = resumed;
+ let chan = makeChan("https://foo.example.com/30");
+ await chanPromise(chan, listener);
+}
+
+add_task(async function test_0RTT_setups() {
+ await test_first_conn_no_resumed();
+
+ // http3.0RTT enabled
+ await test_0RTT(true, true);
+
+ // http3.0RTT disabled
+ await test_0RTT(false, false);
+});
diff --git a/netwerk/test/unit/test_http3_421.js b/netwerk/test/unit/test_http3_421.js
new file mode 100644
index 0000000000..de04babe06
--- /dev/null
+++ b/netwerk/test/unit/test_http3_421.js
@@ -0,0 +1,172 @@
+"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 h2Port = Services.env.get("MOZHTTP2_PORT");
+ Assert.notEqual(h2Port, null);
+ Assert.notEqual(h2Port, "");
+ let h3Port = Services.env.get("MOZHTTP3_PORT");
+ Assert.notEqual(h3Port, null);
+ Assert.notEqual(h3Port, "");
+ h3AltSvc = ":" + h3Port;
+
+ h3Route = "foo.example.com:" + h3Port;
+ do_get_profile();
+ prefs = Services.prefs;
+
+ prefs.setBoolPref("network.http.http3.enable", 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-29");
+ 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-29=: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.enable");
+ 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..201101eb19
--- /dev/null
+++ b/netwerk/test/unit/test_http3_alt_svc.js
@@ -0,0 +1,136 @@
+"use strict";
+
+let httpsOrigin;
+let h3AltSvc;
+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 h2Port = Services.env.get("MOZHTTP2_PORT");
+ Assert.notEqual(h2Port, null);
+ Assert.notEqual(h2Port, "");
+ let h3Port = Services.env.get("MOZHTTP3_PORT");
+ Assert.notEqual(h3Port, null);
+ Assert.notEqual(h3Port, "");
+ h3AltSvc = ":" + h3Port;
+
+ h3Route = "foo.example.com:" + h3Port;
+ do_get_profile();
+ prefs = Services.prefs;
+
+ prefs.setBoolPref("network.http.http3.enable", 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 createPrincipal(url) {
+ var ssm = Services.scriptSecurityManager;
+ try {
+ return ssm.createContentPrincipal(Services.io.newURI(url), {});
+ } catch (e) {
+ return null;
+ }
+}
+
+function makeChan(uri) {
+ let chan = NetUtil.newChannel({
+ uri,
+ loadingPrincipal: createPrincipal(uri),
+ securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_INHERITS_SEC_CONTEXT,
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT,
+ }).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-29");
+ 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-29=:h3port,h3-30=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.enable");
+ prefs.clearUserPref("network.dns.localDomains");
+ prefs.clearUserPref("network.proxy.allow_hijacking_localhost");
+ dump("testDone\n");
+}
diff --git a/netwerk/test/unit/test_http3_coalescing.js b/netwerk/test/unit/test_http3_coalescing.js
new file mode 100644
index 0000000000..b6c19ef626
--- /dev/null
+++ b/netwerk/test/unit/test_http3_coalescing.js
@@ -0,0 +1,113 @@
+/* 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 { setTimeout } = ChromeUtils.importESModule(
+ "resource://gre/modules/Timer.sys.mjs"
+);
+
+let h2Port;
+let h3Port;
+
+const certOverrideService = Cc[
+ "@mozilla.org/security/certoverride;1"
+].getService(Ci.nsICertOverrideService);
+
+function setup() {
+ h2Port = Services.env.get("MOZHTTP2_PORT");
+ Assert.notEqual(h2Port, null);
+ Assert.notEqual(h2Port, "");
+
+ h3Port = Services.env.get("MOZHTTP3_PORT");
+ Assert.notEqual(h3Port, null);
+ Assert.notEqual(h3Port, "");
+ Services.prefs.setBoolPref("network.http.http3.enable", true);
+}
+
+setup();
+registerCleanupFunction(async () => {
+ Services.prefs.clearUserPref("network.dns.upgrade_with_https_rr");
+ Services.prefs.clearUserPref("network.dns.use_https_rr_as_altsvc");
+ Services.prefs.clearUserPref("network.dns.echconfig.enabled");
+ Services.prefs.clearUserPref(
+ "network.dns.echconfig.fallback_to_origin_when_all_failed"
+ );
+ Services.prefs.clearUserPref("network.dns.httpssvc.reset_exclustion_list");
+ Services.prefs.clearUserPref("network.http.http3.enable");
+ Services.prefs.clearUserPref(
+ "network.dns.httpssvc.http3_fast_fallback_timeout"
+ );
+ Services.prefs.clearUserPref(
+ "network.http.http3.alt-svc-mapping-for-testing"
+ );
+ Services.prefs.clearUserPref("network.dns.localDomains");
+ Services.prefs.clearUserPref("network.http.speculative-parallel-limit");
+});
+
+function makeChan(url) {
+ let chan = NetUtil.newChannel({
+ uri: url,
+ loadUsingSystemPrincipal: true,
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT,
+ }).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
+ );
+ }
+ certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData(
+ true
+ );
+ chan.asyncOpen(new ChannelListener(finish, null, flags));
+ });
+}
+
+async function H3CoalescingTest(host1, host2) {
+ Services.prefs.setCharPref(
+ "network.http.http3.alt-svc-mapping-for-testing",
+ `${host1};h3-29=:${h3Port}`
+ );
+ Services.prefs.setCharPref("network.dns.localDomains", host1);
+
+ let chan = makeChan(`https://${host1}`);
+ let [req] = await channelOpenPromise(chan, CL_ALLOW_UNKNOWN_CL);
+ req.QueryInterface(Ci.nsIHttpChannel);
+ Assert.equal(req.protocolVersion, "h3-29");
+ let hash = req.getResponseHeader("x-http3-conn-hash");
+
+ Services.prefs.setCharPref(
+ "network.http.http3.alt-svc-mapping-for-testing",
+ `${host2};h3-29=:${h3Port}`
+ );
+ Services.prefs.setCharPref("network.dns.localDomains", host2);
+
+ chan = makeChan(`https://${host2}`);
+ [req] = await channelOpenPromise(chan, CL_ALLOW_UNKNOWN_CL);
+ req.QueryInterface(Ci.nsIHttpChannel);
+ Assert.equal(req.protocolVersion, "h3-29");
+ // The port used by the second connection should be the same as the first one.
+ Assert.equal(req.getResponseHeader("x-http3-conn-hash"), hash);
+}
+
+add_task(async function testH3CoalescingWithSpeculativeConnection() {
+ await http3_setup_tests("h3-29");
+ Services.prefs.setIntPref("network.http.speculative-parallel-limit", 6);
+ await H3CoalescingTest("foo.h3_coalescing.org", "bar.h3_coalescing.org");
+});
+
+add_task(async function testH3CoalescingWithoutSpeculativeConnection() {
+ Services.prefs.setIntPref("network.http.speculative-parallel-limit", 0);
+ Services.obs.notifyObservers(null, "net:cancel-all-connections");
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => setTimeout(resolve, 1000));
+ await H3CoalescingTest("baz.h3_coalescing.org", "qux.h3_coalescing.org");
+});
diff --git a/netwerk/test/unit/test_http3_direct_proxy.js b/netwerk/test/unit/test_http3_direct_proxy.js
new file mode 100644
index 0000000000..74d2b11fc1
--- /dev/null
+++ b/netwerk/test/unit/test_http3_direct_proxy.js
@@ -0,0 +1,54 @@
+/* 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 a HTTP3 connection can be established when a proxy info says
+// to use direct connection
+
+"use strict";
+
+registerCleanupFunction(async () => {
+ http3_clear_prefs();
+ Services.prefs.clearUserPref("network.proxy.type");
+ Services.prefs.clearUserPref("network.proxy.autoconfig_url");
+});
+
+add_task(async function setup() {
+ await http3_setup_tests("h3-29");
+});
+
+function makeChan(url) {
+ let chan = NetUtil.newChannel({
+ uri: url,
+ loadUsingSystemPrincipal: true,
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT,
+ }).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]);
+ }
+ chan.asyncOpen(new ChannelListener(finish, null, flags));
+ });
+}
+
+add_task(async function testHttp3WithDirectProxy() {
+ var pac =
+ "data:text/plain," +
+ "function FindProxyForURL(url, host) {" +
+ ' return "DIRECT; PROXY foopy:8080;"' +
+ "}";
+
+ // Configure PAC
+ Services.prefs.setIntPref("network.proxy.type", 2);
+ Services.prefs.setCharPref("network.proxy.autoconfig_url", pac);
+
+ let chan = makeChan(`https://foo.example.com`);
+ let [req] = await channelOpenPromise(chan, CL_ALLOW_UNKNOWN_CL);
+ req.QueryInterface(Ci.nsIHttpChannel);
+ Assert.equal(req.protocolVersion, "h3-29");
+});
diff --git a/netwerk/test/unit/test_http3_dns_retry.js b/netwerk/test/unit/test_http3_dns_retry.js
new file mode 100644
index 0000000000..facbd4e90b
--- /dev/null
+++ b/netwerk/test/unit/test_http3_dns_retry.js
@@ -0,0 +1,357 @@
+/* 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 { setTimeout } = ChromeUtils.importESModule(
+ "resource://gre/modules/Timer.sys.mjs"
+);
+
+let h2Port;
+let h3Port;
+let trrServer;
+
+const certOverrideService = Cc[
+ "@mozilla.org/security/certoverride;1"
+].getService(Ci.nsICertOverrideService);
+
+add_setup(async function setup() {
+ h2Port = Services.env.get("MOZHTTP2_PORT");
+ Assert.notEqual(h2Port, null);
+ Assert.notEqual(h2Port, "");
+
+ h3Port = Services.env.get("MOZHTTP3_PORT");
+ Assert.notEqual(h3Port, null);
+ Assert.notEqual(h3Port, "");
+
+ trr_test_setup();
+
+ if (mozinfo.socketprocess_networking) {
+ Cc["@mozilla.org/network/protocol;1?name=http"].getService(
+ Ci.nsIHttpProtocolHandler
+ );
+ Services.dns; // Needed to trigger socket process.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => setTimeout(resolve, 1000));
+ }
+
+ Services.prefs.setIntPref("network.trr.mode", 2); // TRR first
+ Services.prefs.setBoolPref("network.http.http3.enable", true);
+ Services.prefs.setIntPref("network.http.speculative-parallel-limit", 6);
+ Services.prefs.setBoolPref(
+ "network.http.http3.block_loopback_ipv6_addr",
+ true
+ );
+ Services.prefs.setBoolPref(
+ "network.http.http3.retry_different_ip_family",
+ true
+ );
+ Services.prefs.setBoolPref("network.dns.get-ttl", false);
+
+ certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData(
+ true
+ );
+
+ registerCleanupFunction(async () => {
+ certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData(
+ false
+ );
+ trr_clear_prefs();
+ Services.prefs.clearUserPref(
+ "network.http.http3.retry_different_ip_family"
+ );
+ Services.prefs.clearUserPref("network.http.speculative-parallel-limit");
+ Services.prefs.clearUserPref("network.http.http3.block_loopback_ipv6_addr");
+ Services.prefs.clearUserPref("network.dns.get-ttl");
+ if (trrServer) {
+ await trrServer.stop();
+ }
+ });
+});
+
+function makeChan(url) {
+ let chan = NetUtil.newChannel({
+ uri: url,
+ loadUsingSystemPrincipal: true,
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT,
+ }).QueryInterface(Ci.nsIHttpChannel);
+ chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI;
+ return chan;
+}
+
+function channelOpenPromise(chan, flags) {
+ // eslint-disable-next-line no-async-promise-executor
+ return new Promise(async resolve => {
+ function finish(req, buffer) {
+ resolve([req, buffer]);
+ }
+ let internal = chan.QueryInterface(Ci.nsIHttpChannelInternal);
+ internal.setWaitForHTTPSSVCRecord();
+
+ chan.asyncOpen(new ChannelListener(finish, null, flags));
+ });
+}
+
+async function registerDoHAnswers(host, ipv4Answers, ipv6Answers, httpsRecord) {
+ 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`
+ );
+
+ await trrServer.registerDoHAnswers(host, "HTTPS", {
+ answers: httpsRecord,
+ });
+
+ await trrServer.registerDoHAnswers(host, "AAAA", {
+ answers: ipv6Answers,
+ });
+
+ await trrServer.registerDoHAnswers(host, "A", {
+ answers: ipv4Answers,
+ });
+
+ Services.dns.clearCache(true);
+}
+
+// Test if we retry IPv4 address for Http/3 properly.
+add_task(async function test_retry_with_ipv4() {
+ let host = "test.http3_retry.com";
+ let ipv4answers = [
+ {
+ name: host,
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "127.0.0.1",
+ },
+ ];
+ // The UDP socket will return connection refused error because we set
+ // "network.http.http3.block_loopback_ipv6_addr" to true.
+ let ipv6answers = [
+ {
+ name: host,
+ ttl: 55,
+ type: "AAAA",
+ flush: false,
+ data: "::1",
+ },
+ ];
+ let httpsRecord = [
+ {
+ name: host,
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 1,
+ name: host,
+ values: [
+ { key: "alpn", value: "h3" },
+ { key: "port", value: h3Port },
+ ],
+ },
+ },
+ ];
+
+ await registerDoHAnswers(host, ipv4answers, ipv6answers, httpsRecord);
+
+ let chan = makeChan(`https://${host}`);
+ let [req] = await channelOpenPromise(chan);
+ Assert.equal(req.protocolVersion, "h3");
+
+ await trrServer.stop();
+});
+
+add_task(async function test_retry_with_ipv4_disabled() {
+ let host = "test.http3_retry_ipv4_blocked.com";
+ let ipv4answers = [
+ {
+ name: host,
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "127.0.0.1",
+ },
+ ];
+ // The UDP socket will return connection refused error because we set
+ // "network.http.http3.block_loopback_ipv6_addr" to true.
+ let ipv6answers = [
+ {
+ name: host,
+ ttl: 55,
+ type: "AAAA",
+ flush: false,
+ data: "::1",
+ },
+ ];
+ let httpsRecord = [
+ {
+ name: host,
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 1,
+ name: host,
+ values: [
+ { key: "alpn", value: "h3" },
+ { key: "port", value: h3Port },
+ ],
+ },
+ },
+ ];
+
+ await registerDoHAnswers(host, ipv4answers, ipv6answers, httpsRecord);
+
+ let chan = makeChan(`https://${host}`);
+ chan.QueryInterface(Ci.nsIHttpChannelInternal);
+ chan.setIPv4Disabled();
+
+ await channelOpenPromise(chan, CL_EXPECT_FAILURE);
+ await trrServer.stop();
+});
+
+// See bug 1837252. There is no way to observe the outcome of this test, because
+// the crash in bug 1837252 is only triggered by speculative connection.
+// The outcome of this test is no crash.
+add_task(async function test_retry_with_ipv4_failed() {
+ let host = "test.http3_retry_failed.com";
+ // Return a wrong answer intentionally.
+ let ipv4answers = [
+ {
+ name: host,
+ ttl: 55,
+ type: "AAAA",
+ flush: false,
+ data: "127.0.0.1",
+ },
+ ];
+ // The UDP socket will return connection refused error because we set
+ // "network.http.http3.block_loopback_ipv6_addr" to true.
+ let ipv6answers = [
+ {
+ name: host,
+ ttl: 55,
+ type: "AAAA",
+ flush: false,
+ data: "::1",
+ },
+ ];
+ let httpsRecord = [
+ {
+ name: host,
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 1,
+ name: host,
+ values: [
+ { key: "alpn", value: "h3" },
+ { key: "port", value: h3Port },
+ ],
+ },
+ },
+ ];
+
+ await registerDoHAnswers(host, ipv4answers, ipv6answers, httpsRecord);
+
+ // This speculative connection is used to trigger the mechanism to retry
+ // Http/3 connection with a IPv4 address.
+ // We want to make the connection entry's IP preference known,
+ // so DnsAndConnectSocket::mRetryWithDifferentIPFamily will be set to true
+ // before the second speculative connection.
+ let uri = Services.io.newURI(`https://test.http3_retry_failed.com`);
+ Services.io.speculativeConnect(
+ uri,
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ null,
+ false
+ );
+
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => setTimeout(resolve, 3000));
+
+ // When this speculative connection is created, the connection entry is
+ // already set to prefer IPv4. Since we provided an invalid A response,
+ // DnsAndConnectSocket::OnLookupComplete is called with an error.
+ // Since DnsAndConnectSocket::mRetryWithDifferentIPFamily is true, we do
+ // retry DNS lookup. During retry, we should not create UDP connection.
+ Services.io.speculativeConnect(
+ uri,
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ null,
+ false
+ );
+
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => setTimeout(resolve, 3000));
+ await trrServer.stop();
+});
+
+add_task(async function test_retry_with_0rtt() {
+ let host = "test.http3_retry_0rtt.com";
+ let ipv4answers = [
+ {
+ name: host,
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "127.0.0.1",
+ },
+ ];
+ // The UDP socket will return connection refused error because we set
+ // "network.http.http3.block_loopback_ipv6_addr" to true.
+ let ipv6answers = [
+ {
+ name: host,
+ ttl: 55,
+ type: "AAAA",
+ flush: false,
+ data: "::1",
+ },
+ ];
+ let httpsRecord = [
+ {
+ name: host,
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 1,
+ name: host,
+ values: [
+ { key: "alpn", value: "h3" },
+ { key: "port", value: h3Port },
+ ],
+ },
+ },
+ ];
+
+ await registerDoHAnswers(host, ipv4answers, ipv6answers, httpsRecord);
+
+ let chan = makeChan(`https://${host}`);
+ chan.QueryInterface(Ci.nsIHttpChannelInternal);
+ chan.setIPv6Disabled();
+
+ let [req] = await channelOpenPromise(chan);
+ Assert.equal(req.protocolVersion, "h3");
+
+ // Make sure the h3 connection created by the previous test is cleared.
+ Services.obs.notifyObservers(null, "net:cancel-all-connections");
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => setTimeout(resolve, 1000));
+
+ chan = makeChan(`https://${host}`);
+ chan.QueryInterface(Ci.nsIHttpChannelInternal);
+
+ [req] = await channelOpenPromise(chan);
+ Assert.equal(req.protocolVersion, "h3");
+
+ await trrServer.stop();
+});
diff --git a/netwerk/test/unit/test_http3_early_hint_listener.js b/netwerk/test/unit/test_http3_early_hint_listener.js
new file mode 100644
index 0000000000..5100ea3151
--- /dev/null
+++ b/netwerk/test/unit/test_http3_early_hint_listener.js
@@ -0,0 +1,92 @@
+/* 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";
+
+// The server will always respond with a 103 EarlyHint followed by a
+// 200 response.
+// 103 response contains:
+// 1) a non-link header
+// 2) a link header if a request has a "link-to-set" header. If the
+// request header is not set, the response will not have Link headers.
+// A "link-to-set" header may contain multiple link headers
+// separated with a comma.
+
+const earlyhintspath = "/103_response";
+const hint1 = "</style.css>; rel=preload; as=style";
+const hint2 = "</img.png>; rel=preload; as=image";
+
+let EarlyHintsListener = function () {};
+
+EarlyHintsListener.prototype = {
+ _expected_hints: [],
+ earlyHintsReceived: 0,
+
+ QueryInterface: ChromeUtils.generateQI(["nsIEarlyHintObserver"]),
+
+ earlyHint(header) {
+ Assert.ok(this._expected_hints.includes(header));
+ this.earlyHintsReceived += 1;
+ },
+};
+
+function chanPromise(uri, listener, headers) {
+ var principal = Services.scriptSecurityManager.createContentPrincipal(
+ NetUtil.newURI(uri),
+ {}
+ );
+ var chan = NetUtil.newChannel({
+ uri,
+ loadingPrincipal: principal,
+ securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT,
+ });
+
+ chan
+ .QueryInterface(Ci.nsIHttpChannel)
+ .setRequestHeader("link-to-set", headers, false);
+ chan.QueryInterface(Ci.nsIHttpChannelInternal).setEarlyHintObserver(listener);
+
+ return promiseAsyncOpen(chan);
+}
+
+registerCleanupFunction(async () => {
+ http3_clear_prefs();
+});
+
+add_task(async function setup() {
+ await http3_setup_tests("h3-29");
+});
+
+add_task(async function early_hints() {
+ let earlyHints = new EarlyHintsListener();
+ earlyHints._expected_hints = [hint1];
+
+ await chanPromise(
+ `https://foo.example.com${earlyhintspath}`,
+ earlyHints,
+ hint1
+ );
+ Assert.equal(earlyHints.earlyHintsReceived, 1);
+});
+
+// Test when there is no Link header in a 103 response.
+// 103 response will contain non-link headers.
+add_task(async function no_early_hints() {
+ let earlyHints = new EarlyHintsListener();
+
+ await chanPromise(`https://foo.example.com${earlyhintspath}`, earlyHints, "");
+ Assert.equal(earlyHints.earlyHintsReceived, 0);
+});
+
+add_task(async function early_hints_multiple() {
+ let earlyHints = new EarlyHintsListener();
+ earlyHints._expected_hints = [hint1, hint2];
+
+ await chanPromise(
+ `https://foo.example.com${earlyhintspath}`,
+ earlyHints,
+ hint1 + ", " + hint2
+ );
+ Assert.equal(earlyHints.earlyHintsReceived, 2);
+});
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..72f8d61182
--- /dev/null
+++ b/netwerk/test/unit/test_http3_error_before_connect.js
@@ -0,0 +1,109 @@
+/* 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.enable");
+ 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 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 h2Port = Services.env.get("MOZHTTP2_PORT");
+ Assert.notEqual(h2Port, null);
+ let h3Port = Services.env.get("MOZHTTP3_PORT_NO_RESPONSE");
+ Assert.notEqual(h3Port, null);
+ Assert.notEqual(h3Port, "");
+
+ Services.prefs.setBoolPref("network.http.http3.enable", 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-29=:" + 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..ab2b6b7044
--- /dev/null
+++ b/netwerk/test/unit/test_http3_fast_fallback.js
@@ -0,0 +1,908 @@
+/* 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 { setTimeout } = ChromeUtils.importESModule(
+ "resource://gre/modules/Timer.sys.mjs"
+);
+
+let h2Port;
+let h3Port;
+let trrServer;
+
+const { TestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TestUtils.sys.mjs"
+);
+const certOverrideService = Cc[
+ "@mozilla.org/security/certoverride;1"
+].getService(Ci.nsICertOverrideService);
+
+add_setup(async function setup() {
+ h2Port = Services.env.get("MOZHTTP2_PORT");
+ Assert.notEqual(h2Port, null);
+ Assert.notEqual(h2Port, "");
+
+ h3Port = Services.env.get("MOZHTTP3_PORT_NO_RESPONSE");
+ Assert.notEqual(h3Port, null);
+ Assert.notEqual(h3Port, "");
+
+ trr_test_setup();
+
+ if (mozinfo.socketprocess_networking) {
+ Services.dns; // Needed to trigger socket process.
+ await TestUtils.waitForCondition(() => Services.io.socketProcessLaunched);
+ }
+
+ Services.prefs.setIntPref("network.trr.mode", 2); // TRR first
+ Services.prefs.setBoolPref("network.http.http3.enable", true);
+ Services.prefs.setIntPref("network.http.speculative-parallel-limit", 6);
+
+ registerCleanupFunction(async () => {
+ trr_clear_prefs();
+ Services.prefs.clearUserPref("network.dns.upgrade_with_https_rr");
+ Services.prefs.clearUserPref("network.dns.use_https_rr_as_altsvc");
+ Services.prefs.clearUserPref("network.dns.echconfig.enabled");
+ Services.prefs.clearUserPref("network.dns.http3_echconfig.enabled");
+ Services.prefs.clearUserPref(
+ "network.dns.echconfig.fallback_to_origin_when_all_failed"
+ );
+ Services.prefs.clearUserPref("network.dns.httpssvc.reset_exclustion_list");
+ Services.prefs.clearUserPref("network.http.http3.enable");
+ Services.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");
+ Services.prefs.clearUserPref(
+ "network.http.http3.parallel_fallback_conn_limit"
+ );
+ if (trrServer) {
+ await trrServer.stop();
+ }
+ });
+});
+
+function makeChan(url) {
+ let chan = NetUtil.newChannel({
+ uri: url,
+ loadUsingSystemPrincipal: true,
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT,
+ }).QueryInterface(Ci.nsIHttpChannel);
+ chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI;
+ return chan;
+}
+
+function channelOpenPromise(chan, flags, delay) {
+ // eslint-disable-next-line no-async-promise-executor
+ return new Promise(async resolve => {
+ function finish(req, buffer) {
+ resolve([req, buffer]);
+ certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData(
+ false
+ );
+ }
+ let internal = chan.QueryInterface(Ci.nsIHttpChannelInternal);
+ internal.setWaitForHTTPSSVCRecord();
+ certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData(
+ true
+ );
+ if (delay) {
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(r => setTimeout(r, delay));
+ }
+ 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.enable", 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-29=:" + h3Port
+ );
+ Services.prefs.setBoolPref("network.dns.disableIPv6", true);
+ Services.prefs.setIntPref("network.http.speculative-parallel-limit", 6);
+
+ await fast_fallback_test();
+});
+
+// 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");
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ 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);
+
+ 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.enable", true);
+
+ Services.prefs.setIntPref(
+ "network.dns.httpssvc.http3_fast_fallback_timeout",
+ 1000
+ );
+
+ await trrServer.registerDoHAnswers("test.fastfallback.com", "HTTPS", {
+ answers: [
+ {
+ name: "test.fastfallback.com",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 1,
+ name: "test.fastfallback1.com",
+ values: [
+ { key: "alpn", value: "h3-29" },
+ { 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", {
+ answers: [
+ {
+ name: "test.fastfallback1.com",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "127.0.0.1",
+ },
+ ],
+ });
+
+ await trrServer.registerDoHAnswers("test.fastfallback2.com", "A", {
+ answers: [
+ {
+ 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.enable", true);
+
+ Services.prefs.setIntPref(
+ "network.dns.httpssvc.http3_fast_fallback_timeout",
+ 10
+ );
+
+ await trrServer.registerDoHAnswers("test.fastfallback.org", "HTTPS", {
+ answers: [
+ {
+ name: "test.fastfallback.org",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 1,
+ name: "test.fastfallback1.org",
+ values: [
+ { key: "alpn", value: "h3-29" },
+ { 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", {
+ answers: [
+ {
+ name: "test.fastfallback1.org",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "127.0.0.1",
+ },
+ ],
+ });
+
+ await trrServer.registerDoHAnswers("test.fastfallback2.org", "A", {
+ answers: [
+ {
+ 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.setBoolPref("network.dns.http3_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.enable", true);
+
+ Services.prefs.setIntPref(
+ "network.dns.httpssvc.http3_fast_fallback_timeout",
+ 50
+ );
+
+ await trrServer.registerDoHAnswers("test.ech.org", "HTTPS", {
+ answers: [
+ {
+ name: "test.ech.org",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 1,
+ name: "test.ech1.org",
+ values: [
+ { key: "alpn", value: "h3-29" },
+ { 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", {
+ answers: [
+ {
+ name: "test.ech1.org",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "127.0.0.1",
+ },
+ ],
+ });
+
+ await trrServer.registerDoHAnswers("test.ech3.org", "A", {
+ answers: [
+ {
+ 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.setBoolPref("network.dns.http3_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.enable", true);
+
+ Services.prefs.setIntPref(
+ "network.dns.httpssvc.http3_fast_fallback_timeout",
+ 50
+ );
+
+ await trrServer.registerDoHAnswers("test.partial_ech.org", "HTTPS", {
+ answers: [
+ {
+ name: "test.partial_ech.org",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 1,
+ name: "test.partial_ech1.org",
+ values: [
+ { key: "alpn", value: "h3-29" },
+ { 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", {
+ answers: [
+ {
+ name: "test.partial_ech1.org",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "127.0.0.1",
+ },
+ ],
+ });
+
+ await trrServer.registerDoHAnswers("test.partial_ech2.org", "A", {
+ answers: [
+ {
+ name: "test.partial_ech2.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();
+});
+
+add_task(async function testFastfallbackWithoutEchConfig() {
+ 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.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.enable", true);
+
+ Services.prefs.setIntPref(
+ "network.dns.httpssvc.http3_fast_fallback_timeout",
+ 50
+ );
+
+ await trrServer.registerDoHAnswers("test.no_ech_h2.org", "HTTPS", {
+ answers: [
+ {
+ name: "test.no_ech_h2.org",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 1,
+ name: "test.no_ech_h3.org",
+ values: [
+ { key: "alpn", value: "h3-29" },
+ { key: "port", value: h3Port },
+ ],
+ },
+ },
+ ],
+ });
+
+ await trrServer.registerDoHAnswers("test.no_ech_h3.org", "A", {
+ answers: [
+ {
+ name: "test.no_ech_h3.org",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "127.0.0.1",
+ },
+ ],
+ });
+
+ await trrServer.registerDoHAnswers("test.no_ech_h2.org", "A", {
+ answers: [
+ {
+ name: "test.no_ech_h2.org",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "127.0.0.1",
+ },
+ ],
+ });
+
+ let chan = makeChan(`https://test.no_ech_h2.org:${h2Port}/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();
+});
+
+add_task(async function testH3FallbackWithMultipleTransactions() {
+ 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.enable", true);
+
+ // Disable fast fallback.
+ Services.prefs.setIntPref(
+ "network.http.http3.parallel_fallback_conn_limit",
+ 0
+ );
+ Services.prefs.setIntPref("network.http.speculative-parallel-limit", 0);
+
+ await trrServer.registerDoHAnswers("test.multiple_trans.org", "HTTPS", {
+ answers: [
+ {
+ name: "test.multiple_trans.org",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 1,
+ name: "test.multiple_trans.org",
+ values: [
+ { key: "alpn", value: "h3-29" },
+ { key: "port", value: h3Port },
+ ],
+ },
+ },
+ ],
+ });
+
+ await trrServer.registerDoHAnswers("test.multiple_trans.org", "A", {
+ answers: [
+ {
+ name: "test.multiple_trans.org",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "127.0.0.1",
+ },
+ ],
+ });
+
+ let promises = [];
+ for (let i = 0; i < 2; ++i) {
+ let chan = makeChan(
+ `https://test.multiple_trans.org:${h2Port}/server-timing`
+ );
+ promises.push(channelOpenPromise(chan));
+ }
+
+ let res = await Promise.all(promises);
+ res.forEach(function (e) {
+ let [req] = e;
+ Assert.equal(req.protocolVersion, "h2");
+ let internal = req.QueryInterface(Ci.nsIHttpChannelInternal);
+ Assert.equal(internal.remotePort, h2Port);
+ });
+
+ await trrServer.stop();
+});
+
+add_task(async function testTwoFastFallbackTimers() {
+ 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.enable", true);
+
+ Services.prefs.setIntPref("network.http.speculative-parallel-limit", 6);
+ Services.prefs.clearUserPref(
+ "network.http.http3.parallel_fallback_conn_limit"
+ );
+
+ Services.prefs.setCharPref(
+ "network.http.http3.alt-svc-mapping-for-testing",
+ "foo.fallback.org;h3-29=:" + h3Port
+ );
+
+ Services.prefs.setIntPref(
+ "network.dns.httpssvc.http3_fast_fallback_timeout",
+ 10
+ );
+ Services.prefs.setIntPref("network.http.http3.backup_timer_delay", 100);
+
+ await trrServer.registerDoHAnswers("foo.fallback.org", "HTTPS", {
+ answers: [
+ {
+ name: "foo.fallback.org",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 1,
+ name: "foo.fallback.org",
+ values: [
+ { key: "alpn", value: "h3-29" },
+ { key: "port", value: h3Port },
+ ],
+ },
+ },
+ ],
+ });
+
+ await trrServer.registerDoHAnswers("foo.fallback.org", "A", {
+ answers: [
+ {
+ name: "foo.fallback.org",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "127.0.0.1",
+ },
+ ],
+ });
+
+ // Test the case that http3 backup timer is triggered after
+ // fast fallback timer or HTTPS RR.
+ Services.prefs.setIntPref(
+ "network.dns.httpssvc.http3_fast_fallback_timeout",
+ 10
+ );
+ Services.prefs.setIntPref("network.http.http3.backup_timer_delay", 100);
+
+ async function createChannelAndStartTest() {
+ let chan = makeChan(`https://foo.fallback.org:${h2Port}/server-timing`);
+ let [req] = await channelOpenPromise(chan);
+ Assert.equal(req.protocolVersion, "h2");
+ let internal = req.QueryInterface(Ci.nsIHttpChannelInternal);
+ Assert.equal(internal.remotePort, h2Port);
+ }
+
+ await createChannelAndStartTest();
+
+ Services.obs.notifyObservers(null, "net:prune-all-connections");
+ Services.obs.notifyObservers(null, "network:reset-http3-excluded-list");
+ Services.dns.clearCache(true);
+
+ // Do the same test again, but with a different configuration.
+ Services.prefs.setIntPref(
+ "network.dns.httpssvc.http3_fast_fallback_timeout",
+ 100
+ );
+ Services.prefs.setIntPref("network.http.http3.backup_timer_delay", 10);
+
+ await createChannelAndStartTest();
+
+ await trrServer.stop();
+});
+
+add_task(async function testH3FastFallbackWithMultipleTransactions() {
+ 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.enable", true);
+
+ Services.prefs.setIntPref("network.http.speculative-parallel-limit", 6);
+ Services.prefs.clearUserPref(
+ "network.http.http3.parallel_fallback_conn_limit"
+ );
+
+ Services.prefs.setIntPref("network.http.http3.backup_timer_delay", 500);
+
+ Services.prefs.setCharPref(
+ "network.http.http3.alt-svc-mapping-for-testing",
+ "test.multiple_fallback_trans.org;h3-29=:" + h3Port
+ );
+
+ await trrServer.registerDoHAnswers("test.multiple_fallback_trans.org", "A", {
+ answers: [
+ {
+ name: "test.multiple_fallback_trans.org",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "127.0.0.1",
+ },
+ ],
+ });
+
+ let promises = [];
+ for (let i = 0; i < 3; ++i) {
+ let chan = makeChan(
+ `https://test.multiple_fallback_trans.org:${h2Port}/server-timing`
+ );
+ if (i == 0) {
+ promises.push(channelOpenPromise(chan));
+ } else {
+ promises.push(channelOpenPromise(chan, null, 500));
+ }
+ }
+
+ let res = await Promise.all(promises);
+ res.forEach(function (e) {
+ let [req] = e;
+ Assert.equal(req.protocolVersion, "h2");
+ let internal = req.QueryInterface(Ci.nsIHttpChannelInternal);
+ Assert.equal(internal.remotePort, h2Port);
+ });
+
+ await trrServer.stop();
+});
+
+add_task(async function testFastfallbackToTheSameRecord() {
+ 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.setBoolPref("network.dns.http3_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.enable", true);
+
+ Services.prefs.setIntPref(
+ "network.dns.httpssvc.http3_fast_fallback_timeout",
+ 1000
+ );
+
+ await trrServer.registerDoHAnswers("test.ech.org", "HTTPS", {
+ answers: [
+ {
+ name: "test.ech.org",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 1,
+ name: "test.ech1.org",
+ values: [
+ { key: "alpn", value: ["h3-29", "h2"] },
+ { key: "port", value: h2Port },
+ { key: "echconfig", value: "456..." },
+ ],
+ },
+ },
+ ],
+ });
+
+ await trrServer.registerDoHAnswers("test.ech1.org", "A", {
+ answers: [
+ {
+ name: "test.ech1.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();
+});
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..4c0b41089a
--- /dev/null
+++ b/netwerk/test/unit/test_http3_fatal_stream_error.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/. */
+"use strict";
+
+let httpsUri;
+
+registerCleanupFunction(async () => {
+ Services.prefs.clearUserPref("network.http.http3.enable");
+ 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 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 h2Port = Services.env.get("MOZHTTP2_PORT");
+ Assert.notEqual(h2Port, null);
+
+ let h3Port = Services.env.get("MOZHTTP3_PORT_FAILED");
+ Assert.notEqual(h3Port, null);
+ Assert.notEqual(h3Port, "");
+
+ Services.prefs.setBoolPref("network.http.http3.enable", 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-29=:" + 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..2ed8e51bb4
--- /dev/null
+++ b/netwerk/test/unit/test_http3_large_post.js
@@ -0,0 +1,165 @@
+/* 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("h3-29");
+});
+
+let Http3Listener = function (amount) {
+ this.amount = amount;
+};
+
+Http3Listener.prototype = {
+ expectedStatus: Cr.NS_OK,
+ amount: 0,
+ onProgressMaxNotificationCount: 0,
+ onProgressNotificationCount: 0,
+
+ QueryInterface: ChromeUtils.generateQI(["nsIProgressEventSink"]),
+
+ getInterface(iid) {
+ if (iid.equals(Ci.nsIProgressEventSink)) {
+ return this;
+ }
+ throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE);
+ },
+
+ onProgress(request, progress, progressMax) {
+ // we will get notifications for the request and the response.
+ if (progress === progressMax) {
+ this.onProgressMaxNotificationCount += 1;
+ }
+ // For a large upload there should be a multiple notifications.
+ this.onProgressNotificationCount += 1;
+ },
+
+ onStatus(request, status, statusArg) {},
+
+ 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-29");
+ // We should get 2 correctOnProgress, i.e. one for request and one for the response.
+ Assert.equal(this.onProgressMaxNotificationCount, 2);
+ if (this.amount > 500000) {
+ // 10 is an arbitrary number, but we cannot calculate exact number
+ // because it depends on timing.
+ Assert.ok(this.onProgressNotificationCount > 10);
+ }
+ 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(amount);
+ let chan = makeChan("https://foo.example.com/post", amount);
+ chan.notificationCallbacks = listener;
+ 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(amount);
+ let chan = makeChan("https://foo.example.com/post", amount);
+ chan.notificationCallbacks = listener;
+ await chanPromise(chan, listener);
+});
+
+// Send a post in the same way viaduct does in bug 1749957.
+add_task(async function test_bug1749957_bug1750056() {
+ let amount = 200; // Tests the bug as long as it's non-zero.
+ let uri = "https://foo.example.com/post";
+ let listener = new Http3Listener(amount);
+
+ let chan = NetUtil.newChannel({
+ uri,
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+
+ // https://searchfox.org/mozilla-central/rev/1920b17ac5988fcfec4e45e2a94478ebfbfc6f88/toolkit/components/viaduct/ViaductRequest.cpp#120-152
+ {
+ chan.requestMethod = "POST";
+ chan.setRequestHeader("content-length", "" + amount, /* aMerge = */ false);
+
+ let stream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(
+ Ci.nsIStringInputStream
+ );
+ stream.data = generateContent(amount);
+ let uchan = chan.QueryInterface(Ci.nsIUploadChannel2);
+ uchan.explicitSetUploadStream(
+ stream,
+ /* aContentType = */ "",
+ /* aContentLength = */ -1,
+ "POST",
+ /* aStreamHasHeaders = */ false
+ );
+ }
+
+ chan.notificationCallbacks = listener;
+ await chanPromise(chan, listener);
+});
diff --git a/netwerk/test/unit/test_http3_large_post_telemetry.js b/netwerk/test/unit/test_http3_large_post_telemetry.js
new file mode 100644
index 0000000000..33ad4b7d21
--- /dev/null
+++ b/netwerk/test/unit/test_http3_large_post_telemetry.js
@@ -0,0 +1,151 @@
+/* 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/. */
+
+const { TelemetryTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TelemetryTestUtils.sys.mjs"
+);
+
+let indexes_10_100 = [
+ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 16, 18, 20, 22, 24, 27, 30,
+ 33, 37, 41, 46, 51, 57, 63, 70, 78, 87, 97, 108, 120, 133, 148, 165, 184, 205,
+ 228, 254, 282, 314, 349, 388, 431, 479, 533, 593, 659, 733, 815, 906, 1008,
+ 1121, 1247, 1387, 1542, 1715, 1907, 2121, 2359, 2623, 2917, 3244, 3607, 4011,
+ 4460, 4960, 5516, 6134, 6821, 7585, 8435, 9380, 10431, 11600, 12900, 14345,
+ 15952, 17739, 19727, 21937, 24395, 27129, 30169, 33549, 37308, 41488, 46137,
+ 51307, 57056, 63449, 70559, 78465, 87257, 97035, 107908, 120000,
+];
+
+let indexes_gt_100 = [
+ 0, 30000, 30643, 31300, 31971, 32657, 33357, 34072, 34803, 35549, 36311,
+ 37090, 37885, 38697, 39527, 40375, 41241, 42125, 43028, 43951, 44894, 45857,
+ 46840, 47845, 48871, 49919, 50990, 52084, 53201, 54342, 55507, 56697, 57913,
+ 59155, 60424, 61720, 63044, 64396, 65777, 67188, 68629, 70101, 71604, 73140,
+ 74709, 76311, 77948, 79620, 81327, 83071, 84853, 86673, 88532, 90431, 92370,
+ 94351, 96374, 98441, 100552, 102708, 104911, 107161, 109459, 111806, 114204,
+ 116653, 119155, 121710, 124320, 126986, 129709, 132491, 135332, 138234,
+ 141199, 144227, 147320, 150479, 153706, 157002, 160369, 163808, 167321,
+ 170909, 174574, 178318, 182142, 186048, 190038, 194114, 198277, 202529,
+ 206872, 211309, 215841, 220470, 225198, 230028, 234961, 240000,
+];
+
+registerCleanupFunction(async () => {
+ http3_clear_prefs();
+ Services.prefs.clearUserPref(
+ "toolkit.telemetry.testing.overrideProductsCheck"
+ );
+});
+
+add_task(async function setup() {
+ // Enable the collection (during test) for all products so even products
+ // that don't collect the data will be able to run the test without failure.
+ Services.prefs.setBoolPref(
+ "toolkit.telemetry.testing.overrideProductsCheck",
+ true
+ );
+
+ await http3_setup_tests("h3-29");
+});
+
+let Http3Listener = function () {};
+
+Http3Listener.prototype = {
+ onStartRequest: function testOnStartRequest(request) {
+ 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) {
+ let httpVersion = "";
+ try {
+ httpVersion = request.protocolVersion;
+ } catch (e) {}
+ Assert.equal(httpVersion, "h3-29");
+ 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;
+}
+
+async function test_large_post(amount, hist_name, key, indexes) {
+ let hist = TelemetryTestUtils.getAndClearKeyedHistogram(hist_name);
+
+ let listener = new Http3Listener();
+ listener.amount = amount;
+ let chan = makeChan("https://foo.example.com/post", amount);
+ let tchan = chan.QueryInterface(Ci.nsITimedChannel);
+ tchan.timingEnabled = true;
+ await chanPromise(chan, listener);
+
+ let time = (tchan.responseStartTime - tchan.requestStartTime) / 1000;
+ let i = 0;
+ while (i < indexes.length && time > indexes[i + 1]) {
+ i += 1;
+ }
+ TelemetryTestUtils.assertKeyedHistogramValue(hist, key, indexes[i], 1);
+}
+
+add_task(async function test_11M() {
+ await test_large_post(
+ 11 * (1 << 20),
+ "HTTP3_UPLOAD_TIME_10M_100M",
+ "uses_http3_10_50",
+ indexes_10_100
+ );
+});
+
+add_task(async function test_51M() {
+ await test_large_post(
+ 51 * (1 << 20),
+ "HTTP3_UPLOAD_TIME_10M_100M",
+ "uses_http3_50_100",
+ indexes_10_100
+ );
+});
+
+add_task(async function test_101M() {
+ await test_large_post(
+ 101 * (1 << 20),
+ "HTTP3_UPLOAD_TIME_GT_100M",
+ "uses_http3",
+ indexes_gt_100
+ );
+});
diff --git a/netwerk/test/unit/test_http3_perf.js b/netwerk/test/unit/test_http3_perf.js
new file mode 100644
index 0000000000..0a813cdf19
--- /dev/null
+++ b/netwerk/test/unit/test_http3_perf.js
@@ -0,0 +1,262 @@
+"use strict";
+
+// This test is run as part of the perf tests which require the metadata.
+/* exported perfMetadata */
+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++;
+ }
+}
+
+// eslint-disable-next-line no-unused-vars
+function run_test() {
+ let h2Port = Services.env.get("MOZHTTP2_PORT");
+ Assert.notEqual(h2Port, null);
+ Assert.notEqual(h2Port, "");
+ let h3Port = Services.env.get("MOZHTTP3_PORT");
+ Assert.notEqual(h3Port, null);
+ Assert.notEqual(h3Port, "");
+ h3AltSvc = ":" + h3Port;
+
+ h3Route = "foo.example.com:" + h3Port;
+ do_get_profile();
+ prefs = Services.prefs;
+
+ prefs.setBoolPref("network.http.http3.enable", 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-29");
+ 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-29");
+ 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-29=: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.enable");
+ 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_prio_disabled.js b/netwerk/test/unit/test_http3_prio_disabled.js
new file mode 100644
index 0000000000..b73ca98709
--- /dev/null
+++ b/netwerk/test/unit/test_http3_prio_disabled.js
@@ -0,0 +1,106 @@
+/* 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 file can be run directly as a part of parent/main process
+// or indirectly from the wrapper test file as a part of child/content process
+
+// need to get access to helper functions/structures
+// load ensures
+// * testing environment is available (ie Assert.ok())
+/*global inChildProcess, test_flag_priority */
+load("../unit/test_http3_prio_helpers.js");
+
+// direct call to this test file should cleanup after itself
+// otherwise the wrapper will handle
+if (!inChildProcess()) {
+ registerCleanupFunction(async () => {
+ Services.prefs.clearUserPref("network.http.http3.priority");
+ http3_clear_prefs();
+ });
+}
+
+// setup once, before tests
+add_task(async function setup() {
+ // wrapper handles when testing as content process for pref change
+ if (!inChildProcess()) {
+ await http3_setup_tests("h3-29");
+ }
+});
+
+// tests various flags when priority has been disabled on variable incremental
+// this function should only be called the preferences priority disabled
+async function test_http3_prio_disabled(incremental) {
+ await test_flag_priority("disabled (none)", null, null, null, null); // default-test
+ await test_flag_priority(
+ "disabled (urgent_start)",
+ Ci.nsIClassOfService.UrgentStart,
+ null,
+ incremental,
+ null
+ );
+ await test_flag_priority(
+ "disabled (leader)",
+ Ci.nsIClassOfService.Leader,
+ null,
+ incremental,
+ null
+ );
+ await test_flag_priority(
+ "disabled (unblocked)",
+ Ci.nsIClassOfService.Unblocked,
+ null,
+ incremental,
+ null
+ );
+ await test_flag_priority(
+ "disabled (follower)",
+ Ci.nsIClassOfService.Follower,
+ null,
+ incremental,
+ null
+ );
+ await test_flag_priority(
+ "disabled (speculative)",
+ Ci.nsIClassOfService.Speculative,
+ null,
+ incremental,
+ null
+ );
+ await test_flag_priority(
+ "disabled (background)",
+ Ci.nsIClassOfService.Background,
+ null,
+ incremental,
+ null
+ );
+ await test_flag_priority(
+ "disabled (background)",
+ Ci.nsIClassOfService.Tail,
+ null,
+ incremental,
+ null
+ );
+}
+
+// run tests after setup
+
+// test that various urgency flags and incremental=true don't propogate to header
+// when priority setting is disabled
+add_task(async function test_http3_prio_disabled_inc_true() {
+ // wrapper handles when testing as content process for pref change
+ if (!inChildProcess()) {
+ Services.prefs.setBoolPref("network.http.http3.priority", false);
+ }
+ await test_http3_prio_disabled(true);
+});
+
+// test that various urgency flags and incremental=false don't propogate to header
+// when priority setting is disabled
+add_task(async function test_http3_prio_disabled_inc_false() {
+ // wrapper handles when testing as content process for pref change
+ if (!inChildProcess()) {
+ Services.prefs.setBoolPref("network.http.http3.priority", false);
+ }
+ await test_http3_prio_disabled(false);
+});
diff --git a/netwerk/test/unit/test_http3_prio_enabled.js b/netwerk/test/unit/test_http3_prio_enabled.js
new file mode 100644
index 0000000000..6dd30c590a
--- /dev/null
+++ b/netwerk/test/unit/test_http3_prio_enabled.js
@@ -0,0 +1,108 @@
+/* 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 file can be run directly as a part of parent/main process
+// or indirectly from the wrapper test file as a part of child/content process
+
+// need to get access to helper functions/structures
+// load ensures
+// * testing environment is available (ie Assert.ok())
+/*global inChildProcess, test_flag_priority */
+load("../unit/test_http3_prio_helpers.js");
+
+// direct call to this test file should cleanup after itself
+// otherwise the wrapper will handle
+if (!inChildProcess()) {
+ registerCleanupFunction(async () => {
+ Services.prefs.clearUserPref("network.http.http3.priority");
+ http3_clear_prefs();
+ });
+}
+
+// setup once, before tests
+add_task(async function setup() {
+ // wrapper handles when testing as content process for pref change
+ if (!inChildProcess()) {
+ await http3_setup_tests("h3-29");
+ }
+});
+
+// tests various flags when priority has been enabled on variable incremental
+// this function should only be called the preferences priority disabled
+async function test_http3_prio_enabled(incremental) {
+ await test_flag_priority("enabled (none)", null, "u=4", null, false); // default-test
+ await test_flag_priority(
+ "enabled (urgent_start)",
+ Ci.nsIClassOfService.UrgentStart,
+ "u=1",
+ incremental,
+ incremental
+ );
+ await test_flag_priority(
+ "enabled (leader)",
+ Ci.nsIClassOfService.Leader,
+ "u=2",
+ incremental,
+ incremental
+ );
+
+ // if priority-urgency and incremental are both default values
+ // then we shouldn't expect to see the priority header at all
+ // hence when:
+ // incremental=true -> we expect incremental
+ // incremental=false -> we expect null
+ await test_flag_priority(
+ "enabled (unblocked)",
+ Ci.nsIClassOfService.Unblocked,
+ null,
+ incremental,
+ incremental ? incremental : null
+ );
+
+ await test_flag_priority(
+ "enabled (follower)",
+ Ci.nsIClassOfService.Follower,
+ "u=4",
+ incremental,
+ incremental
+ );
+ await test_flag_priority(
+ "enabled (speculative)",
+ Ci.nsIClassOfService.Speculative,
+ "u=6",
+ incremental,
+ incremental
+ );
+ await test_flag_priority(
+ "enabled (background)",
+ Ci.nsIClassOfService.Background,
+ "u=6",
+ incremental,
+ incremental
+ );
+ await test_flag_priority(
+ "enabled (background)",
+ Ci.nsIClassOfService.Tail,
+ "u=6",
+ incremental,
+ incremental
+ );
+}
+
+// with priority enabled: test urgency flags with both incremental enabled and disabled
+add_task(async function test_http3_prio_enabled_incremental_true() {
+ // wrapper handles when testing as content process for pref change
+ if (!inChildProcess()) {
+ Services.prefs.setBoolPref("network.http.http3.priority", true);
+ }
+ await test_http3_prio_enabled(true);
+});
+
+add_task(async function test_http3_prio_enabled_incremental_false() {
+ // wrapper handles when testing as content process for pref change
+ if (!inChildProcess()) {
+ Services.prefs.setBoolPref("network.http.http3.priority", true);
+ }
+ await test_http3_prio_enabled(false);
+});
diff --git a/netwerk/test/unit/test_http3_prio_helpers.js b/netwerk/test/unit/test_http3_prio_helpers.js
new file mode 100644
index 0000000000..c1f6d06bcb
--- /dev/null
+++ b/netwerk/test/unit/test_http3_prio_helpers.js
@@ -0,0 +1,121 @@
+/* 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/. */
+
+// uses head_http3.js, which uses http2-ca.pem
+"use strict";
+
+/* exported inChildProcess, test_flag_priority */
+function inChildProcess() {
+ return Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT;
+}
+
+let Http3Listener = function (
+ closure,
+ expected_priority,
+ expected_incremental,
+ context
+) {
+ this._closure = closure;
+ this._expected_priority = expected_priority;
+ this._expected_incremental = expected_incremental;
+ this._context = context;
+};
+
+// string -> [string, bool]
+// "u=3,i" -> ["u=3", true]
+function parse_priority_response_header(priority) {
+ const priority_array = priority.split(",");
+
+ // parse for urgency string
+ const urgency = priority_array.find(element => element.includes("u="));
+
+ // parse for incremental bool
+ const incremental = !!priority_array.find(element => element == "i");
+
+ return [urgency ? urgency : null, incremental];
+}
+
+Http3Listener.prototype = {
+ resumed: false,
+
+ onStartRequest: function testOnStartRequest(request) {
+ Assert.equal(request.status, Cr.NS_OK);
+ Assert.equal(request.responseStatus, 200);
+
+ let secinfo = request.securityInfo;
+ Assert.equal(secinfo.resumed, this.resumed);
+ Assert.ok(secinfo.serverCert != null);
+
+ // check priority urgency and incremental from response header
+ let priority_urgency = null;
+ let incremental = null;
+ try {
+ const prh = request.getResponseHeader("priority-mirror");
+ [priority_urgency, incremental] = parse_priority_response_header(prh);
+ } catch (e) {
+ console.log("Failed to get priority-mirror from response header");
+ }
+ Assert.equal(priority_urgency, this._expected_priority, this._context);
+ Assert.equal(incremental, this._expected_incremental, this._context);
+ },
+
+ 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-29");
+
+ try {
+ this._closure();
+ } catch (ex) {
+ do_throw("Error in closure function: " + ex);
+ }
+ },
+};
+
+function make_channel(url) {
+ var request = NetUtil.newChannel({
+ uri: url,
+ loadUsingSystemPrincipal: true,
+ });
+ request.QueryInterface(Ci.nsIHttpChannel);
+ return request;
+}
+
+async function test_flag_priority(
+ context,
+ flag,
+ expected_priority,
+ incremental,
+ expected_incremental
+) {
+ var chan = make_channel("https://foo.example.com/priority_mirror");
+ var cos = chan.QueryInterface(Ci.nsIClassOfService);
+
+ // configure the channel with flags
+ if (flag != null) {
+ cos.addClassFlags(flag);
+ }
+
+ // configure the channel with incremental
+ if (incremental != null) {
+ cos.incremental = incremental;
+ }
+
+ await new Promise(resolve =>
+ chan.asyncOpen(
+ new Http3Listener(
+ resolve,
+ expected_priority,
+ expected_incremental,
+ context
+ )
+ )
+ );
+}
diff --git a/netwerk/test/unit/test_http3_server.js b/netwerk/test/unit/test_http3_server.js
new file mode 100644
index 0000000000..3daf9cfa3c
--- /dev/null
+++ b/netwerk/test/unit/test_http3_server.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";
+
+let h2Port;
+let h3Port;
+let trrServer;
+
+const certOverrideService = Cc[
+ "@mozilla.org/security/certoverride;1"
+].getService(Ci.nsICertOverrideService);
+const { TestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TestUtils.sys.mjs"
+);
+
+add_setup(async function setup() {
+ h2Port = Services.env.get("MOZHTTP2_PORT");
+ Assert.notEqual(h2Port, null);
+ Assert.notEqual(h2Port, "");
+
+ h3Port = Services.env.get("MOZHTTP3_PORT_PROXY");
+ Assert.notEqual(h3Port, null);
+ Assert.notEqual(h3Port, "");
+
+ trr_test_setup();
+
+ Services.prefs.setBoolPref("network.dns.upgrade_with_https_rr", true);
+ Services.prefs.setBoolPref("network.dns.use_https_rr_as_altsvc", true);
+
+ registerCleanupFunction(async () => {
+ trr_clear_prefs();
+ Services.prefs.clearUserPref("network.dns.upgrade_with_https_rr");
+ Services.prefs.clearUserPref("network.dns.use_https_rr_as_altsvc");
+ Services.prefs.clearUserPref("network.dns.disablePrefetch");
+ await trrServer.stop();
+ });
+
+ if (mozinfo.socketprocess_networking) {
+ Services.dns;
+ await TestUtils.waitForCondition(() => Services.io.socketProcessLaunched);
+ }
+});
+
+function makeChan(url) {
+ let chan = NetUtil.newChannel({
+ uri: url,
+ loadUsingSystemPrincipal: true,
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT,
+ }).QueryInterface(Ci.nsIHttpChannel);
+ return chan;
+}
+
+function channelOpenPromise(chan, flags) {
+ return new Promise(resolve => {
+ function finish(req, buffer) {
+ certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData(
+ false
+ );
+ resolve([req, buffer]);
+ }
+ let internal = chan.QueryInterface(Ci.nsIHttpChannelInternal);
+ internal.setWaitForHTTPSSVCRecord();
+ certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData(
+ true
+ );
+ chan.asyncOpen(new ChannelListener(finish, null, flags));
+ });
+}
+
+// Use NodeHTTPServer to create an HTTP server and test if the Http/3 server
+// can act as a reverse proxy.
+add_task(async function testHttp3ServerAsReverseProxy() {
+ trrServer = new TRRServer();
+ await trrServer.start();
+ Services.dns.clearCache(true);
+ 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.h3_example.com", "HTTPS", {
+ answers: [
+ {
+ name: "test.h3_example.com",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 1,
+ name: "test.h3_example.com",
+ values: [
+ { key: "alpn", value: "h3-29" },
+ { key: "port", value: h3Port },
+ ],
+ },
+ },
+ ],
+ });
+
+ await trrServer.registerDoHAnswers("test.h3_example.com", "A", {
+ answers: [
+ {
+ name: "test.h3_example.com",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "127.0.0.1",
+ },
+ ],
+ });
+
+ await new TRRDNSListener("test.h3_example.com", {
+ type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC,
+ });
+
+ let server = new NodeHTTPServer();
+ await server.start();
+ registerCleanupFunction(async () => {
+ await server.stop();
+ });
+
+ await server.registerPathHandler("/test", (req, resp) => {
+ if (req.method === "GET") {
+ resp.writeHead(200);
+ resp.end("got GET request");
+ } else if (req.method === "POST") {
+ let received = "";
+ req.on("data", function receivePostData(chunk) {
+ received += chunk.toString();
+ });
+ req.on("end", function finishPost() {
+ resp.writeHead(200);
+ resp.end(received);
+ });
+ }
+ });
+
+ // Tell the Http/3 server which port to forward requests.
+ let chan = makeChan(`https://test.h3_example.com/port?${server.port()}`);
+ await channelOpenPromise(chan, CL_ALLOW_UNKNOWN_CL);
+
+ // Test GET method.
+ chan = makeChan(`https://test.h3_example.com/test`);
+ let [req, buf] = await channelOpenPromise(chan, CL_ALLOW_UNKNOWN_CL);
+ Assert.equal(req.protocolVersion, "h3-29");
+ Assert.equal(buf, "got GET request");
+
+ var stream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(
+ Ci.nsIStringInputStream
+ );
+ stream.data = "b".repeat(500);
+
+ // Test POST method.
+ chan = makeChan(`https://test.h3_example.com/test`);
+ chan
+ .QueryInterface(Ci.nsIUploadChannel)
+ .setUploadStream(stream, "text/plain", stream.available());
+ chan.requestMethod = "POST";
+
+ [req, buf] = await channelOpenPromise(chan, CL_ALLOW_UNKNOWN_CL);
+ Assert.equal(req.protocolVersion, "h3-29");
+ Assert.equal(buf, stream.data);
+
+ await trrServer.stop();
+});
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..b2b5518974
--- /dev/null
+++ b/netwerk/test/unit/test_http3_server_not_existing.js
@@ -0,0 +1,109 @@
+/* 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.enable");
+ 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 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 h2Port = Services.env.get("MOZHTTP2_PORT");
+ Assert.notEqual(h2Port, null);
+
+ Services.prefs.setBoolPref("network.http.http3.enable", 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-29=: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..0bbb183aab
--- /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("h3-29");
+});
+
+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-29");
+ 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_http3_version1.js b/netwerk/test/unit/test_http3_version1.js
new file mode 100644
index 0000000000..65f4eef906
--- /dev/null
+++ b/netwerk/test/unit/test_http3_version1.js
@@ -0,0 +1,93 @@
+/* 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";
+
+registerCleanupFunction(async () => {
+ http3_clear_prefs();
+});
+
+let httpsUri;
+
+add_task(async function pre_setup() {
+ let h2Port = Services.env.get("MOZHTTP2_PORT");
+ Assert.notEqual(h2Port, null);
+ Assert.notEqual(h2Port, "");
+ httpsUri = "https://foo.example.com:" + h2Port + "/";
+ Services.prefs.setBoolPref("network.http.http3.support_version1", true);
+});
+
+add_task(async function setup() {
+ await http3_setup_tests("h3");
+});
+
+function chanPromise(chan, listener) {
+ return new Promise(resolve => {
+ function finish() {
+ resolve();
+ }
+ listener.finish = finish;
+ chan.asyncOpen(listener);
+ });
+}
+
+function makeH2Chan() {
+ let chan = NetUtil.newChannel({
+ uri: httpsUri,
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+ chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI;
+ return chan;
+}
+
+let Http3Listener = function () {};
+
+Http3Listener.prototype = {
+ version1enabled: "",
+
+ onStartRequest: function testOnStartRequest(request) {},
+
+ onDataAvailable: function testOnDataAvailable(request, stream, off, cnt) {
+ read_stream(stream, cnt);
+ },
+
+ onStopRequest: function testOnStopRequest(request, status) {
+ let httpVersion = "";
+ try {
+ httpVersion = request.protocolVersion;
+ } catch (e) {}
+ if (this.version1enabled) {
+ Assert.equal(httpVersion, "h3");
+ } else {
+ Assert.equal(httpVersion, "h2");
+ }
+
+ this.finish();
+ },
+};
+
+add_task(async function test_version1_enabled_1() {
+ Services.prefs.setBoolPref("network.http.http3.support_version1", true);
+ let listener = new Http3Listener();
+ listener.version1enabled = true;
+ let chan = makeH2Chan("https://foo.example.com/");
+ await chanPromise(chan, listener);
+});
+
+add_task(async function test_version1_disabled() {
+ Services.obs.notifyObservers(null, "net:cancel-all-connections");
+ Services.prefs.setBoolPref("network.http.http3.support_version1", false);
+ let listener = new Http3Listener();
+ listener.version1enabled = false;
+ let chan = makeH2Chan("https://foo.example.com/");
+ await chanPromise(chan, listener);
+});
+
+add_task(async function test_version1_enabled_2() {
+ Services.obs.notifyObservers(null, "net:cancel-all-connections");
+ Services.prefs.setBoolPref("network.http.http3.support_version1", true);
+ let listener = new Http3Listener();
+ listener.version1enabled = true;
+ let chan = makeH2Chan("https://foo.example.com/");
+ 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..c689abc2b5
--- /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.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+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 = Services.prefs;
+
+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_408_retry.js b/netwerk/test/unit/test_http_408_retry.js
new file mode 100644
index 0000000000..16392ab4bf
--- /dev/null
+++ b/netwerk/test/unit/test_http_408_retry.js
@@ -0,0 +1,92 @@
+/* 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";
+
+async function loadURL(uri, flags) {
+ let chan = NetUtil.newChannel({
+ uri,
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+ chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI;
+
+ return new Promise(resolve => {
+ chan.asyncOpen(
+ new ChannelListener((req, buff) => resolve({ req, buff }), null, flags)
+ );
+ });
+}
+
+add_task(async function test() {
+ let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(
+ Ci.nsIX509CertDB
+ );
+ addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u");
+
+ async function check408retry(server) {
+ info(`Testing ${server.constructor.name}`);
+ await server.execute(`global.server_name = "${server.constructor.name}";`);
+ if (
+ server.constructor.name == "NodeHTTPServer" ||
+ server.constructor.name == "NodeHTTPSServer"
+ ) {
+ await server.registerPathHandler("/test", (req, resp) => {
+ let oldSock = global.socket;
+ global.socket = resp.socket;
+ if (global.socket == oldSock) {
+ // This function is handled within the httpserver where setTimeout is
+ // available.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout, no-undef
+ setTimeout(
+ arg => {
+ arg.writeHead(408);
+ arg.end("stuff");
+ },
+ 1100,
+ resp
+ );
+ return;
+ }
+ resp.writeHead(200);
+ resp.end(global.server_name);
+ });
+ } else {
+ await server.registerPathHandler("/test", (req, resp) => {
+ global.socket = resp.socket;
+ if (!global.sent408) {
+ global.sent408 = true;
+ resp.writeHead(408);
+ resp.end("stuff");
+ return;
+ }
+ resp.writeHead(200);
+ resp.end(global.server_name);
+ });
+ }
+
+ async function load() {
+ let { req, buff } = await loadURL(
+ `${server.origin()}/test`,
+ CL_ALLOW_UNKNOWN_CL
+ );
+ equal(req.status, Cr.NS_OK);
+ equal(req.QueryInterface(Ci.nsIHttpChannel).responseStatus, 200);
+ equal(buff, server.constructor.name);
+ equal(
+ req.QueryInterface(Ci.nsIHttpChannel).protocolVersion,
+ server.constructor.name == "NodeHTTP2Server" ? "h2" : "http/1.1"
+ );
+ }
+
+ info("first load");
+ await load();
+ info("second load");
+ await load();
+ }
+
+ await with_node_servers(
+ [NodeHTTPServer, NodeHTTPSServer, NodeHTTP2Server],
+ check408retry
+ );
+});
diff --git a/netwerk/test/unit/test_http_headers.js b/netwerk/test/unit/test_http_headers.js
new file mode 100644
index 0000000000..b43cebea1f
--- /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 (let i = 0; i < 100; ++i) {
+ chan.setRequestHeader("foopy" + i, i, false);
+ }
+
+ for (let 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_server_timing.js b/netwerk/test/unit/test_http_server_timing.js
new file mode 100644
index 0000000000..c9d46797f3
--- /dev/null
+++ b/netwerk/test/unit/test_http_server_timing.js
@@ -0,0 +1,97 @@
+/* -*- 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/. */
+
+/* eslint-env node */
+
+"use strict";
+
+const responseServerTiming = [
+ { metric: "metric", duration: "123.4", description: "description" },
+ { metric: "metric2", duration: "456.78", description: "description1" },
+];
+const trailerServerTiming = [
+ { metric: "metric3", duration: "789.11", description: "description2" },
+ { metric: "metric4", duration: "1112.13", description: "description3" },
+];
+
+let port;
+
+let server;
+add_task(async function setup() {
+ server = new NodeHTTPServer();
+ await server.start();
+ registerCleanupFunction(async () => {
+ await server.stop();
+ });
+ server.registerPathHandler("/", (req, res) => {
+ res.setHeader("Content-Type", "text/plain");
+ res.setHeader("Content-Length", "12");
+ res.setHeader("Transfer-Encoding", "chunked");
+ res.setHeader("Trailer", "Server-Timing");
+ res.setHeader(
+ "Server-Timing",
+ "metric; dur=123.4; desc=description, metric2; dur=456.78; desc=description1"
+ );
+ res.write("data reached");
+ res.addTrailers({
+ "Server-Timing":
+ "metric3; dur=789.11; desc=description2, metric4; dur=1112.13; desc=description3",
+ });
+ res.end();
+ });
+ port = server.port();
+});
+
+// Test that secure origins can use server-timing, even with plain http
+add_task(async function test_localhost_origin() {
+ let chan = NetUtil.newChannel({
+ uri: `http://localhost:${port}/`,
+ loadUsingSystemPrincipal: true,
+ });
+ await new Promise(resolve => {
+ chan.asyncOpen(
+ new ChannelListener((request, buffer) => {
+ let channel = request.QueryInterface(Ci.nsITimedChannel);
+ let headers = channel.serverTiming.QueryInterface(Ci.nsIArray);
+ ok(headers.length);
+
+ let expectedResult = responseServerTiming.concat(trailerServerTiming);
+ Assert.equal(headers.length, expectedResult.length);
+
+ for (let 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));
+ }
+ resolve();
+ }, null)
+ );
+ });
+});
+
+// Test that insecure origins can't use server timing.
+add_task(async function test_http_non_localhost() {
+ Services.prefs.setBoolPref("network.dns.native-is-localhost", true);
+ registerCleanupFunction(async () => {
+ Services.prefs.clearUserPref("network.dns.native-is-localhost");
+ });
+
+ let chan = NetUtil.newChannel({
+ uri: `http://example.org:${port}/`,
+ loadUsingSystemPrincipal: true,
+ });
+ await new Promise(resolve => {
+ chan.asyncOpen(
+ new ChannelListener((request, buffer) => {
+ let channel = request.QueryInterface(Ci.nsITimedChannel);
+ let headers = channel.serverTiming.QueryInterface(Ci.nsIArray);
+ Assert.equal(headers.length, 0);
+ resolve();
+ }, null)
+ );
+ });
+});
diff --git a/netwerk/test/unit/test_http_sfv.js b/netwerk/test/unit/test_http_sfv.js
new file mode 100644
index 0000000000..4b7ec9893c
--- /dev/null
+++ b/netwerk/test/unit/test_http_sfv.js
@@ -0,0 +1,597 @@
+"use strict";
+
+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
+ function params() {
+ 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));
+ return inner_list_params;
+ }
+
+ function changeMembers() {
+ // set one of list members to inner list and check it's serialized as expected
+ let members = list_field.members;
+ members[1] = gService.newInnerList(
+ [
+ gService.newItem(
+ gService.newDecimal(-1865.75653),
+ gService.newParameters()
+ ),
+ gService.newItem(gService.newToken("token"), gService.newParameters()),
+ gService.newItem(
+ gService.newString(`no"yes`),
+ gService.newParameters()
+ ),
+ ],
+ params()
+ );
+ return members;
+ }
+
+ list_field.members = changeMembers();
+
+ 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..04958ffd2b
--- /dev/null
+++ b/netwerk/test/unit/test_httpcancel.js
@@ -0,0 +1,261 @@
+// 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.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+const reason = "testing";
+
+function inChildProcess() {
+ return Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT;
+}
+
+var ios = Services.io;
+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.cancelWithReason(Cr.NS_BINDING_ABORTED, reason);
+
+ // 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);
+ // We didn't sync the reason to child process.
+ if (!inChildProcess()) {
+ Assert.equal(request.canceledReason, reason);
+ }
+
+ // 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
+ Services.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 str2 = "b".repeat(128 * 1024);
+ response.write(str2, str2.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_https_rr_ech_prefs.js b/netwerk/test/unit/test_https_rr_ech_prefs.js
new file mode 100644
index 0000000000..f70c672dde
--- /dev/null
+++ b/netwerk/test/unit/test_https_rr_ech_prefs.js
@@ -0,0 +1,535 @@
+/* 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 trrServer;
+
+function setup() {
+ trr_test_setup();
+
+ 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);
+}
+
+setup();
+registerCleanupFunction(async () => {
+ trr_clear_prefs();
+ Services.prefs.clearUserPref("network.dns.upgrade_with_https_rr");
+ Services.prefs.clearUserPref("network.dns.use_https_rr_as_altsvc");
+ Services.prefs.clearUserPref("network.dns.echconfig.enabled");
+ Services.prefs.clearUserPref("network.dns.http3_echconfig.enabled");
+ Services.prefs.clearUserPref("network.http.http3.enable");
+ Services.prefs.clearUserPref("network.http.http2.enabled");
+ if (trrServer) {
+ await trrServer.stop();
+ }
+});
+
+function checkResult(inRecord, noHttp2, noHttp3, result) {
+ if (!result) {
+ Assert.throws(
+ () => {
+ inRecord
+ .QueryInterface(Ci.nsIDNSHTTPSSVCRecord)
+ .GetServiceModeRecord(noHttp2, noHttp3);
+ },
+ /NS_ERROR_NOT_AVAILABLE/,
+ "Should get an error"
+ );
+ return;
+ }
+
+ let record = inRecord
+ .QueryInterface(Ci.nsIDNSHTTPSSVCRecord)
+ .GetServiceModeRecord(noHttp2, noHttp3);
+ Assert.equal(record.priority, result.expectedPriority);
+ Assert.equal(record.name, result.expectedName);
+ Assert.equal(record.selectedAlpn, result.expectedAlpn);
+}
+
+// Test configuration:
+// There are two records: one has a echConfig and the other doesn't.
+// We want to test if the record with echConfig is preferred.
+add_task(async function testEchConfigEnabled() {
+ 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`
+ );
+ Services.prefs.setBoolPref("network.dns.echconfig.enabled", false);
+
+ await trrServer.registerDoHAnswers("test.bar.com", "HTTPS", {
+ answers: [
+ {
+ name: "test.bar.com",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 1,
+ name: "test.bar_1.com",
+ values: [{ key: "alpn", value: ["h3-29"] }],
+ },
+ },
+ {
+ name: "test.bar.com",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 2,
+ name: "test.bar_2.com",
+ values: [
+ { key: "alpn", value: ["h2"] },
+ { key: "echconfig", value: "456..." },
+ ],
+ },
+ },
+ ],
+ });
+
+ let { inRecord } = await new TRRDNSListener("test.bar.com", {
+ type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC,
+ });
+
+ checkResult(inRecord, false, false, {
+ expectedPriority: 1,
+ expectedName: "test.bar_1.com",
+ expectedAlpn: "h3-29",
+ });
+ checkResult(inRecord, false, true, {
+ expectedPriority: 2,
+ expectedName: "test.bar_2.com",
+ expectedAlpn: "h2",
+ });
+ checkResult(inRecord, true, false, {
+ expectedPriority: 1,
+ expectedName: "test.bar_1.com",
+ expectedAlpn: "h3-29",
+ });
+ checkResult(inRecord, true, true);
+
+ Services.prefs.setBoolPref("network.dns.echconfig.enabled", true);
+ Services.dns.clearCache(true);
+
+ ({ inRecord } = await new TRRDNSListener("test.bar.com", {
+ type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC,
+ }));
+
+ checkResult(inRecord, false, false, {
+ expectedPriority: 2,
+ expectedName: "test.bar_2.com",
+ expectedAlpn: "h2",
+ });
+ checkResult(inRecord, false, true, {
+ expectedPriority: 2,
+ expectedName: "test.bar_2.com",
+ expectedAlpn: "h2",
+ });
+ checkResult(inRecord, true, false, {
+ expectedPriority: 1,
+ expectedName: "test.bar_1.com",
+ expectedAlpn: "h3-29",
+ });
+ checkResult(inRecord, true, true);
+
+ await trrServer.stop();
+ trrServer = null;
+});
+
+// Test configuration:
+// There are two records: both have echConfigs, and only one supports h3.
+// This test is about testing which record should we get when
+// network.dns.http3_echconfig.enabled is true and false.
+// When network.dns.http3_echconfig.enabled is false, we should try to
+// connect with h2 and echConfig.
+add_task(async function testTwoRecordsHaveEchConfig() {
+ Services.dns.clearCache(true);
+
+ trrServer = new TRRServer();
+ await trrServer.start();
+
+ Services.prefs.setBoolPref("network.dns.echconfig.enabled", true);
+ Services.prefs.setBoolPref("network.dns.http3_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.foo.com", "HTTPS", {
+ answers: [
+ {
+ name: "test.foo.com",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 1,
+ name: "test.foo_h3.com",
+ values: [
+ { key: "alpn", value: ["h3"] },
+ { key: "echconfig", value: "456..." },
+ ],
+ },
+ },
+ {
+ name: "test.foo.com",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 2,
+ name: "test.foo_h2.com",
+ values: [
+ { key: "alpn", value: ["h2"] },
+ { key: "echconfig", value: "456..." },
+ ],
+ },
+ },
+ ],
+ });
+
+ let { inRecord } = await new TRRDNSListener("test.foo.com", {
+ type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC,
+ });
+
+ checkResult(inRecord, false, false, {
+ expectedPriority: 2,
+ expectedName: "test.foo_h2.com",
+ expectedAlpn: "h2",
+ });
+ checkResult(inRecord, false, true, {
+ expectedPriority: 2,
+ expectedName: "test.foo_h2.com",
+ expectedAlpn: "h2",
+ });
+ checkResult(inRecord, true, false, {
+ expectedPriority: 1,
+ expectedName: "test.foo_h3.com",
+ expectedAlpn: "h3",
+ });
+ checkResult(inRecord, true, true);
+
+ Services.prefs.setBoolPref("network.dns.http3_echconfig.enabled", true);
+ Services.dns.clearCache(true);
+ ({ inRecord } = await new TRRDNSListener("test.foo.com", {
+ type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC,
+ }));
+
+ checkResult(inRecord, false, false, {
+ expectedPriority: 1,
+ expectedName: "test.foo_h3.com",
+ expectedAlpn: "h3",
+ });
+ checkResult(inRecord, false, true, {
+ expectedPriority: 2,
+ expectedName: "test.foo_h2.com",
+ expectedAlpn: "h2",
+ });
+ checkResult(inRecord, true, false, {
+ expectedPriority: 1,
+ expectedName: "test.foo_h3.com",
+ expectedAlpn: "h3",
+ });
+ checkResult(inRecord, true, true);
+
+ await trrServer.stop();
+ trrServer = null;
+});
+
+// Test configuration:
+// There are two records: both have echConfigs, and one supports h3 and h2.
+// When network.dns.http3_echconfig.enabled is false, we should use the record
+// that supports h3 and h2 (the alpn is h2).
+add_task(async function testTwoRecordsHaveEchConfig1() {
+ Services.dns.clearCache(true);
+
+ trrServer = new TRRServer();
+ await trrServer.start();
+
+ Services.prefs.setBoolPref("network.dns.echconfig.enabled", true);
+ Services.prefs.setBoolPref("network.dns.http3_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.foo.com", "HTTPS", {
+ answers: [
+ {
+ name: "test.foo.com",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 1,
+ name: "test.foo_h3.com",
+ values: [
+ { key: "alpn", value: ["h3", "h2"] },
+ { key: "echconfig", value: "456..." },
+ ],
+ },
+ },
+ {
+ name: "test.foo.com",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 2,
+ name: "test.foo_h2.com",
+ values: [
+ { key: "alpn", value: ["h2", "http/1.1"] },
+ { key: "echconfig", value: "456..." },
+ ],
+ },
+ },
+ ],
+ });
+
+ let { inRecord } = await new TRRDNSListener("test.foo.com", {
+ type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC,
+ });
+
+ checkResult(inRecord, false, false, {
+ expectedPriority: 1,
+ expectedName: "test.foo_h3.com",
+ expectedAlpn: "h2",
+ });
+ checkResult(inRecord, false, true, {
+ expectedPriority: 1,
+ expectedName: "test.foo_h3.com",
+ expectedAlpn: "h2",
+ });
+ checkResult(inRecord, true, false, {
+ expectedPriority: 2,
+ expectedName: "test.foo_h2.com",
+ expectedAlpn: "http/1.1",
+ });
+ checkResult(inRecord, true, true, {
+ expectedPriority: 2,
+ expectedName: "test.foo_h2.com",
+ expectedAlpn: "http/1.1",
+ });
+
+ Services.prefs.setBoolPref("network.dns.http3_echconfig.enabled", true);
+ Services.dns.clearCache(true);
+ ({ inRecord } = await new TRRDNSListener("test.foo.com", {
+ type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC,
+ }));
+
+ checkResult(inRecord, false, false, {
+ expectedPriority: 1,
+ expectedName: "test.foo_h3.com",
+ expectedAlpn: "h3",
+ });
+ checkResult(inRecord, false, true, {
+ expectedPriority: 1,
+ expectedName: "test.foo_h3.com",
+ expectedAlpn: "h2",
+ });
+ checkResult(inRecord, true, false, {
+ expectedPriority: 1,
+ expectedName: "test.foo_h3.com",
+ expectedAlpn: "h3",
+ });
+ checkResult(inRecord, true, true, {
+ expectedPriority: 2,
+ expectedName: "test.foo_h2.com",
+ expectedAlpn: "http/1.1",
+ });
+
+ await trrServer.stop();
+ trrServer = null;
+});
+
+// Test configuration:
+// There are two records: only one support h3 and only one has echConfig.
+// This test is about never usng the record without echConfig.
+add_task(async function testOneRecordsHasEchConfig() {
+ Services.dns.clearCache(true);
+
+ trrServer = new TRRServer();
+ await trrServer.start();
+
+ Services.prefs.setBoolPref("network.dns.echconfig.enabled", true);
+ Services.prefs.setBoolPref("network.dns.http3_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.foo.com", "HTTPS", {
+ answers: [
+ {
+ name: "test.foo.com",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 1,
+ name: "test.foo_h3.com",
+ values: [
+ { key: "alpn", value: ["h3"] },
+ { key: "echconfig", value: "456..." },
+ ],
+ },
+ },
+ {
+ name: "test.foo.com",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 2,
+ name: "test.foo_h2.com",
+ values: [{ key: "alpn", value: ["h2"] }],
+ },
+ },
+ ],
+ });
+
+ let { inRecord } = await new TRRDNSListener("test.foo.com", {
+ type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC,
+ });
+
+ checkResult(inRecord, false, false, {
+ expectedPriority: 1,
+ expectedName: "test.foo_h3.com",
+ expectedAlpn: "h3",
+ });
+ checkResult(inRecord, false, true, {
+ expectedPriority: 2,
+ expectedName: "test.foo_h2.com",
+ expectedAlpn: "h2",
+ });
+ checkResult(inRecord, true, false, {
+ expectedPriority: 1,
+ expectedName: "test.foo_h3.com",
+ expectedAlpn: "h3",
+ });
+ checkResult(inRecord, true, true);
+
+ Services.prefs.setBoolPref("network.dns.http3_echconfig.enabled", true);
+ Services.dns.clearCache(true);
+ ({ inRecord } = await new TRRDNSListener("test.foo.com", {
+ type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC,
+ }));
+
+ checkResult(inRecord, false, false, {
+ expectedPriority: 1,
+ expectedName: "test.foo_h3.com",
+ expectedAlpn: "h3",
+ });
+ checkResult(inRecord, false, true, {
+ expectedPriority: 2,
+ expectedName: "test.foo_h2.com",
+ expectedAlpn: "h2",
+ });
+ checkResult(inRecord, true, false, {
+ expectedPriority: 1,
+ expectedName: "test.foo_h3.com",
+ expectedAlpn: "h3",
+ });
+ checkResult(inRecord, true, true);
+
+ await trrServer.stop();
+ trrServer = null;
+});
+
+// Test the case that "network.http.http3.enable" and
+// "network.http.http2.enabled" are true/false.
+add_task(async function testHttp3AndHttp2Pref() {
+ Services.dns.clearCache(true);
+
+ trrServer = new TRRServer();
+ await trrServer.start();
+
+ Services.prefs.setBoolPref("network.http.http3.enable", false);
+ Services.prefs.setBoolPref("network.dns.echconfig.enabled", false);
+ Services.prefs.setBoolPref("network.dns.http3_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.foo.com", "HTTPS", {
+ answers: [
+ {
+ name: "test.foo.com",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 1,
+ name: "test.foo_h3.com",
+ values: [
+ { key: "alpn", value: ["h3", "h2"] },
+ { key: "echconfig", value: "456..." },
+ ],
+ },
+ },
+ {
+ name: "test.foo.com",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 2,
+ name: "test.foo_h2.com",
+ values: [
+ { key: "alpn", value: ["h2"] },
+ { key: "echconfig", value: "456..." },
+ ],
+ },
+ },
+ ],
+ });
+
+ let { inRecord } = await new TRRDNSListener("test.foo.com", {
+ type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC,
+ });
+
+ checkResult(inRecord, false, false, {
+ expectedPriority: 1,
+ expectedName: "test.foo_h3.com",
+ expectedAlpn: "h2",
+ });
+ checkResult(inRecord, false, true, {
+ expectedPriority: 1,
+ expectedName: "test.foo_h3.com",
+ expectedAlpn: "h2",
+ });
+ checkResult(inRecord, true, false);
+ checkResult(inRecord, true, true);
+
+ Services.prefs.setBoolPref("network.http.http2.enabled", false);
+ checkResult(inRecord, false, false);
+
+ Services.prefs.setBoolPref("network.http.http3.enable", true);
+ checkResult(inRecord, false, false, {
+ expectedPriority: 1,
+ expectedName: "test.foo_h3.com",
+ expectedAlpn: "h3",
+ });
+ checkResult(inRecord, false, true);
+ checkResult(inRecord, true, false, {
+ expectedPriority: 1,
+ expectedName: "test.foo_h3.com",
+ expectedAlpn: "h3",
+ });
+ checkResult(inRecord, true, true);
+
+ await trrServer.stop();
+ trrServer = null;
+});
diff --git a/netwerk/test/unit/test_https_rr_sorted_alpn.js b/netwerk/test/unit/test_https_rr_sorted_alpn.js
new file mode 100644
index 0000000000..95284de0c3
--- /dev/null
+++ b/netwerk/test/unit/test_https_rr_sorted_alpn.js
@@ -0,0 +1,226 @@
+/* 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 trrServer;
+
+const { TestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TestUtils.sys.mjs"
+);
+
+add_setup(async function setup() {
+ trr_test_setup();
+ registerCleanupFunction(async () => {
+ trr_clear_prefs();
+ Services.prefs.clearUserPref("network.http.http3.support_version1");
+ Services.prefs.clearUserPref("security.tls.version.max");
+ if (trrServer) {
+ await trrServer.stop();
+ }
+ });
+
+ if (mozinfo.socketprocess_networking) {
+ Services.dns; // Needed to trigger socket process.
+ await TestUtils.waitForCondition(() => Services.io.socketProcessLaunched);
+ }
+});
+
+function checkResult(inRecord, noHttp2, noHttp3, result) {
+ if (!result) {
+ Assert.throws(
+ () => {
+ inRecord
+ .QueryInterface(Ci.nsIDNSHTTPSSVCRecord)
+ .GetServiceModeRecord(noHttp2, noHttp3);
+ },
+ /NS_ERROR_NOT_AVAILABLE/,
+ "Should get an error"
+ );
+ return;
+ }
+
+ let record = inRecord
+ .QueryInterface(Ci.nsIDNSHTTPSSVCRecord)
+ .GetServiceModeRecord(noHttp2, noHttp3);
+ Assert.equal(record.priority, result.expectedPriority);
+ Assert.equal(record.name, result.expectedName);
+ Assert.equal(record.selectedAlpn, result.expectedAlpn);
+}
+
+add_task(async function testSortedAlpnH3() {
+ Services.dns.clearCache(true);
+
+ 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`
+ );
+ Services.prefs.setBoolPref("network.http.http3.support_version1", true);
+ await trrServer.registerDoHAnswers("test.alpn.com", "HTTPS", {
+ answers: [
+ {
+ name: "test.alpn.com",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 1,
+ name: "test.alpn.com",
+ values: [{ key: "alpn", value: ["h2", "http/1.1", "h3-30", "h3"] }],
+ },
+ },
+ ],
+ });
+
+ let { inRecord } = await new TRRDNSListener("test.alpn.com", {
+ type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC,
+ });
+
+ checkResult(inRecord, false, false, {
+ expectedPriority: 1,
+ expectedName: "test.alpn.com",
+ expectedAlpn: "h3",
+ });
+ checkResult(inRecord, false, true, {
+ expectedPriority: 1,
+ expectedName: "test.alpn.com",
+ expectedAlpn: "h2",
+ });
+ checkResult(inRecord, true, false, {
+ expectedPriority: 1,
+ expectedName: "test.alpn.com",
+ expectedAlpn: "h3",
+ });
+ checkResult(inRecord, true, true, {
+ expectedPriority: 1,
+ expectedName: "test.alpn.com",
+ expectedAlpn: "http/1.1",
+ });
+
+ Services.prefs.setBoolPref("network.http.http3.support_version1", false);
+ checkResult(inRecord, false, false, {
+ expectedPriority: 1,
+ expectedName: "test.alpn.com",
+ expectedAlpn: "h3-30",
+ });
+ checkResult(inRecord, false, true, {
+ expectedPriority: 1,
+ expectedName: "test.alpn.com",
+ expectedAlpn: "h2",
+ });
+ checkResult(inRecord, true, false, {
+ expectedPriority: 1,
+ expectedName: "test.alpn.com",
+ expectedAlpn: "h3-30",
+ });
+ checkResult(inRecord, true, true, {
+ expectedPriority: 1,
+ expectedName: "test.alpn.com",
+ expectedAlpn: "http/1.1",
+ });
+ Services.prefs.setBoolPref("network.http.http3.support_version1", true);
+
+ // Disable TLS1.3
+ Services.prefs.setIntPref("security.tls.version.max", 3);
+ checkResult(inRecord, false, false, {
+ expectedPriority: 1,
+ expectedName: "test.alpn.com",
+ expectedAlpn: "h2",
+ });
+ checkResult(inRecord, false, true, {
+ expectedPriority: 1,
+ expectedName: "test.alpn.com",
+ expectedAlpn: "h2",
+ });
+ checkResult(inRecord, true, false, {
+ expectedPriority: 1,
+ expectedName: "test.alpn.com",
+ expectedAlpn: "http/1.1",
+ });
+ checkResult(inRecord, true, true, {
+ expectedPriority: 1,
+ expectedName: "test.alpn.com",
+ expectedAlpn: "http/1.1",
+ });
+
+ // Enable TLS1.3
+ Services.prefs.setIntPref("security.tls.version.max", 4);
+ checkResult(inRecord, false, false, {
+ expectedPriority: 1,
+ expectedName: "test.alpn.com",
+ expectedAlpn: "h3",
+ });
+ checkResult(inRecord, false, true, {
+ expectedPriority: 1,
+ expectedName: "test.alpn.com",
+ expectedAlpn: "h2",
+ });
+ checkResult(inRecord, true, false, {
+ expectedPriority: 1,
+ expectedName: "test.alpn.com",
+ expectedAlpn: "h3",
+ });
+ checkResult(inRecord, true, true, {
+ expectedPriority: 1,
+ expectedName: "test.alpn.com",
+ expectedAlpn: "http/1.1",
+ });
+});
+
+add_task(async function testSortedAlpnH2() {
+ Services.dns.clearCache(true);
+
+ 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.alpn_2.com", "HTTPS", {
+ answers: [
+ {
+ name: "test.alpn_2.com",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 1,
+ name: "test.alpn_2.com",
+ values: [{ key: "alpn", value: ["http/1.1", "h2"] }],
+ },
+ },
+ ],
+ });
+
+ let { inRecord } = await new TRRDNSListener("test.alpn_2.com", {
+ type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC,
+ });
+
+ checkResult(inRecord, false, false, {
+ expectedPriority: 1,
+ expectedName: "test.alpn_2.com",
+ expectedAlpn: "h2",
+ });
+ checkResult(inRecord, false, true, {
+ expectedPriority: 1,
+ expectedName: "test.alpn_2.com",
+ expectedAlpn: "h2",
+ });
+ checkResult(inRecord, true, false, {
+ expectedPriority: 1,
+ expectedName: "test.alpn_2.com",
+ expectedAlpn: "http/1.1",
+ });
+ checkResult(inRecord, true, true, {
+ expectedPriority: 1,
+ expectedName: "test.alpn_2.com",
+ expectedAlpn: "http/1.1",
+ });
+
+ await trrServer.stop();
+ trrServer = null;
+});
diff --git a/netwerk/test/unit/test_httpssvc_ech_with_alpn.js b/netwerk/test/unit/test_httpssvc_ech_with_alpn.js
new file mode 100644
index 0000000000..fbd9768daf
--- /dev/null
+++ b/netwerk/test/unit/test_httpssvc_ech_with_alpn.js
@@ -0,0 +1,246 @@
+/* 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 trrServer;
+
+const certOverrideService = Cc[
+ "@mozilla.org/security/certoverride;1"
+].getService(Ci.nsICertOverrideService);
+
+add_setup(async function setup() {
+ // Allow telemetry probes which may otherwise be disabled for some
+ // applications (e.g. Thunderbird).
+ Services.prefs.setBoolPref(
+ "toolkit.telemetry.testing.overrideProductsCheck",
+ true
+ );
+
+ trr_test_setup();
+
+ 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.setBoolPref("network.dns.http3_echconfig.enabled", false);
+ Services.prefs.setIntPref("network.http.speculative-parallel-limit", 0);
+ Services.prefs.setIntPref("network.trr.mode", Ci.nsIDNSService.MODE_TRRONLY);
+
+ // Set the server to always select http/1.1
+ Services.env.set("MOZ_TLS_ECH_ALPN_FLAG", 1);
+
+ await asyncStartTLSTestServer(
+ "EncryptedClientHelloServer",
+ "../../../security/manager/ssl/tests/unit/test_encrypted_client_hello"
+ );
+});
+
+registerCleanupFunction(async () => {
+ trr_clear_prefs();
+ Services.prefs.clearUserPref("network.trr.mode");
+ Services.prefs.clearUserPref("network.trr.uri");
+ Services.prefs.clearUserPref("network.dns.upgrade_with_https_rr");
+ Services.prefs.clearUserPref("network.dns.use_https_rr_as_altsvc");
+ Services.prefs.clearUserPref("network.dns.echconfig.enabled");
+ Services.prefs.clearUserPref("network.dns.http3_echconfig.enabled");
+ Services.prefs.clearUserPref(
+ "network.dns.echconfig.fallback_to_origin_when_all_failed"
+ );
+ Services.prefs.clearUserPref("network.http.speculative-parallel-limit");
+ Services.prefs.clearUserPref("network.dns.port_prefixed_qname_https_rr");
+ Services.env.set("MOZ_TLS_ECH_ALPN_FLAG", "");
+ if (trrServer) {
+ await trrServer.stop();
+ }
+});
+
+function makeChan(url) {
+ let chan = NetUtil.newChannel({
+ uri: url,
+ loadUsingSystemPrincipal: true,
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT,
+ }).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));
+ });
+}
+
+function ActivityObserver() {}
+
+ActivityObserver.prototype = {
+ activites: [],
+ observeConnectionActivity(
+ aHost,
+ aPort,
+ aSSL,
+ aHasECH,
+ aIsHttp3,
+ aActivityType,
+ aActivitySubtype,
+ aTimestamp,
+ aExtraStringData
+ ) {
+ dump(
+ "*** Connection Activity 0x" +
+ aActivityType.toString(16) +
+ " 0x" +
+ aActivitySubtype.toString(16) +
+ " " +
+ aExtraStringData +
+ "\n"
+ );
+ this.activites.push({ host: aHost, subType: aActivitySubtype });
+ },
+};
+
+function checkHttpActivities(activites) {
+ let foundDNSAndSocket = false;
+ let foundSettingECH = false;
+ let foundConnectionCreated = false;
+ for (let activity of activites) {
+ switch (activity.subType) {
+ case Ci.nsIHttpActivityObserver.ACTIVITY_SUBTYPE_DNSANDSOCKET_CREATED:
+ case Ci.nsIHttpActivityObserver
+ .ACTIVITY_SUBTYPE_SPECULATIVE_DNSANDSOCKET_CREATED:
+ foundDNSAndSocket = true;
+ break;
+ case Ci.nsIHttpActivityDistributor.ACTIVITY_SUBTYPE_ECH_SET:
+ foundSettingECH = true;
+ break;
+ case Ci.nsIHttpActivityDistributor.ACTIVITY_SUBTYPE_CONNECTION_CREATED:
+ foundConnectionCreated = true;
+ break;
+ default:
+ break;
+ }
+ }
+
+ Assert.equal(foundDNSAndSocket, true, "Should have one DnsAndSock created");
+ Assert.equal(foundSettingECH, true, "Should have echConfig");
+ Assert.equal(
+ foundConnectionCreated,
+ true,
+ "Should have one connection created"
+ );
+}
+
+async function testWrapper(alpnAdvertisement) {
+ const ECH_CONFIG_FIXED =
+ "AEn+DQBFTQAgACCKB1Y5SfrGIyk27W82xPpzWTDs3q72c04xSurDWlb9CgAEAAEAA2QWZWNoLXB1YmxpYy5leGFtcGxlLmNvbQAA";
+ trrServer = new TRRServer();
+ await trrServer.start();
+
+ let observerService = Cc[
+ "@mozilla.org/network/http-activity-distributor;1"
+ ].getService(Ci.nsIHttpActivityDistributor);
+ let observer = new ActivityObserver();
+ observerService.addObserver(observer);
+ observerService.observeConnection = true;
+
+ 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", {
+ answers: [
+ {
+ name: "ech-private.example.com",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 1,
+ name: "ech-private.example.com",
+ values: [
+ { key: "alpn", value: alpnAdvertisement },
+ { key: "port", value: 8443 },
+ {
+ key: "echconfig",
+ value: ECH_CONFIG_FIXED,
+ needBase64Decode: true,
+ },
+ ],
+ },
+ },
+ ],
+ });
+
+ await trrServer.registerDoHAnswers("ech-private.example.com", "A", {
+ answers: [
+ {
+ name: "ech-private.example.com",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "127.0.0.1",
+ },
+ ],
+ });
+
+ await new TRRDNSListener("ech-private.example.com", {
+ type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC,
+ });
+
+ HandshakeTelemetryHelpers.resetHistograms();
+ let chan = makeChan(`https://ech-private.example.com`);
+ await channelOpenPromise(chan, CL_ALLOW_UNKNOWN_CL);
+ let securityInfo = chan.securityInfo;
+ Assert.ok(securityInfo.isAcceptedEch, "This host should have accepted ECH");
+
+ // Only check telemetry if network process is disabled.
+ if (!mozinfo.socketprocess_networking) {
+ HandshakeTelemetryHelpers.checkSuccess(["", "_ECH", "_FIRST_TRY"]);
+ HandshakeTelemetryHelpers.checkEmpty(["_CONSERVATIVE", "_ECH_GREASE"]);
+ }
+
+ await trrServer.stop();
+ observerService.removeObserver(observer);
+ observerService.observeConnection = false;
+
+ let filtered = observer.activites.filter(
+ activity => activity.host === "ech-private.example.com"
+ );
+ checkHttpActivities(filtered);
+}
+
+add_task(async function h1Advertised() {
+ await testWrapper(["http/1.1"]);
+});
+
+add_task(async function h2Advertised() {
+ await testWrapper(["h2"]);
+});
+
+add_task(async function h3Advertised() {
+ await testWrapper(["h3"]);
+});
+
+add_task(async function h1h2Advertised() {
+ await testWrapper(["http/1.1", "h2"]);
+});
+
+add_task(async function h2h3Advertised() {
+ await testWrapper(["h3", "h2"]);
+});
+
+add_task(async function unknownAdvertised() {
+ await testWrapper(["foo"]);
+});
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..837006767c
--- /dev/null
+++ b/netwerk/test/unit/test_httpssvc_https_upgrade.js
@@ -0,0 +1,352 @@
+/* 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 certOverrideService = Cc[
+ "@mozilla.org/security/certoverride;1"
+].getService(Ci.nsICertOverrideService);
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+const { TestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TestUtils.sys.mjs"
+);
+
+const ReferrerInfo = Components.Constructor(
+ "@mozilla.org/referrer-info;1",
+ "nsIReferrerInfo",
+ "init"
+);
+
+add_setup(async function setup() {
+ trr_test_setup();
+
+ let h2Port = Services.env.get("MOZHTTP2_PORT");
+ Assert.notEqual(h2Port, null);
+ Assert.notEqual(h2Port, "");
+
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ "https://foo.example.com:" + h2Port + "/httpssvc_as_altsvc"
+ );
+
+ 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.use_https_rr_for_speculative_connection",
+ true
+ );
+
+ registerCleanupFunction(async () => {
+ trr_clear_prefs();
+ Services.prefs.clearUserPref("network.dns.upgrade_with_https_rr");
+ Services.prefs.clearUserPref("network.dns.use_https_rr_as_altsvc");
+ Services.prefs.clearUserPref(
+ "network.dns.use_https_rr_for_speculative_connection"
+ );
+ Services.prefs.clearUserPref("network.dns.notifyResolution");
+ Services.prefs.clearUserPref("network.dns.disablePrefetch");
+ });
+
+ if (mozinfo.socketprocess_networking) {
+ Services.dns; // Needed to trigger socket process.
+ await TestUtils.waitForCondition(() => Services.io.socketProcessLaunched);
+ }
+
+ Services.prefs.setIntPref("network.trr.mode", Ci.nsIDNSService.MODE_TRRFIRST);
+});
+
+function makeChan(url) {
+ let chan = NetUtil.newChannel({
+ uri: url,
+ loadUsingSystemPrincipal: true,
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT,
+ }).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;
+ }
+ throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE);
+ }
+ 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() {
+ Services.dns.clearCache(true);
+ // Do DNS resolution before creating the channel, so the HTTPSSVC record will
+ // be resolved from the cache.
+ await new TRRDNSListener("test.httpssvc.com", {
+ type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC,
+ });
+
+ // 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() {
+ Services.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() {
+ Services.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();
+
+ new TRRDNSListener("foo.notexisted.com", {
+ type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC,
+ expectedSuccess: false,
+ }).then(() => channel.resume());
+ }
+ },
+ };
+
+ 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));
+});
+
+// Test the case that an HTTPS RR is available and the server returns a 307
+// for redirecting back to http.
+add_task(async function testEndlessUpgradeDowngrade() {
+ Services.dns.clearCache(true);
+
+ let httpserv = new HttpServer();
+ let content = "okok";
+ httpserv.start(-1);
+ let port = httpserv.identity.primaryPort;
+ httpserv.registerPathHandler(
+ `/redirect_to_http`,
+ function handler(metadata, response) {
+ response.setHeader("Content-Length", `${content.length}`);
+ response.bodyOutputStream.write(content, content.length);
+ }
+ );
+ httpserv.identity.setPrimary("http", "test.httpsrr.redirect.com", port);
+
+ let chan = makeChan(
+ `http://test.httpsrr.redirect.com:${port}/redirect_to_http?port=${port}`
+ );
+
+ let [, response] = await channelOpenPromise(chan);
+ Assert.equal(response, content);
+ await new Promise(resolve => httpserv.stop(resolve));
+});
+
+add_task(async function testHttpRequestBlocked() {
+ Services.dns.clearCache(true);
+
+ let dnsRequestObserver = {
+ register() {
+ this.obs = Services.obs;
+ 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") {
+ Assert.ok(false, "unreachable");
+ }
+ },
+ };
+
+ dnsRequestObserver.register();
+ Services.prefs.setBoolPref("network.dns.notifyResolution", true);
+ Services.prefs.setBoolPref("network.dns.disablePrefetch", true);
+
+ let httpserv = new HttpServer();
+ httpserv.registerPathHandler("/", function handler(metadata, response) {
+ Assert.ok(false, "unreachable");
+ });
+ httpserv.start(-1);
+ httpserv.identity.setPrimary(
+ "http",
+ "foo.blocked.com",
+ httpserv.identity.primaryPort
+ );
+
+ let chan = makeChan(
+ `http://foo.blocked.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.cancel(Cr.NS_BINDING_ABORTED);
+ }
+ },
+ };
+
+ let [request] = await channelOpenPromise(chan, CL_EXPECT_FAILURE, observer);
+ request.QueryInterface(Ci.nsIHttpChannel);
+ Assert.equal(request.status, Cr.NS_BINDING_ABORTED);
+ dnsRequestObserver.unregister();
+ await new Promise(resolve => httpserv.stop(resolve));
+});
+
+function createPrincipal(url) {
+ return Services.scriptSecurityManager.createContentPrincipal(
+ Services.io.newURI(url),
+ {}
+ );
+}
+
+// Test if the Origin header stays the same after an internal HTTPS upgrade
+// caused by HTTPS RR.
+add_task(async function testHTTPSRRUpgradeWithOriginHeader() {
+ Services.dns.clearCache(true);
+
+ const url = "http://test.httpssvc.com:80/origin_header";
+ const originURL = "http://example.com";
+ let chan = Services.io
+ .newChannelFromURIWithProxyFlags(
+ Services.io.newURI(url),
+ null,
+ Ci.nsIProtocolProxyService.RESOLVE_ALWAYS_TUNNEL,
+ null,
+ createPrincipal(originURL),
+ createPrincipal(url),
+ Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ Ci.nsIContentPolicy.TYPE_DOCUMENT
+ )
+ .QueryInterface(Ci.nsIHttpChannel);
+ chan.referrerInfo = new ReferrerInfo(
+ Ci.nsIReferrerInfo.EMPTY,
+ true,
+ NetUtil.newURI(url)
+ );
+ chan.setRequestHeader("Origin", originURL, false);
+
+ let [req, buf] = await channelOpenPromise(chan);
+
+ req.QueryInterface(Ci.nsIHttpChannel);
+ Assert.equal(req.getResponseHeader("x-connection-http2"), "yes");
+ Assert.equal(buf, originURL);
+});
diff --git a/netwerk/test/unit/test_httpssvc_iphint.js b/netwerk/test/unit/test_httpssvc_iphint.js
new file mode 100644
index 0000000000..13d9a7e648
--- /dev/null
+++ b/netwerk/test/unit/test_httpssvc_iphint.js
@@ -0,0 +1,350 @@
+/* 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 h2Port;
+let trrServer;
+
+const certOverrideService = Cc[
+ "@mozilla.org/security/certoverride;1"
+].getService(Ci.nsICertOverrideService);
+const { TestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TestUtils.sys.mjs"
+);
+
+add_setup(async function setup() {
+ h2Port = Services.env.get("MOZHTTP2_PORT");
+ Assert.notEqual(h2Port, null);
+ Assert.notEqual(h2Port, "");
+
+ trr_test_setup();
+
+ Services.prefs.setBoolPref("network.dns.upgrade_with_https_rr", true);
+ Services.prefs.setBoolPref("network.dns.use_https_rr_as_altsvc", true);
+
+ registerCleanupFunction(async () => {
+ trr_clear_prefs();
+ Services.prefs.clearUserPref("network.dns.upgrade_with_https_rr");
+ Services.prefs.clearUserPref("network.dns.use_https_rr_as_altsvc");
+ Services.prefs.clearUserPref("network.dns.disablePrefetch");
+ await trrServer.stop();
+ });
+
+ if (mozinfo.socketprocess_networking) {
+ Services.dns; // Needed to trigger socket process.
+ await TestUtils.waitForCondition(() => Services.io.socketProcessLaunched);
+ }
+});
+
+// Test if IP hint addresses can be accessed as regular A/AAAA records.
+add_task(async function testStoreIPHint() {
+ 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", {
+ answers: [
+ {
+ name: "test.IPHint.com",
+ ttl: 999,
+ 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 { inRecord } = await new TRRDNSListener("test.IPHint.com", {
+ type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC,
+ });
+
+ let answer = inRecord.QueryInterface(Ci.nsIDNSHTTPSSVCRecord).records;
+ Assert.equal(inRecord.QueryInterface(Ci.nsIDNSHTTPSSVCRecord).ttl, 999);
+ 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(domain, flags, expectedAddresses) {
+ // eslint-disable-next-line no-shadow
+ let { inRecord } = await new TRRDNSListener(domain, {
+ flags,
+ expectedSuccess: false,
+ });
+ Assert.ok(inRecord);
+ inRecord.QueryInterface(Ci.nsIDNSAddrRecord);
+ let addresses = [];
+ while (inRecord.hasMore()) {
+ addresses.push(inRecord.getNextAddrAsString());
+ }
+ Assert.deepEqual(addresses, expectedAddresses);
+ Assert.equal(inRecord.ttl, 999);
+ }
+
+ await verifyAnswer("test.IPHint.com", Ci.nsIDNSService.RESOLVE_IP_HINT, [
+ "1.2.3.4",
+ "5.6.7.8",
+ "::1",
+ "fe80::794f:6d2c:3d5e:7836",
+ ]);
+
+ await verifyAnswer(
+ "test.IPHint.com",
+ Ci.nsIDNSService.RESOLVE_IP_HINT | Ci.nsIDNSService.RESOLVE_DISABLE_IPV4,
+ ["::1", "fe80::794f:6d2c:3d5e:7836"]
+ );
+
+ await verifyAnswer(
+ "test.IPHint.com",
+ Ci.nsIDNSService.RESOLVE_IP_HINT | Ci.nsIDNSService.RESOLVE_DISABLE_IPV6,
+ ["1.2.3.4", "5.6.7.8"]
+ );
+
+ info("checking that IPv6 hints are ignored when disableIPv6 is true");
+ Services.prefs.setBoolPref("network.dns.disableIPv6", true);
+ await trrServer.registerDoHAnswers("testv6.IPHint.com", "HTTPS", {
+ answers: [
+ {
+ name: "testv6.IPHint.com",
+ ttl: 999,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 1,
+ name: "testv6.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"] },
+ ],
+ },
+ },
+ ],
+ });
+
+ ({ inRecord } = await new TRRDNSListener("testv6.IPHint.com", {
+ type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC,
+ }));
+ Services.prefs.setBoolPref("network.dns.disableIPv6", false);
+
+ await verifyAnswer("testv6.IPHint.com", Ci.nsIDNSService.RESOLVE_IP_HINT, [
+ "1.2.3.4",
+ "5.6.7.8",
+ ]);
+
+ await verifyAnswer(
+ "testv6.IPHint.com",
+ Ci.nsIDNSService.RESOLVE_IP_HINT | Ci.nsIDNSService.RESOLVE_DISABLE_IPV6,
+ ["1.2.3.4", "5.6.7.8"]
+ );
+
+ await trrServer.stop();
+});
+
+function makeChan(url) {
+ let chan = NetUtil.newChannel({
+ uri: url,
+ loadUsingSystemPrincipal: true,
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT,
+ }).QueryInterface(Ci.nsIHttpChannel);
+ return chan;
+}
+
+function channelOpenPromise(chan, flags) {
+ 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, flags));
+ });
+}
+
+// Test if we can connect to the server with the IP hint address.
+add_task(async function testConnectionWithIPHint() {
+ Services.dns.clearCache(true);
+ Services.prefs.setIntPref("network.trr.mode", 3);
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ "https://127.0.0.1:" + h2Port + "/httpssvc_use_iphint"
+ );
+
+ // Resolving test.iphint.com should be failed.
+ let { inStatus } = await new TRRDNSListener("test.iphint.com", {
+ expectedSuccess: false,
+ });
+ 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/server-timing`);
+ // 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
+ );
+
+ await trrServer.stop();
+});
+
+// Test the case that we failed to use IP Hint address because DNS cache
+// is bypassed.
+add_task(async function testIPHintWithFreshDNS() {
+ 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`
+ );
+ // To make sure NS_HTTP_REFRESH_DNS not be cleared.
+ Services.prefs.setBoolPref("network.dns.disablePrefetch", true);
+
+ await trrServer.registerDoHAnswers("test.iphint.org", "HTTPS", {
+ answers: [
+ {
+ name: "test.iphint.org",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 0,
+ name: "svc.iphint.net",
+ values: [],
+ },
+ },
+ ],
+ });
+
+ await trrServer.registerDoHAnswers("svc.iphint.net", "HTTPS", {
+ answers: [
+ {
+ name: "svc.iphint.net",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 1,
+ name: "svc.iphint.net",
+ values: [
+ { key: "alpn", value: "h2" },
+ { key: "port", value: h2Port },
+ { key: "ipv4hint", value: "127.0.0.1" },
+ ],
+ },
+ },
+ ],
+ });
+
+ let { inRecord } = await new TRRDNSListener("test.iphint.org", {
+ type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC,
+ });
+
+ let answer = inRecord.QueryInterface(Ci.nsIDNSHTTPSSVCRecord).records;
+ Assert.equal(answer[0].priority, 1);
+ Assert.equal(answer[0].name, "svc.iphint.net");
+
+ certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData(
+ true
+ );
+
+ let chan = makeChan(`https://test.iphint.org/server-timing`);
+ chan.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE;
+ let [req] = await channelOpenPromise(
+ chan,
+ CL_EXPECT_FAILURE | CL_ALLOW_UNKNOWN_CL
+ );
+ // Failed because there no A record for "svc.iphint.net".
+ Assert.equal(req.status, Cr.NS_ERROR_UNKNOWN_HOST);
+
+ await trrServer.registerDoHAnswers("svc.iphint.net", "A", {
+ answers: [
+ {
+ name: "svc.iphint.net",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "127.0.0.1",
+ },
+ ],
+ });
+
+ chan = makeChan(`https://test.iphint.org/server-timing`);
+ chan.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE;
+ [req] = await channelOpenPromise(chan);
+ Assert.equal(req.protocolVersion, "h2");
+ let internal = req.QueryInterface(Ci.nsIHttpChannelInternal);
+ Assert.equal(internal.remotePort, h2Port);
+
+ certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData(
+ false
+ );
+ await trrServer.stop();
+});
diff --git a/netwerk/test/unit/test_httpssvc_priority.js b/netwerk/test/unit/test_httpssvc_priority.js
new file mode 100644
index 0000000000..401e23f85c
--- /dev/null
+++ b/netwerk/test/unit/test_httpssvc_priority.js
@@ -0,0 +1,125 @@
+/* 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";
+
+trr_test_setup();
+registerCleanupFunction(async () => {
+ trr_clear_prefs();
+ Services.prefs.clearUserPref("network.dns.echconfig.enabled");
+});
+
+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", {
+ answers: [
+ {
+ 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 { inRecord } = await new TRRDNSListener("test.priority.com", {
+ type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC,
+ });
+
+ 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);
+ Services.dns.clearCache(true);
+ ({ inRecord } = await new TRRDNSListener("test.priority.com", {
+ type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC,
+ }));
+
+ 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..c261e29c70
--- /dev/null
+++ b/netwerk/test/unit/test_httpssvc_retry_with_ech.js
@@ -0,0 +1,511 @@
+/* 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 { setTimeout } = ChromeUtils.importESModule(
+ "resource://gre/modules/Timer.sys.mjs"
+);
+
+let trrServer;
+let h3Port;
+let h3EchConfig;
+
+const certOverrideService = Cc[
+ "@mozilla.org/security/certoverride;1"
+].getService(Ci.nsICertOverrideService);
+
+function checkSecurityInfo(chan, expectPrivateDNS, expectAcceptedECH) {
+ let securityInfo = chan.securityInfo;
+ Assert.equal(
+ securityInfo.isAcceptedEch,
+ expectAcceptedECH,
+ "ECH Status == Expected Status"
+ );
+ Assert.equal(
+ securityInfo.usedPrivateDNS,
+ expectPrivateDNS,
+ "Private DNS Status == Expected Status"
+ );
+}
+
+add_setup(async function setup() {
+ // Allow telemetry probes which may otherwise be disabled for some
+ // applications (e.g. Thunderbird).
+ Services.prefs.setBoolPref(
+ "toolkit.telemetry.testing.overrideProductsCheck",
+ true
+ );
+
+ trr_test_setup();
+
+ 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.setBoolPref("network.dns.http3_echconfig.enabled", true);
+ Services.prefs.setIntPref("network.http.speculative-parallel-limit", 0);
+ Services.prefs.setIntPref("network.trr.mode", Ci.nsIDNSService.MODE_TRRONLY);
+
+ await asyncStartTLSTestServer(
+ "EncryptedClientHelloServer",
+ "../../../security/manager/ssl/tests/unit/test_encrypted_client_hello"
+ );
+
+ h3Port = Services.env.get("MOZHTTP3_PORT_ECH");
+ Assert.notEqual(h3Port, null);
+ Assert.notEqual(h3Port, "");
+
+ h3EchConfig = Services.env.get("MOZHTTP3_ECH");
+ Assert.notEqual(h3EchConfig, null);
+ Assert.notEqual(h3EchConfig, "");
+});
+
+registerCleanupFunction(async () => {
+ trr_clear_prefs();
+ Services.prefs.clearUserPref("network.trr.mode");
+ Services.prefs.clearUserPref("network.trr.uri");
+ Services.prefs.clearUserPref("network.dns.upgrade_with_https_rr");
+ Services.prefs.clearUserPref("network.dns.use_https_rr_as_altsvc");
+ Services.prefs.clearUserPref("network.dns.echconfig.enabled");
+ Services.prefs.clearUserPref("network.dns.http3_echconfig.enabled");
+ Services.prefs.clearUserPref(
+ "network.dns.echconfig.fallback_to_origin_when_all_failed"
+ );
+ Services.prefs.clearUserPref("network.http.speculative-parallel-limit");
+ Services.prefs.clearUserPref("network.dns.port_prefixed_qname_https_rr");
+ Services.prefs.clearUserPref("security.tls.ech.grease_http3");
+ Services.prefs.clearUserPref("security.tls.ech.grease_probability");
+ if (trrServer) {
+ await trrServer.stop();
+ }
+});
+
+function makeChan(url) {
+ let chan = NetUtil.newChannel({
+ uri: url,
+ loadUsingSystemPrincipal: true,
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT,
+ }).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));
+ });
+}
+
+function ActivityObserver() {}
+
+ActivityObserver.prototype = {
+ activites: [],
+ observeConnectionActivity(
+ aHost,
+ aPort,
+ aSSL,
+ aHasECH,
+ aIsHttp3,
+ aActivityType,
+ aActivitySubtype,
+ aTimestamp,
+ aExtraStringData
+ ) {
+ dump(
+ "*** Connection Activity 0x" +
+ aActivityType.toString(16) +
+ " 0x" +
+ aActivitySubtype.toString(16) +
+ " " +
+ aExtraStringData +
+ "\n"
+ );
+ this.activites.push({ host: aHost, subType: aActivitySubtype });
+ },
+};
+
+function checkHttpActivities(activites, expectECH) {
+ let foundDNSAndSocket = false;
+ let foundSettingECH = false;
+ let foundConnectionCreated = false;
+ for (let activity of activites) {
+ switch (activity.subType) {
+ case Ci.nsIHttpActivityObserver.ACTIVITY_SUBTYPE_DNSANDSOCKET_CREATED:
+ case Ci.nsIHttpActivityObserver
+ .ACTIVITY_SUBTYPE_SPECULATIVE_DNSANDSOCKET_CREATED:
+ foundDNSAndSocket = true;
+ break;
+ case Ci.nsIHttpActivityDistributor.ACTIVITY_SUBTYPE_ECH_SET:
+ foundSettingECH = true;
+ break;
+ case Ci.nsIHttpActivityDistributor.ACTIVITY_SUBTYPE_CONNECTION_CREATED:
+ foundConnectionCreated = true;
+ break;
+ default:
+ break;
+ }
+ }
+
+ Assert.equal(foundDNSAndSocket, true, "Should have one DnsAndSock created");
+ Assert.equal(foundSettingECH, expectECH, "Should have echConfig");
+ Assert.equal(
+ foundConnectionCreated,
+ true,
+ "Should have one connection created"
+ );
+}
+
+add_task(async function testConnectWithECH() {
+ const ECH_CONFIG_FIXED =
+ "AEn+DQBFTQAgACCKB1Y5SfrGIyk27W82xPpzWTDs3q72c04xSurDWlb9CgAEAAEAA2QWZWNoLXB1YmxpYy5leGFtcGxlLmNvbQAA";
+ trrServer = new TRRServer();
+ await trrServer.start();
+
+ let observerService = Cc[
+ "@mozilla.org/network/http-activity-distributor;1"
+ ].getService(Ci.nsIHttpActivityDistributor);
+ let observer = new ActivityObserver();
+ observerService.addObserver(observer);
+ observerService.observeConnection = true;
+
+ 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", {
+ answers: [
+ {
+ 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", {
+ answers: [
+ {
+ name: "ech-private.example.com",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "127.0.0.1",
+ },
+ ],
+ });
+
+ await new TRRDNSListener("ech-private.example.com", {
+ type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC,
+ });
+
+ HandshakeTelemetryHelpers.resetHistograms();
+ let chan = makeChan(`https://ech-private.example.com`);
+ await channelOpenPromise(chan, CL_ALLOW_UNKNOWN_CL);
+ checkSecurityInfo(chan, true, true);
+ // Only check telemetry if network process is disabled.
+ if (!mozinfo.socketprocess_networking) {
+ HandshakeTelemetryHelpers.checkSuccess(["", "_ECH", "_FIRST_TRY"]);
+ HandshakeTelemetryHelpers.checkEmpty(["_CONSERVATIVE", "_ECH_GREASE"]);
+ }
+
+ await trrServer.stop();
+ observerService.removeObserver(observer);
+ observerService.observeConnection = false;
+
+ let filtered = observer.activites.filter(
+ activity => activity.host === "ech-private.example.com"
+ );
+ checkHttpActivities(filtered, true);
+});
+
+add_task(async function testEchRetry() {
+ Services.obs.notifyObservers(null, "net:cancel-all-connections");
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => setTimeout(resolve, 1000));
+
+ Services.dns.clearCache(true);
+
+ const ECH_CONFIG_TRUSTED_RETRY =
+ "AEn+DQBFTQAgACCKB1Y5SfrGIyk27W82xPpzWTDs3q72c04xSurDWlb9CgAEAAMAA2QWZWNoLXB1YmxpYy5leGFtcGxlLmNvbQAA";
+ 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", {
+ answers: [
+ {
+ 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", {
+ answers: [
+ {
+ name: "ech-private.example.com",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "127.0.0.1",
+ },
+ ],
+ });
+
+ await new TRRDNSListener("ech-private.example.com", {
+ type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC,
+ });
+
+ Services.prefs.setBoolPref("network.dns.echconfig.enabled", true);
+
+ HandshakeTelemetryHelpers.resetHistograms();
+ let chan = makeChan(`https://ech-private.example.com`);
+ await channelOpenPromise(chan, CL_ALLOW_UNKNOWN_CL);
+ checkSecurityInfo(chan, true, true);
+ // Only check telemetry if network process is disabled.
+ if (!mozinfo.socketprocess_networking) {
+ for (let hName of ["SSL_HANDSHAKE_RESULT", "SSL_HANDSHAKE_RESULT_ECH"]) {
+ let h = Services.telemetry.getHistogramById(hName);
+ HandshakeTelemetryHelpers.assertHistogramMap(
+ h.snapshot(),
+ new Map([
+ ["0", 1],
+ ["188", 1],
+ ])
+ );
+ }
+ HandshakeTelemetryHelpers.checkEntry(["_FIRST_TRY"], 188, 1);
+ HandshakeTelemetryHelpers.checkEmpty(["_CONSERVATIVE", "_ECH_GREASE"]);
+ }
+
+ await trrServer.stop();
+});
+
+async function H3ECHTest(
+ echConfig,
+ expectedHistKey,
+ expectedHistEntries,
+ advertiseECH
+) {
+ Services.dns.clearCache(true);
+ Services.obs.notifyObservers(null, "net:cancel-all-connections");
+ /* eslint-disable mozilla/no-arbitrary-setTimeout */
+ await new Promise(resolve => setTimeout(resolve, 1000));
+ resetEchTelemetry();
+ trrServer = new TRRServer();
+ await trrServer.start();
+
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${trrServer.port()}/dns-query`
+ );
+ Services.prefs.setBoolPref("network.dns.port_prefixed_qname_https_rr", true);
+
+ let observerService = Cc[
+ "@mozilla.org/network/http-activity-distributor;1"
+ ].getService(Ci.nsIHttpActivityDistributor);
+ Services.obs.notifyObservers(null, "net:cancel-all-connections");
+ let observer = new ActivityObserver();
+ observerService.addObserver(observer);
+ observerService.observeConnection = true;
+ // Clear activities for past connections
+ observer.activites = [];
+
+ let portPrefixedName = `_${h3Port}._https.public.example.com`;
+ let vals = [
+ { key: "alpn", value: "h3-29" },
+ { key: "port", value: h3Port },
+ ];
+ if (advertiseECH) {
+ vals.push({
+ key: "echconfig",
+ value: echConfig,
+ needBase64Decode: true,
+ });
+ }
+ // Only the last record is valid to use.
+
+ await trrServer.registerDoHAnswers(portPrefixedName, "HTTPS", {
+ answers: [
+ {
+ name: portPrefixedName,
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 1,
+ name: ".",
+ values: vals,
+ },
+ },
+ ],
+ });
+
+ await trrServer.registerDoHAnswers("public.example.com", "A", {
+ answers: [
+ {
+ name: "public.example.com",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "127.0.0.1",
+ },
+ ],
+ });
+
+ await new TRRDNSListener("public.example.com", {
+ type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC,
+ port: h3Port,
+ });
+
+ let chan = makeChan(`https://public.example.com:${h3Port}`);
+ let [req] = await channelOpenPromise(chan, CL_ALLOW_UNKNOWN_CL);
+ req.QueryInterface(Ci.nsIHttpChannel);
+ Assert.equal(req.protocolVersion, "h3-29");
+ checkSecurityInfo(chan, true, advertiseECH);
+
+ await trrServer.stop();
+
+ observerService.removeObserver(observer);
+ observerService.observeConnection = false;
+
+ let filtered = observer.activites.filter(
+ activity => activity.host === "public.example.com"
+ );
+ checkHttpActivities(filtered, advertiseECH);
+ await checkEchTelemetry(expectedHistKey, expectedHistEntries);
+}
+
+function resetEchTelemetry() {
+ Services.telemetry.getKeyedHistogramById("HTTP3_ECH_OUTCOME").clear();
+}
+
+async function checkEchTelemetry(histKey, histEntries) {
+ Services.obs.notifyObservers(null, "net:cancel-all-connections");
+ /* eslint-disable mozilla/no-arbitrary-setTimeout */
+ await new Promise(resolve => setTimeout(resolve, 1000));
+ let values = Services.telemetry
+ .getKeyedHistogramById("HTTP3_ECH_OUTCOME")
+ .snapshot()[histKey];
+ if (!mozinfo.socketprocess_networking) {
+ HandshakeTelemetryHelpers.assertHistogramMap(values, histEntries);
+ }
+}
+
+add_task(async function testH3WithNoEch() {
+ Services.prefs.setBoolPref("security.tls.ech.grease_http3", false);
+ Services.prefs.setIntPref("security.tls.ech.grease_probability", 0);
+ await H3ECHTest(
+ h3EchConfig,
+ "NONE",
+ new Map([
+ ["0", 1],
+ ["1", 0],
+ ]),
+ false
+ );
+});
+
+add_task(async function testH3WithECH() {
+ await H3ECHTest(
+ h3EchConfig,
+ "REAL",
+ new Map([
+ ["0", 1],
+ ["1", 0],
+ ]),
+ true
+ );
+});
+
+add_task(async function testH3WithGreaseEch() {
+ Services.prefs.setBoolPref("security.tls.ech.grease_http3", true);
+ Services.prefs.setIntPref("security.tls.ech.grease_probability", 100);
+ await H3ECHTest(
+ h3EchConfig,
+ "GREASE",
+ new Map([
+ ["0", 1],
+ ["1", 0],
+ ]),
+ false
+ );
+});
+
+add_task(async function testH3WithECHRetry() {
+ Services.dns.clearCache(true);
+ Services.obs.notifyObservers(null, "net:cancel-all-connections");
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => setTimeout(resolve, 1000));
+
+ function base64ToArray(base64) {
+ var binary_string = atob(base64);
+ var len = binary_string.length;
+ var bytes = new Uint8Array(len);
+ for (var i = 0; i < len; i++) {
+ bytes[i] = binary_string.charCodeAt(i);
+ }
+ return bytes;
+ }
+
+ let decodedConfig = base64ToArray(h3EchConfig);
+ decodedConfig[6] ^= 0x94;
+ let encoded = btoa(String.fromCharCode.apply(null, decodedConfig));
+ await H3ECHTest(
+ encoded,
+ "REAL",
+ new Map([
+ ["0", 1],
+ ["1", 1],
+ ]),
+ true
+ );
+});
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..4503c00504
--- /dev/null
+++ b/netwerk/test/unit/test_httpssvc_retry_without_ech.js
@@ -0,0 +1,138 @@
+/* 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 trrServer;
+
+const certOverrideService = Cc[
+ "@mozilla.org/security/certoverride;1"
+].getService(Ci.nsICertOverrideService);
+
+add_setup(async function setup() {
+ trr_test_setup();
+
+ Services.prefs.setIntPref("network.trr.mode", Ci.nsIDNSService.MODE_TRRFIRST);
+ 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);
+
+ // An arbitrary, non-ECH server.
+ await asyncStartTLSTestServer(
+ "DelegatedCredentialsServer",
+ "../../../security/manager/ssl/tests/unit/test_delegated_credentials"
+ );
+
+ let nssComponent = Cc["@mozilla.org/psm;1"].getService(Ci.nsINSSComponent);
+ await nssComponent.asyncClearSSLExternalAndInternalSessionCache();
+});
+
+registerCleanupFunction(async () => {
+ trr_clear_prefs();
+ Services.prefs.clearUserPref("network.dns.upgrade_with_https_rr");
+ Services.prefs.clearUserPref("network.dns.use_https_rr_as_altsvc");
+ Services.prefs.clearUserPref("network.dns.echconfig.enabled");
+ Services.prefs.clearUserPref(
+ "network.dns.echconfig.fallback_to_origin_when_all_failed"
+ );
+ if (trrServer) {
+ await trrServer.stop();
+ }
+});
+
+function makeChan(url) {
+ let chan = NetUtil.newChannel({
+ uri: url,
+ loadUsingSystemPrincipal: true,
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT,
+ }).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 =
+ "AEn+DQBFTQAgACCKB1Y5SfrGIyk27W82xPpzWTDs3q72c04xSurDWlb9CgAEAAEAA2QWZWNoLXB1YmxpYy5leGFtcGxlLmNvbQAA";
+ 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`
+ );
+ Services.prefs.setBoolPref(
+ "network.dns.echconfig.fallback_to_origin_when_all_failed",
+ true
+ );
+
+ // Only the last record is valid to use.
+ await trrServer.registerDoHAnswers(
+ "delegated-disabled.example.com",
+ "HTTPS",
+ {
+ answers: [
+ {
+ 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", {
+ answers: [
+ {
+ name: "delegated-disabled.example.com",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "127.0.0.1",
+ },
+ ],
+ });
+
+ await new TRRDNSListener("delegated-disabled.example.com", {
+ type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC,
+ });
+
+ let chan = makeChan(`https://delegated-disabled.example.com:8443`);
+ await channelOpenPromise(chan, CL_ALLOW_UNKNOWN_CL);
+ let securityInfo = chan.securityInfo;
+
+ 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..581caf906e
--- /dev/null
+++ b/netwerk/test/unit/test_httpsuspend.js
@@ -0,0 +1,86 @@
+// This file ensures that suspending a channel directly after opening it
+// suspends future notifications correctly.
+
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+ChromeUtils.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..565407ac3b
--- /dev/null
+++ b/netwerk/test/unit/test_idn_blacklist.js
@@ -0,0 +1,168 @@
+// 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 = Services.prefs;
+ var oldProfile = pbi.getCharPref(
+ "network.IDN.restriction_profile",
+ "moderate"
+ );
+ var idnService = Cc["@mozilla.org/network/idn-service;1"].getService(
+ Ci.nsIIDNService
+ );
+
+ pbi.setCharPref("network.IDN.restriction_profile", "moderate");
+
+ 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.setCharPref("network.IDN.restriction_profile", oldProfile);
+}
diff --git a/netwerk/test/unit/test_idn_spoof.js b/netwerk/test/unit/test_idn_spoof.js
new file mode 100644
index 0000000000..8512d3272e
--- /dev/null
+++ b/netwerk/test/unit/test_idn_spoof.js
@@ -0,0 +1,1052 @@
+// Copyright 2015 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+// https://source.chromium.org/chromium/chromium/src/+/main:LICENSE
+
+// Tests nsIIDNService
+// Imported from https://source.chromium.org/chromium/chromium/src/+/main:components/url_formatter/spoof_checks/idn_spoof_checker_unittest.cc;drc=e544837967287f956ba69af3b228b202e8e7cf1a
+
+"use strict";
+
+const idnService = Cc["@mozilla.org/network/idn-service;1"].getService(
+ Ci.nsIIDNService
+);
+
+const kSafe = 1;
+const kUnsafe = 2;
+const kInvalid = 3;
+
+// prettier-ignore
+let testCases = [
+ // No IDN
+ ["www.google.com", "www.google.com", kSafe],
+ ["www.google.com.", "www.google.com.", kSafe],
+ [".", ".", kSafe],
+ ["", "", kSafe],
+ // Invalid IDN
+ ["xn--example-.com", "xn--example-.com", kInvalid],
+ // IDN
+ // Hanzi (Traditional Chinese)
+ ["xn--1lq90ic7f1rc.cn", "\u5317\u4eac\u5927\u5b78.cn", kSafe],
+ // Hanzi ('video' in Simplified Chinese)
+ ["xn--cy2a840a.com", "\u89c6\u9891.com", kSafe],
+ // Hanzi + '123'
+ ["www.xn--123-p18d.com", "www.\u4e00123.com", kSafe],
+ // Hanzi + Latin : U+56FD is simplified
+ ["www.xn--hello-9n1hm04c.com", "www.hello\u4e2d\u56fd.com", kSafe],
+ // Kanji + Kana (Japanese)
+ ["xn--l8jvb1ey91xtjb.jp", "\u671d\u65e5\u3042\u3055\u3072.jp", kSafe],
+ // Katakana including U+30FC
+ ["xn--tckm4i2e.jp", "\u30b3\u30de\u30fc\u30b9.jp", kSafe],
+ ["xn--3ck7a7g.jp", "\u30ce\u30f3\u30bd.jp", kSafe],
+ // Katakana + Latin (Japanese)
+ ["xn--e-efusa1mzf.jp", "e\u30b3\u30de\u30fc\u30b9.jp", kSafe],
+ ["xn--3bkxe.jp", "\u30c8\u309a.jp", kSafe],
+ // Hangul (Korean)
+ ["www.xn--or3b17p6jjc.kr", "www.\uc804\uc790\uc815\ubd80.kr", kSafe],
+ // b<u-umlaut>cher (German)
+ ["xn--bcher-kva.de", "b\u00fccher.de", kSafe],
+ // a with diaeresis
+ ["www.xn--frgbolaget-q5a.se", "www.f\u00e4rgbolaget.se", kSafe],
+ // c-cedilla (French)
+ ["www.xn--alliancefranaise-npb.fr", "www.alliancefran\u00e7aise.fr", kSafe],
+ // caf'e with acute accent (French)
+ ["xn--caf-dma.fr", "caf\u00e9.fr", kSafe],
+ // c-cedillla and a with tilde (Portuguese)
+ ["xn--poema-9qae5a.com.br", "p\u00e3oema\u00e7\u00e3.com.br", kSafe],
+ // s with caron
+ ["xn--achy-f6a.com", "\u0161achy.com", kSafe],
+ ["xn--kxae4bafwg.gr", "\u03bf\u03c5\u03c4\u03bf\u03c0\u03af\u03b1.gr", kSafe],
+ // Eutopia + 123 (Greek)
+ ["xn---123-pldm0haj2bk.gr", "\u03bf\u03c5\u03c4\u03bf\u03c0\u03af\u03b1-123.gr", kSafe],
+ // Cyrillic (Russian)
+ ["xn--n1aeec9b.r", "\u0442\u043e\u0440\u0442\u044b.r", kSafe],
+ // Cyrillic + 123 (Russian)
+ ["xn---123-45dmmc5f.r", "\u0442\u043e\u0440\u0442\u044b-123.r", kSafe],
+ // 'president' in Russian. Is a wholescript confusable, but allowed.
+ ["xn--d1abbgf6aiiy.xn--p1ai", "\u043f\u0440\u0435\u0437\u0438\u0434\u0435\u043d\u0442.\u0440\u0444", kSafe],
+ // Arabic
+ ["xn--mgba1fmg.eg", "\u0627\u0641\u0644\u0627\u0645.eg", kSafe],
+ // Hebrew
+ ["xn--4dbib.he", "\u05d5\u05d0\u05d4.he", kSafe],
+ // Hebrew + Common
+ ["xn---123-ptf2c5c6bt.il", "\u05e2\u05d1\u05e8\u05d9\u05ea-123.il", kSafe],
+ // Thai
+ ["xn--12c2cc4ag3b4ccu.th", "\u0e2a\u0e32\u0e22\u0e01\u0e32\u0e23\u0e1a\u0e34\u0e19.th", kSafe],
+ // Thai + Common
+ ["xn---123-9goxcp8c9db2r.th", "\u0e20\u0e32\u0e29\u0e32\u0e44\u0e17\u0e22-123.th", kSafe],
+ // Devangari (Hindi)
+ ["www.xn--l1b6a9e1b7c.in", "www.\u0905\u0915\u094b\u0932\u093e.in", kSafe],
+ // Devanagari + Common
+ ["xn---123-kbjl2j0bl2k.in", "\u0939\u093f\u0928\u094d\u0926\u0940-123.in", kSafe],
+
+ // Block mixed numeric + numeric lookalike (12.com, using U+0577).
+ ["xn--1-xcc.com", "1\u0577.com", kUnsafe, "DISABLED"],
+
+ // Block mixed numeric lookalike + numeric (੨0.com, uses U+0A68).
+ ["xn--0-6ee.com", "\u0a680.com", kUnsafe],
+ // Block fully numeric lookalikes (৪੨.com using U+09EA and U+0A68).
+ ["xn--47b6w.com", "\u09ea\u0a68.com", kUnsafe],
+ // Block single script digit lookalikes (using three U+0A68 characters).
+ ["xn--qccaa.com", "\u0a68\u0a68\u0a68.com", kUnsafe, "DISABLED"],
+
+ // URL test with mostly numbers and one confusable character
+ // Georgian 'd' 4000.com
+ ["xn--4000-pfr.com", "\u10eb4000.com", kUnsafe, "DISABLED"],
+
+ // What used to be 5 Aspirational scripts in the earlier versions of UAX 31.
+ // UAX 31 does not define aspirational scripts any more.
+ // See http://www.unicode.org/reports/tr31/#Aspirational_Use_Scripts .
+ // Unified Canadian Syllabary
+ ["xn--dfe0tte.ca", "\u1456\u14c2\u14ef.ca", kUnsafe],
+ // Tifinagh
+ ["xn--4ljxa2bb4a6bxb.ma", "\u2d5c\u2d49\u2d3c\u2d49\u2d4f\u2d30\u2d56.ma", kUnsafe],
+ // Tifinagh with a disallowed character(U+2D6F)
+ ["xn--hmjzaby5d5f.ma", "\u2d5c\u2d49\u2d3c\u2d6f\u2d49\u2d4f.ma", kInvalid],
+
+ // Yi
+ ["xn--4o7a6e1x64c.cn", "\ua188\ua320\ua071\ua0b7.cn", kUnsafe],
+ // Mongolian - 'ordu' (place, camp)
+ ["xn--56ec8bp.cn", "\u1823\u1837\u1833\u1824.cn", kUnsafe],
+ // Mongolian with a disallowed character
+ ["xn--95e5de3ds.cn", "\u1823\u1837\u1804\u1833\u1824.cn", kUnsafe],
+ // Miao/Pollad
+ ["xn--2u0fpf0a.cn", "\U00016f04\U00016f62\U00016f59.cn", kUnsafe],
+
+ // Script mixing tests
+ // The following script combinations are allowed.
+ // HIGHLY_RESTRICTIVE with Latin limited to ASCII-Latin.
+ // ASCII-Latin + Japn (Kana + Han)
+ // ASCII-Latin + Kore (Hangul + Han)
+ // ASCII-Latin + Han + Bopomofo
+ // "payp<alpha>l.com"
+ ["xn--paypl-g9d.com", "payp\u03b1l.com", kUnsafe],
+ // google.gr with Greek omicron and epsilon
+ ["xn--ggl-6xc1ca.gr", "g\u03bf\u03bfgl\u03b5.gr", kUnsafe],
+ // google.ru with Cyrillic o
+ ["xn--ggl-tdd6ba.r", "g\u043e\u043egl\u0435.r", kUnsafe],
+ // h<e with acute>llo<China in Han>.cn
+ ["xn--hllo-bpa7979ih5m.cn", "h\u00e9llo\u4e2d\u56fd.cn", kUnsafe, "DISABLED"],
+ // <Greek rho><Cyrillic a><Cyrillic u>.ru
+ ["xn--2xa6t2b.r", "\u03c1\u0430\u0443.r", kUnsafe],
+ // Georgian + Latin
+ ["xn--abcef-vuu.test", "abc\u10ebef.test", kUnsafe],
+ // Hangul + Latin
+ ["xn--han-eb9ll88m.kr", "\ud55c\uae00han.kr", kSafe],
+ // Hangul + Latin + Han with IDN ccTLD
+ ["xn--han-or0kq92gkm3c.xn--3e0b707e", "\ud55c\uae00han\u97d3.\ud55c\uad6d", kSafe],
+ // non-ASCII Latin + Hangul
+ ["xn--caf-dma9024xvpg.kr", "caf\u00e9\uce74\ud398.kr", kUnsafe, "DISABLED"],
+ // Hangul + Hiragana
+ ["xn--y9j3b9855e.kr", "\ud55c\u3072\u3089.kr", kUnsafe],
+ // <Hiragana>.<Hangul> is allowed because script mixing check is per label.
+ ["xn--y9j3b.xn--3e0b707e", "\u3072\u3089.\ud55c\uad6d", kSafe],
+ // Traditional Han + Latin
+ ["xn--hanzi-u57ii69i.tw", "\u6f22\u5b57hanzi.tw", kSafe],
+ // Simplified Han + Latin
+ ["xn--hanzi-u57i952h.cn", "\u6c49\u5b57hanzi.cn", kSafe],
+ // Simplified Han + Traditonal Han
+ ["xn--hanzi-if9kt8n.cn", "\u6c49\u6f22hanzi.cn", kSafe],
+ // Han + Hiragana + Katakana + Latin
+ ["xn--kanji-ii4dpizfq59yuykqr4b.jp", "\u632f\u308a\u4eee\u540d\u30ab\u30bfkanji.jp", kSafe],
+ // Han + Bopomofo
+ ["xn--5ekcde0577e87tc.tw", "\u6ce8\u97f3\u3105\u3106\u3107\u3108.tw", kSafe],
+ // Han + Latin + Bopomofo
+ ["xn--bopo-ty4cghi8509kk7xd.tw", "\u6ce8\u97f3bopo\u3105\u3106\u3107\u3108.tw", kSafe],
+ // Latin + Bopomofo
+ ["xn--bopomofo-hj5gkalm.tw", "bopomofo\u3105\u3106\u3107\u3108.tw", kSafe],
+ // Bopomofo + Katakana
+ ["xn--lcka3d1bztghi.tw", "\u3105\u3106\u3107\u3108\u30ab\u30bf\u30ab\u30ca.tw", kUnsafe],
+ // Bopomofo + Hangul
+ ["xn--5ekcde4543qbec.tw", "\u3105\u3106\u3107\u3108\uc8fc\uc74c.tw", kUnsafe],
+ // Devanagari + Latin
+ ["xn--ab-3ofh8fqbj6h.in", "ab\u0939\u093f\u0928\u094d\u0926\u0940.in", kUnsafe],
+ // Thai + Latin
+ ["xn--ab-jsi9al4bxdb6n.th", "ab\u0e20\u0e32\u0e29\u0e32\u0e44\u0e17\u0e22.th", kUnsafe],
+ // Armenian + Latin
+ ["xn--bs-red.com", "b\u057ds.com", kUnsafe],
+ // Tibetan + Latin
+ ["xn--foo-vkm.com", "foo\u0f37.com", kUnsafe],
+ // Oriya + Latin
+ ["xn--fo-h3g.com", "fo\u0b66.com", kUnsafe],
+ // Gujarati + Latin
+ ["xn--fo-isg.com", "fo\u0ae6.com", kUnsafe],
+ // <vitamin in Katakana>b1.com
+ ["xn--b1-xi4a7cvc9f.com", "\u30d3\u30bf\u30df\u30f3b1.com", kSafe],
+ // Devanagari + Han
+ ["xn--t2bes3ds6749n.com", "\u0930\u094b\u0932\u0947\u76e7\u0938.com", kUnsafe],
+ // Devanagari + Bengali
+ ["xn--11b0x.in", "\u0915\u0995.in", kUnsafe],
+ // Canadian Syllabary + Latin
+ ["xn--ab-lym.com", "ab\u14bf.com", kUnsafe],
+ ["xn--ab1-p6q.com", "ab1\u14bf.com", kUnsafe],
+ ["xn--1ab-m6qd.com", "\u14bf1ab\u14bf.com", kUnsafe],
+ ["xn--ab-jymc.com", "\u14bfab\u14bf.com", kUnsafe],
+ // Tifinagh + Latin
+ ["xn--liy-bq1b.com", "li\u2d4fy.com", kUnsafe],
+ ["xn--rol-cq1b.com", "rol\u2d4f.com", kUnsafe],
+ ["xn--ily-8p1b.com", "\u2d4fily.com", kUnsafe],
+ ["xn--1ly-8p1b.com", "\u2d4f1ly.com", kUnsafe],
+
+ // Invisibility check
+ // Thai tone mark malek(U+0E48) repeated
+ ["xn--03c0b3ca.th", "\u0e23\u0e35\u0e48\u0e48.th", kUnsafe],
+ // Accute accent repeated
+ ["xn--a-xbba.com", "a\u0301\u0301.com", kInvalid],
+ // 'a' with acuted accent + another acute accent
+ ["xn--1ca20i.com", "\u00e1\u0301.com", kUnsafe, "DISABLED"],
+ // Combining mark at the beginning
+ ["xn--abc-fdc.jp", "\u0300abc.jp", kInvalid],
+
+ // The following three are detected by |dangerous_pattern| regex, but
+ // can be regarded as an extension of blocking repeated diacritic marks.
+ // i followed by U+0307 (combining dot above)
+ ["xn--pixel-8fd.com", "pi\u0307xel.com", kUnsafe],
+ // U+0131 (dotless i) followed by U+0307
+ ["xn--pxel-lza43z.com", "p\u0131\u0307xel.com", kUnsafe],
+ // j followed by U+0307 (combining dot above)
+ ["xn--jack-qwc.com", "j\u0307ack.com", kUnsafe],
+ // l followed by U+0307
+ ["xn--lace-qwc.com", "l\u0307ace.com", kUnsafe],
+
+ // Do not allow a combining mark after dotless i/j.
+ ["xn--pxel-lza29y.com", "p\u0131\u0300xel.com", kUnsafe],
+ ["xn--ack-gpb42h.com", "\u0237\u0301ack.com", kUnsafe],
+
+ // Mixed script confusable
+ // google with Armenian Small Letter Oh(U+0585)
+ ["xn--gogle-lkg.com", "g\u0585ogle.com", kUnsafe],
+ ["xn--range-kkg.com", "\u0585range.com", kUnsafe],
+ ["xn--cucko-pkg.com", "cucko\u0585.com", kUnsafe],
+ // Latin 'o' in Armenian.
+ ["xn--o-ybcg0cu0cq.com", "o\u0580\u0574\u0578\u0582\u0566\u0568.com", kUnsafe],
+ // Hiragana HE(U+3078) mixed with Katakana
+ ["xn--49jxi3as0d0fpc.com", "\u30e2\u30d2\u30fc\u30c8\u3078\u30d6\u30f3.com", kUnsafe, "DISABLED"],
+
+ // U+30FC should be preceded by a Hiragana/Katakana.
+ // Katakana + U+30FC + Han
+ ["xn--lck0ip02qw5ya.jp", "\u30ab\u30fc\u91ce\u7403.jp", kSafe],
+ // Hiragana + U+30FC + Han
+ ["xn--u8j5tr47nw5ya.jp", "\u304b\u30fc\u91ce\u7403.jp", kSafe],
+ // U+30FC + Han
+ ["xn--weka801xo02a.com", "\u30fc\u52d5\u753b\u30fc.com", kUnsafe],
+ // Han + U+30FC + Han
+ ["xn--wekz60nb2ay85atj0b.jp", "\u65e5\u672c\u30fc\u91ce\u7403.jp", kUnsafe],
+ // U+30FC at the beginning
+ ["xn--wek060nb2a.jp", "\u30fc\u65e5\u672c.jp", kUnsafe],
+ // Latin + U+30FC + Latin
+ ["xn--abcdef-r64e.jp", "abc\u30fcdef.jp", kUnsafe],
+
+ // U+30FB (・) is not allowed next to Latin, but allowed otherwise.
+ // U+30FB + Han
+ ["xn--vekt920a.jp", "\u30fb\u91ce.jp", kSafe],
+ // Han + U+30FB + Han
+ ["xn--vek160nb2ay85atj0b.jp", "\u65e5\u672c\u30fb\u91ce\u7403.jp", kSafe],
+ // Latin + U+30FB + Latin
+ ["xn--abcdef-k64e.jp", "abc\u30fbdef.jp", kUnsafe, "DISABLED"],
+ // U+30FB + Latin
+ ["xn--abc-os4b.jp", "\u30fbabc.jp", kUnsafe, "DISABLED"],
+
+ // U+30FD (ヽ) is allowed only after Katakana.
+ // Katakana + U+30FD
+ ["xn--lck2i.jp", "\u30ab\u30fd.jp", kSafe],
+ // Hiragana + U+30FD
+ ["xn--u8j7t.jp", "\u304b\u30fd.jp", kUnsafe, "DISABLED"],
+ // Han + U+30FD
+ ["xn--xek368f.jp", "\u4e00\u30fd.jp", kUnsafe, "DISABLED"],
+ ["xn--a-mju.jp", "a\u30fd.jp", kUnsafe, "DISABLED"],
+ ["xn--a1-bo4a.jp", "a1\u30fd.jp", kUnsafe, "DISABLED"],
+
+ // U+30FE (ヾ) is allowed only after Katakana.
+ // Katakana + U+30FE
+ ["xn--lck4i.jp", "\u30ab\u30fe.jp", kSafe],
+ // Hiragana + U+30FE
+ ["xn--u8j9t.jp", "\u304b\u30fe.jp", kUnsafe, "DISABLED"],
+ // Han + U+30FE
+ ["xn--yek168f.jp", "\u4e00\u30fe.jp", kUnsafe, "DISABLED"],
+ ["xn--a-oju.jp", "a\u30fe.jp", kUnsafe, "DISABLED"],
+ ["xn--a1-eo4a.jp", "a1\u30fe.jp", kUnsafe, "DISABLED"],
+
+ // Cyrillic labels made of Latin-look-alike Cyrillic letters.
+ // 1) ѕсоре.com with ѕсоре in Cyrillic.
+ ["xn--e1argc3h.com", "\u0455\u0441\u043e\u0440\u0435.com", kUnsafe, "DISABLED"],
+ // 2) ѕсоре123.com with ѕсоре in Cyrillic.
+ ["xn--123-qdd8bmf3n.com", "\u0455\u0441\u043e\u0440\u0435123.com", kUnsafe, "DISABLED"],
+ // 3) ѕсоре-рау.com with ѕсоре and рау in Cyrillic.
+ ["xn----8sbn9akccw8m.com", "\u0455\u0441\u043e\u0440\u0435-\u0440\u0430\u0443.com", kUnsafe, "DISABLED"],
+ // 4) ѕсоре1рау.com with scope and pay in Cyrillic and a non-letter between
+ // them.
+ ["xn--1-8sbn9akccw8m.com", "\u0455\u0441\u043e\u0440\u0435\u0031\u0440\u0430\u0443.com", kUnsafe, "DISABLED"],
+
+ // The same as above three, but in IDN TLD (рф).
+ // 1) ѕсоре.рф with ѕсоре in Cyrillic.
+ ["xn--e1argc3h.xn--p1ai", "\u0455\u0441\u043e\u0440\u0435.\u0440\u0444", kSafe],
+ // 2) ѕсоре123.рф with ѕсоре in Cyrillic.
+ ["xn--123-qdd8bmf3n.xn--p1ai", "\u0455\u0441\u043e\u0440\u0435123.\u0440\u0444", kSafe],
+ // 3) ѕсоре-рау.рф with ѕсоре and рау in Cyrillic.
+ ["xn----8sbn9akccw8m.xn--p1ai", "\u0455\u0441\u043e\u0440\u0435-\u0440\u0430\u0443.\u0440\u0444", kSafe],
+ // 4) ѕсоре1рау.com with scope and pay in Cyrillic and a non-letter between
+ // them.
+ ["xn--1-8sbn9akccw8m.xn--p1ai", "\u0455\u0441\u043e\u0440\u0435\u0031\u0440\u0430\u0443.\u0440\u0444", kSafe],
+
+ // Same as above three, but in .ru TLD.
+ // 1) ѕсоре.ru with ѕсоре in Cyrillic.
+ ["xn--e1argc3h.r", "\u0455\u0441\u043e\u0440\u0435.r", kSafe],
+ // 2) ѕсоре123.ru with ѕсоре in Cyrillic.
+ ["xn--123-qdd8bmf3n.r", "\u0455\u0441\u043e\u0440\u0435123.r", kSafe],
+ // 3) ѕсоре-рау.ru with ѕсоре and рау in Cyrillic.
+ ["xn----8sbn9akccw8m.r", "\u0455\u0441\u043e\u0440\u0435-\u0440\u0430\u0443.r", kSafe],
+ // 4) ѕсоре1рау.com with scope and pay in Cyrillic and a non-letter between
+ // them.
+ ["xn--1-8sbn9akccw8m.r", "\u0455\u0441\u043e\u0440\u0435\u0031\u0440\u0430\u0443.r", kSafe],
+
+ // ѕсоре-рау.한국 with ѕсоре and рау in Cyrillic. The label will remain
+ // punycode while the TLD will be decoded.
+ ["xn----8sbn9akccw8m.xn--3e0b707e", "xn----8sbn9akccw8m.\ud55c\uad6d", kSafe, "DISABLED"],
+
+ // музей (museum in Russian) has characters without a Latin-look-alike.
+ ["xn--e1adhj9a.com", "\u043c\u0443\u0437\u0435\u0439.com", kSafe],
+
+ // ѕсоԗе.com is Cyrillic with Latin lookalikes.
+ ["xn--e1ari3f61c.com", "\u0455\u0441\u043e\u0517\u0435.com", kUnsafe, "DISABLED"],
+
+ // ыоԍ.com is Cyrillic with Latin lookalikes.
+ ["xn--n1az74c.com", "\u044b\u043e\u050d.com", kUnsafe],
+
+ // сю.com is Cyrillic with Latin lookalikes.
+ ["xn--q1a0a.com", "\u0441\u044e.com", kUnsafe, "DISABLED"],
+
+ // Regression test for lowercase letters in whole script confusable
+ // lookalike character lists.
+ ["xn--80a8a6a.com", "\u0430\u044c\u0441.com", kUnsafe, "DISABLED"],
+
+ // googlе.한국 where е is Cyrillic. This tests the generic case when one
+ // label is not allowed but other labels in the domain name are still
+ // decoded. Here, googlе is left in punycode but the TLD is decoded.
+ ["xn--googl-3we.xn--3e0b707e", "xn--googl-3we.\ud55c\uad6d", kSafe],
+
+ // Combining Diacritic marks after a script other than Latin-Greek-Cyrillic
+ ["xn--rsa2568fvxya.com", "\ud55c\u0307\uae00.com", kUnsafe, "DISABLED"], // 한́글.com
+ ["xn--rsa0336bjom.com", "\u6f22\u0307\u5b57.com", kUnsafe, "DISABLED"], // 漢̇字.com
+ // नागरी́.com
+ ["xn--lsa922apb7a6do.com", "\u0928\u093e\u0917\u0930\u0940\u0301.com", kUnsafe, "DISABLED"],
+
+ // Similarity checks against the list of top domains. "digklmo68.com" and
+ // 'digklmo68.co.uk" are listed for unittest in the top domain list.
+ // đigklmo68.com:
+ ["xn--igklmo68-kcb.com", "\u0111igklmo68.com", kUnsafe, "DISABLED"],
+ // www.đigklmo68.com:
+ ["www.xn--igklmo68-kcb.com", "www.\u0111igklmo68.com", kUnsafe, "DISABLED"],
+ // foo.bar.đigklmo68.com:
+ ["foo.bar.xn--igklmo68-kcb.com", "foo.bar.\u0111igklmo68.com", kUnsafe, "DISABLED"],
+ // đigklmo68.co.uk:
+ ["xn--igklmo68-kcb.co.uk", "\u0111igklmo68.co.uk", kUnsafe, "DISABLED"],
+ // mail.đigklmo68.co.uk:
+ ["mail.xn--igklmo68-kcb.co.uk", "mail.\u0111igklmo68.co.uk", kUnsafe, "DISABLED"],
+ // di̇gklmo68.com:
+ ["xn--digklmo68-6jf.com", "di\u0307gklmo68.com", kUnsafe],
+ // dig̱klmo68.com:
+ ["xn--digklmo68-7vf.com", "dig\u0331klmo68.com", kUnsafe, "DISABLED"],
+ // digĸlmo68.com:
+ ["xn--diglmo68-omb.com", "dig\u0138lmo68.com", kUnsafe],
+ // digkłmo68.com:
+ ["xn--digkmo68-9ob.com", "digk\u0142mo68.com", kUnsafe, "DISABLED"],
+ // digklṃo68.com:
+ ["xn--digklo68-l89c.com", "digkl\u1e43o68.com", kUnsafe, "DISABLED"],
+ // digklmø68.com:
+ ["xn--digklm68-b5a.com", "digklm\u00f868.com", kUnsafe, "DISABLED"],
+ // digklmoб8.com:
+ ["xn--digklmo8-h7g.com", "digklmo\u04318.com", kUnsafe],
+ // digklmo6৪.com:
+ ["xn--digklmo6-7yr.com", "digklmo6\u09ea.com", kUnsafe],
+
+ // 'islkpx123.com' is in the test domain list.
+ // 'іѕӏкрх123' can look like 'islkpx123' in some fonts.
+ ["xn--123-bed4a4a6hh40i.com", "\u0456\u0455\u04cf\u043a\u0440\u0445123.com", kUnsafe, "DISABLED"],
+
+ // 'o2.com', '28.com', '39.com', '43.com', '89.com', 'oo.com' and 'qq.com'
+ // are all explicitly added to the test domain list to aid testing of
+ // Latin-lookalikes that are numerics in other character sets and similar
+ // edge cases.
+ //
+ // Bengali:
+ ["xn--07be.com", "\u09e6\u09e8.com", kUnsafe, "DISABLED"],
+ ["xn--27be.com", "\u09e8\u09ea.com", kUnsafe, "DISABLED"],
+ ["xn--77ba.com", "\u09ed\u09ed.com", kUnsafe, "DISABLED"],
+ // Gurmukhi:
+ ["xn--qcce.com", "\u0a68\u0a6a.com", kUnsafe, "DISABLED"],
+ ["xn--occe.com", "\u0a66\u0a68.com", kUnsafe, "DISABLED"],
+ ["xn--rccd.com", "\u0a6b\u0a69.com", kUnsafe, "DISABLED"],
+ ["xn--pcca.com", "\u0a67\u0a67.com", kUnsafe, "DISABLED"],
+ // Telugu:
+ ["xn--drcb.com", "\u0c69\u0c68.com", kUnsafe, "DISABLED"],
+ // Devanagari:
+ ["xn--d4be.com", "\u0966\u0968.com", kUnsafe, "DISABLED"],
+ // Kannada:
+ ["xn--yucg.com", "\u0ce6\u0ce9.com", kUnsafe, "DISABLED"],
+ ["xn--yuco.com", "\u0ce6\u0ced.com", kUnsafe, "DISABLED"],
+ // Oriya:
+ ["xn--1jcf.com", "\u0b6b\u0b68.com", kUnsafe, "DISABLED"],
+ ["xn--zjca.com", "\u0b66\u0b66.com", kUnsafe, "DISABLED"],
+ // Gujarati:
+ ["xn--cgce.com", "\u0ae6\u0ae8.com", kUnsafe, "DISABLED"],
+ ["xn--fgci.com", "\u0ae9\u0aed.com", kUnsafe, "DISABLED"],
+ ["xn--dgca.com", "\u0ae7\u0ae7.com", kUnsafe, "DISABLED"],
+
+ // wmhtb.com
+ ["xn--l1acpvx.com", "\u0448\u043c\u043d\u0442\u044c.com", kUnsafe, "DISABLED"],
+ // щмнть.com
+ ["xn--l1acpzs.com", "\u0449\u043c\u043d\u0442\u044c.com", kUnsafe, "DISABLED"],
+ // шмнтв.com
+ ["xn--b1atdu1a.com", "\u0448\u043c\u043d\u0442\u0432.com", kUnsafe, "DISABLED"],
+ // шмԋтв.com
+ ["xn--b1atsw09g.com", "\u0448\u043c\u050b\u0442\u0432.com", kUnsafe],
+ // шмԧтв.com
+ ["xn--b1atsw03i.com", "\u0448\u043c\u0527\u0442\u0432.com", kUnsafe, "DISABLED"],
+ // шмԋԏв.com
+ ["xn--b1at9a12dua.com", "\u0448\u043c\u050b\u050f\u0432.com", kUnsafe],
+ // ഠട345.com
+ ["xn--345-jtke.com", "\u0d20\u0d1f345.com", kUnsafe, "DISABLED"],
+
+ // Test additional confusable LGC characters (most of them without
+ // decomposition into base + diacritc mark). The corresponding ASCII
+ // domain names are in the test top domain list.
+ // ϼκαωχ.com
+ ["xn--mxar4bh6w.com", "\u03fc\u03ba\u03b1\u03c9\u03c7.com", kUnsafe, "DISABLED"],
+ // þħĸŧƅ.com
+ ["xn--vda6f3b2kpf.com", "\u00fe\u0127\u0138\u0167\u0185.com", kUnsafe],
+ // þhktb.com
+ ["xn--hktb-9ra.com", "\u00fehktb.com", kUnsafe, "DISABLED"],
+ // pħktb.com
+ ["xn--pktb-5xa.com", "p\u0127ktb.com", kUnsafe, "DISABLED"],
+ // phĸtb.com
+ ["xn--phtb-m0a.com", "ph\u0138tb.com", kUnsafe],
+ // phkŧb.com
+ ["xn--phkb-d7a.com", "phk\u0167b.com", kUnsafe, "DISABLED"],
+ // phktƅ.com
+ ["xn--phkt-ocb.com", "phkt\u0185.com", kUnsafe],
+ // ҏнкть.com
+ ["xn--j1afq4bxw.com", "\u048f\u043d\u043a\u0442\u044c.com", kUnsafe, "DISABLED"],
+ // ҏћкть.com
+ ["xn--j1aq4a7cvo.com", "\u048f\u045b\u043a\u0442\u044c.com", kUnsafe, "DISABLED"],
+ // ҏңкть.com
+ ["xn--j1aq4azund.com", "\u048f\u04a3\u043a\u0442\u044c.com", kUnsafe, "DISABLED"],
+ // ҏҥкть.com
+ ["xn--j1aq4azuxd.com", "\u048f\u04a5\u043a\u0442\u044c.com", kUnsafe, "DISABLED"],
+ // ҏӈкть.com
+ ["xn--j1aq4azuyj.com", "\u048f\u04c8\u043a\u0442\u044c.com", kUnsafe, "DISABLED"],
+ // ҏԧкть.com
+ ["xn--j1aq4azu9z.com", "\u048f\u0527\u043a\u0442\u044c.com", kUnsafe, "DISABLED"],
+ // ҏԩкть.com
+ ["xn--j1aq4azuq0a.com", "\u048f\u0529\u043a\u0442\u044c.com", kUnsafe, "DISABLED"],
+ // ҏнқть.com
+ ["xn--m1ak4azu6b.com", "\u048f\u043d\u049b\u0442\u044c.com", kUnsafe, "DISABLED"],
+ // ҏнҝть.com
+ ["xn--m1ak4azunc.com", "\u048f\u043d\u049d\u0442\u044c.com", kUnsafe, "DISABLED"],
+ // ҏнҟть.com
+ ["xn--m1ak4azuxc.com", "\u048f\u043d\u049f\u0442\u044c.com", kUnsafe, "DISABLED"],
+ // ҏнҡть.com
+ ["xn--m1ak4azu7c.com", "\u048f\u043d\u04a1\u0442\u044c.com", kUnsafe, "DISABLED"],
+ // ҏнӄть.com
+ ["xn--m1ak4azu8i.com", "\u048f\u043d\u04c4\u0442\u044c.com", kUnsafe, "DISABLED"],
+ // ҏнԟть.com
+ ["xn--m1ak4azuzy.com", "\u048f\u043d\u051f\u0442\u044c.com", kUnsafe, "DISABLED"],
+ // ҏнԟҭь.com
+ ["xn--m1a4a4nnery.com", "\u048f\u043d\u051f\u04ad\u044c.com", kUnsafe, "DISABLED"],
+ // ҏнԟҭҍ.com
+ ["xn--m1a4ne5jry.com", "\u048f\u043d\u051f\u04ad\u048d.com", kUnsafe, "DISABLED"],
+ // ҏнԟҭв.com
+ ["xn--b1av9v8dry.com", "\u048f\u043d\u051f\u04ad\u0432.com", kUnsafe, "DISABLED"],
+ // ҏӊԟҭв.com
+ ["xn--b1a9p8c1e8r.com", "\u048f\u04ca\u051f\u04ad\u0432.com", kUnsafe, "DISABLED"],
+ // wmŋr.com
+ ["xn--wmr-jxa.com", "wm\u014br.com", kUnsafe, "DISABLED"],
+ // шмпґ.com
+ ["xn--l1agz80a.com", "\u0448\u043c\u043f\u0491.com", kUnsafe, "DISABLED"],
+ // щмпґ.com
+ ["xn--l1ag2a0y.com", "\u0449\u043c\u043f\u0491.com", kUnsafe, "DISABLED"],
+ // щӎпґ.com
+ ["xn--o1at1tsi.com", "\u0449\u04ce\u043f\u0491.com", kUnsafe, "DISABLED"],
+ // ґғ.com
+ ["xn--03ae.com", "\u0491\u0493.com", kUnsafe, "DISABLED"],
+ // ґӻ.com
+ ["xn--03a6s.com", "\u0491\u04fb.com", kUnsafe, "DISABLED"],
+ // ҫұҳҽ.com
+ ["xn--r4amg4b.com", "\u04ab\u04b1\u04b3\u04bd.com", kUnsafe, "DISABLED"],
+ // ҫұӽҽ.com
+ ["xn--r4am0b8r.com", "\u04ab\u04b1\u04fd\u04bd.com", kUnsafe, "DISABLED"],
+ // ҫұӿҽ.com
+ ["xn--r4am0b3s.com", "\u04ab\u04b1\u04ff\u04bd.com", kUnsafe, "DISABLED"],
+ // ҫұӿҿ.com
+ ["xn--r4am6b4p.com", "\u04ab\u04b1\u04ff\u04bf.com", kUnsafe, "DISABLED"],
+ // ҫұӿє.com
+ ["xn--91a7osa62a.com", "\u04ab\u04b1\u04ff\u0454.com", kUnsafe, "DISABLED"],
+ // ӏԃԍ.com
+ ["xn--s5a8h4a.com", "\u04cf\u0503\u050d.com", kUnsafe],
+
+ // U+04CF(ӏ) is mapped to multiple characters, lowercase L(l) and
+ // lowercase I(i). Lowercase L is also regarded as similar to digit 1.
+ // The test domain list has {ig, ld, 1gd}.com for Cyrillic.
+ // ӏԍ.com
+ ["xn--s5a8j.com", "\u04cf\u050d.com", kUnsafe],
+ // ӏԃ.com
+ ["xn--s5a8h.com", "\u04cf\u0503.com", kUnsafe],
+ // ӏԍԃ.com
+ ["xn--s5a8h3a.com", "\u04cf\u050d\u0503.com", kUnsafe],
+
+ // 1շ34567890.com
+ ["xn--134567890-gnk.com", "1\u057734567890.com", kUnsafe, "DISABLED"],
+ // ꓲ2345б7890.com
+ ["xn--23457890-e7g93622b.com", "\ua4f22345\u04317890.com", kUnsafe],
+ // 1ᒿ345б7890.com
+ ["xn--13457890-e7g0943b.com", "1\u14bf345\u04317890.com", kUnsafe],
+ // 12з4567890.com
+ ["xn--124567890-10h.com", "12\u04374567890.com", kUnsafe, "DISABLED"],
+ // 12ҙ4567890.com
+ ["xn--124567890-1ti.com", "12\u04994567890.com", kUnsafe, "DISABLED"],
+ // 12ӡ4567890.com
+ ["xn--124567890-mfj.com", "12\u04e14567890.com", kUnsafe, "DISABLED"],
+ // 12उ4567890.com
+ ["xn--124567890-m3r.com", "12\u09094567890.com", kUnsafe, "DISABLED"],
+ // 12ও4567890.com
+ ["xn--124567890-17s.com", "12\u09934567890.com", kUnsafe, "DISABLED"],
+ // 12ਤ4567890.com
+ ["xn--124567890-hfu.com", "12\u0a244567890.com", kUnsafe, "DISABLED"],
+ // 12ဒ4567890.com
+ ["xn--124567890-6s6a.com", "12\u10124567890.com", kUnsafe, "DISABLED"],
+ // 12ვ4567890.com
+ ["xn--124567890-we8a.com", "12\u10D54567890.com", kUnsafe, "DISABLED"],
+ // 12პ4567890.com
+ ["xn--124567890-hh8a.com", "12\u10DE4567890.com", kUnsafe, "DISABLED"],
+ // 123ㄐ567890.com
+ ["xn--123567890-dr5h.com", "123ㄐ567890.com", kUnsafe, "DISABLED"],
+ // 123Ꮞ567890.com
+ ["xn--123567890-dm4b.com", "123\u13ce567890.com", kUnsafe],
+ // 12345б7890.com
+ ["xn--123457890-fzh.com", "12345\u04317890.com", kUnsafe, "DISABLED"],
+ // 12345ճ7890.com
+ ["xn--123457890-fmk.com", "12345ճ7890.com", kUnsafe, "DISABLED"],
+ // 1234567ȣ90.com
+ ["xn--123456790-6od.com", "1234567\u022390.com", kUnsafe],
+ // 12345678୨0.com
+ ["xn--123456780-71w.com", "12345678\u0b680.com", kUnsafe],
+ // 123456789ଠ.com
+ ["xn--123456789-ohw.com", "123456789\u0b20.com", kUnsafe, "DISABLED"],
+ // 123456789ꓳ.com
+ ["xn--123456789-tx75a.com", "123456789\ua4f3.com", kUnsafe],
+
+ // aeœ.com
+ ["xn--ae-fsa.com", "ae\u0153.com", kUnsafe, "DISABLED"],
+ // æce.com
+ ["xn--ce-0ia.com", "\u00e6ce.com", kUnsafe, "DISABLED"],
+ // æœ.com
+ ["xn--6ca2t.com", "\u00e6\u0153.com", kUnsafe, "DISABLED"],
+ // ӕԥ.com
+ ["xn--y5a4n.com", "\u04d5\u0525.com", kUnsafe, "DISABLED"],
+
+ // ငၔဌ၂ဝ.com (entirely made of Myanmar characters)
+ ["xn--ridq5c9hnd.com", "\u1004\u1054\u100c\u1042\u101d.com", kUnsafe, "DISABLED"],
+
+ // ฟรฟร.com (made of two Thai characters. similar to wsws.com in
+ // some fonts)
+ ["xn--w3calb.com", "\u0e1f\u0e23\u0e1f\u0e23.com", kUnsafe, "DISABLED"],
+ // พรบ.com
+ ["xn--r3chp.com", "\u0e1e\u0e23\u0e1a.com", kUnsafe, "DISABLED"],
+ // ฟรบ.com
+ ["xn--r3cjm.com", "\u0e1f\u0e23\u0e1a.com", kUnsafe, "DISABLED"],
+
+ // Lao characters that look like w, s, o, and u.
+ // ພຣບ.com
+ ["xn--f7chp.com", "\u0e9e\u0ea3\u0e9a.com", kUnsafe, "DISABLED"],
+ // ຟຣບ.com
+ ["xn--f7cjm.com", "\u0e9f\u0ea3\u0e9a.com", kUnsafe, "DISABLED"],
+ // ຟຮບ.com
+ ["xn--f7cj9b.com", "\u0e9f\u0eae\u0e9a.com", kUnsafe, "DISABLED"],
+ // ຟຮ໐ບ.com
+ ["xn--f7cj9b5h.com", "\u0e9f\u0eae\u0ed0\u0e9a.com", kUnsafe, "DISABLED"],
+
+ // Lao character that looks like n.
+ // ก11.com
+ ["xn--11-lqi.com", "\u0e0111.com", kUnsafe, "DISABLED"],
+
+ // At one point the skeleton of 'w' was 'vv', ensure that
+ // that it's treated as 'w'.
+ ["xn--wder-qqa.com", "w\u00f3der.com", kUnsafe, "DISABLED"],
+
+ // Mixed digits: the first two will also fail mixed script test
+ // Latin + ASCII digit + Deva digit
+ ["xn--asc1deva-j0q.co.in", "asc1deva\u0967.co.in", kUnsafe],
+ // Latin + Deva digit + Beng digit
+ ["xn--devabeng-f0qu3f.co.in", "deva\u0967beng\u09e7.co.in", kUnsafe],
+ // ASCII digit + Deva digit
+ ["xn--79-v5f.co.in", "7\u09ea9.co.in", kUnsafe],
+ // Deva digit + Beng digit
+ ["xn--e4b0x.co.in", "\u0967\u09e7.co.in", kUnsafe],
+ // U+4E00 (CJK Ideograph One) is not a digit, but it's not allowed next to
+ // non-Kana scripts including numbers.
+ ["xn--d12-s18d.cn", "d12\u4e00.cn", kUnsafe, "DISABLED"],
+ // One that's really long that will force a buffer realloc
+ ["aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", kSafe],
+
+ // Not allowed; characters outside [:Identifier_Status=Allowed:]
+ // Limited Use Scripts: UTS 31 Table 7.
+ // Vai
+ ["xn--sn8a.com", "\ua50b.com", kUnsafe],
+ // 'CARD' look-alike in Cherokee
+ ["xn--58db0a9q.com", "\u13df\u13aa\u13a1\u13a0.com", kUnsafe],
+ // Scripts excluded from Identifiers: UTS 31 Table 4
+ // Coptic
+ ["xn--5ya.com", "\u03e7.com", kUnsafe],
+ // Old Italic
+ ["xn--097cc.com", "\U00010300\U00010301.com", kUnsafe],
+
+ // U+115F (Hangul Filler)
+ ["xn--osd3820f24c.kr", "\uac00\ub098\u115f.kr", kInvalid],
+ ["www.xn--google-ho0coa.com", "www.\u2039google\u203a.com", kUnsafe],
+ // Latin small capital w: hardᴡare.com
+ ["xn--hardare-l41c.com", "hard\u1d21are.com", kUnsafe],
+ // Minus Sign(U+2212)
+ ["xn--t9g238xc2a.jp", "\u65e5\u2212\u672c.jp", kUnsafe],
+ // Latin Small Letter Script G: ɡɡ.com
+ ["xn--0naa.com", "\u0261\u0261.com", kUnsafe],
+ // Hangul Jamo(U+11xx)
+ ["xn--0pdc3b.com", "\u1102\u1103\u1110.com", kUnsafe],
+ // degree sign: 36°c.com
+ ["xn--36c-tfa.com", "36\u00b0c.com", kUnsafe],
+ // Pound sign
+ ["xn--5free-fga.com", "5free\u00a3.com", kUnsafe],
+ // Hebrew points (U+05B0, U+05B6)
+ ["xn--7cbl2kc2a.com", "\u05e1\u05b6\u05e7\u05b0\u05e1.com", kUnsafe],
+ // Danda(U+0964)
+ ["xn--81bp1b6ch8s.com", "\u0924\u093f\u091c\u0964\u0930\u0940.com", kUnsafe],
+ // Small letter script G(U+0261)
+ ["xn--oogle-qmc.com", "\u0261oogle.com", kUnsafe],
+ // Small Katakana Extension(U+31F1)
+ ["xn--wlk.com", "\u31f1.com", kUnsafe],
+ // Heart symbol: ♥
+ ["xn--ab-u0x.com", "ab\u2665.com", kUnsafe],
+ // Emoji
+ ["xn--vi8hiv.xyz", "\U0001f355\U0001f4a9.xyz", kUnsafe],
+ // Registered trade mark
+ ["xn--egistered-fna.com", "\u00aeegistered.com", kUnsafe],
+ // Latin Letter Retroflex Click
+ ["xn--registered-25c.com", "registered\u01c3.com", kUnsafe],
+ // ASCII '!' not allowed in IDN
+ ["xn--!-257eu42c.kr", "\uc548\ub155!.kr", kUnsafe],
+ // 'GOOGLE' in IPA extension: ɢᴏᴏɢʟᴇ
+ ["xn--1naa7pn51hcbaa.com", "\u0262\u1d0f\u1d0f\u0262\u029f\u1d07.com", kUnsafe],
+ // Padlock icon spoof.
+ ["xn--google-hj64e.com", "\U0001f512google.com", kUnsafe],
+
+ // Custom block list
+ // Combining Long Solidus Overlay
+ ["google.xn--comabc-k8d", "google.com\u0338abc", kUnsafe],
+ // Hyphenation Point instead of Katakana Middle dot
+ ["xn--svgy16dha.jp", "\u30a1\u2027\u30a3.jp", kUnsafe],
+ // Gershayim with other Hebrew characters is allowed.
+ ["xn--5db6bh9b.il", "\u05e9\u05d1\u05f4\u05e6.il", kSafe, "DISABLED"],
+ // Hebrew Gershayim with Latin is invalid according to Python's idna
+ // package.
+ ["xn--ab-yod.com", "a\u05f4b.com", kInvalid],
+ // Hebrew Gershayim with Arabic is disallowed.
+ ["xn--5eb7h.eg", "\u0628\u05f4.eg", kUnsafe],
+// #if BUILDFLAG(IS_APPLE)
+ // These characters are blocked due to a font issue on Mac.
+ // Tibetan transliteration characters.
+ ["xn--com-lum.test.pl", "com\u0f8c.test.pl", kUnsafe],
+ // Arabic letter KASHMIRI YEH
+ ["xn--fgb.com", "\u0620.com", kUnsafe, "DISABLED"],
+// #endif
+
+ // Hyphens (http://unicode.org/cldr/utility/confusables.jsp?a=-)
+ // Hyphen-Minus (the only hyphen allowed)
+ // abc-def
+ ["abc-def.com", "abc-def.com", kSafe],
+ // Modifier Letter Minus Sign
+ ["xn--abcdef-5od.com", "abc\u02d7def.com", kUnsafe],
+ // Hyphen
+ ["xn--abcdef-dg0c.com", "abc\u2010def.com", kUnsafe],
+ // Non-Breaking Hyphen
+ // This is actually an invalid IDNA domain (U+2011 normalizes to U+2010),
+ // but it is included to ensure that we do not inadvertently allow this
+ // character to be displayed as Unicode.
+ ["xn--abcdef-kg0c.com", "abc\u2011def.com", kInvalid],
+ // Figure Dash.
+ // Python's idna package refuses to decode the minus signs and dashes. ICU
+ // decodes them but treats them as unsafe in spoof checks, so these test
+ // cases are marked as unsafe instead of invalid.
+ ["xn--abcdef-rg0c.com", "abc\u2012def.com", kUnsafe],
+ // En Dash
+ ["xn--abcdef-yg0c.com", "abc\u2013def.com", kUnsafe],
+ // Hyphen Bullet
+ ["xn--abcdef-kq0c.com", "abc\u2043def.com", kUnsafe],
+ // Minus Sign
+ ["xn--abcdef-5d3c.com", "abc\u2212def.com", kUnsafe],
+ // Heavy Minus Sign
+ ["xn--abcdef-kg1d.com", "abc\u2796def.com", kUnsafe],
+ // Em Dash
+ // Small Em Dash (U+FE58) is normalized to Em Dash.
+ ["xn--abcdef-5g0c.com", "abc\u2014def.com", kUnsafe],
+ // Coptic Small Letter Dialect-P Ni. Looks like dash.
+ // Coptic Capital Letter Dialect-P Ni is normalized to small letter.
+ ["xn--abcdef-yy8d.com", "abc\u2cbbdef.com", kUnsafe],
+
+ // Block NV8 (Not valid in IDN 2008) characters.
+ // U+058A (֊)
+ ["xn--ab-vfd.com", "a\u058ab.com", kUnsafe],
+ ["xn--y9ac3j.com", "\u0561\u058a\u0562.com", kUnsafe],
+ // U+2019 (’)
+ ["xn--ab-n2t.com", "a\u2019b.com", kUnsafe],
+ // U+2027 (‧)
+ ["xn--ab-u3t.com", "a\u2027b.com", kUnsafe],
+ // U+30A0 (゠)
+ ["xn--ab-bg4a.com", "a\u30a0b.com", kUnsafe],
+ ["xn--9bk3828aea.com", "\uac00\u30a0\uac01.com", kUnsafe],
+ ["xn--9bk279fba.com", "\u4e00\u30a0\u4e00.com", kUnsafe],
+ ["xn--n8jl2x.com", "\u304a\u30a0\u3044.com", kUnsafe],
+ ["xn--fbke7f.com", "\u3082\u30a0\u3084.com", kUnsafe],
+
+ // Block single/double-quote-like characters.
+ // U+02BB (ʻ)
+ ["xn--ab-8nb.com", "a\u02bbb.com", kUnsafe, "DISABLED"],
+ // U+02BC (ʼ)
+ ["xn--ab-cob.com", "a\u02bcb.com", kUnsafe, "DISABLED"],
+ // U+144A: Not allowed to mix with scripts other than Canadian Syllabics.
+ ["xn--ab-jom.com", "a\u144ab.com", kUnsafe],
+ ["xn--xcec9s.com", "\u1401\u144a\u1402.com", kUnsafe],
+
+ // Custom dangerous patterns
+ // Two Katakana-Hiragana combining mark in a row
+ ["google.xn--com-oh4ba.evil.jp", "google.com\u309a\u309a.evil.jp", kUnsafe],
+ // Katakana Letter No not enclosed by {Han,Hiragana,Katakana}.
+ ["google.xn--comevil-v04f.jp", "google.com\u30ceevil.jp", kUnsafe, "DISABLED"],
+ // TODO(jshin): Review the danger of allowing the following two.
+ // Hiragana 'No' by itself is allowed.
+ ["xn--ldk.jp", "\u30ce.jp", kSafe],
+ // Hebrew Gershayim used by itself is allowed.
+ ["xn--5eb.il", "\u05f4.il", kSafe, "DISABLED"],
+
+ // Block RTL nonspacing marks (NSM) after unrelated scripts.
+ ["xn--foog-ycg.com", "foog\u0650.com", kUnsafe], // Latin + Arabic N]M
+ ["xn--foog-jdg.com", "foog\u0654.com", kUnsafe], // Latin + Arabic N]M
+ ["xn--foog-jhg.com", "foog\u0670.com", kUnsafe], // Latin + Arbic N]M
+ ["xn--foog-opf.com", "foog\u05b4.com", kUnsafe], // Latin + Hebrew N]M
+ ["xn--shb5495f.com", "\uac00\u0650.com", kUnsafe], // Hang + Arabic N]M
+
+ // 4 Deviation characters between IDNA 2003 and IDNA 2008
+ // When entered in Unicode, the first two are mapped to 'ss' and Greek sigma
+ // and the latter two are mapped away. However, the punycode form should
+ // remain in punycode.
+ // U+00DF(sharp-s)
+ ["xn--fu-hia.de", "fu\u00df.de", kUnsafe, "DISABLED"],
+ // U+03C2(final-sigma)
+ ["xn--mxac2c.gr", "\u03b1\u03b2\u03c2.gr", kUnsafe, "DISABLED"],
+ // U+200C(ZWNJ)
+ ["xn--h2by8byc123p.in", "\u0924\u094d\u200c\u0930\u093f.in", kUnsafe],
+ // U+200C(ZWJ)
+ ["xn--11b6iy14e.in", "\u0915\u094d\u200d.in", kUnsafe],
+
+ // Math Monospace Small A. When entered in Unicode, it's canonicalized to
+ // 'a'. The punycode form should remain in punycode.
+ ["xn--bc-9x80a.xyz", "\U0001d68abc.xyz", kInvalid],
+ // Math Sans Bold Capital Alpha
+ ["xn--bc-rg90a.xyz", "\U0001d756bc.xyz", kInvalid],
+ // U+3000 is canonicalized to a space(U+0020), but the punycode form
+ // should remain in punycode.
+ ["xn--p6j412gn7f.cn", "\u4e2d\u56fd\u3000", kInvalid],
+ // U+3002 is canonicalized to ASCII fullstop(U+002E), but the punycode form
+ // should remain in punycode.
+ ["xn--r6j012gn7f.cn", "\u4e2d\u56fd\u3002", kInvalid],
+ // Invalid punycode
+ // Has a codepoint beyond U+10FFFF.
+ ["xn--krank-kg706554a", "", kInvalid],
+ // '?' in punycode.
+ ["xn--hello?world.com", "", kInvalid],
+
+ // Not allowed in UTS46/IDNA 2008
+ // Georgian Capital Letter(U+10BD)
+ ["xn--1nd.com", "\u10bd.com", kInvalid],
+ // 3rd and 4th characters are '-'.
+ ["xn-----8kci4dhsd", "\u0440\u0443--\u0430\u0432\u0442\u043e", kInvalid],
+ // Leading combining mark
+ ["xn--72b.com", "\u093e.com", kInvalid],
+ // BiDi check per IDNA 2008/UTS 46
+ // Cannot starts with AN(Arabic-Indic Number)
+ ["xn--8hbae.eg", "\u0662\u0660\u0660.eg", kInvalid],
+ // Cannot start with a RTL character and ends with a LTR
+ ["xn--x-ymcov.eg", "\u062c\u0627\u0631x.eg", kInvalid],
+ // Can start with a RTL character and ends with EN(European Number)
+ ["xn--2-ymcov.eg", "\u062c\u0627\u06312.eg", kSafe],
+ // Can start with a RTL and end with AN
+ ["xn--mgbjq0r.eg", "\u062c\u0627\u0631\u0662.eg", kSafe],
+
+ // Extremely rare Latin letters
+ // Latin Ext B - Pinyin: ǔnion.com
+ ["xn--nion-unb.com", "\u01d4nion.com", kUnsafe, "DISABLED"],
+ // Latin Ext C: ⱴase.com
+ ["xn--ase-7z0b.com", "\u2c74ase.com", kUnsafe],
+ // Latin Ext D: ꝴode.com
+ ["xn--ode-ut3l.com", "\ua774ode.com", kUnsafe],
+ // Latin Ext Additional: ḷily.com
+ ["xn--ily-n3y.com", "\u1e37ily.com", kUnsafe, "DISABLED"],
+ // Latin Ext E: ꬺove.com
+ ["xn--ove-8y6l.com", "\uab3aove.com", kUnsafe],
+ // Greek Ext: ᾳβγ.com
+ ["xn--nxac616s.com", "\u1fb3\u03b2\u03b3.com", kInvalid],
+ // Cyrillic Ext A (label cannot begin with an illegal combining character).
+ ["xn--lrj.com", "\u2def.com", kInvalid],
+ // Cyrillic Ext B: ꙡ.com
+ ["xn--kx8a.com", "\ua661.com", kUnsafe],
+ // Cyrillic Ext C: ᲂ.com (Narrow o)
+ ["xn--43f.com", "\u1c82.com", kInvalid],
+
+ // The skeleton of Extended Arabic-Indic Digit Zero (۰) is a dot. Check that
+ // this is handled correctly (crbug/877045).
+ ["xn--dmb", "\u06f0", kSafe],
+
+ // Test that top domains whose skeletons are the same as the domain name are
+ // handled properly. In this case, tést.net should match test.net top
+ // domain and not be converted to unicode.
+ ["xn--tst-bma.net", "t\u00e9st.net", kUnsafe, "DISABLED"],
+ // Variations of the above, for testing crbug.com/925199.
+ // some.tést.net should match test.net.
+ ["some.xn--tst-bma.net", "some.t\u00e9st.net", kUnsafe, "DISABLED"],
+ // The following should not match test.net, so should be converted to
+ // unicode.
+ // ést.net (a suffix of tést.net).
+ ["xn--st-9ia.net", "\u00e9st.net", kSafe],
+ // some.ést.net
+ ["some.xn--st-9ia.net", "some.\u00e9st.net", kSafe],
+ // atést.net (tést.net is a suffix of atést.net)
+ ["xn--atst-cpa.net", "at\u00e9st.net", kSafe],
+ // some.atést.net
+ ["some.xn--atst-cpa.net", "some.at\u00e9st.net", kSafe],
+
+ // Modifier-letter-voicing should be blocked (wwwˬtest.com).
+ ["xn--wwwtest-2be.com", "www\u02ectest.com", kUnsafe, "DISABLED"],
+
+ // oĸ.com: Not a top domain, should be blocked because of Kra.
+ ["xn--o-tka.com", "o\u0138.com", kUnsafe],
+
+ // U+4E00 and U+3127 should be blocked when next to non-CJK.
+ ["xn--ipaddress-w75n.com", "ip\u4e00address.com", kUnsafe, "DISABLED"],
+ ["xn--ipaddress-wx5h.com", "ip\u3127address.com", kUnsafe, "DISABLED"],
+ // U+4E00 and U+3127 at the beginning and end of a string.
+ ["xn--google-gg5e.com", "google\u3127.com", kUnsafe, "DISABLED"],
+ ["xn--google-9f5e.com", "\u3127google.com", kUnsafe, "DISABLED"],
+ ["xn--google-gn7i.com", "google\u4e00.com", kUnsafe, "DISABLED"],
+ ["xn--google-9m7i.com", "\u4e00google.com", kUnsafe, "DISABLED"],
+ // These are allowed because U+4E00 and U+3127 are not immediately next to
+ // non-CJK.
+ ["xn--gamer-fg1hz05u.com", "\u4e00\u751fgamer.com", kSafe],
+ ["xn--gamer-kg1hy05u.com", "gamer\u751f\u4e00.com", kSafe],
+ ["xn--gamer-f94d4426b.com", "\u3127\u751fgamer.com", kSafe],
+ ["xn--gamer-k94d3426b.com", "gamer\u751f\u3127.com", kSafe],
+ ["xn--4gqz91g.com", "\u4e00\u732b.com", kSafe],
+ ["xn--4fkv10r.com", "\u3127\u732b.com", kSafe],
+ // U+4E00 with another ideograph.
+ ["xn--4gqc.com", "\u4e00\u4e01.com", kSafe],
+
+ // CJK ideographs looking like slashes should be blocked when next to
+ // non-CJK.
+ ["example.xn--comtest-k63k", "example.com\u4e36test", kUnsafe, "DISABLED"],
+ ["example.xn--comtest-u83k", "example.com\u4e40test", kUnsafe, "DISABLED"],
+ ["example.xn--comtest-283k", "example.com\u4e41test", kUnsafe, "DISABLED"],
+ ["example.xn--comtest-m83k", "example.com\u4e3ftest", kUnsafe, "DISABLED"],
+ // This is allowed because the ideographs are not immediately next to
+ // non-CJK.
+ ["xn--oiqsace.com", "\u4e36\u4e40\u4e41\u4e3f.com", kSafe],
+
+ // Kana voiced sound marks are not allowed.
+ ["xn--google-1m4e.com", "google\u3099.com", kUnsafe],
+ ["xn--google-8m4e.com", "google\u309A.com", kUnsafe],
+
+ // Small letter theta looks like a zero.
+ ["xn--123456789-yzg.com", "123456789\u03b8.com", kUnsafe, "DISABLED"],
+
+ ["xn--est-118d.net", "\u4e03est.net", kUnsafe, "DISABLED"],
+ ["xn--est-918d.net", "\u4e05est.net", kUnsafe, "DISABLED"],
+ ["xn--est-e28d.net", "\u4e06est.net", kUnsafe, "DISABLED"],
+ ["xn--est-t18d.net", "\u4e01est.net", kUnsafe, "DISABLED"],
+ ["xn--3-cq6a.com", "\u4e293.com", kUnsafe, "DISABLED"],
+ ["xn--cxe-n68d.com", "c\u4e2bxe.com", kUnsafe, "DISABLED"],
+ ["xn--cye-b98d.com", "cy\u4e42e.com", kUnsafe, "DISABLED"],
+
+ // U+05D7 can look like Latin n in many fonts.
+ ["xn--ceba.com", "\u05d7\u05d7.com", kUnsafe, "DISABLED"],
+
+ // U+00FE (þ) and U+00F0 (ð) are only allowed under the .is TLD.
+ ["xn--acdef-wva.com", "a\u00fecdef.com", kUnsafe, "DISABLED"],
+ ["xn--mnpqr-jta.com", "mn\u00f0pqr.com", kUnsafe, "DISABLED"],
+ ["xn--acdef-wva.is", "a\u00fecdef.is", kSafe],
+ ["xn--mnpqr-jta.is", "mn\u00f0pqr.is", kSafe],
+
+ // U+0259 (ə) is only allowed under the .az TLD.
+ ["xn--xample-vyc.com", "\u0259xample.com", kUnsafe, "DISABLED"],
+ ["xn--xample-vyc.az", "\u0259xample.az", kSafe],
+
+ // U+00B7 is only allowed on Catalan domains between two l's.
+ ["xn--googlecom-5pa.com", "google\u00b7com.com", kUnsafe, "DISABLED"],
+ ["xn--ll-0ea.com", "l\u00b7l.com", kUnsafe, "DISABLED"],
+ ["xn--ll-0ea.cat", "l\u00b7l.cat", kSafe],
+ ["xn--al-0ea.cat", "a\u00b7l.cat", kUnsafe, "DISABLED"],
+ ["xn--la-0ea.cat", "l\u00b7a.cat", kUnsafe, "DISABLED"],
+ ["xn--l-fda.cat", "\u00b7l.cat", kUnsafe, "DISABLED"],
+ ["xn--l-gda.cat", "l\u00b7.cat", kUnsafe, "DISABLED"],
+
+ ["xn--googlecom-gk6n.com", "google\u4e28com.com", kUnsafe, "DISABLED"],
+ ["xn--googlecom-0y6n.com", "google\u4e5bcom.com", kUnsafe, "DISABLED"],
+ ["xn--googlecom-v85n.com", "google\u4e03com.com", kUnsafe, "DISABLED"],
+ ["xn--googlecom-g95n.com", "google\u4e05com.com", kUnsafe, "DISABLED"],
+ ["xn--googlecom-go6n.com", "google\u4e36com.com", kUnsafe, "DISABLED"],
+ ["xn--googlecom-b76o.com", "google\u5341com.com", kUnsafe, "DISABLED"],
+ ["xn--googlecom-ql3h.com", "google\u3007com.com", kUnsafe, "DISABLED"],
+ ["xn--googlecom-0r5h.com", "google\u3112com.com", kUnsafe, "DISABLED"],
+ ["xn--googlecom-bu5h.com", "google\u311acom.com", kUnsafe, "DISABLED"],
+ ["xn--googlecom-qv5h.com", "google\u311fcom.com", kUnsafe, "DISABLED"],
+ ["xn--googlecom-0x5h.com", "google\u3127com.com", kUnsafe, "DISABLED"],
+ ["xn--googlecom-by5h.com", "google\u3128com.com", kUnsafe, "DISABLED"],
+ ["xn--googlecom-ly5h.com", "google\u3129com.com", kUnsafe, "DISABLED"],
+ ["xn--googlecom-5o5h.com", "google\u3108com.com", kUnsafe, "DISABLED"],
+ ["xn--googlecom-075n.com", "google\u4e00com.com", kUnsafe, "DISABLED"],
+ ["xn--googlecom-046h.com", "google\u31bacom.com", kUnsafe, "DISABLED"],
+ ["xn--googlecom-026h.com", "google\u31b3com.com", kUnsafe, "DISABLED"],
+ ["xn--googlecom-lg9q.com", "google\u5de5com.com", kUnsafe, "DISABLED"],
+ ["xn--googlecom-g040a.com", "google\u8ba0com.com", kUnsafe, "DISABLED"],
+ ["xn--googlecom-b85n.com", "google\u4e01com.com", kUnsafe, "DISABLED"],
+
+ // Whole-script-confusables. Cyrillic is sufficiently handled in cases above
+ // so it's not included here.
+ // Armenian:
+ ["xn--mbbkpm.com", "\u0578\u057d\u0582\u0585.com", kUnsafe, "DISABLED"],
+ ["xn--mbbkpm.am", "\u0578\u057d\u0582\u0585.am", kSafe],
+ ["xn--mbbkpm.xn--y9a3aq", "\u0578\u057d\u0582\u0585.\u0570\u0561\u0575", kSafe],
+ // Ethiopic:
+ ["xn--6xd66aa62c.com", "\u1220\u12d0\u12d0\u1350.com", kUnsafe, "DISABLED"],
+ ["xn--6xd66aa62c.et", "\u1220\u12d0\u12d0\u1350.et", kSafe],
+ ["xn--6xd66aa62c.xn--m0d3gwjla96a", "\u1220\u12d0\u12d0\u1350.\u12a2\u1275\u12ee\u1335\u12eb", kSafe],
+ // Greek:
+ ["xn--mxapd.com", "\u03b9\u03ba\u03b1.com", kUnsafe, "DISABLED"],
+ ["xn--mxapd.gr", "\u03b9\u03ba\u03b1.gr", kSafe],
+ ["xn--mxapd.xn--qxam", "\u03b9\u03ba\u03b1.\u03b5\u03bb", kSafe],
+ // Georgian:
+ ["xn--gpd3ag.com", "\u10fd\u10ff\u10ee.com", kUnsafe, "DISABLED"],
+ ["xn--gpd3ag.ge", "\u10fd\u10ff\u10ee.ge", kSafe],
+ ["xn--gpd3ag.xn--node", "\u10fd\u10ff\u10ee.\u10d2\u10d4", kSafe],
+ // Hebrew:
+ ["xn--7dbh4a.com", "\u05d7\u05e1\u05d3.com", kUnsafe, "DISABLED"],
+ ["xn--7dbh4a.il", "\u05d7\u05e1\u05d3.il", kSafe],
+ ["xn--9dbq2a.xn--7dbh4a", "\u05e7\u05d5\u05dd.\u05d7\u05e1\u05d3", kSafe],
+ // Myanmar:
+ ["xn--oidbbf41a.com", "\u1004\u1040\u1002\u1001\u1002.com", kUnsafe, "DISABLED"],
+ ["xn--oidbbf41a.mm", "\u1004\u1040\u1002\u1001\u1002.mm", kSafe],
+ ["xn--oidbbf41a.xn--7idjb0f4ck", "\u1004\u1040\u1002\u1001\u1002.\u1019\u103c\u1014\u103a\u1019\u102c", kSafe],
+ // Myanmar Shan digits:
+ ["xn--rmdcmef.com", "\u1090\u1091\u1095\u1096\u1097.com", kUnsafe, "DISABLED"],
+ ["xn--rmdcmef.mm", "\u1090\u1091\u1095\u1096\u1097.mm", kSafe],
+ ["xn--rmdcmef.xn--7idjb0f4ck", "\u1090\u1091\u1095\u1096\u1097.\u1019\u103c\u1014\u103a\u1019\u102c", kSafe],
+// Thai:
+// #if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS)
+ ["xn--o3cedqz2c.com", "\u0e17\u0e19\u0e1a\u0e1e\u0e23\u0e2b.com", kUnsafe, "DISABLED"],
+ ["xn--o3cedqz2c.th", "\u0e17\u0e19\u0e1a\u0e1e\u0e23\u0e2b.th", kSafe],
+ ["xn--o3cedqz2c.xn--o3cw4h", "\u0e17\u0e19\u0e1a\u0e1e\u0e23\u0e2b.\u0e44\u0e17\u0e22", kSafe],
+// #else
+ ["xn--r3ch7hsc.com", "\u0e1e\u0e1a\u0e40\u0e50.com", kUnsafe, "DISABLED"],
+ ["xn--r3ch7hsc.th", "\u0e1e\u0e1a\u0e40\u0e50.th", kSafe],
+ ["xn--r3ch7hsc.xn--o3cw4h", "\u0e1e\u0e1a\u0e40\u0e50.\u0e44\u0e17\u0e22", kSafe],
+// #endif
+
+ // Indic scripts:
+ // Bengali:
+ ["xn--07baub.com", "\u09e6\u09ed\u09e6\u09ed.com", kUnsafe, "DISABLED"],
+ // Devanagari:
+ ["xn--62ba6j.com", "\u093d\u0966\u093d.com", kUnsafe, "DISABLED"],
+ // Gujarati:
+ ["xn--becd.com", "\u0aa1\u0a9f.com", kUnsafe, "DISABLED"],
+ // Gurmukhi:
+ ["xn--occacb.com", "\u0a66\u0a67\u0a66\u0a67.com", kUnsafe, "DISABLED"],
+ // Kannada:
+ ["xn--stca6jf.com", "\u0cbd\u0ce6\u0cbd\u0ce7.com", kUnsafe, "DISABLED"],
+ // Malayalam:
+ ["xn--lwccv.com", "\u0d1f\u0d20\u0d27.com", kUnsafe, "DISABLED"],
+ // Oriya:
+ ["xn--zhca6ub.com", "\u0b6e\u0b20\u0b6e\u0b20.com", kUnsafe, "DISABLED"],
+ // Tamil:
+ ["xn--mlca6ab.com", "\u0b9f\u0baa\u0b9f\u0baa.com", kUnsafe, "DISABLED"],
+ // Telugu:
+ ["xn--brcaabbb.com", "\u0c67\u0c66\u0c67\u0c66\u0c67\u0c66.com", kUnsafe, "DISABLED"],
+
+ // IDN domain matching an IDN top-domain (f\u00f3\u00f3.com)
+ ["xn--fo-5ja.com", "f\u00f3o.com", kUnsafe, "DISABLED"],
+
+ // crbug.com/769547: Subdomains of top domains should be allowed.
+ ["xn--xample-9ua.test.net", "\u00e9xample.test.net", kSafe],
+ // Skeleton of the eTLD+1 matches a top domain, but the eTLD+1 itself is
+ // not a top domain. Should not be decoded to unicode.
+ ["xn--xample-9ua.test.xn--nt-bja", "\u00e9xample.test.n\u00e9t", kUnsafe, "DISABLED"],
+
+ // Digit lookalike check of 16კ.com with character “კ” (U+10D9)
+ // Test case for https://crbug.com/1156531
+ ["xn--16-1ik.com", "16\u10d9.com", kUnsafe, "DISABLED"],
+
+ // Skeleton generator check of officeკ65.com with character “კ” (U+10D9)
+ // Test case for https://crbug.com/1156531
+ ["xn--office65-l04a.com", "office\u10d965.com", kUnsafe],
+
+ // Digit lookalike check of 16ੜ.com with character “ੜ” (U+0A5C)
+ // Test case for https://crbug.com/1156531 (missed skeleton map)
+ ["xn--16-ogg.com", "16\u0a5c.com", kUnsafe, "DISABLED"],
+
+ // Skeleton generator check of officeੜ65.com with character “ੜ” (U+0A5C)
+ // Test case for https://crbug.com/1156531 (missed skeleton map)
+ ["xn--office65-hts.com", "office\u0a5c65.com", kUnsafe],
+
+ // New test cases go ↑↑ above.
+
+ // /!\ WARNING: You MUST use tools/security/idn_test_case_generator.py to
+ // generate new test cases, as specified by the comment at the top of this
+ // test list. Why must you use that python script?
+ // 1. It is easy to get things wrong. There were several hand-crafted
+ // incorrect test cases committed that was later fixed.
+ // 2. This test _also_ is a test of Chromium's IDN encoder/decoder, so using
+ // Chromium's IDN encoder/decoder to generate test files loses an
+ // advantage of having Python's IDN encode/decode the tests.
+];
+
+function checkEquals(a, b, message, expectedFail) {
+ if (!expectedFail) {
+ Assert.equal(a, b, message);
+ } else {
+ Assert.notEqual(a, b, `EXPECTED-FAIL: ${message}`);
+ }
+}
+
+add_task(async function test_chrome_spoofs() {
+ for (let test of testCases) {
+ let isAscii = {};
+ let result = idnService.convertToDisplayIDN(test[0], isAscii);
+ let expectedFail = test.length == 4 && test[3] == "DISABLED";
+ if (test[2] == kSafe) {
+ checkEquals(
+ result,
+ test[1],
+ `kSafe label ${test[0]} should convert to ${test[1]}`,
+ expectedFail
+ );
+ } else if (test[2] == kUnsafe) {
+ checkEquals(
+ result,
+ test[0],
+ `kUnsafe label ${test[0]} should not convert to ${test[1]}`,
+ expectedFail
+ );
+ } else if (test[2] == kInvalid) {
+ checkEquals(
+ result,
+ test[0],
+ `kInvalid label ${test[0]} should stay the same`,
+ expectedFail
+ );
+ }
+ }
+});
diff --git a/netwerk/test/unit/test_idn_urls.js b/netwerk/test/unit/test_idn_urls.js
new file mode 100644
index 0000000000..0059b133c0
--- /dev/null
+++ b/netwerk/test/unit/test_idn_urls.js
@@ -0,0 +1,436 @@
+// 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 = Services.prefs;
+ var oldProfile = pbi.getCharPref(
+ "network.IDN.restriction_profile",
+ "moderate"
+ );
+ 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]);
+
+ 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.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..f2b07c7114
--- /dev/null
+++ b/netwerk/test/unit/test_immutable.js
@@ -0,0 +1,206 @@
+"use strict";
+
+var prefs;
+var http2pref;
+var origin;
+var rcwnpref;
+
+function run_test() {
+ var h2Port = Services.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 = Services.prefs;
+
+ http2pref = prefs.getBoolPref("network.http.http2.enabled");
+ rcwnpref = prefs.getBoolPref("network.http.rcwn.enabled");
+
+ prefs.setBoolPref("network.http.http2.enabled", 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.http2.enabled", http2pref);
+ prefs.setBoolPref("network.http.rcwn.enabled", rcwnpref);
+ prefs.clearUserPref("network.dns.localDomains");
+}
+
+function makeChan(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("/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("/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("/immutable-test-without-attribute");
+ var listener = new Listener();
+ nextTest = doTest4;
+ chan.loadFlags = Ci.nsIRequest.LOAD_BYPASS_CACHE;
+ chan.asyncOpen(listener);
+}
+
+function doTest4() {
+ dump("execute doTest4 - resource with immutable. initial request\n");
+ do_test_pending();
+ expectConditional = false;
+ var chan = makeChan("/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("/immutable-test-with-attribute");
+ var listener = new Listener();
+ nextTest = doTest6;
+ chan.loadFlags = Ci.nsIRequest.VALIDATE_ALWAYS;
+ chan.asyncOpen(listener);
+}
+
+function doTest6() {
+ dump("execute doTest6 - resource with immutable. shift reload\n");
+ do_test_pending();
+ expectConditional = false;
+ var chan = makeChan("/immutable-test-with-attribute");
+ var listener = new Listener();
+ nextTest = doTest7;
+ chan.loadFlags = Ci.nsIRequest.LOAD_BYPASS_CACHE;
+ chan.asyncOpen(listener);
+}
+
+function doTest7() {
+ dump("execute doTest7 - expired resource with immutable. initial request\n");
+ do_test_pending();
+ expectConditional = false;
+ var chan = makeChan("/immutable-test-expired-with-Expires-header");
+ var listener = new Listener();
+ nextTest = doTest8;
+ chan.asyncOpen(listener);
+}
+
+function doTest8() {
+ dump("execute doTest8 - expired resource with immutable. reload\n");
+ do_test_pending();
+ expectConditional = true;
+ var chan = makeChan("/immutable-test-expired-with-Expires-header");
+ var listener = new Listener();
+ nextTest = doTest9;
+ chan.loadFlags = Ci.nsIRequest.VALIDATE_ALWAYS;
+ chan.asyncOpen(listener);
+}
+
+function doTest9() {
+ dump(
+ "execute doTest9 - expired resource with immutable cache extension and Last modified header. initial request\n"
+ );
+ do_test_pending();
+ expectConditional = false;
+ var chan = makeChan("/immutable-test-expired-with-last-modified-header");
+ var listener = new Listener();
+ nextTest = doTest10;
+ chan.asyncOpen(listener);
+}
+
+function doTest10() {
+ dump(
+ "execute doTest10 - expired resource with immutable cache extension and Last modified heder. reload\n"
+ );
+ do_test_pending();
+ expectConditional = true;
+ var chan = makeChan("/immutable-test-expired-with-last-modified-header");
+ var listener = new Listener();
+ nextTest = testsDone;
+ chan.loadFlags = Ci.nsIRequest.VALIDATE_ALWAYS;
+ 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..330e0f632f
--- /dev/null
+++ b/netwerk/test/unit/test_inhibit_caching.js
@@ -0,0 +1,94 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+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);
+}
+
+ChromeUtils.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..a0dd0a19cf
--- /dev/null
+++ b/netwerk/test/unit/test_large_port.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/. */
+
+// 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
--- /dev/null
+++ b/netwerk/test/unit/test_link.lnk
Binary files 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..9d5779437f
--- /dev/null
+++ b/netwerk/test/unit/test_loadgroup_cancel.js
@@ -0,0 +1,96 @@
+/* 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.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+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_localhost_offline.js b/netwerk/test/unit/test_localhost_offline.js
new file mode 100644
index 0000000000..a1f157944b
--- /dev/null
+++ b/netwerk/test/unit/test_localhost_offline.js
@@ -0,0 +1,77 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+var httpServer = null;
+const body = "Hello";
+
+function makeChan(url) {
+ let chan = NetUtil.newChannel({
+ uri: url,
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+ chan.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE;
+ chan.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING;
+ return chan;
+}
+
+function channelOpenPromise(chan, flags) {
+ return new Promise(resolve => {
+ function finish(req, buffer) {
+ resolve([req, buffer]);
+ }
+ chan.asyncOpen(new ChannelListener(finish, null, flags));
+ });
+}
+
+function makeURL(host) {
+ return `http://${host}:${httpServer.identity.primaryPort}/`;
+}
+
+add_task(async function test_localhost_offline() {
+ Services.io.offline = true;
+ Services.prefs.setBoolPref("network.disable-localhost-when-offline", false);
+ let chan = makeChan(makeURL("127.0.0.1"));
+ let [, resp] = await channelOpenPromise(chan);
+ Assert.equal(resp, body, "Should get correct response");
+
+ chan = makeChan(makeURL("localhost"));
+ [, resp] = await channelOpenPromise(chan);
+ Assert.equal(resp, body, "Should get response");
+
+ Services.prefs.setBoolPref("network.disable-localhost-when-offline", true);
+
+ chan = makeChan(makeURL("127.0.0.1"));
+ let [req] = await channelOpenPromise(
+ chan,
+ CL_ALLOW_UNKNOWN_CL | CL_EXPECT_FAILURE
+ );
+ req.QueryInterface(Ci.nsIHttpChannel);
+ Assert.equal(req.status, Cr.NS_ERROR_OFFLINE);
+
+ chan = makeChan(makeURL("localhost"));
+ [req] = await channelOpenPromise(
+ chan,
+ CL_ALLOW_UNKNOWN_CL | CL_EXPECT_FAILURE
+ );
+ req.QueryInterface(Ci.nsIHttpChannel);
+ Assert.equal(req.status, Cr.NS_ERROR_OFFLINE);
+
+ Services.prefs.clearUserPref("network.disable-localhost-when-offline");
+ Services.io.offline = false;
+});
+
+function run_test() {
+ httpServer = new HttpServer();
+ httpServer.registerPathHandler("/", (request, response) => {
+ response.seizePower();
+ response.write("HTTP/1.1 200 OK\r\n");
+ response.write("Content-Length: " + body.length + "\r\n");
+ response.write("\r\n");
+ response.write(body);
+ response.finish();
+ });
+ httpServer.start(-1);
+ run_next_test();
+}
diff --git a/netwerk/test/unit/test_localstreams.js b/netwerk/test/unit/test_localstreams.js
new file mode 100644
index 0000000000..3e9c26b111
--- /dev/null
+++ b/netwerk/test/unit/test_localstreams.js
@@ -0,0 +1,89 @@
+// 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 uri = Services.io.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..010d5d1178
--- /dev/null
+++ b/netwerk/test/unit/test_mismatch_last-modified.js
@@ -0,0 +1,154 @@
+"use strict";
+
+const BinaryInputStream = Components.Constructor(
+ "@mozilla.org/binaryinputstream;1",
+ "nsIBinaryInputStream",
+ "setInputStream"
+);
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+var httpserver = new HttpServer();
+
+// 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);
+ },
+};
+
+ChromeUtils.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);
+ },
+ };
+});
+
+ChromeUtils.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();
+
+ 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..1e93a440ad
--- /dev/null
+++ b/netwerk/test/unit/test_mozTXTToHTMLConv.js
@@ -0,0 +1,394 @@
+/* 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: <URL:http://mozilla.org> then",
+ url: "http://mozilla.org",
+ },
+ {
+ input: "RFC1738: <URL:mailto:john.doe+test@mozilla.org> then",
+ url: "mailto:john.doe+test@mozilla.org",
+ },
+ // -- RFC2396E
+ {
+ input: "RFC2396E: <http://mozilla.org/> then",
+ url: "http://mozilla.org/",
+ },
+ {
+ input: "RFC2396E: <john.doe+test@mozilla.org> 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 <http://www.mozilla.org/maybe today>", // 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&amp;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&amp;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: ["<sup", "2", "</sup>"],
+ },
+ {
+ input: "this is plus-minus: +/-",
+ results: ["&plusmn;"],
+ },
+ {
+ input: "this is a smiley :)",
+ results: ["🙂"],
+ },
+ {
+ input: "this is a smiley :-)",
+ results: ["🙂"],
+ },
+ {
+ input: "this is a smiley :-(",
+ results: ["🙁"],
+ },
+ ];
+
+ 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: " <a href='http://a.example.com/'>foo</a>",
+ shouldChange: false,
+ },
+ {
+ input: "<abbr>see http://abbr.example.com</abbr>",
+ shouldChange: true,
+ },
+ {
+ input: "<!-- see http://comment.example.com/ -->",
+ shouldChange: false,
+ },
+ {
+ input: "<!-- greater > -->",
+ shouldChange: false,
+ },
+ {
+ input: "<!-- lesser < -->",
+ shouldChange: false,
+ },
+ {
+ input:
+ "<style id='ex'>background-image: url(http://example.com/ex.png);</style>",
+ shouldChange: false,
+ },
+ {
+ input: "<style>body > p, body > div { color:blue }</style>",
+ shouldChange: false,
+ },
+ {
+ input: "<script>window.location='http://script.example.com/';</script>",
+ shouldChange: false,
+ },
+ {
+ input: "<head><title>http://head.example.com/</title></head>",
+ shouldChange: false,
+ },
+ {
+ input: "<header>see http://header.example.com</header>",
+ shouldChange: true,
+ },
+ {
+ input: "<iframe src='http://iframe.example.com/' />",
+ shouldChange: false,
+ },
+ {
+ input: "broken end <script",
+ shouldChange: false,
+ },
+ ];
+
+ function hrefLink(url) {
+ return ' href="' + url + '"';
+ }
+
+ function linkText(plaintext) {
+ return ">" + plaintext + "</a>";
+ }
+
+ for (let i = 0; i < scanTXTtests.length; i++) {
+ let t = scanTXTtests[i];
+ let output = converter.scanTXT(t.input, Ci.mozITXTToHTMLConv.kURLs);
+ let link = hrefLink(t.url);
+ let text;
+ if (t.text) {
+ text = linkText(t.text);
+ }
+ if (!output.includes(link)) {
+ do_throw(
+ "Unexpected conversion by scanTXT: input=" +
+ t.input +
+ ", output=" +
+ output +
+ ", link=" +
+ link
+ );
+ }
+ if (text && !output.includes(text)) {
+ do_throw(
+ "Unexpected conversion by scanTXT: input=" +
+ t.input +
+ ", output=" +
+ output +
+ ", text=" +
+ text
+ );
+ }
+ }
+
+ for (let i = 0; i < scanTXTglyph.length; i++) {
+ let t = scanTXTglyph[i];
+ let output = converter.scanTXT(
+ t.input,
+ Ci.mozITXTToHTMLConv.kGlyphSubstitution
+ );
+ for (let j = 0; j < t.results.length; j++) {
+ if (!output.includes(t.results[j])) {
+ do_throw(
+ "Unexpected conversion by scanTXT: input=" +
+ t.input +
+ ", output=" +
+ output +
+ ", expected=" +
+ t.results[j]
+ );
+ }
+ }
+ }
+
+ for (let i = 0; i < scanTXTstrings.length; ++i) {
+ for (let j = 0; j < scanTXTstructs.length; ++j) {
+ let input =
+ scanTXTstructs[j].delimiter +
+ scanTXTstrings[i] +
+ scanTXTstructs[j].delimiter;
+ let expected =
+ "<" +
+ scanTXTstructs[j].tag +
+ ' class="' +
+ scanTXTstructs[j].class +
+ '">' +
+ '<span class="moz-txt-tag">' +
+ scanTXTstructs[j].delimiter +
+ "</span>" +
+ scanTXTstrings[i] +
+ '<span class="moz-txt-tag">' +
+ scanTXTstructs[j].delimiter +
+ "</span>" +
+ "</" +
+ scanTXTstructs[j].tag +
+ ">";
+ let actual = converter.scanTXT(input, Ci.mozITXTToHTMLConv.kStructPhrase);
+ Assert.equal(encodeURIComponent(actual), encodeURIComponent(expected));
+ }
+ }
+
+ for (let i = 0; i < scanHTMLtests.length; i++) {
+ let t = scanHTMLtests[i];
+ let output = converter.scanHTML(t.input, Ci.mozITXTToHTMLConv.kURLs);
+ let changed = t.input != output;
+ if (changed != t.shouldChange) {
+ do_throw(
+ "Unexpected change by scanHTML: changed=" +
+ changed +
+ ", shouldChange=" +
+ t.shouldChange +
+ ", \ninput=" +
+ t.input +
+ ", \noutput=" +
+ output
+ );
+ }
+ }
+}
diff --git a/netwerk/test/unit/test_multipart_byteranges.js b/netwerk/test/unit/test_multipart_byteranges.js
new file mode 100644
index 0000000000..475aced456
--- /dev/null
+++ b/netwerk/test/unit/test_multipart_byteranges.js
@@ -0,0 +1,143 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+var httpserver = null;
+
+ChromeUtils.defineLazyGetter(this, "uri", function () {
+ return "http://localhost:" + httpserver.identity.primaryPort + "/multipart";
+});
+
+function make_channel(url) {
+ return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true });
+}
+
+var multipartBody =
+ "--boundary\r\n" +
+ "Content-type: text/plain\r\n" +
+ "Content-range: bytes 0-2/10\r\n" +
+ "\r\n" +
+ "aaa\r\n" +
+ "--boundary\r\n" +
+ "Content-type: text/plain\r\n" +
+ "Content-range: bytes 3-7/10\r\n" +
+ "\r\n" +
+ "bbbbb" +
+ "\r\n" +
+ "--boundary\r\n" +
+ "Content-type: text/plain\r\n" +
+ "Content-range: bytes 8-9/10\r\n" +
+ "\r\n" +
+ "cc" +
+ "\r\n" +
+ "--boundary--";
+
+function contentHandler(metadata, response) {
+ response.setHeader(
+ "Content-Type",
+ 'multipart/byteranges; boundary="boundary"'
+ );
+ response.bodyOutputStream.write(multipartBody, multipartBody.length);
+}
+
+var numTests = 2;
+var testNum = 0;
+
+var testData = [
+ {
+ data: "aaa",
+ type: "text/plain",
+ isByteRangeRequest: true,
+ startRange: 0,
+ endRange: 2,
+ },
+ {
+ data: "bbbbb",
+ type: "text/plain",
+ isByteRangeRequest: true,
+ startRange: 3,
+ endRange: 7,
+ },
+ {
+ data: "cc",
+ type: "text/plain",
+ isByteRangeRequest: true,
+ startRange: 8,
+ endRange: 9,
+ },
+];
+
+function responseHandler(request, buffer) {
+ Assert.equal(buffer, testData[testNum].data);
+ Assert.equal(
+ request.QueryInterface(Ci.nsIChannel).contentType,
+ testData[testNum].type
+ );
+ Assert.equal(
+ request.QueryInterface(Ci.nsIByteRangeRequest).isByteRangeRequest,
+ testData[testNum].isByteRangeRequest
+ );
+ Assert.equal(
+ request.QueryInterface(Ci.nsIByteRangeRequest).startRange,
+ testData[testNum].startRange
+ );
+ Assert.equal(
+ request.QueryInterface(Ci.nsIByteRangeRequest).endRange,
+ testData[testNum].endRange
+ );
+ if (++testNum == numTests) {
+ httpserver.stop(do_test_finished);
+ }
+}
+
+var multipartListener = {
+ _buffer: "",
+
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIStreamListener",
+ "nsIRequestObserver",
+ ]),
+
+ onStartRequest(request) {
+ this._buffer = "";
+ },
+
+ onDataAvailable(request, stream, offset, count) {
+ try {
+ this._buffer = this._buffer.concat(read_stream(stream, count));
+ dump("BUFFEEE: " + this._buffer + "\n\n");
+ } catch (ex) {
+ do_throw("Error in onDataAvailable: " + ex);
+ }
+ },
+
+ onStopRequest(request, status) {
+ try {
+ responseHandler(request, this._buffer);
+ } catch (ex) {
+ do_throw("Error in closure function: " + ex);
+ }
+ },
+};
+
+function run_test() {
+ httpserver = new HttpServer();
+ httpserver.registerPathHandler("/multipart", contentHandler);
+ httpserver.start(-1);
+
+ var streamConv = Cc["@mozilla.org/streamConverters;1"].getService(
+ Ci.nsIStreamConverterService
+ );
+ var conv = streamConv.asyncConvertData(
+ "multipart/byteranges",
+ "*/*",
+ multipartListener,
+ null
+ );
+
+ var chan = make_channel(uri);
+ chan.asyncOpen(conv, null);
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_multipart_streamconv-byte-by-byte.js b/netwerk/test/unit/test_multipart_streamconv-byte-by-byte.js
new file mode 100644
index 0000000000..c1865f5668
--- /dev/null
+++ b/netwerk/test/unit/test_multipart_streamconv-byte-by-byte.js
@@ -0,0 +1,115 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+var httpserver = null;
+
+ChromeUtils.defineLazyGetter(this, "uri", function () {
+ return "http://localhost:" + httpserver.identity.primaryPort + "/multipart";
+});
+
+function make_channel(url) {
+ return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true });
+}
+
+var multipartBody =
+ "--boundary\r\n\r\nSome text\r\n--boundary\r\nContent-Type: text/x-test\r\n\r\n<?xml version='1.1'?>\r\n<root/>\r\n--boundary\r\n\r\n<?xml version='1.0'?><root/>\r\n--boundary--";
+
+function contentHandler(metadata, response) {
+ response.setHeader("Content-Type", 'multipart/mixed; boundary="boundary"');
+ response.processAsync();
+
+ var body = multipartBody;
+ function byteByByte() {
+ if (!body.length) {
+ response.finish();
+ return;
+ }
+
+ var onebyte = body[0];
+ response.bodyOutputStream.write(onebyte, 1);
+ body = body.substring(1);
+ do_timeout(1, byteByByte);
+ }
+
+ do_timeout(1, byteByByte);
+}
+
+var numTests = 2;
+var testNum = 0;
+
+var testData = [
+ { data: "Some text", type: "text/plain" },
+ { data: "<?xml version='1.1'?>\r\n<root/>", type: "text/x-test" },
+ { data: "<?xml version='1.0'?><root/>", type: "text/xml" },
+];
+
+function responseHandler(request, buffer) {
+ Assert.equal(buffer, testData[testNum].data);
+ Assert.equal(
+ request.QueryInterface(Ci.nsIChannel).contentType,
+ testData[testNum].type
+ );
+ if (++testNum == numTests) {
+ httpserver.stop(do_test_finished);
+ }
+}
+
+var multipartListener = {
+ _buffer: "",
+ _index: 0,
+
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIStreamListener",
+ "nsIRequestObserver",
+ ]),
+
+ onStartRequest(request) {
+ this._buffer = "";
+ },
+
+ onDataAvailable(request, stream, offset, count) {
+ try {
+ this._buffer = this._buffer.concat(read_stream(stream, count));
+ dump("BUFFEEE: " + this._buffer + "\n\n");
+ } catch (ex) {
+ do_throw("Error in onDataAvailable: " + ex);
+ }
+ },
+
+ onStopRequest(request, status) {
+ this._index++;
+ // Second part should be last part
+ Assert.equal(
+ request.QueryInterface(Ci.nsIMultiPartChannel).isLastPart,
+ this._index == testData.length
+ );
+ try {
+ responseHandler(request, this._buffer);
+ } catch (ex) {
+ do_throw("Error in closure function: " + ex);
+ }
+ },
+};
+
+function run_test() {
+ httpserver = new HttpServer();
+ httpserver.registerPathHandler("/multipart", contentHandler);
+ httpserver.start(-1);
+
+ var streamConv = Cc["@mozilla.org/streamConverters;1"].getService(
+ Ci.nsIStreamConverterService
+ );
+ var conv = streamConv.asyncConvertData(
+ "multipart/mixed",
+ "*/*",
+ multipartListener,
+ null
+ );
+
+ var chan = make_channel(uri);
+ chan.asyncOpen(conv);
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_multipart_streamconv.js b/netwerk/test/unit/test_multipart_streamconv.js
new file mode 100644
index 0000000000..c1503e4d8c
--- /dev/null
+++ b/netwerk/test/unit/test_multipart_streamconv.js
@@ -0,0 +1,100 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+var httpserver = null;
+
+ChromeUtils.defineLazyGetter(this, "uri", function () {
+ return "http://localhost:" + httpserver.identity.primaryPort + "/multipart";
+});
+
+function make_channel(url) {
+ return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true });
+}
+
+var multipartBody =
+ "--boundary\r\nSet-Cookie: foo=bar\r\n\r\nSome text\r\n--boundary\r\nContent-Type: text/x-test\r\n\r\n<?xml version='1.1'?>\r\n<root/>\r\n--boundary\r\n\r\n<?xml version='1.0'?><root/>\r\n--boundary--";
+
+function contentHandler(metadata, response) {
+ response.setHeader("Content-Type", 'multipart/mixed; boundary="boundary"');
+ response.bodyOutputStream.write(multipartBody, multipartBody.length);
+}
+
+var numTests = 2;
+var testNum = 0;
+
+var testData = [
+ { data: "Some text", type: "text/plain" },
+ { data: "<?xml version='1.1'?>\r\n<root/>", type: "text/x-test" },
+ { data: "<?xml version='1.0'?><root/>", type: "text/xml" },
+];
+
+function responseHandler(request, buffer) {
+ Assert.equal(buffer, testData[testNum].data);
+ Assert.equal(
+ request.QueryInterface(Ci.nsIChannel).contentType,
+ testData[testNum].type
+ );
+ if (++testNum == numTests) {
+ httpserver.stop(do_test_finished);
+ }
+}
+
+var multipartListener = {
+ _buffer: "",
+ _index: 0,
+
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIStreamListener",
+ "nsIRequestObserver",
+ ]),
+
+ onStartRequest(request) {
+ this._buffer = "";
+ },
+
+ onDataAvailable(request, stream, offset, count) {
+ try {
+ this._buffer = this._buffer.concat(read_stream(stream, count));
+ dump("BUFFEEE: " + this._buffer + "\n\n");
+ } catch (ex) {
+ do_throw("Error in onDataAvailable: " + ex);
+ }
+ },
+
+ onStopRequest(request, status) {
+ this._index++;
+ // Second part should be last part
+ Assert.equal(
+ request.QueryInterface(Ci.nsIMultiPartChannel).isLastPart,
+ this._index == testData.length
+ );
+ try {
+ responseHandler(request, this._buffer);
+ } catch (ex) {
+ do_throw("Error in closure function: " + ex);
+ }
+ },
+};
+
+function run_test() {
+ httpserver = new HttpServer();
+ httpserver.registerPathHandler("/multipart", contentHandler);
+ httpserver.start(-1);
+
+ var streamConv = Cc["@mozilla.org/streamConverters;1"].getService(
+ Ci.nsIStreamConverterService
+ );
+ var conv = streamConv.asyncConvertData(
+ "multipart/mixed",
+ "*/*",
+ multipartListener,
+ null
+ );
+
+ var chan = make_channel(uri);
+ chan.asyncOpen(conv);
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_multipart_streamconv_empty.js b/netwerk/test/unit/test_multipart_streamconv_empty.js
new file mode 100644
index 0000000000..3d3f9dc859
--- /dev/null
+++ b/netwerk/test/unit/test_multipart_streamconv_empty.js
@@ -0,0 +1,68 @@
+"use strict";
+
+function make_channel(url) {
+ return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true });
+}
+
+add_task(async function test_empty() {
+ let uri = "http://localhost";
+ let httpChan = make_channel(uri);
+
+ let channel = Cc["@mozilla.org/network/input-stream-channel;1"]
+ .createInstance(Ci.nsIInputStreamChannel)
+ .QueryInterface(Ci.nsIChannel);
+
+ channel.setURI(httpChan.URI);
+ channel.loadInfo = httpChan.loadInfo;
+
+ let inputStream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(
+ Ci.nsIStringInputStream
+ );
+ inputStream.setUTF8Data(""); // Pass an empty string
+
+ channel.contentStream = inputStream;
+
+ let [status, buffer] = await new Promise(resolve => {
+ let streamConv = Cc["@mozilla.org/streamConverters;1"].getService(
+ Ci.nsIStreamConverterService
+ );
+ let multipartListener = {
+ _buffer: "",
+
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIStreamListener",
+ "nsIRequestObserver",
+ ]),
+
+ onStartRequest(request) {},
+ onDataAvailable(request, stream, offset, count) {
+ try {
+ this._buffer = this._buffer.concat(read_stream(stream, count));
+ dump("BUFFEEE: " + this._buffer + "\n\n");
+ } catch (ex) {
+ do_throw("Error in onDataAvailable: " + ex);
+ }
+ },
+
+ onStopRequest(request, status1) {
+ resolve([status1, this._buffer]);
+ },
+ };
+ let conv = streamConv.asyncConvertData(
+ "multipart/mixed",
+ "*/*",
+ multipartListener,
+ null
+ );
+
+ let chan = make_channel(uri);
+ chan.asyncOpen(conv);
+ });
+
+ Assert.notEqual(
+ status,
+ Cr.NS_OK,
+ "Should be an error code because content has no boundary"
+ );
+ Assert.equal(buffer, "", "Should have received no content");
+});
diff --git a/netwerk/test/unit/test_multipart_streamconv_missing_boundary_lead_dashes.js b/netwerk/test/unit/test_multipart_streamconv_missing_boundary_lead_dashes.js
new file mode 100644
index 0000000000..3ed2bd39c6
--- /dev/null
+++ b/netwerk/test/unit/test_multipart_streamconv_missing_boundary_lead_dashes.js
@@ -0,0 +1,92 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+var httpserver = null;
+
+ChromeUtils.defineLazyGetter(this, "uri", function () {
+ return "http://localhost:" + httpserver.identity.primaryPort + "/multipart";
+});
+
+function make_channel(url) {
+ return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true });
+}
+
+var multipartBody =
+ "\r\nboundary\r\n\r\nSome text\r\nboundary\r\n\r\n<?xml version='1.0'?><root/>\r\nboundary--";
+
+function contentHandler(metadata, response) {
+ response.setHeader("Content-Type", 'multipart/mixed; boundary="boundary"');
+ response.bodyOutputStream.write(multipartBody, multipartBody.length);
+}
+
+var numTests = 2;
+var testNum = 0;
+
+var testData = [
+ { data: "Some text", type: "text/plain" },
+ { data: "<?xml version='1.0'?><root/>", type: "text/xml" },
+];
+
+function responseHandler(request, buffer) {
+ Assert.equal(buffer, testData[testNum].data);
+ Assert.equal(
+ request.QueryInterface(Ci.nsIChannel).contentType,
+ testData[testNum].type
+ );
+ if (++testNum == numTests) {
+ httpserver.stop(do_test_finished);
+ }
+}
+
+var multipartListener = {
+ _buffer: "",
+
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIStreamListener",
+ "nsIRequestObserver",
+ ]),
+
+ onStartRequest(request) {
+ this._buffer = "";
+ },
+
+ onDataAvailable(request, stream, offset, count) {
+ try {
+ this._buffer = this._buffer.concat(read_stream(stream, count));
+ dump("BUFFEEE: " + this._buffer + "\n\n");
+ } catch (ex) {
+ do_throw("Error in onDataAvailable: " + ex);
+ }
+ },
+
+ onStopRequest(request, status) {
+ try {
+ responseHandler(request, this._buffer);
+ } catch (ex) {
+ do_throw("Error in closure function: " + ex);
+ }
+ },
+};
+
+function run_test() {
+ httpserver = new HttpServer();
+ httpserver.registerPathHandler("/multipart", contentHandler);
+ httpserver.start(-1);
+
+ var streamConv = Cc["@mozilla.org/streamConverters;1"].getService(
+ Ci.nsIStreamConverterService
+ );
+ var conv = streamConv.asyncConvertData(
+ "multipart/mixed",
+ "*/*",
+ multipartListener,
+ null
+ );
+
+ var chan = make_channel(uri);
+ chan.asyncOpen(conv);
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_multipart_streamconv_missing_lead_boundary.js b/netwerk/test/unit/test_multipart_streamconv_missing_lead_boundary.js
new file mode 100644
index 0000000000..1df4311b90
--- /dev/null
+++ b/netwerk/test/unit/test_multipart_streamconv_missing_lead_boundary.js
@@ -0,0 +1,92 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+var httpserver = null;
+
+ChromeUtils.defineLazyGetter(this, "uri", function () {
+ return "http://localhost:" + httpserver.identity.primaryPort + "/multipart";
+});
+
+function make_channel(url) {
+ return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true });
+}
+
+var multipartBody =
+ "Preamble\r\n--boundary\r\n\r\nSome text\r\n--boundary\r\n\r\n<?xml version='1.0'?><root/>\r\n--boundary--";
+
+function contentHandler(metadata, response) {
+ response.setHeader("Content-Type", 'multipart/mixed; boundary="boundary"');
+ response.bodyOutputStream.write(multipartBody, multipartBody.length);
+}
+
+var numTests = 2;
+var testNum = 0;
+
+var testData = [
+ { data: "Some text", type: "text/plain" },
+ { data: "<?xml version='1.0'?><root/>", type: "text/xml" },
+];
+
+function responseHandler(request, buffer) {
+ Assert.equal(buffer, testData[testNum].data);
+ Assert.equal(
+ request.QueryInterface(Ci.nsIChannel).contentType,
+ testData[testNum].type
+ );
+ if (++testNum == numTests) {
+ httpserver.stop(do_test_finished);
+ }
+}
+
+var multipartListener = {
+ _buffer: "",
+
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIStreamListener",
+ "nsIRequestObserver",
+ ]),
+
+ onStartRequest(request) {
+ this._buffer = "";
+ },
+
+ onDataAvailable(request, stream, offset, count) {
+ try {
+ this._buffer = this._buffer.concat(read_stream(stream, count));
+ dump("BUFFEEE: " + this._buffer + "\n\n");
+ } catch (ex) {
+ do_throw("Error in onDataAvailable: " + ex);
+ }
+ },
+
+ onStopRequest(request, status) {
+ try {
+ responseHandler(request, this._buffer);
+ } catch (ex) {
+ do_throw("Error in closure function: " + ex);
+ }
+ },
+};
+
+function run_test() {
+ httpserver = new HttpServer();
+ httpserver.registerPathHandler("/multipart", contentHandler);
+ httpserver.start(-1);
+
+ var streamConv = Cc["@mozilla.org/streamConverters;1"].getService(
+ Ci.nsIStreamConverterService
+ );
+ var conv = streamConv.asyncConvertData(
+ "multipart/mixed",
+ "*/*",
+ multipartListener,
+ null
+ );
+
+ var chan = make_channel(uri);
+ chan.asyncOpen(conv);
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_nestedabout_serialize.js b/netwerk/test/unit/test_nestedabout_serialize.js
new file mode 100644
index 0000000000..a8554150d2
--- /dev/null
+++ b/netwerk/test/unit/test_nestedabout_serialize.js
@@ -0,0 +1,39 @@
+"use strict";
+
+const BinaryInputStream = Components.Constructor(
+ "@mozilla.org/binaryinputstream;1",
+ "nsIBinaryInputStream",
+ "setInputStream"
+);
+const BinaryOutputStream = Components.Constructor(
+ "@mozilla.org/binaryoutputstream;1",
+ "nsIBinaryOutputStream",
+ "setOutputStream"
+);
+
+const Pipe = Components.Constructor("@mozilla.org/pipe;1", "nsIPipe", "init");
+
+function run_test() {
+ var ios = Cc["@mozilla.org/network/io-service;1"].createInstance(
+ Ci.nsIIOService
+ );
+
+ var baseURI = ios.newURI("http://example.com/", "UTF-8");
+
+ // This depends on the redirector for about:license having the
+ // nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT flag.
+ var aboutLicense = ios.newURI("about:license", "UTF-8", baseURI);
+
+ var pipe = new Pipe(false, false, 0, 0, null);
+ var output = new BinaryOutputStream(pipe.outputStream);
+ var input = new BinaryInputStream(pipe.inputStream);
+ output.QueryInterface(Ci.nsIObjectOutputStream);
+ input.QueryInterface(Ci.nsIObjectInputStream);
+
+ output.writeCompoundObject(aboutLicense, Ci.nsIURI, true);
+ var copy = input.readObject(true);
+ copy.QueryInterface(Ci.nsIURI);
+
+ Assert.equal(copy.asciiSpec, aboutLicense.asciiSpec);
+ Assert.ok(copy.equals(aboutLicense));
+}
diff --git a/netwerk/test/unit/test_net_addr.js b/netwerk/test/unit/test_net_addr.js
new file mode 100644
index 0000000000..331fa9bc74
--- /dev/null
+++ b/netwerk/test/unit/test_net_addr.js
@@ -0,0 +1,216 @@
+"use strict";
+
+var CC = Components.Constructor;
+
+const ServerSocket = CC(
+ "@mozilla.org/network/server-socket;1",
+ "nsIServerSocket",
+ "init"
+);
+
+/**
+ * TestServer: A single instance of this is created as |serv|. When created,
+ * it starts listening on the loopback address on port |serv.port|. Tests will
+ * connect to it after setting |serv.acceptCallback|, which is invoked after it
+ * accepts a connection.
+ *
+ * Within |serv.acceptCallback|, various properties of |serv| can be used to
+ * run checks. After the callback, the connection is closed, but the server
+ * remains listening until |serv.stop|
+ *
+ * Note: TestServer can only handle a single connection at a time. Tests
+ * should use run_next_test at the end of |serv.acceptCallback| to start the
+ * following test which creates a connection.
+ */
+function TestServer() {
+ this.reset();
+
+ // start server.
+ // any port (-1), loopback only (true), default backlog (-1)
+ this.listener = ServerSocket(-1, true, -1);
+ this.port = this.listener.port;
+ info("server: listening on " + this.port);
+ this.listener.asyncListen(this);
+}
+
+TestServer.prototype = {
+ onSocketAccepted(socket, trans) {
+ info("server: got client connection");
+
+ // one connection at a time.
+ if (this.input !== null) {
+ try {
+ socket.close();
+ } catch (ignore) {}
+ do_throw("Test written to handle one connection at a time.");
+ }
+
+ try {
+ this.input = trans.openInputStream(0, 0, 0);
+ this.output = trans.openOutputStream(0, 0, 0);
+ this.selfAddr = trans.getScriptableSelfAddr();
+ this.peerAddr = trans.getScriptablePeerAddr();
+
+ this.acceptCallback();
+ } catch (e) {
+ /* In a native callback such as onSocketAccepted, exceptions might not
+ * get output correctly or logged to test output. Send them through
+ * do_throw, which fails the test immediately. */
+ do_report_unexpected_exception(e, "in TestServer.onSocketAccepted");
+ }
+
+ this.reset();
+ },
+
+ onStopListening(socket) {},
+
+ /**
+ * Called to close a connection and clean up properties.
+ */
+ reset() {
+ if (this.input) {
+ try {
+ this.input.close();
+ } catch (ignore) {}
+ }
+ if (this.output) {
+ try {
+ this.output.close();
+ } catch (ignore) {}
+ }
+
+ this.input = null;
+ this.output = null;
+ this.acceptCallback = null;
+ this.selfAddr = null;
+ this.peerAddr = null;
+ },
+
+ /**
+ * Cleanup for TestServer and this test case.
+ */
+ stop() {
+ this.reset();
+ try {
+ this.listener.close();
+ } catch (ignore) {}
+ },
+};
+
+/**
+ * Helper function.
+ * Compares two nsINetAddr objects and ensures they are logically equivalent.
+ */
+function checkAddrEqual(lhs, rhs) {
+ Assert.equal(lhs.family, rhs.family);
+
+ if (lhs.family === Ci.nsINetAddr.FAMILY_INET) {
+ Assert.equal(lhs.address, rhs.address);
+ Assert.equal(lhs.port, rhs.port);
+ }
+
+ /* TODO: fully support ipv6 and local */
+}
+
+/**
+ * An instance of SocketTransportService, used to create connections.
+ */
+var sts;
+
+/**
+ * Single instance of TestServer
+ */
+var serv;
+
+/**
+ * A place for individual tests to place Objects of importance for access
+ * throughout asynchronous testing. Particularly important for any output or
+ * input streams opened, as cleanup of those objects (by the garbage collector)
+ * causes the stream to close and may have other side effects.
+ */
+var testDataStore = null;
+
+/**
+ * IPv4 test.
+ */
+function testIpv4() {
+ testDataStore = {
+ transport: null,
+ ouput: null,
+ };
+
+ serv.acceptCallback = function () {
+ // disable the timeoutCallback
+ serv.timeoutCallback = function () {};
+
+ var selfAddr = testDataStore.transport.getScriptableSelfAddr();
+ var peerAddr = testDataStore.transport.getScriptablePeerAddr();
+
+ // check peerAddr against expected values
+ Assert.equal(peerAddr.family, Ci.nsINetAddr.FAMILY_INET);
+ Assert.equal(peerAddr.port, testDataStore.transport.port);
+ Assert.equal(peerAddr.port, serv.port);
+ Assert.equal(peerAddr.address, "127.0.0.1");
+
+ // check selfAddr against expected values
+ Assert.equal(selfAddr.family, Ci.nsINetAddr.FAMILY_INET);
+ Assert.equal(selfAddr.address, "127.0.0.1");
+
+ // check that selfAddr = server.peerAddr and vice versa.
+ checkAddrEqual(selfAddr, serv.peerAddr);
+ checkAddrEqual(peerAddr, serv.selfAddr);
+
+ testDataStore = null;
+ executeSoon(run_next_test);
+ };
+
+ // Useful timeout for debugging test hangs
+ /*serv.timeoutCallback = function(tname) {
+ if (tname === 'testIpv4')
+ do_throw('testIpv4 never completed a connection to TestServ');
+ };
+ do_timeout(connectTimeout, function(){ serv.timeoutCallback('testIpv4'); });*/
+
+ testDataStore.transport = sts.createTransport(
+ [],
+ "127.0.0.1",
+ serv.port,
+ null,
+ null
+ );
+ /*
+ * Need to hold |output| so that the output stream doesn't close itself and
+ * the associated connection.
+ */
+ testDataStore.output = testDataStore.transport.openOutputStream(
+ Ci.nsITransport.OPEN_BLOCKING,
+ 0,
+ 0
+ );
+
+ /* NEXT:
+ * openOutputStream -> onSocketAccepted -> acceptedCallback -> run_next_test
+ * OR (if the above timeout is uncommented)
+ * <connectTimeout lapses> -> timeoutCallback -> do_throw
+ */
+}
+
+/**
+ * Running the tests.
+ */
+function run_test() {
+ sts = Cc["@mozilla.org/network/socket-transport-service;1"].getService(
+ Ci.nsISocketTransportService
+ );
+ serv = new TestServer();
+
+ registerCleanupFunction(function () {
+ serv.stop();
+ });
+
+ add_test(testIpv4);
+ /* TODO: testIpv6 */
+ /* TODO: testLocal */
+
+ run_next_test();
+}
diff --git a/netwerk/test/unit/test_network_connectivity_service.js b/netwerk/test/unit/test_network_connectivity_service.js
new file mode 100644
index 0000000000..6774e79e34
--- /dev/null
+++ b/netwerk/test/unit/test_network_connectivity_service.js
@@ -0,0 +1,215 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+/**
+ * Waits for an observer notification to fire.
+ *
+ * @param {String} topicName The notification topic.
+ * @returns {Promise} A promise that fulfills when the notification is fired.
+ */
+function promiseObserverNotification(topicName, matchFunc) {
+ return new Promise((resolve, reject) => {
+ Services.obs.addObserver(function observe(subject, topic, data) {
+ let matches = typeof matchFunc != "function" || matchFunc(subject, data);
+ if (!matches) {
+ return;
+ }
+ Services.obs.removeObserver(observe, topic);
+ resolve({ subject, data });
+ }, topicName);
+ });
+}
+
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("network.connectivity-service.DNSv4.domain");
+ Services.prefs.clearUserPref("network.connectivity-service.DNSv6.domain");
+ Services.prefs.clearUserPref("network.captive-portal-service.testMode");
+ Services.prefs.clearUserPref("network.connectivity-service.IPv4.url");
+ Services.prefs.clearUserPref("network.connectivity-service.IPv6.url");
+});
+
+let httpserver = null;
+let httpserverv6 = null;
+ChromeUtils.defineLazyGetter(this, "URL", function () {
+ return "http://localhost:" + httpserver.identity.primaryPort + "/content";
+});
+
+ChromeUtils.defineLazyGetter(this, "URLv6", function () {
+ return "http://[::1]:" + httpserverv6.identity.primaryPort + "/content";
+});
+
+function contentHandler(metadata, response) {
+ response.setHeader("Content-Type", "text/plain");
+ response.setHeader("Cache-Control", "no-cache");
+
+ const responseBody = "anybody";
+ response.bodyOutputStream.write(responseBody, responseBody.length);
+}
+
+const kDNSv6Domain =
+ mozinfo.os == "linux" || mozinfo.os == "android"
+ ? "ip6-localhost"
+ : "localhost";
+
+add_task(async function testDNS() {
+ let ncs = Cc[
+ "@mozilla.org/network/network-connectivity-service;1"
+ ].getService(Ci.nsINetworkConnectivityService);
+
+ // Set the endpoints, trigger a DNS recheck, and wait for it to complete.
+ Services.prefs.setCharPref(
+ "network.connectivity-service.DNSv4.domain",
+ "example.org"
+ );
+ Services.prefs.setCharPref(
+ "network.connectivity-service.DNSv6.domain",
+ kDNSv6Domain
+ );
+ ncs.recheckDNS();
+ await promiseObserverNotification(
+ "network:connectivity-service:dns-checks-complete"
+ );
+
+ equal(
+ ncs.DNSv4,
+ Ci.nsINetworkConnectivityService.OK,
+ "Check DNSv4 support (expect OK)"
+ );
+ equal(
+ ncs.DNSv6,
+ Ci.nsINetworkConnectivityService.OK,
+ "Check DNSv6 support (expect OK)"
+ );
+
+ // Set the endpoints to non-exitant domains, trigger a DNS recheck, and wait for it to complete.
+ Services.prefs.setCharPref(
+ "network.connectivity-service.DNSv4.domain",
+ "does-not-exist.example"
+ );
+ Services.prefs.setCharPref(
+ "network.connectivity-service.DNSv6.domain",
+ "does-not-exist.example"
+ );
+ let observerNotification = promiseObserverNotification(
+ "network:connectivity-service:dns-checks-complete"
+ );
+ ncs.recheckDNS();
+ await observerNotification;
+
+ equal(
+ ncs.DNSv4,
+ Ci.nsINetworkConnectivityService.NOT_AVAILABLE,
+ "Check DNSv4 support (expect N/A)"
+ );
+ equal(
+ ncs.DNSv6,
+ Ci.nsINetworkConnectivityService.NOT_AVAILABLE,
+ "Check DNSv6 support (expect N/A)"
+ );
+
+ // Set the endpoints back to the proper domains, and simulate a captive portal
+ // event.
+ Services.prefs.setCharPref(
+ "network.connectivity-service.DNSv4.domain",
+ "example.org"
+ );
+ Services.prefs.setCharPref(
+ "network.connectivity-service.DNSv6.domain",
+ kDNSv6Domain
+ );
+ observerNotification = promiseObserverNotification(
+ "network:connectivity-service:dns-checks-complete"
+ );
+ Services.obs.notifyObservers(null, "network:captive-portal-connectivity");
+ // This will cause the state to go to UNKNOWN for a bit, until the check is completed.
+ equal(
+ ncs.DNSv4,
+ Ci.nsINetworkConnectivityService.UNKNOWN,
+ "Check DNSv4 support (expect UNKNOWN)"
+ );
+ equal(
+ ncs.DNSv6,
+ Ci.nsINetworkConnectivityService.UNKNOWN,
+ "Check DNSv6 support (expect UNKNOWN)"
+ );
+
+ await observerNotification;
+
+ equal(
+ ncs.DNSv4,
+ Ci.nsINetworkConnectivityService.OK,
+ "Check DNSv4 support (expect OK)"
+ );
+ equal(
+ ncs.DNSv6,
+ Ci.nsINetworkConnectivityService.OK,
+ "Check DNSv6 support (expect OK)"
+ );
+
+ httpserver = new HttpServer();
+ httpserver.registerPathHandler("/content", contentHandler);
+ httpserver.start(-1);
+
+ httpserverv6 = new HttpServer();
+ httpserverv6.registerPathHandler("/contentt", contentHandler);
+ httpserverv6._start(-1, "[::1]");
+
+ // Before setting the pref, this status is unknown in automation
+ equal(
+ ncs.IPv4,
+ Ci.nsINetworkConnectivityService.UNKNOWN,
+ "Check IPv4 support (expect UNKNOWN)"
+ );
+ equal(
+ ncs.IPv6,
+ Ci.nsINetworkConnectivityService.UNKNOWN,
+ "Check IPv6 support (expect UNKNOWN)"
+ );
+
+ Services.prefs.setBoolPref("network.captive-portal-service.testMode", true);
+ Services.prefs.setCharPref("network.connectivity-service.IPv4.url", URL);
+ Services.prefs.setCharPref("network.connectivity-service.IPv6.url", URLv6);
+ observerNotification = promiseObserverNotification(
+ "network:connectivity-service:ip-checks-complete"
+ );
+ ncs.recheckIPConnectivity();
+ await observerNotification;
+
+ equal(
+ ncs.IPv4,
+ Ci.nsINetworkConnectivityService.OK,
+ "Check IPv4 support (expect OK)"
+ );
+ equal(
+ ncs.IPv6,
+ Ci.nsINetworkConnectivityService.OK,
+ "Check IPv6 support (expect OK)"
+ );
+
+ // check that the CPS status is NOT_AVAILABLE when the endpoint is down.
+ await new Promise(resolve => httpserver.stop(resolve));
+ await new Promise(resolve => httpserverv6.stop(resolve));
+ observerNotification = promiseObserverNotification(
+ "network:connectivity-service:ip-checks-complete"
+ );
+ Services.obs.notifyObservers(null, "network:captive-portal-connectivity");
+ await observerNotification;
+
+ equal(
+ ncs.IPv4,
+ Ci.nsINetworkConnectivityService.NOT_AVAILABLE,
+ "Check IPv4 support (expect NOT_AVAILABLE)"
+ );
+ equal(
+ ncs.IPv6,
+ Ci.nsINetworkConnectivityService.NOT_AVAILABLE,
+ "Check IPv6 support (expect NOT_AVAILABLE)"
+ );
+});
diff --git a/netwerk/test/unit/test_networking_over_socket_process.js b/netwerk/test/unit/test_networking_over_socket_process.js
new file mode 100644
index 0000000000..79e17140e1
--- /dev/null
+++ b/netwerk/test/unit/test_networking_over_socket_process.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";
+
+var { setTimeout } = ChromeUtils.importESModule(
+ "resource://gre/modules/Timer.sys.mjs"
+);
+const { TestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TestUtils.sys.mjs"
+);
+
+let h2Port;
+let trrServer;
+
+const certOverrideService = Cc[
+ "@mozilla.org/security/certoverride;1"
+].getService(Ci.nsICertOverrideService);
+
+function setup() {
+ Services.prefs.setIntPref("network.max_socket_process_failed_count", 2);
+ trr_test_setup();
+
+ h2Port = Services.env.get("MOZHTTP2_PORT");
+ Assert.notEqual(h2Port, null);
+ Assert.notEqual(h2Port, "");
+
+ Assert.ok(mozinfo.socketprocess_networking);
+}
+
+setup();
+registerCleanupFunction(async () => {
+ Services.prefs.clearUserPref("network.max_socket_process_failed_count");
+ trr_clear_prefs();
+ if (trrServer) {
+ await trrServer.stop();
+ }
+});
+
+function makeChan(url) {
+ let chan = NetUtil.newChannel({
+ uri: url,
+ loadUsingSystemPrincipal: true,
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT,
+ }).QueryInterface(Ci.nsIHttpChannel);
+ 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));
+ });
+}
+
+add_task(async function setupTRRServer() {
+ 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("test.example.com", "HTTPS", {
+ answers: [
+ {
+ name: "test.example.com",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 1,
+ name: "test.example.com",
+ values: [
+ { key: "alpn", value: ["h2"] },
+ { key: "port", value: h2Port },
+ ],
+ },
+ },
+ ],
+ });
+
+ await trrServer.registerDoHAnswers("test.example.com", "A", {
+ answers: [
+ {
+ name: "test.example.com",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "127.0.0.1",
+ },
+ ],
+ });
+});
+
+async function doTestSimpleRequest(fromSocketProcess) {
+ let { inRecord } = await new TRRDNSListener("test.example.com", "127.0.0.1");
+ inRecord.QueryInterface(Ci.nsIDNSAddrRecord);
+ Assert.equal(inRecord.resolvedInSocketProcess(), fromSocketProcess);
+
+ let chan = makeChan(`https://test.example.com/server-timing`);
+ let [req] = await channelOpenPromise(chan);
+ // Test if this request is done by h2.
+ Assert.equal(req.getResponseHeader("x-connection-http2"), "yes");
+
+ let internal = chan.QueryInterface(Ci.nsIHttpChannelInternal);
+ Assert.equal(internal.isLoadedBySocketProcess, fromSocketProcess);
+}
+
+// Test if the data is loaded from socket process.
+add_task(async function testSimpleRequest() {
+ await doTestSimpleRequest(true);
+});
+
+function killSocketProcess(pid) {
+ const ProcessTools = Cc["@mozilla.org/processtools-service;1"].getService(
+ Ci.nsIProcessToolsService
+ );
+ ProcessTools.kill(pid);
+}
+
+// Test if socket process is restarted.
+add_task(async function testSimpleRequestAfterCrash() {
+ let socketProcessId = Services.io.socketProcessId;
+ info(`socket process pid is ${socketProcessId}`);
+ Assert.ok(socketProcessId != 0);
+
+ killSocketProcess(socketProcessId);
+
+ info("wait socket process restart...");
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => setTimeout(resolve, 1000));
+ Services.dns; // Needed to trigger socket process.
+ await TestUtils.waitForCondition(() => Services.io.socketProcessLaunched);
+
+ await doTestSimpleRequest(true);
+});
+
+// Test if data is loaded from parent process.
+add_task(async function testTooManyCrashes() {
+ let socketProcessId = Services.io.socketProcessId;
+ info(`socket process pid is ${socketProcessId}`);
+ Assert.ok(socketProcessId != 0);
+
+ let socketProcessCrashed = false;
+ Services.obs.addObserver(function observe(subject, topic, data) {
+ Services.obs.removeObserver(observe, topic);
+ socketProcessCrashed = true;
+ }, "network:socket-process-crashed");
+
+ killSocketProcess(socketProcessId);
+ await TestUtils.waitForCondition(() => socketProcessCrashed);
+ await doTestSimpleRequest(false);
+});
diff --git a/netwerk/test/unit/test_no_cookies_after_last_pb_exit.js b/netwerk/test/unit/test_no_cookies_after_last_pb_exit.js
new file mode 100644
index 0000000000..0f798ab0da
--- /dev/null
+++ b/netwerk/test/unit/test_no_cookies_after_last_pb_exit.js
@@ -0,0 +1,135 @@
+"use strict";
+
+do_get_profile();
+
+// This test checks that active private-browsing HTTP channels, do not save
+// cookies after the termination of the private-browsing session.
+
+// This test consists in following steps:
+// - starts a http server
+// - no cookies at this point
+// - does a beacon request in private-browsing mode
+// - after the completion of the request, a cookie should be set (cookie cleanup)
+// - does a beacon request in private-browsing mode and dispatch a
+// last-pb-context-exit notification
+// - after the completion of the request, no cookies should be set
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+let server;
+
+function setupServer() {
+ info("Starting the server...");
+
+ function beaconHandler(metadata, response) {
+ response.setHeader("Cache-Control", "max-age=10000", false);
+ response.setStatusLine(metadata.httpVersion, 204, "No Content");
+ response.setHeader("Set-Cookie", "a=b; path=/beacon; sameSite=lax", false);
+ response.bodyOutputStream.write("", 0);
+ }
+
+ server = new HttpServer();
+ server.registerPathHandler("/beacon", beaconHandler);
+ server.start(-1);
+ next();
+}
+
+function shutdownServer() {
+ info("Terminating the server...");
+ server.stop(next);
+}
+
+function sendRequest(notification) {
+ info("Sending a request...");
+
+ var privateLoadContext = Cu.createPrivateLoadContext();
+
+ var path =
+ "http://localhost:" +
+ server.identity.primaryPort +
+ "/beacon?" +
+ Math.random();
+
+ var uri = NetUtil.newURI(path);
+ var securityFlags =
+ Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL |
+ Ci.nsILoadInfo.SEC_COOKIES_INCLUDE;
+ var principal = Services.scriptSecurityManager.createContentPrincipal(uri, {
+ privateBrowsingId: 1,
+ });
+
+ var chan = NetUtil.newChannel({
+ uri,
+ loadingPrincipal: principal,
+ securityFlags,
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_BEACON,
+ });
+
+ chan.notificationCallbacks = Cu.createPrivateLoadContext();
+
+ let loadGroup = Cc["@mozilla.org/network/load-group;1"].createInstance(
+ Ci.nsILoadGroup
+ );
+
+ loadGroup.notificationCallbacks = Cu.createPrivateLoadContext();
+ chan.loadGroup = loadGroup;
+
+ chan.notificationCallbacks = privateLoadContext;
+ var channelListener = new ChannelListener(next, null, CL_ALLOW_UNKNOWN_CL);
+
+ if (notification) {
+ info("Sending notification...");
+ Services.obs.notifyObservers(null, "last-pb-context-exited");
+ }
+
+ chan.asyncOpen(channelListener);
+}
+
+function checkCookies(hasCookie) {
+ let cm = Services.cookies;
+ Assert.equal(
+ cm.cookieExists("localhost", "/beacon", "a", { privateBrowsingId: 1 }),
+ hasCookie
+ );
+ cm.removeAll();
+ next();
+}
+
+const steps = [
+ setupServer,
+
+ // no cookie at startup
+ () => checkCookies(false),
+
+ // no last-pb-context-exit notification
+ () => sendRequest(false),
+ () => checkCookies(true),
+
+ // last-pb-context-exit notification
+ () => sendRequest(true),
+ () => checkCookies(false),
+
+ shutdownServer,
+];
+
+function next() {
+ if (!steps.length) {
+ do_test_finished();
+ return;
+ }
+
+ steps.shift()();
+}
+
+function run_test() {
+ // We don't want to have CookieJarSettings blocking this test.
+ Services.prefs.setBoolPref(
+ "network.cookieJarSettings.unblocked_for_testing",
+ true
+ );
+
+ do_test_pending();
+ next();
+}
diff --git a/netwerk/test/unit/test_node_execute.js b/netwerk/test/unit/test_node_execute.js
new file mode 100644
index 0000000000..3640514a8e
--- /dev/null
+++ b/netwerk/test/unit/test_node_execute.js
@@ -0,0 +1,87 @@
+// This test checks that the interaction between NodeServer.execute defined in
+// httpd.js and the node server that we're interacting with defined in
+// moz-http2.js is working properly.
+/* global my_defined_var */
+
+"use strict";
+
+add_task(async function test_execute() {
+ function f() {
+ return "bla";
+ }
+ let id = await NodeServer.fork();
+ equal(await NodeServer.execute(id, `"hello"`), "hello");
+ equal(await NodeServer.execute(id, `(() => "hello")()`), "hello");
+ equal(await NodeServer.execute(id, `my_defined_var = 1;`), 1);
+ equal(await NodeServer.execute(id, `(() => my_defined_var)()`), 1);
+ equal(await NodeServer.execute(id, `my_defined_var`), 1);
+
+ await NodeServer.execute(id, `not_defined_var`)
+ .then(() => {
+ ok(false, "should have thrown");
+ })
+ .catch(e => {
+ equal(e.message, "ReferenceError: not_defined_var is not defined");
+ ok(
+ e.stack.includes("moz-http2-child.js"),
+ `stack should be coming from moz-http2-child.js - ${e.stack}`
+ );
+ });
+ await NodeServer.execute("definitely_wrong_id", `"hello"`)
+ .then(() => {
+ ok(false, "should have thrown");
+ })
+ .catch(e => {
+ equal(e.message, "Error: could not find id");
+ ok(
+ e.stack.includes("moz-http2.js"),
+ `stack should be coming from moz-http2.js - ${e.stack}`
+ );
+ });
+
+ // Defines f in the context of the node server.
+ // The implementation of NodeServer.execute prepends `functionName =` to the
+ // body of the function we pass so it gets attached to the global context
+ // in the server.
+ equal(await NodeServer.execute(id, f), undefined);
+ equal(await NodeServer.execute(id, `f()`), "bla");
+
+ class myClass {
+ static doStuff() {
+ return my_defined_var;
+ }
+ }
+
+ equal(await NodeServer.execute(id, myClass), undefined);
+ equal(await NodeServer.execute(id, `myClass.doStuff()`), 1);
+
+ equal(await NodeServer.kill(id), undefined);
+ await NodeServer.execute(id, `f()`)
+ .then(() => ok(false, "should throw"))
+ .catch(e => equal(e.message, "Error: could not find id"));
+ id = await NodeServer.fork();
+ // Check that a child process dying during a call throws an error.
+ await NodeServer.execute(id, `process.exit()`)
+ .then(() => ok(false, "should throw"))
+ .catch(e =>
+ equal(e.message, "child process exit closing code: 0 signal: null")
+ );
+
+ id = await NodeServer.fork();
+ equal(
+ await NodeServer.execute(
+ id,
+ `setTimeout(function() { sendBackResponse(undefined) }, 0); 2`
+ ),
+ 2
+ );
+ await new Promise(resolve => do_timeout(10, resolve));
+ await NodeServer.kill(id)
+ .then(() => ok(false, "should throw"))
+ .catch(e =>
+ equal(
+ e.message,
+ `forked process without handler sent: {"error":"","errorStack":""}\n`
+ )
+ );
+});
diff --git a/netwerk/test/unit/test_nojsredir.js b/netwerk/test/unit/test_nojsredir.js
new file mode 100644
index 0000000000..6b6831a222
--- /dev/null
+++ b/netwerk/test/unit/test_nojsredir.js
@@ -0,0 +1,67 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+var httpserver = new HttpServer();
+var index = 0;
+var tests = [
+ { url: "/test/test", datalen: 16 },
+
+ // Test that the http channel fails and the response body is suppressed
+ // bug 255119
+ {
+ url: "/test/test",
+ responseheader: ["Location: javascript:alert()"],
+ flags: CL_EXPECT_FAILURE,
+ datalen: 0,
+ },
+];
+
+function setupChannel(url) {
+ return NetUtil.newChannel({
+ uri: "http://localhost:" + httpserver.identity.primaryPort + url,
+ loadUsingSystemPrincipal: true,
+ });
+}
+
+function startIter() {
+ var channel = setupChannel(tests[index].url);
+ channel.asyncOpen(
+ new ChannelListener(completeIter, channel, tests[index].flags)
+ );
+}
+
+function completeIter(request, data, ctx) {
+ Assert.ok(data.length == tests[index].datalen);
+ if (++index < tests.length) {
+ startIter();
+ } else {
+ httpserver.stop(do_test_finished);
+ }
+}
+
+function run_test() {
+ httpserver.registerPathHandler("/test/test", handler);
+ httpserver.start(-1);
+
+ startIter();
+ do_test_pending();
+}
+
+function handler(metadata, response) {
+ var body = "thequickbrownfox";
+ response.setHeader("Content-Type", "text/plain", false);
+
+ var header = tests[index].responseheader;
+ if (header != undefined) {
+ for (var i = 0; i < header.length; i++) {
+ var splitHdr = header[i].split(": ");
+ response.setHeader(splitHdr[0], splitHdr[1], false);
+ }
+ }
+
+ response.setStatusLine(metadata.httpVersion, 302, "Redirected");
+ response.bodyOutputStream.write(body, body.length);
+}
diff --git a/netwerk/test/unit/test_non_ipv4_hostname_ending_in_number_cookie_db.js b/netwerk/test/unit/test_non_ipv4_hostname_ending_in_number_cookie_db.js
new file mode 100644
index 0000000000..2d4859857c
--- /dev/null
+++ b/netwerk/test/unit/test_non_ipv4_hostname_ending_in_number_cookie_db.js
@@ -0,0 +1,128 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Note that E2E test doesn't seem possible via setting a cookie
+// from xpcshell (main process) since:
+// 1. setCookieStringFromHttp requires a valid URL
+// 2. setCookieStringFromDocument requires access to the document
+// 3. Services.cookies.add() is just a backdoor that will bypass
+// Similarly, even with a browser test, in order to call
+// content.document.cookie we would need to SpecialPowers.spawn
+// into a BrowserTestUtils tab which requires a valid URL
+
+// not possible to make a valid url with non-ipv4 hostname ending in a number
+add_task(async function test_url_failure() {
+ let validUrl = Services.io.newURI("https://example.com/");
+ Assert.equal(validUrl.host, "example.com");
+
+ // ipv4 ending in number is fine
+ let validUrl2 = Services.io.newURI("https://1.2.3.4");
+ Assert.equal(validUrl2.host, "1.2.3.4");
+
+ // non-ipv4 ending in number is not
+ try {
+ Assert.throws(
+ () => {
+ Services.io.newURI("https://example.0");
+ },
+ /NS_ERROR_MALFORMED_URI/,
+ "non-ipv3 ending in number throws"
+ );
+ } catch {}
+});
+
+add_task(async function test_migrate_invalid_cookie() {
+ let profile = do_get_profile();
+ let dbFile = do_get_cookie_file(profile);
+ Assert.ok(!dbFile.exists());
+
+ let schema12db = new CookieDatabaseConnection(dbFile, 12);
+
+ let now = Date.now() * 1000; // date in microseconds
+ let futureExpiry = Math.round(now / 1e6 + 1000);
+
+ let cookie1 = new Cookie(
+ "cookie-name1",
+ "cookie-value1",
+ "cookie-host1.com",
+ "/", // path
+ futureExpiry,
+ now,
+ now,
+ false,
+ false,
+ false
+ );
+
+ // non-ipv4 urls that have a hostname ending in a number are now invalid
+ let badcookie = new Cookie(
+ "cookie-name",
+ "cookie-value",
+ "cookie-host.0",
+ "/", // path
+ futureExpiry,
+ now,
+ now,
+ false,
+ false,
+ false
+ );
+
+ let cookie2 = new Cookie(
+ "cookie-name2",
+ "cookie-value2",
+ "cookie-host2.com",
+ "/", // path
+ futureExpiry,
+ now,
+ now,
+ false,
+ false,
+ false
+ );
+
+ schema12db.insertCookie(cookie1);
+ schema12db.insertCookie(badcookie);
+ schema12db.insertCookie(cookie2);
+
+ // check that 3 cookies were added to the db
+ Assert.equal(do_count_cookies_in_db(schema12db.db), 3);
+ Assert.equal(do_count_cookies_in_db(schema12db.db, "cookie-host1.com"), 1);
+ Assert.equal(do_count_cookies_in_db(schema12db.db, "cookie-host2.com"), 1);
+ Assert.equal(do_count_cookies_in_db(schema12db.db, "cookie-host.0"), 1);
+
+ // start the cookie service, pull the data into memory
+ // check that cookie1 and cookie2 were brought into memory
+ // and that badcookie was not
+ let cookie1Exists = false;
+ let cookie2Exists = false;
+ let badcookieExists = false;
+ for (let cookie of Services.cookies.cookies) {
+ if (cookie.host == "cookie-host1.com") {
+ cookie1Exists = true;
+ }
+ if (cookie.host == "cookie-host2.com") {
+ cookie2Exists = true;
+ }
+ if (cookie.host == "cookie-host.0") {
+ badcookieExists = true;
+ }
+ }
+ Assert.ok(cookie1Exists, "Cookie 1 was inadvertently removed");
+ Assert.ok(cookie2Exists, "Cookie 2 was inadvertently removed");
+ Assert.ok(!badcookieExists, "Bad cookie was not filtered by migration");
+ // Schema was upgraded by cookie service
+ Assert.equal(schema12db.db.schemaVersion, 13);
+
+ // reload to make sure removal was written correctly
+ await promise_close_profile();
+ do_load_profile();
+
+ // check that the db was also updated
+ Assert.equal(do_count_cookies_in_db(schema12db.db), 2);
+ Assert.equal(do_count_cookies_in_db(schema12db.db, "cookie-host1.com"), 1);
+ Assert.equal(do_count_cookies_in_db(schema12db.db, "cookie-host2.com"), 1);
+ Assert.equal(do_count_cookies_in_db(schema12db.db, "cookie-host.0"), 0);
+
+ schema12db.close();
+});
diff --git a/netwerk/test/unit/test_nsIBufferedOutputStream_writeFrom_block.js b/netwerk/test/unit/test_nsIBufferedOutputStream_writeFrom_block.js
new file mode 100644
index 0000000000..577b2c6da8
--- /dev/null
+++ b/netwerk/test/unit/test_nsIBufferedOutputStream_writeFrom_block.js
@@ -0,0 +1,193 @@
+"use strict";
+
+var CC = Components.Constructor;
+
+var Pipe = CC("@mozilla.org/pipe;1", Ci.nsIPipe, "init");
+var BufferedOutputStream = CC(
+ "@mozilla.org/network/buffered-output-stream;1",
+ Ci.nsIBufferedOutputStream,
+ "init"
+);
+var ScriptableInputStream = CC(
+ "@mozilla.org/scriptableinputstream;1",
+ Ci.nsIScriptableInputStream,
+ "init"
+);
+
+// Verify that pipes behave as we expect. Subsequent tests assume
+// pipes behave as demonstrated here.
+add_test(function checkWouldBlockPipe() {
+ // Create a pipe with a one-byte buffer
+ var pipe = new Pipe(true, true, 1, 1);
+
+ // Writing two bytes should transfer only one byte, and
+ // return a partial count, not would-block.
+ Assert.equal(pipe.outputStream.write("xy", 2), 1);
+ Assert.equal(pipe.inputStream.available(), 1);
+
+ do_check_throws_nsIException(
+ () => pipe.outputStream.write("y", 1),
+ "NS_BASE_STREAM_WOULD_BLOCK"
+ );
+
+ // Check that nothing was written to the pipe.
+ Assert.equal(pipe.inputStream.available(), 1);
+ run_next_test();
+});
+
+// A writeFrom to a buffered stream should return
+// NS_BASE_STREAM_WOULD_BLOCK if no data was written.
+add_test(function writeFromBlocksImmediately() {
+ // Create a full pipe for our output stream. This will 'would-block' when
+ // written to.
+ var outPipe = new Pipe(true, true, 1, 1);
+ Assert.equal(outPipe.outputStream.write("x", 1), 1);
+
+ // Create a buffered stream, and fill its buffer, so the next write will
+ // try to flush.
+ var buffered = new BufferedOutputStream(outPipe.outputStream, 10);
+ Assert.equal(buffered.write("0123456789", 10), 10);
+
+ // Create a pipe with some data to be our input stream for the writeFrom
+ // call.
+ var inPipe = new Pipe(true, true, 1, 1);
+ Assert.equal(inPipe.outputStream.write("y", 1), 1);
+
+ Assert.equal(inPipe.inputStream.available(), 1);
+ do_check_throws_nsIException(
+ () => buffered.writeFrom(inPipe.inputStream, 1),
+ "NS_BASE_STREAM_WOULD_BLOCK"
+ );
+
+ // No data should have been consumed from the pipe.
+ Assert.equal(inPipe.inputStream.available(), 1);
+
+ run_next_test();
+});
+
+// A writeFrom to a buffered stream should return a partial count if any
+// data is written, when the last Flush call can only flush a portion of
+// the data.
+add_test(function writeFromReturnsPartialCountOnPartialFlush() {
+ // Create a pipe for our output stream. This will accept five bytes, and
+ // then 'would-block'.
+ var outPipe = new Pipe(true, true, 5, 1);
+
+ // Create a reference to the pipe's readable end that can be used
+ // from JavaScript.
+ var outPipeReadable = new ScriptableInputStream(outPipe.inputStream);
+
+ // Create a buffered stream whose buffer is too large to be flushed
+ // entirely to the output pipe.
+ var buffered = new BufferedOutputStream(outPipe.outputStream, 7);
+
+ // Create a pipe to be our input stream for the writeFrom call.
+ var inPipe = new Pipe(true, true, 15, 1);
+
+ // Write some data to our input pipe, for the rest of the test to consume.
+ Assert.equal(inPipe.outputStream.write("0123456789abcde", 15), 15);
+ Assert.equal(inPipe.inputStream.available(), 15);
+
+ // Write from the input pipe to the buffered stream. The buffered stream
+ // will fill its seven-byte buffer; and then the flush will only succeed
+ // in writing five bytes to the output pipe. The writeFrom call should
+ // return the number of bytes it consumed from inputStream.
+ Assert.equal(buffered.writeFrom(inPipe.inputStream, 11), 7);
+ Assert.equal(outPipe.inputStream.available(), 5);
+ Assert.equal(inPipe.inputStream.available(), 8);
+
+ // The partially-successful Flush should have created five bytes of
+ // available space in the buffered stream's buffer, so we should be able
+ // to write five bytes to it without blocking.
+ Assert.equal(buffered.writeFrom(inPipe.inputStream, 5), 5);
+ Assert.equal(outPipe.inputStream.available(), 5);
+ Assert.equal(inPipe.inputStream.available(), 3);
+
+ // Attempting to write any more data should would-block.
+ do_check_throws_nsIException(
+ () => buffered.writeFrom(inPipe.inputStream, 1),
+ "NS_BASE_STREAM_WOULD_BLOCK"
+ );
+
+ // No data should have been consumed from the pipe.
+ Assert.equal(inPipe.inputStream.available(), 3);
+
+ // Push the rest of the data through, checking that it all came through.
+ Assert.equal(outPipeReadable.available(), 5);
+ Assert.equal(outPipeReadable.read(5), "01234");
+ // Flush returns NS_ERROR_FAILURE if it can't transfer the full amount.
+ do_check_throws_nsIException(() => buffered.flush(), "NS_ERROR_FAILURE");
+ Assert.equal(outPipeReadable.available(), 5);
+ Assert.equal(outPipeReadable.read(5), "56789");
+ buffered.flush();
+ Assert.equal(outPipeReadable.available(), 2);
+ Assert.equal(outPipeReadable.read(2), "ab");
+ Assert.equal(buffered.writeFrom(inPipe.inputStream, 3), 3);
+ buffered.flush();
+ Assert.equal(outPipeReadable.available(), 3);
+ Assert.equal(outPipeReadable.read(3), "cde");
+
+ run_next_test();
+});
+
+// A writeFrom to a buffered stream should return a partial count if any
+// data is written, when the last Flush call blocks.
+add_test(function writeFromReturnsPartialCountOnBlock() {
+ // Create a pipe for our output stream. This will accept five bytes, and
+ // then 'would-block'.
+ var outPipe = new Pipe(true, true, 5, 1);
+
+ // Create a reference to the pipe's readable end that can be used
+ // from JavaScript.
+ var outPipeReadable = new ScriptableInputStream(outPipe.inputStream);
+
+ // Create a buffered stream whose buffer is too large to be flushed
+ // entirely to the output pipe.
+ var buffered = new BufferedOutputStream(outPipe.outputStream, 7);
+
+ // Create a pipe to be our input stream for the writeFrom call.
+ var inPipe = new Pipe(true, true, 15, 1);
+
+ // Write some data to our input pipe, for the rest of the test to consume.
+ Assert.equal(inPipe.outputStream.write("0123456789abcde", 15), 15);
+ Assert.equal(inPipe.inputStream.available(), 15);
+
+ // Write enough from the input pipe to the buffered stream to fill the
+ // output pipe's buffer, and then flush it. Nothing should block or fail,
+ // but the output pipe should now be full.
+ Assert.equal(buffered.writeFrom(inPipe.inputStream, 5), 5);
+ buffered.flush();
+ Assert.equal(outPipe.inputStream.available(), 5);
+ Assert.equal(inPipe.inputStream.available(), 10);
+
+ // Now try to write more from the input pipe than the buffered stream's
+ // buffer can hold. It will attempt to flush, but the output pipe will
+ // would-block without accepting any data. writeFrom should return the
+ // correct partial count.
+ Assert.equal(buffered.writeFrom(inPipe.inputStream, 10), 7);
+ Assert.equal(outPipe.inputStream.available(), 5);
+ Assert.equal(inPipe.inputStream.available(), 3);
+
+ // Attempting to write any more data should would-block.
+ do_check_throws_nsIException(
+ () => buffered.writeFrom(inPipe.inputStream, 3),
+ "NS_BASE_STREAM_WOULD_BLOCK"
+ );
+
+ // No data should have been consumed from the pipe.
+ Assert.equal(inPipe.inputStream.available(), 3);
+
+ // Push the rest of the data through, checking that it all came through.
+ Assert.equal(outPipeReadable.available(), 5);
+ Assert.equal(outPipeReadable.read(5), "01234");
+ // Flush returns NS_ERROR_FAILURE if it can't transfer the full amount.
+ do_check_throws_nsIException(() => buffered.flush(), "NS_ERROR_FAILURE");
+ Assert.equal(outPipeReadable.available(), 5);
+ Assert.equal(outPipeReadable.read(5), "56789");
+ Assert.equal(buffered.writeFrom(inPipe.inputStream, 3), 3);
+ buffered.flush();
+ Assert.equal(outPipeReadable.available(), 5);
+ Assert.equal(outPipeReadable.read(5), "abcde");
+
+ run_next_test();
+});
diff --git a/netwerk/test/unit/test_ntlm_authentication.js b/netwerk/test/unit/test_ntlm_authentication.js
new file mode 100644
index 0000000000..f17f7ebf7d
--- /dev/null
+++ b/netwerk/test/unit/test_ntlm_authentication.js
@@ -0,0 +1,268 @@
+// 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.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+// Turn off the authentication dialog blocking for this test.
+var prefs = Services.prefs;
+prefs.setIntPref("network.auth.subresource-http-auth-allow", 2);
+
+ChromeUtils.defineLazyGetter(this, "URL", function () {
+ return "http://localhost:" + httpserv.identity.primaryPort;
+});
+
+ChromeUtils.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;
+
+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) {
+ authInfo.username = this.user;
+ authInfo.password = this.pass;
+ 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);
+ },
+};
+
+function makeChan(url, loadingUrl) {
+ var principal = Services.scriptSecurityManager.createContentPrincipal(
+ Services.io.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,
+ });
+}
+
+// /auth/ntlm/simple
+function authNtlmSimple(metadata, response) {
+ if (!metadata.hasHeader("Authorization")) {
+ response.setStatusLine(metadata.httpVersion, 401, "Unauthorized");
+ response.setHeader("WWW-Authenticate", "NTLM", false);
+ return;
+ }
+
+ let challenge = metadata.getHeader("Authorization");
+ if (!challenge.startsWith("NTLM ")) {
+ response.setStatusLine(metadata.httpVersion, 401, "Unauthorized");
+ return;
+ }
+
+ let decoded = atob(challenge.substring(5));
+ info(decoded);
+
+ if (!decoded.startsWith("NTLMSSP\0")) {
+ response.setStatusLine(metadata.httpVersion, 401, "Unauthorized");
+ return;
+ }
+
+ let isNegotiate = decoded.substring(8).startsWith("\x01\x00\x00\x00");
+ let isAuthenticate = decoded.substring(8).startsWith("\x03\x00\x00\x00");
+
+ if (isNegotiate) {
+ response.setStatusLine(metadata.httpVersion, 401, "Unauthorized");
+ response.setHeader(
+ "WWW-Authenticate",
+ "NTLM TlRMTVNTUAACAAAAAAAAAAAoAAABggAAASNFZ4mrze8AAAAAAAAAAAAAAAAAAAAA",
+ false
+ );
+ return;
+ }
+
+ if (isAuthenticate) {
+ let body = "OK";
+ response.bodyOutputStream.write(body, body.length);
+ return;
+ }
+
+ // Something else went wrong.
+ response.setStatusLine(metadata.httpVersion, 401, "Unauthorized");
+}
+
+let httpserv;
+add_task(async function test_ntlm() {
+ Services.prefs.setBoolPref("network.auth.force-generic-ntlm", true);
+ Services.prefs.setBoolPref("network.auth.force-generic-ntlm-v1", true);
+
+ httpserv = new HttpServer();
+ httpserv.registerPathHandler("/auth/ntlm/simple", authNtlmSimple);
+ httpserv.start(-1);
+
+ registerCleanupFunction(async () => {
+ Services.prefs.clearUserPref("network.auth.force-generic-ntlm");
+ Services.prefs.clearUserPref("network.auth.force-generic-ntlm-v1");
+
+ await httpserv.stop();
+ });
+
+ var chan = makeChan(URL + "/auth/ntlm/simple", URL);
+
+ chan.notificationCallbacks = new Requestor(FLAG_RETURN_FALSE, 2);
+ let [req, buf] = await new Promise(resolve => {
+ chan.asyncOpen(
+ new ChannelListener((req1, buf1) => resolve([req1, buf1]), null)
+ );
+ });
+ Assert.ok(buf);
+ Assert.equal(req.QueryInterface(Ci.nsIHttpChannel).responseStatus, 200);
+});
diff --git a/netwerk/test/unit/test_ntlm_proxy_and_web_auth.js b/netwerk/test/unit/test_ntlm_proxy_and_web_auth.js
new file mode 100644
index 0000000000..3435839275
--- /dev/null
+++ b/netwerk/test/unit/test_ntlm_proxy_and_web_auth.js
@@ -0,0 +1,362 @@
+// Unit tests for a NTLM authenticated proxy, proxying for a NTLM authenticated
+// web server.
+//
+// Currently the tests do not determine whether the Authentication dialogs have
+// been displayed.
+//
+
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+ChromeUtils.defineLazyGetter(this, "URL", function () {
+ return "http://localhost:" + httpserver.identity.primaryPort;
+});
+
+function AuthPrompt() {}
+
+AuthPrompt.prototype = {
+ user: "guest",
+ pass: "guest",
+
+ QueryInterface: ChromeUtils.generateQI(["nsIAuthPrompt2"]),
+
+ promptAuth: function ap_promptAuth(channel, level, authInfo) {
+ authInfo.username = this.user;
+ authInfo.password = this.pass;
+
+ return true;
+ },
+
+ asyncPromptAuth: function ap_async(chan, cb, ctx, lvl, info) {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
+ },
+};
+
+function Requestor() {}
+
+Requestor.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["nsIInterfaceRequestor"]),
+
+ getInterface: function requestor_gi(iid) {
+ if (iid.equals(Ci.nsIAuthPrompt2)) {
+ // Allow the prompt to store state by caching it here
+ if (!this.prompt) {
+ this.prompt = new AuthPrompt();
+ }
+ return this.prompt;
+ }
+
+ throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE);
+ },
+
+ prompt: null,
+};
+
+function makeChan(url, loadingUrl) {
+ var principal = Services.scriptSecurityManager.createContentPrincipal(
+ Services.io.newURI(loadingUrl),
+ {}
+ );
+ return NetUtil.newChannel({
+ uri: url,
+ loadingPrincipal: principal,
+ securityFlags: Ci.nsILoadInfo.SEC_REQUIRE_SAME_ORIGIN_INHERITS_SEC_CONTEXT,
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER,
+ });
+}
+
+function TestListener(resolve) {
+ this.resolve = resolve;
+}
+TestListener.prototype.onStartRequest = function (request, context) {
+ // Need to do the instanceof to allow request.responseStatus
+ // to be read.
+ if (!(request instanceof Ci.nsIHttpChannel)) {
+ dump("Expecting an HTTP channel");
+ }
+
+ Assert.equal(expectedResponse, request.responseStatus, "HTTP Status code");
+};
+TestListener.prototype.onStopRequest = function (request, context, status) {
+ Assert.equal(expectedRequests, requestsMade, "Number of requests made ");
+
+ this.resolve();
+};
+TestListener.prototype.onDataAvaiable = function (
+ request,
+ context,
+ stream,
+ offset,
+ count
+) {
+ read_stream(stream, count);
+};
+
+// NTLM Messages, for the received type 1 and 3 messages only check that they
+// are of the expected type.
+const NTLM_TYPE1_PREFIX = "NTLM TlRMTVNTUAABAAAA";
+const NTLM_TYPE2_PREFIX = "NTLM TlRMTVNTUAACAAAA";
+const NTLM_TYPE3_PREFIX = "NTLM TlRMTVNTUAADAAAA";
+const NTLM_PREFIX_LEN = 21;
+
+const NTLM_CHALLENGE =
+ NTLM_TYPE2_PREFIX +
+ "DAAMADAAAAABAoEAASNFZ4mrze8AAAAAAAAAAGIAYgA8AAAAR" +
+ "ABPAE0AQQBJAE4AAgAMAEQATwBNAEEASQBOAAEADABTAEUAUg" +
+ "BWAEUAUgAEABQAZABvAG0AYQBpAG4ALgBjAG8AbQADACIAcwB" +
+ "lAHIAdgBlAHIALgBkAG8AbQBhAGkAbgAuAGMAbwBtAAAAAAA=";
+
+const PROXY_CHALLENGE =
+ NTLM_TYPE2_PREFIX +
+ "DgAOADgAAAAFgooCqLNOPe2aZOAAAAAAAAAAAFAAUABGAAAA" +
+ "BgEAAAAAAA9HAFcATAAtAE0ATwBaAAIADgBHAFcATAAtAE0A" +
+ "TwBaAAEADgBHAFcATAAtAE0ATwBaAAQAAgAAAAMAEgBsAG8A" +
+ "YwBhAGwAaABvAHMAdAAHAAgAOKEwGEZL0gEAAAAA";
+
+// Proxy and Web server responses for the happy path scenario.
+// i.e. successful proxy auth and successful web server auth
+//
+function authHandler(metadata, response) {
+ let authorization;
+ let authPrefix;
+ switch (requestsMade) {
+ case 0:
+ // Proxy - First request to the Proxy resppond with a 407 to start auth
+ response.setStatusLine(metadata.httpVersion, 407, "Unauthorized");
+ response.setHeader("Proxy-Authenticate", "NTLM", false);
+ break;
+ case 1:
+ // Proxy - Expecting a type 1 negotiate message from the client
+ authorization = metadata.getHeader("Proxy-Authorization");
+ authPrefix = authorization.substring(0, NTLM_PREFIX_LEN);
+ Assert.equal(NTLM_TYPE1_PREFIX, authPrefix, "Expecting a Type 1 message");
+ response.setStatusLine(metadata.httpVersion, 407, "Unauthorized");
+ response.setHeader("Proxy-Authenticate", PROXY_CHALLENGE, false);
+ break;
+ case 2:
+ // Proxy - Expecting a type 3 Authenticate message from the client
+ // Will respond with a 401 to start web server auth sequence
+ authorization = metadata.getHeader("Proxy-Authorization");
+ authPrefix = authorization.substring(0, NTLM_PREFIX_LEN);
+ Assert.equal(NTLM_TYPE3_PREFIX, authPrefix, "Expecting a Type 3 message");
+ response.setStatusLine(metadata.httpVersion, 401, "Unauthorized");
+ response.setHeader("WWW-Authenticate", "NTLM", false);
+ break;
+ case 3:
+ // Web Server - Expecting a type 1 negotiate message from the client
+ authorization = metadata.getHeader("Authorization");
+ authPrefix = authorization.substring(0, NTLM_PREFIX_LEN);
+ Assert.equal(NTLM_TYPE1_PREFIX, authPrefix, "Expecting a Type 1 message");
+ response.setStatusLine(metadata.httpVersion, 401, "Unauthorized");
+ response.setHeader("WWW-Authenticate", NTLM_CHALLENGE, false);
+ break;
+ case 4:
+ // Web Server - Expecting a type 3 Authenticate message from the client
+ authorization = metadata.getHeader("Authorization");
+ authPrefix = authorization.substring(0, NTLM_PREFIX_LEN);
+ Assert.equal(NTLM_TYPE3_PREFIX, authPrefix, "Expecting a Type 3 message");
+ response.setStatusLine(metadata.httpVersion, 200, "Successful");
+ break;
+ default:
+ // We should be authenticated and further requests are permitted
+ authorization = metadata.getHeader("Authorization");
+ authorization = metadata.getHeader("Proxy-Authorization");
+ Assert.isnull(authorization);
+ response.setStatusLine(metadata.httpVersion, 200, "Successful");
+ }
+ requestsMade++;
+}
+
+// Proxy responses simulating an invalid proxy password
+// Note: that the connection should not be reused after the
+// proxy auth fails.
+//
+function authHandlerInvalidProxyPassword(metadata, response) {
+ let authorization;
+ let authPrefix;
+ switch (requestsMade) {
+ case 0:
+ // Proxy - First request respond with a 407 to initiate auth sequence
+ response.setStatusLine(metadata.httpVersion, 407, "Unauthorized");
+ response.setHeader("Proxy-Authenticate", "NTLM", false);
+ break;
+ case 1:
+ // Proxy - Expecting a type 1 negotiate message from the client
+ authorization = metadata.getHeader("Proxy-Authorization");
+ authPrefix = authorization.substring(0, NTLM_PREFIX_LEN);
+ Assert.equal(NTLM_TYPE1_PREFIX, authPrefix, "Expecting a Type 1 message");
+ response.setStatusLine(metadata.httpVersion, 407, "Unauthorized");
+ response.setHeader("Proxy-Authenticate", PROXY_CHALLENGE, false);
+ break;
+ case 2:
+ // Proxy - Expecting a type 3 Authenticate message from the client
+ // Respond with a 407 to indicate invalid credentials
+ //
+ authorization = metadata.getHeader("Proxy-Authorization");
+ authPrefix = authorization.substring(0, NTLM_PREFIX_LEN);
+ Assert.equal(NTLM_TYPE3_PREFIX, authPrefix, "Expecting a Type 3 message");
+ response.setStatusLine(metadata.httpVersion, 407, "Unauthorized");
+ response.setHeader("Proxy-Authenticate", "NTLM", false);
+ break;
+ default:
+ // Strictly speaking the connection should not be reused at this point
+ // and reaching here should be an error, but have commented out for now
+ //dump( "ERROR: NTLM Proxy Authentication, connection should not be reused");
+ //Assert.fail();
+ response.setStatusLine(metadata.httpVersion, 407, "Unauthorized");
+ }
+ requestsMade++;
+}
+
+// Proxy and web server responses simulating a successful Proxy auth
+// and a failed web server auth
+// Note: the connection should not be reused once the password failure is
+// detected
+function authHandlerInvalidWebPassword(metadata, response) {
+ let authorization;
+ let authPrefix;
+ switch (requestsMade) {
+ case 0:
+ // Proxy - First request return a 407 to start Proxy auth
+ response.setStatusLine(metadata.httpVersion, 407, "Unauthorized");
+ response.setHeader("Proxy-Authenticate", "NTLM", false);
+ break;
+ case 1:
+ // Proxy - Expecting a type 1 negotiate message from the client
+ authorization = metadata.getHeader("Proxy-Authorization");
+ authPrefix = authorization.substring(0, NTLM_PREFIX_LEN);
+ Assert.equal(NTLM_TYPE1_PREFIX, authPrefix, "Expecting a Type 1 message");
+ response.setStatusLine(metadata.httpVersion, 407, "Unauthorized");
+ response.setHeader("Proxy-Authenticate", NTLM_CHALLENGE, false);
+ break;
+ case 2:
+ // Proxy - Expecting a type 3 Authenticate message from the client
+ // Responds with a 401 to start web server auth
+ authorization = metadata.getHeader("Proxy-Authorization");
+ authPrefix = authorization.substring(0, NTLM_PREFIX_LEN);
+ Assert.equal(NTLM_TYPE3_PREFIX, authPrefix, "Expecting a Type 3 message");
+ response.setStatusLine(metadata.httpVersion, 401, "Unauthorized");
+ response.setHeader("WWW-Authenticate", "NTLM", false);
+ break;
+ case 3:
+ // Web Server - Expecting a type 1 negotiate message from the client
+ authorization = metadata.getHeader("Authorization");
+ authPrefix = authorization.substring(0, NTLM_PREFIX_LEN);
+ Assert.equal(NTLM_TYPE1_PREFIX, authPrefix, "Expecting a Type 1 message");
+ response.setStatusLine(metadata.httpVersion, 401, "Unauthorized");
+ response.setHeader("WWW-Authenticate", NTLM_CHALLENGE, false);
+ break;
+ case 4:
+ // Web Server - Expecting a type 3 Authenticate message from the client
+ // Respond with a 401 to restart the auth sequence.
+ authorization = metadata.getHeader("Authorization");
+ authPrefix = authorization.substring(0, NTLM_PREFIX_LEN);
+ Assert.equal(NTLM_TYPE3_PREFIX, authPrefix, "Expecting a Type 1 message");
+ response.setStatusLine(metadata.httpVersion, 401, "Unauthorized");
+ break;
+ default:
+ // We should not get called past step 4
+ Assert.ok(
+ false,
+ "ERROR: NTLM Auth failed connection should not be reused"
+ );
+ }
+ requestsMade++;
+}
+
+// Tests to run test_bad_proxy_pass and test_bad_web_pass are split into two stages
+// so that once we determine how detect password dialog displays we can check
+// that the are displayed correctly, i.e. proxy password should not be prompted
+// for when retrying the web server password
+
+var httpserver = null;
+function setup() {
+ httpserver = new HttpServer();
+ httpserver.start(-1);
+
+ Services.prefs.setCharPref("network.proxy.http", "localhost");
+ Services.prefs.setIntPref(
+ "network.proxy.http_port",
+ httpserver.identity.primaryPort
+ );
+ Services.prefs.setCharPref("network.proxy.no_proxies_on", "");
+ Services.prefs.setIntPref("network.proxy.type", 1);
+ Services.prefs.setBoolPref("network.proxy.allow_hijacking_localhost", true);
+
+ registerCleanupFunction(async () => {
+ 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.proxy.allow_hijacking_localhost");
+
+ await httpserver.stop();
+ });
+}
+setup();
+
+var expectedRequests = 0; // Number of HTTP requests that are expected
+var requestsMade = 0; // The number of requests that were made
+var expectedResponse = 0; // The response code
+// Note that any test failures in the HTTP handler
+// will manifest as a 500 response code
+
+// Common test setup
+// Parameters:
+// path - path component of the URL
+// handler - http handler function for the httpserver
+// requests - expected number oh http requests
+// response - expected http response code
+// clearCache - clear the authentication cache before running the test
+function setupTest(path, handler, requests, response, clearCache) {
+ requestsMade = 0;
+ expectedRequests = requests;
+ expectedResponse = response;
+
+ // clear the auth cache if requested
+ if (clearCache) {
+ dump("Clearing auth cache");
+ Cc["@mozilla.org/network/http-auth-manager;1"]
+ .getService(Ci.nsIHttpAuthManager)
+ .clearAll();
+ }
+
+ return new Promise(resolve => {
+ var chan = makeChan(URL + path, URL);
+ httpserver.registerPathHandler(path, handler);
+ chan.notificationCallbacks = new Requestor();
+ chan.asyncOpen(new TestListener(resolve));
+ });
+}
+
+// Happy code path
+// Succesful proxy and web server auth.
+add_task(async function test_happy_path() {
+ dump("RUNNING TEST: test_happy_path");
+ await setupTest("/auth", authHandler, 5, 200, 1);
+});
+
+// Failed proxy authentication
+add_task(async function test_bad_proxy_pass_stage01() {
+ dump("RUNNING TEST: test_bad_proxy_pass_stage01");
+ await setupTest("/auth", authHandlerInvalidProxyPassword, 4, 407, 1);
+});
+// Successful logon after failed proxy auth
+add_task(async function test_bad_proxy_pass_stage02() {
+ dump("RUNNING TEST: test_bad_proxy_pass_stage02");
+ await setupTest("/auth", authHandler, 5, 200, 0);
+});
+
+// successful proxy logon, unsuccessful web server sign on
+add_task(async function test_bad_web_pass_stage01() {
+ dump("RUNNING TEST: test_bad_web_pass_stage01");
+ await setupTest("/auth", authHandlerInvalidWebPassword, 5, 401, 1);
+});
+// successful logon after failed web server auth.
+add_task(async function test_bad_web_pass_stage02() {
+ dump("RUNNING TEST: test_bad_web_pass_stage02");
+ await setupTest("/auth", authHandler, 5, 200, 0);
+});
diff --git a/netwerk/test/unit/test_ntlm_proxy_auth.js b/netwerk/test/unit/test_ntlm_proxy_auth.js
new file mode 100644
index 0000000000..b6cb27890b
--- /dev/null
+++ b/netwerk/test/unit/test_ntlm_proxy_auth.js
@@ -0,0 +1,408 @@
+// Unit tests for a NTLM authenticated proxy
+//
+// Currently the tests do not determine whether the Authentication dialogs have
+// been displayed.
+//
+
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+ChromeUtils.defineLazyGetter(this, "URL", function () {
+ return "http://localhost:" + httpserver.identity.primaryPort;
+});
+
+function AuthPrompt() {}
+
+AuthPrompt.prototype = {
+ user: "guest",
+ pass: "guest",
+
+ QueryInterface: ChromeUtils.generateQI(["nsIAuthPrompt2"]),
+
+ promptAuth: function ap_promptAuth(channel, level, authInfo) {
+ authInfo.username = this.user;
+ authInfo.password = this.pass;
+
+ return true;
+ },
+
+ asyncPromptAuth: function ap_async(chan, cb, ctx, lvl, info) {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
+ },
+};
+
+function Requestor() {}
+
+Requestor.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["nsIInterfaceRequestor"]),
+
+ getInterface: function requestor_gi(iid) {
+ if (iid.equals(Ci.nsIAuthPrompt2)) {
+ // Allow the prompt to store state by caching it here
+ if (!this.prompt) {
+ this.prompt = new AuthPrompt();
+ }
+ return this.prompt;
+ }
+
+ throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE);
+ },
+
+ prompt: null,
+};
+
+function makeChan(url, loadingUrl) {
+ var principal = Services.scriptSecurityManager.createContentPrincipal(
+ Services.io.newURI(loadingUrl),
+ {}
+ );
+ return NetUtil.newChannel({
+ uri: url,
+ loadingPrincipal: principal,
+ securityFlags: Ci.nsILoadInfo.SEC_REQUIRE_SAME_ORIGIN_INHERITS_SEC_CONTEXT,
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER,
+ });
+}
+
+function TestListener(resolve) {
+ this.resolve = resolve;
+}
+TestListener.prototype.onStartRequest = function (request, context) {
+ // Need to do the instanceof to allow request.responseStatus
+ // to be read.
+ if (!(request instanceof Ci.nsIHttpChannel)) {
+ dump("Expecting an HTTP channel");
+ }
+
+ Assert.equal(expectedResponse, request.responseStatus, "HTTP Status code");
+};
+TestListener.prototype.onStopRequest = function (request, context, status) {
+ Assert.equal(expectedRequests, requestsMade, "Number of requests made ");
+ Assert.equal(
+ exptTypeOneCount,
+ ntlmTypeOneCount,
+ "Number of type one messages received"
+ );
+ Assert.equal(
+ exptTypeTwoCount,
+ ntlmTypeTwoCount,
+ "Number of type two messages received"
+ );
+
+ this.resolve();
+};
+TestListener.prototype.onDataAvaiable = function (
+ request,
+ context,
+ stream,
+ offset,
+ count
+) {
+ read_stream(stream, count);
+};
+
+// NTLM Messages, for the received type 1 and 3 messages only check that they
+// are of the expected type.
+const NTLM_TYPE1_PREFIX = "NTLM TlRMTVNTUAABAAAA";
+const NTLM_TYPE2_PREFIX = "NTLM TlRMTVNTUAACAAAA";
+const NTLM_TYPE3_PREFIX = "NTLM TlRMTVNTUAADAAAA";
+const NTLM_PREFIX_LEN = 21;
+
+const PROXY_CHALLENGE =
+ NTLM_TYPE2_PREFIX +
+ "DgAOADgAAAAFgooCqLNOPe2aZOAAAAAAAAAAAFAAUABGAAAA" +
+ "BgEAAAAAAA9HAFcATAAtAE0ATwBaAAIADgBHAFcATAAtAE0A" +
+ "TwBaAAEADgBHAFcATAAtAE0ATwBaAAQAAgAAAAMAEgBsAG8A" +
+ "YwBhAGwAaABvAHMAdAAHAAgAOKEwGEZL0gEAAAAA";
+
+// Proxy responses for the happy path scenario.
+// i.e. successful proxy auth
+//
+function successfulAuth(metadata, response) {
+ let authorization;
+ let authPrefix;
+ switch (requestsMade) {
+ case 0:
+ // Proxy - First request to the Proxy resppond with a 407 to start auth
+ response.setStatusLine(metadata.httpVersion, 407, "Unauthorized");
+ response.setHeader("Proxy-Authenticate", "NTLM", false);
+ break;
+ case 1:
+ // Proxy - Expecting a type 1 negotiate message from the client
+ authorization = metadata.getHeader("Proxy-Authorization");
+ authPrefix = authorization.substring(0, NTLM_PREFIX_LEN);
+ Assert.equal(NTLM_TYPE1_PREFIX, authPrefix, "Expecting a Type 1 message");
+ response.setStatusLine(metadata.httpVersion, 407, "Unauthorized");
+ response.setHeader("Proxy-Authenticate", PROXY_CHALLENGE, false);
+ break;
+ case 2:
+ // Proxy - Expecting a type 3 Authenticate message from the client
+ // Will respond with a 401 to start web server auth sequence
+ authorization = metadata.getHeader("Proxy-Authorization");
+ authPrefix = authorization.substring(0, NTLM_PREFIX_LEN);
+ Assert.equal(NTLM_TYPE3_PREFIX, authPrefix, "Expecting a Type 3 message");
+ response.setStatusLine(metadata.httpVersion, 200, "Successful");
+ break;
+ default:
+ // We should be authenticated and further requests are permitted
+ authorization = metadata.getHeader("Proxy-Authorization");
+ Assert.isnull(authorization);
+ response.setStatusLine(metadata.httpVersion, 200, "Successful");
+ }
+ requestsMade++;
+}
+
+// Proxy responses simulating an invalid proxy password
+// Note: that the connection should not be reused after the
+// proxy auth fails.
+//
+function failedAuth(metadata, response) {
+ let authorization;
+ let authPrefix;
+ switch (requestsMade) {
+ case 0:
+ // Proxy - First request respond with a 407 to initiate auth sequence
+ response.setStatusLine(metadata.httpVersion, 407, "Unauthorized");
+ response.setHeader("Proxy-Authenticate", "NTLM", false);
+ break;
+ case 1:
+ // Proxy - Expecting a type 1 negotiate message from the client
+ authorization = metadata.getHeader("Proxy-Authorization");
+ authPrefix = authorization.substring(0, NTLM_PREFIX_LEN);
+ Assert.equal(NTLM_TYPE1_PREFIX, authPrefix, "Expecting a Type 1 message");
+ response.setStatusLine(metadata.httpVersion, 407, "Unauthorized");
+ response.setHeader("Proxy-Authenticate", PROXY_CHALLENGE, false);
+ break;
+ case 2:
+ // Proxy - Expecting a type 3 Authenticate message from the client
+ // Respond with a 407 to indicate invalid credentials
+ authorization = metadata.getHeader("Proxy-Authorization");
+ authPrefix = authorization.substring(0, NTLM_PREFIX_LEN);
+ Assert.equal(NTLM_TYPE3_PREFIX, authPrefix, "Expecting a Type 3 message");
+ response.setStatusLine(metadata.httpVersion, 407, "Unauthorized");
+ response.setHeader("Proxy-Authenticate", "NTLM", false);
+ break;
+ default:
+ // Strictly speaking the connection should not be reused at this point
+ // commenting out for now.
+ dump("ERROR: NTLM Proxy Authentication, connection should not be reused");
+ // assert.fail();
+ response.setStatusLine(metadata.httpVersion, 407, "Unauthorized");
+ }
+ requestsMade++;
+}
+//
+// Simulate a connection reset once the connection has been authenticated
+// Detects bug 486508
+//
+function connectionReset(metadata, response) {
+ let authorization;
+ let authPrefix;
+ switch (requestsMade) {
+ case 0:
+ // Proxy - First request to the Proxy resppond with a 407 to start auth
+ response.setStatusLine(metadata.httpVersion, 407, "Unauthorized");
+ response.setHeader("Proxy-Authenticate", "NTLM", false);
+ break;
+ case 1:
+ // Proxy - Expecting a type 1 negotiate message from the client
+ authorization = metadata.getHeader("Proxy-Authorization");
+ authPrefix = authorization.substring(0, NTLM_PREFIX_LEN);
+ Assert.equal(NTLM_TYPE1_PREFIX, authPrefix, "Expecting a Type 1 message");
+ ntlmTypeOneCount++;
+ response.setStatusLine(metadata.httpVersion, 407, "Unauthorized");
+ response.setHeader("Proxy-Authenticate", PROXY_CHALLENGE, false);
+ break;
+ case 2:
+ authorization = metadata.getHeader("Proxy-Authorization");
+ authPrefix = authorization.substring(0, NTLM_PREFIX_LEN);
+ Assert.equal(NTLM_TYPE3_PREFIX, authPrefix, "Expecting a Type 3 message");
+ ntlmTypeTwoCount++;
+ try {
+ response.seizePower();
+ response.finish();
+ } catch (e) {
+ Assert.ok(false, "unexpected exception" + e);
+ }
+ break;
+ default:
+ // Should not get any further requests on this channel
+ dump("ERROR: NTLM Proxy Authentication, connection should not be reused");
+ Assert.ok(false);
+ }
+ requestsMade++;
+}
+
+//
+// Reset the connection after a negotiate message has been received
+//
+function connectionReset02(metadata, response) {
+ var connectionNumber = httpserver.connectionNumber;
+ switch (requestsMade) {
+ case 0:
+ // Proxy - First request to the Proxy respond with a 407 to start auth
+ response.setStatusLine(metadata.httpVersion, 407, "Unauthorized");
+ response.setHeader("Proxy-Authenticate", "NTLM", false);
+ Assert.equal(connectionNumber, httpserver.connectionNumber);
+ break;
+ case 1:
+ // eslint-disable-next-line no-fallthrough
+ default:
+ // Proxy - Expecting a type 1 negotiate message from the client
+ Assert.equal(connectionNumber, httpserver.connectionNumber);
+ var authorization = metadata.getHeader("Proxy-Authorization");
+ var authPrefix = authorization.substring(0, NTLM_PREFIX_LEN);
+ Assert.equal(NTLM_TYPE1_PREFIX, authPrefix, "Expecting a Type 1 message");
+ ntlmTypeOneCount++;
+ try {
+ response.seizePower();
+ response.finish();
+ } catch (e) {
+ Assert.ok(false, "unexpected exception" + e);
+ }
+ }
+ requestsMade++;
+}
+
+var httpserver = null;
+function setup() {
+ httpserver = new HttpServer();
+ httpserver.start(-1);
+
+ Services.prefs.setCharPref("network.proxy.http", "localhost");
+ Services.prefs.setIntPref(
+ "network.proxy.http_port",
+ httpserver.identity.primaryPort
+ );
+ Services.prefs.setCharPref("network.proxy.no_proxies_on", "");
+ Services.prefs.setIntPref("network.proxy.type", 1);
+ Services.prefs.setBoolPref("network.proxy.allow_hijacking_localhost", true);
+
+ registerCleanupFunction(async () => {
+ 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.proxy.allow_hijacking_localhost");
+
+ await httpserver.stop();
+ });
+}
+setup();
+
+var expectedRequests = 0; // Number of HTTP requests that are expected
+var requestsMade = 0; // The number of requests that were made
+var expectedResponse = 0; // The response code
+// Note that any test failures in the HTTP handler
+// will manifest as a 500 response code
+
+// Common test setup
+// Parameters:
+// path - path component of the URL
+// handler - http handler function for the httpserver
+// requests - expected number oh http requests
+// response - expected http response code
+// clearCache - clear the authentication cache before running the test
+function setupTest(path, handler, requests, response, clearCache) {
+ requestsMade = 0;
+ expectedRequests = requests;
+ expectedResponse = response;
+
+ // clear the auth cache if requested
+ if (clearCache) {
+ dump("Clearing auth cache");
+ Cc["@mozilla.org/network/http-auth-manager;1"]
+ .getService(Ci.nsIHttpAuthManager)
+ .clearAll();
+ }
+
+ return new Promise(resolve => {
+ var chan = makeChan(URL + path, URL);
+ httpserver.registerPathHandler(path, handler);
+ chan.notificationCallbacks = new Requestor();
+ chan.asyncOpen(new TestListener(resolve));
+ });
+}
+
+let ntlmTypeOneCount = 0; // The number of NTLM type one messages received
+let exptTypeOneCount = 0; // The number of NTLM type one messages that should be received
+let ntlmTypeTwoCount = 0; // The number of NTLM type two messages received
+let exptTypeTwoCount = 0; // The number of NTLM type two messages that should received
+
+// Happy code path
+// Successful proxy auth.
+async function test_happy_path() {
+ dump("RUNNING TEST: test_happy_path");
+ await setupTest("/auth", successfulAuth, 3, 200, 1);
+}
+
+// Failed proxy authentication
+async function test_failed_auth() {
+ dump("RUNNING TEST:failed auth ");
+ await setupTest("/auth", failedAuth, 4, 407, 1);
+}
+
+// Test connection reset, after successful auth
+async function test_connection_reset() {
+ dump("RUNNING TEST:connection reset ");
+ ntlmTypeOneCount = 0;
+ ntlmTypeTwoCount = 0;
+ exptTypeOneCount = 1;
+ exptTypeTwoCount = 1;
+ await setupTest("/auth", connectionReset, 3, 500, 1);
+}
+
+// Test connection reset after sending a negotiate.
+// Client should retry request using the same connection
+async function test_connection_reset02() {
+ dump("RUNNING TEST:connection reset ");
+ ntlmTypeOneCount = 0;
+ ntlmTypeTwoCount = 0;
+ let maxRetryAttempt = 5;
+ exptTypeOneCount = maxRetryAttempt;
+ exptTypeTwoCount = 0;
+
+ Services.prefs.setIntPref(
+ "network.http.request.max-attempts",
+ maxRetryAttempt
+ );
+
+ await setupTest("/auth", connectionReset02, maxRetryAttempt + 1, 500, 1);
+}
+
+add_task(
+ { pref_set: [["network.auth.use_redirect_for_retries", false]] },
+ test_happy_path
+);
+add_task(
+ { pref_set: [["network.auth.use_redirect_for_retries", false]] },
+ test_failed_auth
+);
+add_task(
+ { pref_set: [["network.auth.use_redirect_for_retries", false]] },
+ test_connection_reset
+);
+add_task(
+ { pref_set: [["network.auth.use_redirect_for_retries", false]] },
+ test_connection_reset02
+);
+
+add_task(
+ { pref_set: [["network.auth.use_redirect_for_retries", true]] },
+ test_happy_path
+);
+add_task(
+ { pref_set: [["network.auth.use_redirect_for_retries", true]] },
+ test_failed_auth
+);
+add_task(
+ { pref_set: [["network.auth.use_redirect_for_retries", true]] },
+ test_connection_reset
+);
+add_task(
+ { pref_set: [["network.auth.use_redirect_for_retries", true]] },
+ test_connection_reset02
+);
diff --git a/netwerk/test/unit/test_ntlm_web_auth.js b/netwerk/test/unit/test_ntlm_web_auth.js
new file mode 100644
index 0000000000..1325477bc6
--- /dev/null
+++ b/netwerk/test/unit/test_ntlm_web_auth.js
@@ -0,0 +1,251 @@
+// Unit tests for a NTLM authenticated web server.
+//
+// Currently the tests do not determine whether the Authentication dialogs have
+// been displayed.
+//
+
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+ChromeUtils.defineLazyGetter(this, "URL", function () {
+ return "http://localhost:" + httpserver.identity.primaryPort;
+});
+
+function AuthPrompt() {}
+
+AuthPrompt.prototype = {
+ user: "guest",
+ pass: "guest",
+
+ QueryInterface: ChromeUtils.generateQI(["nsIAuthPrompt2"]),
+
+ promptAuth: function ap_promptAuth(channel, level, authInfo) {
+ authInfo.username = this.user;
+ authInfo.password = this.pass;
+
+ return true;
+ },
+
+ asyncPromptAuth: function ap_async(chan, cb, ctx, lvl, info) {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
+ },
+};
+
+function Requestor() {}
+
+Requestor.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["nsIInterfaceRequestor"]),
+
+ getInterface: function requestor_gi(iid) {
+ if (iid.equals(Ci.nsIAuthPrompt2)) {
+ // Allow the prompt to store state by caching it here
+ if (!this.prompt) {
+ this.prompt = new AuthPrompt();
+ }
+ return this.prompt;
+ }
+
+ throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE);
+ },
+
+ prompt: null,
+};
+
+function makeChan(url, loadingUrl) {
+ var principal = Services.scriptSecurityManager.createContentPrincipal(
+ Services.io.newURI(loadingUrl),
+ {}
+ );
+ return NetUtil.newChannel({
+ uri: url,
+ loadingPrincipal: principal,
+ securityFlags: Ci.nsILoadInfo.SEC_REQUIRE_SAME_ORIGIN_INHERITS_SEC_CONTEXT,
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER,
+ });
+}
+
+function TestListener() {}
+TestListener.prototype.onStartRequest = function (request, context) {
+ // Need to do the instanceof to allow request.responseStatus
+ // to be read.
+ if (!(request instanceof Ci.nsIHttpChannel)) {
+ dump("Expecting an HTTP channel");
+ }
+
+ Assert.equal(expectedResponse, request.responseStatus, "HTTP Status code");
+};
+TestListener.prototype.onStopRequest = function (request, context, status) {
+ Assert.equal(expectedRequests, requestsMade, "Number of requests made ");
+
+ if (current_test < tests.length - 1) {
+ current_test++;
+ tests[current_test]();
+ } else {
+ do_test_pending();
+ httpserver.stop(do_test_finished);
+ }
+
+ do_test_finished();
+};
+TestListener.prototype.onDataAvaiable = function (
+ request,
+ context,
+ stream,
+ offset,
+ count
+) {
+ read_stream(stream, count);
+};
+
+// NTLM Messages, for the received type 1 and 3 messages only check that they
+// are of the expected type.
+const NTLM_TYPE1_PREFIX = "NTLM TlRMTVNTUAABAAAA";
+const NTLM_TYPE2_PREFIX = "NTLM TlRMTVNTUAACAAAA";
+const NTLM_TYPE3_PREFIX = "NTLM TlRMTVNTUAADAAAA";
+const NTLM_PREFIX_LEN = 21;
+
+const NTLM_CHALLENGE =
+ NTLM_TYPE2_PREFIX +
+ "DAAMADAAAAABAoEAASNFZ4mrze8AAAAAAAAAAGIAYgA8AAAAR" +
+ "ABPAE0AQQBJAE4AAgAMAEQATwBNAEEASQBOAAEADABTAEUAUg" +
+ "BWAEUAUgAEABQAZABvAG0AYQBpAG4ALgBjAG8AbQADACIAcwB" +
+ "lAHIAdgBlAHIALgBkAG8AbQBhAGkAbgAuAGMAbwBtAAAAAAA=";
+
+// Web server responses for the happy path scenario.
+// i.e. successful web server auth
+//
+function successfulAuth(metadata, response) {
+ let authorization;
+ let authPrefix;
+ switch (requestsMade) {
+ case 0:
+ // Web Server - Initial request
+ // Will respond with a 401 to start web server auth sequence
+ response.setStatusLine(metadata.httpVersion, 401, "Unauthorized");
+ response.setHeader("WWW-Authenticate", "NTLM", false);
+ break;
+ case 1:
+ // Web Server - Expecting a type 1 negotiate message from the client
+ authorization = metadata.getHeader("Authorization");
+ authPrefix = authorization.substring(0, NTLM_PREFIX_LEN);
+ Assert.equal(NTLM_TYPE1_PREFIX, authPrefix, "Expecting a Type 1 message");
+ response.setStatusLine(metadata.httpVersion, 401, "Unauthorized");
+ response.setHeader("WWW-Authenticate", NTLM_CHALLENGE, false);
+ break;
+ case 2:
+ // Web Server - Expecting a type 3 Authenticate message from the client
+ authorization = metadata.getHeader("Authorization");
+ authPrefix = authorization.substring(0, NTLM_PREFIX_LEN);
+ Assert.equal(NTLM_TYPE3_PREFIX, authPrefix, "Expecting a Type 3 message");
+ response.setStatusLine(metadata.httpVersion, 200, "Successful");
+ break;
+ default:
+ // We should be authenticated and further requests are permitted
+ authorization = metadata.getHeader("Authorization");
+ Assert.isnull(authorization);
+ response.setStatusLine(metadata.httpVersion, 200, "Successful");
+ }
+ requestsMade++;
+}
+
+// web server responses simulating an unsuccessful web server auth
+function failedAuth(metadata, response) {
+ let authorization;
+ let authPrefix;
+ switch (requestsMade) {
+ case 0:
+ // Web Server - First request return a 401 to start auth sequence
+ response.setStatusLine(metadata.httpVersion, 401, "Unauthorized");
+ response.setHeader("WWW-Authenticate", "NTLM", false);
+ break;
+ case 1:
+ // Web Server - Expecting a type 1 negotiate message from the client
+ authorization = metadata.getHeader("Authorization");
+ authPrefix = authorization.substring(0, NTLM_PREFIX_LEN);
+ Assert.equal(NTLM_TYPE1_PREFIX, authPrefix, "Expecting a Type 1 message");
+ response.setStatusLine(metadata.httpVersion, 401, "Unauthorized");
+ response.setHeader("WWW-Authenticate", NTLM_CHALLENGE, false);
+ break;
+ case 2:
+ // Web Server - Expecting a type 3 Authenticate message from the client
+ // Respond with a 401 to restart the auth sequence.
+ authorization = metadata.getHeader("Authorization");
+ authPrefix = authorization.substring(0, NTLM_PREFIX_LEN);
+ Assert.equal(NTLM_TYPE3_PREFIX, authPrefix, "Expecting a Type 1 message");
+ response.setStatusLine(metadata.httpVersion, 401, "Unauthorized");
+ break;
+ default:
+ // We should not get called past step 2
+ // Strictly speaking the connection should not be used again
+ // commented out for testing
+ // dump( "ERROR: NTLM Auth failed connection should not be reused");
+ //Assert.fail();
+ response.setHeader("WWW-Authenticate", "NTLM", false);
+ }
+ requestsMade++;
+}
+
+var tests = [test_happy_path, test_failed_auth];
+var current_test = 0;
+
+var httpserver = null;
+function run_test() {
+ httpserver = new HttpServer();
+ httpserver.start(-1);
+
+ tests[0]();
+}
+
+var expectedRequests = 0; // Number of HTTP requests that are expected
+var requestsMade = 0; // The number of requests that were made
+var expectedResponse = 0; // The response code
+// Note that any test failures in the HTTP handler
+// will manifest as a 500 response code
+
+// Common test setup
+// Parameters:
+// path - path component of the URL
+// handler - http handler function for the httpserver
+// requests - expected number oh http requests
+// response - expected http response code
+// clearCache - clear the authentication cache before running the test
+function setupTest(path, handler, requests, response, clearCache) {
+ requestsMade = 0;
+ expectedRequests = requests;
+ expectedResponse = response;
+
+ // clear the auth cache if requested
+ if (clearCache) {
+ dump("Clearing auth cache");
+ Cc["@mozilla.org/network/http-auth-manager;1"]
+ .getService(Ci.nsIHttpAuthManager)
+ .clearAll();
+ }
+
+ var chan = makeChan(URL + path, URL);
+ httpserver.registerPathHandler(path, handler);
+ chan.notificationCallbacks = new Requestor();
+ chan.asyncOpen(new TestListener());
+
+ return chan;
+}
+
+// Happy code path
+// Succesful web server auth.
+function test_happy_path() {
+ dump("RUNNING TEST: test_happy_path");
+ setupTest("/auth", successfulAuth, 3, 200, 1);
+
+ do_test_pending();
+}
+
+// Unsuccessful web server sign on
+function test_failed_auth() {
+ dump("RUNNING TEST: test_failed_auth");
+ setupTest("/auth", failedAuth, 3, 401, 1);
+
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_oblivious_http.js b/netwerk/test/unit/test_oblivious_http.js
new file mode 100644
index 0000000000..027426038d
--- /dev/null
+++ b/netwerk/test/unit/test_oblivious_http.js
@@ -0,0 +1,206 @@
+/* Any copyright is dedicated to the Public Domain.
+ * https://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+class ObliviousHttpTestRequest {
+ constructor(method, uri, headers, content) {
+ this.method = method;
+ this.uri = uri;
+ this.headers = headers;
+ this.content = content;
+ }
+}
+
+class ObliviousHttpTestResponse {
+ constructor(status, headers, content) {
+ this.status = status;
+ this.headers = headers;
+ this.content = content;
+ }
+}
+
+class ObliviousHttpTestCase {
+ constructor(request, response) {
+ this.request = request;
+ this.response = response;
+ }
+}
+
+add_task(async function test_oblivious_http() {
+ let testcases = [
+ new ObliviousHttpTestCase(
+ new ObliviousHttpTestRequest(
+ "GET",
+ NetUtil.newURI("https://example.com"),
+ { "X-Some-Header": "header value" },
+ ""
+ ),
+ new ObliviousHttpTestResponse(200, {}, "Hello, World!")
+ ),
+ new ObliviousHttpTestCase(
+ new ObliviousHttpTestRequest(
+ "POST",
+ NetUtil.newURI("http://example.test"),
+ { "X-Some-Header": "header value", "X-Some-Other-Header": "25" },
+ "Posting some content..."
+ ),
+ new ObliviousHttpTestResponse(
+ 418,
+ { "X-Teapot": "teapot" },
+ "I'm a teapot"
+ )
+ ),
+ new ObliviousHttpTestCase(
+ new ObliviousHttpTestRequest(
+ "GET",
+ NetUtil.newURI("http://example.test/404"),
+ { "X-Some-Header": "header value", "X-Some-Other-Header": "25" },
+ ""
+ ),
+ undefined // 404 relay
+ ),
+ ];
+
+ for (let testcase of testcases) {
+ await run_one_testcase(testcase);
+ }
+});
+
+async function run_one_testcase(testcase) {
+ let ohttp = Cc["@mozilla.org/network/oblivious-http;1"].getService(
+ Ci.nsIObliviousHttp
+ );
+ let ohttpServer = ohttp.server();
+
+ let httpServer = new HttpServer();
+ httpServer.registerPathHandler("/", function (request, response) {
+ let inputStream = Cc["@mozilla.org/scriptableinputstream;1"].createInstance(
+ Ci.nsIScriptableInputStream
+ );
+ inputStream.init(request.bodyInputStream);
+ let requestBody = inputStream.readBytes(inputStream.available());
+ let ohttpResponse = ohttpServer.decapsulate(stringToBytes(requestBody));
+ let bhttp = Cc["@mozilla.org/network/binary-http;1"].getService(
+ Ci.nsIBinaryHttp
+ );
+ let decodedRequest = bhttp.decodeRequest(ohttpResponse.request);
+ equal(decodedRequest.method, testcase.request.method);
+ equal(decodedRequest.scheme, testcase.request.uri.scheme);
+ equal(decodedRequest.authority, testcase.request.uri.hostPort);
+ equal(decodedRequest.path, testcase.request.uri.pathQueryRef);
+ for (
+ let i = 0;
+ i < decodedRequest.headerNames.length &&
+ i < decodedRequest.headerValues.length;
+ i++
+ ) {
+ equal(
+ decodedRequest.headerValues[i],
+ testcase.request.headers[decodedRequest.headerNames[i]]
+ );
+ }
+ equal(bytesToString(decodedRequest.content), testcase.request.content);
+
+ let responseHeaderNames = ["content-type"];
+ let responseHeaderValues = ["text/plain"];
+ for (let headerName of Object.keys(testcase.response.headers)) {
+ responseHeaderNames.push(headerName);
+ responseHeaderValues.push(testcase.response.headers[headerName]);
+ }
+ let binaryResponse = new BinaryHttpResponse(
+ testcase.response.status,
+ responseHeaderNames,
+ responseHeaderValues,
+ stringToBytes(testcase.response.content)
+ );
+ let responseBytes = bhttp.encodeResponse(binaryResponse);
+ let encResponse = ohttpResponse.encapsulate(responseBytes);
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "message/ohttp-res", false);
+ response.write(bytesToString(encResponse));
+ });
+ httpServer.start(-1);
+
+ let ohttpService = Cc[
+ "@mozilla.org/network/oblivious-http-service;1"
+ ].getService(Ci.nsIObliviousHttpService);
+ let relayURI = NetUtil.newURI(
+ `http://localhost:${httpServer.identity.primaryPort}/`
+ );
+ if (!testcase.response) {
+ relayURI = NetUtil.newURI(
+ `http://localhost:${httpServer.identity.primaryPort}/404`
+ );
+ }
+ let obliviousHttpChannel = ohttpService
+ .newChannel(relayURI, testcase.request.uri, ohttpServer.encodedConfig)
+ .QueryInterface(Ci.nsIHttpChannel);
+ for (let headerName of Object.keys(testcase.request.headers)) {
+ obliviousHttpChannel.setRequestHeader(
+ headerName,
+ testcase.request.headers[headerName],
+ false
+ );
+ }
+ if (testcase.request.method == "POST") {
+ let uploadChannel = obliviousHttpChannel.QueryInterface(
+ Ci.nsIUploadChannel2
+ );
+ ok(uploadChannel);
+ let bodyStream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(
+ Ci.nsIStringInputStream
+ );
+ bodyStream.setData(
+ testcase.request.content,
+ testcase.request.content.length
+ );
+ uploadChannel.explicitSetUploadStream(
+ bodyStream,
+ null,
+ -1,
+ testcase.request.method,
+ false
+ );
+ }
+ let response = await new Promise((resolve, reject) => {
+ NetUtil.asyncFetch(obliviousHttpChannel, function (inputStream, result) {
+ let scriptableInputStream = Cc[
+ "@mozilla.org/scriptableinputstream;1"
+ ].createInstance(Ci.nsIScriptableInputStream);
+ scriptableInputStream.init(inputStream);
+ try {
+ // If decoding failed just return undefined.
+ inputStream.available();
+ } catch (e) {
+ resolve(undefined);
+ return;
+ }
+ let responseBody = scriptableInputStream.readBytes(
+ inputStream.available()
+ );
+ resolve(responseBody);
+ });
+ });
+ if (testcase.response) {
+ equal(response, testcase.response.content);
+ for (let headerName of Object.keys(testcase.response.headers)) {
+ equal(
+ obliviousHttpChannel.getResponseHeader(headerName),
+ testcase.response.headers[headerName]
+ );
+ }
+ } else {
+ let relayChannel = obliviousHttpChannel.QueryInterface(
+ Ci.nsIObliviousHttpChannel
+ ).relayChannel;
+ equal(relayChannel.responseStatus, 404);
+ }
+ await new Promise((resolve, reject) => {
+ httpServer.stop(resolve);
+ });
+}
diff --git a/netwerk/test/unit/test_obs-fold.js b/netwerk/test/unit/test_obs-fold.js
new file mode 100644
index 0000000000..fbdc2a4c9d
--- /dev/null
+++ b/netwerk/test/unit/test_obs-fold.js
@@ -0,0 +1,75 @@
+/* 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.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+function makeChan(url) {
+ return NetUtil.newChannel({
+ uri: url,
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+}
+
+let body = "abcd";
+function request_handler1(metadata, response) {
+ response.seizePower();
+ response.write("HTTP/1.1 200 OK\r\n");
+ response.write("Content-Type: text/plain\r\n");
+ response.write("X-header-first: FIRSTVALUE\r\n");
+ response.write("X-header-second: 1; second\r\n");
+ response.write(`Content-Length: ${body.length}\r\n`);
+ response.write("\r\n");
+ response.write(body);
+ response.finish();
+}
+
+// This handler is for obs-fold
+// The line that contains X-header-second starts with a space. As a consequence
+// it gets folded into the previous line.
+function request_handler2(metadata, response) {
+ response.seizePower();
+ response.write("HTTP/1.1 200 OK\r\n");
+ response.write("Content-Type: text/plain\r\n");
+ response.write("X-header-first: FIRSTVALUE\r\n");
+ // Note the space at the begining of the line
+ response.write(" X-header-second: 1; second\r\n");
+ response.write(`Content-Length: ${body.length}\r\n`);
+ response.write("\r\n");
+ response.write(body);
+ response.finish();
+}
+
+add_task(async function test() {
+ let http_server = new HttpServer();
+ http_server.registerPathHandler("/test1", request_handler1);
+ http_server.registerPathHandler("/test2", request_handler2);
+ http_server.start(-1);
+ const port = http_server.identity.primaryPort;
+
+ let chan1 = makeChan(`http://localhost:${port}/test1`);
+ await new Promise(resolve => {
+ chan1.asyncOpen(new ChannelListener(resolve));
+ });
+ equal(chan1.getResponseHeader("X-header-first"), "FIRSTVALUE");
+ equal(chan1.getResponseHeader("X-header-second"), "1; second");
+
+ let chan2 = makeChan(`http://localhost:${port}/test2`);
+ await new Promise(resolve => {
+ chan2.asyncOpen(new ChannelListener(resolve));
+ });
+ equal(
+ chan2.getResponseHeader("X-header-first"),
+ "FIRSTVALUE X-header-second: 1; second"
+ );
+ Assert.throws(
+ () => chan2.getResponseHeader("X-header-second"),
+ /NS_ERROR_NOT_AVAILABLE/
+ );
+
+ await new Promise(resolve => http_server.stop(resolve));
+});
diff --git a/netwerk/test/unit/test_offline_status.js b/netwerk/test/unit/test_offline_status.js
new file mode 100644
index 0000000000..22fde0bd20
--- /dev/null
+++ b/netwerk/test/unit/test_offline_status.js
@@ -0,0 +1,15 @@
+"use strict";
+
+function run_test() {
+ try {
+ var linkService = Cc[
+ "@mozilla.org/network/network-link-service;1"
+ ].getService(Ci.nsINetworkLinkService);
+
+ // The offline status should depends on the link status
+ Assert.notEqual(Services.io.offline, linkService.isLinkUp);
+ } catch (e) {
+ // The network link service might not be available
+ Assert.equal(Services.io.offline, false);
+ }
+}
diff --git a/netwerk/test/unit/test_ohttp.js b/netwerk/test/unit/test_ohttp.js
new file mode 100644
index 0000000000..b7bd2f1d06
--- /dev/null
+++ b/netwerk/test/unit/test_ohttp.js
@@ -0,0 +1,41 @@
+/* Any copyright is dedicated to the Public Domain.
+ * https://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+function test_known_config() {
+ let ohttp = Cc["@mozilla.org/network/oblivious-http;1"].getService(
+ Ci.nsIObliviousHttp
+ );
+ let encodedConfig = hexStringToBytes(
+ "0100209403aafe76dfd4568481e04e44b42d744287eae4070b50e48baa7a91a4e80d5600080001000100010003"
+ );
+ let request = hexStringToBytes(
+ "00034745540568747470730b6578616d706c652e636f6d012f"
+ );
+ let ohttpRequest = ohttp.encapsulateRequest(encodedConfig, request);
+ ok(ohttpRequest);
+}
+
+function test_with_server() {
+ let ohttp = Cc["@mozilla.org/network/oblivious-http;1"].getService(
+ Ci.nsIObliviousHttp
+ );
+ let server = ohttp.server();
+ ok(server.encodedConfig);
+ let request = hexStringToBytes(
+ "00034745540568747470730b6578616d706c652e636f6d012f"
+ );
+ let ohttpRequest = ohttp.encapsulateRequest(server.encodedConfig, request);
+ let ohttpResponse = server.decapsulate(ohttpRequest.encRequest);
+ ok(ohttpResponse);
+ deepEqual(ohttpResponse.request, request);
+ let response = hexStringToBytes("0140c8");
+ let encResponse = ohttpResponse.encapsulate(response);
+ deepEqual(ohttpRequest.response.decapsulate(encResponse), response);
+}
+
+function run_test() {
+ test_known_config();
+ test_with_server();
+}
diff --git a/netwerk/test/unit/test_orb_empty_header.js b/netwerk/test/unit/test_orb_empty_header.js
new file mode 100644
index 0000000000..24866b7073
--- /dev/null
+++ b/netwerk/test/unit/test_orb_empty_header.js
@@ -0,0 +1,83 @@
+/* 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 */
+/* import-globals-from head_servers.js */
+
+function makeChan(uri) {
+ var principal =
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ "http://example.com"
+ );
+ let chan = NetUtil.newChannel({
+ uri,
+ loadingPrincipal: principal,
+ securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_INHERITS_SEC_CONTEXT,
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER,
+ }).QueryInterface(Ci.nsIHttpChannel);
+ chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI;
+ return chan;
+}
+
+function inChildProcess() {
+ return Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT;
+}
+
+async function setup() {
+ if (!inChildProcess()) {
+ Services.prefs.setBoolPref("browser.opaqueResponseBlocking", true);
+ }
+ let server = new NodeHTTPServer();
+ await server.start();
+ registerCleanupFunction(async () => {
+ await server.stop();
+ });
+ await server.registerPathHandler("/dosniff", (req, resp) => {
+ resp.writeHead(500, {
+ "Content-Type": "application/json",
+ "Set-Cookie": "mycookie",
+ });
+ resp.write("good");
+ resp.end("done");
+ });
+ await server.registerPathHandler("/nosniff", (req, resp) => {
+ resp.writeHead(500, {
+ "Content-Type": "application/msword",
+ "Set-Cookie": "mycookie",
+ });
+ resp.write("good");
+ resp.end("done");
+ });
+
+ return server;
+}
+async function test_empty_header(server, doSniff) {
+ let chan;
+ if (doSniff) {
+ chan = makeChan(`${server.origin()}/dosniff`);
+ } else {
+ chan = makeChan(`${server.origin()}/nosniff`);
+ }
+ let req = await new Promise(resolve => {
+ chan.asyncOpen(new ChannelListener(resolve, null, CL_EXPECT_FAILURE));
+ });
+ equal(req.status, Cr.NS_ERROR_FAILURE);
+ equal(req.QueryInterface(Ci.nsIHttpChannel).responseStatus, 500);
+
+ req.visitResponseHeaders({
+ visitHeader: function visit(_aName, _aValue) {
+ ok(false);
+ },
+ });
+}
+
+add_task(async function () {
+ let server = await setup();
+ await test_empty_header(server, true);
+ await test_empty_header(server, false);
+});
diff --git a/netwerk/test/unit/test_origin.js b/netwerk/test/unit/test_origin.js
new file mode 100644
index 0000000000..9ef86e8885
--- /dev/null
+++ b/netwerk/test/unit/test_origin.js
@@ -0,0 +1,323 @@
+"use strict";
+
+var h2Port;
+var prefs;
+var http2pref;
+var extpref;
+var loadGroup;
+
+function run_test() {
+ h2Port = Services.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 = Services.prefs;
+
+ http2pref = prefs.getBoolPref("network.http.http2.enabled");
+ extpref = prefs.getBoolPref("network.http.originextension");
+
+ prefs.setBoolPref("network.http.http2.enabled", 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.http2.enabled", 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);
+}
+
+let origin;
+var nextTest;
+var nextPortExpectedToBeSame = false;
+var currentPort = 0;
+var forceReload = false;
+var forceFailListener = 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();
+ },
+};
+
+var FailListener = function () {};
+FailListener.prototype = {
+ onStartRequest: function testOnStartRequest(request) {
+ Assert.ok(request instanceof Ci.nsIHttpChannel);
+ Assert.ok(!Components.isSuccessCode(request.status));
+ },
+ onDataAvailable: function testOnDataAvailable(request, stream, off, cnt) {
+ read_stream(stream, cnt);
+ },
+ onStopRequest: function testOnStopRequest(request, status) {
+ Assert.ok(!Components.isSuccessCode(request.status));
+ nextTest();
+ do_test_finished();
+ },
+};
+
+function testsDone() {
+ dump("testsDone\n");
+ resetPrefs();
+}
+
+function doTest() {
+ dump("execute doTest " + origin + "\n");
+ var chan = makeChan(origin);
+ var listener;
+ if (!forceFailListener) {
+ listener = new Listener();
+ } else {
+ listener = new FailListener();
+ }
+ forceFailListener = false;
+
+ if (!forceReload) {
+ chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI;
+ } else {
+ chan.loadFlags =
+ Ci.nsIRequest.LOAD_FRESH_CONNECTION |
+ Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI;
+ }
+ forceReload = false;
+ 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() {
+ // plain connection reuse
+ dump("doTest2()\n");
+ origin = "https://foo.example.com:" + h2Port + "/origin-2";
+ nextTest = doTest3;
+ nextPortExpectedToBeSame = true;
+ do_test_pending();
+ doTest();
+}
+
+function doTest3() {
+ // 7540 style coalescing
+ dump("doTest3()\n");
+ origin = "https://alt1.example.com:" + h2Port + "/origin-3";
+ nextTest = doTest4;
+ nextPortExpectedToBeSame = true;
+ do_test_pending();
+ doTest();
+}
+
+function doTest4() {
+ // forces an empty origin frame to be omitted
+ dump("doTest4()\n");
+ origin = "https://foo.example.com:" + h2Port + "/origin-4";
+ nextTest = doTest5;
+ nextPortExpectedToBeSame = true;
+ do_test_pending();
+ doTest();
+}
+
+function doTest5() {
+ // 7540 style coalescing should not work due to empty origin set
+ dump("doTest5()\n");
+ origin = "https://alt1.example.com:" + h2Port + "/origin-5";
+ nextTest = doTest6;
+ nextPortExpectedToBeSame = false;
+ do_test_pending();
+ doTest();
+}
+
+function doTest6() {
+ // get a fresh connection with alt1 and alt2 in origin set
+ // note that there is no dns for alt2
+ dump("doTest6()\n");
+ origin = "https://foo.example.com:" + h2Port + "/origin-6";
+ nextTest = doTest7;
+ nextPortExpectedToBeSame = false;
+ forceReload = true;
+ do_test_pending();
+ doTest();
+}
+
+function doTest7() {
+ // check conn reuse to ensure sni is implicit in origin set
+ dump("doTest7()\n");
+ origin = "https://foo.example.com:" + h2Port + "/origin-7";
+ nextTest = doTest8;
+ nextPortExpectedToBeSame = true;
+ do_test_pending();
+ doTest();
+}
+
+function doTest8() {
+ // alt1 is in origin set (and is 7540 eligible)
+ dump("doTest8()\n");
+ origin = "https://alt1.example.com:" + h2Port + "/origin-8";
+ nextTest = doTest9;
+ nextPortExpectedToBeSame = true;
+ do_test_pending();
+ doTest();
+}
+
+function doTest9() {
+ // alt2 is in origin set but does not have dns
+ dump("doTest9()\n");
+ origin = "https://alt2.example.com:" + h2Port + "/origin-9";
+ nextTest = doTest10;
+ nextPortExpectedToBeSame = true;
+ do_test_pending();
+ doTest();
+}
+
+function doTest10() {
+ // bar is in origin set but does not have dns like alt2
+ // but the cert is not valid for bar. so expect a failure
+ dump("doTest10()\n");
+ origin = "https://bar.example.com:" + h2Port + "/origin-10";
+ nextTest = doTest11;
+ nextPortExpectedToBeSame = false;
+ forceFailListener = true;
+ do_test_pending();
+ doTest();
+}
+
+var Http2PushApiListener = function () {};
+
+Http2PushApiListener.prototype = {
+ fooOK: false,
+ alt1OK: false,
+
+ getInterface(aIID) {
+ return this.QueryInterface(aIID);
+ },
+
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIHttpPushListener",
+ "nsIStreamListener",
+ ]),
+
+ // nsIHttpPushListener
+ onPush: function onPush(associatedChannel, pushChannel) {
+ dump(
+ "push api onpush " +
+ pushChannel.originalURI.spec +
+ " associated to " +
+ associatedChannel.originalURI.spec +
+ "\n"
+ );
+
+ Assert.equal(
+ associatedChannel.originalURI.spec,
+ "https://foo.example.com:" + h2Port + "/origin-11-a"
+ );
+ Assert.equal(pushChannel.getRequestHeader("x-pushed-request"), "true");
+
+ if (
+ pushChannel.originalURI.spec ===
+ "https://foo.example.com:" + h2Port + "/origin-11-b"
+ ) {
+ this.fooOK = true;
+ } else if (
+ pushChannel.originalURI.spec ===
+ "https://alt1.example.com:" + h2Port + "/origin-11-e"
+ ) {
+ this.alt1OK = true;
+ } else {
+ // any push of bar or madeup should not end up in onPush()
+ Assert.equal(true, false);
+ }
+ pushChannel.cancel(Cr.NS_ERROR_ABORT);
+ },
+
+ // normal Channel listeners
+ onStartRequest: function pushAPIOnStart(request) {
+ dump("push api onstart " + request.originalURI.spec + "\n");
+ },
+
+ onDataAvailable: function pushAPIOnDataAvailable(
+ request,
+ stream,
+ offset,
+ cnt
+ ) {
+ read_stream(stream, cnt);
+ },
+
+ onStopRequest: function test_onStopR(request, status) {
+ dump("push api onstop " + request.originalURI.spec + "\n");
+ Assert.ok(this.fooOK);
+ Assert.ok(this.alt1OK);
+ nextTest();
+ do_test_finished();
+ },
+};
+
+function doTest11() {
+ // we are connected with an SNI of foo from test6
+ // but the origin set is alt1, alt2, bar - foo is implied
+ // and bar is not actually covered by the cert
+ //
+ // the server will push foo (b-OK), bar (c-NOT OK), madeup (d-NOT OK), alt1 (e-OK),
+
+ dump("doTest11()\n");
+ do_test_pending();
+ loadGroup = Cc["@mozilla.org/network/load-group;1"].createInstance(
+ Ci.nsILoadGroup
+ );
+ var chan = makeChan("https://foo.example.com:" + h2Port + "/origin-11-a");
+ chan.loadGroup = loadGroup;
+ var listener = new Http2PushApiListener();
+ nextTest = testsDone;
+ chan.notificationCallbacks = listener;
+ chan.asyncOpen(listener);
+}
diff --git a/netwerk/test/unit/test_original_sent_received_head.js b/netwerk/test/unit/test_original_sent_received_head.js
new file mode 100644
index 0000000000..744528b832
--- /dev/null
+++ b/netwerk/test/unit/test_original_sent_received_head.js
@@ -0,0 +1,249 @@
+//
+// HTTP headers test
+// Response headers can be changed after they have been received, e.g. empty
+// headers are deleted, some duplicate header are merged (if no error is
+// thrown), etc.
+//
+// The "original header" is introduced to hold the header array in the order
+// and the form as they have been received from the network.
+// Here, the "original headers" are tested.
+//
+// Original headers will be stored in the cache as well. This test checks
+// that too.
+
+// Note: sets Cc and Ci variables
+
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+ChromeUtils.defineLazyGetter(this, "URL", function () {
+ return "http://localhost:" + httpserver.identity.primaryPort;
+});
+
+var httpserver = new HttpServer();
+var testpath = "/simple";
+var httpbody = "0123456789";
+
+var dbg = 1;
+
+function run_test() {
+ if (dbg) {
+ print("============== START ==========");
+ }
+
+ httpserver.registerPathHandler(testpath, serverHandler);
+ httpserver.start(-1);
+ run_next_test();
+}
+
+add_test(function test_headerChange() {
+ if (dbg) {
+ print("============== test_headerChange setup: in");
+ }
+
+ var channel1 = setupChannel(testpath);
+ channel1.loadFlags = Ci.nsIRequest.LOAD_BYPASS_CACHE;
+
+ // ChannelListener defined in head_channels.js
+ channel1.asyncOpen(new ChannelListener(checkResponse, null));
+
+ if (dbg) {
+ print("============== test_headerChange setup: out");
+ }
+});
+
+add_test(function test_fromCache() {
+ if (dbg) {
+ print("============== test_fromCache setup: in");
+ }
+
+ var channel2 = setupChannel(testpath);
+ channel2.loadFlags = Ci.nsIRequest.LOAD_FROM_CACHE;
+
+ // ChannelListener defined in head_channels.js
+ channel2.asyncOpen(new ChannelListener(checkResponse, null));
+
+ if (dbg) {
+ print("============== test_fromCache setup: out");
+ }
+});
+
+add_test(function finish() {
+ if (dbg) {
+ print("============== STOP ==========");
+ }
+ httpserver.stop(do_test_finished);
+});
+
+function setupChannel(path) {
+ var chan = NetUtil.newChannel({
+ uri: URL + path,
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+ chan.requestMethod = "GET";
+ return chan;
+}
+
+function serverHandler(metadata, response) {
+ if (dbg) {
+ print("============== serverHandler: in");
+ }
+
+ let etag;
+ try {
+ etag = metadata.getHeader("If-None-Match");
+ } catch (ex) {
+ etag = "";
+ }
+ if (etag == "testtag") {
+ if (dbg) {
+ print("============== 304 answerr: in");
+ }
+ response.setStatusLine("1.1", 304, "Not Modified");
+ } else {
+ response.setHeader("Content-Type", "text/plain", false);
+ response.setStatusLine("1.1", 200, "OK");
+
+ // Set a empty header. A empty link header will not appear in header list,
+ // but in the "original headers", it will be still exactly as received.
+ response.setHeaderNoCheck("Link", "", true);
+ response.setHeaderNoCheck("Link", "value1");
+ response.setHeaderNoCheck("Link", "value2");
+ response.setHeaderNoCheck("Location", "loc");
+ response.setHeader("Cache-Control", "max-age=10000", false);
+ response.setHeader("ETag", "testtag", false);
+ response.bodyOutputStream.write(httpbody, httpbody.length);
+ }
+ if (dbg) {
+ print("============== serverHandler: out");
+ }
+}
+
+function checkResponse(request, data, context) {
+ if (dbg) {
+ print("============== checkResponse: in");
+ }
+
+ request.QueryInterface(Ci.nsIHttpChannel);
+ Assert.equal(request.responseStatus, 200);
+ Assert.equal(request.responseStatusText, "OK");
+ Assert.ok(request.requestSucceeded);
+
+ // Response header have only one link header.
+ var linkHeaderFound = 0;
+ var locationHeaderFound = 0;
+ request.visitResponseHeaders({
+ visitHeader: function visit(aName, aValue) {
+ if (aName == "link") {
+ linkHeaderFound++;
+ Assert.equal(aValue, "value1, value2");
+ }
+ if (aName == "location") {
+ locationHeaderFound++;
+ Assert.equal(aValue, "loc");
+ }
+ },
+ });
+ Assert.equal(linkHeaderFound, 1);
+ Assert.equal(locationHeaderFound, 1);
+
+ // The "original header" still contains 3 link headers.
+ var linkOrgHeaderFound = 0;
+ var locationOrgHeaderFound = 0;
+ request.visitOriginalResponseHeaders({
+ visitHeader: function visitOrg(aName, aValue) {
+ if (aName == "link") {
+ if (linkOrgHeaderFound == 0) {
+ Assert.equal(aValue, "");
+ } else if (linkOrgHeaderFound == 1) {
+ Assert.equal(aValue, "value1");
+ } else {
+ Assert.equal(aValue, "value2");
+ }
+ linkOrgHeaderFound++;
+ }
+ if (aName == "location") {
+ locationOrgHeaderFound++;
+ Assert.equal(aValue, "loc");
+ }
+ },
+ });
+ Assert.equal(linkOrgHeaderFound, 3);
+ Assert.equal(locationOrgHeaderFound, 1);
+
+ if (dbg) {
+ print("============== Remove headers");
+ }
+ // Remove header.
+ request.setResponseHeader("Link", "", false);
+ request.setResponseHeader("Location", "", false);
+
+ var linkHeaderFound2 = false;
+ var locationHeaderFound2 = 0;
+ request.visitResponseHeaders({
+ visitHeader: function visit(aName, aValue) {
+ if (aName == "Link") {
+ linkHeaderFound2 = true;
+ }
+ if (aName == "Location") {
+ locationHeaderFound2 = true;
+ }
+ },
+ });
+ Assert.ok(!linkHeaderFound2, "There should be no link header");
+ Assert.ok(!locationHeaderFound2, "There should be no location headers.");
+
+ // The "original header" still contains the empty header.
+ var linkOrgHeaderFound2 = 0;
+ var locationOrgHeaderFound2 = 0;
+ request.visitOriginalResponseHeaders({
+ visitHeader: function visitOrg(aName, aValue) {
+ if (aName == "link") {
+ if (linkOrgHeaderFound2 == 0) {
+ Assert.equal(aValue, "");
+ } else if (linkOrgHeaderFound2 == 1) {
+ Assert.equal(aValue, "value1");
+ } else {
+ Assert.equal(aValue, "value2");
+ }
+ linkOrgHeaderFound2++;
+ }
+ if (aName == "location") {
+ locationOrgHeaderFound2++;
+ Assert.equal(aValue, "loc");
+ }
+ },
+ });
+ Assert.ok(linkOrgHeaderFound2 == 3, "Original link header still here.");
+ Assert.ok(
+ locationOrgHeaderFound2 == 1,
+ "Original location header still here."
+ );
+
+ if (dbg) {
+ print("============== Test GetResponseHeader");
+ }
+ var linkOrgHeaderFound3 = 0;
+ request.getOriginalResponseHeader("link", {
+ visitHeader: function visitOrg(aName, aValue) {
+ if (linkOrgHeaderFound3 == 0) {
+ Assert.equal(aValue, "");
+ } else if (linkOrgHeaderFound3 == 1) {
+ Assert.equal(aValue, "value1");
+ } else {
+ Assert.equal(aValue, "value2");
+ }
+ linkOrgHeaderFound3++;
+ },
+ });
+ Assert.ok(linkOrgHeaderFound2 == 3, "Original link header still here.");
+
+ if (dbg) {
+ print("============== checkResponse: out");
+ }
+
+ run_next_test();
+}
diff --git a/netwerk/test/unit/test_pac_reload_after_network_change.js b/netwerk/test/unit/test_pac_reload_after_network_change.js
new file mode 100644
index 0000000000..1b236473ab
--- /dev/null
+++ b/netwerk/test/unit/test_pac_reload_after_network_change.js
@@ -0,0 +1,75 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "gProxyService",
+ "@mozilla.org/network/protocol-proxy-service;1",
+ "nsIProtocolProxyService"
+);
+var { setTimeout } = ChromeUtils.importESModule(
+ "resource://gre/modules/Timer.sys.mjs"
+);
+
+let pacServer;
+const proxyPort = 4433;
+
+add_setup(async function () {
+ pacServer = new HttpServer();
+ pacServer.registerPathHandler(
+ "/proxy.pac",
+ function handler(metadata, response) {
+ let content = `function FindProxyForURL(url, host) { return "HTTPS localhost:${proxyPort}"; }`;
+ response.setHeader("Content-Length", `${content.length}`);
+ response.bodyOutputStream.write(content, content.length);
+ }
+ );
+ pacServer.start(-1);
+});
+
+registerCleanupFunction(async () => {
+ Services.prefs.clearUserPref("network.proxy.type");
+ Services.prefs.clearUserPref("network.proxy.autoconfig_url");
+ Services.prefs.clearUserPref("network.proxy.reload_pac_delay");
+});
+
+async function getProxyInfo() {
+ return new Promise((resolve, reject) => {
+ let uri = Services.io.newURI("http://www.mozilla.org/");
+ gProxyService.asyncResolve(uri, 0, {
+ onProxyAvailable(_req, _uri, pi, _status) {
+ resolve(pi);
+ },
+ });
+ });
+}
+
+// Test if we can successfully get PAC when the PAC server is available.
+add_task(async function testPAC() {
+ // Configure PAC
+ Services.prefs.setIntPref("network.proxy.type", 2);
+ Services.prefs.setCharPref(
+ "network.proxy.autoconfig_url",
+ `http://localhost:${pacServer.identity.primaryPort}/proxy.pac`
+ );
+
+ let pi = await getProxyInfo();
+ Assert.equal(pi.port, proxyPort, "Expected proxy port to be the same");
+ Assert.equal(pi.type, "https", "Expected proxy type to be https");
+});
+
+// When PAC server is down, we should not use proxy at all.
+add_task(async function testWhenPACServerDown() {
+ Services.prefs.setIntPref("network.proxy.reload_pac_delay", 0);
+ await new Promise(resolve => pacServer.stop(resolve));
+
+ Services.obs.notifyObservers(null, "network:link-status-changed", "changed");
+
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => setTimeout(resolve, 3000));
+
+ let pi = await getProxyInfo();
+ Assert.equal(pi, null, "should have no proxy");
+});
diff --git a/netwerk/test/unit/test_parse_content_type.js b/netwerk/test/unit/test_parse_content_type.js
new file mode 100644
index 0000000000..20b76d558f
--- /dev/null
+++ b/netwerk/test/unit/test_parse_content_type.js
@@ -0,0 +1,365 @@
+/* -*- 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 hadCharset = {};
+var type;
+
+function reset() {
+ delete charset.value;
+ delete hadCharset.value;
+ type = undefined;
+}
+
+function check(aType, aCharset, aHadCharset) {
+ Assert.equal(type, aType);
+ Assert.equal(aCharset, charset.value);
+ Assert.equal(aHadCharset, hadCharset.value);
+ reset();
+}
+
+add_task(function test_parseResponseContentType() {
+ var netutil = Services.io;
+
+ type = netutil.parseRequestContentType("text/html", charset, hadCharset);
+ check("text/html", "", false);
+
+ type = netutil.parseResponseContentType("text/html", charset, hadCharset);
+ check("text/html", "", false);
+
+ type = netutil.parseRequestContentType("TEXT/HTML", charset, hadCharset);
+ check("text/html", "", false);
+
+ type = netutil.parseResponseContentType("TEXT/HTML", charset, hadCharset);
+ check("text/html", "", false);
+
+ type = netutil.parseRequestContentType(
+ "text/html, text/html",
+ charset,
+ hadCharset
+ );
+ check("", "", false);
+
+ type = netutil.parseResponseContentType(
+ "text/html, text/html",
+ charset,
+ hadCharset
+ );
+ check("text/html", "", false);
+
+ type = netutil.parseRequestContentType(
+ "text/html, text/plain",
+ charset,
+ hadCharset
+ );
+ check("", "", false);
+
+ type = netutil.parseResponseContentType(
+ "text/html, text/plain",
+ charset,
+ hadCharset
+ );
+ check("text/plain", "", false);
+
+ type = netutil.parseRequestContentType("text/html, ", charset, hadCharset);
+ check("", "", false);
+
+ type = netutil.parseResponseContentType("text/html, ", charset, hadCharset);
+ check("text/html", "", false);
+
+ type = netutil.parseRequestContentType("text/html, */*", charset, hadCharset);
+ check("", "", false);
+
+ type = netutil.parseResponseContentType(
+ "text/html, */*",
+ charset,
+ hadCharset
+ );
+ check("text/html", "", false);
+
+ type = netutil.parseRequestContentType("text/html, foo", charset, hadCharset);
+ check("", "", false);
+
+ type = netutil.parseResponseContentType(
+ "text/html, foo",
+ charset,
+ hadCharset
+ );
+ check("text/html", "", false);
+
+ type = netutil.parseRequestContentType(
+ "text/html; charset=ISO-8859-1",
+ charset,
+ hadCharset
+ );
+ check("text/html", "ISO-8859-1", true);
+
+ type = netutil.parseResponseContentType(
+ "text/html; charset=ISO-8859-1",
+ charset,
+ hadCharset
+ );
+ check("text/html", "ISO-8859-1", true);
+
+ type = netutil.parseRequestContentType(
+ 'text/html; charset="ISO-8859-1"',
+ charset,
+ hadCharset
+ );
+ check("text/html", "ISO-8859-1", true);
+
+ type = netutil.parseResponseContentType(
+ 'text/html; charset="ISO-8859-1"',
+ charset,
+ hadCharset
+ );
+ check("text/html", "ISO-8859-1", true);
+
+ type = netutil.parseRequestContentType(
+ "text/html; charset='ISO-8859-1'",
+ charset,
+ hadCharset
+ );
+ check("text/html", "'ISO-8859-1'", true);
+
+ type = netutil.parseResponseContentType(
+ "text/html; charset='ISO-8859-1'",
+ charset,
+ hadCharset
+ );
+ check("text/html", "'ISO-8859-1'", true);
+
+ type = netutil.parseRequestContentType(
+ 'text/html; charset="ISO-8859-1", text/html',
+ charset,
+ hadCharset
+ );
+ check("", "", false);
+
+ type = netutil.parseResponseContentType(
+ 'text/html; charset="ISO-8859-1", text/html',
+ charset,
+ hadCharset
+ );
+ check("text/html", "ISO-8859-1", true);
+
+ type = netutil.parseRequestContentType(
+ 'text/html; charset="ISO-8859-1", text/html; charset=UTF8',
+ charset,
+ hadCharset
+ );
+ check("", "", false);
+
+ type = netutil.parseResponseContentType(
+ 'text/html; charset="ISO-8859-1", text/html; charset=UTF8',
+ charset,
+ hadCharset
+ );
+ check("text/html", "UTF8", true);
+
+ type = netutil.parseRequestContentType(
+ "text/html; charset=ISO-8859-1, TEXT/HTML",
+ charset,
+ hadCharset
+ );
+ check("", "", false);
+
+ type = netutil.parseResponseContentType(
+ "text/html; charset=ISO-8859-1, TEXT/HTML",
+ charset,
+ hadCharset
+ );
+ check("text/html", "ISO-8859-1", true);
+
+ type = netutil.parseRequestContentType(
+ "text/html; charset=ISO-8859-1, TEXT/plain",
+ charset,
+ hadCharset
+ );
+ check("", "", false);
+
+ type = netutil.parseResponseContentType(
+ "text/html; charset=ISO-8859-1, TEXT/plain",
+ charset,
+ hadCharset
+ );
+ check("text/plain", "", true);
+
+ type = netutil.parseRequestContentType(
+ "text/plain, TEXT/HTML; charset=ISO-8859-1, text/html, TEXT/HTML",
+ charset,
+ hadCharset
+ );
+ check("", "", false);
+
+ type = netutil.parseResponseContentType(
+ "text/plain, TEXT/HTML; charset=ISO-8859-1, text/html, TEXT/HTML",
+ charset,
+ hadCharset
+ );
+ check("text/html", "ISO-8859-1", true);
+
+ type = netutil.parseRequestContentType(
+ 'text/plain, TEXT/HTML; param="charset=UTF8"; charset="ISO-8859-1"; param2="charset=UTF16", text/html, TEXT/HTML',
+ charset,
+ hadCharset
+ );
+ check("", "", false);
+
+ type = netutil.parseResponseContentType(
+ 'text/plain, TEXT/HTML; param="charset=UTF8"; charset="ISO-8859-1"; param2="charset=UTF16", text/html, TEXT/HTML',
+ charset,
+ hadCharset
+ );
+ check("text/html", "ISO-8859-1", true);
+
+ type = netutil.parseRequestContentType(
+ 'text/plain, TEXT/HTML; param=charset=UTF8; charset="ISO-8859-1"; param2=charset=UTF16, text/html, TEXT/HTML',
+ charset,
+ hadCharset
+ );
+ check("", "", false);
+
+ type = netutil.parseResponseContentType(
+ 'text/plain, TEXT/HTML; param=charset=UTF8; charset="ISO-8859-1"; param2=charset=UTF16, text/html, TEXT/HTML',
+ charset,
+ hadCharset
+ );
+ check("text/html", "ISO-8859-1", true);
+
+ type = netutil.parseRequestContentType(
+ 'text/plain, TEXT/HTML; param=charset=UTF8; charset="ISO-8859-1"; param2=charset=UTF16, text/html, TEXT/HTML',
+ charset,
+ hadCharset
+ );
+ check("", "", false);
+
+ type = netutil.parseRequestContentType(
+ "text/plain; param= , text/html",
+ charset,
+ hadCharset
+ );
+ check("", "", false);
+
+ type = netutil.parseResponseContentType(
+ "text/plain; param= , text/html",
+ charset,
+ hadCharset
+ );
+ check("text/html", "", false);
+
+ type = netutil.parseRequestContentType(
+ 'text/plain; param=", text/html"',
+ charset,
+ hadCharset
+ );
+ check("text/plain", "", false);
+
+ type = netutil.parseResponseContentType(
+ 'text/plain; param=", text/html"',
+ charset,
+ hadCharset
+ );
+ check("text/plain", "", false);
+
+ type = netutil.parseRequestContentType(
+ 'text/plain; param=", \\" , text/html"',
+ charset,
+ hadCharset
+ );
+ check("text/plain", "", false);
+
+ type = netutil.parseResponseContentType(
+ 'text/plain; param=", \\" , text/html"',
+ charset,
+ hadCharset
+ );
+ check("text/plain", "", false);
+
+ type = netutil.parseRequestContentType(
+ 'text/plain; param=", \\" , text/html , "',
+ charset,
+ hadCharset
+ );
+ check("text/plain", "", false);
+
+ type = netutil.parseResponseContentType(
+ 'text/plain; param=", \\" , text/html , "',
+ charset,
+ hadCharset
+ );
+ check("text/plain", "", false);
+
+ type = netutil.parseRequestContentType(
+ 'text/plain param=", \\" , text/html , "',
+ charset,
+ hadCharset
+ );
+ check("", "", false);
+
+ type = netutil.parseResponseContentType(
+ 'text/plain param=", \\" , text/html , "',
+ charset,
+ hadCharset
+ );
+ check("text/plain", "", false);
+
+ type = netutil.parseRequestContentType(
+ "text/plain charset=UTF8",
+ charset,
+ hadCharset
+ );
+ check("", "", false);
+
+ type = netutil.parseResponseContentType(
+ "text/plain charset=UTF8",
+ charset,
+ hadCharset
+ );
+ check("text/plain", "", false);
+
+ type = netutil.parseRequestContentType(
+ 'text/plain, TEXT/HTML; param="charset=UTF8"; ; param2="charset=UTF16", text/html, TEXT/HTML',
+ charset,
+ hadCharset
+ );
+ check("", "", false);
+
+ type = netutil.parseResponseContentType(
+ 'text/plain, TEXT/HTML; param="charset=UTF8"; ; param2="charset=UTF16", text/html, TEXT/HTML',
+ charset,
+ hadCharset
+ );
+ check("text/html", "", false);
+
+ // Bug 562915 - correctness: "\x" is "x"
+ type = netutil.parseResponseContentType(
+ 'text/plain; charset="UTF\\-8"',
+ charset,
+ hadCharset
+ );
+ check("text/plain", "UTF-8", true);
+
+ // Bug 700589
+
+ // check that single quote doesn't confuse parsing of subsequent parameters
+ type = netutil.parseResponseContentType(
+ 'text/plain; x=\'; charset="UTF-8"',
+ charset,
+ hadCharset
+ );
+ check("text/plain", "UTF-8", true);
+
+ // check that single quotes do not get removed from extracted charset
+ type = netutil.parseResponseContentType(
+ "text/plain; charset='UTF-8'",
+ charset,
+ hadCharset
+ );
+ check("text/plain", "'UTF-8'", true);
+});
diff --git a/netwerk/test/unit/test_partial_response_entry_size_smart_shrink.js b/netwerk/test/unit/test_partial_response_entry_size_smart_shrink.js
new file mode 100644
index 0000000000..0dd5482708
--- /dev/null
+++ b/netwerk/test/unit/test_partial_response_entry_size_smart_shrink.js
@@ -0,0 +1,106 @@
+/*
+
+ This is only a crash test. We load a partial content, cache it. Then we change the limit
+ for single cache entry size (shrink it) so that the next request for the rest of the content
+ will hit that limit and doom/remove the entry. We change the size manually, but in reality
+ it's being changed by cache smart size.
+
+*/
+
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+ChromeUtils.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 });
+}
+
+// Have 2kb response (8 * 2 ^ 8)
+var responseBody = "response";
+for (var i = 0; i < 8; ++i) {
+ responseBody += responseBody;
+}
+
+function contentHandler(metadata, response) {
+ response.setHeader("Content-Type", "text/plain", false);
+ response.setHeader("ETag", "range");
+ response.setHeader("Accept-Ranges", "bytes");
+ response.setHeader("Cache-Control", "max-age=360000");
+
+ if (!metadata.hasHeader("If-Range")) {
+ response.setHeader("Content-Length", responseBody.length + "");
+ response.processAsync();
+ let slice = responseBody.slice(0, 100);
+ response.bodyOutputStream.write(slice, slice.length);
+ response.finish();
+ } else {
+ let slice = responseBody.slice(100);
+ response.setStatusLine(metadata.httpVersion, 206, "Partial Content");
+ response.setHeader(
+ "Content-Range",
+ (responseBody.length - slice.length).toString() +
+ "-" +
+ (responseBody.length - 1).toString() +
+ "/" +
+ responseBody.length.toString()
+ );
+
+ response.setHeader("Content-Length", slice.length + "");
+ response.bodyOutputStream.write(slice, slice.length);
+ }
+}
+
+let enforceSoftPref;
+let enforceStrictChunkedPref;
+
+function run_test() {
+ enforceSoftPref = Services.prefs.getBoolPref(
+ "network.http.enforce-framing.soft"
+ );
+ Services.prefs.setBoolPref("network.http.enforce-framing.soft", false);
+
+ enforceStrictChunkedPref = Services.prefs.getBoolPref(
+ "network.http.enforce-framing.strict_chunked_encoding"
+ );
+ Services.prefs.setBoolPref(
+ "network.http.enforce-framing.strict_chunked_encoding",
+ false
+ );
+
+ httpServer = new HttpServer();
+ httpServer.registerPathHandler("/content", contentHandler);
+ httpServer.start(-1);
+
+ var chan = make_channel(URL + "/content");
+ chan.asyncOpen(new ChannelListener(firstTimeThrough, null, CL_IGNORE_CL));
+ do_test_pending();
+}
+
+function firstTimeThrough(request, buffer) {
+ // Change single cache entry limit to 1 kb. This emulates smart size change.
+ Services.prefs.setIntPref("browser.cache.disk.max_entry_size", 1);
+
+ var chan = make_channel(URL + "/content");
+ chan.asyncOpen(new ChannelListener(finish_test, null));
+}
+
+function finish_test(request, buffer) {
+ Assert.equal(buffer, responseBody);
+ Services.prefs.setBoolPref(
+ "network.http.enforce-framing.soft",
+ enforceSoftPref
+ );
+ Services.prefs.setBoolPref(
+ "network.http.enforce-framing.strict_chunked_encoding",
+ enforceStrictChunkedPref
+ );
+ httpServer.stop(do_test_finished);
+}
diff --git a/netwerk/test/unit/test_permmgr.js b/netwerk/test/unit/test_permmgr.js
new file mode 100644
index 0000000000..5d10429d58
--- /dev/null
+++ b/netwerk/test/unit/test_permmgr.js
@@ -0,0 +1,125 @@
+// tests nsIPermissionManager
+
+"use strict";
+
+var hosts = [
+ // format: [host, type, permission]
+ ["http://mozilla.org", "cookie", 1],
+ ["http://mozilla.org", "image", 2],
+ ["http://mozilla.org", "popup", 3],
+ ["http://mozilla.com", "cookie", 1],
+ ["http://www.mozilla.com", "cookie", 2],
+ ["http://dev.mozilla.com", "cookie", 3],
+];
+
+var results = [
+ // format: [host, type, testPermission result, testExactPermission result]
+ // test defaults
+ ["http://localhost", "cookie", 0, 0],
+ ["http://spreadfirefox.com", "cookie", 0, 0],
+ // test different types
+ ["http://mozilla.org", "cookie", 1, 1],
+ ["http://mozilla.org", "image", 2, 2],
+ ["http://mozilla.org", "popup", 3, 3],
+ // test subdomains
+ ["http://www.mozilla.org", "cookie", 1, 0],
+ ["http://www.dev.mozilla.org", "cookie", 1, 0],
+ // test different permissions on subdomains
+ ["http://mozilla.com", "cookie", 1, 1],
+ ["http://www.mozilla.com", "cookie", 2, 2],
+ ["http://dev.mozilla.com", "cookie", 3, 3],
+ ["http://www.dev.mozilla.com", "cookie", 3, 0],
+];
+
+function run_test() {
+ Services.prefs.setCharPref("permissions.manager.defaultsUrl", "");
+ var pm = Services.perms;
+
+ var ioService = Services.io;
+
+ var secMan = Services.scriptSecurityManager;
+
+ // nsIPermissionManager implementation is an extension; don't fail if it's not there
+ if (!pm) {
+ return;
+ }
+
+ // put a few hosts in
+ for (let i = 0; i < hosts.length; ++i) {
+ let uri = ioService.newURI(hosts[i][0]);
+ let principal = secMan.createContentPrincipal(uri, {});
+
+ pm.addFromPrincipal(principal, hosts[i][1], hosts[i][2]);
+ }
+
+ // test the result
+ for (let i = 0; i < results.length; ++i) {
+ let uri = ioService.newURI(results[i][0]);
+ let principal = secMan.createContentPrincipal(uri, {});
+
+ Assert.equal(
+ pm.testPermissionFromPrincipal(principal, results[i][1]),
+ results[i][2]
+ );
+ Assert.equal(
+ pm.testExactPermissionFromPrincipal(principal, results[i][1]),
+ results[i][3]
+ );
+ }
+
+ // test the all property ...
+ var perms = pm.all;
+ Assert.equal(perms.length, hosts.length);
+
+ // ... remove all the hosts ...
+ for (let j = 0; j < perms.length; ++j) {
+ pm.removePermission(perms[j]);
+ }
+
+ // ... ensure each and every element is equal ...
+ for (let i = 0; i < hosts.length; ++i) {
+ for (let j = 0; j < perms.length; ++j) {
+ if (
+ perms[j].matchesURI(ioService.newURI(hosts[i][0]), true) &&
+ hosts[i][1] == perms[j].type &&
+ hosts[i][2] == perms[j].capability
+ ) {
+ perms.splice(j, 1);
+ break;
+ }
+ }
+ }
+ Assert.equal(perms.length, 0);
+
+ // ... and check the permmgr's empty
+ Assert.equal(pm.all.length, 0);
+
+ // test UTF8 normalization behavior: expect ASCII/ACE host encodings
+ var utf8 = "b\u00FCcher.dolske.org"; // "bücher.dolske.org"
+ var aceref = "xn--bcher-kva.dolske.org";
+ var principal = secMan.createContentPrincipal(
+ ioService.newURI("http://" + utf8),
+ {}
+ );
+ pm.addFromPrincipal(principal, "utf8", 1);
+ Assert.notEqual(Services.perms.all.length, 0);
+ var ace = Services.perms.all[0];
+ Assert.equal(ace.principal.asciiHost, aceref);
+ Assert.equal(Services.perms.all.length > 1, false);
+
+ // test removeAll()
+ pm.removeAll();
+ Assert.equal(Services.perms.all.length, 0);
+
+ principal = secMan.createContentPrincipalFromOrigin(
+ "https://www.example.com"
+ );
+ pm.addFromPrincipal(principal, "offline-app", pm.ALLOW_ACTION);
+ // Remove existing entry.
+ let perm = pm.getPermissionObject(principal, "offline-app", true);
+ pm.removePermission(perm);
+ // Try to remove already deleted entry.
+ perm = pm.getPermissionObject(principal, "offline-app", true);
+ pm.removePermission(perm);
+ Assert.equal(Services.perms.all.length, 0);
+}
diff --git a/netwerk/test/unit/test_ping_aboutnetworking.js b/netwerk/test/unit/test_ping_aboutnetworking.js
new file mode 100644
index 0000000000..fbaaeaa405
--- /dev/null
+++ b/netwerk/test/unit/test_ping_aboutnetworking.js
@@ -0,0 +1,103 @@
+/* -*- 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 gDashboard = Cc["@mozilla.org/network/dashboard;1"].getService(
+ Ci.nsIDashboard
+);
+
+function connectionFailed(status) {
+ let status_ok = [
+ "NS_NET_STATUS_RESOLVING_HOST",
+ "NS_NET_STATUS_RESOLVED_HOST",
+ "NS_NET_STATUS_CONNECTING_TO",
+ "NS_NET_STATUS_CONNECTED_TO",
+ ];
+ for (let i = 0; i < status_ok.length; i++) {
+ if (status == status_ok[i]) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+function test_sockets(serverSocket) {
+ // TODO: enable this test in bug 1581892.
+ if (mozinfo.socketprocess_networking) {
+ info("skip test_sockets");
+ do_test_finished();
+ return;
+ }
+
+ do_test_pending();
+ gDashboard.requestSockets(function (data) {
+ let index = -1;
+ info("requestSockets: " + JSON.stringify(data.sockets));
+ for (let i = 0; i < data.sockets.length; i++) {
+ if (data.sockets[i].host == "127.0.0.1") {
+ index = i;
+ break;
+ }
+ }
+ Assert.notEqual(index, -1);
+ Assert.equal(data.sockets[index].port, serverSocket.port);
+ Assert.equal(data.sockets[index].type, "TCP");
+
+ do_test_finished();
+ });
+}
+
+function run_test() {
+ var ps = Services.prefs;
+ // disable network changed events to avoid the the risk of having the dns
+ // cache getting flushed behind our back
+ ps.setBoolPref("network.notify.changed", false);
+ // Localhost is hardcoded to loopback and isn't cached, disable that with this pref
+ ps.setBoolPref("network.proxy.allow_hijacking_localhost", true);
+
+ registerCleanupFunction(function () {
+ ps.clearUserPref("network.notify.changed");
+ ps.clearUserPref("network.proxy.allow_hijacking_localhost");
+ });
+
+ let serverSocket = Cc["@mozilla.org/network/server-socket;1"].createInstance(
+ Ci.nsIServerSocket
+ );
+ serverSocket.init(-1, true, -1);
+
+ do_test_pending();
+ gDashboard.requestConnection(
+ "localhost",
+ serverSocket.port,
+ "tcp",
+ 15,
+ function (connInfo) {
+ if (connInfo.status == "NS_NET_STATUS_CONNECTED_TO") {
+ do_test_pending();
+ gDashboard.requestDNSInfo(function (data) {
+ let found = false;
+ info("requestDNSInfo: " + JSON.stringify(data.entries));
+ for (let i = 0; i < data.entries.length; i++) {
+ if (data.entries[i].hostname == "localhost") {
+ found = true;
+ break;
+ }
+ }
+ Assert.equal(found, true);
+
+ do_test_finished();
+ test_sockets(serverSocket);
+ });
+
+ do_test_finished();
+ }
+ if (connectionFailed(connInfo.status)) {
+ do_throw(connInfo.status);
+ }
+ }
+ );
+}
diff --git a/netwerk/test/unit/test_plaintext_sniff.js b/netwerk/test/unit/test_plaintext_sniff.js
new file mode 100644
index 0000000000..896e458165
--- /dev/null
+++ b/netwerk/test/unit/test_plaintext_sniff.js
@@ -0,0 +1,211 @@
+// Test the plaintext-or-binary sniffer
+
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+// List of Content-Type headers to test. For each header we have an array.
+// The first element in the array is the Content-Type header string. The
+// second element in the array is a boolean indicating whether we allow
+// sniffing for that type.
+var contentTypeHeaderList = [
+ ["text/plain", true],
+ ["text/plain; charset=ISO-8859-1", true],
+ ["text/plain; charset=iso-8859-1", true],
+ ["text/plain; charset=UTF-8", true],
+ ["text/plain; charset=unknown", false],
+ ["text/plain; param", false],
+ ["text/plain; charset=ISO-8859-1; param", false],
+ ["text/plain; charset=iso-8859-1; param", false],
+ ["text/plain; charset=UTF-8; param", false],
+ ["text/plain; charset=utf-8", false],
+ ["text/plain; charset=utf8", false],
+ ["text/plain; charset=UTF8", false],
+ ["text/plain; charset=iSo-8859-1", false],
+];
+
+// List of response bodies to test. For each response we have an array. The
+// first element in the array is the body string. The second element in the
+// array is a boolean indicating whether that string should sniff as binary.
+var bodyList = [["Plaintext", false]];
+
+// List of possible BOMs
+var BOMList = [
+ "\xFE\xFF", // UTF-16BE
+ "\xFF\xFE", // UTF-16LE
+ "\xEF\xBB\xBF", // UTF-8
+ "\x00\x00\xFE\xFF", // UCS-4BE
+ "\x00\x00\xFF\xFE", // UCS-4LE
+];
+
+// Build up bodyList. The things we treat as binary are ASCII codes 0-8,
+// 14-26, 28-31. That is, the control char range, except for tab, newline,
+// vertical tab, form feed, carriage return, and ESC (this last being used by
+// Shift_JIS, apparently).
+function isBinaryChar(ch) {
+ return (
+ (0 <= ch && ch <= 8) || (14 <= ch && ch <= 26) || (28 <= ch && ch <= 31)
+ );
+}
+
+// Test chars on their own
+var i;
+for (i = 0; i <= 127; ++i) {
+ bodyList.push([String.fromCharCode(i), isBinaryChar(i)]);
+}
+
+// Test that having a BOM prevents plaintext sniffing
+var j;
+for (i = 0; i <= 127; ++i) {
+ for (j = 0; j < BOMList.length; ++j) {
+ bodyList.push([BOMList[j] + String.fromCharCode(i, i), false]);
+ }
+}
+
+// Test that having a BOM requires at least 4 chars to kick in
+for (i = 0; i <= 127; ++i) {
+ for (j = 0; j < BOMList.length; ++j) {
+ bodyList.push([
+ BOMList[j] + String.fromCharCode(i),
+ BOMList[j].length == 2 && isBinaryChar(i),
+ ]);
+ }
+}
+
+function makeChan(headerIdx, bodyIdx) {
+ var chan = NetUtil.newChannel({
+ uri:
+ "http://localhost:" +
+ httpserv.identity.primaryPort +
+ "/" +
+ headerIdx +
+ "/" +
+ bodyIdx,
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+
+ chan.loadFlags |= Ci.nsIChannel.LOAD_CALL_CONTENT_SNIFFERS;
+
+ return chan;
+}
+
+function makeListener(headerIdx, bodyIdx) {
+ var listener = {
+ onStartRequest: function test_onStartR(request) {
+ try {
+ var chan = request.QueryInterface(Ci.nsIChannel);
+
+ Assert.equal(chan.status, Cr.NS_OK);
+
+ var type = chan.contentType;
+
+ var expectedType =
+ contentTypeHeaderList[headerIdx][1] && bodyList[bodyIdx][1]
+ ? "application/x-vnd.mozilla.guess-from-ext"
+ : "text/plain";
+ if (expectedType != type) {
+ do_throw(
+ "Unexpected sniffed type '" +
+ type +
+ "'. " +
+ "Should be '" +
+ expectedType +
+ "'. " +
+ "Header is ['" +
+ contentTypeHeaderList[headerIdx][0] +
+ "', " +
+ contentTypeHeaderList[headerIdx][1] +
+ "]. " +
+ "Body is ['" +
+ bodyList[bodyIdx][0].toSource() +
+ "', " +
+ bodyList[bodyIdx][1] +
+ "]."
+ );
+ }
+ Assert.equal(expectedType, type);
+ } 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) {
+ // Advance to next test
+ ++headerIdx;
+ if (headerIdx == contentTypeHeaderList.length) {
+ headerIdx = 0;
+ ++bodyIdx;
+ }
+
+ if (bodyIdx == bodyList.length) {
+ do_test_pending();
+ httpserv.stop(do_test_finished);
+ } else {
+ doTest(headerIdx, bodyIdx);
+ }
+
+ do_test_finished();
+ },
+ };
+
+ return listener;
+}
+
+function doTest(headerIdx, bodyIdx) {
+ var chan = makeChan(headerIdx, bodyIdx);
+
+ var listener = makeListener(headerIdx, bodyIdx);
+
+ chan.asyncOpen(listener);
+
+ do_test_pending();
+}
+
+function createResponse(headerIdx, bodyIdx, metadata, response) {
+ response.setHeader(
+ "Content-Type",
+ contentTypeHeaderList[headerIdx][0],
+ false
+ );
+ response.bodyOutputStream.write(
+ bodyList[bodyIdx][0],
+ bodyList[bodyIdx][0].length
+ );
+}
+
+function makeHandler(headerIdx, bodyIdx) {
+ var f = function handlerClosure(metadata, response) {
+ return createResponse(headerIdx, bodyIdx, metadata, response);
+ };
+ return f;
+}
+
+var httpserv;
+function run_test() {
+ // disable on Windows for now, because it seems to leak sockets and die.
+ // Silly operating system!
+ // This is a really nasty way to detect Windows. I wish we could do better.
+ if (mozinfo.os == "win") {
+ //failing eslint no-empty test
+ }
+
+ httpserv = new HttpServer();
+
+ for (i = 0; i < contentTypeHeaderList.length; ++i) {
+ for (j = 0; j < bodyList.length; ++j) {
+ httpserv.registerPathHandler("/" + i + "/" + j, makeHandler(i, j));
+ }
+ }
+
+ httpserv.start(-1);
+
+ doTest(0, 0);
+}
diff --git a/netwerk/test/unit/test_port_remapping.js b/netwerk/test/unit/test_port_remapping.js
new file mode 100644
index 0000000000..274a1117ec
--- /dev/null
+++ b/netwerk/test/unit/test_port_remapping.js
@@ -0,0 +1,50 @@
+// This test is checking the `network.socket.forcePort` preference has an effect.
+// We remap an ilusional port `8765` to go to the port the server actually binds to.
+
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+function make_channel(url, callback, ctx) {
+ return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true });
+}
+
+const REMAPPED_PORT = 8765;
+
+add_task(async function check_protocols() {
+ function contentHandler(metadata, response) {
+ let responseBody = "The server should never return this!";
+ response.setHeader("Content-Type", "text/plain");
+ response.bodyOutputStream.write(responseBody, responseBody.length);
+ }
+
+ const httpserv = new HttpServer();
+ httpserv.registerPathHandler("/content", contentHandler);
+ httpserv.start(-1);
+
+ do_get_profile();
+ Services.prefs.setCharPref(
+ "network.socket.forcePort",
+ `${REMAPPED_PORT}=${httpserv.identity.primaryPort}`
+ );
+
+ function get_response() {
+ return new Promise(resolve => {
+ const URL = `http://localhost:${REMAPPED_PORT}/content`;
+ const channel = make_channel(URL);
+ channel.asyncOpen(
+ new ChannelListener((request, data) => {
+ resolve(data);
+ })
+ );
+ });
+ }
+
+ // We expect "Bad request" from the test server because the server doesn't
+ // have identity for the remapped port. We don't want to add it too, because
+ // that would not prove we actualy remap the port number.
+ Assert.equal(await get_response(), "Bad request\n");
+ await new Promise(resolve => httpserv.stop(resolve));
+});
diff --git a/netwerk/test/unit/test_post.js b/netwerk/test/unit/test_post.js
new file mode 100644
index 0000000000..f57a149920
--- /dev/null
+++ b/netwerk/test/unit/test_post.js
@@ -0,0 +1,141 @@
+//
+// POST test
+//
+
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+ChromeUtils.defineLazyGetter(this, "URL", function () {
+ return "http://localhost:" + httpserver.identity.primaryPort;
+});
+
+var httpserver = new HttpServer();
+var testpath = "/simple";
+
+var testfile = do_get_file("../unit/data/test_readline6.txt");
+
+const BOUNDARY = "AaB03x";
+var teststring1 =
+ "--" +
+ BOUNDARY +
+ "\r\n" +
+ 'Content-Disposition: form-data; name="body"\r\n\r\n' +
+ "0123456789\r\n" +
+ "--" +
+ BOUNDARY +
+ "\r\n" +
+ 'Content-Disposition: form-data; name="files"; filename="' +
+ testfile.leafName +
+ '"\r\n' +
+ "Content-Type: application/octet-stream\r\n" +
+ "Content-Length: " +
+ testfile.fileSize +
+ "\r\n\r\n";
+var teststring2 = "--" + BOUNDARY + "--\r\n";
+
+const BUFFERSIZE = 4096;
+var correctOnProgress = false;
+
+var listenerCallback = {
+ QueryInterface: ChromeUtils.generateQI(["nsIProgressEventSink"]),
+
+ getInterface(iid) {
+ if (iid.equals(Ci.nsIProgressEventSink)) {
+ return this;
+ }
+ throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE);
+ },
+
+ onProgress(request, progress, progressMax) {
+ // this works because the response is 0 bytes and does not trigger onprogress
+ if (progress === progressMax) {
+ correctOnProgress = true;
+ }
+ },
+
+ onStatus(request, status, statusArg) {},
+};
+
+function run_test() {
+ var sstream1 = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(
+ Ci.nsIStringInputStream
+ );
+ sstream1.data = teststring1;
+
+ var fstream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(
+ Ci.nsIFileInputStream
+ );
+ fstream.init(testfile, -1, -1, 0);
+
+ var buffered = Cc[
+ "@mozilla.org/network/buffered-input-stream;1"
+ ].createInstance(Ci.nsIBufferedInputStream);
+ buffered.init(fstream, BUFFERSIZE);
+
+ var sstream2 = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(
+ Ci.nsIStringInputStream
+ );
+ sstream2.data = teststring2;
+
+ var multi = Cc["@mozilla.org/io/multiplex-input-stream;1"].createInstance(
+ Ci.nsIMultiplexInputStream
+ );
+ multi.appendStream(sstream1);
+ multi.appendStream(buffered);
+ multi.appendStream(sstream2);
+
+ var mime = Cc["@mozilla.org/network/mime-input-stream;1"].createInstance(
+ Ci.nsIMIMEInputStream
+ );
+ mime.addHeader("Content-Type", "multipart/form-data; boundary=" + BOUNDARY);
+ mime.setData(multi);
+
+ httpserver.registerPathHandler(testpath, serverHandler);
+ httpserver.start(-1);
+
+ var channel = setupChannel(testpath);
+
+ channel
+ .QueryInterface(Ci.nsIUploadChannel)
+ .setUploadStream(mime, "", mime.available());
+ channel.requestMethod = "POST";
+ channel.notificationCallbacks = listenerCallback;
+ channel.asyncOpen(new ChannelListener(checkRequest, channel));
+ do_test_pending();
+}
+
+function setupChannel(path) {
+ return NetUtil.newChannel({
+ uri: URL + path,
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+}
+
+function serverHandler(metadata, response) {
+ Assert.equal(metadata.method, "POST");
+
+ var data = read_stream(
+ metadata.bodyInputStream,
+ metadata.bodyInputStream.available()
+ );
+
+ var testfile_stream = Cc[
+ "@mozilla.org/network/file-input-stream;1"
+ ].createInstance(Ci.nsIFileInputStream);
+ testfile_stream.init(testfile, -1, -1, 0);
+
+ Assert.equal(
+ teststring1 +
+ read_stream(testfile_stream, testfile_stream.available()) +
+ teststring2,
+ data
+ );
+}
+
+function checkRequest(request, data, context) {
+ Assert.ok(correctOnProgress);
+ httpserver.stop(do_test_finished);
+}
diff --git a/netwerk/test/unit/test_predictor.js b/netwerk/test/unit/test_predictor.js
new file mode 100644
index 0000000000..dce86ce5e0
--- /dev/null
+++ b/netwerk/test/unit/test_predictor.js
@@ -0,0 +1,852 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+const ReferrerInfo = Components.Constructor(
+ "@mozilla.org/referrer-info;1",
+ "nsIReferrerInfo",
+ "init"
+);
+
+var running_single_process = false;
+
+var predictor = null;
+
+function is_child_process() {
+ return Services.appinfo.processType == Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT;
+}
+
+function extract_origin(uri) {
+ var o = uri.scheme + "://" + uri.asciiHost;
+ if (uri.port !== -1) {
+ o = o + ":" + uri.port;
+ }
+ return o;
+}
+
+var origin_attributes = {};
+
+var ValidityChecker = function (verifier, httpStatus) {
+ this.verifier = verifier;
+ this.httpStatus = httpStatus;
+};
+
+ValidityChecker.prototype = {
+ verifier: null,
+ httpStatus: 0,
+
+ QueryInterface: ChromeUtils.generateQI(["nsICacheEntryOpenCallback"]),
+
+ onCacheEntryCheck(entry) {
+ return Ci.nsICacheEntryOpenCallback.ENTRY_WANTED;
+ },
+
+ onCacheEntryAvailable(entry, isnew, status) {
+ // Check if forced valid
+ Assert.equal(entry.isForcedValid, this.httpStatus === 200);
+ this.verifier.maybe_run_next_test();
+ },
+};
+
+var Verifier = function _verifier(
+ testing,
+ expected_prefetches,
+ expected_preconnects,
+ expected_preresolves
+) {
+ this.verifying = testing;
+ this.expected_prefetches = expected_prefetches;
+ this.expected_preconnects = expected_preconnects;
+ this.expected_preresolves = expected_preresolves;
+};
+
+Verifier.prototype = {
+ complete: false,
+ verifying: null,
+ expected_prefetches: null,
+ expected_preconnects: null,
+ expected_preresolves: null,
+
+ getInterface: function verifier_getInterface(iid) {
+ return this.QueryInterface(iid);
+ },
+
+ QueryInterface: ChromeUtils.generateQI(["nsINetworkPredictorVerifier"]),
+
+ maybe_run_next_test: function verifier_maybe_run_next_test() {
+ if (
+ this.expected_prefetches.length === 0 &&
+ this.expected_preconnects.length === 0 &&
+ this.expected_preresolves.length === 0 &&
+ !this.complete
+ ) {
+ this.complete = true;
+ Assert.ok(true, "Well this is unexpected...");
+ // This kicks off the ability to run the next test
+ reset_predictor();
+ }
+ },
+
+ onPredictPrefetch: function verifier_onPredictPrefetch(uri, status) {
+ var index = this.expected_prefetches.indexOf(uri.asciiSpec);
+ if (index == -1 && !this.complete) {
+ Assert.ok(false, "Got prefetch for unexpected uri " + uri.asciiSpec);
+ } else {
+ this.expected_prefetches.splice(index, 1);
+ }
+
+ dump("checking validity of entry for " + uri.spec + "\n");
+ var checker = new ValidityChecker(this, status);
+ asyncOpenCacheEntry(
+ uri.spec,
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ Services.loadContextInfo.default,
+ checker
+ );
+ },
+
+ onPredictPreconnect: function verifier_onPredictPreconnect(uri) {
+ var origin = extract_origin(uri);
+ var index = this.expected_preconnects.indexOf(origin);
+ if (index == -1 && !this.complete) {
+ Assert.ok(false, "Got preconnect for unexpected uri " + origin);
+ } else {
+ this.expected_preconnects.splice(index, 1);
+ }
+ this.maybe_run_next_test();
+ },
+
+ onPredictDNS: function verifier_onPredictDNS(uri) {
+ var origin = extract_origin(uri);
+ var index = this.expected_preresolves.indexOf(origin);
+ if (index == -1 && !this.complete) {
+ Assert.ok(false, "Got preresolve for unexpected uri " + origin);
+ } else {
+ this.expected_preresolves.splice(index, 1);
+ }
+ this.maybe_run_next_test();
+ },
+};
+
+function reset_predictor() {
+ if (running_single_process || is_child_process()) {
+ predictor.reset();
+ } else {
+ sendCommand("predictor.reset();");
+ }
+}
+
+function newURI(s) {
+ return Services.io.newURI(s);
+}
+
+var prepListener = {
+ numEntriesToOpen: 0,
+ numEntriesOpened: 0,
+ continueCallback: null,
+
+ QueryInterface: ChromeUtils.generateQI(["nsICacheEntryOpenCallback"]),
+
+ init(entriesToOpen, cb) {
+ this.numEntriesOpened = 0;
+ this.numEntriesToOpen = entriesToOpen;
+ this.continueCallback = cb;
+ },
+
+ onCacheEntryCheck(entry) {
+ return Ci.nsICacheEntryOpenCallback.ENTRY_WANTED;
+ },
+
+ onCacheEntryAvailable(entry, isNew, result) {
+ Assert.equal(result, Cr.NS_OK);
+ entry.setMetaDataElement("predictor_test", "1");
+ entry.metaDataReady();
+ this.numEntriesOpened++;
+ if (this.numEntriesToOpen == this.numEntriesOpened) {
+ this.continueCallback();
+ }
+ },
+};
+
+function open_and_continue(uris, continueCallback) {
+ var ds = Services.cache2.diskCacheStorage(Services.loadContextInfo.default);
+
+ prepListener.init(uris.length, continueCallback);
+ for (var i = 0; i < uris.length; ++i) {
+ ds.asyncOpenURI(
+ uris[i],
+ "",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ prepListener
+ );
+ }
+}
+
+function test_link_hover() {
+ if (!running_single_process && !is_child_process()) {
+ // This one we can just proxy to the child and be done with, no extra setup
+ // is necessary.
+ sendCommand("test_link_hover();");
+ return;
+ }
+
+ var uri = newURI("http://localhost:4444/foo/bar");
+ var referrer = newURI("http://localhost:4444/foo");
+ var preconns = ["http://localhost:4444"];
+
+ var verifier = new Verifier("hover", [], preconns, []);
+ predictor.predict(
+ uri,
+ referrer,
+ predictor.PREDICT_LINK,
+ origin_attributes,
+ verifier
+ );
+}
+
+const pageload_toplevel = newURI("http://localhost:4444/index.html");
+
+function continue_test_pageload() {
+ var subresources = [
+ "http://localhost:4444/style.css",
+ "http://localhost:4443/jquery.js",
+ "http://localhost:4444/image.png",
+ ];
+
+ // This is necessary to learn the origin stuff
+ predictor.learn(
+ pageload_toplevel,
+ null,
+ predictor.LEARN_LOAD_TOPLEVEL,
+ origin_attributes
+ );
+ do_timeout(0, () => {
+ // allow the learn() to run on the main thread
+ var preconns = [];
+
+ var sruri = newURI(subresources[0]);
+ predictor.learn(
+ sruri,
+ pageload_toplevel,
+ predictor.LEARN_LOAD_SUBRESOURCE,
+ origin_attributes
+ );
+ do_timeout(0, () => {
+ preconns.push(extract_origin(sruri));
+
+ sruri = newURI(subresources[1]);
+ predictor.learn(
+ sruri,
+ pageload_toplevel,
+ predictor.LEARN_LOAD_SUBRESOURCE,
+ origin_attributes
+ );
+ do_timeout(0, () => {
+ preconns.push(extract_origin(sruri));
+
+ sruri = newURI(subresources[2]);
+ predictor.learn(
+ sruri,
+ pageload_toplevel,
+ predictor.LEARN_LOAD_SUBRESOURCE,
+ origin_attributes
+ );
+ do_timeout(0, () => {
+ preconns.push(extract_origin(sruri));
+
+ var verifier = new Verifier("pageload", [], preconns, []);
+ predictor.predict(
+ pageload_toplevel,
+ null,
+ predictor.PREDICT_LOAD,
+ origin_attributes,
+ verifier
+ );
+ });
+ });
+ });
+ });
+}
+
+function test_pageload() {
+ open_and_continue([pageload_toplevel], function () {
+ if (running_single_process) {
+ continue_test_pageload();
+ } else {
+ sendCommand("continue_test_pageload();");
+ }
+ });
+}
+
+const redirect_inituri = newURI("http://localhost:4443/redirect");
+const redirect_targeturi = newURI("http://localhost:4444/index.html");
+
+function continue_test_redirect() {
+ var subresources = [
+ "http://localhost:4444/style.css",
+ "http://localhost:4443/jquery.js",
+ "http://localhost:4444/image.png",
+ ];
+
+ predictor.learn(
+ redirect_inituri,
+ null,
+ predictor.LEARN_LOAD_TOPLEVEL,
+ origin_attributes
+ );
+ do_timeout(0, () => {
+ predictor.learn(
+ redirect_targeturi,
+ null,
+ predictor.LEARN_LOAD_TOPLEVEL,
+ origin_attributes
+ );
+ do_timeout(0, () => {
+ predictor.learn(
+ redirect_targeturi,
+ redirect_inituri,
+ predictor.LEARN_LOAD_REDIRECT,
+ origin_attributes
+ );
+ do_timeout(0, () => {
+ var preconns = [];
+ preconns.push(extract_origin(redirect_targeturi));
+
+ var sruri = newURI(subresources[0]);
+ predictor.learn(
+ sruri,
+ redirect_targeturi,
+ predictor.LEARN_LOAD_SUBRESOURCE,
+ origin_attributes
+ );
+ do_timeout(0, () => {
+ preconns.push(extract_origin(sruri));
+
+ sruri = newURI(subresources[1]);
+ predictor.learn(
+ sruri[1],
+ redirect_targeturi,
+ predictor.LEARN_LOAD_SUBRESOURCE,
+ origin_attributes
+ );
+ do_timeout(0, () => {
+ preconns.push(extract_origin(sruri));
+
+ sruri = newURI(subresources[2]);
+ predictor.learn(
+ sruri[2],
+ redirect_targeturi,
+ predictor.LEARN_LOAD_SUBRESOURCE,
+ origin_attributes
+ );
+ do_timeout(0, () => {
+ preconns.push(extract_origin(sruri));
+
+ var verifier = new Verifier("redirect", [], preconns, []);
+ predictor.predict(
+ redirect_inituri,
+ null,
+ predictor.PREDICT_LOAD,
+ origin_attributes,
+ verifier
+ );
+ });
+ });
+ });
+ });
+ });
+ });
+}
+
+// Test is currently disabled.
+// eslint-disable-next-line no-unused-vars
+function test_redirect() {
+ open_and_continue([redirect_inituri, redirect_targeturi], function () {
+ if (running_single_process) {
+ continue_test_redirect();
+ } else {
+ sendCommand("continue_test_redirect();");
+ }
+ });
+}
+
+// Test is currently disabled.
+// eslint-disable-next-line no-unused-vars
+function test_startup() {
+ if (!running_single_process && !is_child_process()) {
+ // This one we can just proxy to the child and be done with, no extra setup
+ // is necessary.
+ sendCommand("test_startup();");
+ return;
+ }
+
+ var uris = ["http://localhost:4444/startup", "http://localhost:4443/startup"];
+ var preconns = [];
+ var uri = newURI(uris[0]);
+ predictor.learn(uri, null, predictor.LEARN_STARTUP, origin_attributes);
+ do_timeout(0, () => {
+ preconns.push(extract_origin(uri));
+
+ uri = newURI(uris[1]);
+ predictor.learn(uri, null, predictor.LEARN_STARTUP, origin_attributes);
+ do_timeout(0, () => {
+ preconns.push(extract_origin(uri));
+
+ var verifier = new Verifier("startup", [], preconns, []);
+ predictor.predict(
+ null,
+ null,
+ predictor.PREDICT_STARTUP,
+ origin_attributes,
+ verifier
+ );
+ });
+ });
+}
+
+const dns_toplevel = newURI("http://localhost:4444/index.html");
+
+function continue_test_dns() {
+ var subresource = "http://localhost:4443/jquery.js";
+
+ predictor.learn(
+ dns_toplevel,
+ null,
+ predictor.LEARN_LOAD_TOPLEVEL,
+ origin_attributes
+ );
+ do_timeout(0, () => {
+ var sruri = newURI(subresource);
+ predictor.learn(
+ sruri,
+ dns_toplevel,
+ predictor.LEARN_LOAD_SUBRESOURCE,
+ origin_attributes
+ );
+ do_timeout(0, () => {
+ var preresolves = [extract_origin(sruri)];
+ var verifier = new Verifier("dns", [], [], preresolves);
+ predictor.predict(
+ dns_toplevel,
+ null,
+ predictor.PREDICT_LOAD,
+ origin_attributes,
+ verifier
+ );
+ });
+ });
+}
+
+function test_dns() {
+ open_and_continue([dns_toplevel], function () {
+ // Ensure that this will do preresolves
+ Services.prefs.setIntPref(
+ "network.predictor.preconnect-min-confidence",
+ 101
+ );
+ if (running_single_process) {
+ continue_test_dns();
+ } else {
+ sendCommand("continue_test_dns();");
+ }
+ });
+}
+
+const origin_toplevel = newURI("http://localhost:4444/index.html");
+
+function continue_test_origin() {
+ var subresources = [
+ "http://localhost:4444/style.css",
+ "http://localhost:4443/jquery.js",
+ "http://localhost:4444/image.png",
+ ];
+ predictor.learn(
+ origin_toplevel,
+ null,
+ predictor.LEARN_LOAD_TOPLEVEL,
+ origin_attributes
+ );
+ do_timeout(0, () => {
+ var preconns = [];
+
+ var sruri = newURI(subresources[0]);
+ predictor.learn(
+ sruri,
+ origin_toplevel,
+ predictor.LEARN_LOAD_SUBRESOURCE,
+ origin_attributes
+ );
+ do_timeout(0, () => {
+ var origin = extract_origin(sruri);
+ if (!preconns.includes(origin)) {
+ preconns.push(origin);
+ }
+
+ sruri = newURI(subresources[1]);
+ predictor.learn(
+ sruri,
+ origin_toplevel,
+ predictor.LEARN_LOAD_SUBRESOURCE,
+ origin_attributes
+ );
+ do_timeout(0, () => {
+ var origin1 = extract_origin(sruri);
+ if (!preconns.includes(origin1)) {
+ preconns.push(origin1);
+ }
+
+ sruri = newURI(subresources[2]);
+ predictor.learn(
+ sruri,
+ origin_toplevel,
+ predictor.LEARN_LOAD_SUBRESOURCE,
+ origin_attributes
+ );
+ do_timeout(0, () => {
+ var origin2 = extract_origin(sruri);
+ if (!preconns.includes(origin2)) {
+ preconns.push(origin2);
+ }
+
+ var loaduri = newURI("http://localhost:4444/anotherpage.html");
+ var verifier = new Verifier("origin", [], preconns, []);
+ predictor.predict(
+ loaduri,
+ null,
+ predictor.PREDICT_LOAD,
+ origin_attributes,
+ verifier
+ );
+ });
+ });
+ });
+ });
+}
+
+function test_origin() {
+ open_and_continue([origin_toplevel], function () {
+ if (running_single_process) {
+ continue_test_origin();
+ } else {
+ sendCommand("continue_test_origin();");
+ }
+ });
+}
+
+var httpserv = null;
+var prefetch_tluri;
+var prefetch_sruri;
+
+function prefetchHandler(metadata, response) {
+ response.setStatusLine(metadata.httpVersion, 200, "OK");
+ var body = "Success (meow meow meow).";
+
+ response.bodyOutputStream.write(body, body.length);
+}
+
+var prefetchListener = {
+ onStartRequest(request) {
+ Assert.equal(request.status, Cr.NS_OK);
+ },
+
+ onDataAvailable(request, stream, offset, cnt) {
+ read_stream(stream, cnt);
+ },
+
+ onStopRequest(request, status) {
+ run_next_test();
+ },
+};
+
+function test_prefetch_setup() {
+ // Disable preconnects and preresolves
+ Services.prefs.setIntPref("network.predictor.preconnect-min-confidence", 101);
+ Services.prefs.setIntPref("network.predictor.preresolve-min-confidence", 101);
+
+ Services.prefs.setBoolPref("network.predictor.enable-prefetch", true);
+
+ // Makes it so we only have to call test_prefetch_prime twice to make prefetch
+ // do its thing.
+ Services.prefs.setIntPref("network.predictor.prefetch-rolling-load-count", 2);
+
+ // This test does not run in e10s-mode, so we'll just go ahead and skip it.
+ // We've left the e10s test code in below, just in case someone wants to try
+ // to make it work at some point in the future.
+ if (!running_single_process) {
+ dump("skipping test_prefetch_setup due to e10s\n");
+ run_next_test();
+ return;
+ }
+
+ httpserv = new HttpServer();
+ httpserv.registerPathHandler("/cat.jpg", prefetchHandler);
+ httpserv.start(-1);
+
+ var tluri =
+ "http://127.0.0.1:" + httpserv.identity.primaryPort + "/index.html";
+ var sruri = "http://127.0.0.1:" + httpserv.identity.primaryPort + "/cat.jpg";
+ prefetch_tluri = newURI(tluri);
+ prefetch_sruri = newURI(sruri);
+ if (!running_single_process && !is_child_process()) {
+ // Give the child process access to these values
+ sendCommand('prefetch_tluri = newURI("' + tluri + '");');
+ sendCommand('prefetch_sruri = newURI("' + sruri + '");');
+ }
+
+ run_next_test();
+}
+
+// Used to "prime the pump" for prefetch - it makes sure all our learns go
+// through as expected so that prefetching will happen.
+function test_prefetch_prime() {
+ // This test does not run in e10s-mode, so we'll just go ahead and skip it.
+ // We've left the e10s test code in below, just in case someone wants to try
+ // to make it work at some point in the future.
+ if (!running_single_process) {
+ dump("skipping test_prefetch_prime due to e10s\n");
+ run_next_test();
+ return;
+ }
+
+ open_and_continue([prefetch_tluri], function () {
+ if (running_single_process) {
+ predictor.learn(
+ prefetch_tluri,
+ null,
+ predictor.LEARN_LOAD_TOPLEVEL,
+ origin_attributes
+ );
+ predictor.learn(
+ prefetch_sruri,
+ prefetch_tluri,
+ predictor.LEARN_LOAD_SUBRESOURCE,
+ origin_attributes
+ );
+ } else {
+ sendCommand(
+ "predictor.learn(prefetch_tluri, null, predictor.LEARN_LOAD_TOPLEVEL, origin_attributes);"
+ );
+ sendCommand(
+ "predictor.learn(prefetch_sruri, prefetch_tluri, predictor.LEARN_LOAD_SUBRESOURCE, origin_attributes);"
+ );
+ }
+
+ // This runs in the parent or only process
+ var channel = NetUtil.newChannel({
+ uri: prefetch_sruri.asciiSpec,
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+ channel.requestMethod = "GET";
+ channel.referrerInfo = new ReferrerInfo(
+ Ci.nsIReferrerInfo.EMPTY,
+ true,
+ prefetch_tluri
+ );
+ channel.asyncOpen(prefetchListener);
+ });
+}
+
+function test_prefetch() {
+ // This test does not run in e10s-mode, so we'll just go ahead and skip it.
+ // We've left the e10s test code in below, just in case someone wants to try
+ // to make it work at some point in the future.
+ if (!running_single_process) {
+ dump("skipping test_prefetch due to e10s\n");
+ run_next_test();
+ return;
+ }
+
+ // Setup for this has all been taken care of by test_prefetch_prime, so we can
+ // continue on without pausing here.
+ if (running_single_process) {
+ continue_test_prefetch();
+ } else {
+ sendCommand("continue_test_prefetch();");
+ }
+}
+
+function continue_test_prefetch() {
+ var prefetches = [prefetch_sruri.asciiSpec];
+ var verifier = new Verifier("prefetch", prefetches, [], []);
+ predictor.predict(
+ prefetch_tluri,
+ null,
+ predictor.PREDICT_LOAD,
+ origin_attributes,
+ verifier
+ );
+}
+
+function test_visitor_doom() {
+ // See bug 1708673
+ Services.prefs.setBoolPref("network.cache.bug1708673", true);
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("network.cache.bug1708673");
+ });
+
+ let p1 = new Promise(resolve => {
+ let doomTasks = [];
+ let visitor = {
+ onCacheStorageInfo() {},
+ async onCacheEntryInfo(
+ aURI,
+ aIdEnhance,
+ aDataSize,
+ aAltDataSize,
+ aFetchCount,
+ aLastModifiedTime,
+ aExpirationTime,
+ aPinned,
+ aInfo
+ ) {
+ let storages = [
+ Services.cache2.memoryCacheStorage(aInfo),
+ Services.cache2.diskCacheStorage(aInfo, false),
+ ];
+ console.debug("asyncDoomURI", aURI.spec);
+ let doomTask = Promise.all(
+ storages.map(storage => {
+ return new Promise(resolve1 => {
+ storage.asyncDoomURI(aURI, aIdEnhance, {
+ onCacheEntryDoomed: resolve1,
+ });
+ });
+ })
+ );
+ doomTasks.push(doomTask);
+ },
+ onCacheEntryVisitCompleted() {
+ Promise.allSettled(doomTasks).then(resolve);
+ },
+ QueryInterface: ChromeUtils.generateQI(["nsICacheStorageVisitor"]),
+ };
+ Services.cache2.asyncVisitAllStorages(visitor, true);
+ });
+
+ let p2 = new Promise(resolve => {
+ reset_predictor();
+ resolve();
+ });
+
+ do_test_pending();
+ Promise.allSettled([p1, p2]).then(() => {
+ return new Promise(resolve => {
+ let entryCount = 0;
+ let visitor = {
+ onCacheStorageInfo() {},
+ async onCacheEntryInfo(
+ aURI,
+ aIdEnhance,
+ aDataSize,
+ aAltDataSize,
+ aFetchCount,
+ aLastModifiedTime,
+ aExpirationTime,
+ aPinned,
+ aInfo
+ ) {
+ entryCount++;
+ },
+ onCacheEntryVisitCompleted() {
+ Assert.equal(entryCount, 0);
+ resolve();
+ },
+ QueryInterface: ChromeUtils.generateQI(["nsICacheStorageVisitor"]),
+ };
+ Services.cache2.asyncVisitAllStorages(visitor, true);
+ }).then(run_next_test);
+ });
+}
+
+function cleanup() {
+ observer.cleaningUp = true;
+ if (running_single_process) {
+ // The http server is required (and started) by the prefetch test, which
+ // only runs in single-process mode, so don't try to shut it down if we're
+ // in e10s mode.
+ do_test_pending();
+ httpserv.stop(do_test_finished);
+ }
+ reset_predictor();
+}
+
+var tests = [
+ // This must ALWAYS come first, to ensure a clean slate
+ reset_predictor,
+ test_link_hover,
+ test_pageload,
+ // TODO: These are disabled until the features are re-written
+ //test_redirect,
+ //test_startup,
+ // END DISABLED TESTS
+ test_origin,
+ test_dns,
+ test_prefetch_setup,
+ test_prefetch_prime,
+ test_prefetch_prime,
+ test_prefetch,
+ test_visitor_doom,
+ // This must ALWAYS come last, to ensure we clean up after ourselves
+ cleanup,
+];
+
+var observer = {
+ cleaningUp: false,
+
+ QueryInterface: ChromeUtils.generateQI(["nsIObserver"]),
+
+ observe(subject, topic, data) {
+ if (topic != "predictor-reset-complete") {
+ return;
+ }
+
+ if (this.cleaningUp) {
+ unregisterObserver();
+ }
+
+ run_next_test();
+ },
+};
+
+function registerObserver() {
+ Services.obs.addObserver(observer, "predictor-reset-complete");
+}
+
+function unregisterObserver() {
+ Services.obs.removeObserver(observer, "predictor-reset-complete");
+}
+
+function run_test_real() {
+ tests.forEach(f => add_test(f));
+ do_get_profile();
+
+ Services.prefs.setBoolPref("network.predictor.enabled", true);
+ Services.prefs.setBoolPref("network.predictor.doing-tests", true);
+
+ predictor = Cc["@mozilla.org/network/predictor;1"].getService(
+ Ci.nsINetworkPredictor
+ );
+
+ registerObserver();
+
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("network.predictor.preconnect-min-confidence");
+ Services.prefs.clearUserPref("network.predictor.enabled");
+ Services.prefs.clearUserPref("network.predictor.preresolve-min-confidence");
+ Services.prefs.clearUserPref("network.predictor.enable-prefetch");
+ Services.prefs.clearUserPref(
+ "network.predictor.prefetch-rolling-load-count"
+ );
+ Services.prefs.clearUserPref("network.predictor.doing-tests");
+ });
+
+ run_next_test();
+}
+
+function run_test() {
+ // This indirection is necessary to make e10s tests work as expected
+ running_single_process = true;
+ run_test_real();
+}
diff --git a/netwerk/test/unit/test_private_cookie_changed.js b/netwerk/test/unit/test_private_cookie_changed.js
new file mode 100644
index 0000000000..89e9f5e75a
--- /dev/null
+++ b/netwerk/test/unit/test_private_cookie_changed.js
@@ -0,0 +1,44 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+"use strict";
+
+function makeChan(uri, isPrivate) {
+ var chan = NetUtil.newChannel({
+ uri: uri.spec,
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+
+ chan.QueryInterface(Ci.nsIPrivateBrowsingChannel).setPrivate(isPrivate);
+ return chan;
+}
+
+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);
+
+ let publicNotifications = 0;
+ let privateNotifications = 0;
+ Services.obs.addObserver(function () {
+ publicNotifications++;
+ }, "cookie-changed");
+ Services.obs.addObserver(function () {
+ privateNotifications++;
+ }, "private-cookie-changed");
+
+ let uri = NetUtil.newURI("http://foo.com/");
+ let publicChan = makeChan(uri, false);
+ let svc = Services.cookies.QueryInterface(Ci.nsICookieService);
+ svc.setCookieStringFromHttp(uri, "oh=hai", publicChan);
+ let privateChan = makeChan(uri, true);
+ svc.setCookieStringFromHttp(uri, "oh=hai", privateChan);
+ Assert.equal(publicNotifications, 1);
+ Assert.equal(privateNotifications, 1);
+}
diff --git a/netwerk/test/unit/test_private_necko_channel.js b/netwerk/test/unit/test_private_necko_channel.js
new file mode 100644
index 0000000000..dc6a73e145
--- /dev/null
+++ b/netwerk/test/unit/test_private_necko_channel.js
@@ -0,0 +1,60 @@
+//
+// Private channel test
+//
+
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+var httpserver = new HttpServer();
+var testpath = "/simple";
+var httpbody = "0123456789";
+
+function run_test() {
+ // Simulate a profile dir for xpcshell
+ do_get_profile();
+
+ // Start off with an empty cache
+ evict_cache_entries();
+
+ httpserver.registerPathHandler(testpath, serverHandler);
+ httpserver.start(-1);
+
+ var channel = setupChannel(testpath);
+ channel.loadGroup = Cc["@mozilla.org/network/load-group;1"].createInstance();
+
+ channel.QueryInterface(Ci.nsIPrivateBrowsingChannel);
+ channel.setPrivate(true);
+
+ channel.asyncOpen(new ChannelListener(checkRequest, channel));
+
+ do_test_pending();
+}
+
+function setupChannel(path) {
+ return NetUtil.newChannel({
+ uri: "http://localhost:" + httpserver.identity.primaryPort + path,
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+}
+
+function serverHandler(metadata, response) {
+ response.setHeader("Content-Type", "text/plain", false);
+ response.bodyOutputStream.write(httpbody, httpbody.length);
+}
+
+function checkRequest(request, data, context) {
+ get_device_entry_count("disk", null, function (count) {
+ Assert.equal(count, 0);
+ get_device_entry_count(
+ "disk",
+ Services.loadContextInfo.private,
+ function (count1) {
+ Assert.equal(count1, 1);
+ httpserver.stop(do_test_finished);
+ }
+ );
+ });
+}
diff --git a/netwerk/test/unit/test_progress.js b/netwerk/test/unit/test_progress.js
new file mode 100644
index 0000000000..8363f0e337
--- /dev/null
+++ b/netwerk/test/unit/test_progress.js
@@ -0,0 +1,145 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+ChromeUtils.defineLazyGetter(this, "URL", function () {
+ return "http://localhost:" + httpserver.identity.primaryPort;
+});
+
+var httpserver = new HttpServer();
+var testpath = "/simple";
+var httpbody = "0123456789";
+
+var last = 0,
+ max = 0;
+
+const STATUS_RECEIVING_FROM = 0x4b0006;
+const LOOPS = 50000;
+
+const TYPE_ONSTATUS = 1;
+const TYPE_ONPROGRESS = 2;
+const TYPE_ONSTARTREQUEST = 3;
+const TYPE_ONDATAAVAILABLE = 4;
+const TYPE_ONSTOPREQUEST = 5;
+
+var ProgressCallback = function () {};
+
+ProgressCallback.prototype = {
+ _listener: null,
+ _got_onstartrequest: false,
+ _got_onstatus_after_onstartrequest: false,
+ _last_callback_handled: null,
+ statusArg: "",
+ finish: null,
+
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIProgressEventSink",
+ "nsIStreamListener",
+ "nsIRequestObserver",
+ ]),
+
+ getInterface(iid) {
+ if (
+ iid.equals(Ci.nsIProgressEventSink) ||
+ iid.equals(Ci.nsIStreamListener) ||
+ iid.equals(Ci.nsIRequestObserver)
+ ) {
+ return this;
+ }
+ throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE);
+ },
+
+ onStartRequest(request) {
+ Assert.equal(this._last_callback_handled, TYPE_ONSTATUS);
+ this._got_onstartrequest = true;
+ this._last_callback_handled = TYPE_ONSTARTREQUEST;
+
+ this._listener = new ChannelListener(checkRequest, request);
+ this._listener.onStartRequest(request);
+ },
+
+ onDataAvailable(request, data, offset, count) {
+ Assert.equal(this._last_callback_handled, TYPE_ONPROGRESS);
+ this._last_callback_handled = TYPE_ONDATAAVAILABLE;
+
+ this._listener.onDataAvailable(request, data, offset, count);
+ },
+
+ onStopRequest(request, status) {
+ Assert.equal(this._last_callback_handled, TYPE_ONDATAAVAILABLE);
+ Assert.ok(this._got_onstatus_after_onstartrequest);
+ this._last_callback_handled = TYPE_ONSTOPREQUEST;
+
+ this._listener.onStopRequest(request, status);
+ delete this._listener;
+ this.finish();
+ },
+
+ onProgress(request, progress, progressMax) {
+ Assert.equal(this._last_callback_handled, TYPE_ONSTATUS);
+ this._last_callback_handled = TYPE_ONPROGRESS;
+
+ Assert.equal(this.mStatus, STATUS_RECEIVING_FROM);
+ last = progress;
+ max = progressMax;
+ },
+
+ onStatus(request, status, statusArg) {
+ if (!this._got_onstartrequest) {
+ // Ensure that all messages before onStartRequest are onStatus
+ if (this._last_callback_handled) {
+ Assert.equal(this._last_callback_handled, TYPE_ONSTATUS);
+ }
+ } else if (this._last_callback_handled == TYPE_ONSTARTREQUEST) {
+ this._got_onstatus_after_onstartrequest = true;
+ } else {
+ Assert.equal(this._last_callback_handled, TYPE_ONDATAAVAILABLE);
+ }
+ this._last_callback_handled = TYPE_ONSTATUS;
+
+ Assert.equal(statusArg, this.statusArg);
+ this.mStatus = status;
+ },
+
+ mStatus: 0,
+};
+
+registerCleanupFunction(async () => {
+ await httpserver.stop();
+});
+
+function chanPromise(uri, statusArg) {
+ return new Promise(resolve => {
+ var chan = NetUtil.newChannel({
+ uri,
+ loadUsingSystemPrincipal: true,
+ });
+ chan.QueryInterface(Ci.nsIHttpChannel);
+ chan.requestMethod = "GET";
+ let listener = new ProgressCallback();
+ listener.statusArg = statusArg;
+ chan.notificationCallbacks = listener;
+ listener.finish = resolve;
+ chan.asyncOpen(listener);
+ });
+}
+
+add_task(async function test_http1_1() {
+ httpserver.registerPathHandler(testpath, serverHandler);
+ httpserver.start(-1);
+ await chanPromise(URL + testpath, "localhost");
+});
+
+function serverHandler(metadata, response) {
+ response.setHeader("Content-Type", "text/plain", false);
+ for (let i = 0; i < LOOPS; i++) {
+ response.bodyOutputStream.write(httpbody, httpbody.length);
+ }
+}
+
+function checkRequest(request, data, context) {
+ Assert.equal(last, httpbody.length * LOOPS);
+ Assert.equal(max, httpbody.length * LOOPS);
+}
diff --git a/netwerk/test/unit/test_progress_no_proxy_and_proxy.js b/netwerk/test/unit/test_progress_no_proxy_and_proxy.js
new file mode 100644
index 0000000000..cb132360c8
--- /dev/null
+++ b/netwerk/test/unit/test_progress_no_proxy_and_proxy.js
@@ -0,0 +1,205 @@
+/* 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";
+
+// This test can be merged with test_progress.js once HTTP/3 tests are
+// enabled on all plaforms.
+
+var last = 0;
+var max = 0;
+var using_proxy = false;
+
+const RESPONSE_LENGTH = 3000000;
+const STATUS_RECEIVING_FROM = 0x4b0006;
+
+const TYPE_ONSTATUS = 1;
+const TYPE_ONPROGRESS = 2;
+const TYPE_ONSTARTREQUEST = 3;
+const TYPE_ONDATAAVAILABLE = 4;
+const TYPE_ONSTOPREQUEST = 5;
+
+var ProgressCallback = function () {};
+
+ProgressCallback.prototype = {
+ _listener: null,
+ _got_onstartrequest: false,
+ _got_onstatus_after_onstartrequest: false,
+ _last_callback_handled: null,
+ statusArg: "",
+ finish: null,
+
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIProgressEventSink",
+ "nsIStreamListener",
+ "nsIRequestObserver",
+ ]),
+
+ getInterface(iid) {
+ if (
+ iid.equals(Ci.nsIProgressEventSink) ||
+ iid.equals(Ci.nsIStreamListener) ||
+ iid.equals(Ci.nsIRequestObserver)
+ ) {
+ return this;
+ }
+ throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE);
+ },
+
+ onStartRequest(request) {
+ Assert.equal(this._last_callback_handled, TYPE_ONSTATUS);
+ this._got_onstartrequest = true;
+ this._last_callback_handled = TYPE_ONSTARTREQUEST;
+
+ this._listener = new ChannelListener(checkRequest, request);
+ this._listener.onStartRequest(request);
+ },
+
+ onDataAvailable(request, data, offset, count) {
+ Assert.equal(this._last_callback_handled, TYPE_ONPROGRESS);
+ this._last_callback_handled = TYPE_ONDATAAVAILABLE;
+
+ this._listener.onDataAvailable(request, data, offset, count);
+ },
+
+ onStopRequest(request, status) {
+ Assert.equal(this._last_callback_handled, TYPE_ONDATAAVAILABLE);
+ Assert.ok(this._got_onstatus_after_onstartrequest);
+ this._last_callback_handled = TYPE_ONSTOPREQUEST;
+
+ this._listener.onStopRequest(request, status);
+ delete this._listener;
+
+ check_http_info(request, this.expected_httpVersion, using_proxy);
+
+ this.finish();
+ },
+
+ onProgress(request, progress, progressMax) {
+ Assert.equal(this._last_callback_handled, TYPE_ONSTATUS);
+ this._last_callback_handled = TYPE_ONPROGRESS;
+
+ Assert.equal(this.mStatus, STATUS_RECEIVING_FROM);
+ last = progress;
+ max = progressMax;
+ },
+
+ onStatus(request, status, statusArg) {
+ if (!this._got_onstartrequest) {
+ // Ensure that all messages before onStartRequest are onStatus
+ if (this._last_callback_handled) {
+ Assert.equal(this._last_callback_handled, TYPE_ONSTATUS);
+ }
+ } else if (this._last_callback_handled == TYPE_ONSTARTREQUEST) {
+ this._got_onstatus_after_onstartrequest = true;
+ } else {
+ Assert.equal(this._last_callback_handled, TYPE_ONDATAAVAILABLE);
+ }
+ this._last_callback_handled = TYPE_ONSTATUS;
+
+ Assert.equal(statusArg, this.statusArg);
+ this.mStatus = status;
+ },
+
+ mStatus: 0,
+};
+
+function chanPromise(uri, statusArg, version) {
+ return new Promise(resolve => {
+ var chan = makeHTTPChannel(uri, using_proxy);
+ chan.requestMethod = "GET";
+ let listener = new ProgressCallback();
+ listener.statusArg = statusArg;
+ chan.notificationCallbacks = listener;
+ listener.expected_httpVersion = version;
+ listener.finish = resolve;
+ chan.asyncOpen(listener);
+ });
+}
+
+function checkRequest(request, data, context) {
+ Assert.equal(last, RESPONSE_LENGTH);
+ Assert.equal(max, RESPONSE_LENGTH);
+}
+
+async function check_progress(server) {
+ info(`Testing ${server.constructor.name}`);
+ await server.registerPathHandler("/test", (req, resp) => {
+ // Generate a post.
+ function generateContent(size) {
+ return "0".repeat(size);
+ }
+
+ resp.writeHead(200, {
+ "content-type": "application/json",
+ "content-length": "3000000",
+ });
+ resp.end(generateContent(3000000));
+ });
+ await chanPromise(
+ `${server.origin()}/test`,
+ `${server.domain()}`,
+ server.version()
+ );
+}
+
+add_task(async function setup() {
+ let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(
+ Ci.nsIX509CertDB
+ );
+ addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u");
+ addCertFromFile(certdb, "proxy-ca.pem", "CTu,u,u");
+});
+
+add_task(async function test_http_1_and_2() {
+ await with_node_servers(
+ [NodeHTTPServer, NodeHTTPSServer, NodeHTTP2Server],
+ check_progress
+ );
+});
+
+add_task(async function test_http_proxy() {
+ using_proxy = true;
+ let proxy = new NodeHTTPProxyServer();
+ await proxy.start();
+ await with_node_servers(
+ [NodeHTTPServer, NodeHTTPSServer, NodeHTTP2Server],
+ check_progress
+ );
+ await proxy.stop();
+ using_proxy = false;
+});
+
+add_task(async function test_https_proxy() {
+ using_proxy = true;
+ let proxy = new NodeHTTPSProxyServer();
+ await proxy.start();
+ await with_node_servers(
+ [NodeHTTPServer, NodeHTTPSServer, NodeHTTP2Server],
+ check_progress
+ );
+ await proxy.stop();
+ using_proxy = false;
+});
+
+add_task(async function test_http2_proxy() {
+ using_proxy = true;
+ let proxy = new NodeHTTP2ProxyServer();
+ await proxy.start();
+ await with_node_servers(
+ [NodeHTTPServer, NodeHTTPSServer, NodeHTTP2Server],
+ check_progress
+ );
+ await proxy.stop();
+ using_proxy = false;
+});
+
+add_task(async function test_http3() {
+ await http3_setup_tests("h3-29");
+ await chanPromise(
+ "https://foo.example.com/" + RESPONSE_LENGTH,
+ "foo.example.com",
+ "h3-29"
+ );
+ http3_clear_prefs();
+});
diff --git a/netwerk/test/unit/test_protocolproxyservice-async-filters.js b/netwerk/test/unit/test_protocolproxyservice-async-filters.js
new file mode 100644
index 0000000000..fcf43d63ef
--- /dev/null
+++ b/netwerk/test/unit/test_protocolproxyservice-async-filters.js
@@ -0,0 +1,435 @@
+/* -*- 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 testcase exercises the Protocol Proxy Service's async filter functionality
+// run_filter_*() are entry points for each individual test.
+
+"use strict";
+
+var pps = Cc["@mozilla.org/network/protocol-proxy-service;1"].getService();
+
+/**
+ * Test nsIProtocolHandler that allows proxying, but doesn't allow HTTP
+ * proxying.
+ */
+function TestProtocolHandler() {}
+TestProtocolHandler.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["nsIProtocolHandler"]),
+ scheme: "moz-test",
+ defaultPort: -1,
+ protocolFlags:
+ Ci.nsIProtocolHandler.URI_NOAUTH |
+ Ci.nsIProtocolHandler.URI_NORELATIVE |
+ Ci.nsIProtocolHandler.ALLOWS_PROXY |
+ Ci.nsIProtocolHandler.URI_DANGEROUS_TO_LOAD,
+ newChannel(uri, aLoadInfo) {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
+ },
+ allowPort(port, scheme) {
+ return true;
+ },
+};
+
+function TestProtocolHandlerFactory() {}
+TestProtocolHandlerFactory.prototype = {
+ createInstance(iid) {
+ return new TestProtocolHandler().QueryInterface(iid);
+ },
+};
+
+function register_test_protocol_handler() {
+ var reg = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
+ reg.registerFactory(
+ Components.ID("{4ea7dd3a-8cae-499c-9f18-e1de773ca25b}"),
+ "TestProtocolHandler",
+ "@mozilla.org/network/protocol;1?name=moz-test",
+ new TestProtocolHandlerFactory()
+ );
+}
+
+function check_proxy(pi, type, host, port, flags, timeout, hasNext) {
+ Assert.notEqual(pi, null);
+ Assert.equal(pi.type, type);
+ Assert.equal(pi.host, host);
+ Assert.equal(pi.port, port);
+ if (flags != -1) {
+ Assert.equal(pi.flags, flags);
+ }
+ if (timeout != -1) {
+ Assert.equal(pi.failoverTimeout, timeout);
+ }
+ if (hasNext) {
+ Assert.notEqual(pi.failoverProxy, null);
+ } else {
+ Assert.equal(pi.failoverProxy, null);
+ }
+}
+
+const SYNC = 0;
+const THROW = 1;
+const ASYNC = 2;
+
+function TestFilter(type, host, port, flags, timeout, result) {
+ this._type = type;
+ this._host = host;
+ this._port = port;
+ this._flags = flags;
+ this._timeout = timeout;
+ this._result = result;
+}
+TestFilter.prototype = {
+ _type: "",
+ _host: "",
+ _port: -1,
+ _flags: 0,
+ _timeout: 0,
+ _async: false,
+ _throwing: false,
+
+ QueryInterface: ChromeUtils.generateQI(["nsIProtocolProxyFilter"]),
+
+ applyFilter(uri, pi, cb) {
+ if (this._result == THROW) {
+ throw Components.Exception("", Cr.NS_ERROR_FAILURE);
+ }
+
+ var pi_tail = pps.newProxyInfo(
+ this._type,
+ this._host,
+ this._port,
+ "",
+ "",
+ this._flags,
+ this._timeout,
+ null
+ );
+ if (pi) {
+ pi.failoverProxy = pi_tail;
+ } else {
+ pi = pi_tail;
+ }
+
+ if (this._result == ASYNC) {
+ executeSoon(() => {
+ cb.onProxyFilterResult(pi);
+ });
+ } else {
+ cb.onProxyFilterResult(pi);
+ }
+ },
+};
+
+function resolveCallback() {}
+resolveCallback.prototype = {
+ nextFunction: null,
+
+ QueryInterface: ChromeUtils.generateQI(["nsIProtocolProxyCallback"]),
+
+ onProxyAvailable(req, channel, pi, status) {
+ this.nextFunction(pi);
+ },
+};
+
+// ==============================================================
+
+var filter1;
+var filter2;
+var filter3;
+
+function run_filter_test1() {
+ filter1 = new TestFilter("http", "foo", 8080, 0, 10, ASYNC);
+ filter2 = new TestFilter("http", "bar", 8090, 0, 10, ASYNC);
+ pps.registerFilter(filter1, 20);
+ pps.registerFilter(filter2, 10);
+
+ var cb = new resolveCallback();
+ cb.nextFunction = filter_test1_1;
+ var channel = NetUtil.newChannel({
+ uri: "http://www.mozilla.org/",
+ loadUsingSystemPrincipal: true,
+ });
+ pps.asyncResolve(channel, 0, cb);
+}
+
+function filter_test1_1(pi) {
+ check_proxy(pi, "http", "bar", 8090, 0, 10, true);
+ check_proxy(pi.failoverProxy, "http", "foo", 8080, 0, 10, false);
+
+ pps.unregisterFilter(filter2);
+
+ var cb = new resolveCallback();
+ cb.nextFunction = filter_test1_2;
+ var channel = NetUtil.newChannel({
+ uri: "http://www.mozilla.org/",
+ loadUsingSystemPrincipal: true,
+ });
+ pps.asyncResolve(channel, 0, cb);
+}
+
+function filter_test1_2(pi) {
+ check_proxy(pi, "http", "foo", 8080, 0, 10, false);
+
+ pps.unregisterFilter(filter1);
+
+ var cb = new resolveCallback();
+ cb.nextFunction = filter_test1_3;
+ var channel = NetUtil.newChannel({
+ uri: "http://www.mozilla.org/",
+ loadUsingSystemPrincipal: true,
+ });
+ pps.asyncResolve(channel, 0, cb);
+}
+
+function filter_test1_3(pi) {
+ Assert.equal(pi, null);
+ run_filter2_sync_async();
+}
+
+function run_filter2_sync_async() {
+ filter1 = new TestFilter("http", "foo", 8080, 0, 10, SYNC);
+ filter2 = new TestFilter("http", "bar", 8090, 0, 10, ASYNC);
+ pps.registerFilter(filter1, 20);
+ pps.registerFilter(filter2, 10);
+
+ var cb = new resolveCallback();
+ cb.nextFunction = filter_test2_1;
+ var channel = NetUtil.newChannel({
+ uri: "http://www.mozilla.org/",
+ loadUsingSystemPrincipal: true,
+ });
+ pps.asyncResolve(channel, 0, cb);
+}
+
+function filter_test2_1(pi) {
+ check_proxy(pi, "http", "bar", 8090, 0, 10, true);
+ check_proxy(pi.failoverProxy, "http", "foo", 8080, 0, 10, false);
+
+ pps.unregisterFilter(filter1);
+ pps.unregisterFilter(filter2);
+
+ run_filter3_async_sync();
+}
+
+function run_filter3_async_sync() {
+ filter1 = new TestFilter("http", "foo", 8080, 0, 10, ASYNC);
+ filter2 = new TestFilter("http", "bar", 8090, 0, 10, SYNC);
+ pps.registerFilter(filter1, 20);
+ pps.registerFilter(filter2, 10);
+
+ var cb = new resolveCallback();
+ cb.nextFunction = filter_test3_1;
+ var channel = NetUtil.newChannel({
+ uri: "http://www.mozilla.org/",
+ loadUsingSystemPrincipal: true,
+ });
+ pps.asyncResolve(channel, 0, cb);
+}
+
+function filter_test3_1(pi) {
+ check_proxy(pi, "http", "bar", 8090, 0, 10, true);
+ check_proxy(pi.failoverProxy, "http", "foo", 8080, 0, 10, false);
+
+ pps.unregisterFilter(filter1);
+ pps.unregisterFilter(filter2);
+
+ run_filter4_throwing_sync_sync();
+}
+
+function run_filter4_throwing_sync_sync() {
+ filter1 = new TestFilter("", "", 0, 0, 0, THROW);
+ filter2 = new TestFilter("http", "foo", 8080, 0, 10, SYNC);
+ filter3 = new TestFilter("http", "bar", 8090, 0, 10, SYNC);
+ pps.registerFilter(filter1, 20);
+ pps.registerFilter(filter2, 10);
+ pps.registerFilter(filter3, 5);
+
+ var cb = new resolveCallback();
+ cb.nextFunction = filter_test4_1;
+ var channel = NetUtil.newChannel({
+ uri: "http://www.mozilla2.org/",
+ loadUsingSystemPrincipal: true,
+ });
+ pps.asyncResolve(channel, 0, cb);
+}
+
+function filter_test4_1(pi) {
+ check_proxy(pi, "http", "bar", 8090, 0, 10, true);
+ check_proxy(pi.failoverProxy, "http", "foo", 8080, 0, 10, false);
+
+ pps.unregisterFilter(filter1);
+ pps.unregisterFilter(filter2);
+ pps.unregisterFilter(filter3);
+
+ run_filter5_sync_sync_throwing();
+}
+
+function run_filter5_sync_sync_throwing() {
+ filter1 = new TestFilter("http", "foo", 8080, 0, 10, SYNC);
+ filter2 = new TestFilter("http", "bar", 8090, 0, 10, SYNC);
+ filter3 = new TestFilter("", "", 0, 0, 0, THROW);
+ pps.registerFilter(filter1, 20);
+ pps.registerFilter(filter2, 10);
+ pps.registerFilter(filter3, 5);
+
+ var cb = new resolveCallback();
+ cb.nextFunction = filter_test5_1;
+ var channel = NetUtil.newChannel({
+ uri: "http://www.mozilla.org/",
+ loadUsingSystemPrincipal: true,
+ });
+ pps.asyncResolve(channel, 0, cb);
+}
+
+function filter_test5_1(pi) {
+ check_proxy(pi, "http", "bar", 8090, 0, 10, true);
+ check_proxy(pi.failoverProxy, "http", "foo", 8080, 0, 10, false);
+
+ pps.unregisterFilter(filter1);
+ pps.unregisterFilter(filter2);
+ pps.unregisterFilter(filter3);
+
+ run_filter5_2_throwing_async_async();
+}
+
+function run_filter5_2_throwing_async_async() {
+ filter1 = new TestFilter("", "", 0, 0, 0, THROW);
+ filter2 = new TestFilter("http", "foo", 8080, 0, 10, ASYNC);
+ filter3 = new TestFilter("http", "bar", 8090, 0, 10, ASYNC);
+ pps.registerFilter(filter1, 20);
+ pps.registerFilter(filter2, 10);
+ pps.registerFilter(filter3, 5);
+
+ var cb = new resolveCallback();
+ cb.nextFunction = filter_test5_2;
+ var channel = NetUtil.newChannel({
+ uri: "http://www.mozilla.org/",
+ loadUsingSystemPrincipal: true,
+ });
+ pps.asyncResolve(channel, 0, cb);
+}
+
+function filter_test5_2(pi) {
+ check_proxy(pi, "http", "bar", 8090, 0, 10, true);
+ check_proxy(pi.failoverProxy, "http", "foo", 8080, 0, 10, false);
+
+ pps.unregisterFilter(filter1);
+ pps.unregisterFilter(filter2);
+ pps.unregisterFilter(filter3);
+
+ run_filter6_async_async_throwing();
+}
+
+function run_filter6_async_async_throwing() {
+ filter1 = new TestFilter("http", "foo", 8080, 0, 10, ASYNC);
+ filter2 = new TestFilter("http", "bar", 8090, 0, 10, ASYNC);
+ filter3 = new TestFilter("", "", 0, 0, 0, THROW);
+ pps.registerFilter(filter1, 20);
+ pps.registerFilter(filter2, 10);
+ pps.registerFilter(filter3, 5);
+
+ var cb = new resolveCallback();
+ cb.nextFunction = filter_test6_1;
+ var channel = NetUtil.newChannel({
+ uri: "http://www.mozilla.org/",
+ loadUsingSystemPrincipal: true,
+ });
+ pps.asyncResolve(channel, 0, cb);
+}
+
+function filter_test6_1(pi) {
+ check_proxy(pi, "http", "bar", 8090, 0, 10, true);
+ check_proxy(pi.failoverProxy, "http", "foo", 8080, 0, 10, false);
+
+ pps.unregisterFilter(filter1);
+ pps.unregisterFilter(filter2);
+ pps.unregisterFilter(filter3);
+
+ run_filter7_async_throwing_async();
+}
+
+function run_filter7_async_throwing_async() {
+ filter1 = new TestFilter("http", "foo", 8080, 0, 10, ASYNC);
+ filter2 = new TestFilter("", "", 0, 0, 0, THROW);
+ filter3 = new TestFilter("http", "bar", 8090, 0, 10, ASYNC);
+ pps.registerFilter(filter1, 20);
+ pps.registerFilter(filter2, 10);
+ pps.registerFilter(filter3, 5);
+
+ var cb = new resolveCallback();
+ cb.nextFunction = filter_test7_1;
+ var channel = NetUtil.newChannel({
+ uri: "http://www.mozilla.org/",
+ loadUsingSystemPrincipal: true,
+ });
+ pps.asyncResolve(channel, 0, cb);
+}
+
+function filter_test7_1(pi) {
+ check_proxy(pi, "http", "bar", 8090, 0, 10, true);
+ check_proxy(pi.failoverProxy, "http", "foo", 8080, 0, 10, false);
+
+ pps.unregisterFilter(filter1);
+ pps.unregisterFilter(filter2);
+ pps.unregisterFilter(filter3);
+
+ run_filter8_sync_throwing_sync();
+}
+
+function run_filter8_sync_throwing_sync() {
+ filter1 = new TestFilter("http", "foo", 8080, 0, 10, SYNC);
+ filter2 = new TestFilter("", "", 0, 0, 0, THROW);
+ filter3 = new TestFilter("http", "bar", 8090, 0, 10, SYNC);
+ pps.registerFilter(filter1, 20);
+ pps.registerFilter(filter2, 10);
+ pps.registerFilter(filter3, 5);
+
+ var cb = new resolveCallback();
+ cb.nextFunction = filter_test8_1;
+ var channel = NetUtil.newChannel({
+ uri: "http://www.mozilla.org/",
+ loadUsingSystemPrincipal: true,
+ });
+ pps.asyncResolve(channel, 0, cb);
+}
+
+function filter_test8_1(pi) {
+ check_proxy(pi, "http", "bar", 8090, 0, 10, true);
+ check_proxy(pi.failoverProxy, "http", "foo", 8080, 0, 10, false);
+
+ pps.unregisterFilter(filter1);
+ pps.unregisterFilter(filter2);
+ pps.unregisterFilter(filter3);
+
+ run_filter9_throwing();
+}
+
+function run_filter9_throwing() {
+ filter1 = new TestFilter("", "", 0, 0, 0, THROW);
+ pps.registerFilter(filter1, 20);
+
+ var cb = new resolveCallback();
+ cb.nextFunction = filter_test9_1;
+ var channel = NetUtil.newChannel({
+ uri: "http://www.mozilla.org/",
+ loadUsingSystemPrincipal: true,
+ });
+ pps.asyncResolve(channel, 0, cb);
+}
+
+function filter_test9_1(pi) {
+ Assert.equal(pi, null);
+ do_test_finished();
+}
+
+// =========================================
+
+function run_test() {
+ register_test_protocol_handler();
+
+ // start of asynchronous test chain
+ run_filter_test1();
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_protocolproxyservice.js b/netwerk/test/unit/test_protocolproxyservice.js
new file mode 100644
index 0000000000..64472651f6
--- /dev/null
+++ b/netwerk/test/unit/test_protocolproxyservice.js
@@ -0,0 +1,1071 @@
+/* -*- 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 testcase exercises the Protocol Proxy Service
+
+// These are the major sub tests:
+// run_filter_test();
+// run_filter_test2()
+// run_filter_test3()
+// run_pref_test();
+// run_pac_test();
+// run_pac_cancel_test();
+// run_proxy_host_filters_test();
+// run_myipaddress_test();
+// run_failed_script_test();
+// run_isresolvable_test();
+
+"use strict";
+
+var ios = Services.io;
+var pps = Cc["@mozilla.org/network/protocol-proxy-service;1"].getService();
+var prefs = Services.prefs;
+var again = true;
+
+/**
+ * Test nsIProtocolHandler that allows proxying, but doesn't allow HTTP
+ * proxying.
+ */
+function TestProtocolHandler() {}
+TestProtocolHandler.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["nsIProtocolHandler"]),
+ scheme: "moz-test",
+ defaultPort: -1,
+ protocolFlags:
+ Ci.nsIProtocolHandler.URI_NOAUTH |
+ Ci.nsIProtocolHandler.URI_NORELATIVE |
+ Ci.nsIProtocolHandler.ALLOWS_PROXY |
+ Ci.nsIProtocolHandler.URI_DANGEROUS_TO_LOAD,
+ newChannel(uri, aLoadInfo) {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
+ },
+ allowPort(port, scheme) {
+ return true;
+ },
+};
+
+function TestProtocolHandlerFactory() {}
+TestProtocolHandlerFactory.prototype = {
+ createInstance(iid) {
+ return new TestProtocolHandler().QueryInterface(iid);
+ },
+};
+
+function register_test_protocol_handler() {
+ var reg = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
+ reg.registerFactory(
+ Components.ID("{4ea7dd3a-8cae-499c-9f18-e1de773ca25b}"),
+ "TestProtocolHandler",
+ "@mozilla.org/network/protocol;1?name=moz-test",
+ new TestProtocolHandlerFactory()
+ );
+}
+
+function check_proxy(pi, type, host, port, flags, timeout, hasNext) {
+ Assert.notEqual(pi, null);
+ Assert.equal(pi.type, type);
+ Assert.equal(pi.host, host);
+ Assert.equal(pi.port, port);
+ if (flags != -1) {
+ Assert.equal(pi.flags, flags);
+ }
+ if (timeout != -1) {
+ Assert.equal(pi.failoverTimeout, timeout);
+ }
+ if (hasNext) {
+ Assert.notEqual(pi.failoverProxy, null);
+ } else {
+ Assert.equal(pi.failoverProxy, null);
+ }
+}
+
+function TestFilter(type, host, port, flags, timeout) {
+ this._type = type;
+ this._host = host;
+ this._port = port;
+ this._flags = flags;
+ this._timeout = timeout;
+}
+TestFilter.prototype = {
+ _type: "",
+ _host: "",
+ _port: -1,
+ _flags: 0,
+ _timeout: 0,
+ QueryInterface: ChromeUtils.generateQI(["nsIProtocolProxyFilter"]),
+ applyFilter(uri, pi, cb) {
+ var pi_tail = pps.newProxyInfo(
+ this._type,
+ this._host,
+ this._port,
+ "",
+ "",
+ this._flags,
+ this._timeout,
+ null
+ );
+ if (pi) {
+ pi.failoverProxy = pi_tail;
+ } else {
+ pi = pi_tail;
+ }
+ cb.onProxyFilterResult(pi);
+ },
+};
+
+function BasicFilter() {}
+BasicFilter.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["nsIProtocolProxyFilter"]),
+ applyFilter(uri, pi, cb) {
+ cb.onProxyFilterResult(
+ pps.newProxyInfo(
+ "http",
+ "localhost",
+ 8080,
+ "",
+ "",
+ 0,
+ 10,
+ pps.newProxyInfo("direct", "", -1, "", "", 0, 0, null)
+ )
+ );
+ },
+};
+
+function BasicChannelFilter() {}
+BasicChannelFilter.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["nsIProtocolProxyChannelFilter"]),
+ applyFilter(channel, pi, cb) {
+ cb.onProxyFilterResult(
+ pps.newProxyInfo(
+ "http",
+ channel.URI.host,
+ 7777,
+ "",
+ "",
+ 0,
+ 10,
+ pps.newProxyInfo("direct", "", -1, "", "", 0, 0, null)
+ )
+ );
+ },
+};
+
+function resolveCallback() {}
+resolveCallback.prototype = {
+ nextFunction: null,
+
+ QueryInterface: ChromeUtils.generateQI(["nsIProtocolProxyCallback"]),
+
+ onProxyAvailable(req, channel, pi, status) {
+ this.nextFunction(pi);
+ },
+};
+
+function run_filter_test() {
+ var channel = NetUtil.newChannel({
+ uri: "http://www.mozilla.org/",
+ loadUsingSystemPrincipal: true,
+ });
+
+ // Verify initial state
+ var cb = new resolveCallback();
+ cb.nextFunction = filter_test0_1;
+ pps.asyncResolve(channel, 0, cb);
+}
+
+var filter01;
+var filter02;
+
+function filter_test0_1(pi) {
+ Assert.equal(pi, null);
+
+ // Push a filter and verify the results
+
+ filter01 = new BasicFilter();
+ filter02 = new BasicFilter();
+ pps.registerFilter(filter01, 10);
+ pps.registerFilter(filter02, 20);
+
+ var cb = new resolveCallback();
+ cb.nextFunction = filter_test0_2;
+ var channel = NetUtil.newChannel({
+ uri: "http://www.mozilla.org/",
+ loadUsingSystemPrincipal: true,
+ });
+ pps.asyncResolve(channel, 0, cb);
+}
+
+function filter_test0_2(pi) {
+ check_proxy(pi, "http", "localhost", 8080, 0, 10, true);
+ check_proxy(pi.failoverProxy, "direct", "", -1, 0, 0, false);
+
+ pps.unregisterFilter(filter02);
+
+ var cb = new resolveCallback();
+ cb.nextFunction = filter_test0_3;
+ var channel = NetUtil.newChannel({
+ uri: "http://www.mozilla.org/",
+ loadUsingSystemPrincipal: true,
+ });
+ pps.asyncResolve(channel, 0, cb);
+}
+
+function filter_test0_3(pi) {
+ check_proxy(pi, "http", "localhost", 8080, 0, 10, true);
+ check_proxy(pi.failoverProxy, "direct", "", -1, 0, 0, false);
+
+ // Remove filter and verify that we return to the initial state
+
+ pps.unregisterFilter(filter01);
+
+ var cb = new resolveCallback();
+ cb.nextFunction = filter_test0_4;
+ var channel = NetUtil.newChannel({
+ uri: "http://www.mozilla.org/",
+ loadUsingSystemPrincipal: true,
+ });
+ pps.asyncResolve(channel, 0, cb);
+}
+
+var filter03;
+
+function filter_test0_4(pi) {
+ Assert.equal(pi, null);
+ filter03 = new BasicChannelFilter();
+ pps.registerChannelFilter(filter03, 10);
+ var cb = new resolveCallback();
+ cb.nextFunction = filter_test0_5;
+ var channel = NetUtil.newChannel({
+ uri: "http://www.mozilla.org/",
+ loadUsingSystemPrincipal: true,
+ });
+ pps.asyncResolve(channel, 0, cb);
+}
+
+function filter_test0_5(pi) {
+ pps.unregisterChannelFilter(filter03);
+ check_proxy(pi, "http", "www.mozilla.org", 7777, 0, 10, true);
+ check_proxy(pi.failoverProxy, "direct", "", -1, 0, 0, false);
+ run_filter_test_uri();
+}
+
+function run_filter_test_uri() {
+ var cb = new resolveCallback();
+ cb.nextFunction = filter_test_uri0_1;
+ var uri = ios.newURI("http://www.mozilla.org/");
+ pps.asyncResolve(uri, 0, cb);
+}
+
+function filter_test_uri0_1(pi) {
+ Assert.equal(pi, null);
+
+ // Push a filter and verify the results
+
+ filter01 = new BasicFilter();
+ filter02 = new BasicFilter();
+ pps.registerFilter(filter01, 10);
+ pps.registerFilter(filter02, 20);
+
+ var cb = new resolveCallback();
+ cb.nextFunction = filter_test_uri0_2;
+ var uri = ios.newURI("http://www.mozilla.org/");
+ pps.asyncResolve(uri, 0, cb);
+}
+
+function filter_test_uri0_2(pi) {
+ check_proxy(pi, "http", "localhost", 8080, 0, 10, true);
+ check_proxy(pi.failoverProxy, "direct", "", -1, 0, 0, false);
+
+ pps.unregisterFilter(filter02);
+
+ var cb = new resolveCallback();
+ cb.nextFunction = filter_test_uri0_3;
+ var uri = ios.newURI("http://www.mozilla.org/");
+ pps.asyncResolve(uri, 0, cb);
+}
+
+function filter_test_uri0_3(pi) {
+ check_proxy(pi, "http", "localhost", 8080, 0, 10, true);
+ check_proxy(pi.failoverProxy, "direct", "", -1, 0, 0, false);
+
+ // Remove filter and verify that we return to the initial state
+
+ pps.unregisterFilter(filter01);
+
+ var cb = new resolveCallback();
+ cb.nextFunction = filter_test_uri0_4;
+ var uri = ios.newURI("http://www.mozilla.org/");
+ pps.asyncResolve(uri, 0, cb);
+}
+
+function filter_test_uri0_4(pi) {
+ Assert.equal(pi, null);
+ run_filter_test2();
+}
+
+var filter11;
+var filter12;
+
+function run_filter_test2() {
+ // Push a filter and verify the results
+
+ filter11 = new TestFilter("http", "foo", 8080, 0, 10);
+ filter12 = new TestFilter("http", "bar", 8090, 0, 10);
+ pps.registerFilter(filter11, 20);
+ pps.registerFilter(filter12, 10);
+
+ var cb = new resolveCallback();
+ cb.nextFunction = filter_test1_1;
+ var channel = NetUtil.newChannel({
+ uri: "http://www.mozilla.org/",
+ loadUsingSystemPrincipal: true,
+ });
+ pps.asyncResolve(channel, 0, cb);
+}
+
+function filter_test1_1(pi) {
+ check_proxy(pi, "http", "bar", 8090, 0, 10, true);
+ check_proxy(pi.failoverProxy, "http", "foo", 8080, 0, 10, false);
+
+ pps.unregisterFilter(filter12);
+
+ var cb = new resolveCallback();
+ cb.nextFunction = filter_test1_2;
+ var channel = NetUtil.newChannel({
+ uri: "http://www.mozilla.org/",
+ loadUsingSystemPrincipal: true,
+ });
+ pps.asyncResolve(channel, 0, cb);
+}
+
+function filter_test1_2(pi) {
+ check_proxy(pi, "http", "foo", 8080, 0, 10, false);
+
+ // Remove filter and verify that we return to the initial state
+
+ pps.unregisterFilter(filter11);
+
+ var cb = new resolveCallback();
+ cb.nextFunction = filter_test1_3;
+ var channel = NetUtil.newChannel({
+ uri: "http://www.mozilla.org/",
+ loadUsingSystemPrincipal: true,
+ });
+ pps.asyncResolve(channel, 0, cb);
+}
+
+function filter_test1_3(pi) {
+ Assert.equal(pi, null);
+ run_filter_test3();
+}
+
+var filter_3_1;
+
+function run_filter_test3() {
+ var channel = NetUtil.newChannel({
+ uri: "http://www.mozilla.org/",
+ loadUsingSystemPrincipal: true,
+ });
+ // Push a filter and verify the results asynchronously
+
+ filter_3_1 = new TestFilter("http", "foo", 8080, 0, 10);
+ pps.registerFilter(filter_3_1, 20);
+
+ var cb = new resolveCallback();
+ cb.nextFunction = filter_test3_1;
+ pps.asyncResolve(channel, 0, cb);
+}
+
+function filter_test3_1(pi) {
+ check_proxy(pi, "http", "foo", 8080, 0, 10, false);
+ pps.unregisterFilter(filter_3_1);
+ run_pref_test();
+}
+
+function run_pref_test() {
+ var channel = NetUtil.newChannel({
+ uri: "http://www.mozilla.org/",
+ loadUsingSystemPrincipal: true,
+ });
+ // Verify 'direct' setting
+
+ prefs.setIntPref("network.proxy.type", 0);
+
+ var cb = new resolveCallback();
+ cb.nextFunction = pref_test1_1;
+ pps.asyncResolve(channel, 0, cb);
+}
+
+function pref_test1_1(pi) {
+ Assert.equal(pi, null);
+
+ // Verify 'manual' setting
+ prefs.setIntPref("network.proxy.type", 1);
+
+ var cb = new resolveCallback();
+ cb.nextFunction = pref_test1_2;
+ var channel = NetUtil.newChannel({
+ uri: "http://www.mozilla.org/",
+ loadUsingSystemPrincipal: true,
+ });
+ pps.asyncResolve(channel, 0, cb);
+}
+
+function pref_test1_2(pi) {
+ // nothing yet configured
+ Assert.equal(pi, null);
+
+ // try HTTP configuration
+ prefs.setCharPref("network.proxy.http", "foopy");
+ prefs.setIntPref("network.proxy.http_port", 8080);
+
+ var cb = new resolveCallback();
+ cb.nextFunction = pref_test1_3;
+ var channel = NetUtil.newChannel({
+ uri: "http://www.mozilla.org/",
+ loadUsingSystemPrincipal: true,
+ });
+ pps.asyncResolve(channel, 0, cb);
+}
+
+function pref_test1_3(pi) {
+ check_proxy(pi, "http", "foopy", 8080, 0, -1, false);
+
+ prefs.setCharPref("network.proxy.http", "");
+ prefs.setIntPref("network.proxy.http_port", 0);
+
+ // try SOCKS configuration
+ prefs.setCharPref("network.proxy.socks", "barbar");
+ prefs.setIntPref("network.proxy.socks_port", 1203);
+
+ var cb = new resolveCallback();
+ cb.nextFunction = pref_test1_4;
+ var channel = NetUtil.newChannel({
+ uri: "http://www.mozilla.org/",
+ loadUsingSystemPrincipal: true,
+ });
+ pps.asyncResolve(channel, 0, cb);
+}
+
+function pref_test1_4(pi) {
+ check_proxy(pi, "socks", "barbar", 1203, 0, -1, false);
+ run_pac_test();
+}
+
+function TestResolveCallback(type, nexttest) {
+ this.type = type;
+ this.nexttest = nexttest;
+}
+TestResolveCallback.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["nsIProtocolProxyCallback"]),
+
+ onProxyAvailable: function TestResolveCallback_onProxyAvailable(
+ req,
+ channel,
+ pi,
+ status
+ ) {
+ dump("*** channelURI=" + channel.URI.spec + ", status=" + status + "\n");
+
+ if (this.type == null) {
+ Assert.equal(pi, null);
+ } else {
+ Assert.notEqual(req, null);
+ Assert.notEqual(channel, null);
+ Assert.equal(status, 0);
+ Assert.notEqual(pi, null);
+ check_proxy(pi, this.type, "foopy", 8080, 0, -1, true);
+ check_proxy(pi.failoverProxy, "direct", "", -1, -1, -1, false);
+ }
+
+ this.nexttest();
+ },
+};
+
+var originalTLSProxy;
+
+function run_pac_test() {
+ var pac =
+ "data:text/plain," +
+ "function FindProxyForURL(url, host) {" +
+ ' return "PROXY foopy:8080; DIRECT";' +
+ "}";
+ var channel = NetUtil.newChannel({
+ uri: "http://www.mozilla.org/",
+ loadUsingSystemPrincipal: true,
+ });
+ // Configure PAC
+
+ prefs.setIntPref("network.proxy.type", 2);
+ prefs.setCharPref("network.proxy.autoconfig_url", pac);
+ pps.asyncResolve(channel, 0, new TestResolveCallback("http", run_pac2_test));
+}
+
+function run_pac2_test() {
+ var pac =
+ "data:text/plain," +
+ "function FindProxyForURL(url, host) {" +
+ ' return "HTTPS foopy:8080; DIRECT";' +
+ "}";
+ var channel = NetUtil.newChannel({
+ uri: "http://www.mozilla.org/",
+ loadUsingSystemPrincipal: true,
+ });
+ // Configure PAC
+ originalTLSProxy = prefs.getBoolPref("network.proxy.proxy_over_tls");
+
+ prefs.setCharPref("network.proxy.autoconfig_url", pac);
+ prefs.setBoolPref("network.proxy.proxy_over_tls", true);
+
+ pps.asyncResolve(channel, 0, new TestResolveCallback("https", run_pac3_test));
+}
+
+function run_pac3_test() {
+ var pac =
+ "data:text/plain," +
+ "function FindProxyForURL(url, host) {" +
+ ' return "HTTPS foopy:8080; DIRECT";' +
+ "}";
+ var channel = NetUtil.newChannel({
+ uri: "http://www.mozilla.org/",
+ loadUsingSystemPrincipal: true,
+ });
+ // Configure PAC
+ prefs.setCharPref("network.proxy.autoconfig_url", pac);
+ prefs.setBoolPref("network.proxy.proxy_over_tls", false);
+
+ pps.asyncResolve(channel, 0, new TestResolveCallback(null, run_pac4_test));
+}
+
+function run_pac4_test() {
+ // Bug 1251332
+ let wRange = [
+ ["SUN", "MON", "SAT", "MON"], // for Sun
+ ["SUN", "TUE", "SAT", "TUE"], // for Mon
+ ["MON", "WED", "SAT", "WED"], // for Tue
+ ["TUE", "THU", "SAT", "THU"], // for Wed
+ ["WED", "FRI", "WED", "SUN"], // for Thu
+ ["THU", "SAT", "THU", "SUN"], // for Fri
+ ["FRI", "SAT", "FRI", "SUN"], // for Sat
+ ];
+ let today = new Date().getDay();
+ var pac =
+ "data:text/plain," +
+ "function FindProxyForURL(url, host) {" +
+ ' if (weekdayRange("' +
+ wRange[today][0] +
+ '", "' +
+ wRange[today][1] +
+ '") &&' +
+ ' weekdayRange("' +
+ wRange[today][2] +
+ '", "' +
+ wRange[today][3] +
+ '")) {' +
+ ' return "PROXY foopy:8080; DIRECT";' +
+ " }" +
+ "}";
+ var channel = NetUtil.newChannel({
+ uri: "http://www.mozilla.org/",
+ loadUsingSystemPrincipal: true,
+ });
+ // Configure PAC
+
+ prefs.setIntPref("network.proxy.type", 2);
+ prefs.setCharPref("network.proxy.autoconfig_url", pac);
+ pps.asyncResolve(
+ channel,
+ 0,
+ new TestResolveCallback("http", run_utf8_pac_test)
+ );
+}
+
+function run_utf8_pac_test() {
+ var pac =
+ "data:text/plain;charset=UTF-8," +
+ "function FindProxyForURL(url, host) {" +
+ " /*" +
+ " U+00A9 COPYRIGHT SIGN: %C2%A9," +
+ " U+0B87 TAMIL LETTER I: %E0%AE%87," +
+ " U+10398 UGARITIC LETTER THANNA: %F0%90%8E%98 " +
+ " */" +
+ ' var multiBytes = "%C2%A9 %E0%AE%87 %F0%90%8E%98"; ' +
+ " /* 6 UTF-16 units above if PAC script run as UTF-8; 11 units if run as Latin-1 */ " +
+ " return multiBytes.length === 6 " +
+ ' ? "PROXY foopy:8080; DIRECT" ' +
+ ' : "PROXY epicfail-utf8:12345; DIRECT";' +
+ "}";
+
+ var channel = NetUtil.newChannel({
+ uri: "http://www.mozilla.org/",
+ loadUsingSystemPrincipal: true,
+ });
+
+ // Configure PAC
+ prefs.setIntPref("network.proxy.type", 2);
+ prefs.setCharPref("network.proxy.autoconfig_url", pac);
+
+ pps.asyncResolve(
+ channel,
+ 0,
+ new TestResolveCallback("http", run_latin1_pac_test)
+ );
+}
+
+function run_latin1_pac_test() {
+ var pac =
+ "data:text/plain," +
+ "function FindProxyForURL(url, host) {" +
+ " /* A too-long encoding of U+0000, so not valid UTF-8 */ " +
+ ' var multiBytes = "%C0%80"; ' +
+ " /* 2 UTF-16 units because interpreted as Latin-1 */ " +
+ " return multiBytes.length === 2 " +
+ ' ? "PROXY foopy:8080; DIRECT" ' +
+ ' : "PROXY epicfail-latin1:12345; DIRECT";' +
+ "}";
+
+ var channel = NetUtil.newChannel({
+ uri: "http://www.mozilla.org/",
+ loadUsingSystemPrincipal: true,
+ });
+
+ // Configure PAC
+ prefs.setIntPref("network.proxy.type", 2);
+ prefs.setCharPref("network.proxy.autoconfig_url", pac);
+
+ pps.asyncResolve(
+ channel,
+ 0,
+ new TestResolveCallback("http", finish_pac_test)
+ );
+}
+
+function finish_pac_test() {
+ prefs.setBoolPref("network.proxy.proxy_over_tls", originalTLSProxy);
+ run_pac_cancel_test();
+}
+
+function TestResolveCancelationCallback() {}
+TestResolveCancelationCallback.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["nsIProtocolProxyCallback"]),
+
+ onProxyAvailable: function TestResolveCancelationCallback_onProxyAvailable(
+ req,
+ channel,
+ pi,
+ status
+ ) {
+ dump("*** channelURI=" + channel.URI.spec + ", status=" + status + "\n");
+
+ Assert.notEqual(req, null);
+ Assert.notEqual(channel, null);
+ Assert.equal(status, Cr.NS_ERROR_ABORT);
+ Assert.equal(pi, null);
+
+ prefs.setCharPref("network.proxy.autoconfig_url", "");
+ prefs.setIntPref("network.proxy.type", 0);
+
+ run_proxy_host_filters_test();
+ },
+};
+
+function run_pac_cancel_test() {
+ var channel = NetUtil.newChannel({
+ uri: "http://www.mozilla.org/",
+ loadUsingSystemPrincipal: true,
+ });
+ // Configure PAC
+ var pac =
+ "data:text/plain," +
+ "function FindProxyForURL(url, host) {" +
+ ' return "PROXY foopy:8080; DIRECT";' +
+ "}";
+ prefs.setIntPref("network.proxy.type", 2);
+ prefs.setCharPref("network.proxy.autoconfig_url", pac);
+
+ var req = pps.asyncResolve(channel, 0, new TestResolveCancelationCallback());
+ req.cancel(Cr.NS_ERROR_ABORT);
+}
+
+var hostList;
+var hostIDX;
+var bShouldBeFiltered;
+var hostNextFX;
+
+function check_host_filters(hl, shouldBe, nextFX) {
+ hostList = hl;
+ hostIDX = 0;
+ bShouldBeFiltered = shouldBe;
+ hostNextFX = nextFX;
+
+ if (hostList.length > hostIDX) {
+ check_host_filter(hostIDX);
+ }
+}
+
+function check_host_filters_cb() {
+ hostIDX++;
+ if (hostList.length > hostIDX) {
+ check_host_filter(hostIDX);
+ } else {
+ hostNextFX();
+ }
+}
+
+function check_host_filter(i) {
+ dump(
+ "*** uri=" + hostList[i] + " bShouldBeFiltered=" + bShouldBeFiltered + "\n"
+ );
+ var channel = NetUtil.newChannel({
+ uri: hostList[i],
+ loadUsingSystemPrincipal: true,
+ });
+ var cb = new resolveCallback();
+ cb.nextFunction = host_filter_cb;
+ pps.asyncResolve(channel, 0, cb);
+}
+
+function host_filter_cb(proxy) {
+ if (bShouldBeFiltered) {
+ Assert.equal(proxy, null);
+ } else {
+ Assert.notEqual(proxy, null);
+ // Just to be sure, let's check that the proxy is correct
+ // - this should match the proxy setup in the calling function
+ check_proxy(proxy, "http", "foopy", 8080, 0, -1, false);
+ }
+ check_host_filters_cb();
+}
+
+// Verify that hists in the host filter list are not proxied
+// refers to "network.proxy.no_proxies_on"
+
+var uriStrUseProxyList;
+var hostFilterList;
+var uriStrFilterList;
+
+function run_proxy_host_filters_test() {
+ // Get prefs object from DOM
+ // Setup a basic HTTP proxy configuration
+ // - pps.resolve() needs this to return proxy info for non-filtered hosts
+ prefs.setIntPref("network.proxy.type", 1);
+ prefs.setCharPref("network.proxy.http", "foopy");
+ prefs.setIntPref("network.proxy.http_port", 8080);
+
+ // Setup host filter list string for "no_proxies_on"
+ hostFilterList =
+ "www.mozilla.org, www.google.com, www.apple.com, " +
+ ".domain, .domain2.org";
+ prefs.setCharPref("network.proxy.no_proxies_on", hostFilterList);
+ Assert.equal(
+ prefs.getCharPref("network.proxy.no_proxies_on"),
+ hostFilterList
+ );
+
+ // Check the hosts that should be filtered out
+ uriStrFilterList = [
+ "http://www.mozilla.org/",
+ "http://www.google.com/",
+ "http://www.apple.com/",
+ "http://somehost.domain/",
+ "http://someotherhost.domain/",
+ "http://somehost.domain2.org/",
+ "http://somehost.subdomain.domain2.org/",
+ ];
+ check_host_filters(uriStrFilterList, true, host_filters_1);
+}
+
+function host_filters_1() {
+ // Check the hosts that should be proxied
+ uriStrUseProxyList = [
+ "http://www.mozilla.com/",
+ "http://mail.google.com/",
+ "http://somehost.domain.co.uk/",
+ "http://somelocalhost/",
+ ];
+ check_host_filters(uriStrUseProxyList, false, host_filters_2);
+}
+
+function host_filters_2() {
+ // Set no_proxies_on to include local hosts
+ prefs.setCharPref(
+ "network.proxy.no_proxies_on",
+ hostFilterList + ", <local>"
+ );
+ Assert.equal(
+ prefs.getCharPref("network.proxy.no_proxies_on"),
+ hostFilterList + ", <local>"
+ );
+ // Amend lists - move local domain to filtered list
+ uriStrFilterList.push(uriStrUseProxyList.pop());
+ check_host_filters(uriStrFilterList, true, host_filters_3);
+}
+
+function host_filters_3() {
+ check_host_filters(uriStrUseProxyList, false, host_filters_4);
+}
+
+function host_filters_4() {
+ // Cleanup
+ prefs.setCharPref("network.proxy.no_proxies_on", "");
+ Assert.equal(prefs.getCharPref("network.proxy.no_proxies_on"), "");
+
+ run_myipaddress_test();
+}
+
+function run_myipaddress_test() {
+ // This test makes sure myIpAddress() comes up with some valid
+ // IP address other than localhost. The DUT must be configured with
+ // an Internet route for this to work - though no Internet traffic
+ // should be created.
+
+ var pac =
+ "data:text/plain," +
+ "var pacUseMultihomedDNS = true;\n" +
+ "function FindProxyForURL(url, host) {" +
+ ' return "PROXY " + myIpAddress() + ":1234";' +
+ "}";
+
+ // no traffic to this IP is ever sent, it is just a public IP that
+ // does not require DNS to determine a route.
+ var channel = NetUtil.newChannel({
+ uri: "http://192.0.43.10/",
+ loadUsingSystemPrincipal: true,
+ });
+ prefs.setIntPref("network.proxy.type", 2);
+ prefs.setCharPref("network.proxy.autoconfig_url", pac);
+
+ var cb = new resolveCallback();
+ cb.nextFunction = myipaddress_callback;
+ pps.asyncResolve(channel, 0, cb);
+}
+
+function myipaddress_callback(pi) {
+ Assert.notEqual(pi, null);
+ Assert.equal(pi.type, "http");
+ Assert.equal(pi.port, 1234);
+
+ // make sure we didn't return localhost
+ Assert.notEqual(pi.host, null);
+ Assert.notEqual(pi.host, "127.0.0.1");
+ Assert.notEqual(pi.host, "::1");
+
+ run_myipaddress_test_2();
+}
+
+function run_myipaddress_test_2() {
+ // test that myIPAddress() can be used outside of the scope of
+ // FindProxyForURL(). bug 829646.
+
+ var pac =
+ "data:text/plain," +
+ "var pacUseMultihomedDNS = true;\n" +
+ "var myaddr = myIpAddress(); " +
+ "function FindProxyForURL(url, host) {" +
+ ' return "PROXY " + myaddr + ":5678";' +
+ "}";
+
+ var channel = NetUtil.newChannel({
+ uri: "http://www.mozilla.org/",
+ loadUsingSystemPrincipal: true,
+ });
+ prefs.setIntPref("network.proxy.type", 2);
+ prefs.setCharPref("network.proxy.autoconfig_url", pac);
+
+ var cb = new resolveCallback();
+ cb.nextFunction = myipaddress2_callback;
+ pps.asyncResolve(channel, 0, cb);
+}
+
+function myipaddress2_callback(pi) {
+ Assert.notEqual(pi, null);
+ Assert.equal(pi.type, "http");
+ Assert.equal(pi.port, 5678);
+
+ // make sure we didn't return localhost
+ Assert.notEqual(pi.host, null);
+ Assert.notEqual(pi.host, "127.0.0.1");
+ Assert.notEqual(pi.host, "::1");
+
+ run_failed_script_test();
+}
+
+function run_failed_script_test() {
+ // test to make sure we go direct with invalid PAC
+ // eslint-disable-next-line no-useless-concat
+ var pac = "data:text/plain," + "\nfor(;\n";
+
+ var channel = NetUtil.newChannel({
+ uri: "http://www.mozilla.org/",
+ loadUsingSystemPrincipal: true,
+ });
+ prefs.setIntPref("network.proxy.type", 2);
+ prefs.setCharPref("network.proxy.autoconfig_url", pac);
+
+ var cb = new resolveCallback();
+ cb.nextFunction = failed_script_callback;
+ pps.asyncResolve(channel, 0, cb);
+}
+
+var directFilter;
+const TEST_URI = "http://127.0.0.1:7247/";
+
+function failed_script_callback(pi) {
+ // we should go direct
+ Assert.equal(pi, null);
+
+ // test that we honor filters when configured to go direct
+ prefs.setIntPref("network.proxy.type", 0);
+ directFilter = new TestFilter("http", "127.0.0.1", 7246, 0, 0);
+ pps.registerFilter(directFilter, 10);
+
+ // test that on-modify-request contains the proxy info too
+ var obs = Cc["@mozilla.org/observer-service;1"].getService();
+ obs = obs.QueryInterface(Ci.nsIObserverService);
+ obs.addObserver(directFilterListener, "http-on-modify-request");
+
+ var ssm = Services.scriptSecurityManager;
+ let uri = TEST_URI;
+ var chan = NetUtil.newChannel({
+ uri,
+ loadingPrincipal: ssm.createContentPrincipal(Services.io.newURI(uri), {}),
+ securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_INHERITS_SEC_CONTEXT,
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT,
+ });
+
+ chan.asyncOpen(directFilterListener);
+}
+
+var directFilterListener = {
+ onModifyRequestCalled: false,
+
+ onStartRequest: function test_onStart(request) {},
+ onDataAvailable: function test_OnData() {},
+
+ onStopRequest: function test_onStop(request, status) {
+ // check on the PI from the channel itself
+ request.QueryInterface(Ci.nsIProxiedChannel);
+ check_proxy(request.proxyInfo, "http", "127.0.0.1", 7246, 0, 0, false);
+ pps.unregisterFilter(directFilter);
+
+ // check on the PI from on-modify-request
+ Assert.ok(this.onModifyRequestCalled);
+ var obs = Cc["@mozilla.org/observer-service;1"].getService();
+ obs = obs.QueryInterface(Ci.nsIObserverService);
+ obs.removeObserver(this, "http-on-modify-request");
+
+ run_isresolvable_test();
+ },
+
+ observe(subject, topic, data) {
+ if (
+ topic === "http-on-modify-request" &&
+ subject instanceof Ci.nsIHttpChannel &&
+ subject instanceof Ci.nsIProxiedChannel
+ ) {
+ info("check proxy in observe uri=" + subject.URI.spec);
+ if (subject.URI.spec != TEST_URI) {
+ return;
+ }
+ check_proxy(subject.proxyInfo, "http", "127.0.0.1", 7246, 0, 0, false);
+ this.onModifyRequestCalled = true;
+ }
+ },
+};
+
+function run_isresolvable_test() {
+ // test a non resolvable host in the pac file
+
+ var pac =
+ "data:text/plain," +
+ "function FindProxyForURL(url, host) {" +
+ ' if (isResolvable("nonexistant.lan.onion"))' +
+ ' return "DIRECT";' +
+ ' return "PROXY 127.0.0.1:1234";' +
+ "}";
+
+ var channel = NetUtil.newChannel({
+ uri: "http://www.mozilla.org/",
+ loadUsingSystemPrincipal: true,
+ });
+ prefs.setIntPref("network.proxy.type", 2);
+ prefs.setCharPref("network.proxy.autoconfig_url", pac);
+
+ var cb = new resolveCallback();
+ cb.nextFunction = isresolvable_callback;
+ pps.asyncResolve(channel, 0, cb);
+}
+
+function isresolvable_callback(pi) {
+ Assert.notEqual(pi, null);
+ Assert.equal(pi.type, "http");
+ Assert.equal(pi.port, 1234);
+ Assert.equal(pi.host, "127.0.0.1");
+
+ run_localhost_pac();
+}
+
+function run_localhost_pac() {
+ // test localhost in the pac file
+
+ var pac =
+ "data:text/plain," +
+ "function FindProxyForURL(url, host) {" +
+ ' return "PROXY totallycrazy:1234";' +
+ "}";
+
+ // Use default filter list string for "no_proxies_on" ("localhost, 127.0.0.1")
+ prefs.clearUserPref("network.proxy.no_proxies_on");
+ var channel = NetUtil.newChannel({
+ uri: "http://localhost/",
+ loadUsingSystemPrincipal: true,
+ });
+ prefs.setIntPref("network.proxy.type", 2);
+ prefs.setCharPref("network.proxy.autoconfig_url", pac);
+
+ var cb = new resolveCallback();
+ cb.nextFunction = localhost_callback;
+ pps.asyncResolve(channel, 0, cb);
+}
+
+function localhost_callback(pi) {
+ Assert.equal(pi, null); // no proxy!
+
+ prefs.setIntPref("network.proxy.type", 0);
+
+ if (mozinfo.socketprocess_networking && again) {
+ info("run test again");
+ again = false;
+ cleanUp();
+ prefs.setBoolPref("network.proxy.parse_pac_on_socket_process", true);
+ run_filter_test();
+ } else {
+ cleanUp();
+ do_test_finished();
+ }
+}
+
+function cleanUp() {
+ prefs.clearUserPref("network.proxy.type");
+ prefs.clearUserPref("network.proxy.http");
+ prefs.clearUserPref("network.proxy.http_port");
+ prefs.clearUserPref("network.proxy.socks");
+ prefs.clearUserPref("network.proxy.socks_port");
+ prefs.clearUserPref("network.proxy.autoconfig_url");
+ prefs.clearUserPref("network.proxy.proxy_over_tls");
+ prefs.clearUserPref("network.proxy.no_proxies_on");
+ prefs.clearUserPref("network.proxy.parse_pac_on_socket_process");
+}
+
+function run_test() {
+ register_test_protocol_handler();
+
+ prefs.setBoolPref("network.proxy.parse_pac_on_socket_process", false);
+ // start of asynchronous test chain
+ run_filter_test();
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_proxy-failover_canceled.js b/netwerk/test/unit/test_proxy-failover_canceled.js
new file mode 100644
index 0000000000..e594c497cc
--- /dev/null
+++ b/netwerk/test/unit/test_proxy-failover_canceled.js
@@ -0,0 +1,57 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+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.bodyOutputStream.write(responseBody, responseBody.length);
+}
+
+function finish_test(request, buffer) {
+ Assert.equal(buffer, "");
+ httpServer.stop(do_test_finished);
+}
+
+function run_test() {
+ httpServer = new HttpServer();
+ httpServer.registerPathHandler("/content", contentHandler);
+ httpServer.start(-1);
+
+ // we want to cancel the failover proxy engage, so, do not allow
+ // redirects from now.
+
+ var nc = new ChannelEventSink();
+ nc._flags = ES_ABORT_REDIRECT;
+
+ var prefs = Services.prefs.getBranch("network.proxy.");
+ prefs.setIntPref("type", 2);
+ prefs.setCharPref("no_proxies_on", "nothing");
+ prefs.setBoolPref("allow_hijacking_localhost", true);
+ prefs.setCharPref(
+ "autoconfig_url",
+ "data:text/plain," +
+ "function FindProxyForURL(url, host) {return 'PROXY a_non_existent_domain_x7x6c572v:80; PROXY localhost:" +
+ httpServer.identity.primaryPort +
+ "';}"
+ );
+
+ var chan = make_channel(
+ "http://localhost:" + httpServer.identity.primaryPort + "/content"
+ );
+ chan.notificationCallbacks = nc;
+ chan.asyncOpen(new ChannelListener(finish_test, null, CL_EXPECT_FAILURE));
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_proxy-failover_passing.js b/netwerk/test/unit/test_proxy-failover_passing.js
new file mode 100644
index 0000000000..a6fab56690
--- /dev/null
+++ b/netwerk/test/unit/test_proxy-failover_passing.js
@@ -0,0 +1,45 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+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.bodyOutputStream.write(responseBody, responseBody.length);
+}
+
+function finish_test(request, buffer) {
+ Assert.equal(buffer, responseBody);
+ httpServer.stop(do_test_finished);
+}
+
+function run_test() {
+ httpServer = new HttpServer();
+ httpServer.registerPathHandler("/content", contentHandler);
+ httpServer.start(-1);
+
+ var prefs = Services.prefs.getBranch("network.proxy.");
+ prefs.setIntPref("type", 2);
+ prefs.setCharPref(
+ "autoconfig_url",
+ "data:text/plain," +
+ "function FindProxyForURL(url, host) {return 'PROXY a_non_existent_domain_x7x6c572v:80; PROXY localhost:" +
+ httpServer.identity.primaryPort +
+ "';}"
+ );
+
+ var chan = make_channel(
+ "http://localhost:" + httpServer.identity.primaryPort + "/content"
+ );
+ chan.asyncOpen(new ChannelListener(finish_test, null));
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_proxy-replace_canceled.js b/netwerk/test/unit/test_proxy-replace_canceled.js
new file mode 100644
index 0000000000..b39d5e97f6
--- /dev/null
+++ b/netwerk/test/unit/test_proxy-replace_canceled.js
@@ -0,0 +1,57 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+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.bodyOutputStream.write(responseBody, responseBody.length);
+}
+
+function finish_test(request, buffer) {
+ Assert.equal(buffer, "");
+ httpServer.stop(do_test_finished);
+}
+
+function run_test() {
+ httpServer = new HttpServer();
+ httpServer.registerPathHandler("/content", contentHandler);
+ httpServer.start(-1);
+
+ var prefs = Services.prefs.getBranch("network.proxy.");
+ prefs.setIntPref("type", 2);
+ prefs.setCharPref(
+ "autoconfig_url",
+ "data:text/plain," +
+ "function FindProxyForURL(url, host) {return 'PROXY localhost:" +
+ httpServer.identity.primaryPort +
+ "';}"
+ );
+
+ // this test assumed that a AsyncOnChannelRedirect query is made for
+ // each proxy failover or on the inital proxy only when PAC mode is used.
+ // Neither of those are documented anywhere that I can find and the latter
+ // hasn't been a useful property because it is PAC dependent and the type
+ // is generally unknown and OS driven. 769764 changed that to remove the
+ // internal redirect used to setup the initial proxy/channel as that isn't
+ // a redirect in any sense.
+
+ var chan = make_channel(
+ "http://localhost:" + httpServer.identity.primaryPort + "/content"
+ );
+ chan.asyncOpen(new ChannelListener(finish_test, null, CL_EXPECT_FAILURE));
+ chan.cancel(Cr.NS_BINDING_ABORTED);
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_proxy-replace_passing.js b/netwerk/test/unit/test_proxy-replace_passing.js
new file mode 100644
index 0000000000..9c843c001e
--- /dev/null
+++ b/netwerk/test/unit/test_proxy-replace_passing.js
@@ -0,0 +1,45 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+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.bodyOutputStream.write(responseBody, responseBody.length);
+}
+
+function finish_test(request, buffer) {
+ Assert.equal(buffer, responseBody);
+ httpServer.stop(do_test_finished);
+}
+
+function run_test() {
+ httpServer = new HttpServer();
+ httpServer.registerPathHandler("/content", contentHandler);
+ httpServer.start(-1);
+
+ var prefs = Services.prefs.getBranch("network.proxy.");
+ prefs.setIntPref("type", 2);
+ prefs.setCharPref(
+ "autoconfig_url",
+ "data:text/plain," +
+ "function FindProxyForURL(url, host) {return 'PROXY localhost:" +
+ httpServer.identity.primaryPort +
+ "';}"
+ );
+
+ var chan = make_channel(
+ "http://localhost:" + httpServer.identity.primaryPort + "/content"
+ );
+ chan.asyncOpen(new ChannelListener(finish_test, null));
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_proxy-slow-upload.js b/netwerk/test/unit/test_proxy-slow-upload.js
new file mode 100644
index 0000000000..1a406890e1
--- /dev/null
+++ b/netwerk/test/unit/test_proxy-slow-upload.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/. */
+
+"use strict";
+
+/* import-globals-from head_cache.js */
+/* import-globals-from head_cookies.js */
+/* import-globals-from head_channels.js */
+/* import-globals-from head_servers.js */
+
+const SIZE = 4096;
+const CONTENT = "x".repeat(SIZE);
+
+add_task(async function test_slow_upload() {
+ let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(
+ Ci.nsIX509CertDB
+ );
+ addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u");
+ addCertFromFile(certdb, "proxy-ca.pem", "CTu,u,u");
+
+ let proxies = [
+ NodeHTTPProxyServer,
+ NodeHTTPSProxyServer,
+ NodeHTTP2ProxyServer,
+ ];
+ for (let p of proxies) {
+ let proxy = new p();
+ await proxy.start();
+ registerCleanupFunction(async () => {
+ await proxy.stop();
+ });
+
+ await with_node_servers(
+ [NodeHTTPServer, NodeHTTPSServer, NodeHTTP2Server],
+ async server => {
+ info(`Testing ${p.name} with ${server.constructor.name}`);
+ await server.execute(
+ `global.server_name = "${server.constructor.name}";`
+ );
+ await server.registerPathHandler("/test", (req, resp) => {
+ let content = "";
+ req.on("data", data => {
+ global.data_count = (global.data_count || 0) + 1;
+ content += data;
+ });
+ req.on("end", () => {
+ resp.writeHead(200);
+ resp.end(content);
+ });
+ });
+
+ let sstream = Cc[
+ "@mozilla.org/io/string-input-stream;1"
+ ].createInstance(Ci.nsIStringInputStream);
+ sstream.data = CONTENT;
+
+ let mime = Cc[
+ "@mozilla.org/network/mime-input-stream;1"
+ ].createInstance(Ci.nsIMIMEInputStream);
+ mime.addHeader("Content-Type", "multipart/form-data; boundary=zzzzz");
+ mime.setData(sstream);
+
+ let tq = Cc["@mozilla.org/network/throttlequeue;1"].createInstance(
+ Ci.nsIInputChannelThrottleQueue
+ );
+ // Make sure the request takes more than one read.
+ tq.init(100 + SIZE / 2, 100 + SIZE / 2);
+
+ let chan = NetUtil.newChannel({
+ uri: `${server.origin()}/test`,
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+
+ let tic = chan.QueryInterface(Ci.nsIThrottledInputChannel);
+ tic.throttleQueue = tq;
+
+ chan
+ .QueryInterface(Ci.nsIUploadChannel)
+ .setUploadStream(mime, "", mime.available());
+ chan.requestMethod = "POST";
+
+ let { req, buff } = await new Promise(resolve => {
+ chan.asyncOpen(
+ new ChannelListener(
+ (req1, buff1) => resolve({ req: req1, buff: buff1 }),
+ null,
+ CL_ALLOW_UNKNOWN_CL
+ )
+ );
+ });
+ equal(req.status, Cr.NS_OK);
+ equal(req.QueryInterface(Ci.nsIHttpChannel).responseStatus, 200);
+ ok(buff == CONTENT, "Content must match");
+ ok(!!req.QueryInterface(Ci.nsIProxiedChannel).proxyInfo);
+ greater(
+ await server.execute(`global.data_count`),
+ 1,
+ "Content should have been streamed to the server in several chunks"
+ );
+ }
+ );
+ await proxy.stop();
+ }
+});
diff --git a/netwerk/test/unit/test_proxy_cancel.js b/netwerk/test/unit/test_proxy_cancel.js
new file mode 100644
index 0000000000..d891f3147c
--- /dev/null
+++ b/netwerk/test/unit/test_proxy_cancel.js
@@ -0,0 +1,397 @@
+/* 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";
+
+/* globals setTimeout */
+
+const { TestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TestUtils.sys.mjs"
+);
+
+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_cancel_after_asyncOpen() {
+ let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(
+ Ci.nsIX509CertDB
+ );
+ addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u");
+ addCertFromFile(certdb, "proxy-ca.pem", "CTu,u,u");
+
+ let proxies = [
+ NodeHTTPProxyServer,
+ NodeHTTPSProxyServer,
+ NodeHTTP2ProxyServer,
+ ];
+ for (let p of proxies) {
+ let proxy = new p();
+ await proxy.start();
+ registerCleanupFunction(async () => {
+ await proxy.stop();
+ });
+ await with_node_servers(
+ [NodeHTTPServer, NodeHTTPSServer, NodeHTTP2Server],
+ async server => {
+ info(`Testing ${p.name} with ${server.constructor.name}`);
+ await server.execute(
+ `global.server_name = "${server.constructor.name}";`
+ );
+
+ await server.registerPathHandler("/test", (req, resp) => {
+ resp.writeHead(200);
+ resp.end(global.server_name);
+ });
+
+ let chan = makeChan(`${server.origin()}/test`);
+ let openPromise = new Promise(resolve => {
+ chan.asyncOpen(
+ new ChannelListener(
+ (req, buff) => resolve({ req, buff }),
+ null,
+ CL_EXPECT_FAILURE
+ )
+ );
+ });
+ chan.cancel(Cr.NS_ERROR_ABORT);
+ let { req } = await openPromise;
+ Assert.equal(req.status, Cr.NS_ERROR_ABORT);
+ }
+ );
+ await proxy.stop();
+ }
+});
+
+// const NS_NET_STATUS_CONNECTING_TO = 0x4b0007;
+// const NS_NET_STATUS_CONNECTED_TO = 0x4b0004;
+// const NS_NET_STATUS_SENDING_TO = 0x4b0005;
+const NS_NET_STATUS_WAITING_FOR = 0x4b000a; // 2152398858
+const NS_NET_STATUS_RECEIVING_FROM = 0x4b0006;
+// const NS_NET_STATUS_TLS_HANDSHAKE_STARTING = 0x4b000c; // 2152398860
+// const NS_NET_STATUS_TLS_HANDSHAKE_ENDED = 0x4b000d; // 2152398861
+
+add_task(async function test_cancel_after_connect_http2proxy() {
+ let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(
+ Ci.nsIX509CertDB
+ );
+ addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u");
+ addCertFromFile(certdb, "proxy-ca.pem", "CTu,u,u");
+
+ await with_node_servers(
+ [NodeHTTPServer, NodeHTTPSServer, NodeHTTP2Server],
+ async server => {
+ // Set up a proxy for each server to make sure proxy state is clean
+ // for each test.
+ let proxy = new NodeHTTP2ProxyServer();
+ await proxy.start();
+ registerCleanupFunction(async () => {
+ await proxy.stop();
+ });
+ await proxy.execute(`
+ global.session_counter = 0;
+ global.proxy.on("session", () => {
+ global.session_counter++;
+ });
+ `);
+
+ info(`Testing ${proxy.constructor.name} with ${server.constructor.name}`);
+ await server.execute(
+ `global.server_name = "${server.constructor.name}";`
+ );
+
+ await server.registerPathHandler("/test", (req, resp) => {
+ global.reqCount = (global.reqCount || 0) + 1;
+ resp.writeHead(200);
+ resp.end(global.server_name);
+ });
+
+ let chan = makeChan(`${server.origin()}/test`);
+ chan.notificationCallbacks = {
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIInterfaceRequestor",
+ "nsIProgressEventSink",
+ ]),
+
+ getInterface(iid) {
+ return this.QueryInterface(iid);
+ },
+
+ onProgress(request, progress, progressMax) {},
+ onStatus(request, status, statusArg) {
+ info(`status = ${status}`);
+ // XXX(valentin): Is this the best status to be cancelling?
+ if (status == NS_NET_STATUS_WAITING_FOR) {
+ info("cancelling connected channel");
+ chan.cancel(Cr.NS_ERROR_ABORT);
+ }
+ },
+ };
+ let openPromise = new Promise(resolve => {
+ chan.asyncOpen(
+ new ChannelListener(
+ (req, buff) => resolve({ req, buff }),
+ null,
+ CL_EXPECT_FAILURE
+ )
+ );
+ });
+ let { req } = await openPromise;
+ Assert.equal(req.status, Cr.NS_ERROR_ABORT);
+
+ // Since we're cancelling just after connect, we'd expect that no
+ // requests are actually registered. But because we're cancelling on the
+ // main thread, and the request is being performed on the socket thread,
+ // it might actually reach the server, especially in chaos test mode.
+ // Assert.equal(
+ // await server.execute(`global.reqCount || 0`),
+ // 0,
+ // `No requests should have been made at this point`
+ // );
+ Assert.equal(await proxy.execute(`global.session_counter`), 1);
+
+ chan = makeChan(`${server.origin()}/test`);
+ await new Promise(resolve => {
+ chan.asyncOpen(
+ new ChannelListener(
+ // eslint-disable-next-line no-shadow
+ (req, buff) => resolve({ req, buff }),
+ null,
+ CL_ALLOW_UNKNOWN_CL
+ )
+ );
+ });
+
+ // Check that there's still only one session.
+ Assert.equal(await proxy.execute(`global.session_counter`), 1);
+ await proxy.stop();
+ }
+ );
+});
+
+add_task(async function test_cancel_after_sending_request() {
+ let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(
+ Ci.nsIX509CertDB
+ );
+ addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u");
+ addCertFromFile(certdb, "proxy-ca.pem", "CTu,u,u");
+
+ await with_node_servers(
+ [NodeHTTPServer, NodeHTTPSServer, NodeHTTP2Server],
+ async server => {
+ let proxies = [
+ NodeHTTPProxyServer,
+ NodeHTTPSProxyServer,
+ NodeHTTP2ProxyServer,
+ ];
+ for (let p of proxies) {
+ let proxy = new p();
+ await proxy.start();
+ registerCleanupFunction(async () => {
+ await proxy.stop();
+ });
+
+ await proxy.execute(`
+ global.session_counter = 0;
+ global.proxy.on("session", () => {
+ global.session_counter++;
+ });
+ `);
+ info(`Testing ${p.name} with ${server.constructor.name}`);
+ await server.execute(
+ `global.server_name = "${server.constructor.name}";`
+ );
+
+ await server.registerPathHandler("/test", (req, resp) => {
+ // Here we simmulate a slow response to give the test time to
+ // cancel the channel before receiving the response.
+ global.request_count = (global.request_count || 0) + 1;
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ setTimeout(() => {
+ resp.writeHead(200);
+ resp.end(global.server_name);
+ }, 2000);
+ });
+ await server.registerPathHandler("/instant", (req, resp) => {
+ resp.writeHead(200);
+ resp.end(global.server_name);
+ });
+
+ // It seems proxy.on("session") only gets emitted after a full request.
+ // So we first load a simple request, then the long lasting request
+ // that we then cancel before it has the chance to complete.
+ let chan = makeChan(`${server.origin()}/instant`);
+ await new Promise(resolve => {
+ chan.asyncOpen(
+ new ChannelListener(resolve, null, CL_ALLOW_UNKNOWN_CL)
+ );
+ });
+
+ chan = makeChan(`${server.origin()}/test`);
+ let openPromise = new Promise(resolve => {
+ chan.asyncOpen(
+ new ChannelListener(
+ (req, buff) => resolve({ req, buff }),
+ null,
+ CL_EXPECT_FAILURE
+ )
+ );
+ });
+ // XXX(valentin) This might be a little racy
+ await TestUtils.waitForCondition(async () => {
+ return (await server.execute("global.request_count")) > 0;
+ });
+
+ chan.cancel(Cr.NS_ERROR_ABORT);
+
+ let { req } = await openPromise;
+ Assert.equal(req.status, Cr.NS_ERROR_ABORT);
+
+ async function checkSessionCounter() {
+ if (p.name == "NodeHTTP2ProxyServer") {
+ Assert.equal(await proxy.execute(`global.session_counter`), 1);
+ }
+ }
+
+ await checkSessionCounter();
+
+ chan = makeChan(`${server.origin()}/instant`);
+ await new Promise(resolve => {
+ chan.asyncOpen(
+ new ChannelListener(
+ // eslint-disable-next-line no-shadow
+ (req, buff) => resolve({ req, buff }),
+ null,
+ CL_ALLOW_UNKNOWN_CL
+ )
+ );
+ });
+ await checkSessionCounter();
+
+ await proxy.stop();
+ }
+ }
+ );
+});
+
+add_task(async function test_cancel_during_response() {
+ let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(
+ Ci.nsIX509CertDB
+ );
+ addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u");
+ addCertFromFile(certdb, "proxy-ca.pem", "CTu,u,u");
+
+ await with_node_servers(
+ [NodeHTTPServer, NodeHTTPSServer, NodeHTTP2Server],
+ async server => {
+ let proxies = [
+ NodeHTTPProxyServer,
+ NodeHTTPSProxyServer,
+ NodeHTTP2ProxyServer,
+ ];
+ for (let p of proxies) {
+ let proxy = new p();
+ await proxy.start();
+ registerCleanupFunction(async () => {
+ await proxy.stop();
+ });
+
+ await proxy.execute(`
+ global.session_counter = 0;
+ global.proxy.on("session", () => {
+ global.session_counter++;
+ });
+ `);
+
+ info(`Testing ${p.name} with ${server.constructor.name}`);
+ await server.execute(
+ `global.server_name = "${server.constructor.name}";`
+ );
+
+ await server.registerPathHandler("/test", (req, resp) => {
+ resp.writeHead(200);
+ resp.write("a".repeat(1000));
+ // Here we send the response back in two chunks.
+ // The channel should be cancelled after the first one.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ setTimeout(() => {
+ resp.write("a".repeat(1000));
+ resp.end(global.server_name);
+ }, 2000);
+ });
+ await server.registerPathHandler("/instant", (req, resp) => {
+ resp.writeHead(200);
+ resp.end(global.server_name);
+ });
+
+ let chan = makeChan(`${server.origin()}/test`);
+
+ chan.notificationCallbacks = {
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIInterfaceRequestor",
+ "nsIProgressEventSink",
+ ]),
+
+ getInterface(iid) {
+ return this.QueryInterface(iid);
+ },
+
+ onProgress(request, progress, progressMax) {
+ info(`progress: ${progress}/${progressMax}`);
+ // Check that we never get more than 1000 bytes.
+ Assert.equal(progress, 1000);
+ },
+ onStatus(request, status, statusArg) {
+ if (status == NS_NET_STATUS_RECEIVING_FROM) {
+ info("cancelling when receiving request");
+ chan.cancel(Cr.NS_ERROR_ABORT);
+ }
+ },
+ };
+
+ let openPromise = new Promise(resolve => {
+ chan.asyncOpen(
+ new ChannelListener(
+ (req, buff) => resolve({ req, buff }),
+ null,
+ CL_EXPECT_FAILURE
+ )
+ );
+ });
+
+ let { req } = await openPromise;
+ Assert.equal(req.status, Cr.NS_ERROR_ABORT);
+
+ async function checkSessionCounter() {
+ if (p.name == "NodeHTTP2ProxyServer") {
+ Assert.equal(await proxy.execute(`global.session_counter`), 1);
+ }
+ }
+
+ await checkSessionCounter();
+
+ chan = makeChan(`${server.origin()}/instant`);
+ await new Promise(resolve => {
+ chan.asyncOpen(
+ new ChannelListener(
+ // eslint-disable-next-line no-shadow
+ (req, buff) => resolve({ req, buff }),
+ null,
+ CL_ALLOW_UNKNOWN_CL
+ )
+ );
+ });
+
+ await checkSessionCounter();
+
+ await proxy.stop();
+ }
+ }
+ );
+});
diff --git a/netwerk/test/unit/test_proxy_pac.js b/netwerk/test/unit/test_proxy_pac.js
new file mode 100644
index 0000000000..343ad771fb
--- /dev/null
+++ b/netwerk/test/unit/test_proxy_pac.js
@@ -0,0 +1,126 @@
+// These are globlas defined for proxy servers, in ProxyAutoConfig.cpp. See
+// PACGlobalFunctions
+/* globals dnsResolve, alert */
+
+"use strict";
+
+const { TestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TestUtils.sys.mjs"
+);
+const override = Cc["@mozilla.org/network/native-dns-override;1"].getService(
+ Ci.nsINativeDNSResolverOverride
+);
+
+class ConsoleListener {
+ messages = [];
+ observe(message) {
+ // Ignore unexpected messages.
+ if (!(message instanceof Ci.nsIConsoleMessage)) {
+ return;
+ }
+ if (!message.message.includes("PAC")) {
+ return;
+ }
+
+ this.messages.push(message.message);
+ }
+
+ register() {
+ Services.console.registerListener(this);
+ }
+
+ unregister() {
+ Services.console.unregisterListener(this);
+ }
+
+ clear() {
+ this.messages = [];
+ }
+}
+
+async function configurePac(fn) {
+ let pacURI = `data:application/x-ns-proxy-autoconfig;charset=utf-8,${encodeURIComponent(
+ fn.toString()
+ )}`;
+ Services.prefs.setIntPref("network.proxy.type", 2);
+ Services.prefs.setStringPref("network.proxy.autoconfig_url", pacURI);
+
+ // Do a request so the PAC gets loaded
+ let chan = NetUtil.newChannel({
+ uri: `http://localhost:1234/`,
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+ await new Promise(resolve =>
+ chan.asyncOpen(new ChannelListener(resolve, null, CL_EXPECT_FAILURE))
+ );
+
+ await TestUtils.waitForCondition(
+ () =>
+ !!consoleListener.messages.filter(
+ e => e.includes("PAC file installed from"),
+ 0
+ ).length,
+ "Wait for PAC file to be installed."
+ );
+ consoleListener.clear();
+}
+
+let consoleListener = null;
+function setup() {
+ Services.prefs.setIntPref("network.proxy.type", 2);
+ Services.prefs.setStringPref("network.proxy.autoconfig_url", "");
+ consoleListener = new ConsoleListener();
+ consoleListener.register();
+
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("network.proxy.type");
+ Services.prefs.clearUserPref("network.proxy.autoconfig_url");
+ if (consoleListener) {
+ consoleListener.unregister();
+ consoleListener = null;
+ }
+ });
+}
+setup();
+
+// This method checks that calling dnsResult(null) does not result in
+// resolving the DNS name "null"
+add_task(async function test_bug1724345() {
+ consoleListener.clear();
+ // isInNet is defined by ascii_pac_utils.js which is included for proxies.
+ /* globals isInNet */
+ let pac = function FindProxyForURL(url, host) {
+ alert(`PAC resolving: ${host}`);
+ let destIP = dnsResolve(host);
+ alert(`PAC result: ${destIP}`);
+ alert(
+ `PAC isInNet: ${host} ${destIP} ${isInNet(
+ destIP,
+ "127.0.0.0",
+ "255.0.0.0"
+ )}`
+ );
+ return "DIRECT";
+ };
+
+ await configurePac(pac);
+
+ override.clearOverrides();
+ override.addIPOverride("example.org", "N/A");
+ override.addIPOverride("null", "127.0.0.1");
+ Services.dns.clearCache(true);
+
+ let chan = NetUtil.newChannel({
+ uri: `http://example.org:1234/`,
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+ await new Promise(resolve =>
+ chan.asyncOpen(new ChannelListener(resolve, null, CL_EXPECT_FAILURE))
+ );
+ ok(
+ !!consoleListener.messages.filter(e =>
+ e.includes("PAC isInNet: example.org null false")
+ ).length,
+ `should have proper result ${consoleListener.messages}`
+ );
+});
diff --git a/netwerk/test/unit/test_proxyconnect.js b/netwerk/test/unit/test_proxyconnect.js
new file mode 100644
index 0000000000..7015b3bffb
--- /dev/null
+++ b/netwerk/test/unit/test_proxyconnect.js
@@ -0,0 +1,360 @@
+/* -*- 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/. */
+
+// test_connectonly tests happy path of proxy connect
+// 1. CONNECT to localhost:socketserver_port
+// 2. Write 200 Connection established
+// 3. Write data to the tunnel (and read server-side)
+// 4. Read data from the tunnel (and write server-side)
+// 5. done
+// test_connectonly_noproxy tests an http channel with only connect set but
+// no proxy configured.
+// 1. OnTransportAvailable callback NOT called (checked in step 2)
+// 2. StopRequest callback called
+// 3. done
+// test_connectonly_nonhttp tests an http channel with only connect set with a
+// non-http proxy.
+// 1. OnTransportAvailable callback NOT called (checked in step 2)
+// 2. StopRequest callback called
+// 3. done
+
+// -1 then initialized with an actual port from the serversocket
+"use strict";
+
+var socketserver_port = -1;
+
+const CC = Components.Constructor;
+const ServerSocket = CC(
+ "@mozilla.org/network/server-socket;1",
+ "nsIServerSocket",
+ "init"
+);
+const BinaryInputStream = CC(
+ "@mozilla.org/binaryinputstream;1",
+ "nsIBinaryInputStream",
+ "setInputStream"
+);
+const BinaryOutputStream = CC(
+ "@mozilla.org/binaryoutputstream;1",
+ "nsIBinaryOutputStream",
+ "setOutputStream"
+);
+
+const STATE_NONE = 0;
+const STATE_READ_CONNECT_REQUEST = 1;
+const STATE_WRITE_CONNECTION_ESTABLISHED = 2;
+const STATE_CHECK_WRITE = 3; // write to the tunnel
+const STATE_CHECK_WRITE_READ = 4; // wrote to the tunnel, check connection data
+const STATE_CHECK_READ = 5; // read from the tunnel
+const STATE_CHECK_READ_WROTE = 6; // wrote to connection, check tunnel data
+const STATE_COMPLETED = 100;
+
+const CONNECT_RESPONSE_STRING = "HTTP/1.1 200 Connection established\r\n\r\n";
+const CHECK_WRITE_STRING = "hello";
+const CHECK_READ_STRING = "world";
+const ALPN = "webrtc";
+
+var connectRequest = "";
+var checkWriteData = "";
+var checkReadData = "";
+
+var threadManager;
+var socket;
+var streamIn;
+var streamOut;
+var accepted = false;
+var acceptedSocket;
+var state = STATE_NONE;
+var transportAvailable = false;
+var proxiedChannel;
+var listener = {
+ expectedCode: -1, // uninitialized
+
+ onStartRequest: function test_onStartR(request) {},
+
+ onDataAvailable: function test_ODA() {
+ do_throw("Should not get any data!");
+ },
+
+ onStopRequest: function test_onStopR(request, status) {
+ if (state === STATE_COMPLETED) {
+ Assert.equal(transportAvailable, false, "transport available not called");
+ Assert.equal(status, 0x80004005, "error code matches");
+ Assert.equal(proxiedChannel.httpProxyConnectResponseCode, 200);
+ nextTest();
+ return;
+ }
+
+ Assert.equal(accepted, true, "socket accepted");
+ accepted = false;
+ },
+};
+
+var upgradeListener = {
+ onTransportAvailable: (transport, socketIn, socketOut) => {
+ if (!transport || !socketIn || !socketOut) {
+ do_throw("on transport available failed");
+ }
+
+ if (state !== STATE_CHECK_WRITE) {
+ do_throw("bad state");
+ }
+
+ transportAvailable = true;
+
+ socketIn.asyncWait(connectHandler, 0, 0, threadManager.mainThread);
+ socketOut.asyncWait(connectHandler, 0, 0, threadManager.mainThread);
+ },
+ QueryInterface: ChromeUtils.generateQI(["nsIHttpUpgradeListener"]),
+};
+
+var connectHandler = {
+ onInputStreamReady: input => {
+ try {
+ const bis = new BinaryInputStream(input);
+ var data = bis.readByteArray(input.available());
+
+ dataAvailable(data);
+
+ if (state !== STATE_COMPLETED) {
+ input.asyncWait(connectHandler, 0, 0, threadManager.mainThread);
+ }
+ } catch (e) {
+ do_throw(e);
+ }
+ },
+ onOutputStreamReady: output => {
+ writeData(output);
+ },
+ QueryInterface: iid => {
+ if (
+ iid.equals(Ci.nsISupports) ||
+ iid.equals(Ci.nsIInputStreamCallback) ||
+ iid.equals(Ci.nsIOutputStreamCallback)
+ ) {
+ return this;
+ }
+ throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE);
+ },
+};
+
+function dataAvailable(data) {
+ switch (state) {
+ case STATE_READ_CONNECT_REQUEST:
+ connectRequest += String.fromCharCode.apply(String, data);
+ const headerEnding = connectRequest.indexOf("\r\n\r\n");
+ const alpnHeaderIndex = connectRequest.indexOf(`ALPN: ${ALPN}`);
+
+ if (headerEnding != -1) {
+ const requestLine = `CONNECT localhost:${socketserver_port} HTTP/1.1`;
+ Assert.equal(connectRequest.indexOf(requestLine), 0, "connect request");
+ Assert.equal(headerEnding, connectRequest.length - 4, "req head only");
+ Assert.notEqual(alpnHeaderIndex, -1, "alpn header found");
+
+ state = STATE_WRITE_CONNECTION_ESTABLISHED;
+ streamOut.asyncWait(connectHandler, 0, 0, threadManager.mainThread);
+ }
+
+ break;
+ case STATE_CHECK_WRITE_READ:
+ checkWriteData += String.fromCharCode.apply(String, data);
+
+ if (checkWriteData.length >= CHECK_WRITE_STRING.length) {
+ Assert.equal(checkWriteData, CHECK_WRITE_STRING, "correct write data");
+
+ state = STATE_CHECK_READ;
+ streamOut.asyncWait(connectHandler, 0, 0, threadManager.mainThread);
+ }
+
+ break;
+ case STATE_CHECK_READ_WROTE:
+ checkReadData += String.fromCharCode.apply(String, data);
+
+ if (checkReadData.length >= CHECK_READ_STRING.length) {
+ Assert.equal(checkReadData, CHECK_READ_STRING, "correct read data");
+
+ state = STATE_COMPLETED;
+
+ streamIn.asyncWait(null, 0, 0, null);
+ acceptedSocket.close(0);
+
+ nextTest();
+ }
+
+ break;
+ default:
+ do_throw("bad state: " + state);
+ }
+}
+
+function writeData(output) {
+ let bos = new BinaryOutputStream(output);
+
+ switch (state) {
+ case STATE_WRITE_CONNECTION_ESTABLISHED:
+ bos.write(CONNECT_RESPONSE_STRING, CONNECT_RESPONSE_STRING.length);
+ state = STATE_CHECK_WRITE;
+ break;
+ case STATE_CHECK_READ:
+ bos.write(CHECK_READ_STRING, CHECK_READ_STRING.length);
+ state = STATE_CHECK_READ_WROTE;
+ break;
+ case STATE_CHECK_WRITE:
+ bos.write(CHECK_WRITE_STRING, CHECK_WRITE_STRING.length);
+ state = STATE_CHECK_WRITE_READ;
+ break;
+ default:
+ do_throw("bad state: " + state);
+ }
+}
+
+function makeChan(url) {
+ if (!url) {
+ url = "https://localhost:" + socketserver_port + "/";
+ }
+
+ var flags =
+ Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL |
+ Ci.nsILoadInfo.SEC_DONT_FOLLOW_REDIRECTS |
+ Ci.nsILoadInfo.SEC_COOKIES_OMIT;
+
+ var chan = NetUtil.newChannel({
+ uri: url,
+ loadUsingSystemPrincipal: true,
+ securityFlags: flags,
+ });
+ chan = chan.QueryInterface(Ci.nsIHttpChannel);
+
+ var internal = chan.QueryInterface(Ci.nsIHttpChannelInternal);
+ internal.HTTPUpgrade(ALPN, upgradeListener);
+ internal.setConnectOnly();
+
+ return chan;
+}
+
+function socketAccepted(socket1, transport) {
+ accepted = true;
+
+ // copied from httpd.js
+ const SEGMENT_SIZE = 8192;
+ const SEGMENT_COUNT = 1024;
+
+ switch (state) {
+ case STATE_NONE:
+ state = STATE_READ_CONNECT_REQUEST;
+ break;
+ default:
+ return;
+ }
+
+ acceptedSocket = transport;
+
+ try {
+ streamIn = transport
+ .openInputStream(0, SEGMENT_SIZE, SEGMENT_COUNT)
+ .QueryInterface(Ci.nsIAsyncInputStream);
+ streamOut = transport
+ .openOutputStream(0, 0, 0)
+ .QueryInterface(Ci.nsIAsyncOutputStream);
+
+ streamIn.asyncWait(connectHandler, 0, 0, threadManager.mainThread);
+ } catch (e) {
+ transport.close(Cr.NS_BINDING_ABORTED);
+ do_throw(e);
+ }
+}
+
+function stopListening() {
+ if (tests && tests.length !== 0 && do_throw) {
+ do_throw("should never stop");
+ }
+}
+
+function createProxy() {
+ try {
+ threadManager = Cc["@mozilla.org/thread-manager;1"].getService();
+
+ socket = new ServerSocket(-1, true, 1);
+ socketserver_port = socket.port;
+
+ socket.asyncListen({
+ onSocketAccepted: socketAccepted,
+ onStopListening: stopListening,
+ });
+ } catch (e) {
+ do_throw(e);
+ }
+}
+
+function test_connectonly() {
+ Services.prefs.setCharPref("network.proxy.ssl", "localhost");
+ Services.prefs.setIntPref("network.proxy.ssl_port", socketserver_port);
+ Services.prefs.setBoolPref("network.proxy.allow_hijacking_localhost", true);
+ Services.prefs.setIntPref("network.proxy.type", 1);
+
+ var chan = makeChan();
+ proxiedChannel = chan.QueryInterface(Ci.nsIProxiedChannel);
+ chan.asyncOpen(listener);
+
+ do_test_pending();
+}
+
+function test_connectonly_noproxy() {
+ clearPrefs();
+ var chan = makeChan();
+ chan.asyncOpen(listener);
+
+ do_test_pending();
+}
+
+function test_connectonly_nonhttp() {
+ clearPrefs();
+
+ Services.prefs.setCharPref("network.proxy.socks", "localhost");
+ Services.prefs.setIntPref("network.proxy.socks_port", socketserver_port);
+ Services.prefs.setBoolPref("network.proxy.allow_hijacking_localhost", true);
+ Services.prefs.setIntPref("network.proxy.type", 1);
+
+ var chan = makeChan();
+ chan.asyncOpen(listener);
+
+ do_test_pending();
+}
+
+function nextTest() {
+ transportAvailable = false;
+
+ if (!tests.length) {
+ do_test_finished();
+ return;
+ }
+
+ tests.shift()();
+ do_test_finished();
+}
+
+var tests = [
+ test_connectonly,
+ test_connectonly_noproxy,
+ test_connectonly_nonhttp,
+];
+
+function clearPrefs() {
+ Services.prefs.clearUserPref("network.proxy.ssl");
+ Services.prefs.clearUserPref("network.proxy.ssl_port");
+ Services.prefs.clearUserPref("network.proxy.socks");
+ Services.prefs.clearUserPref("network.proxy.socks_port");
+ Services.prefs.clearUserPref("network.proxy.allow_hijacking_localhost");
+ Services.prefs.clearUserPref("network.proxy.type");
+}
+
+function run_test() {
+ createProxy();
+
+ registerCleanupFunction(clearPrefs);
+
+ nextTest();
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_psl.js b/netwerk/test/unit/test_psl.js
new file mode 100644
index 0000000000..d9c0c2965d
--- /dev/null
+++ b/netwerk/test/unit/test_psl.js
@@ -0,0 +1,39 @@
+"use strict";
+
+var idna = Cc["@mozilla.org/network/idn-service;1"].getService(
+ Ci.nsIIDNService
+);
+
+function run_test() {
+ var file = do_get_file("data/test_psl.txt");
+ var uri = Services.io.newFileURI(file);
+ var srvScope = {};
+ Services.scriptloader.loadSubScript(uri.spec, srvScope);
+}
+
+// Exported to the loaded script
+/* exported checkPublicSuffix */
+function checkPublicSuffix(host, expectedSuffix) {
+ var actualSuffix = null;
+ try {
+ actualSuffix = Services.eTLD.getBaseDomainFromHost(host);
+ } catch (e) {
+ if (
+ e.result != Cr.NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS &&
+ e.result != Cr.NS_ERROR_ILLEGAL_VALUE
+ ) {
+ throw e;
+ }
+ }
+ // The EffectiveTLDService always gives back punycoded labels.
+ // The test suite wants to get back what it put in.
+ if (
+ actualSuffix !== null &&
+ expectedSuffix !== null &&
+ /(^|\.)xn--/.test(actualSuffix) &&
+ !/(^|\.)xn--/.test(expectedSuffix)
+ ) {
+ actualSuffix = idna.convertACEtoUTF8(actualSuffix);
+ }
+ Assert.equal(actualSuffix, expectedSuffix);
+}
diff --git a/netwerk/test/unit/test_race_cache_with_network.js b/netwerk/test/unit/test_race_cache_with_network.js
new file mode 100644
index 0000000000..3e34c6af46
--- /dev/null
+++ b/netwerk/test/unit/test_race_cache_with_network.js
@@ -0,0 +1,273 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+var httpserver = new HttpServer();
+httpserver.start(-1);
+const PORT = httpserver.identity.primaryPort;
+
+function make_channel(url) {
+ return NetUtil.newChannel({
+ uri: url,
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+}
+
+let gResponseBody = "blahblah";
+let g200Counter = 0;
+let g304Counter = 0;
+function test_handler(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");
+ g304Counter++;
+ } else {
+ response.setStatusLine(metadata.httpVersion, 200, "OK");
+ response.bodyOutputStream.write(gResponseBody, gResponseBody.length);
+ g200Counter++;
+ }
+}
+
+function cached_handler(metadata, response) {
+ response.setHeader("Content-Type", "text/plain");
+ response.setHeader("Cache-Control", "max-age=3600");
+ response.setHeader("ETag", "test-etag1");
+
+ response.setStatusLine(metadata.httpVersion, 200, "OK");
+ response.bodyOutputStream.write(gResponseBody, gResponseBody.length);
+
+ g200Counter++;
+}
+
+let gResponseCounter = 0;
+let gIsFromCache = 0;
+function checkContent(request, buffer, context, isFromCache) {
+ Assert.equal(buffer, gResponseBody);
+ info(
+ "isRacing: " +
+ request.QueryInterface(Ci.nsICacheInfoChannel).isRacing() +
+ "\n"
+ );
+ gResponseCounter++;
+ if (isFromCache) {
+ gIsFromCache++;
+ }
+ executeSoon(() => {
+ testGenerator.next();
+ });
+}
+
+function run_test() {
+ do_get_profile();
+ // In this test, we manually use |TriggerNetwork| to prove we could send
+ // net and cache reqeust simultaneously. Therefore we should disable
+ // racing in the HttpChannel first.
+ Services.prefs.setBoolPref("network.http.rcwn.enabled", false);
+ httpserver.registerPathHandler("/rcwn", test_handler);
+ httpserver.registerPathHandler("/rcwn_cached", cached_handler);
+ testGenerator.next();
+ do_test_pending();
+}
+
+let testGenerator = testSteps();
+function* testSteps() {
+ /*
+ * In this test, we have a relatively low timeout of 200ms and an assertion that
+ * the timer works properly by checking that the time was greater than 200ms.
+ * With a timer precision of 100ms (for example) we will clamp downwards to 200
+ * and cause the assertion to fail. To resolve this, we hardcode a precision of
+ * 20ms.
+ */
+ Services.prefs.setBoolPref("privacy.reduceTimerPrecision", true);
+ Services.prefs.setIntPref(
+ "privacy.resistFingerprinting.reduceTimerPrecision.microseconds",
+ 20000
+ );
+
+ registerCleanupFunction(function () {
+ Services.prefs.clearUserPref("privacy.reduceTimerPrecision");
+ Services.prefs.clearUserPref(
+ "privacy.resistFingerprinting.reduceTimerPrecision.microseconds"
+ );
+ });
+
+ // Initial request. Stores the response in the cache.
+ let channel = make_channel("http://localhost:" + PORT + "/rcwn");
+ channel.asyncOpen(new ChannelListener(checkContent, null));
+ yield undefined;
+ equal(gResponseCounter, 1);
+ equal(g200Counter, 1, "check number of 200 responses");
+ equal(g304Counter, 0, "check number of 304 responses");
+
+ // Checks that response is returned from the cache, after a 304 response.
+ channel = make_channel("http://localhost:" + PORT + "/rcwn");
+ channel.asyncOpen(new ChannelListener(checkContent, null));
+ yield undefined;
+ equal(gResponseCounter, 2);
+ equal(g200Counter, 1, "check number of 200 responses");
+ equal(g304Counter, 1, "check number of 304 responses");
+
+ // Checks that delaying the response from the cache works.
+ channel = make_channel("http://localhost:" + PORT + "/rcwn");
+ channel
+ .QueryInterface(Ci.nsIRaceCacheWithNetwork)
+ .test_delayCacheEntryOpeningBy(200);
+ let startTime = Date.now();
+ channel.asyncOpen(new ChannelListener(checkContent, null));
+ yield undefined;
+ greaterOrEqual(
+ Date.now() - startTime,
+ 200,
+ "Check that timer works properly"
+ );
+ equal(gResponseCounter, 3);
+ equal(g200Counter, 1, "check number of 200 responses");
+ equal(g304Counter, 2, "check number of 304 responses");
+
+ // Checks that we can trigger the cache open immediately, even if the cache delay is set very high.
+ channel = make_channel("http://localhost:" + PORT + "/rcwn");
+ channel
+ .QueryInterface(Ci.nsIRaceCacheWithNetwork)
+ .test_delayCacheEntryOpeningBy(100000);
+ channel.asyncOpen(new ChannelListener(checkContent, null));
+ do_timeout(50, function () {
+ channel
+ .QueryInterface(Ci.nsIRaceCacheWithNetwork)
+ .test_triggerDelayedOpenCacheEntry();
+ });
+ yield undefined;
+ equal(gResponseCounter, 4);
+ equal(g200Counter, 1, "check number of 200 responses");
+ equal(g304Counter, 3, "check number of 304 responses");
+
+ // Sets a high delay for the cache fetch, and triggers the network activity.
+ channel = make_channel("http://localhost:" + PORT + "/rcwn");
+ channel
+ .QueryInterface(Ci.nsIRaceCacheWithNetwork)
+ .test_delayCacheEntryOpeningBy(100000);
+ channel.QueryInterface(Ci.nsIRaceCacheWithNetwork).test_triggerNetwork(50);
+ channel.asyncOpen(new ChannelListener(checkContent, null));
+ // Trigger network after 50 ms.
+ yield undefined;
+ equal(gResponseCounter, 5);
+ equal(g200Counter, 2, "check number of 200 responses");
+ equal(g304Counter, 3, "check number of 304 responses");
+
+ // Sets a high delay for the cache fetch, and triggers the network activity.
+ // While the network response is produced, we trigger the cache fetch.
+ // Because the network response was the first, a non-conditional request is sent.
+ channel = make_channel("http://localhost:" + PORT + "/rcwn");
+ channel
+ .QueryInterface(Ci.nsIRaceCacheWithNetwork)
+ .test_delayCacheEntryOpeningBy(100000);
+ channel.QueryInterface(Ci.nsIRaceCacheWithNetwork).test_triggerNetwork(50);
+ channel.asyncOpen(new ChannelListener(checkContent, null));
+ yield undefined;
+ equal(gResponseCounter, 6);
+ equal(g200Counter, 3, "check number of 200 responses");
+ equal(g304Counter, 3, "check number of 304 responses");
+
+ // Triggers cache open before triggering network.
+ channel = make_channel("http://localhost:" + PORT + "/rcwn");
+ channel
+ .QueryInterface(Ci.nsIRaceCacheWithNetwork)
+ .test_delayCacheEntryOpeningBy(100000);
+ channel.QueryInterface(Ci.nsIRaceCacheWithNetwork).test_triggerNetwork(5000);
+ channel.asyncOpen(new ChannelListener(checkContent, null));
+ channel
+ .QueryInterface(Ci.nsIRaceCacheWithNetwork)
+ .test_triggerDelayedOpenCacheEntry();
+ yield undefined;
+ equal(gResponseCounter, 7);
+ equal(
+ g200Counter,
+ 3,
+ `check number of 200 responses | 200: ${g200Counter}, 304: ${g304Counter}`
+ );
+ equal(
+ g304Counter,
+ 4,
+ `check number of 304 responses | 200: ${g200Counter}, 304: ${g304Counter}`
+ );
+
+ // Load the cached handler so we don't need to revalidate
+ channel = make_channel("http://localhost:" + PORT + "/rcwn_cached");
+ channel.asyncOpen(new ChannelListener(checkContent, null));
+ yield undefined;
+ equal(gResponseCounter, 8);
+ equal(g200Counter, 4, "check number of 200 responses");
+ equal(g304Counter, 4, "check number of 304 responses");
+
+ // Make sure response is loaded from the cache, not the network
+ channel = make_channel("http://localhost:" + PORT + "/rcwn_cached");
+ channel.asyncOpen(new ChannelListener(checkContent, null));
+ yield undefined;
+ equal(gResponseCounter, 9);
+ equal(g200Counter, 4, "check number of 200 responses");
+ equal(g304Counter, 4, "check number of 304 responses");
+
+ // Cache times out, so we trigger the network
+ gIsFromCache = 0;
+ channel = make_channel("http://localhost:" + PORT + "/rcwn_cached");
+ channel
+ .QueryInterface(Ci.nsIRaceCacheWithNetwork)
+ .test_delayCacheEntryOpeningBy(100000);
+ // trigger network after 50 ms
+ channel.QueryInterface(Ci.nsIRaceCacheWithNetwork).test_triggerNetwork(50);
+ channel.asyncOpen(new ChannelListener(checkContent, null));
+ yield undefined;
+ equal(gResponseCounter, 10);
+ equal(gIsFromCache, 0, "should be from the network");
+ equal(g200Counter, 5, "check number of 200 responses");
+ equal(g304Counter, 4, "check number of 304 responses");
+
+ // Cache callback comes back right after network is triggered.
+ channel = make_channel("http://localhost:" + PORT + "/rcwn_cached");
+ channel
+ .QueryInterface(Ci.nsIRaceCacheWithNetwork)
+ .test_delayCacheEntryOpeningBy(55);
+ channel.QueryInterface(Ci.nsIRaceCacheWithNetwork).test_triggerNetwork(50);
+ channel.asyncOpen(new ChannelListener(checkContent, null));
+ yield undefined;
+ equal(gResponseCounter, 11);
+ info("IsFromCache: " + gIsFromCache + "\n");
+ info("Number of 200 responses: " + g200Counter + "\n");
+ equal(g304Counter, 4, "check number of 304 responses");
+
+ // Set an increasingly high timeout to trigger opening the cache entry
+ // This way we ensure that some of the entries we will get from the network,
+ // and some we will get from the cache.
+ gIsFromCache = 0;
+ for (var i = 0; i < 50; i++) {
+ channel = make_channel("http://localhost:" + PORT + "/rcwn_cached");
+ channel
+ .QueryInterface(Ci.nsIRaceCacheWithNetwork)
+ .test_delayCacheEntryOpeningBy(i * 100);
+ channel.QueryInterface(Ci.nsIRaceCacheWithNetwork).test_triggerNetwork(10);
+ channel.asyncOpen(new ChannelListener(checkContent, null));
+ // This may be racy. The delay was chosen because the distribution of net-cache
+ // results was around 25-25 on my machine.
+ yield undefined;
+ }
+
+ greater(gIsFromCache, 0, "Some of the responses should be from the cache");
+ less(gIsFromCache, 50, "Some of the responses should be from the net");
+
+ httpserver.stop(do_test_finished);
+}
diff --git a/netwerk/test/unit/test_range_requests.js b/netwerk/test/unit/test_range_requests.js
new file mode 100644
index 0000000000..cb7a1e7a72
--- /dev/null
+++ b/netwerk/test/unit/test_range_requests.js
@@ -0,0 +1,443 @@
+//
+// This test makes sure range-requests are sent and treated the way we want
+// See bug #612135 for a thorough discussion on the subject
+//
+// Necko does a range-request for a partial cache-entry iff
+//
+// 1) size of the cached entry < value of the cached Content-Length header
+// (not tested here - see bug #612135 comments 108-110)
+// 2) the size of the cached entry is > 0 (see bug #628607)
+// 3) the cached entry does not have a "no-store" Cache-Control header
+// 4) the cached entry does not have a Content-Encoding (see bug #613159)
+// 5) the request does not have a conditional-request header set by client
+// 6) nsHttpResponseHead::IsResumable() is true for the cached entry
+// 7) a basic positive test that makes sure byte ranges work
+// 8) ensure NS_ERROR_CORRUPTED_CONTENT is thrown when total entity size
+// of 206 does not match content-length of 200
+//
+// The test has one handler for each case and run_tests() fires one request
+// for each. None of the handlers should see a Range-header.
+
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+var httpserver = null;
+
+const clearTextBody = "This is a slightly longer test\n";
+const encodedBody = [
+ 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,
+];
+
+const partial_data_length = 4;
+var port = null; // set in run_test
+
+function make_channel(url, callback, ctx) {
+ return NetUtil.newChannel({
+ uri: url,
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+}
+
+// StreamListener which cancels its request on first data available
+function Canceler(continueFn) {
+ this.continueFn = continueFn;
+}
+Canceler.prototype = {
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIStreamListener",
+ "nsIRequestObserver",
+ ]),
+ onStartRequest(request) {},
+
+ onDataAvailable(request, stream, offset, count) {
+ // Read stream so we don't assert for not reading from the stream
+ // if cancelling the channel is slow.
+ read_stream(stream, count);
+
+ request.QueryInterface(Ci.nsIChannel).cancel(Cr.NS_BINDING_ABORTED);
+ },
+ onStopRequest(request, status) {
+ Assert.equal(status, Cr.NS_BINDING_ABORTED);
+ this.continueFn(request, null);
+ },
+};
+// Simple StreamListener which performs no validations
+function MyListener(continueFn) {
+ this.continueFn = continueFn;
+ this._buffer = null;
+}
+MyListener.prototype = {
+ 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) {
+ this.continueFn(request, this._buffer);
+ },
+};
+
+var case_8_range_request = false;
+function FailedChannelListener(continueFn) {
+ this.continueFn = continueFn;
+}
+FailedChannelListener.prototype = {
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIStreamListener",
+ "nsIRequestObserver",
+ ]),
+ onStartRequest(request) {},
+
+ onDataAvailable(request, stream, offset, count) {
+ read_stream(stream, count);
+ },
+
+ onStopRequest(request, status) {
+ if (case_8_range_request) {
+ Assert.equal(status, Cr.NS_ERROR_CORRUPTED_CONTENT);
+ }
+ this.continueFn(request, null);
+ },
+};
+
+function received_cleartext(request, data) {
+ Assert.equal(clearTextBody, data);
+ testFinished();
+}
+
+function setStdHeaders(response, length) {
+ response.setHeader("Content-Type", "text/plain", false);
+ response.setHeader("ETag", "Just testing");
+ response.setHeader("Cache-Control", "max-age: 360000");
+ response.setHeader("Accept-Ranges", "bytes");
+ response.setHeader("Content-Length", "" + length);
+}
+
+function handler_2(metadata, response) {
+ setStdHeaders(response, clearTextBody.length);
+ Assert.ok(!metadata.hasHeader("Range"));
+ response.bodyOutputStream.write(clearTextBody, clearTextBody.length);
+}
+function received_partial_2(request, data) {
+ Assert.equal(data, undefined);
+ var chan = make_channel("http://localhost:" + port + "/test_2");
+ chan.asyncOpen(new ChannelListener(received_cleartext, null));
+}
+
+var case_3_request_no = 0;
+function handler_3(metadata, response) {
+ var body = clearTextBody;
+ setStdHeaders(response, body.length);
+ response.setHeader("Cache-Control", "no-store", false);
+ switch (case_3_request_no) {
+ case 0:
+ Assert.ok(!metadata.hasHeader("Range"));
+ body = body.slice(0, partial_data_length);
+ response.processAsync();
+ response.bodyOutputStream.write(body, body.length);
+ response.finish();
+ break;
+ case 1:
+ Assert.ok(!metadata.hasHeader("Range"));
+ response.bodyOutputStream.write(body, body.length);
+ break;
+ default:
+ response.setStatusLine(metadata.httpVersion, 404, "Not Found");
+ }
+ case_3_request_no++;
+}
+function received_partial_3(request, data) {
+ Assert.equal(partial_data_length, data.length);
+ var chan = make_channel("http://localhost:" + port + "/test_3");
+ chan.asyncOpen(new ChannelListener(received_cleartext, null));
+}
+
+var case_4_request_no = 0;
+function handler_4(metadata, response) {
+ switch (case_4_request_no) {
+ case 0:
+ Assert.ok(!metadata.hasHeader("Range"));
+ var body = encodedBody;
+ setStdHeaders(response, body.length);
+ response.setHeader("Content-Encoding", "gzip", false);
+ body = body.slice(0, partial_data_length);
+ var bos = Cc["@mozilla.org/binaryoutputstream;1"].createInstance(
+ Ci.nsIBinaryOutputStream
+ );
+ bos.setOutputStream(response.bodyOutputStream);
+ response.processAsync();
+ bos.writeByteArray(body);
+ response.finish();
+ break;
+ case 1:
+ Assert.ok(!metadata.hasHeader("Range"));
+ setStdHeaders(response, clearTextBody.length);
+ response.bodyOutputStream.write(clearTextBody, clearTextBody.length);
+ break;
+ default:
+ response.setStatusLine(metadata.httpVersion, 404, "Not Found");
+ }
+ case_4_request_no++;
+}
+function received_partial_4(request, data) {
+ // checking length does not work with encoded data
+ // do_check_eq(partial_data_length, data.length);
+ var chan = make_channel("http://localhost:" + port + "/test_4");
+ chan.asyncOpen(new MyListener(received_cleartext));
+}
+
+var case_5_request_no = 0;
+function handler_5(metadata, response) {
+ var body = clearTextBody;
+ setStdHeaders(response, body.length);
+ switch (case_5_request_no) {
+ case 0:
+ Assert.ok(!metadata.hasHeader("Range"));
+ body = body.slice(0, partial_data_length);
+ response.processAsync();
+ response.bodyOutputStream.write(body, body.length);
+ response.finish();
+ break;
+ case 1:
+ Assert.ok(!metadata.hasHeader("Range"));
+ response.bodyOutputStream.write(body, body.length);
+ break;
+ default:
+ response.setStatusLine(metadata.httpVersion, 404, "Not Found");
+ }
+ case_5_request_no++;
+}
+function received_partial_5(request, data) {
+ Assert.equal(partial_data_length, data.length);
+ var chan = make_channel("http://localhost:" + port + "/test_5");
+ chan.setRequestHeader("If-Match", "Some eTag", false);
+ chan.asyncOpen(new ChannelListener(received_cleartext, null));
+}
+
+var case_6_request_no = 0;
+function handler_6(metadata, response) {
+ switch (case_6_request_no) {
+ case 0:
+ Assert.ok(!metadata.hasHeader("Range"));
+ var body = clearTextBody;
+ setStdHeaders(response, body.length);
+ response.setHeader("Accept-Ranges", "", false);
+ body = body.slice(0, partial_data_length);
+ response.processAsync();
+ response.bodyOutputStream.write(body, body.length);
+ response.finish();
+ break;
+ case 1:
+ Assert.ok(!metadata.hasHeader("Range"));
+ setStdHeaders(response, clearTextBody.length);
+ response.bodyOutputStream.write(clearTextBody, clearTextBody.length);
+ break;
+ default:
+ response.setStatusLine(metadata.httpVersion, 404, "Not Found");
+ }
+ case_6_request_no++;
+}
+function received_partial_6(request, data) {
+ // would like to verify that the response does not have Accept-Ranges
+ Assert.equal(partial_data_length, data.length);
+ var chan = make_channel("http://localhost:" + port + "/test_6");
+ chan.asyncOpen(new ChannelListener(received_cleartext, null));
+}
+
+const simpleBody = "0123456789";
+
+function received_simple(request, data) {
+ Assert.equal(simpleBody, data);
+ testFinished();
+}
+
+var case_7_request_no = 0;
+function handler_7(metadata, response) {
+ switch (case_7_request_no) {
+ case 0:
+ Assert.ok(!metadata.hasHeader("Range"));
+ response.setHeader("Content-Type", "text/plain", false);
+ response.setHeader("ETag", "test7Etag");
+ response.setHeader("Accept-Ranges", "bytes");
+ response.setHeader("Cache-Control", "max-age=360000");
+ response.setHeader("Content-Length", "10");
+ response.processAsync();
+ response.bodyOutputStream.write(simpleBody.slice(0, 4), 4);
+ response.finish();
+ break;
+ case 1:
+ response.setHeader("Content-Type", "text/plain", false);
+ response.setHeader("ETag", "test7Etag");
+ if (metadata.hasHeader("Range")) {
+ Assert.ok(metadata.hasHeader("If-Range"));
+ response.setStatusLine(metadata.httpVersion, 206, "Partial Content");
+ response.setHeader("Content-Range", "4-9/10");
+ response.setHeader("Content-Length", "6");
+ response.bodyOutputStream.write(simpleBody.slice(4), 6);
+ } else {
+ response.setHeader("Content-Length", "10");
+ response.bodyOutputStream.write(simpleBody, 10);
+ }
+ break;
+ default:
+ response.setStatusLine(metadata.httpVersion, 404, "Not Found");
+ }
+ case_7_request_no++;
+}
+function received_partial_7(request, data) {
+ // make sure we get the first 4 bytes
+ Assert.equal(4, data.length);
+ // do it again to get the rest
+ var chan = make_channel("http://localhost:" + port + "/test_7");
+ chan.asyncOpen(new ChannelListener(received_simple, null));
+}
+
+var case_8_request_no = 0;
+function handler_8(metadata, response) {
+ switch (case_8_request_no) {
+ case 0:
+ Assert.ok(!metadata.hasHeader("Range"));
+ response.setHeader("Content-Type", "text/plain", false);
+ response.setHeader("ETag", "test8Etag");
+ response.setHeader("Accept-Ranges", "bytes");
+ response.setHeader("Cache-Control", "max-age=360000");
+ response.setHeader("Content-Length", "10");
+ response.processAsync();
+ response.bodyOutputStream.write(simpleBody.slice(0, 4), 4);
+ response.finish();
+ break;
+ case 1:
+ if (metadata.hasHeader("Range")) {
+ Assert.ok(metadata.hasHeader("If-Range"));
+ case_8_range_request = true;
+ }
+ response.setStatusLine(metadata.httpVersion, 206, "Partial Content");
+ response.setHeader("Content-Type", "text/plain", false);
+ response.setHeader("ETag", "test8Etag");
+ response.setHeader("Content-Range", "4-8/9"); // intentionally broken
+ response.setHeader("Content-Length", "5");
+ response.bodyOutputStream.write(simpleBody.slice(4), 5);
+ break;
+ default:
+ response.setStatusLine(metadata.httpVersion, 404, "Not Found");
+ }
+ case_8_request_no++;
+}
+function received_partial_8(request, data) {
+ // make sure we get the first 4 bytes
+ Assert.equal(4, data.length);
+ // do it again to get the rest
+ var chan = make_channel("http://localhost:" + port + "/test_8");
+ chan.asyncOpen(
+ new FailedChannelListener(testFinished, null, CL_EXPECT_LATE_FAILURE)
+ );
+}
+
+var case_9_request_no = 0;
+function handler_9(metadata, response) {
+ switch (case_9_request_no) {
+ case 0:
+ Assert.ok(!metadata.hasHeader("Range"));
+ response.setHeader("Content-Type", "text/plain", false);
+ response.setHeader("ETag", "W/test9WeakEtag");
+ response.setHeader("Accept-Ranges", "bytes");
+ response.setHeader("Cache-Control", "max-age=360000");
+ response.setHeader("Content-Length", "10");
+ response.processAsync();
+ response.bodyOutputStream.write(simpleBody.slice(0, 4), 4);
+ response.finish(); // truncated response
+ break;
+ case 1:
+ Assert.ok(!metadata.hasHeader("Range"));
+ response.setHeader("Content-Type", "text/plain", false);
+ response.setHeader("ETag", "W/test9WeakEtag");
+ response.setHeader("Accept-Ranges", "bytes");
+ response.setHeader("Cache-Control", "max-age=360000");
+ response.setHeader("Content-Length", "10");
+ response.processAsync();
+ response.bodyOutputStream.write(simpleBody, 10);
+ response.finish(); // full response
+ break;
+ default:
+ response.setStatusLine(metadata.httpVersion, 404, "Not Found");
+ }
+ case_9_request_no++;
+}
+function received_partial_9(request, data) {
+ Assert.equal(partial_data_length, data.length);
+ var chan = make_channel("http://localhost:" + port + "/test_9");
+ chan.asyncOpen(new ChannelListener(received_simple, null));
+}
+
+// Simple mechanism to keep track of tests and stop the server
+var numTestsFinished = 0;
+function testFinished() {
+ if (++numTestsFinished == 7) {
+ httpserver.stop(do_test_finished);
+ }
+}
+
+function run_test() {
+ httpserver = new HttpServer();
+ httpserver.registerPathHandler("/test_2", handler_2);
+ httpserver.registerPathHandler("/test_3", handler_3);
+ httpserver.registerPathHandler("/test_4", handler_4);
+ httpserver.registerPathHandler("/test_5", handler_5);
+ httpserver.registerPathHandler("/test_6", handler_6);
+ httpserver.registerPathHandler("/test_7", handler_7);
+ httpserver.registerPathHandler("/test_8", handler_8);
+ httpserver.registerPathHandler("/test_9", handler_9);
+ httpserver.start(-1);
+
+ port = httpserver.identity.primaryPort;
+
+ // wipe out cached content
+ evict_cache_entries();
+
+ // Case 2: zero-length partial entry must not trigger range-request
+ let chan = make_channel("http://localhost:" + port + "/test_2");
+ chan.asyncOpen(new Canceler(received_partial_2));
+
+ // Case 3: no-store response must not trigger range-request
+ chan = make_channel("http://localhost:" + port + "/test_3");
+ chan.asyncOpen(new MyListener(received_partial_3));
+
+ // Case 4: response with content-encoding must not trigger range-request
+ chan = make_channel("http://localhost:" + port + "/test_4");
+ chan.asyncOpen(new MyListener(received_partial_4));
+
+ // Case 5: conditional request-header set by client
+ chan = make_channel("http://localhost:" + port + "/test_5");
+ chan.asyncOpen(new MyListener(received_partial_5));
+
+ // Case 6: response is not resumable (drop the Accept-Ranges header)
+ chan = make_channel("http://localhost:" + port + "/test_6");
+ chan.asyncOpen(new MyListener(received_partial_6));
+
+ // Case 7: a basic positive test
+ chan = make_channel("http://localhost:" + port + "/test_7");
+ chan.asyncOpen(new MyListener(received_partial_7));
+
+ // Case 8: check that mismatched 206 and 200 sizes throw error
+ chan = make_channel("http://localhost:" + port + "/test_8");
+ chan.asyncOpen(new MyListener(received_partial_8));
+
+ // Case 9: check that weak etag is not used for a range request
+ chan = make_channel("http://localhost:" + port + "/test_9");
+ chan.asyncOpen(new MyListener(received_partial_9));
+
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_rcwn_always_cache_new_content.js b/netwerk/test/unit/test_rcwn_always_cache_new_content.js
new file mode 100644
index 0000000000..160ab631ca
--- /dev/null
+++ b/netwerk/test/unit/test_rcwn_always_cache_new_content.js
@@ -0,0 +1,116 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+var httpserver = new HttpServer();
+httpserver.start(-1);
+const PORT = httpserver.identity.primaryPort;
+
+function make_channel(url) {
+ return NetUtil.newChannel({
+ uri: url,
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+}
+
+let gFirstResponseBody = "first version";
+let gSecondResponseBody = "second version";
+let gRequestCounter = 0;
+
+function test_handler(metadata, response) {
+ response.setHeader("Content-Type", "text/plain");
+ response.setHeader("Cache-Control", "max-age=3600");
+ response.setHeader("ETag", "test-etag1");
+
+ switch (gRequestCounter) {
+ case 0:
+ response.bodyOutputStream.write(
+ gFirstResponseBody,
+ gFirstResponseBody.length
+ );
+ break;
+ case 1:
+ response.bodyOutputStream.write(
+ gSecondResponseBody,
+ gSecondResponseBody.length
+ );
+ break;
+ default:
+ do_throw("Unexpected request");
+ }
+ response.setStatusLine(metadata.httpVersion, 200, "OK");
+}
+
+function checkContent(request, buffer, context, isFromCache) {
+ let isRacing = request.QueryInterface(Ci.nsICacheInfoChannel).isRacing();
+ switch (gRequestCounter) {
+ case 0:
+ Assert.equal(buffer, gFirstResponseBody);
+ Assert.ok(!isFromCache);
+ Assert.ok(!isRacing);
+ break;
+ case 1:
+ Assert.equal(buffer, gSecondResponseBody);
+ Assert.ok(!isFromCache);
+ Assert.ok(isRacing);
+ break;
+ case 2:
+ Assert.equal(buffer, gSecondResponseBody);
+ Assert.ok(isFromCache);
+ Assert.ok(!isRacing);
+ break;
+ default:
+ do_throw("Unexpected request");
+ }
+
+ gRequestCounter++;
+ executeSoon(() => {
+ testGenerator.next();
+ });
+}
+
+function run_test() {
+ do_get_profile();
+ // In this test, we race the requests manually using |TriggerNetwork|,
+ // therefore we should disable racing in the HttpChannel first.
+ Services.prefs.setBoolPref("network.http.rcwn.enabled", false);
+ httpserver.registerPathHandler("/rcwn", test_handler);
+ testGenerator.next();
+ do_test_pending();
+}
+
+let testGenerator = testSteps();
+function* testSteps() {
+ // Store first version of the content in the cache.
+ let channel = make_channel("http://localhost:" + PORT + "/rcwn");
+ channel.asyncOpen(new ChannelListener(checkContent, null));
+ yield undefined;
+ equal(gRequestCounter, 1);
+
+ // Simulate the network victory by setting high delay for the cache fetch and
+ // triggering the network.
+ channel = make_channel("http://localhost:" + PORT + "/rcwn");
+ channel
+ .QueryInterface(Ci.nsIRaceCacheWithNetwork)
+ .test_delayCacheEntryOpeningBy(100000);
+ // Trigger network after 50 ms.
+ channel.QueryInterface(Ci.nsIRaceCacheWithNetwork).test_triggerNetwork(50);
+ channel.asyncOpen(new ChannelListener(checkContent, null));
+ yield undefined;
+ equal(gRequestCounter, 2);
+
+ // Simulate navigation back by specifying VALIDATE_NEVER flag.
+ channel = make_channel("http://localhost:" + PORT + "/rcwn");
+ channel.loadFlags = Ci.nsIRequest.VALIDATE_NEVER;
+ channel.asyncOpen(new ChannelListener(checkContent, null));
+ yield undefined;
+ equal(gRequestCounter, 3);
+
+ httpserver.stop(do_test_finished);
+}
diff --git a/netwerk/test/unit/test_rcwn_interrupted.js b/netwerk/test/unit/test_rcwn_interrupted.js
new file mode 100644
index 0000000000..5f3d059999
--- /dev/null
+++ b/netwerk/test/unit/test_rcwn_interrupted.js
@@ -0,0 +1,108 @@
+/*
+
+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.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+var httpProtocolHandler = Cc[
+ "@mozilla.org/network/protocol;1?name=http"
+].getService(Ci.nsIHttpProtocolHandler);
+
+ChromeUtils.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.processAsync();
+ do_timeout(500, () => {
+ 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);
+
+ response.finish();
+ });
+}
+
+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, CL_IGNORE_DELAYS)
+ );
+ var chan2 = make_channel(URL + "/content");
+ chan2
+ .QueryInterface(Ci.nsIRaceCacheWithNetwork)
+ .test_delayCacheEntryOpeningBy(200);
+ chan2.QueryInterface(Ci.nsIRaceCacheWithNetwork).test_triggerNetwork(50);
+ chan2.asyncOpen(
+ new ChannelListener(secondTimeThrough, null, CL_IGNORE_DELAYS)
+ );
+ });
+
+ 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_readline.js b/netwerk/test/unit/test_readline.js
new file mode 100644
index 0000000000..ac0c915406
--- /dev/null
+++ b/netwerk/test/unit/test_readline.js
@@ -0,0 +1,104 @@
+/* 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 PR_RDONLY = 0x1;
+
+function new_file_input_stream(file) {
+ var stream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(
+ Ci.nsIFileInputStream
+ );
+ stream.init(file, PR_RDONLY, 0, 0);
+ return stream;
+}
+
+function new_line_input_stream(filename) {
+ return new_file_input_stream(do_get_file(filename)).QueryInterface(
+ Ci.nsILineInputStream
+ );
+}
+
+var test_array = [
+ { file: "data/test_readline1.txt", lines: [] },
+ { file: "data/test_readline2.txt", lines: [""] },
+ { file: "data/test_readline3.txt", lines: ["", "", "", "", ""] },
+ { file: "data/test_readline4.txt", lines: ["1", "23", "456", "", "78901"] },
+ {
+ file: "data/test_readline5.txt",
+ lines: [
+ "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxE",
+ ],
+ },
+ {
+ file: "data/test_readline6.txt",
+ lines: [
+ "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxE",
+ ],
+ },
+ {
+ file: "data/test_readline7.txt",
+ lines: [
+ "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxE",
+ "",
+ ],
+ },
+ {
+ file: "data/test_readline8.txt",
+ lines: [
+ "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzSxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxE",
+ ],
+ },
+];
+
+function err(file, lineNo, msg) {
+ do_throw('"' + file + '" line ' + lineNo + ", " + msg);
+}
+
+function run_test() {
+ for (var test of test_array) {
+ var lineStream = new_line_input_stream(test.file);
+ var lineNo = 0;
+ var more = false;
+ var line = {};
+ more = lineStream.readLine(line);
+ for (var check of test.lines) {
+ ++lineNo;
+ if (lineNo == test.lines.length) {
+ if (more) {
+ err(
+ test.file,
+ lineNo,
+ "There should be no more data after the last line"
+ );
+ }
+ } else if (!more) {
+ err(test.file, lineNo, "There should be more data after this line");
+ }
+ if (line.value != check) {
+ err(
+ test.file,
+ lineNo,
+ "Wrong value, got '" + line.value + "' expected '" + check + "'"
+ );
+ }
+ dump(
+ 'ok "' +
+ test.file +
+ '" line ' +
+ lineNo +
+ " (length " +
+ line.value.length +
+ "): '" +
+ line.value +
+ "'\n"
+ );
+ more = lineStream.readLine(line);
+ }
+ if (more) {
+ err(test.file, lineNo, "'more' should be false after reading all lines");
+ }
+ dump('ok "' + test.file + '" succeeded\n');
+ lineStream.close();
+ }
+}
diff --git a/netwerk/test/unit/test_redirect-caching_canceled.js b/netwerk/test/unit/test_redirect-caching_canceled.js
new file mode 100644
index 0000000000..0ecf2f74b7
--- /dev/null
+++ b/netwerk/test/unit/test_redirect-caching_canceled.js
@@ -0,0 +1,64 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+ChromeUtils.defineLazyGetter(this, "URL", function () {
+ return "http://localhost:" + httpServer.identity.primaryPort;
+});
+
+var httpServer = null;
+// Need to randomize, because apparently no one clears our cache
+var randomPath = "/redirect/" + Math.random();
+
+ChromeUtils.defineLazyGetter(this, "randomURI", function () {
+ return URL + randomPath;
+});
+
+function make_channel(url, callback, ctx) {
+ return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true });
+}
+
+const responseBody = "response body";
+
+function redirectHandler(metadata, response) {
+ response.setStatusLine(metadata.httpVersion, 301, "Moved");
+ response.setHeader("Location", URL + "/content", false);
+ response.setHeader("Cache-control", "max-age=1000", false);
+}
+
+function contentHandler(metadata, response) {
+ response.setHeader("Content-Type", "text/plain");
+ response.bodyOutputStream.write(responseBody, responseBody.length);
+}
+
+function firstTimeThrough(request, buffer) {
+ Assert.equal(buffer, responseBody);
+ var chan = make_channel(randomURI);
+ chan.asyncOpen(new ChannelListener(secondTimeThrough, null));
+}
+
+function secondTimeThrough(request, buffer) {
+ Assert.equal(buffer, responseBody);
+ var chan = make_channel(randomURI);
+ chan.loadFlags |= Ci.nsIRequest.LOAD_FROM_CACHE;
+ chan.notificationCallbacks = new ChannelEventSink(ES_ABORT_REDIRECT);
+ chan.asyncOpen(new ChannelListener(finish_test, null, CL_EXPECT_FAILURE));
+}
+
+function finish_test(request, buffer) {
+ Assert.equal(buffer, "");
+ httpServer.stop(do_test_finished);
+}
+
+function run_test() {
+ httpServer = new HttpServer();
+ httpServer.registerPathHandler(randomPath, redirectHandler);
+ httpServer.registerPathHandler("/content", contentHandler);
+ httpServer.start(-1);
+
+ var chan = make_channel(randomURI);
+ chan.asyncOpen(new ChannelListener(firstTimeThrough, null));
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_redirect-caching_failure.js b/netwerk/test/unit/test_redirect-caching_failure.js
new file mode 100644
index 0000000000..2f8105dd94
--- /dev/null
+++ b/netwerk/test/unit/test_redirect-caching_failure.js
@@ -0,0 +1,81 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+/*
+ * The test is checking async redirect code path that is loading a cached
+ * redirect. But creation of the target channel fails before we even try
+ * to do async open on it. We force the creation error by forbidding
+ * the port number the URI contains. It must be done only after we have
+ * attempted to do the redirect (open the target URL) otherwise it's not
+ * cached.
+ */
+
+function inChildProcess() {
+ return Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT;
+}
+
+ChromeUtils.defineLazyGetter(this, "URL", function () {
+ return "http://localhost:" + httpServer.identity.primaryPort;
+});
+
+var httpServer = null;
+// Need to randomize, because apparently no one clears our cache
+var randomPath = "/redirect/" + Math.random();
+
+ChromeUtils.defineLazyGetter(this, "randomURI", function () {
+ return URL + randomPath;
+});
+
+function make_channel(url, callback, ctx) {
+ return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true });
+}
+
+var serverRequestCount = 0;
+
+function redirectHandler(metadata, response) {
+ ++serverRequestCount;
+ response.setStatusLine(metadata.httpVersion, 301, "Moved");
+ response.setHeader("Location", "http://non-existent.tld:65400", false);
+ response.setHeader("Cache-control", "max-age=1000", false);
+}
+
+function firstTimeThrough(request) {
+ Assert.equal(request.status, Cr.NS_ERROR_UNKNOWN_HOST);
+ Assert.equal(serverRequestCount, 1);
+
+ const nextHop = () => {
+ var chan = make_channel(randomURI);
+ chan.loadFlags |= Ci.nsIRequest.LOAD_FROM_CACHE;
+ chan.asyncOpen(new ChannelListener(finish_test, null, CL_EXPECT_FAILURE));
+ };
+
+ if (inChildProcess()) {
+ do_send_remote_message("disable-ports");
+ do_await_remote_message("disable-ports-done").then(nextHop);
+ } else {
+ Services.prefs.setCharPref("network.security.ports.banned", "65400");
+ nextHop();
+ }
+}
+
+function finish_test(request, buffer) {
+ Assert.equal(request.status, Cr.NS_ERROR_PORT_ACCESS_NOT_ALLOWED);
+ Assert.equal(serverRequestCount, 1);
+ Assert.equal(buffer, "");
+
+ httpServer.stop(do_test_finished);
+}
+
+function run_test() {
+ httpServer = new HttpServer();
+ httpServer.registerPathHandler(randomPath, redirectHandler);
+ httpServer.start(-1);
+
+ var chan = make_channel(randomURI);
+ chan.asyncOpen(
+ new ChannelListener(firstTimeThrough, null, CL_EXPECT_FAILURE)
+ );
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_redirect-caching_passing.js b/netwerk/test/unit/test_redirect-caching_passing.js
new file mode 100644
index 0000000000..0e6616f493
--- /dev/null
+++ b/netwerk/test/unit/test_redirect-caching_passing.js
@@ -0,0 +1,56 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+ChromeUtils.defineLazyGetter(this, "URL", function () {
+ return "http://localhost:" + httpserver.identity.primaryPort;
+});
+
+var httpserver = null;
+// Need to randomize, because apparently no one clears our cache
+var randomPath = "/redirect/" + Math.random();
+
+ChromeUtils.defineLazyGetter(this, "randomURI", function () {
+ return URL + randomPath;
+});
+
+function make_channel(url, callback, ctx) {
+ return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true });
+}
+
+const responseBody = "response body";
+
+function redirectHandler(metadata, response) {
+ response.setStatusLine(metadata.httpVersion, 301, "Moved");
+ response.setHeader("Location", URL + "/content", false);
+}
+
+function contentHandler(metadata, response) {
+ response.setHeader("Content-Type", "text/plain");
+ response.bodyOutputStream.write(responseBody, responseBody.length);
+}
+
+function firstTimeThrough(request, buffer) {
+ Assert.equal(buffer, responseBody);
+ var chan = make_channel(randomURI);
+ chan.loadFlags |= Ci.nsIRequest.LOAD_FROM_CACHE;
+ chan.asyncOpen(new ChannelListener(finish_test, null));
+}
+
+function finish_test(request, buffer) {
+ Assert.equal(buffer, responseBody);
+ httpserver.stop(do_test_finished);
+}
+
+function run_test() {
+ httpserver = new HttpServer();
+ httpserver.registerPathHandler(randomPath, redirectHandler);
+ httpserver.registerPathHandler("/content", contentHandler);
+ httpserver.start(-1);
+
+ var chan = make_channel(randomURI);
+ chan.asyncOpen(new ChannelListener(firstTimeThrough, null));
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_redirect_baduri.js b/netwerk/test/unit/test_redirect_baduri.js
new file mode 100644
index 0000000000..f83c9a264f
--- /dev/null
+++ b/netwerk/test/unit/test_redirect_baduri.js
@@ -0,0 +1,44 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+/*
+ * Test whether we fail bad URIs in HTTP redirect as CORRUPTED_CONTENT.
+ */
+
+var httpServer = null;
+
+var BadRedirectPath = "/BadRedirect";
+ChromeUtils.defineLazyGetter(this, "BadRedirectURI", function () {
+ return (
+ "http://localhost:" + httpServer.identity.primaryPort + BadRedirectPath
+ );
+});
+
+function make_channel(url, callback, ctx) {
+ return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true });
+}
+
+function BadRedirectHandler(metadata, response) {
+ response.setStatusLine(metadata.httpVersion, 301, "Moved");
+ // '>' in URI will fail to parse: we should not render response
+ response.setHeader("Location", "http://localhost:4444>BadRedirect", false);
+}
+
+function checkFailed(request, buffer) {
+ Assert.equal(request.status, Cr.NS_ERROR_CORRUPTED_CONTENT);
+
+ httpServer.stop(do_test_finished);
+}
+
+function run_test() {
+ httpServer = new HttpServer();
+ httpServer.registerPathHandler(BadRedirectPath, BadRedirectHandler);
+ httpServer.start(-1);
+
+ var chan = make_channel(BadRedirectURI);
+ chan.asyncOpen(new ChannelListener(checkFailed, null, CL_EXPECT_FAILURE));
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_redirect_canceled.js b/netwerk/test/unit/test_redirect_canceled.js
new file mode 100644
index 0000000000..9eb9d8b33a
--- /dev/null
+++ b/netwerk/test/unit/test_redirect_canceled.js
@@ -0,0 +1,50 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+ChromeUtils.defineLazyGetter(this, "URL", function () {
+ return "http://localhost:" + httpServer.identity.primaryPort;
+});
+
+var httpServer = null;
+// Need to randomize, because apparently no one clears our cache
+var randomPath = "/redirect/" + Math.random();
+
+ChromeUtils.defineLazyGetter(this, "randomURI", function () {
+ return URL + randomPath;
+});
+
+function make_channel(url, callback, ctx) {
+ return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true });
+}
+
+const responseBody = "response body";
+
+function redirectHandler(metadata, response) {
+ response.setStatusLine(metadata.httpVersion, 301, "Moved");
+ response.setHeader("Location", URL + "/content", false);
+}
+
+function contentHandler(metadata, response) {
+ response.setHeader("Content-Type", "text/plain");
+ response.bodyOutputStream.write(responseBody, responseBody.length);
+}
+
+function finish_test(request, buffer) {
+ Assert.equal(buffer, "");
+ httpServer.stop(do_test_finished);
+}
+
+function run_test() {
+ httpServer = new HttpServer();
+ httpServer.registerPathHandler(randomPath, redirectHandler);
+ httpServer.registerPathHandler("/content", contentHandler);
+ httpServer.start(-1);
+
+ var chan = make_channel(randomURI);
+ chan.notificationCallbacks = new ChannelEventSink(ES_ABORT_REDIRECT);
+ chan.asyncOpen(new ChannelListener(finish_test, null));
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_redirect_different-protocol.js b/netwerk/test/unit/test_redirect_different-protocol.js
new file mode 100644
index 0000000000..78b305c84e
--- /dev/null
+++ b/netwerk/test/unit/test_redirect_different-protocol.js
@@ -0,0 +1,49 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+ChromeUtils.defineLazyGetter(this, "URL", function () {
+ return "http://localhost:" + httpServer.identity.primaryPort;
+});
+
+var httpServer = null;
+// Need to randomize, because apparently no one clears our cache
+var randomPath = "/redirect/" + Math.random();
+
+ChromeUtils.defineLazyGetter(this, "randomURI", function () {
+ return URL + randomPath;
+});
+
+function make_channel(url, callback, ctx) {
+ return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true });
+}
+
+const redirectTargetBody = "response body";
+const response301Body = "redirect body";
+
+function redirectHandler(metadata, response) {
+ response.setStatusLine(metadata.httpVersion, 301, "Moved");
+ response.bodyOutputStream.write(response301Body, response301Body.length);
+ response.setHeader(
+ "Location",
+ "data:text/plain," + redirectTargetBody,
+ false
+ );
+}
+
+function finish_test(request, buffer) {
+ Assert.equal(buffer, redirectTargetBody);
+ httpServer.stop(do_test_finished);
+}
+
+function run_test() {
+ httpServer = new HttpServer();
+ httpServer.registerPathHandler(randomPath, redirectHandler);
+ httpServer.start(-1);
+
+ var chan = make_channel(randomURI);
+ chan.asyncOpen(new ChannelListener(finish_test, null, 0));
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_redirect_failure.js b/netwerk/test/unit/test_redirect_failure.js
new file mode 100644
index 0000000000..28b4668580
--- /dev/null
+++ b/netwerk/test/unit/test_redirect_failure.js
@@ -0,0 +1,59 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+/*
+ * The test is checking async redirect code path that is loading a
+ * redirect. But creation of the target channel fails before we even try
+ * to do async open on it. We force the creation error by forbidding
+ * the port number the URI contains.
+ */
+
+function inChildProcess() {
+ return Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT;
+}
+
+ChromeUtils.defineLazyGetter(this, "URL", function () {
+ return "http://localhost:" + httpServer.identity.primaryPort;
+});
+
+var httpServer = null;
+// Need to randomize, because apparently no one clears our cache
+var randomPath = "/redirect/" + Math.random();
+
+ChromeUtils.defineLazyGetter(this, "randomURI", function () {
+ return URL + randomPath;
+});
+
+function make_channel(url, callback, ctx) {
+ return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true });
+}
+
+function redirectHandler(metadata, response) {
+ response.setStatusLine(metadata.httpVersion, 301, "Moved");
+ response.setHeader("Location", "http://non-existent.tld:65400", false);
+ response.setHeader("Cache-Control", "no-cache", false);
+}
+
+function finish_test(request, buffer) {
+ Assert.equal(request.status, Cr.NS_ERROR_PORT_ACCESS_NOT_ALLOWED);
+
+ Assert.equal(buffer, "");
+ httpServer.stop(do_test_finished);
+}
+
+function run_test() {
+ httpServer = new HttpServer();
+ httpServer.registerPathHandler(randomPath, redirectHandler);
+ httpServer.start(-1);
+
+ if (!inChildProcess()) {
+ Services.prefs.setCharPref("network.security.ports.banned", "65400");
+ }
+
+ var chan = make_channel(randomURI);
+ chan.asyncOpen(new ChannelListener(finish_test, null, CL_EXPECT_FAILURE));
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_redirect_from_script.js b/netwerk/test/unit/test_redirect_from_script.js
new file mode 100644
index 0000000000..0c42d66b64
--- /dev/null
+++ b/netwerk/test/unit/test_redirect_from_script.js
@@ -0,0 +1,247 @@
+/*
+ * Test whether the rewrite-requests-from-script API implemented here:
+ * https://bugzilla.mozilla.org/show_bug.cgi?id=765934 is functioning
+ * correctly
+ *
+ * The test has the following components:
+ *
+ * testViaXHR() checks that internal redirects occur correctly for requests
+ * made with XMLHttpRequest objects.
+ *
+ * testViaAsyncOpen() checks that internal redirects occur correctly when made
+ * with nsIHTTPChannel.asyncOpen().
+ *
+ * Both of the above functions tests four requests:
+ *
+ * Test 1: a simple case that redirects within a server;
+ * Test 2: a second that redirects to a second webserver;
+ * Test 3: internal script redirects in response to a server-side 302 redirect;
+ * Test 4: one internal script redirects in response to another's redirect.
+ *
+ * The successful redirects are confirmed by the presence of a custom response
+ * header.
+ *
+ */
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+// the topic we observe to use the API. http-on-opening-request might also
+// work for some purposes.
+let redirectHook = "http-on-modify-request";
+
+var httpServer = null,
+ httpServer2 = null;
+
+ChromeUtils.defineLazyGetter(this, "port1", function () {
+ return httpServer.identity.primaryPort;
+});
+
+ChromeUtils.defineLazyGetter(this, "port2", function () {
+ return httpServer2.identity.primaryPort;
+});
+
+// Test Part 1: a cross-path redirect on a single HTTP server
+// http://localhost:port1/bait -> http://localhost:port1/switch
+var baitPath = "/bait";
+ChromeUtils.defineLazyGetter(this, "baitURI", function () {
+ return "http://localhost:" + port1 + baitPath;
+});
+var baitText = "you got the worm";
+
+var redirectedPath = "/switch";
+ChromeUtils.defineLazyGetter(this, "redirectedURI", function () {
+ return "http://localhost:" + port1 + redirectedPath;
+});
+var redirectedText = "worms are not tasty";
+
+// Test Part 2: Now, a redirect to a different server
+// http://localhost:port1/bait2 -> http://localhost:port2/switch
+var bait2Path = "/bait2";
+ChromeUtils.defineLazyGetter(this, "bait2URI", function () {
+ return "http://localhost:" + port1 + bait2Path;
+});
+
+ChromeUtils.defineLazyGetter(this, "redirected2URI", function () {
+ return "http://localhost:" + port2 + redirectedPath;
+});
+
+// Test Part 3, begin with a serverside redirect that itself turns into an instance
+// of Test Part 1
+var bait3Path = "/bait3";
+ChromeUtils.defineLazyGetter(this, "bait3URI", function () {
+ return "http://localhost:" + port1 + bait3Path;
+});
+
+// Test Part 4, begin with this client-side redirect and which then redirects
+// to an instance of Test Part 1
+var bait4Path = "/bait4";
+ChromeUtils.defineLazyGetter(this, "bait4URI", function () {
+ return "http://localhost:" + port1 + bait4Path;
+});
+
+var testHeaderName = "X-Redirected-By-Script";
+var testHeaderVal = "Success";
+var testHeaderVal2 = "Success on server 2";
+
+function make_channel(url, callback, ctx) {
+ return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true });
+}
+
+function baitHandler(metadata, response) {
+ // Content-Type required: https://bugzilla.mozilla.org/show_bug.cgi?id=748117
+ response.setHeader("Content-Type", "text/html", false);
+ response.bodyOutputStream.write(baitText, baitText.length);
+}
+
+function redirectedHandler(metadata, response) {
+ response.setHeader("Content-Type", "text/html", false);
+ response.bodyOutputStream.write(redirectedText, redirectedText.length);
+ response.setHeader(testHeaderName, testHeaderVal);
+}
+
+function redirected2Handler(metadata, response) {
+ response.setHeader("Content-Type", "text/html", false);
+ response.bodyOutputStream.write(redirectedText, redirectedText.length);
+ response.setHeader(testHeaderName, testHeaderVal2);
+}
+
+function bait3Handler(metadata, response) {
+ response.setHeader("Content-Type", "text/html", false);
+ response.setStatusLine(metadata.httpVersion, 302, "Found");
+ response.setHeader("Location", baitURI);
+}
+
+function Redirector() {
+ this.register();
+}
+
+Redirector.prototype = {
+ // This class observes an event and uses that to
+ // trigger a redirectTo(uri) redirect using the new API
+ register() {
+ Services.obs.addObserver(this, redirectHook, true);
+ },
+
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIObserver",
+ "nsISupportsWeakReference",
+ ]),
+
+ observe(subject, topic, data) {
+ if (topic == redirectHook) {
+ if (!(subject instanceof Ci.nsIHttpChannel)) {
+ do_throw(redirectHook + " observed a non-HTTP channel");
+ }
+ var channel = subject.QueryInterface(Ci.nsIHttpChannel);
+ var target = null;
+ if (channel.URI.spec == baitURI) {
+ target = redirectedURI;
+ }
+ if (channel.URI.spec == bait2URI) {
+ target = redirected2URI;
+ }
+ if (channel.URI.spec == bait4URI) {
+ target = baitURI;
+ }
+ // if we have a target, redirect there
+ if (target) {
+ var tURI = Services.io.newURI(target);
+ try {
+ channel.redirectTo(tURI);
+ } catch (e) {
+ do_throw("Exception in redirectTo " + e + "\n");
+ }
+ }
+ }
+ },
+};
+
+function makeAsyncTest(uri, headerValue, nextTask) {
+ // Make a test to check a redirect that is created with channel.asyncOpen()
+
+ // Produce a callback function which checks for the presence of headerValue,
+ // and then continues to the next async test task
+ var verifier = function (req, buffer) {
+ if (!(req instanceof Ci.nsIHttpChannel)) {
+ do_throw(req + " is not an nsIHttpChannel, catastrophe imminent!");
+ }
+
+ var httpChannel = req.QueryInterface(Ci.nsIHttpChannel);
+ Assert.equal(httpChannel.getResponseHeader(testHeaderName), headerValue);
+ Assert.equal(buffer, redirectedText);
+ nextTask();
+ };
+
+ // Produce a function to run an asyncOpen test using the above verifier
+ var test = function () {
+ var chan = make_channel(uri);
+ chan.asyncOpen(new ChannelListener(verifier));
+ };
+ return test;
+}
+
+// will be defined in run_test because of the lazy getters,
+// since the server's port is defined dynamically
+var testViaAsyncOpen4 = null;
+var testViaAsyncOpen3 = null;
+var testViaAsyncOpen2 = null;
+var testViaAsyncOpen = null;
+
+function testViaXHR() {
+ runXHRTest(baitURI, testHeaderVal);
+ runXHRTest(bait2URI, testHeaderVal2);
+ runXHRTest(bait3URI, testHeaderVal);
+ runXHRTest(bait4URI, testHeaderVal);
+}
+
+function runXHRTest(uri, headerValue) {
+ // Check that making an XHR request for uri winds up redirecting to a result with the
+ // appropriate headerValue
+ var req = new XMLHttpRequest();
+ req.open("GET", uri, false);
+ req.send();
+ Assert.equal(req.getResponseHeader(testHeaderName), headerValue);
+ Assert.equal(req.response, redirectedText);
+}
+
+function done() {
+ httpServer.stop(function () {
+ httpServer2.stop(do_test_finished);
+ });
+}
+
+// Needed for side-effects
+new Redirector();
+
+function run_test() {
+ httpServer = new HttpServer();
+ httpServer.registerPathHandler(baitPath, baitHandler);
+ httpServer.registerPathHandler(bait2Path, baitHandler);
+ httpServer.registerPathHandler(bait3Path, bait3Handler);
+ httpServer.registerPathHandler(bait4Path, baitHandler);
+ httpServer.registerPathHandler(redirectedPath, redirectedHandler);
+ httpServer.start(-1);
+ httpServer2 = new HttpServer();
+ httpServer2.registerPathHandler(redirectedPath, redirected2Handler);
+ httpServer2.start(-1);
+
+ // The tests depend on each other, and therefore need to be defined in the
+ // reverse of the order they are called in. It is therefore best to read this
+ // stanza backwards!
+ testViaAsyncOpen4 = makeAsyncTest(bait4URI, testHeaderVal, done);
+ testViaAsyncOpen3 = makeAsyncTest(bait3URI, testHeaderVal, testViaAsyncOpen4);
+ testViaAsyncOpen2 = makeAsyncTest(
+ bait2URI,
+ testHeaderVal2,
+ testViaAsyncOpen3
+ );
+ testViaAsyncOpen = makeAsyncTest(baitURI, testHeaderVal, testViaAsyncOpen2);
+
+ testViaXHR();
+ testViaAsyncOpen(); // will call done() asynchronously for cleanup
+
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_redirect_from_script_after-open_passing.js b/netwerk/test/unit/test_redirect_from_script_after-open_passing.js
new file mode 100644
index 0000000000..315f888f64
--- /dev/null
+++ b/netwerk/test/unit/test_redirect_from_script_after-open_passing.js
@@ -0,0 +1,247 @@
+/*
+ * Test whether the rewrite-requests-from-script API implemented here:
+ * https://bugzilla.mozilla.org/show_bug.cgi?id=765934 is functioning
+ * correctly
+ *
+ * The test has the following components:
+ *
+ * testViaXHR() checks that internal redirects occur correctly for requests
+ * made with XMLHttpRequest objects.
+ *
+ * testViaAsyncOpen() checks that internal redirects occur correctly when made
+ * with nsIHTTPChannel.asyncOpen().
+ *
+ * Both of the above functions tests four requests:
+ *
+ * Test 1: a simple case that redirects within a server;
+ * Test 2: a second that redirects to a second webserver;
+ * Test 3: internal script redirects in response to a server-side 302 redirect;
+ * Test 4: one internal script redirects in response to another's redirect.
+ *
+ * The successful redirects are confirmed by the presence of a custom response
+ * header.
+ *
+ */
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+// the topic we observe to use the API. http-on-opening-request might also
+// work for some purposes.
+let redirectHook = "http-on-examine-response";
+
+var httpServer = null,
+ httpServer2 = null;
+
+ChromeUtils.defineLazyGetter(this, "port1", function () {
+ return httpServer.identity.primaryPort;
+});
+
+ChromeUtils.defineLazyGetter(this, "port2", function () {
+ return httpServer2.identity.primaryPort;
+});
+
+// Test Part 1: a cross-path redirect on a single HTTP server
+// http://localhost:port1/bait -> http://localhost:port1/switch
+var baitPath = "/bait";
+ChromeUtils.defineLazyGetter(this, "baitURI", function () {
+ return "http://localhost:" + port1 + baitPath;
+});
+var baitText = "you got the worm";
+
+var redirectedPath = "/switch";
+ChromeUtils.defineLazyGetter(this, "redirectedURI", function () {
+ return "http://localhost:" + port1 + redirectedPath;
+});
+var redirectedText = "worms are not tasty";
+
+// Test Part 2: Now, a redirect to a different server
+// http://localhost:port1/bait2 -> http://localhost:port2/switch
+var bait2Path = "/bait2";
+ChromeUtils.defineLazyGetter(this, "bait2URI", function () {
+ return "http://localhost:" + port1 + bait2Path;
+});
+
+ChromeUtils.defineLazyGetter(this, "redirected2URI", function () {
+ return "http://localhost:" + port2 + redirectedPath;
+});
+
+// Test Part 3, begin with a serverside redirect that itself turns into an instance
+// of Test Part 1
+var bait3Path = "/bait3";
+ChromeUtils.defineLazyGetter(this, "bait3URI", function () {
+ return "http://localhost:" + port1 + bait3Path;
+});
+
+// Test Part 4, begin with this client-side redirect and which then redirects
+// to an instance of Test Part 1
+var bait4Path = "/bait4";
+ChromeUtils.defineLazyGetter(this, "bait4URI", function () {
+ return "http://localhost:" + port1 + bait4Path;
+});
+
+var testHeaderName = "X-Redirected-By-Script";
+var testHeaderVal = "Success";
+var testHeaderVal2 = "Success on server 2";
+
+function make_channel(url, callback, ctx) {
+ return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true });
+}
+
+function baitHandler(metadata, response) {
+ // Content-Type required: https://bugzilla.mozilla.org/show_bug.cgi?id=748117
+ response.setHeader("Content-Type", "text/html", false);
+ response.bodyOutputStream.write(baitText, baitText.length);
+}
+
+function redirectedHandler(metadata, response) {
+ response.setHeader("Content-Type", "text/html", false);
+ response.bodyOutputStream.write(redirectedText, redirectedText.length);
+ response.setHeader(testHeaderName, testHeaderVal);
+}
+
+function redirected2Handler(metadata, response) {
+ response.setHeader("Content-Type", "text/html", false);
+ response.bodyOutputStream.write(redirectedText, redirectedText.length);
+ response.setHeader(testHeaderName, testHeaderVal2);
+}
+
+function bait3Handler(metadata, response) {
+ response.setHeader("Content-Type", "text/html", false);
+ response.setStatusLine(metadata.httpVersion, 302, "Found");
+ response.setHeader("Location", baitURI);
+}
+
+function Redirector() {
+ this.register();
+}
+
+Redirector.prototype = {
+ // This class observes an event and uses that to
+ // trigger a redirectTo(uri) redirect using the new API
+ register() {
+ Services.obs.addObserver(this, redirectHook, true);
+ },
+
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIObserver",
+ "nsISupportsWeakReference",
+ ]),
+
+ observe(subject, topic, data) {
+ if (topic == redirectHook) {
+ if (!(subject instanceof Ci.nsIHttpChannel)) {
+ do_throw(redirectHook + " observed a non-HTTP channel");
+ }
+ var channel = subject.QueryInterface(Ci.nsIHttpChannel);
+ var target = null;
+ if (channel.URI.spec == baitURI) {
+ target = redirectedURI;
+ }
+ if (channel.URI.spec == bait2URI) {
+ target = redirected2URI;
+ }
+ if (channel.URI.spec == bait4URI) {
+ target = baitURI;
+ }
+ // if we have a target, redirect there
+ if (target) {
+ var tURI = Services.io.newURI(target);
+ try {
+ channel.redirectTo(tURI);
+ } catch (e) {
+ do_throw("Exception in redirectTo " + e + "\n");
+ }
+ }
+ }
+ },
+};
+
+function makeAsyncTest(uri, headerValue, nextTask) {
+ // Make a test to check a redirect that is created with channel.asyncOpen()
+
+ // Produce a callback function which checks for the presence of headerValue,
+ // and then continues to the next async test task
+ var verifier = function (req, buffer) {
+ if (!(req instanceof Ci.nsIHttpChannel)) {
+ do_throw(req + " is not an nsIHttpChannel, catastrophe imminent!");
+ }
+
+ var httpChannel = req.QueryInterface(Ci.nsIHttpChannel);
+ Assert.equal(httpChannel.getResponseHeader(testHeaderName), headerValue);
+ Assert.equal(buffer, redirectedText);
+ nextTask();
+ };
+
+ // Produce a function to run an asyncOpen test using the above verifier
+ var test = function () {
+ var chan = make_channel(uri);
+ chan.asyncOpen(new ChannelListener(verifier));
+ };
+ return test;
+}
+
+// will be defined in run_test because of the lazy getters,
+// since the server's port is defined dynamically
+var testViaAsyncOpen4 = null;
+var testViaAsyncOpen3 = null;
+var testViaAsyncOpen2 = null;
+var testViaAsyncOpen = null;
+
+function testViaXHR() {
+ runXHRTest(baitURI, testHeaderVal);
+ runXHRTest(bait2URI, testHeaderVal2);
+ runXHRTest(bait3URI, testHeaderVal);
+ runXHRTest(bait4URI, testHeaderVal);
+}
+
+function runXHRTest(uri, headerValue) {
+ // Check that making an XHR request for uri winds up redirecting to a result with the
+ // appropriate headerValue
+ var req = new XMLHttpRequest();
+ req.open("GET", uri, false);
+ req.send();
+ Assert.equal(req.getResponseHeader(testHeaderName), headerValue);
+ Assert.equal(req.response, redirectedText);
+}
+
+function done() {
+ httpServer.stop(function () {
+ httpServer2.stop(do_test_finished);
+ });
+}
+
+// Needed for side-effects
+new Redirector();
+
+function run_test() {
+ httpServer = new HttpServer();
+ httpServer.registerPathHandler(baitPath, baitHandler);
+ httpServer.registerPathHandler(bait2Path, baitHandler);
+ httpServer.registerPathHandler(bait3Path, bait3Handler);
+ httpServer.registerPathHandler(bait4Path, baitHandler);
+ httpServer.registerPathHandler(redirectedPath, redirectedHandler);
+ httpServer.start(-1);
+ httpServer2 = new HttpServer();
+ httpServer2.registerPathHandler(redirectedPath, redirected2Handler);
+ httpServer2.start(-1);
+
+ // The tests depend on each other, and therefore need to be defined in the
+ // reverse of the order they are called in. It is therefore best to read this
+ // stanza backwards!
+ testViaAsyncOpen4 = makeAsyncTest(bait4URI, testHeaderVal, done);
+ testViaAsyncOpen3 = makeAsyncTest(bait3URI, testHeaderVal, testViaAsyncOpen4);
+ testViaAsyncOpen2 = makeAsyncTest(
+ bait2URI,
+ testHeaderVal2,
+ testViaAsyncOpen3
+ );
+ testViaAsyncOpen = makeAsyncTest(baitURI, testHeaderVal, testViaAsyncOpen2);
+
+ testViaXHR();
+ testViaAsyncOpen(); // will call done() asynchronously for cleanup
+
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_redirect_history.js b/netwerk/test/unit/test_redirect_history.js
new file mode 100644
index 0000000000..124f976881
--- /dev/null
+++ b/netwerk/test/unit/test_redirect_history.js
@@ -0,0 +1,75 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+const ReferrerInfo = Components.Constructor(
+ "@mozilla.org/referrer-info;1",
+ "nsIReferrerInfo",
+ "init"
+);
+
+ChromeUtils.defineLazyGetter(this, "URL", function () {
+ return "http://localhost:" + httpServer.identity.primaryPort;
+});
+
+var httpServer = null;
+var redirects = [];
+const numRedirects = 10;
+
+function make_channel(url, callback, ctx) {
+ return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true });
+}
+
+const responseBody = "response body";
+
+function contentHandler(request, response) {
+ response.setHeader("Content-Type", "text/plain");
+ response.bodyOutputStream.write(responseBody, responseBody.length);
+}
+
+function finish_test(request, buffer) {
+ Assert.equal(buffer, responseBody);
+ let chan = request.QueryInterface(Ci.nsIChannel);
+ let redirectChain = chan.loadInfo.redirectChain;
+
+ Assert.equal(numRedirects - 1, redirectChain.length);
+ for (let i = 0; i < numRedirects - 1; ++i) {
+ let principal = redirectChain[i].principal;
+ Assert.equal(URL + redirects[i], principal.spec);
+ Assert.equal(redirectChain[i].referrerURI.spec, "http://test.com/");
+ Assert.equal(redirectChain[i].remoteAddress, "127.0.0.1");
+ }
+ httpServer.stop(do_test_finished);
+}
+
+function redirectHandler(index, request, response) {
+ response.setStatusLine(request.httpVersion, 301, "Moved");
+ let path = redirects[index + 1];
+ response.setHeader("Location", URL + path, false);
+}
+
+function run_test() {
+ httpServer = new HttpServer();
+ for (let i = 0; i < numRedirects; ++i) {
+ var randomPath = "/redirect/" + Math.random();
+ redirects.push(randomPath);
+ if (i < numRedirects - 1) {
+ httpServer.registerPathHandler(randomPath, redirectHandler.bind(this, i));
+ } else {
+ // The last one doesn't redirect
+ httpServer.registerPathHandler(
+ redirects[numRedirects - 1],
+ contentHandler
+ );
+ }
+ }
+ httpServer.start(-1);
+
+ var chan = make_channel(URL + redirects[0]);
+ var uri = NetUtil.newURI("http://test.com");
+ var httpChan = chan.QueryInterface(Ci.nsIHttpChannel);
+ httpChan.referrerInfo = new ReferrerInfo(Ci.nsIReferrerInfo.EMPTY, true, uri);
+ chan.asyncOpen(new ChannelListener(finish_test, null));
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_redirect_loop.js b/netwerk/test/unit/test_redirect_loop.js
new file mode 100644
index 0000000000..f25bee18c5
--- /dev/null
+++ b/netwerk/test/unit/test_redirect_loop.js
@@ -0,0 +1,88 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+/*
+ * This xpcshell test checks whether we detect infinite HTTP redirect loops.
+ * We check loops with "Location:" set to 1) full URI, 2) relative URI, and 3)
+ * empty Location header (which resolves to a relative link to the original
+ * URI when the original URI ends in a slash).
+ */
+
+var httpServer = new HttpServer();
+httpServer.start(-1);
+const PORT = httpServer.identity.primaryPort;
+
+var fullLoopPath = "/fullLoop";
+var fullLoopURI = "http://localhost:" + PORT + fullLoopPath;
+
+var relativeLoopPath = "/relativeLoop";
+var relativeLoopURI = "http://localhost:" + PORT + relativeLoopPath;
+
+// must use directory-style URI, so empty Location redirects back to self
+var emptyLoopPath = "/empty/";
+var emptyLoopURI = "http://localhost:" + PORT + emptyLoopPath;
+
+function make_channel(url, callback, ctx) {
+ return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true });
+}
+
+function fullLoopHandler(metadata, response) {
+ response.setStatusLine(metadata.httpVersion, 301, "Moved");
+ response.setHeader(
+ "Location",
+ "http://localhost:" + PORT + "/fullLoop",
+ false
+ );
+}
+
+function relativeLoopHandler(metadata, response) {
+ response.setStatusLine(metadata.httpVersion, 301, "Moved");
+ response.setHeader("Location", "relativeLoop", false);
+}
+
+function emptyLoopHandler(metadata, response) {
+ // Comrades! We must seize power from the petty-bourgeois running dogs of
+ // httpd.js in order to reply with a blank Location header!
+ response.seizePower();
+ response.write("HTTP/1.0 301 Moved\r\n");
+ response.write("Location: \r\n");
+ response.write("Content-Length: 4\r\n");
+ response.write("\r\n");
+ response.write("oops");
+ response.finish();
+}
+
+function testFullLoop(request, buffer) {
+ Assert.equal(request.status, Cr.NS_ERROR_REDIRECT_LOOP);
+
+ var chan = make_channel(relativeLoopURI);
+ chan.asyncOpen(
+ new ChannelListener(testRelativeLoop, null, CL_EXPECT_FAILURE)
+ );
+}
+
+function testRelativeLoop(request, buffer) {
+ Assert.equal(request.status, Cr.NS_ERROR_REDIRECT_LOOP);
+
+ var chan = make_channel(emptyLoopURI);
+ chan.asyncOpen(new ChannelListener(testEmptyLoop, null, CL_EXPECT_FAILURE));
+}
+
+function testEmptyLoop(request, buffer) {
+ Assert.equal(request.status, Cr.NS_ERROR_REDIRECT_LOOP);
+
+ httpServer.stop(do_test_finished);
+}
+
+function run_test() {
+ httpServer.registerPathHandler(fullLoopPath, fullLoopHandler);
+ httpServer.registerPathHandler(relativeLoopPath, relativeLoopHandler);
+ httpServer.registerPathHandler(emptyLoopPath, emptyLoopHandler);
+
+ var chan = make_channel(fullLoopURI);
+ chan.asyncOpen(new ChannelListener(testFullLoop, null, CL_EXPECT_FAILURE));
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_redirect_passing.js b/netwerk/test/unit/test_redirect_passing.js
new file mode 100644
index 0000000000..b712e4d819
--- /dev/null
+++ b/netwerk/test/unit/test_redirect_passing.js
@@ -0,0 +1,55 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+ChromeUtils.defineLazyGetter(this, "URL", function () {
+ return "http://localhost:" + httpServer.identity.primaryPort;
+});
+
+var httpServer = null;
+// Need to randomize, because apparently no one clears our cache
+var randomPath = "/redirect/" + Math.random();
+
+ChromeUtils.defineLazyGetter(this, "randomURI", function () {
+ return URL + randomPath;
+});
+
+function make_channel(url, callback, ctx) {
+ return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true });
+}
+
+const responseBody = "response body";
+
+function redirectHandler(metadata, response) {
+ response.setStatusLine(metadata.httpVersion, 301, "Moved");
+ response.setHeader("Location", URL + "/content", false);
+}
+
+function contentHandler(metadata, response) {
+ response.setHeader("Content-Type", "text/plain");
+ response.bodyOutputStream.write(responseBody, responseBody.length);
+}
+
+function firstTimeThrough(request, buffer) {
+ Assert.equal(buffer, responseBody);
+ var chan = make_channel(randomURI);
+ chan.asyncOpen(new ChannelListener(finish_test, null));
+}
+
+function finish_test(request, buffer) {
+ Assert.equal(buffer, responseBody);
+ httpServer.stop(do_test_finished);
+}
+
+function run_test() {
+ httpServer = new HttpServer();
+ httpServer.registerPathHandler(randomPath, redirectHandler);
+ httpServer.registerPathHandler("/content", contentHandler);
+ httpServer.start(-1);
+
+ var chan = make_channel(randomURI);
+ chan.asyncOpen(new ChannelListener(firstTimeThrough, null));
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_redirect_protocol_telemetry.js b/netwerk/test/unit/test_redirect_protocol_telemetry.js
new file mode 100644
index 0000000000..526ba440ec
--- /dev/null
+++ b/netwerk/test/unit/test_redirect_protocol_telemetry.js
@@ -0,0 +1,65 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+function make_channel(url, callback, ctx) {
+ return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true });
+}
+
+add_task(async function check_protocols() {
+ // Enable the collection (during test) for all products so even products
+ // that don't collect the data will be able to run the test without failure.
+ Services.prefs.setBoolPref(
+ "toolkit.telemetry.testing.overrideProductsCheck",
+ true
+ );
+
+ let httpserv = new HttpServer();
+ httpserv.registerPathHandler("/redirect", redirectHandler);
+ httpserv.registerPathHandler("/content", contentHandler);
+ httpserv.start(-1);
+
+ var responseProtocol;
+
+ function redirectHandler(metadata, response) {
+ response.setStatusLine(metadata.httpVersion, 301, "Moved");
+ let location =
+ responseProtocol == "data"
+ ? "data:text/plain,test"
+ : `${responseProtocol}://localhost:${httpserv.identity.primaryPort}/content`;
+ response.setHeader("Location", location, false);
+ response.setHeader("Cache-Control", "no-cache", false);
+ }
+
+ function contentHandler(metadata, response) {
+ let responseBody = "Content body";
+ response.setHeader("Content-Type", "text/plain");
+ response.bodyOutputStream.write(responseBody, responseBody.length);
+ }
+
+ function make_test(protocol) {
+ do_get_profile();
+ let redirect_hist = TelemetryTestUtils.getAndClearKeyedHistogram(
+ "NETWORK_HTTP_REDIRECT_TO_SCHEME"
+ );
+ return new Promise(resolve => {
+ const URL = `http://localhost:${httpserv.identity.primaryPort}/redirect`;
+ responseProtocol = protocol;
+ let channel = make_channel(URL);
+ let p = new Promise(resolve1 =>
+ channel.asyncOpen(new ChannelListener(resolve1))
+ );
+ p.then((request, buffer) => {
+ TelemetryTestUtils.assertKeyedHistogramSum(redirect_hist, protocol, 1);
+ resolve();
+ });
+ });
+ }
+
+ await make_test("http");
+ await make_test("data");
+
+ await new Promise(resolve => httpserv.stop(resolve));
+});
diff --git a/netwerk/test/unit/test_redirect_veto.js b/netwerk/test/unit/test_redirect_veto.js
new file mode 100644
index 0000000000..bcc9ddb625
--- /dev/null
+++ b/netwerk/test/unit/test_redirect_veto.js
@@ -0,0 +1,102 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+ChromeUtils.defineLazyGetter(this, "URL", function () {
+ return "http://localhost:" + httpServer.identity.primaryPort;
+});
+
+var httpServer = null;
+// Need to randomize, because apparently no one clears our cache
+var randomPath = "/redirect/" + Math.random();
+
+ChromeUtils.defineLazyGetter(this, "randomURI", function () {
+ return URL + randomPath;
+});
+
+function make_channel(url, callback, ctx) {
+ return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true });
+}
+
+const responseBody = "response body";
+
+function redirectHandler(metadata, response) {
+ response.setStatusLine(metadata.httpVersion, 301, "Moved");
+ response.setHeader("Location", URL + "/content", false);
+}
+
+function contentHandler(metadata, response) {
+ response.setHeader("Content-Type", "text/plain");
+ response.bodyOutputStream.write(responseBody, responseBody.length);
+}
+
+let ChannelEventSink2 = {
+ _classDescription: "WebRequest channel event sink",
+ _classID: Components.ID("115062f8-92f1-11e5-8b7f-08001110f7ec"),
+ _contractID: "@mozilla.org/webrequest/channel-event-sink;1",
+
+ QueryInterface: ChromeUtils.generateQI(["nsIChannelEventSink", "nsIFactory"]),
+
+ init() {
+ Components.manager
+ .QueryInterface(Ci.nsIComponentRegistrar)
+ .registerFactory(
+ this._classID,
+ this._classDescription,
+ this._contractID,
+ this
+ );
+ },
+
+ register() {
+ Services.catMan.addCategoryEntry(
+ "net-channel-event-sinks",
+ this._contractID,
+ this._contractID,
+ false,
+ true
+ );
+ },
+
+ unregister() {
+ Services.catMan.deleteCategoryEntry(
+ "net-channel-event-sinks",
+ this._contractID,
+ false
+ );
+ },
+
+ // nsIChannelEventSink implementation
+ asyncOnChannelRedirect(oldChannel, newChannel, flags, redirectCallback) {
+ // Abort the redirection
+ redirectCallback.onRedirectVerifyCallback(Cr.NS_ERROR_NO_INTERFACE);
+ },
+
+ // nsIFactory implementation
+ createInstance(iid) {
+ return this.QueryInterface(iid);
+ },
+};
+
+add_task(async function test_redirect_veto() {
+ httpServer = new HttpServer();
+ httpServer.registerPathHandler(randomPath, redirectHandler);
+ httpServer.registerPathHandler("/content", contentHandler);
+ httpServer.start(-1);
+
+ ChannelEventSink2.init();
+ ChannelEventSink2.register();
+
+ let chan = make_channel(randomURI);
+ let [req, buff] = await new Promise(resolve =>
+ chan.asyncOpen(
+ new ChannelListener((aReq, aBuff) => resolve([aReq, aBuff], null))
+ )
+ );
+ Assert.equal(buff, "");
+ Assert.equal(req.status, Cr.NS_OK);
+ await httpServer.stop();
+ ChannelEventSink2.unregister();
+});
diff --git a/netwerk/test/unit/test_reentrancy.js b/netwerk/test/unit/test_reentrancy.js
new file mode 100644
index 0000000000..3bd7f54f79
--- /dev/null
+++ b/netwerk/test/unit/test_reentrancy.js
@@ -0,0 +1,109 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+ChromeUtils.defineLazyGetter(this, "URL", function () {
+ return "http://localhost:" + httpserver.identity.primaryPort;
+});
+
+var httpserver = new HttpServer();
+var testpath = "/simple";
+var httpbody = "<?xml version='1.0' ?><root>0123456789</root>";
+
+function syncXHR() {
+ var xhr = new XMLHttpRequest();
+ xhr.open("GET", URL + testpath, false);
+ xhr.send(null);
+}
+
+const MAX_TESTS = 2;
+
+var listener = {
+ _done_onStart: false,
+ _done_onData: false,
+ _test: 0,
+
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIStreamListener",
+ "nsIRequestObserver",
+ ]),
+
+ onStartRequest(request) {
+ switch (this._test) {
+ case 0:
+ request.suspend();
+ syncXHR();
+ request.resume();
+ break;
+ case 1:
+ request.suspend();
+ syncXHR();
+ executeSoon(function () {
+ request.resume();
+ });
+ break;
+ case 2:
+ executeSoon(function () {
+ request.suspend();
+ });
+ executeSoon(function () {
+ request.resume();
+ });
+ syncXHR();
+ break;
+ }
+
+ this._done_onStart = true;
+ },
+
+ onDataAvailable(request, stream, offset, count) {
+ Assert.ok(this._done_onStart);
+ read_stream(stream, count);
+ this._done_onData = true;
+ },
+
+ onStopRequest(request, status) {
+ Assert.ok(this._done_onData);
+ this._reset();
+ if (this._test <= MAX_TESTS) {
+ next_test();
+ } else {
+ httpserver.stop(do_test_finished);
+ }
+ },
+
+ _reset() {
+ this._done_onStart = false;
+ this._done_onData = false;
+ this._test++;
+ },
+};
+
+function makeChan(url) {
+ return NetUtil.newChannel({
+ uri: url,
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+}
+
+function next_test() {
+ var chan = makeChan(URL + testpath);
+ chan.QueryInterface(Ci.nsIRequest);
+ chan.asyncOpen(listener);
+}
+
+function run_test() {
+ httpserver.registerPathHandler(testpath, serverHandler);
+ httpserver.start(-1);
+
+ next_test();
+
+ do_test_pending();
+}
+
+function serverHandler(metadata, response) {
+ response.setHeader("Content-Type", "text/xml", false);
+ response.bodyOutputStream.write(httpbody, httpbody.length);
+}
diff --git a/netwerk/test/unit/test_referrer.js b/netwerk/test/unit/test_referrer.js
new file mode 100644
index 0000000000..0c42849a43
--- /dev/null
+++ b/netwerk/test/unit/test_referrer.js
@@ -0,0 +1,248 @@
+"use strict";
+
+const ReferrerInfo = Components.Constructor(
+ "@mozilla.org/referrer-info;1",
+ "nsIReferrerInfo",
+ "init"
+);
+
+function getTestReferrer(server_uri, referer_uri, isPrivate = false) {
+ var uri = NetUtil.newURI(server_uri);
+ let referrer = NetUtil.newURI(referer_uri);
+ let principal = Services.scriptSecurityManager.createContentPrincipal(
+ referrer,
+ { privateBrowsingId: isPrivate ? 1 : 0 }
+ );
+ var chan = NetUtil.newChannel({
+ uri,
+ loadingPrincipal: principal,
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER,
+ securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ });
+
+ chan.QueryInterface(Ci.nsIHttpChannel);
+ chan.referrerInfo = new ReferrerInfo(
+ Ci.nsIReferrerInfo.EMPTY,
+ true,
+ referrer
+ );
+ var header = null;
+ try {
+ header = chan.getRequestHeader("Referer");
+ } catch (NS_ERROR_NOT_AVAILABLE) {}
+ return header;
+}
+
+function run_test() {
+ var prefs = Services.prefs;
+
+ var server_uri = "http://bar.examplesite.com/path2";
+ var server_uri_2 = "http://bar.example.com/anotherpath";
+ var referer_uri = "http://foo.example.com/path";
+ var referer_uri_2 = "http://bar.examplesite.com/path3?q=blah";
+ var referer_uri_2_anchor = "http://bar.examplesite.com/path3?q=blah#anchor";
+ var referer_uri_idn = "http://sub1.\xe4lt.example/path";
+
+ // for https tests
+ var server_uri_https = "https://bar.example.com/anotherpath";
+ var referer_uri_https = "https://bar.example.com/path3?q=blah";
+ var referer_uri_2_https = "https://bar.examplesite.com/path3?q=blah";
+
+ // tests for sendRefererHeader
+ prefs.setIntPref("network.http.sendRefererHeader", 0);
+ Assert.equal(null, getTestReferrer(server_uri, referer_uri));
+ prefs.setIntPref("network.http.sendRefererHeader", 2);
+ Assert.equal(
+ getTestReferrer(server_uri, referer_uri),
+ "http://foo.example.com/"
+ );
+
+ // test that https ref is not sent to http
+ Assert.equal(null, getTestReferrer(server_uri_2, referer_uri_https));
+
+ // tests for referer.defaultPolicy
+ prefs.setIntPref("network.http.referer.defaultPolicy", 0);
+ Assert.equal(null, getTestReferrer(server_uri, referer_uri));
+ prefs.setIntPref("network.http.referer.defaultPolicy", 1);
+ Assert.equal(null, getTestReferrer(server_uri, referer_uri));
+ Assert.equal(getTestReferrer(server_uri, referer_uri_2), referer_uri_2);
+ prefs.setIntPref("network.http.referer.defaultPolicy", 2);
+ Assert.equal(null, getTestReferrer(server_uri, referer_uri_https));
+ Assert.equal(
+ getTestReferrer(server_uri_https, referer_uri_https),
+ referer_uri_https
+ );
+ Assert.equal(
+ getTestReferrer(server_uri_https, referer_uri_2_https),
+ "https://bar.examplesite.com/"
+ );
+ Assert.equal(getTestReferrer(server_uri, referer_uri_2), referer_uri_2);
+ Assert.equal(
+ getTestReferrer(server_uri, referer_uri),
+ "http://foo.example.com/"
+ );
+ prefs.setIntPref("network.http.referer.defaultPolicy", 3);
+ Assert.equal(getTestReferrer(server_uri, referer_uri), referer_uri);
+ Assert.equal(null, getTestReferrer(server_uri_2, referer_uri_https));
+
+ // tests for referer.defaultPolicy.pbmode
+ prefs.setIntPref("network.http.referer.defaultPolicy.pbmode", 0);
+ Assert.equal(null, getTestReferrer(server_uri, referer_uri, true));
+ prefs.setIntPref("network.http.referer.defaultPolicy.pbmode", 1);
+ Assert.equal(null, getTestReferrer(server_uri, referer_uri, true));
+ Assert.equal(getTestReferrer(server_uri, referer_uri_2, true), referer_uri_2);
+ prefs.setIntPref("network.http.referer.defaultPolicy.pbmode", 2);
+ Assert.equal(null, getTestReferrer(server_uri, referer_uri_https, true));
+ Assert.equal(
+ getTestReferrer(server_uri_https, referer_uri_https, true),
+ referer_uri_https
+ );
+ Assert.equal(
+ getTestReferrer(server_uri_https, referer_uri_2_https, true),
+ "https://bar.examplesite.com/"
+ );
+ Assert.equal(getTestReferrer(server_uri, referer_uri_2, true), referer_uri_2);
+ Assert.equal(
+ getTestReferrer(server_uri, referer_uri, true),
+ "http://foo.example.com/"
+ );
+ prefs.setIntPref("network.http.referer.defaultPolicy.pbmode", 3);
+ Assert.equal(getTestReferrer(server_uri, referer_uri, true), referer_uri);
+ Assert.equal(null, getTestReferrer(server_uri_2, referer_uri_https, true));
+
+ // tests for referer.spoofSource
+ prefs.setBoolPref("network.http.referer.spoofSource", true);
+ Assert.equal(getTestReferrer(server_uri, referer_uri), server_uri);
+ prefs.setBoolPref("network.http.referer.spoofSource", false);
+ Assert.equal(getTestReferrer(server_uri, referer_uri), referer_uri);
+
+ // tests for referer.XOriginPolicy
+ prefs.setIntPref("network.http.referer.XOriginPolicy", 2);
+ Assert.equal(null, getTestReferrer(server_uri_2, referer_uri));
+ Assert.equal(getTestReferrer(server_uri, referer_uri_2), referer_uri_2);
+ prefs.setIntPref("network.http.referer.XOriginPolicy", 1);
+ Assert.equal(getTestReferrer(server_uri_2, referer_uri), referer_uri);
+ Assert.equal(null, getTestReferrer(server_uri, referer_uri));
+ // https test
+ Assert.equal(
+ getTestReferrer(server_uri_https, referer_uri_https),
+ referer_uri_https
+ );
+ prefs.setIntPref("network.http.referer.XOriginPolicy", 0);
+ Assert.equal(getTestReferrer(server_uri, referer_uri), referer_uri);
+
+ // tests for referer.trimmingPolicy
+ prefs.setIntPref("network.http.referer.trimmingPolicy", 1);
+ Assert.equal(
+ getTestReferrer(server_uri, referer_uri_2),
+ "http://bar.examplesite.com/path3"
+ );
+ Assert.equal(
+ getTestReferrer(server_uri, referer_uri_idn),
+ "http://sub1.xn--lt-uia.example/path"
+ );
+ prefs.setIntPref("network.http.referer.trimmingPolicy", 2);
+ Assert.equal(
+ getTestReferrer(server_uri, referer_uri_2),
+ "http://bar.examplesite.com/"
+ );
+ Assert.equal(
+ getTestReferrer(server_uri, referer_uri_idn),
+ "http://sub1.xn--lt-uia.example/"
+ );
+ // https test
+ Assert.equal(
+ getTestReferrer(server_uri_https, referer_uri_https),
+ "https://bar.example.com/"
+ );
+ prefs.setIntPref("network.http.referer.trimmingPolicy", 0);
+ // test that anchor is lopped off in ordinary case
+ Assert.equal(
+ getTestReferrer(server_uri, referer_uri_2_anchor),
+ referer_uri_2
+ );
+
+ // tests for referer.XOriginTrimmingPolicy
+ prefs.setIntPref("network.http.referer.XOriginTrimmingPolicy", 1);
+ Assert.equal(
+ getTestReferrer(server_uri, referer_uri),
+ "http://foo.example.com/path"
+ );
+ Assert.equal(
+ getTestReferrer(server_uri, referer_uri_idn),
+ "http://sub1.xn--lt-uia.example/path"
+ );
+ Assert.equal(
+ getTestReferrer(server_uri, referer_uri_2),
+ "http://bar.examplesite.com/path3?q=blah"
+ );
+ prefs.setIntPref("network.http.referer.trimmingPolicy", 1);
+ Assert.equal(
+ getTestReferrer(server_uri, referer_uri_2),
+ "http://bar.examplesite.com/path3"
+ );
+ prefs.setIntPref("network.http.referer.XOriginTrimmingPolicy", 2);
+ Assert.equal(
+ getTestReferrer(server_uri, referer_uri),
+ "http://foo.example.com/"
+ );
+ Assert.equal(
+ getTestReferrer(server_uri, referer_uri_idn),
+ "http://sub1.xn--lt-uia.example/"
+ );
+ Assert.equal(
+ getTestReferrer(server_uri, referer_uri_2),
+ "http://bar.examplesite.com/path3"
+ );
+ prefs.setIntPref("network.http.referer.trimmingPolicy", 0);
+ Assert.equal(
+ getTestReferrer(server_uri, referer_uri_2),
+ "http://bar.examplesite.com/path3?q=blah"
+ );
+ // https tests
+ Assert.equal(
+ getTestReferrer(server_uri_https, referer_uri_https),
+ "https://bar.example.com/path3?q=blah"
+ );
+ Assert.equal(
+ getTestReferrer(server_uri_https, referer_uri_2_https),
+ "https://bar.examplesite.com/"
+ );
+ prefs.setIntPref("network.http.referer.XOriginTrimmingPolicy", 0);
+ // test that anchor is lopped off in ordinary case
+ Assert.equal(
+ getTestReferrer(server_uri, referer_uri_2_anchor),
+ referer_uri_2
+ );
+
+ // test referrer length limitation
+ // referer_uri's length is 27 and origin's length is 23
+ prefs.setIntPref("network.http.referer.referrerLengthLimit", 27);
+ Assert.equal(getTestReferrer(server_uri, referer_uri), referer_uri);
+ prefs.setIntPref("network.http.referer.referrerLengthLimit", 26);
+ Assert.equal(
+ getTestReferrer(server_uri, referer_uri),
+ "http://foo.example.com/"
+ );
+ prefs.setIntPref("network.http.referer.referrerLengthLimit", 22);
+ Assert.equal(getTestReferrer(server_uri, referer_uri), null);
+ prefs.setIntPref("network.http.referer.referrerLengthLimit", 0);
+ Assert.equal(getTestReferrer(server_uri, referer_uri), referer_uri);
+ prefs.setIntPref("network.http.referer.referrerLengthLimit", 4096);
+ Assert.equal(getTestReferrer(server_uri, referer_uri), referer_uri);
+
+ // combination test: send spoofed path-only when hosts match
+ var combo_referer_uri = "http://blah.foo.com/path?q=hot";
+ var dest_uri = "http://blah.foo.com:9999/spoofedpath?q=bad";
+ prefs.setIntPref("network.http.referer.trimmingPolicy", 1);
+ prefs.setBoolPref("network.http.referer.spoofSource", true);
+ prefs.setIntPref("network.http.referer.XOriginPolicy", 2);
+ Assert.equal(
+ getTestReferrer(dest_uri, combo_referer_uri),
+ "http://blah.foo.com:9999/spoofedpath"
+ );
+ Assert.equal(
+ null,
+ getTestReferrer(dest_uri, "http://gah.foo.com/anotherpath")
+ );
+}
diff --git a/netwerk/test/unit/test_referrer_cross_origin.js b/netwerk/test/unit/test_referrer_cross_origin.js
new file mode 100644
index 0000000000..ada64fcced
--- /dev/null
+++ b/netwerk/test/unit/test_referrer_cross_origin.js
@@ -0,0 +1,332 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+"use strict";
+
+const ReferrerInfo = Components.Constructor(
+ "@mozilla.org/referrer-info;1",
+ "nsIReferrerInfo",
+ "init"
+);
+
+function test_policy(test) {
+ info("Running test: " + test.toSource());
+
+ let prefs = Services.prefs;
+
+ if (test.trimmingPolicy !== undefined) {
+ prefs.setIntPref(
+ "network.http.referer.trimmingPolicy",
+ test.trimmingPolicy
+ );
+ } else {
+ prefs.setIntPref("network.http.referer.trimmingPolicy", 0);
+ }
+
+ if (test.XOriginTrimmingPolicy !== undefined) {
+ prefs.setIntPref(
+ "network.http.referer.XOriginTrimmingPolicy",
+ test.XOriginTrimmingPolicy
+ );
+ } else {
+ prefs.setIntPref("network.http.referer.XOriginTrimmingPolicy", 0);
+ }
+
+ if (test.disallowRelaxingDefault) {
+ prefs.setBoolPref(
+ "network.http.referer.disallowCrossSiteRelaxingDefault",
+ test.disallowRelaxingDefault
+ );
+ } else {
+ prefs.setBoolPref(
+ "network.http.referer.disallowCrossSiteRelaxingDefault",
+ false
+ );
+ }
+
+ let referrer = NetUtil.newURI(test.referrer);
+ let triggeringPrincipal =
+ Services.scriptSecurityManager.createContentPrincipal(referrer, {});
+ let chan = NetUtil.newChannel({
+ uri: test.url,
+ loadingPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+ triggeringPrincipal,
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER,
+ securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ });
+
+ chan.QueryInterface(Ci.nsIHttpChannel);
+ chan.referrerInfo = new ReferrerInfo(test.policy, true, referrer);
+
+ if (test.expectedReferrerSpec === undefined) {
+ try {
+ chan.getRequestHeader("Referer");
+ do_throw("Should not find a Referer header!");
+ } catch (e) {}
+ } else {
+ let header = chan.getRequestHeader("Referer");
+ Assert.equal(header, test.expectedReferrerSpec);
+ }
+}
+
+const nsIReferrerInfo = Ci.nsIReferrerInfo;
+var gTests = [
+ // Test same origin policy w/o cross origin
+ {
+ policy: nsIReferrerInfo.SAME_ORIGIN,
+ url: "https://test.example/foo?a",
+ referrer: "https://test.example/foo?a",
+ expectedReferrerSpec: "https://test.example/foo?a",
+ },
+ {
+ policy: nsIReferrerInfo.SAME_ORIGIN,
+ url: "https://test.example/foo?a",
+ referrer: "https://foo.example/foo?a",
+ expectedReferrerSpec: undefined,
+ },
+ {
+ policy: nsIReferrerInfo.SAME_ORIGIN,
+ trimmingPolicy: 1,
+ url: "https://test.example/foo?a",
+ referrer: "https://test.example/foo?a",
+ expectedReferrerSpec: "https://test.example/foo",
+ },
+ {
+ policy: nsIReferrerInfo.SAME_ORIGIN,
+ trimmingPolicy: 1,
+ url: "https://test.example/foo?a",
+ referrer: "https://foo.example/foo?a",
+ expectedReferrerSpec: undefined,
+ },
+ {
+ policy: nsIReferrerInfo.SAME_ORIGIN,
+ trimmingPolicy: 2,
+ url: "https://test.example/foo?a",
+ referrer: "https://test.example/foo?a",
+ expectedReferrerSpec: "https://test.example/",
+ },
+ {
+ policy: nsIReferrerInfo.SAME_ORIGIN,
+ trimmingPolicy: 2,
+ url: "https://test.example/foo?a",
+ referrer: "https://foo.example/foo?a",
+ expectedReferrerSpec: undefined,
+ },
+
+ // Test origin when xorigin policy w/o cross origin
+ {
+ policy: nsIReferrerInfo.ORIGIN_WHEN_CROSS_ORIGIN,
+ url: "https://test.example/foo?a",
+ referrer: "https://test.example/foo?a",
+ expectedReferrerSpec: "https://test.example/foo?a",
+ },
+ {
+ policy: nsIReferrerInfo.ORIGIN_WHEN_CROSS_ORIGIN,
+ url: "https://test.example/foo?a",
+ referrer: "https://foo.example/foo?a",
+ expectedReferrerSpec: "https://foo.example/",
+ },
+ {
+ policy: nsIReferrerInfo.ORIGIN_WHEN_CROSS_ORIGIN,
+ trimmingPolicy: 1,
+ url: "https://test.example/foo?a",
+ referrer: "https://test.example/foo?a",
+ expectedReferrerSpec: "https://test.example/foo",
+ },
+ {
+ policy: nsIReferrerInfo.ORIGIN_WHEN_CROSS_ORIGIN,
+ trimmingPolicy: 1,
+ url: "https://test.example/foo?a",
+ referrer: "https://foo.example/foo?a",
+ expectedReferrerSpec: "https://foo.example/",
+ },
+ {
+ policy: nsIReferrerInfo.ORIGIN_WHEN_CROSS_ORIGIN,
+ trimmingPolicy: 2,
+ url: "https://test.example/foo?a",
+ referrer: "https://test.example/foo?a",
+ expectedReferrerSpec: "https://test.example/",
+ },
+ {
+ policy: nsIReferrerInfo.ORIGIN_WHEN_CROSS_ORIGIN,
+ trimmingPolicy: 2,
+ url: "https://test.example/foo?a",
+ referrer: "https://foo.example/foo?a",
+ expectedReferrerSpec: "https://foo.example/",
+ },
+ {
+ policy: nsIReferrerInfo.ORIGIN_WHEN_CROSS_ORIGIN,
+ XOriginTrimmingPolicy: 1,
+ url: "https://test.example/foo?a",
+ referrer: "https://test.example/foo?a",
+ expectedReferrerSpec: "https://test.example/foo?a",
+ },
+ {
+ policy: nsIReferrerInfo.ORIGIN_WHEN_CROSS_ORIGIN,
+ XOriginTrimmingPolicy: 1,
+ url: "https://test.example/foo?a",
+ referrer: "https://foo.example/foo?a",
+ expectedReferrerSpec: "https://foo.example/",
+ },
+ {
+ policy: nsIReferrerInfo.ORIGIN_WHEN_CROSS_ORIGIN,
+ XOriginTrimmingPolicy: 2,
+ url: "https://test.example/foo?a",
+ referrer: "https://test.example/foo?a",
+ expectedReferrerSpec: "https://test.example/foo?a",
+ },
+ {
+ policy: nsIReferrerInfo.ORIGIN_WHEN_CROSS_ORIGIN,
+ XOriginTrimmingPolicy: 2,
+ url: "https://test.example/foo?a",
+ referrer: "https://foo.example/foo?a",
+ expectedReferrerSpec: "https://foo.example/",
+ },
+
+ // Test strict origin when xorigin policy w/o cross origin
+ {
+ policy: nsIReferrerInfo.STRICT_ORIGIN_WHEN_CROSS_ORIGIN,
+ url: "https://test.example/foo?a",
+ referrer: "https://test.example/foo?a",
+ expectedReferrerSpec: "https://test.example/foo?a",
+ },
+ {
+ policy: nsIReferrerInfo.STRICT_ORIGIN_WHEN_CROSS_ORIGIN,
+ url: "https://test.example/foo?a",
+ referrer: "https://foo.example/foo?a",
+ expectedReferrerSpec: "https://foo.example/",
+ },
+ {
+ policy: nsIReferrerInfo.STRICT_ORIGIN_WHEN_CROSS_ORIGIN,
+ url: "http://test.example/foo?a",
+ referrer: "https://foo.example/foo?a",
+ expectedReferrerSpec: undefined,
+ },
+ {
+ policy: nsIReferrerInfo.STRICT_ORIGIN_WHEN_CROSS_ORIGIN,
+ trimmingPolicy: 1,
+ url: "https://test.example/foo?a",
+ referrer: "https://test.example/foo?a",
+ expectedReferrerSpec: "https://test.example/foo",
+ },
+ {
+ policy: nsIReferrerInfo.STRICT_ORIGIN_WHEN_CROSS_ORIGIN,
+ trimmingPolicy: 1,
+ url: "https://test.example/foo?a",
+ referrer: "https://foo.example/foo?a",
+ expectedReferrerSpec: "https://foo.example/",
+ },
+ {
+ policy: nsIReferrerInfo.STRICT_ORIGIN_WHEN_CROSS_ORIGIN,
+ trimmingPolicy: 1,
+ url: "http://test.example/foo?a",
+ referrer: "https://foo.example/foo?a",
+ expectedReferrerSpec: undefined,
+ },
+ {
+ policy: nsIReferrerInfo.STRICT_ORIGIN_WHEN_CROSS_ORIGIN,
+ trimmingPolicy: 2,
+ url: "https://test.example/foo?a",
+ referrer: "https://test.example/foo?a",
+ expectedReferrerSpec: "https://test.example/",
+ },
+ {
+ policy: nsIReferrerInfo.STRICT_ORIGIN_WHEN_CROSS_ORIGIN,
+ trimmingPolicy: 2,
+ url: "https://test.example/foo?a",
+ referrer: "https://foo.example/foo?a",
+ expectedReferrerSpec: "https://foo.example/",
+ },
+ {
+ policy: nsIReferrerInfo.STRICT_ORIGIN_WHEN_CROSS_ORIGIN,
+ trimmingPolicy: 2,
+ url: "http://test.example/foo?a",
+ referrer: "https://foo.example/foo?a",
+ expectedReferrerSpec: undefined,
+ },
+ {
+ policy: nsIReferrerInfo.STRICT_ORIGIN_WHEN_CROSS_ORIGIN,
+ XOriginTrimmingPolicy: 1,
+ url: "https://test.example/foo?a",
+ referrer: "https://test.example/foo?a",
+ expectedReferrerSpec: "https://test.example/foo?a",
+ },
+ {
+ policy: nsIReferrerInfo.STRICT_ORIGIN_WHEN_CROSS_ORIGIN,
+ XOriginTrimmingPolicy: 1,
+ url: "https://test.example/foo?a",
+ referrer: "https://foo.example/foo?a",
+ expectedReferrerSpec: "https://foo.example/",
+ },
+ {
+ policy: nsIReferrerInfo.STRICT_ORIGIN_WHEN_CROSS_ORIGIN,
+ XOriginTrimmingPolicy: 1,
+ url: "http://test.example/foo?a",
+ referrer: "https://foo.example/foo?a",
+ expectedReferrerSpec: undefined,
+ },
+ {
+ policy: nsIReferrerInfo.STRICT_ORIGIN_WHEN_CROSS_ORIGIN,
+ XOriginTrimmingPolicy: 2,
+ url: "https://test.example/foo?a",
+ referrer: "https://test.example/foo?a",
+ expectedReferrerSpec: "https://test.example/foo?a",
+ },
+ {
+ policy: nsIReferrerInfo.STRICT_ORIGIN_WHEN_CROSS_ORIGIN,
+ XOriginTrimmingPolicy: 2,
+ url: "https://test.example/foo?a",
+ referrer: "https://foo.example/foo?a",
+ expectedReferrerSpec: "https://foo.example/",
+ },
+ {
+ policy: nsIReferrerInfo.STRICT_ORIGIN_WHEN_CROSS_ORIGIN,
+ XOriginTrimmingPolicy: 2,
+ url: "http://test.example/foo?a",
+ referrer: "https://foo.example/foo?a",
+ expectedReferrerSpec: undefined,
+ },
+
+ // Test mix and choose max of XOriginTrimmingPolicy and trimmingPolicy
+ {
+ policy: nsIReferrerInfo.UNSAFE_URL,
+ XOriginTrimmingPolicy: 2,
+ trimmingPolicy: 1,
+ url: "https://test.example/foo?a",
+ referrer: "https://test1.example/foo?a",
+ expectedReferrerSpec: "https://test1.example/",
+ },
+ {
+ policy: nsIReferrerInfo.UNSAFE_URL,
+ XOriginTrimmingPolicy: 2,
+ trimmingPolicy: 1,
+ url: "https://test.example/foo?a",
+ referrer: "https://test.example/foo?a",
+ expectedReferrerSpec: "https://test.example/foo",
+ },
+ {
+ policy: nsIReferrerInfo.UNSAFE_URL,
+ XOriginTrimmingPolicy: 1,
+ trimmingPolicy: 2,
+ url: "https://test.example/foo?a",
+ referrer: "https://test.example/foo?a",
+ expectedReferrerSpec: "https://test.example/",
+ },
+ {
+ policy: nsIReferrerInfo.UNSAFE_URL,
+ XOriginTrimmingPolicy: 1,
+ trimmingPolicy: 0,
+ url: "https://test.example/foo?a",
+ referrer: "https://test1.example/foo?a",
+ expectedReferrerSpec: "https://test1.example/foo",
+ },
+];
+
+function run_test() {
+ gTests.forEach(test => test_policy(test));
+ Services.prefs.clearUserPref("network.http.referer.trimmingPolicy");
+ Services.prefs.clearUserPref("network.http.referer.XOriginTrimmingPolicy");
+ Services.prefs.clearUserPref(
+ "network.http.referer.disallowCrossSiteRelaxingDefault"
+ );
+}
diff --git a/netwerk/test/unit/test_referrer_policy.js b/netwerk/test/unit/test_referrer_policy.js
new file mode 100644
index 0000000000..18b1cb3a16
--- /dev/null
+++ b/netwerk/test/unit/test_referrer_policy.js
@@ -0,0 +1,154 @@
+"use strict";
+
+const ReferrerInfo = Components.Constructor(
+ "@mozilla.org/referrer-info;1",
+ "nsIReferrerInfo",
+ "init"
+);
+
+function test_policy(test) {
+ info("Running test: " + test.toSource());
+
+ var prefs = Services.prefs;
+ if (test.defaultReferrerPolicyPref !== undefined) {
+ prefs.setIntPref(
+ "network.http.referer.defaultPolicy",
+ test.defaultReferrerPolicyPref
+ );
+ } else {
+ prefs.setIntPref("network.http.referer.defaultPolicy", 3);
+ }
+
+ if (test.disallowRelaxingDefault) {
+ prefs.setBoolPref(
+ "network.http.referer.disallowCrossSiteRelaxingDefault",
+ test.disallowRelaxingDefault
+ );
+ } else {
+ prefs.setBoolPref(
+ "network.http.referer.disallowCrossSiteRelaxingDefault",
+ false
+ );
+ }
+
+ var uri = NetUtil.newURI(test.url);
+ var chan = NetUtil.newChannel({
+ uri,
+ loadUsingSystemPrincipal: true,
+ });
+
+ var referrer = NetUtil.newURI(test.referrer);
+ chan.QueryInterface(Ci.nsIHttpChannel);
+ chan.referrerInfo = new ReferrerInfo(test.policy, true, referrer);
+
+ if (test.expectedReferrerSpec === undefined) {
+ try {
+ chan.getRequestHeader("Referer");
+ do_throw("Should not find a Referer header!");
+ } catch (e) {}
+ } else {
+ var header = chan.getRequestHeader("Referer");
+ Assert.equal(header, test.expectedReferrerSpec);
+ }
+}
+
+const nsIReferrerInfo = Ci.nsIReferrerInfo;
+// Assuming cross origin because we have no triggering principal available
+var gTests = [
+ {
+ policy: nsIReferrerInfo.EMPTY,
+ defaultReferrerPolicyPref: 0,
+ url: "https://test.example/foo",
+ referrer: "https://test.example/referrer",
+ expectedReferrerSpec: undefined,
+ },
+ {
+ policy: nsIReferrerInfo.EMPTY,
+ defaultReferrerPolicyPref: 1,
+ url: "http://test.example/foo",
+ referrer: "http://test1.example/referrer",
+ expectedReferrerSpec: undefined,
+ },
+ {
+ policy: nsIReferrerInfo.EMPTY,
+ defaultReferrerPolicyPref: 2,
+ url: "https://sub1.\xe4lt.example/foo",
+ referrer: "https://sub1.\xe4lt.example/referrer",
+ expectedReferrerSpec: "https://sub1.xn--lt-uia.example/",
+ },
+ {
+ policy: nsIReferrerInfo.EMPTY,
+ defaultReferrerPolicyPref: 2,
+ url: "https://test.example/foo",
+ referrer: "https://test1.example/referrer",
+ expectedReferrerSpec: "https://test1.example/",
+ },
+ {
+ policy: nsIReferrerInfo.EMPTY,
+ defaultReferrerPolicyPref: 3,
+ url: "https://test.example/foo",
+ referrer: "https://test.example/referrer",
+ expectedReferrerSpec: "https://test.example/referrer",
+ },
+ {
+ policy: nsIReferrerInfo.EMPTY,
+ defaultReferrerPolicyPref: 3,
+ url: "https://sub1.\xe4lt.example/foo",
+ referrer: "https://sub1.\xe4lt.example/referrer",
+ expectedReferrerSpec: "https://sub1.xn--lt-uia.example/referrer",
+ },
+ {
+ policy: nsIReferrerInfo.EMPTY,
+ defaultReferrerPolicyPref: 3,
+ url: "http://test.example/foo",
+ referrer: "https://test.example/referrer",
+ expectedReferrerSpec: undefined,
+ },
+ {
+ policy: nsIReferrerInfo.NO_REFERRER,
+ url: "https://test.example/foo",
+ referrer: "https://test.example/referrer",
+ expectedReferrerSpec: undefined,
+ },
+ {
+ policy: nsIReferrerInfo.ORIGIN,
+ url: "https://test.example/foo",
+ referrer: "https://test.example/referrer",
+ expectedReferrerSpec: "https://test.example/",
+ },
+ {
+ policy: nsIReferrerInfo.ORIGIN,
+ url: "https://sub1.\xe4lt.example/foo",
+ referrer: "https://sub1.\xe4lt.example/referrer",
+ expectedReferrerSpec: "https://sub1.xn--lt-uia.example/",
+ },
+ {
+ policy: nsIReferrerInfo.UNSAFE_URL,
+ url: "https://test.example/foo",
+ referrer: "https://test.example/referrer",
+ expectedReferrerSpec: "https://test.example/referrer",
+ },
+ {
+ policy: nsIReferrerInfo.UNSAFE_URL,
+ url: "https://sub1.\xe4lt.example/foo",
+ referrer: "https://sub1.\xe4lt.example/referrer",
+ expectedReferrerSpec: "https://sub1.xn--lt-uia.example/referrer",
+ },
+ {
+ policy: nsIReferrerInfo.UNSAFE_URL,
+ url: "http://test.example/foo",
+ referrer: "https://test.example/referrer",
+ expectedReferrerSpec: "https://test.example/referrer",
+ },
+ {
+ policy: nsIReferrerInfo.UNSAFE_URL,
+ url: "http://sub1.\xe4lt.example/foo",
+ referrer: "https://sub1.\xe4lt.example/referrer",
+ expectedReferrerSpec: "https://sub1.xn--lt-uia.example/referrer",
+ },
+];
+
+function run_test() {
+ gTests.forEach(test => test_policy(test));
+ Services.prefs.clearUserPref("network.http.referer.disallowRelaxingDefault");
+}
diff --git a/netwerk/test/unit/test_reopen.js b/netwerk/test/unit/test_reopen.js
new file mode 100644
index 0000000000..6bae0e71cf
--- /dev/null
+++ b/netwerk/test/unit/test_reopen.js
@@ -0,0 +1,136 @@
+// This testcase verifies that channels can't be reopened
+// See https://bugzilla.mozilla.org/show_bug.cgi?id=372486
+
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+const BinaryInputStream = Components.Constructor(
+ "@mozilla.org/binaryinputstream;1",
+ "nsIBinaryInputStream",
+ "setInputStream"
+);
+
+const NS_ERROR_IN_PROGRESS = 0x804b000f;
+const NS_ERROR_ALREADY_OPENED = 0x804b0049;
+
+var chan = null;
+var httpserv = null;
+
+[test_data_channel, test_http_channel, test_file_channel, end].forEach(f =>
+ add_test(f)
+);
+
+// Utility functions
+
+function makeChan(url) {
+ return (chan = NetUtil.newChannel({
+ uri: url,
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIChannel));
+}
+
+function new_file_channel(file) {
+ return NetUtil.newChannel({
+ uri: Services.io.newFileURI(file),
+ loadUsingSystemPrincipal: true,
+ });
+}
+
+function check_throws(closure, error) {
+ var thrown = false;
+ try {
+ closure();
+ } catch (e) {
+ if (error instanceof Array) {
+ Assert.notEqual(error.indexOf(e.result), -1);
+ } else {
+ Assert.equal(e.result, error);
+ }
+ thrown = true;
+ }
+ Assert.ok(thrown);
+}
+
+function check_open_throws(error) {
+ check_throws(function () {
+ chan.open(listener);
+ }, error);
+}
+
+function check_async_open_throws(error) {
+ check_throws(function () {
+ chan.asyncOpen(listener);
+ }, error);
+}
+
+var listener = {
+ onStartRequest: function test_onStartR(request) {
+ check_async_open_throws(NS_ERROR_IN_PROGRESS);
+ },
+
+ onDataAvailable: function test_ODA(request, inputStream, offset, count) {
+ new BinaryInputStream(inputStream).readByteArray(count); // required by API
+ check_async_open_throws(NS_ERROR_IN_PROGRESS);
+ },
+
+ onStopRequest: function test_onStopR(request, status) {
+ // Once onStopRequest is reached, the channel is marked as having been
+ // opened
+ check_async_open_throws(NS_ERROR_ALREADY_OPENED);
+ do_timeout(0, after_channel_closed);
+ },
+};
+
+function after_channel_closed() {
+ check_async_open_throws(NS_ERROR_ALREADY_OPENED);
+
+ run_next_test();
+}
+
+function test_channel(createChanClosure) {
+ // First, synchronous reopening test
+ chan = createChanClosure();
+ chan.open();
+ check_open_throws(NS_ERROR_IN_PROGRESS);
+ check_async_open_throws([NS_ERROR_IN_PROGRESS, NS_ERROR_ALREADY_OPENED]);
+
+ // Then, asynchronous one
+ chan = createChanClosure();
+ chan.asyncOpen(listener);
+ check_open_throws(NS_ERROR_IN_PROGRESS);
+ check_async_open_throws(NS_ERROR_IN_PROGRESS);
+}
+
+function test_data_channel() {
+ test_channel(function () {
+ return makeChan("data:text/plain,foo");
+ });
+}
+
+function test_http_channel() {
+ test_channel(function () {
+ return makeChan("http://localhost:" + httpserv.identity.primaryPort + "/");
+ });
+}
+
+function test_file_channel() {
+ var file = do_get_file("data/test_readline1.txt");
+ test_channel(function () {
+ return new_file_channel(file);
+ });
+}
+
+function end() {
+ httpserv.stop(do_test_finished);
+}
+
+function run_test() {
+ // start server
+ httpserv = new HttpServer();
+ httpserv.start(-1);
+
+ run_next_test();
+}
diff --git a/netwerk/test/unit/test_reply_without_content_type.js b/netwerk/test/unit/test_reply_without_content_type.js
new file mode 100644
index 0000000000..f32ad81d84
--- /dev/null
+++ b/netwerk/test/unit/test_reply_without_content_type.js
@@ -0,0 +1,151 @@
+//
+// tests HTTP replies that lack content-type (where we try to sniff content-type).
+//
+
+// Note: sets Cc and Ci variables
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+var httpserver = new HttpServer();
+var testpath = "/simple_plainText";
+var httpbody = "<html><body>omg hai</body></html>";
+var testpathGZip = "/simple_gzip";
+//this is compressed httpbody;
+var httpbodyGZip = [
+ "0x1f",
+ "0x8b",
+ "0x8",
+ "0x0",
+ "0x0",
+ "0x0",
+ "0x0",
+ "0x0",
+ "0x0",
+ "0x3",
+ "0xb3",
+ "0xc9",
+ "0x28",
+ "0xc9",
+ "0xcd",
+ "0xb1",
+ "0xb3",
+ "0x49",
+ "0xca",
+ "0x4f",
+ "0xa9",
+ "0xb4",
+ "0xcb",
+ "0xcf",
+ "0x4d",
+ "0x57",
+ "0xc8",
+ "0x48",
+ "0xcc",
+ "0xb4",
+ "0xd1",
+ "0x7",
+ "0xf3",
+ "0x6c",
+ "0xf4",
+ "0xc1",
+ "0x52",
+ "0x0",
+ "0x4",
+ "0x99",
+ "0x79",
+ "0x2b",
+ "0x21",
+ "0x0",
+ "0x0",
+ "0x0",
+];
+
+var dbg = 0;
+if (dbg) {
+ print("============== START ==========");
+}
+
+add_test(function test_plainText() {
+ if (dbg) {
+ print("============== test_plainText: in");
+ }
+ httpserver.registerPathHandler(testpath, serverHandler_plainText);
+ httpserver.start(-1);
+ var channel = setupChannel(testpath);
+ // ChannelListener defined in head_channels.js
+ channel.asyncOpen(new ChannelListener(checkRequest, channel));
+ do_test_pending();
+ if (dbg) {
+ print("============== test_plainText: out");
+ }
+});
+
+add_test(function test_GZip() {
+ if (dbg) {
+ print("============== test_GZip: in");
+ }
+ httpserver.registerPathHandler(testpathGZip, serverHandler_GZip);
+ httpserver.start(-1);
+ var channel = setupChannel(testpathGZip);
+ // ChannelListener defined in head_channels.js
+ channel.asyncOpen(new ChannelListener(checkRequest, channel, CL_EXPECT_GZIP));
+ do_test_pending();
+ if (dbg) {
+ print("============== test_GZip: out");
+ }
+});
+
+function setupChannel(path) {
+ var chan = NetUtil.newChannel({
+ uri: "http://localhost:" + httpserver.identity.primaryPort + path,
+ loadUsingSystemPrincipal: true,
+ });
+ chan.QueryInterface(Ci.nsIHttpChannel);
+ chan.requestMethod = "GET";
+ return chan;
+}
+
+function serverHandler_plainText(metadata, response) {
+ if (dbg) {
+ print("============== serverHandler plainText: in");
+ }
+ // no content type set
+ // response.setHeader("Content-Type", "text/plain", false);
+ response.bodyOutputStream.write(httpbody, httpbody.length);
+ if (dbg) {
+ print("============== serverHandler plainText: out");
+ }
+}
+
+function serverHandler_GZip(metadata, response) {
+ if (dbg) {
+ print("============== serverHandler GZip: in");
+ }
+ // no content type set
+ // response.setHeader("Content-Type", "text/plain", false);
+ response.setHeader("Content-Encoding", "gzip", false);
+ var bos = Cc["@mozilla.org/binaryoutputstream;1"].createInstance(
+ Ci.nsIBinaryOutputStream
+ );
+ bos.setOutputStream(response.bodyOutputStream);
+ bos.writeByteArray(httpbodyGZip);
+ if (dbg) {
+ print("============== serverHandler GZip: out");
+ }
+}
+
+function checkRequest(request, data, context) {
+ if (dbg) {
+ print("============== checkRequest: in");
+ }
+ Assert.equal(data, httpbody);
+ Assert.equal(request.QueryInterface(Ci.nsIChannel).contentType, "text/html");
+ httpserver.stop(do_test_finished);
+ run_next_test();
+ if (dbg) {
+ print("============== checkRequest: out");
+ }
+}
diff --git a/netwerk/test/unit/test_resumable_channel.js b/netwerk/test/unit/test_resumable_channel.js
new file mode 100644
index 0000000000..0407aa4983
--- /dev/null
+++ b/netwerk/test/unit/test_resumable_channel.js
@@ -0,0 +1,426 @@
+/* Tests various aspects of nsIResumableChannel in combination with HTTP */
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+ChromeUtils.defineLazyGetter(this, "URL", function () {
+ return "http://localhost:" + httpserver.identity.primaryPort;
+});
+
+var httpserver = null;
+
+const NS_ERROR_ENTITY_CHANGED = 0x804b0020;
+const NS_ERROR_NOT_RESUMABLE = 0x804b0019;
+
+const rangeBody = "Body of the range request handler.\r\n";
+
+function make_channel(url, callback, ctx) {
+ return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true });
+}
+
+function AuthPrompt2() {}
+
+AuthPrompt2.prototype = {
+ user: "guest",
+ pass: "guest",
+
+ QueryInterface: ChromeUtils.generateQI(["nsIAuthPrompt2"]),
+
+ promptAuth: function ap2_promptAuth(channel, level, authInfo) {
+ authInfo.username = this.user;
+ authInfo.password = this.pass;
+ return true;
+ },
+
+ asyncPromptAuth: function ap2_async(chan, cb, ctx, lvl, info) {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
+ },
+};
+
+function Requestor() {}
+
+Requestor.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["nsIInterfaceRequestor"]),
+
+ getInterface: function requestor_gi(iid) {
+ if (iid.equals(Ci.nsIAuthPrompt2)) {
+ // Allow the prompt to store state by caching it here
+ if (!this.prompt2) {
+ this.prompt2 = new AuthPrompt2();
+ }
+ return this.prompt2;
+ }
+
+ throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE);
+ },
+
+ prompt2: null,
+};
+
+function run_test() {
+ dump("*** run_test\n");
+ httpserver = new HttpServer();
+ httpserver.registerPathHandler("/auth", authHandler);
+ httpserver.registerPathHandler("/range", rangeHandler);
+ httpserver.registerPathHandler("/acceptranges", acceptRangesHandler);
+ httpserver.registerPathHandler("/redir", redirHandler);
+
+ var entityID;
+
+ function get_entity_id(request, data, ctx) {
+ dump("*** get_entity_id()\n");
+ Assert.ok(
+ request instanceof Ci.nsIResumableChannel,
+ "must be a resumable channel"
+ );
+ entityID = request.entityID;
+ dump("*** entity id = " + entityID + "\n");
+
+ // Try a non-resumable URL (responds with 200)
+ var chan = make_channel(URL);
+ chan.nsIResumableChannel.resumeAt(1, entityID);
+ chan.asyncOpen(new ChannelListener(try_resume, null, CL_EXPECT_FAILURE));
+ }
+
+ function try_resume(request, data, ctx) {
+ dump("*** try_resume()\n");
+ Assert.equal(request.status, NS_ERROR_NOT_RESUMABLE);
+
+ // Try a successful resume
+ var chan = make_channel(URL + "/range");
+ chan.nsIResumableChannel.resumeAt(1, entityID);
+ chan.asyncOpen(new ChannelListener(try_resume_zero, null));
+ }
+
+ function try_resume_zero(request, data, ctx) {
+ dump("*** try_resume_zero()\n");
+ Assert.ok(request.nsIHttpChannel.requestSucceeded);
+ Assert.equal(data, rangeBody.substring(1));
+
+ // Try a server which doesn't support range requests
+ var chan = make_channel(URL + "/acceptranges");
+ chan.nsIResumableChannel.resumeAt(0, entityID);
+ chan.nsIHttpChannel.setRequestHeader("X-Range-Type", "none", false);
+ chan.asyncOpen(new ChannelListener(try_no_range, null, CL_EXPECT_FAILURE));
+ }
+
+ function try_no_range(request, data, ctx) {
+ dump("*** try_no_range()\n");
+ Assert.ok(request.nsIHttpChannel.requestSucceeded);
+ Assert.equal(request.status, NS_ERROR_NOT_RESUMABLE);
+
+ // Try a server which supports "bytes" range requests
+ var chan = make_channel(URL + "/acceptranges");
+ chan.nsIResumableChannel.resumeAt(0, entityID);
+ chan.nsIHttpChannel.setRequestHeader("X-Range-Type", "bytes", false);
+ chan.asyncOpen(new ChannelListener(try_bytes_range, null));
+ }
+
+ function try_bytes_range(request, data, ctx) {
+ dump("*** try_bytes_range()\n");
+ Assert.ok(request.nsIHttpChannel.requestSucceeded);
+ Assert.equal(data, rangeBody);
+
+ // Try a server which supports "foo" and "bar" range requests
+ var chan = make_channel(URL + "/acceptranges");
+ chan.nsIResumableChannel.resumeAt(0, entityID);
+ chan.nsIHttpChannel.setRequestHeader("X-Range-Type", "foo, bar", false);
+ chan.asyncOpen(
+ new ChannelListener(try_foo_bar_range, null, CL_EXPECT_FAILURE)
+ );
+ }
+
+ function try_foo_bar_range(request, data, ctx) {
+ dump("*** try_foo_bar_range()\n");
+ Assert.ok(request.nsIHttpChannel.requestSucceeded);
+ Assert.equal(request.status, NS_ERROR_NOT_RESUMABLE);
+
+ // Try a server which supports "foobar" range requests
+ var chan = make_channel(URL + "/acceptranges");
+ chan.nsIResumableChannel.resumeAt(0, entityID);
+ chan.nsIHttpChannel.setRequestHeader("X-Range-Type", "foobar", false);
+ chan.asyncOpen(
+ new ChannelListener(try_foobar_range, null, CL_EXPECT_FAILURE)
+ );
+ }
+
+ function try_foobar_range(request, data, ctx) {
+ dump("*** try_foobar_range()\n");
+ Assert.ok(request.nsIHttpChannel.requestSucceeded);
+ Assert.equal(request.status, NS_ERROR_NOT_RESUMABLE);
+
+ // Try a server which supports "bytes" and "foobar" range requests
+ var chan = make_channel(URL + "/acceptranges");
+ chan.nsIResumableChannel.resumeAt(0, entityID);
+ chan.nsIHttpChannel.setRequestHeader(
+ "X-Range-Type",
+ "bytes, foobar",
+ false
+ );
+ chan.asyncOpen(new ChannelListener(try_bytes_foobar_range, null));
+ }
+
+ function try_bytes_foobar_range(request, data, ctx) {
+ dump("*** try_bytes_foobar_range()\n");
+ Assert.ok(request.nsIHttpChannel.requestSucceeded);
+ Assert.equal(data, rangeBody);
+
+ // Try a server which supports "bytesfoo" and "bar" range requests
+ var chan = make_channel(URL + "/acceptranges");
+ chan.nsIResumableChannel.resumeAt(0, entityID);
+ chan.nsIHttpChannel.setRequestHeader(
+ "X-Range-Type",
+ "bytesfoo, bar",
+ false
+ );
+ chan.asyncOpen(
+ new ChannelListener(try_bytesfoo_bar_range, null, CL_EXPECT_FAILURE)
+ );
+ }
+
+ function try_bytesfoo_bar_range(request, data, ctx) {
+ dump("*** try_bytesfoo_bar_range()\n");
+ Assert.ok(request.nsIHttpChannel.requestSucceeded);
+ Assert.equal(request.status, NS_ERROR_NOT_RESUMABLE);
+
+ // Try a server which doesn't send Accept-Ranges header at all
+ var chan = make_channel(URL + "/acceptranges");
+ chan.nsIResumableChannel.resumeAt(0, entityID);
+ chan.asyncOpen(new ChannelListener(try_no_accept_ranges, null));
+ }
+
+ function try_no_accept_ranges(request, data, ctx) {
+ dump("*** try_no_accept_ranges()\n");
+ Assert.ok(request.nsIHttpChannel.requestSucceeded);
+ Assert.equal(data, rangeBody);
+
+ // Try a successful suspend/resume from 0
+ var chan = make_channel(URL + "/range");
+ chan.nsIResumableChannel.resumeAt(0, entityID);
+ chan.asyncOpen(
+ new ChannelListener(
+ try_suspend_resume,
+ null,
+ CL_SUSPEND | CL_EXPECT_3S_DELAY
+ )
+ );
+ }
+
+ function try_suspend_resume(request, data, ctx) {
+ dump("*** try_suspend_resume()\n");
+ Assert.ok(request.nsIHttpChannel.requestSucceeded);
+ Assert.equal(data, rangeBody);
+
+ // Try a successful resume from 0
+ var chan = make_channel(URL + "/range");
+ chan.nsIResumableChannel.resumeAt(0, entityID);
+ chan.asyncOpen(new ChannelListener(success, null));
+ }
+
+ function success(request, data, ctx) {
+ dump("*** success()\n");
+ Assert.ok(request.nsIHttpChannel.requestSucceeded);
+ Assert.equal(data, rangeBody);
+
+ // Authentication (no password; working resume)
+ // (should not give us any data)
+ var chan = make_channel(URL + "/range");
+ chan.nsIResumableChannel.resumeAt(1, entityID);
+ chan.nsIHttpChannel.setRequestHeader("X-Need-Auth", "true", false);
+ chan.asyncOpen(
+ new ChannelListener(test_auth_nopw, null, CL_EXPECT_FAILURE)
+ );
+ }
+
+ function test_auth_nopw(request, data, ctx) {
+ dump("*** test_auth_nopw()\n");
+ Assert.ok(!request.nsIHttpChannel.requestSucceeded);
+ Assert.equal(request.status, NS_ERROR_ENTITY_CHANGED);
+
+ // Authentication + not working resume
+ var chan = make_channel(
+ "http://guest:guest@localhost:" +
+ httpserver.identity.primaryPort +
+ "/auth"
+ );
+ chan.nsIResumableChannel.resumeAt(1, entityID);
+ chan.notificationCallbacks = new Requestor();
+ chan.asyncOpen(new ChannelListener(test_auth, null, CL_EXPECT_FAILURE));
+ }
+ function test_auth(request, data, ctx) {
+ dump("*** test_auth()\n");
+ Assert.equal(request.status, NS_ERROR_NOT_RESUMABLE);
+ Assert.ok(request.nsIHttpChannel.responseStatus < 300);
+
+ // Authentication + working resume
+ var chan = make_channel(
+ "http://guest:guest@localhost:" +
+ httpserver.identity.primaryPort +
+ "/range"
+ );
+ chan.nsIResumableChannel.resumeAt(1, entityID);
+ chan.notificationCallbacks = new Requestor();
+ chan.nsIHttpChannel.setRequestHeader("X-Need-Auth", "true", false);
+ chan.asyncOpen(new ChannelListener(test_auth_resume, null));
+ }
+
+ function test_auth_resume(request, data, ctx) {
+ dump("*** test_auth_resume()\n");
+ Assert.equal(data, rangeBody.substring(1));
+ Assert.ok(request.nsIHttpChannel.requestSucceeded);
+
+ // 404 page (same content length as real content)
+ var chan = make_channel(URL + "/range");
+ chan.nsIResumableChannel.resumeAt(1, entityID);
+ chan.nsIHttpChannel.setRequestHeader("X-Want-404", "true", false);
+ chan.asyncOpen(new ChannelListener(test_404, null, CL_EXPECT_FAILURE));
+ }
+
+ function test_404(request, data, ctx) {
+ dump("*** test_404()\n");
+ Assert.equal(request.status, NS_ERROR_ENTITY_CHANGED);
+ Assert.equal(request.nsIHttpChannel.responseStatus, 404);
+
+ // 416 Requested Range Not Satisfiable
+ var chan = make_channel(URL + "/range");
+ chan.nsIResumableChannel.resumeAt(1000, entityID);
+ chan.asyncOpen(new ChannelListener(test_416, null, CL_EXPECT_FAILURE));
+ }
+
+ function test_416(request, data, ctx) {
+ dump("*** test_416()\n");
+ Assert.equal(request.status, NS_ERROR_ENTITY_CHANGED);
+ Assert.equal(request.nsIHttpChannel.responseStatus, 416);
+
+ // Redirect + successful resume
+ var chan = make_channel(URL + "/redir");
+ chan.nsIHttpChannel.setRequestHeader("X-Redir-To", URL + "/range", false);
+ chan.nsIResumableChannel.resumeAt(1, entityID);
+ chan.asyncOpen(new ChannelListener(test_redir_resume, null));
+ }
+
+ function test_redir_resume(request, data, ctx) {
+ dump("*** test_redir_resume()\n");
+ Assert.ok(request.nsIHttpChannel.requestSucceeded);
+ Assert.equal(data, rangeBody.substring(1));
+ Assert.equal(request.nsIHttpChannel.responseStatus, 206);
+
+ // Redirect + failed resume
+ var chan = make_channel(URL + "/redir");
+ chan.nsIHttpChannel.setRequestHeader("X-Redir-To", URL + "/", false);
+ chan.nsIResumableChannel.resumeAt(1, entityID);
+ chan.asyncOpen(
+ new ChannelListener(test_redir_noresume, null, CL_EXPECT_FAILURE)
+ );
+ }
+
+ function test_redir_noresume(request, data, ctx) {
+ dump("*** test_redir_noresume()\n");
+ Assert.equal(request.status, NS_ERROR_NOT_RESUMABLE);
+
+ httpserver.stop(do_test_finished);
+ }
+
+ httpserver.start(-1);
+ var chan = make_channel(URL + "/range");
+ chan.asyncOpen(new ChannelListener(get_entity_id, null));
+ do_test_pending();
+}
+
+// HANDLERS
+
+function handleAuth(metadata, response) {
+ // btoa("guest:guest"), but that function is not available here
+ var expectedHeader = "Basic Z3Vlc3Q6Z3Vlc3Q=";
+
+ if (
+ metadata.hasHeader("Authorization") &&
+ metadata.getHeader("Authorization") == expectedHeader
+ ) {
+ response.setStatusLine(metadata.httpVersion, 200, "OK, authorized");
+ response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false);
+
+ return true;
+ }
+ // didn't know guest:guest, failure
+ response.setStatusLine(metadata.httpVersion, 401, "Unauthorized");
+ response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false);
+ return false;
+}
+
+// /auth
+function authHandler(metadata, response) {
+ response.setHeader("Content-Type", "text/html", false);
+ var body = handleAuth(metadata, response) ? "success" : "failure";
+ response.bodyOutputStream.write(body, body.length);
+}
+
+// /range
+function rangeHandler(metadata, response) {
+ response.setHeader("Content-Type", "text/html", false);
+
+ if (metadata.hasHeader("X-Need-Auth")) {
+ if (!handleAuth(metadata, response)) {
+ body = "auth failed";
+ response.bodyOutputStream.write(body, body.length);
+ return;
+ }
+ }
+
+ if (metadata.hasHeader("X-Want-404")) {
+ response.setStatusLine(metadata.httpVersion, 404, "Not Found");
+ body = rangeBody;
+ response.bodyOutputStream.write(body, body.length);
+ return;
+ }
+
+ var body = rangeBody;
+
+ if (metadata.hasHeader("Range")) {
+ // Syntax: bytes=[from]-[to] (we don't support multiple ranges)
+ var matches = metadata
+ .getHeader("Range")
+ .match(/^\s*bytes=(\d+)?-(\d+)?\s*$/);
+ var from = matches[1] === undefined ? 0 : matches[1];
+ var to = matches[2] === undefined ? rangeBody.length - 1 : matches[2];
+ if (from >= rangeBody.length) {
+ response.setStatusLine(metadata.httpVersion, 416, "Start pos too high");
+ response.setHeader("Content-Range", "*/" + rangeBody.length, false);
+ return;
+ }
+ body = body.substring(from, to + 1);
+ // always respond to successful range requests with 206
+ response.setStatusLine(metadata.httpVersion, 206, "Partial Content");
+ response.setHeader(
+ "Content-Range",
+ from + "-" + to + "/" + rangeBody.length,
+ false
+ );
+ }
+
+ response.bodyOutputStream.write(body, body.length);
+}
+
+// /acceptranges
+function acceptRangesHandler(metadata, response) {
+ response.setHeader("Content-Type", "text/html", false);
+ if (metadata.hasHeader("X-Range-Type")) {
+ response.setHeader(
+ "Accept-Ranges",
+ metadata.getHeader("X-Range-Type"),
+ false
+ );
+ }
+ response.bodyOutputStream.write(rangeBody, rangeBody.length);
+}
+
+// /redir
+function redirHandler(metadata, response) {
+ response.setStatusLine(metadata.httpVersion, 302, "Found");
+ response.setHeader("Content-Type", "text/html", false);
+ response.setHeader("Location", metadata.getHeader("X-Redir-To"), false);
+ var body = "redirect\r\n";
+ response.bodyOutputStream.write(body, body.length);
+}
diff --git a/netwerk/test/unit/test_resumable_truncate.js b/netwerk/test/unit/test_resumable_truncate.js
new file mode 100644
index 0000000000..cdcab591c2
--- /dev/null
+++ b/netwerk/test/unit/test_resumable_truncate.js
@@ -0,0 +1,95 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+var httpserver = null;
+
+function make_channel(url, callback, ctx) {
+ return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true });
+}
+
+const responseBody = "response body";
+
+function cachedHandler(metadata, response) {
+ var body = responseBody;
+ if (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 = responseBody.slice(from, to + 1);
+ // always respond to successful range requests with 206
+ response.setStatusLine(metadata.httpVersion, 206, "Partial Content");
+ response.setHeader(
+ "Content-Range",
+ from + "-" + to + "/" + responseBody.length,
+ false
+ );
+ }
+
+ response.setHeader("Content-Type", "text/plain", false);
+ response.setHeader("ETag", "Just testing");
+ response.setHeader("Accept-Ranges", "bytes");
+
+ response.bodyOutputStream.write(body, body.length);
+}
+
+function Canceler(continueFn) {
+ this.continueFn = continueFn;
+}
+
+Canceler.prototype = {
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIStreamListener",
+ "nsIRequestObserver",
+ ]),
+
+ onStartRequest(request) {},
+
+ onDataAvailable(request, stream, offset, count) {
+ request.QueryInterface(Ci.nsIChannel).cancel(Cr.NS_BINDING_ABORTED);
+ },
+
+ onStopRequest(request, status) {
+ Assert.equal(status, Cr.NS_BINDING_ABORTED);
+ this.continueFn();
+ },
+};
+
+function finish_test() {
+ httpserver.stop(do_test_finished);
+}
+
+function start_cache_read() {
+ var chan = make_channel(
+ "http://localhost:" + httpserver.identity.primaryPort + "/cached/test.gz"
+ );
+ chan.asyncOpen(new ChannelListener(finish_test, null));
+}
+
+function start_canceler() {
+ var chan = make_channel(
+ "http://localhost:" + httpserver.identity.primaryPort + "/cached/test.gz"
+ );
+ chan.asyncOpen(new Canceler(start_cache_read));
+}
+
+function run_test() {
+ httpserver = new HttpServer();
+ httpserver.registerPathHandler("/cached/test.gz", cachedHandler);
+ httpserver.start(-1);
+
+ var chan = make_channel(
+ "http://localhost:" + httpserver.identity.primaryPort + "/cached/test.gz"
+ );
+ chan.asyncOpen(new ChannelListener(start_canceler, null));
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_retry_0rtt.js b/netwerk/test/unit/test_retry_0rtt.js
new file mode 100644
index 0000000000..e1770ab8a1
--- /dev/null
+++ b/netwerk/test/unit/test_retry_0rtt.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";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+var httpServer = null;
+
+let handlerCallbacks = {};
+
+function listenHandler(metadata, response) {
+ info(metadata.path);
+ handlerCallbacks[metadata.path] = (handlerCallbacks[metadata.path] || 0) + 1;
+}
+
+function handlerCount(path) {
+ return handlerCallbacks[path] || 0;
+}
+
+ChromeUtils.importESModule("resource://gre/modules/AppConstants.sys.mjs");
+
+// Bug 1805371: Tests that require FaultyServer can't currently be built
+// with system NSS.
+add_setup(
+ {
+ skip_if: () => AppConstants.MOZ_SYSTEM_NSS,
+ },
+ async () => {
+ httpServer = new HttpServer();
+ httpServer.registerPrefixHandler("/callback/", listenHandler);
+ httpServer.start(-1);
+
+ registerCleanupFunction(async () => {
+ await httpServer.stop();
+ });
+
+ Services.env.set(
+ "FAULTY_SERVER_CALLBACK_PORT",
+ httpServer.identity.primaryPort
+ );
+ Services.env.set("MOZ_TLS_SERVER_0RTT", "1");
+ await asyncStartTLSTestServer(
+ "FaultyServer",
+ "../../../security/manager/ssl/tests/unit/test_faulty_server"
+ );
+ let nssComponent = Cc["@mozilla.org/psm;1"].getService(Ci.nsINSSComponent);
+ await nssComponent.asyncClearSSLExternalAndInternalSessionCache();
+ }
+);
+
+async function sleep(time) {
+ return new Promise(resolve => {
+ do_timeout(time * 1000, resolve);
+ });
+}
+
+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 => {
+ chan.asyncOpen(
+ new ChannelListener((req, buffer) => resolve([req, buffer]), null, flags)
+ );
+ });
+}
+
+add_task(
+ {
+ skip_if: () => AppConstants.MOZ_SYSTEM_NSS,
+ },
+ async function testRetry0Rtt() {
+ var retryDomains = [
+ "0rtt-alert-bad-mac.example.com",
+ "0rtt-alert-protocol-version.example.com",
+ //"0rtt-alert-unexpected.example.com", // TODO(bug 1753204): uncomment this
+ ];
+
+ Services.prefs.setCharPref("network.dns.localDomains", retryDomains);
+
+ Services.prefs.setBoolPref("network.ssl_tokens_cache_enabled", true);
+
+ for (var i = 0; i < retryDomains.length; i++) {
+ {
+ let countOfEarlyData = handlerCount("/callback/1");
+ let chan = makeChan(`https://${retryDomains[i]}:8443`);
+ let [, buf] = await channelOpenPromise(chan, CL_ALLOW_UNKNOWN_CL);
+ ok(buf);
+ equal(
+ handlerCount("/callback/1"),
+ countOfEarlyData,
+ "no early data sent"
+ );
+ }
+
+ // The server has an anti-replay mechanism that prohibits it from
+ // accepting 0-RTT connections immediately at startup.
+ await sleep(1);
+
+ {
+ let countOfEarlyData = handlerCount("/callback/1");
+ let chan = makeChan(`https://${retryDomains[i]}:8443`);
+ let [, buf] = await channelOpenPromise(chan, CL_ALLOW_UNKNOWN_CL);
+ ok(buf);
+ equal(
+ handlerCount("/callback/1"),
+ countOfEarlyData + 1,
+ "got early data"
+ );
+ }
+ }
+ }
+);
diff --git a/netwerk/test/unit/test_safeoutputstream.js b/netwerk/test/unit/test_safeoutputstream.js
new file mode 100644
index 0000000000..4925394ce4
--- /dev/null
+++ b/netwerk/test/unit/test_safeoutputstream.js
@@ -0,0 +1,70 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 4 -*- */
+/* 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 write_atomic(file, str) {
+ var stream = Cc[
+ "@mozilla.org/network/atomic-file-output-stream;1"
+ ].createInstance(Ci.nsIFileOutputStream);
+ stream.init(file, -1, -1, 0);
+ do {
+ var written = stream.write(str, str.length);
+ if (written == str.length) {
+ break;
+ }
+ str = str.substring(written);
+ } while (1);
+ stream.QueryInterface(Ci.nsISafeOutputStream).finish();
+ stream.close();
+}
+
+function write(file, str) {
+ var stream = Cc[
+ "@mozilla.org/network/safe-file-output-stream;1"
+ ].createInstance(Ci.nsIFileOutputStream);
+ stream.init(file, -1, -1, 0);
+ do {
+ var written = stream.write(str, str.length);
+ if (written == str.length) {
+ break;
+ }
+ str = str.substring(written);
+ } while (1);
+ stream.QueryInterface(Ci.nsISafeOutputStream).finish();
+ stream.close();
+}
+
+function checkFile(file, str) {
+ var stream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(
+ Ci.nsIFileInputStream
+ );
+ stream.init(file, -1, -1, 0);
+
+ var scriptStream = Cc["@mozilla.org/scriptableinputstream;1"].createInstance(
+ Ci.nsIScriptableInputStream
+ );
+ scriptStream.init(stream);
+
+ Assert.equal(scriptStream.read(scriptStream.available()), str);
+ scriptStream.close();
+}
+
+function run_test() {
+ var filename = "\u0913";
+ var file = Services.dirsvc.get("TmpD", Ci.nsIFile);
+ file.append(filename);
+
+ write(file, "First write");
+ checkFile(file, "First write");
+
+ write(file, "Second write");
+ checkFile(file, "Second write");
+
+ write_atomic(file, "First write: Atomic");
+ checkFile(file, "First write: Atomic");
+
+ write_atomic(file, "Second write: Atomic");
+ checkFile(file, "Second write: Atomic");
+}
diff --git a/netwerk/test/unit/test_safeoutputstream_append.js b/netwerk/test/unit/test_safeoutputstream_append.js
new file mode 100644
index 0000000000..9716001bd2
--- /dev/null
+++ b/netwerk/test/unit/test_safeoutputstream_append.js
@@ -0,0 +1,45 @@
+/* atomic-file-output-stream and safe-file-output-stream should throw and
+ * exception if PR_APPEND is explicity specified without PR_TRUNCATE. */
+"use strict";
+
+const PR_WRONLY = 0x02;
+const PR_CREATE_FILE = 0x08;
+const PR_APPEND = 0x10;
+const PR_TRUNCATE = 0x20;
+
+function check_flag(file, contractID, flags, throws) {
+ let stream = Cc[contractID].createInstance(Ci.nsIFileOutputStream);
+
+ if (throws) {
+ /* NS_ERROR_INVALID_ARG is reported as NS_ERROR_ILLEGAL_VALUE, since they
+ * are same value. */
+ Assert.throws(
+ () => stream.init(file, flags, 0o644, 0),
+ /NS_ERROR_ILLEGAL_VALUE/
+ );
+ } else {
+ stream.init(file, flags, 0o644, 0);
+ stream.close();
+ }
+}
+
+function run_test() {
+ let filename = "test.txt";
+ let file = Services.dirsvc.get("TmpD", Ci.nsIFile);
+ file.append(filename);
+
+ let tests = [
+ [PR_WRONLY | PR_CREATE_FILE | PR_APPEND | PR_TRUNCATE, false],
+ [PR_WRONLY | PR_CREATE_FILE | PR_TRUNCATE, false],
+ [PR_WRONLY | PR_CREATE_FILE | PR_APPEND, true],
+ [-1, false],
+ ];
+ for (let contractID of [
+ "@mozilla.org/network/atomic-file-output-stream;1",
+ "@mozilla.org/network/safe-file-output-stream;1",
+ ]) {
+ for (let [flags, throws] of tests) {
+ check_flag(file, contractID, flags, throws);
+ }
+ }
+}
diff --git a/netwerk/test/unit/test_schema_10_migration.js b/netwerk/test/unit/test_schema_10_migration.js
new file mode 100644
index 0000000000..af50c967fd
--- /dev/null
+++ b/netwerk/test/unit/test_schema_10_migration.js
@@ -0,0 +1,181 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test cookie database migration from version 10 (prerelease Gecko 2.0) to the
+// current version, presently 12.
+"use strict";
+
+var test_generator = do_run_test();
+
+function run_test() {
+ do_test_pending();
+ test_generator.next();
+}
+
+function finish_test() {
+ executeSoon(function () {
+ test_generator.return();
+ do_test_finished();
+ });
+}
+
+function* do_run_test() {
+ // Set up a profile.
+ let profile = do_get_profile();
+
+ // Start the cookieservice, to force creation of a database.
+ // Get the sessionCookies to join the initialization in cookie thread
+ Services.cookies.sessionCookies;
+
+ // Close the profile.
+ do_close_profile(test_generator);
+ yield;
+
+ // Remove the cookie file in order to create another database file.
+ do_get_cookie_file(profile).remove(false);
+
+ // Create a schema 10 database.
+ let schema10db = new CookieDatabaseConnection(
+ do_get_cookie_file(profile),
+ 10
+ );
+
+ let now = Date.now() * 1000;
+ let futureExpiry = Math.round(now / 1e6 + 1000);
+ let pastExpiry = Math.round(now / 1e6 - 1000);
+
+ // Populate it, with:
+ // 1) Unexpired, unique cookies.
+ for (let i = 0; i < 20; ++i) {
+ let cookie = new Cookie(
+ "oh" + i,
+ "hai",
+ "foo.com",
+ "/",
+ futureExpiry,
+ now,
+ now + i,
+ false,
+ false,
+ false
+ );
+
+ schema10db.insertCookie(cookie);
+ }
+
+ // 2) Expired, unique cookies.
+ for (let i = 20; i < 40; ++i) {
+ let cookie = new Cookie(
+ "oh" + i,
+ "hai",
+ "bar.com",
+ "/",
+ pastExpiry,
+ now,
+ now + i,
+ false,
+ false,
+ false
+ );
+
+ schema10db.insertCookie(cookie);
+ }
+
+ // 3) Many copies of the same cookie, some of which have expired and
+ // some of which have not.
+ for (let i = 40; i < 45; ++i) {
+ let cookie = new Cookie(
+ "oh",
+ "hai",
+ "baz.com",
+ "/",
+ futureExpiry + i,
+ now,
+ now + i,
+ false,
+ false,
+ false
+ );
+
+ try {
+ schema10db.insertCookie(cookie);
+ } catch (e) {}
+ }
+ for (let i = 45; i < 50; ++i) {
+ let cookie = new Cookie(
+ "oh",
+ "hai",
+ "baz.com",
+ "/",
+ pastExpiry - i,
+ now,
+ now + i,
+ false,
+ false,
+ false
+ );
+
+ try {
+ schema10db.insertCookie(cookie);
+ } catch (e) {}
+ }
+ for (let i = 50; i < 55; ++i) {
+ let cookie = new Cookie(
+ "oh",
+ "hai",
+ "baz.com",
+ "/",
+ futureExpiry - i,
+ now,
+ now + i,
+ false,
+ false,
+ false
+ );
+
+ try {
+ schema10db.insertCookie(cookie);
+ } catch (e) {}
+ }
+ for (let i = 55; i < 60; ++i) {
+ let cookie = new Cookie(
+ "oh",
+ "hai",
+ "baz.com",
+ "/",
+ pastExpiry + i,
+ now,
+ now + i,
+ false,
+ false,
+ false
+ );
+
+ try {
+ schema10db.insertCookie(cookie);
+ } catch (e) {}
+ }
+
+ // Close it.
+ schema10db.close();
+ schema10db = null;
+
+ // Load the database, forcing migration to the current schema version. Then
+ // test the expected set of cookies:
+ do_load_profile();
+
+ // 1) All unexpired, unique cookies exist.
+ Assert.equal(Services.cookies.countCookiesFromHost("foo.com"), 20);
+
+ // 2) All expired, unique cookies exist.
+ Assert.equal(Services.cookies.countCookiesFromHost("bar.com"), 20);
+
+ // 3) Only one cookie remains, and it's the one with the highest expiration
+ // time.
+ Assert.equal(Services.cookies.countCookiesFromHost("baz.com"), 1);
+ let cookies = Services.cookies.getCookiesFromHost("baz.com", {});
+ let cookie = cookies[0];
+ Assert.equal(cookie.expiry, futureExpiry + 40);
+
+ finish_test();
+}
diff --git a/netwerk/test/unit/test_schema_12_migration.js b/netwerk/test/unit/test_schema_12_migration.js
new file mode 100644
index 0000000000..ded67a1214
--- /dev/null
+++ b/netwerk/test/unit/test_schema_12_migration.js
@@ -0,0 +1,181 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test cookie database migration from version 12 to the current version,
+// presently 13, which added the "partitioned" cookie attribute.
+"use strict";
+
+var test_generator = do_run_test();
+
+function run_test() {
+ do_test_pending();
+ test_generator.next();
+}
+
+function finish_test() {
+ executeSoon(function () {
+ test_generator.return();
+ do_test_finished();
+ });
+}
+
+function* do_run_test() {
+ // Set up a profile.
+ let profile = do_get_profile();
+
+ // Start the cookieservice, to force creation of a database.
+ // Get the sessionCookies to join the initialization in cookie thread
+ Services.cookies.sessionCookies;
+
+ // Close the profile.
+ do_close_profile(test_generator);
+ yield;
+
+ // Remove the cookie file in order to create another database file.
+ do_get_cookie_file(profile).remove(false);
+
+ // Create a schema 12 database.
+ let schema12db = new CookieDatabaseConnection(
+ do_get_cookie_file(profile),
+ 12
+ );
+
+ let now = Date.now() * 1000;
+ let futureExpiry = Math.round(now / 1e6 + 1000);
+ let pastExpiry = Math.round(now / 1e6 - 1000);
+
+ // Populate it, with:
+ // 1) Unexpired, unique cookies.
+ for (let i = 0; i < 20; ++i) {
+ let cookie = new Cookie(
+ "oh" + i,
+ "hai",
+ "foo.com",
+ "/",
+ futureExpiry,
+ now,
+ now,
+ false,
+ false,
+ false
+ );
+
+ schema12db.insertCookie(cookie);
+ }
+
+ // 2) Expired, unique cookies.
+ for (let i = 20; i < 40; ++i) {
+ let cookie = new Cookie(
+ "oh" + i,
+ "hai",
+ "bar.com",
+ "/",
+ pastExpiry,
+ now,
+ now,
+ false,
+ false,
+ false
+ );
+
+ schema12db.insertCookie(cookie);
+ }
+
+ // 3) Many copies of the same cookie, some of which have expired and
+ // some of which have not.
+ for (let i = 40; i < 45; ++i) {
+ let cookie = new Cookie(
+ "oh",
+ "hai",
+ "baz.com",
+ "/",
+ futureExpiry + i,
+ now,
+ now,
+ false,
+ false,
+ false
+ );
+
+ try {
+ schema12db.insertCookie(cookie);
+ } catch (e) {}
+ }
+ for (let i = 45; i < 50; ++i) {
+ let cookie = new Cookie(
+ "oh",
+ "hai",
+ "baz.com",
+ "/",
+ pastExpiry - i,
+ now,
+ now,
+ false,
+ false,
+ false
+ );
+
+ try {
+ schema12db.insertCookie(cookie);
+ } catch (e) {}
+ }
+ for (let i = 50; i < 55; ++i) {
+ let cookie = new Cookie(
+ "oh",
+ "hai",
+ "baz.com",
+ "/",
+ futureExpiry - i,
+ now,
+ now,
+ false,
+ false,
+ false
+ );
+
+ try {
+ schema12db.insertCookie(cookie);
+ } catch (e) {}
+ }
+ for (let i = 55; i < 60; ++i) {
+ let cookie = new Cookie(
+ "oh",
+ "hai",
+ "baz.com",
+ "/",
+ pastExpiry + i,
+ now,
+ now,
+ false,
+ false,
+ false
+ );
+
+ try {
+ schema12db.insertCookie(cookie);
+ } catch (e) {}
+ }
+
+ // Close it.
+ schema12db.close();
+ schema12db = null;
+
+ // Load the database, forcing migration to the current schema version. Then
+ // test the expected set of cookies:
+ do_load_profile();
+
+ // 1) All unexpired, unique cookies exist.
+ Assert.equal(Services.cookies.countCookiesFromHost("foo.com"), 20);
+
+ // 2) All expired, unique cookies exist.
+ Assert.equal(Services.cookies.countCookiesFromHost("bar.com"), 20);
+
+ // 3) Only one cookie remains, and it's the one with the highest expiration
+ // time.
+ Assert.equal(Services.cookies.countCookiesFromHost("baz.com"), 1);
+ let cookies = Services.cookies.getCookiesFromHost("baz.com", {});
+ let cookie = cookies[0];
+ Assert.equal(cookie.expiry, futureExpiry + 40);
+
+ finish_test();
+}
diff --git a/netwerk/test/unit/test_schema_13_db.js b/netwerk/test/unit/test_schema_13_db.js
new file mode 100644
index 0000000000..b79d89199b
--- /dev/null
+++ b/netwerk/test/unit/test_schema_13_db.js
@@ -0,0 +1,87 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test cookie database schema 13
+"use strict";
+
+add_task(async function test_schema_13_db() {
+ // Set up a profile.
+ let profile = do_get_profile();
+
+ // Start the cookieservice, to force creation of a database.
+ Services.cookies.sessionCookies;
+
+ // Assert schema 13 cookie db was created
+ Assert.ok(do_get_cookie_file(profile).exists());
+ let dbConnection = Services.storage.openDatabase(do_get_cookie_file(profile));
+ let version = dbConnection.schemaVersion;
+ dbConnection.close();
+ Assert.equal(version, 13);
+
+ // Close the profile.
+ await promise_close_profile();
+
+ // Open CookieDatabaseConnection to manipulate DB without using services.
+ let schema13db = new CookieDatabaseConnection(
+ do_get_cookie_file(profile),
+ 13
+ );
+
+ let now = Date.now() * 1000;
+ let futureExpiry = Math.round(now / 1e6 + 1000);
+
+ let N = 20;
+ // Populate db with N unexpired, unique, partially partitioned cookies.
+ for (let i = 0; i < N; ++i) {
+ let cookie = new Cookie(
+ "test" + i,
+ "Some data",
+ "foo.com",
+ "/",
+ futureExpiry,
+ now,
+ now,
+ false,
+ false,
+ false,
+ false,
+ {},
+ Ci.nsICookie.SAMESITE_NONE,
+ Ci.nsICookie.SAMESITE_NONE,
+ Ci.nsICookie.SCHEME_UNSET,
+ !!(i % 2) // isPartitioned
+ );
+ schema13db.insertCookie(cookie);
+ }
+ schema13db.close();
+ schema13db = null;
+
+ // Reload profile.
+ await promise_load_profile();
+
+ // Assert inserted cookies are in the db and correctly handled by services.
+ Assert.equal(Services.cookies.countCookiesFromHost("foo.com"), N);
+
+ // Open connection to manipulated db
+ dbConnection = Services.storage.openDatabase(do_get_cookie_file(profile));
+ // Check that schema is still correct after profile reload / db opening
+ Assert.equal(dbConnection.schemaVersion, 13);
+
+ // Count cookies with isPartitionedAttributeSet set to 1 (true)
+ let stmt = dbConnection.createStatement(
+ "SELECT COUNT(1) FROM moz_cookies WHERE isPartitionedAttributeSet = 1"
+ );
+ let success = stmt.executeStep();
+ Assert.ok(success);
+
+ // Assert the correct number of partitioned cookies was inserted / read
+ let isPartitionedAttributeSetCount = stmt.getInt32(0);
+ stmt.finalize();
+ Assert.equal(isPartitionedAttributeSetCount, N / 2);
+
+ // Cleanup
+ Services.cookies.removeAll();
+ stmt.finalize();
+ dbConnection.close();
+ do_close_profile();
+});
diff --git a/netwerk/test/unit/test_schema_2_migration.js b/netwerk/test/unit/test_schema_2_migration.js
new file mode 100644
index 0000000000..03588c0130
--- /dev/null
+++ b/netwerk/test/unit/test_schema_2_migration.js
@@ -0,0 +1,303 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test cookie database migration from version 2 (Gecko 1.9.3) to the current
+// version, presently 4 (Gecko 2.0).
+"use strict";
+
+var test_generator = do_run_test();
+
+function run_test() {
+ do_test_pending();
+ test_generator.next();
+}
+
+function finish_test() {
+ executeSoon(function () {
+ test_generator.return();
+ do_test_finished();
+ });
+}
+
+function* do_run_test() {
+ // Set up a profile.
+ let profile = do_get_profile();
+
+ // Start the cookieservice, to force creation of a database.
+ // Get the sessionCookies to join the initialization in cookie thread
+ Services.cookies.sessionCookies;
+
+ // Close the profile.
+ do_close_profile(test_generator);
+ yield;
+
+ // Remove the cookie file in order to create another database file.
+ do_get_cookie_file(profile).remove(false);
+
+ // Create a schema 2 database.
+ let schema2db = new CookieDatabaseConnection(do_get_cookie_file(profile), 2);
+
+ let now = Date.now() * 1000;
+ let futureExpiry = Math.round(now / 1e6 + 1000);
+ let pastExpiry = Math.round(now / 1e6 - 1000);
+
+ // Populate it, with:
+ // 1) Unexpired, unique cookies.
+ for (let i = 0; i < 20; ++i) {
+ let cookie = new Cookie(
+ "oh" + i,
+ "hai",
+ "foo.com",
+ "/",
+ futureExpiry,
+ now,
+ now + i,
+ false,
+ false,
+ false
+ );
+
+ schema2db.insertCookie(cookie);
+ }
+
+ // 2) Expired, unique cookies.
+ for (let i = 20; i < 40; ++i) {
+ let cookie = new Cookie(
+ "oh" + i,
+ "hai",
+ "bar.com",
+ "/",
+ pastExpiry,
+ now,
+ now + i,
+ false,
+ false,
+ false
+ );
+
+ schema2db.insertCookie(cookie);
+ }
+
+ // 3) Many copies of the same cookie, some of which have expired and
+ // some of which have not.
+ for (let i = 40; i < 45; ++i) {
+ let cookie = new Cookie(
+ "oh",
+ "hai",
+ "baz.com",
+ "/",
+ futureExpiry + i,
+ now,
+ now + i,
+ false,
+ false,
+ false
+ );
+
+ schema2db.insertCookie(cookie);
+ }
+ for (let i = 45; i < 50; ++i) {
+ let cookie = new Cookie(
+ "oh",
+ "hai",
+ "baz.com",
+ "/",
+ pastExpiry - i,
+ now,
+ now + i,
+ false,
+ false,
+ false
+ );
+
+ schema2db.insertCookie(cookie);
+ }
+ for (let i = 50; i < 55; ++i) {
+ let cookie = new Cookie(
+ "oh",
+ "hai",
+ "baz.com",
+ "/",
+ futureExpiry - i,
+ now,
+ now + i,
+ false,
+ false,
+ false
+ );
+
+ schema2db.insertCookie(cookie);
+ }
+ for (let i = 55; i < 60; ++i) {
+ let cookie = new Cookie(
+ "oh",
+ "hai",
+ "baz.com",
+ "/",
+ pastExpiry + i,
+ now,
+ now + i,
+ false,
+ false,
+ false
+ );
+
+ schema2db.insertCookie(cookie);
+ }
+
+ // Close it.
+ schema2db.close();
+ schema2db = null;
+
+ // Load the database, forcing migration to the current schema version. Then
+ // test the expected set of cookies:
+ do_load_profile();
+
+ // 1) All unexpired, unique cookies exist.
+ Assert.equal(Services.cookies.countCookiesFromHost("foo.com"), 20);
+
+ // 2) All expired, unique cookies exist.
+ Assert.equal(Services.cookies.countCookiesFromHost("bar.com"), 20);
+
+ // 3) Only one cookie remains, and it's the one with the highest expiration
+ // time.
+ Assert.equal(Services.cookies.countCookiesFromHost("baz.com"), 1);
+ let cookies = Services.cookies.getCookiesFromHost("baz.com", {});
+ let cookie = cookies[0];
+ Assert.equal(cookie.expiry, futureExpiry + 44);
+
+ do_close_profile(test_generator);
+ yield;
+
+ // Open the database so we can execute some more schema 2 statements on it.
+ schema2db = new CookieDatabaseConnection(do_get_cookie_file(profile), 2);
+
+ // Populate it with more cookies.
+ for (let i = 60; i < 80; ++i) {
+ schema2db.insertCookie(
+ new Cookie(
+ "oh" + i,
+ "hai",
+ "foo.com",
+ "/",
+ futureExpiry,
+ now,
+ now + i,
+ false,
+ false,
+ false
+ )
+ );
+ }
+ for (let i = 80; i < 100; ++i) {
+ schema2db.insertCookie(
+ new Cookie(
+ "oh" + i,
+ "hai",
+ "cat.com",
+ "/",
+ futureExpiry,
+ now,
+ now + i,
+ false,
+ false,
+ false
+ )
+ );
+ }
+
+ // Attempt to add a cookie with the same (name, host, path) values as another
+ // cookie. This should succeed since we have a REPLACE clause for conflict on
+ // the unique index.
+ cookie = new Cookie(
+ "oh",
+ "hai",
+ "baz.com",
+ "/",
+ futureExpiry,
+ now,
+ now + 100,
+ false,
+ false,
+ false
+ );
+
+ schema2db.insertCookie(cookie);
+
+ // Check that there is, indeed, a singular cookie for baz.com.
+ Assert.equal(do_count_cookies_in_db(schema2db.db, "baz.com"), 1);
+
+ // Close it.
+ schema2db.close();
+ schema2db = null;
+
+ // Back up the database, so we can test both asynchronous and synchronous
+ // loading separately.
+ let file = do_get_cookie_file(profile);
+ let copy = profile.clone();
+ copy.append("cookies.sqlite.copy");
+ file.copyTo(null, copy.leafName);
+
+ // Load the database asynchronously, forcing a purge of the newly-added
+ // cookies. (Their baseDomain column will be NULL.)
+ do_load_profile(test_generator);
+ yield;
+
+ // Test the expected set of cookies.
+ Assert.equal(Services.cookies.countCookiesFromHost("foo.com"), 40);
+ Assert.equal(Services.cookies.countCookiesFromHost("bar.com"), 20);
+ Assert.equal(Services.cookies.countCookiesFromHost("baz.com"), 1);
+ Assert.equal(Services.cookies.countCookiesFromHost("cat.com"), 20);
+
+ do_close_profile(test_generator);
+ yield;
+
+ // Copy the database back.
+ file.remove(false);
+ copy.copyTo(null, file.leafName);
+
+ // Load the database host-at-a-time.
+ do_load_profile();
+
+ // Test the expected set of cookies.
+ Assert.equal(Services.cookies.countCookiesFromHost("foo.com"), 40);
+ Assert.equal(Services.cookies.countCookiesFromHost("bar.com"), 20);
+ Assert.equal(Services.cookies.countCookiesFromHost("baz.com"), 1);
+ Assert.equal(Services.cookies.countCookiesFromHost("cat.com"), 20);
+
+ do_close_profile(test_generator);
+ yield;
+
+ // Open the database and prove that they were deleted.
+ schema2db = new CookieDatabaseConnection(do_get_cookie_file(profile), 2);
+ Assert.equal(do_count_cookies_in_db(schema2db.db), 81);
+ Assert.equal(do_count_cookies_in_db(schema2db.db, "foo.com"), 40);
+ Assert.equal(do_count_cookies_in_db(schema2db.db, "bar.com"), 20);
+ schema2db.close();
+
+ // Copy the database back.
+ file.remove(false);
+ copy.copyTo(null, file.leafName);
+
+ // Load the database synchronously, in its entirety.
+ do_load_profile();
+ Assert.equal(do_count_cookies(), 81);
+
+ // Test the expected set of cookies.
+ Assert.equal(Services.cookies.countCookiesFromHost("foo.com"), 40);
+ Assert.equal(Services.cookies.countCookiesFromHost("bar.com"), 20);
+ Assert.equal(Services.cookies.countCookiesFromHost("baz.com"), 1);
+ Assert.equal(Services.cookies.countCookiesFromHost("cat.com"), 20);
+
+ do_close_profile(test_generator);
+ yield;
+
+ // Open the database and prove that they were deleted.
+ schema2db = new CookieDatabaseConnection(do_get_cookie_file(profile), 2);
+ Assert.equal(do_count_cookies_in_db(schema2db.db), 81);
+ Assert.equal(do_count_cookies_in_db(schema2db.db, "foo.com"), 40);
+ Assert.equal(do_count_cookies_in_db(schema2db.db, "bar.com"), 20);
+ schema2db.close();
+
+ finish_test();
+}
diff --git a/netwerk/test/unit/test_schema_3_migration.js b/netwerk/test/unit/test_schema_3_migration.js
new file mode 100644
index 0000000000..7b5c639950
--- /dev/null
+++ b/netwerk/test/unit/test_schema_3_migration.js
@@ -0,0 +1,170 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test cookie database migration from version 3 (prerelease Gecko 2.0) to the
+// current version, presently 4 (Gecko 2.0).
+"use strict";
+
+var test_generator = do_run_test();
+
+function run_test() {
+ do_test_pending();
+ test_generator.next();
+}
+
+function finish_test() {
+ executeSoon(function () {
+ test_generator.return();
+ do_test_finished();
+ });
+}
+
+function* do_run_test() {
+ // Set up a profile.
+ let profile = do_get_profile();
+
+ // Start the cookieservice, to force creation of a database.
+ // Get the sessionCookies to join the initialization in cookie thread
+ Services.cookies.sessionCookies;
+
+ // Close the profile.
+ do_close_profile(test_generator);
+ yield;
+
+ // Remove the cookie file in order to create another database file.
+ do_get_cookie_file(profile).remove(false);
+
+ // Create a schema 3 database.
+ let schema3db = new CookieDatabaseConnection(do_get_cookie_file(profile), 3);
+
+ let now = Date.now() * 1000;
+ let futureExpiry = Math.round(now / 1e6 + 1000);
+ let pastExpiry = Math.round(now / 1e6 - 1000);
+
+ // Populate it, with:
+ // 1) Unexpired, unique cookies.
+ for (let i = 0; i < 20; ++i) {
+ let cookie = new Cookie(
+ "oh" + i,
+ "hai",
+ "foo.com",
+ "/",
+ futureExpiry,
+ now,
+ now + i,
+ false,
+ false,
+ false
+ );
+
+ schema3db.insertCookie(cookie);
+ }
+
+ // 2) Expired, unique cookies.
+ for (let i = 20; i < 40; ++i) {
+ let cookie = new Cookie(
+ "oh" + i,
+ "hai",
+ "bar.com",
+ "/",
+ pastExpiry,
+ now,
+ now + i,
+ false,
+ false,
+ false
+ );
+
+ schema3db.insertCookie(cookie);
+ }
+
+ // 3) Many copies of the same cookie, some of which have expired and
+ // some of which have not.
+ for (let i = 40; i < 45; ++i) {
+ let cookie = new Cookie(
+ "oh",
+ "hai",
+ "baz.com",
+ "/",
+ futureExpiry + i,
+ now,
+ now + i,
+ false,
+ false,
+ false
+ );
+
+ schema3db.insertCookie(cookie);
+ }
+ for (let i = 45; i < 50; ++i) {
+ let cookie = new Cookie(
+ "oh",
+ "hai",
+ "baz.com",
+ "/",
+ pastExpiry - i,
+ now,
+ now + i,
+ false,
+ false,
+ false
+ );
+
+ schema3db.insertCookie(cookie);
+ }
+ for (let i = 50; i < 55; ++i) {
+ let cookie = new Cookie(
+ "oh",
+ "hai",
+ "baz.com",
+ "/",
+ futureExpiry - i,
+ now,
+ now + i,
+ false,
+ false,
+ false
+ );
+
+ schema3db.insertCookie(cookie);
+ }
+ for (let i = 55; i < 60; ++i) {
+ let cookie = new Cookie(
+ "oh",
+ "hai",
+ "baz.com",
+ "/",
+ pastExpiry + i,
+ now,
+ now + i,
+ false,
+ false,
+ false
+ );
+
+ schema3db.insertCookie(cookie);
+ }
+
+ // Close it.
+ schema3db.close();
+ schema3db = null;
+
+ // Load the database, forcing migration to the current schema version. Then
+ // test the expected set of cookies:
+ do_load_profile();
+
+ // 1) All unexpired, unique cookies exist.
+ Assert.equal(Services.cookies.countCookiesFromHost("foo.com"), 20);
+
+ // 2) All expired, unique cookies exist.
+ Assert.equal(Services.cookies.countCookiesFromHost("bar.com"), 20);
+
+ // 3) Only one cookie remains, and it's the one with the highest expiration
+ // time.
+ Assert.equal(Services.cookies.countCookiesFromHost("baz.com"), 1);
+ let cookies = Services.cookies.getCookiesFromHost("baz.com", {});
+ let cookie = cookies[0];
+ Assert.equal(cookie.expiry, futureExpiry + 44);
+
+ finish_test();
+}
diff --git a/netwerk/test/unit/test_separate_connections.js b/netwerk/test/unit/test_separate_connections.js
new file mode 100644
index 0000000000..4f5c82ec97
--- /dev/null
+++ b/netwerk/test/unit/test_separate_connections.js
@@ -0,0 +1,104 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+ChromeUtils.defineLazyGetter(this, "URL", function () {
+ return "http://localhost:" + httpserv.identity.primaryPort;
+});
+
+// This unit test ensures each container has its own connection pool.
+// We verify this behavior by opening channels with different userContextId,
+// and their connection info's hash keys should be different.
+
+// In the first round of this test, we record the hash key in each container.
+// In the second round, we check if each container's hash key is consistent
+// and different from other container's hash key.
+
+let httpserv = null;
+let gSecondRoundStarted = false;
+
+function handler(metadata, response) {
+ response.setHeader("Content-Type", "text/plain", false);
+ response.setHeader("Cache-Control", "no-cache", false);
+ response.setStatusLine(metadata.httpVersion, 200, "OK");
+ let body = "0123456789";
+ response.bodyOutputStream.write(body, body.length);
+}
+
+function makeChan(url, userContextId) {
+ let chan = NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true });
+ chan.loadInfo.originAttributes = { userContextId };
+ return chan;
+}
+
+let previousHashKeys = [];
+
+function Listener(userContextId) {
+ this.userContextId = userContextId;
+}
+
+let gTestsRun = 0;
+Listener.prototype = {
+ onStartRequest(request) {
+ request
+ .QueryInterface(Ci.nsIHttpChannel)
+ .QueryInterface(Ci.nsIHttpChannelInternal);
+
+ Assert.equal(
+ request.loadInfo.originAttributes.userContextId,
+ this.userContextId
+ );
+
+ let hashKey = request.connectionInfoHashKey;
+ if (gSecondRoundStarted) {
+ // Compare the hash keys with the previous set ones.
+ // Hash keys should match if and only if their userContextId are the same.
+ for (let userContextId = 0; userContextId < 3; userContextId++) {
+ if (userContextId == this.userContextId) {
+ Assert.equal(hashKey, previousHashKeys[userContextId]);
+ } else {
+ Assert.notEqual(hashKey, previousHashKeys[userContextId]);
+ }
+ }
+ } else {
+ // Set the hash keys in the first round.
+ previousHashKeys[this.userContextId] = hashKey;
+ }
+ },
+ onDataAvailable(request, stream, off, cnt) {
+ read_stream(stream, cnt);
+ },
+ onStopRequest() {
+ gTestsRun++;
+ if (gTestsRun == 3) {
+ gTestsRun = 0;
+ if (gSecondRoundStarted) {
+ // The second round finishes.
+ httpserv.stop(do_test_finished);
+ } else {
+ // The first round finishes. Do the second round.
+ gSecondRoundStarted = true;
+ doTest();
+ }
+ }
+ },
+};
+
+function doTest() {
+ for (let userContextId = 0; userContextId < 3; userContextId++) {
+ let chan = makeChan(URL, userContextId);
+ let listener = new Listener(userContextId);
+ chan.asyncOpen(listener);
+ }
+}
+
+function run_test() {
+ do_test_pending();
+ httpserv = new HttpServer();
+ httpserv.registerPathHandler("/", handler);
+ httpserv.start(-1);
+
+ doTest();
+}
diff --git a/netwerk/test/unit/test_servers.js b/netwerk/test/unit/test_servers.js
new file mode 100644
index 0000000000..1aee369f5c
--- /dev/null
+++ b/netwerk/test/unit/test_servers.js
@@ -0,0 +1,324 @@
+/* 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 */
+/* import-globals-from head_servers.js */
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+function makeChan(uri) {
+ let chan = NetUtil.newChannel({
+ uri,
+ 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]);
+ }
+ chan.asyncOpen(new ChannelListener(finish, null, flags));
+ });
+}
+
+function registerSimplePathHandler(server, path) {
+ return server.registerPathHandler(path, (req, resp) => {
+ resp.writeHead(200);
+ resp.end("done");
+ });
+}
+
+function regiisterServerNamePathHandler(server, path) {
+ return server.registerPathHandler(path, (req, resp) => {
+ resp.writeHead(200);
+ resp.end(global.server_name);
+ });
+}
+
+add_task(async function test_dual_stack() {
+ 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_dualStack(-1);
+
+ let chan = makeChan(`http://127.0.0.1:${httpserv.identity.primaryPort}/`);
+ let [, response] = await channelOpenPromise(chan);
+ Assert.equal(response, content);
+
+ chan = makeChan(`http://[::1]:${httpserv.identity.primaryPort}/`);
+ [, response] = await channelOpenPromise(chan);
+ Assert.equal(response, content);
+ await new Promise(resolve => httpserv.stop(resolve));
+});
+
+add_task(async function test_http() {
+ let server = new NodeHTTPServer();
+ await server.start();
+ registerCleanupFunction(async () => {
+ await server.stop();
+ });
+ let chan = makeChan(`http://localhost:${server.port()}/test`);
+ let req = await new Promise(resolve => {
+ chan.asyncOpen(new ChannelListener(resolve, null, CL_ALLOW_UNKNOWN_CL));
+ });
+ equal(req.status, Cr.NS_OK);
+ equal(req.QueryInterface(Ci.nsIHttpChannel).responseStatus, 404);
+ await registerSimplePathHandler(server, "/test");
+ chan = makeChan(`http://localhost:${server.port()}/test`);
+ req = await new Promise(resolve => {
+ chan.asyncOpen(new ChannelListener(resolve, null, CL_ALLOW_UNKNOWN_CL));
+ });
+ equal(req.status, Cr.NS_OK);
+ equal(req.QueryInterface(Ci.nsIHttpChannel).responseStatus, 200);
+ equal(req.QueryInterface(Ci.nsIHttpChannel).protocolVersion, "http/1.1");
+ equal(req.QueryInterface(Ci.nsIHttpChannelInternal).isProxyUsed, false);
+
+ await server.stop();
+});
+
+add_task(async function test_https() {
+ let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(
+ Ci.nsIX509CertDB
+ );
+ addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u");
+
+ let server = new NodeHTTPSServer();
+ await server.start();
+ registerCleanupFunction(async () => {
+ await server.stop();
+ });
+ let chan = makeChan(`https://localhost:${server.port()}/test`);
+ let req = await new Promise(resolve => {
+ chan.asyncOpen(new ChannelListener(resolve, null, CL_ALLOW_UNKNOWN_CL));
+ });
+ equal(req.status, Cr.NS_OK);
+ equal(req.QueryInterface(Ci.nsIHttpChannel).responseStatus, 404);
+ await registerSimplePathHandler(server, "/test");
+ chan = makeChan(`https://localhost:${server.port()}/test`);
+ req = await new Promise(resolve => {
+ chan.asyncOpen(new ChannelListener(resolve, null, CL_ALLOW_UNKNOWN_CL));
+ });
+ equal(req.status, Cr.NS_OK);
+ equal(req.QueryInterface(Ci.nsIHttpChannel).responseStatus, 200);
+ equal(req.QueryInterface(Ci.nsIHttpChannel).protocolVersion, "http/1.1");
+
+ await server.stop();
+});
+
+add_task(async function test_http2() {
+ let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(
+ Ci.nsIX509CertDB
+ );
+ addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u");
+
+ let server = new NodeHTTP2Server();
+ await server.start();
+ registerCleanupFunction(async () => {
+ await server.stop();
+ });
+ let chan = makeChan(`https://localhost:${server.port()}/test`);
+ let req = await new Promise(resolve => {
+ chan.asyncOpen(new ChannelListener(resolve, null, CL_ALLOW_UNKNOWN_CL));
+ });
+ equal(req.status, Cr.NS_OK);
+ equal(req.QueryInterface(Ci.nsIHttpChannel).responseStatus, 404);
+ await registerSimplePathHandler(server, "/test");
+ chan = makeChan(`https://localhost:${server.port()}/test`);
+ req = await new Promise(resolve => {
+ chan.asyncOpen(new ChannelListener(resolve, null, CL_ALLOW_UNKNOWN_CL));
+ });
+ equal(req.status, Cr.NS_OK);
+ equal(req.QueryInterface(Ci.nsIHttpChannel).responseStatus, 200);
+ equal(req.QueryInterface(Ci.nsIHttpChannel).protocolVersion, "h2");
+
+ await server.stop();
+});
+
+add_task(async function test_http1_proxy() {
+ let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(
+ Ci.nsIX509CertDB
+ );
+ addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u");
+
+ let proxy = new NodeHTTPProxyServer();
+ await proxy.start();
+ registerCleanupFunction(async () => {
+ await proxy.stop();
+ });
+
+ let chan = makeChan(`http://localhost:${proxy.port()}/test`);
+ let req = await new Promise(resolve => {
+ chan.asyncOpen(new ChannelListener(resolve, null, CL_ALLOW_UNKNOWN_CL));
+ });
+ equal(req.status, Cr.NS_OK);
+ equal(req.QueryInterface(Ci.nsIHttpChannel).responseStatus, 405);
+
+ await with_node_servers(
+ [NodeHTTPServer, NodeHTTPSServer, NodeHTTP2Server],
+ async server => {
+ await server.execute(
+ `global.server_name = "${server.constructor.name}";`
+ );
+ await regiisterServerNamePathHandler(server, "/test");
+ let [req1, buff] = await channelOpenPromise(
+ makeChan(`${server.origin()}/test`),
+ CL_ALLOW_UNKNOWN_CL
+ );
+ equal(req1.status, Cr.NS_OK);
+ equal(req1.QueryInterface(Ci.nsIHttpChannel).responseStatus, 200);
+ equal(buff, server.constructor.name);
+ //Bug 1792187: Check if proxy is set to true when a proxy is used.
+ equal(req1.QueryInterface(Ci.nsIHttpChannelInternal).isProxyUsed, true);
+ equal(
+ req1.QueryInterface(Ci.nsIHttpChannel).protocolVersion,
+ server.constructor.name == "NodeHTTP2Server" ? "h2" : "http/1.1"
+ );
+ }
+ );
+
+ await proxy.stop();
+});
+
+add_task(async function test_https_proxy() {
+ let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(
+ Ci.nsIX509CertDB
+ );
+ addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u");
+ addCertFromFile(certdb, "proxy-ca.pem", "CTu,u,u");
+
+ let proxy = new NodeHTTPSProxyServer();
+ await proxy.start();
+ registerCleanupFunction(async () => {
+ await proxy.stop();
+ });
+
+ let chan = makeChan(`https://localhost:${proxy.port()}/test`);
+ let req = await new Promise(resolve => {
+ chan.asyncOpen(new ChannelListener(resolve, null, CL_ALLOW_UNKNOWN_CL));
+ });
+ equal(req.status, Cr.NS_OK);
+ equal(req.QueryInterface(Ci.nsIHttpChannel).responseStatus, 405);
+
+ await with_node_servers(
+ [NodeHTTPServer, NodeHTTPSServer, NodeHTTP2Server],
+ async server => {
+ await server.execute(
+ `global.server_name = "${server.constructor.name}";`
+ );
+ await regiisterServerNamePathHandler(server, "/test");
+
+ let [req1, buff] = await channelOpenPromise(
+ makeChan(`${server.origin()}/test`),
+ CL_ALLOW_UNKNOWN_CL
+ );
+ equal(req1.status, Cr.NS_OK);
+ equal(req1.QueryInterface(Ci.nsIHttpChannel).responseStatus, 200);
+ equal(buff, server.constructor.name);
+ }
+ );
+
+ await proxy.stop();
+});
+
+add_task(async function test_http2_proxy() {
+ let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(
+ Ci.nsIX509CertDB
+ );
+ addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u");
+ addCertFromFile(certdb, "proxy-ca.pem", "CTu,u,u");
+
+ let proxy = new NodeHTTP2ProxyServer();
+ await proxy.start();
+ registerCleanupFunction(async () => {
+ await proxy.stop();
+ });
+
+ let chan = makeChan(`https://localhost:${proxy.port()}/test`);
+ let req = await new Promise(resolve => {
+ chan.asyncOpen(new ChannelListener(resolve, null, CL_ALLOW_UNKNOWN_CL));
+ });
+ equal(req.status, Cr.NS_OK);
+ equal(req.QueryInterface(Ci.nsIHttpChannel).responseStatus, 405);
+
+ await with_node_servers(
+ [NodeHTTPServer, NodeHTTPSServer, NodeHTTP2Server],
+ async server => {
+ await server.execute(
+ `global.server_name = "${server.constructor.name}";`
+ );
+ await regiisterServerNamePathHandler(server, "/test");
+ let [req1, buff] = await channelOpenPromise(
+ makeChan(`${server.origin()}/test`),
+ CL_ALLOW_UNKNOWN_CL
+ );
+ equal(req1.status, Cr.NS_OK);
+ equal(req1.QueryInterface(Ci.nsIHttpChannel).responseStatus, 200);
+ equal(buff, server.constructor.name);
+ }
+ );
+
+ await proxy.stop();
+});
+
+add_task(async function test_proxy_with_redirects() {
+ let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(
+ Ci.nsIX509CertDB
+ );
+ addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u");
+
+ let proxies = [
+ NodeHTTPProxyServer,
+ NodeHTTPSProxyServer,
+ NodeHTTP2ProxyServer,
+ ];
+ for (let p of proxies) {
+ let proxy = new p();
+ await proxy.start();
+ registerCleanupFunction(async () => {
+ await proxy.stop();
+ });
+
+ await with_node_servers(
+ [NodeHTTPServer, NodeHTTPSServer, NodeHTTP2Server],
+ async server => {
+ info(`Testing ${p.name} with ${server.constructor.name}`);
+ await server.execute(
+ `global.server_name = "${server.constructor.name}";`
+ );
+ await server.registerPathHandler("/redirect", (req, resp) => {
+ resp.writeHead(302, {
+ Location: "/test",
+ });
+ resp.end(global.server_name);
+ });
+ await server.registerPathHandler("/test", (req, resp) => {
+ resp.writeHead(200);
+ resp.end(global.server_name);
+ });
+
+ let chan = makeChan(`${server.origin()}/redirect`);
+ let [req, buff] = await channelOpenPromise(chan, CL_ALLOW_UNKNOWN_CL);
+ equal(req.status, Cr.NS_OK);
+ equal(req.QueryInterface(Ci.nsIHttpChannel).responseStatus, 200);
+ equal(buff, server.constructor.name);
+ req.QueryInterface(Ci.nsIProxiedChannel);
+ ok(!!req.proxyInfo);
+ notEqual(req.proxyInfo.type, "direct");
+ }
+ );
+ await proxy.stop();
+ }
+});
diff --git a/netwerk/test/unit/test_signature_extraction.js b/netwerk/test/unit/test_signature_extraction.js
new file mode 100644
index 0000000000..3d9db2bdbf
--- /dev/null
+++ b/netwerk/test/unit/test_signature_extraction.js
@@ -0,0 +1,203 @@
+/* -*- 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 signature extraction using Windows Authenticode APIs of
+ * downloaded files.
+ */
+
+////////////////////////////////////////////////////////////////////////////////
+//// Globals
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ FileTestUtils: "resource://testing-common/FileTestUtils.sys.mjs",
+});
+
+const BackgroundFileSaverOutputStream = Components.Constructor(
+ "@mozilla.org/network/background-file-saver;1?mode=outputstream",
+ "nsIBackgroundFileSaver"
+);
+
+const StringInputStream = Components.Constructor(
+ "@mozilla.org/io/string-input-stream;1",
+ "nsIStringInputStream",
+ "setData"
+);
+
+const TEST_FILE_NAME_1 = "test-backgroundfilesaver-1.txt";
+
+/**
+ * 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);
+}
+
+/**
+ * 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(saver, aTarget) {
+ if (aOnTargetChangeFn) {
+ aOnTargetChangeFn(aTarget);
+ }
+ },
+ onSaveComplete: function BFSO_onSaveComplete(saver, 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, aContext, aStatusCode) {
+ if (Components.isSuccessCode(aStatusCode)) {
+ resolve();
+ } else {
+ reject(new Components.Exception(aStatusCode));
+ }
+ },
+ },
+ null
+ );
+ });
+}
+
+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.");
+ }
+ });
+});
+
+function readFileToString(aFilename) {
+ let f = do_get_file(aFilename);
+ let stream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(
+ Ci.nsIFileInputStream
+ );
+ stream.init(f, -1, 0, 0);
+ let buf = NetUtil.readInputStreamToString(stream, stream.available());
+ return buf;
+}
+
+add_task(async function test_signature() {
+ // Check that we get a signature if the saver is finished on Windows.
+ let destFile = getTempFile(TEST_FILE_NAME_1);
+
+ let data = readFileToString("data/signed_win.exe");
+ let saver = new BackgroundFileSaverOutputStream();
+ let completionPromise = promiseSaverComplete(saver);
+
+ try {
+ saver.signatureInfo;
+ do_throw("Can't get signature before saver is complete.");
+ } catch (ex) {
+ if (ex.result != Cr.NS_ERROR_NOT_AVAILABLE) {
+ throw ex;
+ }
+ }
+
+ saver.enableSignatureInfo();
+ saver.setTarget(destFile, false);
+ await promiseCopyToSaver(data, saver, true);
+
+ saver.finish(Cr.NS_OK);
+ await completionPromise;
+
+ // There's only one Array of certs(raw bytes) in the signature array.
+ Assert.equal(1, saver.signatureInfo.length);
+ let certLists = saver.signatureInfo;
+ Assert.ok(certLists.length === 1);
+
+ // Check that it has 3 certs(raw bytes).
+ let certs = certLists[0];
+ Assert.ok(certs.length === 3);
+
+ const certDB = Cc["@mozilla.org/security/x509certdb;1"].getService(
+ Ci.nsIX509CertDB
+ );
+ let signer = certDB.constructX509(certs[0]);
+ let issuer = certDB.constructX509(certs[1]);
+ let root = certDB.constructX509(certs[2]);
+
+ let organization = "Microsoft Corporation";
+ Assert.equal("Microsoft Corporation", signer.commonName);
+ Assert.equal(organization, signer.organization);
+ Assert.equal("Copyright (c) 2002 Microsoft Corp.", signer.organizationalUnit);
+
+ Assert.equal("Microsoft Code Signing PCA", issuer.commonName);
+ Assert.equal(organization, issuer.organization);
+ Assert.equal("Copyright (c) 2000 Microsoft Corp.", issuer.organizationalUnit);
+
+ Assert.equal("Microsoft Root Authority", root.commonName);
+ Assert.ok(!root.organization);
+ Assert.equal("Copyright (c) 1997 Microsoft Corp.", root.organizationalUnit);
+
+ // Clean up.
+ destFile.remove(false);
+});
+
+add_task(function test_teardown() {
+ gStillRunning = false;
+});
diff --git a/netwerk/test/unit/test_simple.js b/netwerk/test/unit/test_simple.js
new file mode 100644
index 0000000000..52c068f517
--- /dev/null
+++ b/netwerk/test/unit/test_simple.js
@@ -0,0 +1,70 @@
+//
+// Simple HTTP test: fetches page
+//
+
+// Note: sets Cc and Ci variables
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+var httpserver = new HttpServer();
+var testpath = "/simple";
+var httpbody = "0123456789";
+
+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);
+ var channel = setupChannel(testpath);
+ // ChannelListener defined in head_channels.js
+ channel.asyncOpen(new ChannelListener(checkRequest, channel));
+ if (dbg) {
+ print("============== setup_test: out");
+ }
+}
+
+function setupChannel(path) {
+ var chan = NetUtil.newChannel({
+ uri: "http://localhost:" + httpserver.identity.primaryPort + path,
+ loadUsingSystemPrincipal: true,
+ });
+ chan.QueryInterface(Ci.nsIHttpChannel);
+ chan.requestMethod = "GET";
+ return chan;
+}
+
+function serverHandler(metadata, response) {
+ if (dbg) {
+ print("============== serverHandler: in");
+ }
+ response.setHeader("Content-Type", "text/plain", false);
+ response.bodyOutputStream.write(httpbody, httpbody.length);
+ if (dbg) {
+ print("============== serverHandler: out");
+ }
+}
+
+function checkRequest(request, data, context) {
+ if (dbg) {
+ print("============== checkRequest: in");
+ }
+ Assert.equal(data, httpbody);
+ httpserver.stop(do_test_finished);
+ if (dbg) {
+ print("============== checkRequest: out");
+ }
+}
diff --git a/netwerk/test/unit/test_sockettransportsvc_available.js b/netwerk/test/unit/test_sockettransportsvc_available.js
new file mode 100644
index 0000000000..664b6a853d
--- /dev/null
+++ b/netwerk/test/unit/test_sockettransportsvc_available.js
@@ -0,0 +1,11 @@
+"use strict";
+
+function run_test() {
+ try {
+ var sts = Cc["@mozilla.org/network/socket-transport-service;1"].getService(
+ Ci.nsISocketTransportService
+ );
+ } catch (e) {}
+
+ Assert.ok(!!sts);
+}
diff --git a/netwerk/test/unit/test_socks.js b/netwerk/test/unit/test_socks.js
new file mode 100644
index 0000000000..6ce9f4895e
--- /dev/null
+++ b/netwerk/test/unit/test_socks.js
@@ -0,0 +1,520 @@
+"use strict";
+
+var CC = Components.Constructor;
+
+const ServerSocket = CC(
+ "@mozilla.org/network/server-socket;1",
+ "nsIServerSocket",
+ "init"
+);
+const BinaryInputStream = CC(
+ "@mozilla.org/binaryinputstream;1",
+ "nsIBinaryInputStream",
+ "setInputStream"
+);
+const DirectoryService = CC(
+ "@mozilla.org/file/directory_service;1",
+ "nsIProperties"
+);
+const Process = CC("@mozilla.org/process/util;1", "nsIProcess", "init");
+
+const currentThread =
+ Cc["@mozilla.org/thread-manager;1"].getService().currentThread;
+
+var socks_test_server = null;
+var socks_listen_port = -1;
+
+function getAvailableBytes(input) {
+ var len = 0;
+
+ try {
+ len = input.available();
+ } catch (e) {}
+
+ return len;
+}
+
+function runScriptSubprocess(script, args) {
+ var ds = new DirectoryService();
+ var bin = ds.get("XREExeF", Ci.nsIFile);
+ if (!bin.exists()) {
+ do_throw("Can't find xpcshell binary");
+ }
+
+ var file = do_get_file(script);
+ var proc = new Process(bin);
+ var procArgs = [file.path].concat(args);
+
+ proc.run(false, procArgs, procArgs.length);
+
+ return proc;
+}
+
+function buf2ip(buf) {
+ if (buf.length == 16) {
+ var ip =
+ ((buf[0] << 4) | buf[1]).toString(16) +
+ ":" +
+ ((buf[2] << 4) | buf[3]).toString(16) +
+ ":" +
+ ((buf[4] << 4) | buf[5]).toString(16) +
+ ":" +
+ ((buf[6] << 4) | buf[7]).toString(16) +
+ ":" +
+ ((buf[8] << 4) | buf[9]).toString(16) +
+ ":" +
+ ((buf[10] << 4) | buf[11]).toString(16) +
+ ":" +
+ ((buf[12] << 4) | buf[13]).toString(16) +
+ ":" +
+ ((buf[14] << 4) | buf[15]).toString(16);
+ for (var i = 8; i >= 2; i--) {
+ var re = new RegExp("(^|:)(0(:|$)){" + i + "}");
+ var shortip = ip.replace(re, "::");
+ if (shortip != ip) {
+ return shortip;
+ }
+ }
+ return ip;
+ }
+ return buf.join(".");
+}
+
+function buf2int(buf) {
+ var n = 0;
+
+ for (var i in buf) {
+ n |= buf[i] << ((buf.length - i - 1) * 8);
+ }
+
+ return n;
+}
+
+function buf2str(buf) {
+ return String.fromCharCode.apply(null, buf);
+}
+
+const STATE_WAIT_GREETING = 1;
+const STATE_WAIT_SOCKS4_REQUEST = 2;
+const STATE_WAIT_SOCKS4_USERNAME = 3;
+const STATE_WAIT_SOCKS4_HOSTNAME = 4;
+const STATE_WAIT_SOCKS5_GREETING = 5;
+const STATE_WAIT_SOCKS5_REQUEST = 6;
+const STATE_WAIT_PONG = 7;
+const STATE_GOT_PONG = 8;
+
+function SocksClient(server, client_in, client_out) {
+ this.server = server;
+ this.type = "";
+ this.username = "";
+ this.dest_name = "";
+ this.dest_addr = [];
+ this.dest_port = [];
+
+ this.client_in = client_in;
+ this.client_out = client_out;
+ this.inbuf = [];
+ this.outbuf = String();
+ this.state = STATE_WAIT_GREETING;
+ this.waitRead(this.client_in);
+}
+SocksClient.prototype = {
+ onInputStreamReady(input) {
+ var len = getAvailableBytes(input);
+
+ if (len == 0) {
+ print("server: client closed!");
+ Assert.equal(this.state, STATE_GOT_PONG);
+ this.close();
+ this.server.testCompleted(this);
+ return;
+ }
+
+ var bin = new BinaryInputStream(input);
+ var data = bin.readByteArray(len);
+ this.inbuf = this.inbuf.concat(data);
+
+ switch (this.state) {
+ case STATE_WAIT_GREETING:
+ this.checkSocksGreeting();
+ break;
+ case STATE_WAIT_SOCKS4_REQUEST:
+ this.checkSocks4Request();
+ break;
+ case STATE_WAIT_SOCKS4_USERNAME:
+ this.checkSocks4Username();
+ break;
+ case STATE_WAIT_SOCKS4_HOSTNAME:
+ this.checkSocks4Hostname();
+ break;
+ case STATE_WAIT_SOCKS5_GREETING:
+ this.checkSocks5Greeting();
+ break;
+ case STATE_WAIT_SOCKS5_REQUEST:
+ this.checkSocks5Request();
+ break;
+ case STATE_WAIT_PONG:
+ this.checkPong();
+ break;
+ default:
+ do_throw("server: read in invalid state!");
+ }
+
+ this.waitRead(input);
+ },
+
+ onOutputStreamReady(output) {
+ var len = output.write(this.outbuf, this.outbuf.length);
+ if (len != this.outbuf.length) {
+ this.outbuf = this.outbuf.substring(len);
+ this.waitWrite(output);
+ } else {
+ this.outbuf = String();
+ }
+ },
+
+ waitRead(input) {
+ input.asyncWait(this, 0, 0, currentThread);
+ },
+
+ waitWrite(output) {
+ output.asyncWait(this, 0, 0, currentThread);
+ },
+
+ write(buf) {
+ this.outbuf += buf;
+ this.waitWrite(this.client_out);
+ },
+
+ checkSocksGreeting() {
+ if (!this.inbuf.length) {
+ return;
+ }
+
+ if (this.inbuf[0] == 4) {
+ print("server: got socks 4");
+ this.type = "socks4";
+ this.state = STATE_WAIT_SOCKS4_REQUEST;
+ this.checkSocks4Request();
+ } else if (this.inbuf[0] == 5) {
+ print("server: got socks 5");
+ this.type = "socks";
+ this.state = STATE_WAIT_SOCKS5_GREETING;
+ this.checkSocks5Greeting();
+ } else {
+ do_throw("Unknown socks protocol!");
+ }
+ },
+
+ checkSocks4Request() {
+ if (this.inbuf.length < 8) {
+ return;
+ }
+
+ Assert.equal(this.inbuf[1], 0x01);
+
+ this.dest_port = this.inbuf.slice(2, 4);
+ this.dest_addr = this.inbuf.slice(4, 8);
+
+ this.inbuf = this.inbuf.slice(8);
+ this.state = STATE_WAIT_SOCKS4_USERNAME;
+ this.checkSocks4Username();
+ },
+
+ readString() {
+ var i = this.inbuf.indexOf(0);
+ var str = null;
+
+ if (i >= 0) {
+ var buf = this.inbuf.slice(0, i);
+ str = buf2str(buf);
+ this.inbuf = this.inbuf.slice(i + 1);
+ }
+
+ return str;
+ },
+
+ checkSocks4Username() {
+ var str = this.readString();
+
+ if (str == null) {
+ return;
+ }
+
+ this.username = str;
+ if (
+ this.dest_addr[0] == 0 &&
+ this.dest_addr[1] == 0 &&
+ this.dest_addr[2] == 0 &&
+ this.dest_addr[3] != 0
+ ) {
+ this.state = STATE_WAIT_SOCKS4_HOSTNAME;
+ this.checkSocks4Hostname();
+ } else {
+ this.sendSocks4Response();
+ }
+ },
+
+ checkSocks4Hostname() {
+ var str = this.readString();
+
+ if (str == null) {
+ return;
+ }
+
+ this.dest_name = str;
+ this.sendSocks4Response();
+ },
+
+ sendSocks4Response() {
+ this.outbuf = "\x00\x5a\x00\x00\x00\x00\x00\x00";
+ this.sendPing();
+ },
+
+ checkSocks5Greeting() {
+ if (this.inbuf.length < 2) {
+ return;
+ }
+ var nmethods = this.inbuf[1];
+ if (this.inbuf.length < 2 + nmethods) {
+ return;
+ }
+
+ Assert.ok(nmethods >= 1);
+ var methods = this.inbuf.slice(2, 2 + nmethods);
+ Assert.ok(0 in methods);
+
+ this.inbuf = [];
+ this.state = STATE_WAIT_SOCKS5_REQUEST;
+ this.write("\x05\x00");
+ },
+
+ checkSocks5Request() {
+ if (this.inbuf.length < 4) {
+ return;
+ }
+
+ Assert.equal(this.inbuf[0], 0x05);
+ Assert.equal(this.inbuf[1], 0x01);
+ Assert.equal(this.inbuf[2], 0x00);
+
+ var atype = this.inbuf[3];
+ var len;
+ var name = false;
+
+ switch (atype) {
+ case 0x01:
+ len = 4;
+ break;
+ case 0x03:
+ len = this.inbuf[4];
+ name = true;
+ break;
+ case 0x04:
+ len = 16;
+ break;
+ default:
+ do_throw("Unknown address type " + atype);
+ }
+
+ if (name) {
+ if (this.inbuf.length < 4 + len + 1 + 2) {
+ return;
+ }
+
+ let buf = this.inbuf.slice(5, 5 + len);
+ this.dest_name = buf2str(buf);
+ len += 1;
+ } else {
+ if (this.inbuf.length < 4 + len + 2) {
+ return;
+ }
+
+ this.dest_addr = this.inbuf.slice(4, 4 + len);
+ }
+
+ len += 4;
+ this.dest_port = this.inbuf.slice(len, len + 2);
+ this.inbuf = this.inbuf.slice(len + 2);
+ this.sendSocks5Response();
+ },
+
+ sendSocks5Response() {
+ if (this.dest_addr.length == 16) {
+ // send a successful response with the address, [::1]:80
+ this.outbuf +=
+ "\x05\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x80";
+ } else {
+ // send a successful response with the address, 127.0.0.1:80
+ this.outbuf += "\x05\x00\x00\x01\x7f\x00\x00\x01\x00\x80";
+ }
+ this.sendPing();
+ },
+
+ sendPing() {
+ print("server: sending ping");
+ this.state = STATE_WAIT_PONG;
+ this.outbuf += "PING!";
+ this.inbuf = [];
+ this.waitWrite(this.client_out);
+ },
+
+ checkPong() {
+ var pong = buf2str(this.inbuf);
+ Assert.equal(pong, "PONG!");
+ this.state = STATE_GOT_PONG;
+ },
+
+ close() {
+ this.client_in.close();
+ this.client_out.close();
+ },
+};
+
+function SocksTestServer() {
+ this.listener = ServerSocket(-1, true, -1);
+ socks_listen_port = this.listener.port;
+ print("server: listening on", socks_listen_port);
+ this.listener.asyncListen(this);
+ this.test_cases = [];
+ this.client_connections = [];
+ this.client_subprocess = null;
+ // port is used as the ID for test cases
+ this.test_port_id = 8000;
+ this.tests_completed = 0;
+}
+SocksTestServer.prototype = {
+ addTestCase(test) {
+ test.finished = false;
+ test.port = this.test_port_id++;
+ this.test_cases.push(test);
+ },
+
+ pickTest(id) {
+ for (var i in this.test_cases) {
+ var test = this.test_cases[i];
+ if (test.port == id) {
+ this.tests_completed++;
+ return test;
+ }
+ }
+ do_throw("No test case with id " + id);
+ return null;
+ },
+
+ testCompleted(client) {
+ var port_id = buf2int(client.dest_port);
+ var test = this.pickTest(port_id);
+
+ print("server: test finished", test.port);
+ Assert.ok(test != null);
+ Assert.equal(test.expectedType || test.type, client.type);
+ Assert.equal(test.port, port_id);
+
+ if (test.remote_dns) {
+ Assert.equal(test.host, client.dest_name);
+ } else {
+ Assert.equal(test.host, buf2ip(client.dest_addr));
+ }
+
+ if (this.test_cases.length == this.tests_completed) {
+ print("server: all tests completed");
+ this.close();
+ do_test_finished();
+ }
+ },
+
+ runClientSubprocess() {
+ var argv = [];
+
+ // marshaled: socks_ver|server_port|dest_host|dest_port|<remote|local>
+ for (var test of this.test_cases) {
+ var arg =
+ test.type +
+ "|" +
+ String(socks_listen_port) +
+ "|" +
+ test.host +
+ "|" +
+ test.port +
+ "|";
+ if (test.remote_dns) {
+ arg += "remote";
+ } else {
+ arg += "local";
+ }
+ print("server: using test case", arg);
+ argv.push(arg);
+ }
+
+ this.client_subprocess = runScriptSubprocess(
+ "socks_client_subprocess.js",
+ argv
+ );
+ },
+
+ onSocketAccepted(socket, trans) {
+ print("server: got client connection");
+ var input = trans.openInputStream(0, 0, 0);
+ var output = trans.openOutputStream(0, 0, 0);
+ var client = new SocksClient(this, input, output);
+ this.client_connections.push(client);
+ },
+
+ onStopListening(socket) {},
+
+ close() {
+ if (this.client_subprocess) {
+ try {
+ this.client_subprocess.kill();
+ } catch (x) {
+ do_note_exception(x, "Killing subprocess failed");
+ }
+ this.client_subprocess = null;
+ }
+ this.client_connections = [];
+ if (this.listener) {
+ this.listener.close();
+ this.listener = null;
+ }
+ },
+};
+
+function run_test() {
+ socks_test_server = new SocksTestServer();
+
+ socks_test_server.addTestCase({
+ type: "socks4",
+ host: "127.0.0.1",
+ remote_dns: false,
+ });
+ socks_test_server.addTestCase({
+ type: "socks4",
+ host: "12345.xxx",
+ remote_dns: true,
+ });
+ socks_test_server.addTestCase({
+ type: "socks4",
+ expectedType: "socks",
+ host: "::1",
+ remote_dns: false,
+ });
+ socks_test_server.addTestCase({
+ type: "socks",
+ host: "127.0.0.1",
+ remote_dns: false,
+ });
+ socks_test_server.addTestCase({
+ type: "socks",
+ host: "abcdefg.xxx",
+ remote_dns: true,
+ });
+ socks_test_server.addTestCase({
+ type: "socks",
+ host: "::1",
+ remote_dns: false,
+ });
+ socks_test_server.runClientSubprocess();
+
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_speculative_connect.js b/netwerk/test/unit/test_speculative_connect.js
new file mode 100644
index 0000000000..f99ace0d71
--- /dev/null
+++ b/netwerk/test/unit/test_speculative_connect.js
@@ -0,0 +1,382 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 4 -*- */
+/* vim: set ts=4 sts=4 et sw=4 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";
+
+var CC = Components.Constructor;
+const ServerSocket = CC(
+ "@mozilla.org/network/server-socket;1",
+ "nsIServerSocket",
+ "init"
+);
+var serv;
+var ios;
+
+/** Example local IP addresses (literal IP address hostname).
+ *
+ * Note: for IPv6 Unique Local and Link Local, a wider range of addresses is
+ * set aside than those most commonly used. Technically, link local addresses
+ * include those beginning with fe80:: through febf::, although in practise
+ * only fe80:: is used. Necko code blocks speculative connections for the wider
+ * range; hence, this test considers that range too.
+ */
+var localIPv4Literals = [
+ // IPv4 RFC1918 \
+ "10.0.0.1",
+ "10.10.10.10",
+ "10.255.255.255", // 10/8
+ "172.16.0.1",
+ "172.23.172.12",
+ "172.31.255.255", // 172.16/20
+ "192.168.0.1",
+ "192.168.192.168",
+ "192.168.255.255", // 192.168/16
+ // IPv4 Link Local
+ "169.254.0.1",
+ "169.254.192.154",
+ "169.254.255.255", // 169.254/16
+];
+var localIPv6Literals = [
+ // IPv6 Unique Local fc00::/7
+ "fc00::1",
+ "fdfe:dcba:9876:abcd:ef01:2345:6789:abcd",
+ "fdff:ffff:ffff:ffff:ffff:ffff:ffff:ffff",
+ // IPv6 Link Local fe80::/10
+ "fe80::1",
+ "fe80::abcd:ef01:2345:6789",
+ "febf:ffff:ffff:ffff:ffff:ffff:ffff:ffff",
+];
+var localIPLiterals = localIPv4Literals.concat(localIPv6Literals);
+
+/** Test function list and descriptions.
+ */
+var testList = [
+ test_localhost_http_speculative_connect,
+ test_localhost_https_speculative_connect,
+ test_hostnames_resolving_to_local_addresses,
+ test_proxies_with_local_addresses,
+];
+
+var testDescription = [
+ "Expect pass with localhost, http",
+ "Expect pass with localhost, https",
+ "Expect failure with resolved local IPs",
+ "Expect failure for proxies with local IPs",
+];
+
+var testIdx = 0;
+var hostIdx = 0;
+
+/** TestServer
+ *
+ * Implements nsIServerSocket for test_speculative_connect.
+ */
+function TestServer() {
+ this.listener = ServerSocket(-1, true, -1);
+ this.listener.asyncListen(this);
+}
+
+TestServer.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["nsIServerSocket"]),
+ onSocketAccepted(socket, trans) {
+ try {
+ this.listener.close();
+ } catch (e) {}
+ Assert.ok(true);
+ next_test();
+ },
+
+ onStopListening(socket) {},
+};
+
+/** TestFailedStreamCallback
+ *
+ * Implements nsI[Input|Output]StreamCallback for socket layer tests.
+ * Expect failure in all cases
+ */
+function TestFailedStreamCallback(transport, hostname, next) {
+ this.transport = transport;
+ this.hostname = hostname;
+ this.next = next;
+ this.dummyContent = "G";
+ this.closed = false;
+}
+
+TestFailedStreamCallback.prototype = {
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIInputStreamCallback",
+ "nsIOutputStreamCallback",
+ ]),
+ processException(e) {
+ if (this.closed) {
+ return;
+ }
+ do_check_instanceof(e, Ci.nsIException);
+ // A refusal to connect speculatively should throw an error.
+ Assert.equal(e.result, Cr.NS_ERROR_CONNECTION_REFUSED);
+ this.closed = true;
+ this.transport.close(Cr.NS_BINDING_ABORTED);
+ this.next();
+ },
+ onOutputStreamReady(outstream) {
+ info("outputstream handler.");
+ Assert.notEqual(typeof outstream, undefined);
+ try {
+ outstream.write(this.dummyContent, this.dummyContent.length);
+ } catch (e) {
+ this.processException(e);
+ return;
+ }
+ info("no exception on write. Wait for read.");
+ },
+ onInputStreamReady(instream) {
+ info("inputstream handler.");
+ Assert.notEqual(typeof instream, undefined);
+ try {
+ instream.available();
+ } catch (e) {
+ this.processException(e);
+ return;
+ }
+ do_throw("Speculative Connect should have failed for " + this.hostname);
+ this.transport.close(Cr.NS_BINDING_ABORTED);
+ this.next();
+ },
+};
+
+/** test_localhost_http_speculative_connect
+ *
+ * Tests a basic positive case using nsIOService.SpeculativeConnect:
+ * connecting to localhost via http.
+ */
+function test_localhost_http_speculative_connect() {
+ serv = new TestServer();
+ var ssm = Services.scriptSecurityManager;
+ var URI = ios.newURI(
+ "http://localhost:" + serv.listener.port + "/just/a/test"
+ );
+ var principal = ssm.createContentPrincipal(URI, {});
+
+ ios
+ .QueryInterface(Ci.nsISpeculativeConnect)
+ .speculativeConnect(URI, principal, null, false);
+}
+
+/** test_localhost_https_speculative_connect
+ *
+ * Tests a basic positive case using nsIOService.SpeculativeConnect:
+ * connecting to localhost via https.
+ */
+function test_localhost_https_speculative_connect() {
+ serv = new TestServer();
+ var ssm = Services.scriptSecurityManager;
+ var URI = ios.newURI(
+ "https://localhost:" + serv.listener.port + "/just/a/test"
+ );
+ var principal = ssm.createContentPrincipal(URI, {});
+
+ ios
+ .QueryInterface(Ci.nsISpeculativeConnect)
+ .speculativeConnect(URI, principal, null, false);
+}
+
+/* Speculative connections should not be allowed for hosts with local IP
+ * addresses (Bug 853423). That list includes:
+ * -- IPv4 RFC1918 and Link Local Addresses.
+ * -- IPv6 Unique and Link Local Addresses.
+ *
+ * Two tests are required:
+ * 1. Verify IP Literals passed to the SpeculativeConnect API.
+ * 2. Verify hostnames that need to be resolved at the socket layer.
+ */
+
+/** test_hostnames_resolving_to_addresses
+ *
+ * Common test function for resolved hostnames. Takes a list of hosts, a
+ * boolean to determine if the test is expected to succeed or fail, and a
+ * function to call the next test case.
+ */
+function test_hostnames_resolving_to_addresses(host, next) {
+ info(host);
+ var sts = Cc["@mozilla.org/network/socket-transport-service;1"].getService(
+ Ci.nsISocketTransportService
+ );
+ Assert.notEqual(typeof sts, undefined);
+ var transport = sts.createTransport([], host, 80, null, null);
+ Assert.notEqual(typeof transport, undefined);
+
+ transport.connectionFlags = Ci.nsISocketTransport.DISABLE_RFC1918;
+ transport.setTimeout(Ci.nsISocketTransport.TIMEOUT_CONNECT, 1);
+ transport.setTimeout(Ci.nsISocketTransport.TIMEOUT_READ_WRITE, 1);
+ Assert.equal(1, transport.getTimeout(Ci.nsISocketTransport.TIMEOUT_CONNECT));
+
+ var outStream = transport.openOutputStream(
+ Ci.nsITransport.OPEN_UNBUFFERED,
+ 0,
+ 0
+ );
+ var inStream = transport.openInputStream(0, 0, 0);
+ Assert.notEqual(typeof outStream, undefined);
+ Assert.notEqual(typeof inStream, undefined);
+
+ var callback = new TestFailedStreamCallback(transport, host, next);
+ Assert.notEqual(typeof callback, undefined);
+
+ // Need to get main thread pointer to ensure nsSocketTransport::AsyncWait
+ // adds callback to ns*StreamReadyEvent on main thread, and doesn't
+ // addref off the main thread.
+ var gThreadManager = Services.tm;
+ var mainThread = gThreadManager.currentThread;
+
+ try {
+ outStream
+ .QueryInterface(Ci.nsIAsyncOutputStream)
+ .asyncWait(callback, 0, 0, mainThread);
+ inStream
+ .QueryInterface(Ci.nsIAsyncInputStream)
+ .asyncWait(callback, 0, 0, mainThread);
+ } catch (e) {
+ do_throw("asyncWait should not fail!");
+ }
+}
+
+/**
+ * test_hostnames_resolving_to_local_addresses
+ *
+ * Creates an nsISocketTransport and simulates a speculative connect request
+ * for a hostname that resolves to a local IP address.
+ * Runs asynchronously; on test success (i.e. failure to connect), the callback
+ * will call this function again until all hostnames in the test list are done.
+ *
+ * Note: This test also uses an IP literal for the hostname. This should be ok,
+ * as the socket layer will ask for the hostname to be resolved anyway, and DNS
+ * code should return a numerical version of the address internally.
+ */
+function test_hostnames_resolving_to_local_addresses() {
+ if (hostIdx >= localIPLiterals.length) {
+ // No more local IP addresses; move on.
+ next_test();
+ return;
+ }
+ var host = localIPLiterals[hostIdx++];
+ // Test another local IP address when the current one is done.
+ var next = test_hostnames_resolving_to_local_addresses;
+ test_hostnames_resolving_to_addresses(host, next);
+}
+
+/** test_speculative_connect_with_host_list
+ *
+ * Common test function for resolved proxy hosts. Takes a list of hosts, a
+ * boolean to determine if the test is expected to succeed or fail, and a
+ * function to call the next test case.
+ */
+function test_proxies(proxyHost, next) {
+ info("Proxy: " + proxyHost);
+ var sts = Cc["@mozilla.org/network/socket-transport-service;1"].getService(
+ Ci.nsISocketTransportService
+ );
+ Assert.notEqual(typeof sts, undefined);
+ var pps = Cc["@mozilla.org/network/protocol-proxy-service;1"].getService();
+ Assert.notEqual(typeof pps, undefined);
+
+ var proxyInfo = pps.newProxyInfo("http", proxyHost, 8080, "", "", 0, 1, null);
+ Assert.notEqual(typeof proxyInfo, undefined);
+
+ var transport = sts.createTransport([], "dummyHost", 80, proxyInfo, null);
+ Assert.notEqual(typeof transport, undefined);
+
+ transport.connectionFlags = Ci.nsISocketTransport.DISABLE_RFC1918;
+
+ transport.setTimeout(Ci.nsISocketTransport.TIMEOUT_CONNECT, 1);
+ Assert.equal(1, transport.getTimeout(Ci.nsISocketTransport.TIMEOUT_CONNECT));
+ transport.setTimeout(Ci.nsISocketTransport.TIMEOUT_READ_WRITE, 1);
+
+ var outStream = transport.openOutputStream(
+ Ci.nsITransport.OPEN_UNBUFFERED,
+ 0,
+ 0
+ );
+ var inStream = transport.openInputStream(0, 0, 0);
+ Assert.notEqual(typeof outStream, undefined);
+ Assert.notEqual(typeof inStream, undefined);
+
+ var callback = new TestFailedStreamCallback(transport, proxyHost, next);
+ Assert.notEqual(typeof callback, undefined);
+
+ // Need to get main thread pointer to ensure nsSocketTransport::AsyncWait
+ // adds callback to ns*StreamReadyEvent on main thread, and doesn't
+ // addref off the main thread.
+ var gThreadManager = Services.tm;
+ var mainThread = gThreadManager.currentThread;
+
+ try {
+ outStream
+ .QueryInterface(Ci.nsIAsyncOutputStream)
+ .asyncWait(callback, 0, 0, mainThread);
+ inStream
+ .QueryInterface(Ci.nsIAsyncInputStream)
+ .asyncWait(callback, 0, 0, mainThread);
+ } catch (e) {
+ do_throw("asyncWait should not fail!");
+ }
+}
+
+/**
+ * test_proxies_with_local_addresses
+ *
+ * Creates an nsISocketTransport and simulates a speculative connect request
+ * for a proxy that resolves to a local IP address.
+ * Runs asynchronously; on test success (i.e. failure to connect), the callback
+ * will call this function again until all proxies in the test list are done.
+ *
+ * Note: This test also uses an IP literal for the proxy. This should be ok,
+ * as the socket layer will ask for the proxy to be resolved anyway, and DNS
+ * code should return a numerical version of the address internally.
+ */
+function test_proxies_with_local_addresses() {
+ if (hostIdx >= localIPLiterals.length) {
+ // No more local IP addresses; move on.
+ next_test();
+ return;
+ }
+ var host = localIPLiterals[hostIdx++];
+ // Test another local IP address when the current one is done.
+ var next = test_proxies_with_local_addresses;
+ test_proxies(host, next);
+}
+
+/** next_test
+ *
+ * Calls the next test in testList. Each test is responsible for calling this
+ * function when its test cases are complete.
+ */
+function next_test() {
+ if (testIdx >= testList.length) {
+ // No more tests; we're done.
+ do_test_finished();
+ return;
+ }
+ info("SpeculativeConnect: " + testDescription[testIdx]);
+ hostIdx = 0;
+ // Start next test in list.
+ testList[testIdx++]();
+}
+
+/** run_test
+ *
+ * Main entry function for test execution.
+ */
+function run_test() {
+ ios = Services.io;
+
+ Services.prefs.setIntPref("network.http.speculative-parallel-limit", 6);
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("network.http.speculative-parallel-limit");
+ });
+
+ do_test_pending();
+ next_test();
+}
diff --git a/netwerk/test/unit/test_stale-while-revalidate_loop.js b/netwerk/test/unit/test_stale-while-revalidate_loop.js
new file mode 100644
index 0000000000..dc28815119
--- /dev/null
+++ b/netwerk/test/unit/test_stale-while-revalidate_loop.js
@@ -0,0 +1,43 @@
+/*
+
+Tests the Cache-control: stale-while-revalidate response directive.
+
+Loads a HTTPS resource with the stale-while-revalidate and tries to load it
+twice.
+
+*/
+
+"use strict";
+
+function make_channel(url) {
+ return NetUtil.newChannel({
+ uri: url,
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+}
+
+async function get_response(channel, fromCache) {
+ return new Promise(resolve => {
+ channel.asyncOpen(
+ new ChannelListener((request, buffer, ctx, isFromCache) => {
+ resolve(buffer);
+ })
+ );
+ });
+}
+
+add_task(async function () {
+ do_get_profile();
+ const PORT = Services.env.get("MOZHTTP2_PORT");
+ const URI = `https://localhost:${PORT}/stale-while-revalidate-loop-test`;
+
+ let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(
+ Ci.nsIX509CertDB
+ );
+ addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u");
+
+ let response = await get_response(make_channel(URI), false);
+ ok(response == "1", "got response ver 1");
+ response = await get_response(make_channel(URI), false);
+ ok(response == "1", "got response ver 1");
+});
diff --git a/netwerk/test/unit/test_stale-while-revalidate_max-age-0.js b/netwerk/test/unit/test_stale-while-revalidate_max-age-0.js
new file mode 100644
index 0000000000..01bb6639bc
--- /dev/null
+++ b/netwerk/test/unit/test_stale-while-revalidate_max-age-0.js
@@ -0,0 +1,113 @@
+/*
+
+Tests the Cache-control: stale-while-revalidate response directive.
+
+Purpose is to check we perform the background revalidation when max-age=0 but
+the window is set and we hit it.
+
+* Make request #1.
+ - response is from the server and version=1
+ - max-age=0, stale-while-revalidate=9999
+* Switch version of the data on the server and prolong the max-age to not let req #3
+ do a bck reval at the end of the test (prevent leaks/shutdown races.)
+* Make request #2 in 2 seconds (entry should be expired by that time, but fall into
+ the reval window.)
+ - response is from the cache, version=1
+ - a new background request should be made for the data
+* Wait for "http-on-background-revalidation" notifying finish of the background reval.
+* Make request #3.
+ - response is from the cache, version=2
+* Done.
+
+*/
+
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+let max_age;
+let version;
+let generate_response = ver => `response version=${ver}`;
+
+function test_handler(metadata, response) {
+ const originalBody = generate_response(version);
+ response.setHeader("Content-Type", "text/html", false);
+ response.setHeader(
+ "Cache-control",
+ `max-age=${max_age}, stale-while-revalidate=9999`,
+ false
+ );
+ response.setStatusLine(metadata.httpVersion, 200, "OK");
+ response.bodyOutputStream.write(originalBody, originalBody.length);
+}
+
+function make_channel(url) {
+ return NetUtil.newChannel({
+ uri: url,
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+}
+
+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 sleep(time) {
+ return new Promise(resolve => {
+ do_timeout(time * 1000, resolve);
+ });
+}
+
+async function stop_server(httpserver) {
+ return new Promise(resolve => {
+ httpserver.stop(resolve);
+ });
+}
+
+async function background_reval_promise() {
+ return new Promise(resolve => {
+ Services.obs.addObserver(resolve, "http-on-background-revalidation");
+ });
+}
+
+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;
+
+ version = 1;
+ max_age = 0;
+ response = await get_response(make_channel(URI), false);
+ ok(response == generate_response(1), "got response ver 1");
+
+ await sleep(2);
+
+ // must specifically wait for the internal channel to finish the reval to make
+ // the test race-free.
+ let reval_done = background_reval_promise();
+
+ version = 2;
+ max_age = 100;
+ response = await get_response(make_channel(URI), true);
+ ok(response == generate_response(1), "got response ver 1");
+
+ await reval_done;
+
+ response = await get_response(make_channel(URI), true);
+ ok(response == generate_response(2), "got response ver 2");
+
+ await stop_server(httpserver);
+});
diff --git a/netwerk/test/unit/test_stale-while-revalidate_negative.js b/netwerk/test/unit/test_stale-while-revalidate_negative.js
new file mode 100644
index 0000000000..c9ef87e2dd
--- /dev/null
+++ b/netwerk/test/unit/test_stale-while-revalidate_negative.js
@@ -0,0 +1,92 @@
+/*
+
+Tests the Cache-control: stale-while-revalidate response directive.
+
+Purpose is to check we DON'T perform the background revalidation when we make the
+request past the reval window.
+
+* Make request #1.
+ - response is from the server and version=1
+ - max-age=1, stale-while-revalidate=1
+* Switch version of the data on the server.
+* Make request #2 in 3 seconds (entry should be expired by that time and no longer
+ fall into the reval window.)
+ - response is from the server, version=2
+* Done.
+
+*/
+
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+let max_age;
+let version;
+let generate_response = ver => `response version=${ver}`;
+
+function test_handler(metadata, response) {
+ const originalBody = generate_response(version);
+ response.setHeader("Content-Type", "text/html", false);
+ response.setHeader(
+ "Cache-control",
+ `max-age=${max_age}, stale-while-revalidate=1`,
+ false
+ );
+ response.setStatusLine(metadata.httpVersion, 200, "OK");
+ response.bodyOutputStream.write(originalBody, originalBody.length);
+}
+
+function make_channel(url) {
+ return NetUtil.newChannel({
+ uri: url,
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+}
+
+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 sleep(time) {
+ return new Promise(resolve => {
+ do_timeout(time * 1000, 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`;
+
+ let response;
+
+ version = 1;
+ max_age = 1;
+ response = await get_response(make_channel(URI), false);
+ ok(response == generate_response(1), "got response ver 1");
+
+ await sleep(max_age + 1 /* stale window */ + 1 /* to expire the window */);
+
+ version = 2;
+ response = await get_response(make_channel(URI), false);
+ ok(response == generate_response(2), "got response ver 2");
+
+ await stop_server(httpserver);
+});
diff --git a/netwerk/test/unit/test_stale-while-revalidate_positive.js b/netwerk/test/unit/test_stale-while-revalidate_positive.js
new file mode 100644
index 0000000000..51ce6bdd83
--- /dev/null
+++ b/netwerk/test/unit/test_stale-while-revalidate_positive.js
@@ -0,0 +1,113 @@
+/*
+
+Tests the Cache-control: stale-while-revalidate response directive.
+
+Purpose is to check we perform the background revalidation when the window is set
+and we hit it.
+
+* Make request #1.
+ - response is from the server and version=1
+ - max-age=1, stale-while-revalidate=9999
+* Switch version of the data on the server and prolong the max-age to not let req #3
+ do a bck reval at the end of the test (prevent leaks/shutdown races.)
+* Make request #2 in 2 seconds (entry should be expired by that time, but fall into
+ the reval window.)
+ - response is from the cache, version=1
+ - a new background request should be made for the data
+* Wait for "http-on-background-revalidation" notifying finish of the background reval.
+* Make request #3.
+ - response is from the cache, version=2
+* Done.
+
+*/
+
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+let max_age;
+let version;
+let generate_response = ver => `response version=${ver}`;
+
+function test_handler(metadata, response) {
+ const originalBody = generate_response(version);
+ response.setHeader("Content-Type", "text/html", false);
+ response.setHeader(
+ "Cache-control",
+ `max-age=${max_age}, stale-while-revalidate=9999`,
+ false
+ );
+ response.setStatusLine(metadata.httpVersion, 200, "OK");
+ response.bodyOutputStream.write(originalBody, originalBody.length);
+}
+
+function make_channel(url) {
+ return NetUtil.newChannel({
+ uri: url,
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+}
+
+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 sleep(time) {
+ return new Promise(resolve => {
+ do_timeout(time * 1000, resolve);
+ });
+}
+
+async function stop_server(httpserver) {
+ return new Promise(resolve => {
+ httpserver.stop(resolve);
+ });
+}
+
+async function background_reval_promise() {
+ return new Promise(resolve => {
+ Services.obs.addObserver(resolve, "http-on-background-revalidation");
+ });
+}
+
+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;
+
+ version = 1;
+ max_age = 1;
+ response = await get_response(make_channel(URI), false);
+ ok(response == generate_response(1), "got response ver 1");
+
+ await sleep(max_age + 1);
+
+ // must specifically wait for the internal channel to finish the reval to make
+ // the test race-free.
+ let reval_done = background_reval_promise();
+
+ version = 2;
+ max_age = 100;
+ response = await get_response(make_channel(URI), true);
+ ok(response == generate_response(1), "got response ver 1");
+
+ await reval_done;
+
+ response = await get_response(make_channel(URI), true);
+ ok(response == generate_response(2), "got response ver 2");
+
+ await stop_server(httpserver);
+});
diff --git a/netwerk/test/unit/test_standardurl.js b/netwerk/test/unit/test_standardurl.js
new file mode 100644
index 0000000000..cf3f736929
--- /dev/null
+++ b/netwerk/test/unit/test_standardurl.js
@@ -0,0 +1,1053 @@
+"use strict";
+
+const gPrefs = Services.prefs;
+
+function symmetricEquality(expect, a, b) {
+ /* Use if/else instead of |do_check_eq(expect, a.spec == b.spec)| so
+ that we get the specs output on the console if the check fails.
+ */
+ if (expect) {
+ /* Check all the sub-pieces too, since that can help with
+ debugging cases when equals() returns something unexpected */
+ /* We don't check port in the loop, because it can be defaulted in
+ some cases. */
+ [
+ "spec",
+ "prePath",
+ "scheme",
+ "userPass",
+ "username",
+ "password",
+ "hostPort",
+ "host",
+ "pathQueryRef",
+ "filePath",
+ "query",
+ "ref",
+ "directory",
+ "fileName",
+ "fileBaseName",
+ "fileExtension",
+ ].map(function (prop) {
+ dump("Testing '" + prop + "'\n");
+ Assert.equal(a[prop], b[prop]);
+ });
+ } else {
+ Assert.notEqual(a.spec, b.spec);
+ }
+ Assert.equal(expect, a.equals(b));
+ Assert.equal(expect, b.equals(a));
+}
+
+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 pairToURLs(pair) {
+ Assert.equal(pair.length, 2);
+ return pair.map(stringToURL);
+}
+
+add_test(function test_setEmptyPath() {
+ var pairs = [
+ ["http://example.com", "http://example.com/tests/dom/tests"],
+ ["http://example.com:80", "http://example.com/tests/dom/tests"],
+ ["http://example.com:80/", "http://example.com/tests/dom/test"],
+ ["http://example.com/", "http://example.com/tests/dom/tests"],
+ ["http://example.com/a", "http://example.com/tests/dom/tests"],
+ ["http://example.com:80/a", "http://example.com/tests/dom/tests"],
+ ].map(pairToURLs);
+
+ for (var [provided, target] of pairs) {
+ symmetricEquality(false, target, provided);
+
+ provided = provided.mutate().setPathQueryRef("").finalize();
+ target = target.mutate().setPathQueryRef("").finalize();
+
+ Assert.equal(provided.spec, target.spec);
+ symmetricEquality(true, target, provided);
+ }
+ run_next_test();
+});
+
+add_test(function test_setQuery() {
+ var pairs = [
+ ["http://example.com", "http://example.com/?foo"],
+ ["http://example.com/bar", "http://example.com/bar?foo"],
+ ["http://example.com#bar", "http://example.com/?foo#bar"],
+ ["http://example.com/#bar", "http://example.com/?foo#bar"],
+ ["http://example.com/?longerthanfoo#bar", "http://example.com/?foo#bar"],
+ ["http://example.com/?longerthanfoo", "http://example.com/?foo"],
+ /* And one that's nonempty but shorter than "foo" */
+ ["http://example.com/?f#bar", "http://example.com/?foo#bar"],
+ ["http://example.com/?f", "http://example.com/?foo"],
+ ].map(pairToURLs);
+
+ for (var [provided, target] of pairs) {
+ symmetricEquality(false, provided, target);
+
+ provided = provided
+ .mutate()
+ .setQuery("foo")
+ .finalize()
+ .QueryInterface(Ci.nsIURL);
+
+ Assert.equal(provided.spec, target.spec);
+ symmetricEquality(true, provided, target);
+ }
+
+ [provided, target] = [
+ "http://example.com/#",
+ "http://example.com/?foo#bar",
+ ].map(stringToURL);
+ symmetricEquality(false, provided, target);
+ provided = provided
+ .mutate()
+ .setQuery("foo")
+ .finalize()
+ .QueryInterface(Ci.nsIURL);
+ symmetricEquality(false, provided, target);
+
+ var newProvided = Services.io
+ .newURI("#bar", null, provided)
+ .QueryInterface(Ci.nsIURL);
+
+ Assert.equal(newProvided.spec, target.spec);
+ symmetricEquality(true, newProvided, target);
+ run_next_test();
+});
+
+add_test(function test_setRef() {
+ var tests = [
+ ["http://example.com", "", "http://example.com/"],
+ ["http://example.com:80", "", "http://example.com:80/"],
+ ["http://example.com:80/", "", "http://example.com:80/"],
+ ["http://example.com/", "", "http://example.com/"],
+ ["http://example.com/a", "", "http://example.com/a"],
+ ["http://example.com:80/a", "", "http://example.com:80/a"],
+
+ ["http://example.com", "x", "http://example.com/#x"],
+ ["http://example.com:80", "x", "http://example.com:80/#x"],
+ ["http://example.com:80/", "x", "http://example.com:80/#x"],
+ ["http://example.com/", "x", "http://example.com/#x"],
+ ["http://example.com/a", "x", "http://example.com/a#x"],
+ ["http://example.com:80/a", "x", "http://example.com:80/a#x"],
+
+ ["http://example.com", "xx", "http://example.com/#xx"],
+ ["http://example.com:80", "xx", "http://example.com:80/#xx"],
+ ["http://example.com:80/", "xx", "http://example.com:80/#xx"],
+ ["http://example.com/", "xx", "http://example.com/#xx"],
+ ["http://example.com/a", "xx", "http://example.com/a#xx"],
+ ["http://example.com:80/a", "xx", "http://example.com:80/a#xx"],
+
+ [
+ "http://example.com",
+ "xxxxxxxxxxxxxx",
+ "http://example.com/#xxxxxxxxxxxxxx",
+ ],
+ [
+ "http://example.com:80",
+ "xxxxxxxxxxxxxx",
+ "http://example.com:80/#xxxxxxxxxxxxxx",
+ ],
+ [
+ "http://example.com:80/",
+ "xxxxxxxxxxxxxx",
+ "http://example.com:80/#xxxxxxxxxxxxxx",
+ ],
+ [
+ "http://example.com/",
+ "xxxxxxxxxxxxxx",
+ "http://example.com/#xxxxxxxxxxxxxx",
+ ],
+ [
+ "http://example.com/a",
+ "xxxxxxxxxxxxxx",
+ "http://example.com/a#xxxxxxxxxxxxxx",
+ ],
+ [
+ "http://example.com:80/a",
+ "xxxxxxxxxxxxxx",
+ "http://example.com:80/a#xxxxxxxxxxxxxx",
+ ],
+ ];
+
+ for (var [before, ref, result] of tests) {
+ /* Test1: starting with empty ref */
+ var a = stringToURL(before);
+ a = a.mutate().setRef(ref).finalize().QueryInterface(Ci.nsIURL);
+ var b = stringToURL(result);
+
+ Assert.equal(a.spec, b.spec);
+ Assert.equal(ref, b.ref);
+ symmetricEquality(true, a, b);
+
+ /* Test2: starting with non-empty */
+ a = a.mutate().setRef("yyyy").finalize().QueryInterface(Ci.nsIURL);
+ var c = stringToURL(before);
+ c = c.mutate().setRef("yyyy").finalize().QueryInterface(Ci.nsIURL);
+ symmetricEquality(true, a, c);
+
+ /* Test3: reset the ref */
+ a = a.mutate().setRef("").finalize().QueryInterface(Ci.nsIURL);
+ symmetricEquality(true, a, stringToURL(before));
+
+ /* Test4: verify again after reset */
+ a = a.mutate().setRef(ref).finalize().QueryInterface(Ci.nsIURL);
+ symmetricEquality(true, a, b);
+ }
+ run_next_test();
+});
+
+// Bug 960014 - Make nsStandardURL::SetHost less magical around IPv6
+add_test(function test_ipv6() {
+ var url = stringToURL("http://example.com");
+ url = url.mutate().setHost("[2001::1]").finalize();
+ Assert.equal(url.host, "2001::1");
+
+ url = stringToURL("http://example.com");
+ url = url.mutate().setHostPort("[2001::1]:30").finalize();
+ Assert.equal(url.host, "2001::1");
+ Assert.equal(url.port, 30);
+ Assert.equal(url.hostPort, "[2001::1]:30");
+
+ url = stringToURL("http://example.com");
+ url = url.mutate().setHostPort("2001:1").finalize();
+ Assert.equal(url.host, "0.0.7.209");
+ Assert.equal(url.port, 1);
+ Assert.equal(url.hostPort, "0.0.7.209:1");
+ run_next_test();
+});
+
+add_test(function test_ipv6_fail() {
+ var url = stringToURL("http://example.com");
+
+ Assert.throws(
+ () => {
+ url = url.mutate().setHost("2001::1").finalize();
+ },
+ /NS_ERROR_MALFORMED_URI/,
+ "missing brackets"
+ );
+ Assert.throws(
+ () => {
+ url = url.mutate().setHost("[2001::1]:20").finalize();
+ },
+ /NS_ERROR_MALFORMED_URI/,
+ "url.host with port"
+ );
+ Assert.throws(
+ () => {
+ url = url.mutate().setHost("[2001::1").finalize();
+ },
+ /NS_ERROR_MALFORMED_URI/,
+ "missing last bracket"
+ );
+ Assert.throws(
+ () => {
+ url = url.mutate().setHost("2001::1]").finalize();
+ },
+ /NS_ERROR_MALFORMED_URI/,
+ "missing first bracket"
+ );
+ Assert.throws(
+ () => {
+ url = url.mutate().setHost("2001[::1]").finalize();
+ },
+ /NS_ERROR_MALFORMED_URI/,
+ "bad bracket position"
+ );
+ Assert.throws(
+ () => {
+ url = url.mutate().setHost("[]").finalize();
+ },
+ /NS_ERROR_MALFORMED_URI/,
+ "empty IPv6 address"
+ );
+ Assert.throws(
+ () => {
+ url = url.mutate().setHost("[hello]").finalize();
+ },
+ /NS_ERROR_MALFORMED_URI/,
+ "bad IPv6 address"
+ );
+ Assert.throws(
+ () => {
+ url = url.mutate().setHost("[192.168.1.1]").finalize();
+ },
+ /NS_ERROR_MALFORMED_URI/,
+ "bad IPv6 address"
+ );
+ Assert.throws(
+ () => {
+ url = url.mutate().setHostPort("2001::1").finalize();
+ },
+ /NS_ERROR_MALFORMED_URI/,
+ "missing brackets"
+ );
+ Assert.throws(
+ () => {
+ url = url.mutate().setHostPort("[2001::1]30").finalize();
+ },
+ /NS_ERROR_MALFORMED_URI/,
+ "missing : after IP"
+ );
+ Assert.throws(
+ () => {
+ url = url.mutate().setHostPort("[2001:1]").finalize();
+ },
+ /NS_ERROR_MALFORMED_URI/,
+ "bad IPv6 address"
+ );
+ Assert.throws(
+ () => {
+ url = url.mutate().setHostPort("[2001:1]10").finalize();
+ },
+ /NS_ERROR_MALFORMED_URI/,
+ "bad IPv6 address"
+ );
+ Assert.throws(
+ () => {
+ url = url.mutate().setHostPort("[2001:1]10:20").finalize();
+ },
+ /NS_ERROR_MALFORMED_URI/,
+ "bad IPv6 address"
+ );
+ Assert.throws(
+ () => {
+ url = url.mutate().setHostPort("[2001:1]:10:20").finalize();
+ },
+ /NS_ERROR_MALFORMED_URI/,
+ "bad IPv6 address"
+ );
+ Assert.throws(
+ () => {
+ url = url.mutate().setHostPort("[2001:1").finalize();
+ },
+ /NS_ERROR_MALFORMED_URI/,
+ "bad IPv6 address"
+ );
+ Assert.throws(
+ () => {
+ url = url.mutate().setHostPort("2001]:1").finalize();
+ },
+ /NS_ERROR_MALFORMED_URI/,
+ "bad IPv6 address"
+ );
+ Assert.throws(
+ () => {
+ url = url.mutate().setHostPort("2001:1]").finalize();
+ },
+ /NS_ERROR_MALFORMED_URI/,
+ "bad IPv6 address"
+ );
+ Assert.throws(
+ () => {
+ url = url.mutate().setHostPort("").finalize();
+ },
+ /NS_ERROR_UNEXPECTED/,
+ "Empty hostPort should fail"
+ );
+
+ // These checks used to fail, but now don't (see bug 1433958 comment 57)
+ url = url.mutate().setHostPort("[2001::1]:").finalize();
+ Assert.equal(url.spec, "http://[2001::1]/");
+ url = url.mutate().setHostPort("[2002::1]:bad").finalize();
+ Assert.equal(url.spec, "http://[2002::1]/");
+
+ run_next_test();
+});
+
+add_test(function test_clearedSpec() {
+ var url = stringToURL("http://example.com/path");
+ Assert.throws(
+ () => {
+ url = url.mutate().setSpec("http: example").finalize();
+ },
+ /NS_ERROR_MALFORMED_URI/,
+ "set bad spec"
+ );
+ Assert.throws(
+ () => {
+ url = url.mutate().setSpec("").finalize();
+ },
+ /NS_ERROR_MALFORMED_URI/,
+ "set empty spec"
+ );
+ Assert.equal(url.spec, "http://example.com/path");
+ url = url
+ .mutate()
+ .setHost("allizom.org")
+ .finalize()
+ .QueryInterface(Ci.nsIURL);
+
+ var ref = stringToURL("http://allizom.org/path");
+ symmetricEquality(true, url, ref);
+ run_next_test();
+});
+
+add_test(function test_escapeBrackets() {
+ // Query
+ var url = stringToURL("http://example.com/?a[x]=1");
+ Assert.equal(url.spec, "http://example.com/?a[x]=1");
+
+ url = stringToURL("http://example.com/?a%5Bx%5D=1");
+ Assert.equal(url.spec, "http://example.com/?a%5Bx%5D=1");
+
+ url = stringToURL("http://[2001::1]/?a[x]=1");
+ Assert.equal(url.spec, "http://[2001::1]/?a[x]=1");
+
+ url = stringToURL("http://[2001::1]/?a%5Bx%5D=1");
+ Assert.equal(url.spec, "http://[2001::1]/?a%5Bx%5D=1");
+
+ // Path
+ url = stringToURL("http://example.com/brackets[x]/test");
+ Assert.equal(url.spec, "http://example.com/brackets[x]/test");
+
+ url = stringToURL("http://example.com/a%5Bx%5D/test");
+ Assert.equal(url.spec, "http://example.com/a%5Bx%5D/test");
+ run_next_test();
+});
+
+add_test(function test_escapeQuote() {
+ var url = stringToURL("http://example.com/#'");
+ Assert.equal(url.spec, "http://example.com/#'");
+ Assert.equal(url.ref, "'");
+ url = url.mutate().setRef("test'test").finalize();
+ Assert.equal(url.spec, "http://example.com/#test'test");
+ Assert.equal(url.ref, "test'test");
+ run_next_test();
+});
+
+add_test(function test_apostropheEncoding() {
+ // For now, single quote is escaped everywhere _except_ the path.
+ // This policy is controlled by the bitmask in nsEscape.cpp::EscapeChars[]
+ var url = stringToURL("http://example.com/dir'/file'.ext'");
+ Assert.equal(url.spec, "http://example.com/dir'/file'.ext'");
+ run_next_test();
+});
+
+add_test(function test_accentEncoding() {
+ var url = stringToURL("http://example.com/?hello=`");
+ Assert.equal(url.spec, "http://example.com/?hello=`");
+ Assert.equal(url.query, "hello=`");
+
+ url = stringToURL("http://example.com/?hello=%2C");
+ Assert.equal(url.spec, "http://example.com/?hello=%2C");
+ Assert.equal(url.query, "hello=%2C");
+ run_next_test();
+});
+
+add_test(
+ { skip_if: () => AppConstants.MOZ_APP_NAME == "thunderbird" },
+ function test_percentDecoding() {
+ var url = stringToURL("http://%70%61%73%74%65%62%69%6E.com");
+ Assert.equal(url.spec, "http://pastebin.com/");
+
+ // Disallowed hostname characters are rejected even when percent encoded
+ Assert.throws(
+ () => {
+ url = stringToURL("http://example.com%0a%23.google.com/");
+ },
+ /NS_ERROR_MALFORMED_URI/,
+ "invalid characters are not allowed"
+ );
+ run_next_test();
+ }
+);
+
+add_test(function test_hugeStringThrows() {
+ let prefs = Services.prefs;
+ let maxLen = prefs.getIntPref("network.standard-url.max-length");
+ let url = stringToURL("http://test:test@example.com");
+
+ let hugeString = new Array(maxLen + 1).fill("a").join("");
+ let setters = [
+ { method: "setSpec", qi: Ci.nsIURIMutator },
+ { method: "setUsername", qi: Ci.nsIURIMutator },
+ { method: "setPassword", qi: Ci.nsIURIMutator },
+ { method: "setFilePath", qi: Ci.nsIURIMutator },
+ { method: "setHostPort", qi: Ci.nsIURIMutator },
+ { method: "setHost", qi: Ci.nsIURIMutator },
+ { method: "setUserPass", qi: Ci.nsIURIMutator },
+ { method: "setPathQueryRef", qi: Ci.nsIURIMutator },
+ { method: "setQuery", qi: Ci.nsIURIMutator },
+ { method: "setRef", qi: Ci.nsIURIMutator },
+ { method: "setScheme", qi: Ci.nsIURIMutator },
+ { method: "setFileName", qi: Ci.nsIURLMutator },
+ { method: "setFileExtension", qi: Ci.nsIURLMutator },
+ { method: "setFileBaseName", qi: Ci.nsIURLMutator },
+ ];
+
+ for (let prop of setters) {
+ Assert.throws(
+ () =>
+ (url = url
+ .mutate()
+ .QueryInterface(prop.qi)
+ [prop.method](hugeString)
+ .finalize()),
+ /NS_ERROR_MALFORMED_URI/,
+ `Passing a huge string to "${prop.method}" should throw`
+ );
+ }
+
+ run_next_test();
+});
+
+add_test(function test_filterWhitespace() {
+ let url = stringToURL(
+ " \r\n\th\nt\rt\tp://ex\r\n\tample.com/path\r\n\t/\r\n\tto the/fil\r\n\te.e\r\n\txt?que\r\n\try#ha\r\n\tsh \r\n\t "
+ );
+ Assert.equal(
+ url.spec,
+ "http://example.com/path/to%20the/file.ext?query#hash"
+ );
+
+ // These setters should filter \r\n\t.
+ url = stringToURL("http://test.com/path?query#hash");
+ url = url.mutate().setFilePath("pa\r\n\tth").finalize();
+ Assert.equal(url.spec, "http://test.com/path?query#hash");
+ url = url.mutate().setQuery("que\r\n\try").finalize();
+ Assert.equal(url.spec, "http://test.com/path?query#hash");
+ url = url.mutate().setRef("ha\r\n\tsh").finalize();
+ Assert.equal(url.spec, "http://test.com/path?query#hash");
+ url = url
+ .mutate()
+ .QueryInterface(Ci.nsIURLMutator)
+ .setFileName("fi\r\n\tle.name")
+ .finalize();
+ Assert.equal(url.spec, "http://test.com/fi%0D%0A%09le.name?query#hash");
+
+ run_next_test();
+});
+
+add_test(function test_backslashReplacement() {
+ var url = stringToURL(
+ "http:\\\\test.com\\path/to\\file?query\\backslash#hash\\"
+ );
+ Assert.equal(
+ url.spec,
+ "http://test.com/path/to/file?query\\backslash#hash\\"
+ );
+
+ url = stringToURL("http:\\\\test.com\\example.org/path\\to/file");
+ Assert.equal(url.spec, "http://test.com/example.org/path/to/file");
+ Assert.equal(url.host, "test.com");
+ Assert.equal(url.pathQueryRef, "/example.org/path/to/file");
+
+ run_next_test();
+});
+
+add_test(function test_authority_host() {
+ Assert.throws(
+ () => {
+ stringToURL("http:");
+ },
+ /NS_ERROR_MALFORMED_URI/,
+ "TYPE_AUTHORITY should have host"
+ );
+ Assert.throws(
+ () => {
+ stringToURL("http:///");
+ },
+ /NS_ERROR_MALFORMED_URI/,
+ "TYPE_AUTHORITY should have host"
+ );
+
+ run_next_test();
+});
+
+add_test(function test_trim_C0_and_space() {
+ var url = stringToURL(
+ "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f http://example.com/ \x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f "
+ );
+ Assert.equal(url.spec, "http://example.com/");
+ url = url
+ .mutate()
+ .setSpec(
+ "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f http://test.com/ \x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f "
+ )
+ .finalize();
+ Assert.equal(url.spec, "http://test.com/");
+ Assert.throws(
+ () => {
+ url = url
+ .mutate()
+ .setSpec(
+ "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 "
+ )
+ .finalize();
+ },
+ /NS_ERROR_MALFORMED_URI/,
+ "set empty spec"
+ );
+ run_next_test();
+});
+
+// This tests that C0-and-space characters in the path, query and ref are
+// percent encoded.
+add_test(function test_encode_C0_and_space() {
+ function toHex(d) {
+ var hex = d.toString(16);
+ if (hex.length == 1) {
+ hex = "0" + hex;
+ }
+ return hex.toUpperCase();
+ }
+
+ for (var i = 0x0; i <= 0x20; i++) {
+ // These characters get filtered - they are not encoded.
+ if (
+ String.fromCharCode(i) == "\r" ||
+ String.fromCharCode(i) == "\n" ||
+ String.fromCharCode(i) == "\t"
+ ) {
+ continue;
+ }
+ let url = stringToURL(
+ "http://example.com/pa" +
+ String.fromCharCode(i) +
+ "th?qu" +
+ String.fromCharCode(i) +
+ "ery#ha" +
+ String.fromCharCode(i) +
+ "sh"
+ );
+ Assert.equal(
+ url.spec,
+ "http://example.com/pa%" +
+ toHex(i) +
+ "th?qu%" +
+ toHex(i) +
+ "ery#ha%" +
+ toHex(i) +
+ "sh"
+ );
+ }
+
+ // Additionally, we need to check the setters.
+ let url = stringToURL("http://example.com/path?query#hash");
+ url = url.mutate().setFilePath("pa\0th").finalize();
+ Assert.equal(url.spec, "http://example.com/pa%00th?query#hash");
+ url = url.mutate().setQuery("qu\0ery").finalize();
+ Assert.equal(url.spec, "http://example.com/pa%00th?qu%00ery#hash");
+ url = url.mutate().setRef("ha\0sh").finalize();
+ Assert.equal(url.spec, "http://example.com/pa%00th?qu%00ery#ha%00sh");
+ url = url
+ .mutate()
+ .QueryInterface(Ci.nsIURLMutator)
+ .setFileName("fi\0le.name")
+ .finalize();
+ Assert.equal(url.spec, "http://example.com/fi%00le.name?qu%00ery#ha%00sh");
+
+ run_next_test();
+});
+
+add_test(function test_ipv4Normalize() {
+ var localIPv4s = [
+ "http://127.0.0.1",
+ "http://127.0.1",
+ "http://127.1",
+ "http://2130706433",
+ "http://0177.00.00.01",
+ "http://0177.00.01",
+ "http://0177.01",
+ "http://00000000000000000000000000177.0000000.0000000.0001",
+ "http://000000177.0000001",
+ "http://017700000001",
+ "http://0x7f.0x00.0x00.0x01",
+ "http://0x7f.0x01",
+ "http://0x7f000001",
+ "http://0x007f.0x0000.0x0000.0x0001",
+ "http://000177.0.00000.0x0001",
+ "http://127.0.0.1.",
+ ].map(stringToURL);
+
+ let url;
+ for (url of localIPv4s) {
+ Assert.equal(url.spec, "http://127.0.0.1/");
+ }
+
+ // These should treated as a domain instead of an IPv4.
+ var nonIPv4s = [
+ "http://2+3/",
+ "http://0.0.0.-1/",
+ "http://1.2.3.4../",
+ "resource://123/",
+ "resource://4294967296/",
+ ];
+ var spec;
+ for (spec of nonIPv4s) {
+ url = stringToURL(spec);
+ Assert.equal(url.spec, spec);
+ }
+
+ url = stringToURL("resource://path/to/resource/");
+ url = url.mutate().setHost("123").finalize();
+ Assert.equal(url.host, "123");
+
+ run_next_test();
+});
+
+add_test(function test_invalidHostChars() {
+ var url = stringToURL("http://example.org/");
+ for (let i = 0; i <= 0x20; i++) {
+ // These characters get filtered.
+ if (
+ String.fromCharCode(i) == "\r" ||
+ String.fromCharCode(i) == "\n" ||
+ String.fromCharCode(i) == "\t"
+ ) {
+ continue;
+ }
+ Assert.throws(
+ () => {
+ url = url
+ .mutate()
+ .setHost("a" + String.fromCharCode(i) + "b")
+ .finalize();
+ },
+ /NS_ERROR_MALFORMED_URI/,
+ "Trying to set hostname containing char code: " + i
+ );
+ }
+ for (let c of '@[]*<>|:"') {
+ Assert.throws(
+ () => {
+ url = url
+ .mutate()
+ .setHost("a" + c)
+ .finalize();
+ },
+ /NS_ERROR_MALFORMED_URI/,
+ "Trying to set hostname containing char: " + c
+ );
+ }
+
+ // It also can't contain /, \, #, ?, but we treat these characters as
+ // hostname separators, so there is no way to set them and fail.
+ run_next_test();
+});
+
+add_test(function test_normalize_ipv6() {
+ var url = stringToURL("http://example.com");
+ url = url.mutate().setHost("[::192.9.5.5]").finalize();
+ Assert.equal(url.spec, "http://[::c009:505]/");
+
+ run_next_test();
+});
+
+add_test(function test_emptyPassword() {
+ var url = stringToURL("http://a:@example.com");
+ Assert.equal(url.spec, "http://a@example.com/");
+ url = url.mutate().setPassword("pp").finalize();
+ Assert.equal(url.spec, "http://a:pp@example.com/");
+ url = url.mutate().setPassword("").finalize();
+ Assert.equal(url.spec, "http://a@example.com/");
+ url = url.mutate().setUserPass("xxx:").finalize();
+ Assert.equal(url.spec, "http://xxx@example.com/");
+ url = url.mutate().setPassword("zzzz").finalize();
+ Assert.equal(url.spec, "http://xxx:zzzz@example.com/");
+ url = url.mutate().setUserPass("xxxxx:yyyyyy").finalize();
+ Assert.equal(url.spec, "http://xxxxx:yyyyyy@example.com/");
+ url = url.mutate().setUserPass("z:").finalize();
+ Assert.equal(url.spec, "http://z@example.com/");
+ url = url.mutate().setPassword("ppppppppppp").finalize();
+ Assert.equal(url.spec, "http://z:ppppppppppp@example.com/");
+
+ url = stringToURL("http://example.com");
+ url = url.mutate().setPassword("").finalize(); // Still empty. Should work.
+ Assert.equal(url.spec, "http://example.com/");
+
+ run_next_test();
+});
+
+add_test(function test_emptyUser() {
+ let url = stringToURL("http://:a@example.com/path/to/something?query#hash");
+ Assert.equal(url.spec, "http://:a@example.com/path/to/something?query#hash");
+ url = stringToURL("http://:@example.com/path/to/something?query#hash");
+ Assert.equal(url.spec, "http://example.com/path/to/something?query#hash");
+
+ const kurl = stringToURL(
+ "http://user:pass@example.com:8888/path/to/something?query#hash"
+ );
+ url = kurl.mutate().setUsername("").finalize();
+ Assert.equal(
+ url.spec,
+ "http://:pass@example.com:8888/path/to/something?query#hash"
+ );
+ Assert.equal(url.host, "example.com");
+ Assert.equal(url.hostPort, "example.com:8888");
+ Assert.equal(url.filePath, "/path/to/something");
+ Assert.equal(url.query, "query");
+ Assert.equal(url.ref, "hash");
+ url = kurl.mutate().setUserPass(":pass1").finalize();
+ Assert.equal(
+ url.spec,
+ "http://:pass1@example.com:8888/path/to/something?query#hash"
+ );
+ Assert.equal(url.host, "example.com");
+ Assert.equal(url.hostPort, "example.com:8888");
+ Assert.equal(url.filePath, "/path/to/something");
+ Assert.equal(url.query, "query");
+ Assert.equal(url.ref, "hash");
+ url = url.mutate().setUsername("user2").finalize();
+ Assert.equal(
+ url.spec,
+ "http://user2:pass1@example.com:8888/path/to/something?query#hash"
+ );
+ Assert.equal(url.host, "example.com");
+ url = url.mutate().setUserPass(":pass234").finalize();
+ Assert.equal(
+ url.spec,
+ "http://:pass234@example.com:8888/path/to/something?query#hash"
+ );
+ Assert.equal(url.host, "example.com");
+ url = url.mutate().setUserPass("").finalize();
+ Assert.equal(
+ url.spec,
+ "http://example.com:8888/path/to/something?query#hash"
+ );
+ Assert.equal(url.host, "example.com");
+ url = url.mutate().setPassword("pa").finalize();
+ Assert.equal(
+ url.spec,
+ "http://:pa@example.com:8888/path/to/something?query#hash"
+ );
+ Assert.equal(url.host, "example.com");
+ url = url.mutate().setUserPass("user:pass").finalize();
+ symmetricEquality(true, url.QueryInterface(Ci.nsIURL), kurl);
+
+ url = stringToURL("http://example.com:8888/path/to/something?query#hash");
+ url = url.mutate().setPassword("pass").finalize();
+ Assert.equal(
+ url.spec,
+ "http://:pass@example.com:8888/path/to/something?query#hash"
+ );
+ url = url.mutate().setUsername("").finalize();
+ Assert.equal(
+ url.spec,
+ "http://:pass@example.com:8888/path/to/something?query#hash"
+ );
+
+ url = stringToURL("http://example.com:8888");
+ url = url.mutate().setUsername("user").finalize();
+ url = url.mutate().setUsername("").finalize();
+ Assert.equal(url.spec, "http://example.com:8888/");
+
+ url = stringToURL("http://:pass@example.com");
+ Assert.equal(url.spec, "http://:pass@example.com/");
+ url = url.mutate().setPassword("").finalize();
+ Assert.equal(url.spec, "http://example.com/");
+ url = url.mutate().setUserPass("user:pass").finalize();
+ Assert.equal(url.spec, "http://user:pass@example.com/");
+ Assert.equal(url.host, "example.com");
+ url = url.mutate().setUserPass("u:p").finalize();
+ Assert.equal(url.spec, "http://u:p@example.com/");
+ Assert.equal(url.host, "example.com");
+ url = url.mutate().setUserPass("u1:p23").finalize();
+ Assert.equal(url.spec, "http://u1:p23@example.com/");
+ Assert.equal(url.host, "example.com");
+ url = url.mutate().setUsername("u").finalize();
+ Assert.equal(url.spec, "http://u:p23@example.com/");
+ Assert.equal(url.host, "example.com");
+ url = url.mutate().setPassword("p").finalize();
+ Assert.equal(url.spec, "http://u:p@example.com/");
+ Assert.equal(url.host, "example.com");
+
+ url = url.mutate().setUserPass("u2:p2").finalize();
+ Assert.equal(url.spec, "http://u2:p2@example.com/");
+ Assert.equal(url.host, "example.com");
+ url = url.mutate().setUserPass("u23:p23").finalize();
+ Assert.equal(url.spec, "http://u23:p23@example.com/");
+ Assert.equal(url.host, "example.com");
+
+ run_next_test();
+});
+
+registerCleanupFunction(function () {
+ gPrefs.clearUserPref("network.standard-url.punycode-host");
+});
+
+add_test(function test_idna_host() {
+ // See bug 945240 - this test makes sure that URLs return a punycode hostname
+ let url = stringToURL(
+ "http://user:password@ält.example.org:8080/path?query#etc"
+ );
+ equal(url.host, "xn--lt-uia.example.org");
+ equal(url.hostPort, "xn--lt-uia.example.org:8080");
+ equal(url.prePath, "http://user:password@xn--lt-uia.example.org:8080");
+ equal(
+ url.spec,
+ "http://user:password@xn--lt-uia.example.org:8080/path?query#etc"
+ );
+ equal(
+ url.specIgnoringRef,
+ "http://user:password@xn--lt-uia.example.org:8080/path?query"
+ );
+ equal(
+ url
+ .QueryInterface(Ci.nsISensitiveInfoHiddenURI)
+ .getSensitiveInfoHiddenSpec(),
+ "http://user:****@xn--lt-uia.example.org:8080/path?query#etc"
+ );
+
+ equal(url.displayHost, "ält.example.org");
+ equal(url.displayHostPort, "ält.example.org:8080");
+ equal(
+ url.displaySpec,
+ "http://user:password@ält.example.org:8080/path?query#etc"
+ );
+
+ equal(url.asciiHost, "xn--lt-uia.example.org");
+ equal(url.asciiHostPort, "xn--lt-uia.example.org:8080");
+ equal(
+ url.asciiSpec,
+ "http://user:password@xn--lt-uia.example.org:8080/path?query#etc"
+ );
+
+ url = url.mutate().setRef("").finalize(); // SetRef calls InvalidateCache()
+ equal(
+ url.spec,
+ "http://user:password@xn--lt-uia.example.org:8080/path?query"
+ );
+ equal(
+ url.displaySpec,
+ "http://user:password@ält.example.org:8080/path?query"
+ );
+ equal(
+ url.asciiSpec,
+ "http://user:password@xn--lt-uia.example.org:8080/path?query"
+ );
+
+ url = stringToURL("http://user:password@www.ält.com:8080/path?query#etc");
+ url = url.mutate().setRef("").finalize();
+ equal(url.spec, "http://user:password@www.xn--lt-uia.com:8080/path?query");
+
+ run_next_test();
+});
+
+add_test(
+ { skip_if: () => AppConstants.MOZ_APP_NAME == "thunderbird" },
+ function test_bug1517025() {
+ Assert.throws(
+ () => {
+ stringToURL("https://b%9a/");
+ },
+ /NS_ERROR_MALFORMED_URI/,
+ "bad URI"
+ );
+
+ Assert.throws(
+ () => {
+ stringToURL("https://b%9ª/");
+ },
+ /NS_ERROR_MALFORMED_URI/,
+ "bad URI"
+ );
+
+ let base = stringToURL(
+ "https://bug1517025.bmoattachments.org/attachment.cgi?id=9033787"
+ );
+ Assert.throws(
+ () => {
+ Services.io.newURI("/\\b%9ª", "windows-1252", base);
+ },
+ /NS_ERROR_MALFORMED_URI/,
+ "bad URI"
+ );
+
+ run_next_test();
+ }
+);
+
+add_task(async function test_emptyHostWithURLType() {
+ let makeURL = (str, type) => {
+ return Cc["@mozilla.org/network/standard-url-mutator;1"]
+ .createInstance(Ci.nsIStandardURLMutator)
+ .init(type, 80, str, "UTF-8", null)
+ .finalize()
+ .QueryInterface(Ci.nsIURL);
+ };
+
+ let url = makeURL("http://foo.com/bar/", Ci.nsIStandardURL.URLTYPE_AUTHORITY);
+ Assert.throws(
+ () => url.mutate().setHost("").finalize().spec,
+ /NS_ERROR_UNEXPECTED/,
+ "Empty host is not allowed for URLTYPE_AUTHORITY"
+ );
+
+ url = makeURL("http://foo.com/bar/", Ci.nsIStandardURL.URLTYPE_STANDARD);
+ Assert.throws(
+ () => url.mutate().setHost("").finalize().spec,
+ /NS_ERROR_UNEXPECTED/,
+ "Empty host is not allowed for URLTYPE_STANDARD"
+ );
+
+ url = makeURL("http://foo.com/bar/", Ci.nsIStandardURL.URLTYPE_NO_AUTHORITY);
+ equal(
+ url.spec,
+ "http:///bar/",
+ "Host is removed when parsing URLTYPE_NO_AUTHORITY"
+ );
+ equal(
+ url.mutate().setHost("").finalize().spec,
+ "http:///bar/",
+ "Setting an empty host does nothing for URLTYPE_NO_AUTHORITY"
+ );
+ Assert.throws(
+ () => url.mutate().setHost("something").finalize().spec,
+ /NS_ERROR_UNEXPECTED/,
+ "Setting a non-empty host is not allowed for URLTYPE_NO_AUTHORITY"
+ );
+ equal(
+ url.mutate().setHost("#j").finalize().spec,
+ "http:///bar/",
+ "Setting a pseudo-empty host does nothing for URLTYPE_NO_AUTHORITY"
+ );
+
+ url = makeURL(
+ "http://example.org:123/foo?bar#baz",
+ Ci.nsIStandardURL.URLTYPE_AUTHORITY
+ );
+ Assert.throws(
+ () => url.mutate().setHost("#j").finalize().spec,
+ /NS_ERROR_UNEXPECTED/,
+ "A pseudo-empty host is not allowed for URLTYPE_AUTHORITY"
+ );
+});
+
+add_task(async function test_fuzz() {
+ let makeURL = str => {
+ return (
+ Cc["@mozilla.org/network/standard-url-mutator;1"]
+ .createInstance(Ci.nsIStandardURLMutator)
+ .QueryInterface(Ci.nsIURIMutator)
+ // .init(type, 80, str, "UTF-8", null)
+ .setSpec(str)
+ .finalize()
+ .QueryInterface(Ci.nsIURL)
+ );
+ };
+
+ Assert.throws(() => {
+ let url = makeURL("/");
+ url.mutate().setHost("(").finalize();
+ }, /NS_ERROR_MALFORMED_URI/);
+});
+
+add_task(async function test_bug1648493() {
+ let url = stringToURL("https://example.com/");
+ url = url.mutate().setScheme("file").finalize();
+ url = url.mutate().setScheme("resource").finalize();
+ url = url.mutate().setPassword("ê").finalize();
+ url = url.mutate().setUsername("ç").finalize();
+ url = url.mutate().setScheme("t").finalize();
+ equal(url.spec, "t://%C3%83%C2%A7:%C3%83%C2%AA@example.com/");
+ equal(url.username, "%C3%83%C2%A7");
+});
diff --git a/netwerk/test/unit/test_standardurl_default_port.js b/netwerk/test/unit/test_standardurl_default_port.js
new file mode 100644
index 0000000000..a62edcf0e7
--- /dev/null
+++ b/netwerk/test/unit/test_standardurl_default_port.js
@@ -0,0 +1,58 @@
+/* -*- Mode: javascript; indent-tabs-mode: nil; js-indent-level: 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/. */
+
+/* This test exercises the nsIStandardURL "setDefaultPort" API. */
+
+"use strict";
+
+function run_test() {
+ 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.nsIStandardURL);
+ }
+
+ // Create a nsStandardURL:
+ var origUrlStr = "http://foo.com/";
+ var stdUrl = stringToURL(origUrlStr);
+ Assert.equal(-1, stdUrl.port);
+
+ // Changing default port shouldn't adjust the value returned by "port",
+ // or the string representation.
+ let def100Url = stdUrl
+ .mutate()
+ .QueryInterface(Ci.nsIStandardURLMutator)
+ .setDefaultPort(100)
+ .finalize();
+ Assert.equal(-1, def100Url.port);
+ Assert.equal(def100Url.spec, origUrlStr);
+
+ // Changing port directly should update .port and .spec, though:
+ let port200Url = stdUrl.mutate().setPort("200").finalize();
+ Assert.equal(200, port200Url.port);
+ Assert.equal(port200Url.spec, "http://foo.com:200/");
+
+ // ...but then if we change default port to match the custom port,
+ // the custom port should reset to -1 and disappear from .spec:
+ let def200Url = port200Url
+ .mutate()
+ .QueryInterface(Ci.nsIStandardURLMutator)
+ .setDefaultPort(200)
+ .finalize();
+ Assert.equal(-1, def200Url.port);
+ Assert.equal(def200Url.spec, origUrlStr);
+
+ // And further changes to default port should not make custom port reappear.
+ let def300Url = def200Url
+ .mutate()
+ .QueryInterface(Ci.nsIStandardURLMutator)
+ .setDefaultPort(300)
+ .finalize();
+ Assert.equal(-1, def300Url.port);
+ Assert.equal(def300Url.spec, origUrlStr);
+}
diff --git a/netwerk/test/unit/test_standardurl_port.js b/netwerk/test/unit/test_standardurl_port.js
new file mode 100644
index 0000000000..76fdad6405
--- /dev/null
+++ b/netwerk/test/unit/test_standardurl_port.js
@@ -0,0 +1,53 @@
+"use strict";
+
+function run_test() {
+ function makeURI(aURLSpec, aCharset) {
+ return Services.io.newURI(aURLSpec, aCharset);
+ }
+
+ var httpURI = makeURI("http://foo.com");
+ Assert.equal(-1, httpURI.port);
+
+ // Setting to default shouldn't cause a change
+ httpURI = httpURI.mutate().setPort(80).finalize();
+ Assert.equal(-1, httpURI.port);
+
+ // Setting to default after setting to non-default shouldn't cause a change (bug 403480)
+ httpURI = httpURI.mutate().setPort(123).finalize();
+ Assert.equal(123, httpURI.port);
+ httpURI = httpURI.mutate().setPort(80).finalize();
+ Assert.equal(-1, httpURI.port);
+ Assert.ok(!/80/.test(httpURI.spec));
+
+ // URL parsers shouldn't set ports to default value (bug 407538)
+ httpURI = httpURI.mutate().setSpec("http://foo.com:81").finalize();
+ Assert.equal(81, httpURI.port);
+ httpURI = httpURI.mutate().setSpec("http://foo.com:80").finalize();
+ Assert.equal(-1, httpURI.port);
+ Assert.ok(!/80/.test(httpURI.spec));
+
+ httpURI = makeURI("http://foo.com");
+ Assert.equal(-1, httpURI.port);
+ Assert.ok(!/80/.test(httpURI.spec));
+
+ httpURI = makeURI("http://foo.com:80");
+ Assert.equal(-1, httpURI.port);
+ Assert.ok(!/80/.test(httpURI.spec));
+
+ httpURI = makeURI("http://foo.com:80");
+ Assert.equal(-1, httpURI.port);
+ Assert.ok(!/80/.test(httpURI.spec));
+
+ httpURI = makeURI("https://foo.com");
+ Assert.equal(-1, httpURI.port);
+ Assert.ok(!/443/.test(httpURI.spec));
+
+ httpURI = makeURI("https://foo.com:443");
+ Assert.equal(-1, httpURI.port);
+ Assert.ok(!/443/.test(httpURI.spec));
+
+ // XXX URL parsers shouldn't set ports to default value, even when changing scheme?
+ // not really possible given current nsIURI impls
+ //httpURI.spec = "https://foo.com:443";
+ //do_check_eq(-1, httpURI.port);
+}
diff --git a/netwerk/test/unit/test_streamcopier.js b/netwerk/test/unit/test_streamcopier.js
new file mode 100644
index 0000000000..f550923546
--- /dev/null
+++ b/netwerk/test/unit/test_streamcopier.js
@@ -0,0 +1,63 @@
+"use strict";
+
+var testStr = "This is a test. ";
+for (var i = 0; i < 10; ++i) {
+ testStr += testStr;
+}
+
+function run_test() {
+ // Set up our stream to copy
+ var inStr = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(
+ Ci.nsIStringInputStream
+ );
+ inStr.setData(testStr, testStr.length);
+
+ // Set up our destination stream. Make sure to use segments a good
+ // bit smaller than our data length.
+ Assert.ok(testStr.length > 1024 * 10);
+ var pipe = Cc["@mozilla.org/pipe;1"].createInstance(Ci.nsIPipe);
+ pipe.init(true, true, 1024, 0xffffffff, null);
+
+ var streamCopier = Cc[
+ "@mozilla.org/network/async-stream-copier;1"
+ ].createInstance(Ci.nsIAsyncStreamCopier);
+ streamCopier.init(
+ inStr,
+ pipe.outputStream,
+ null,
+ true,
+ true,
+ 1024,
+ true,
+ true
+ );
+
+ var ctx = {};
+ ctx.wrappedJSObject = ctx;
+
+ var observer = {
+ onStartRequest(aRequest) {},
+ onStopRequest(aRequest, aStatusCode) {
+ Assert.equal(aStatusCode, 0);
+ var sis = Cc["@mozilla.org/scriptableinputstream;1"].createInstance(
+ Ci.nsIScriptableInputStream
+ );
+ sis.init(pipe.inputStream);
+ var result = "";
+ var temp;
+ try {
+ // Need this because read() can throw at EOF
+ while ((temp = sis.read(1024))) {
+ result += temp;
+ }
+ } catch (e) {
+ Assert.equal(e.result, Cr.NS_BASE_STREAM_CLOSED);
+ }
+ Assert.equal(result, testStr);
+ do_test_finished();
+ },
+ };
+
+ streamCopier.asyncCopy(observer, ctx);
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_substituting_protocol_handler.js b/netwerk/test/unit/test_substituting_protocol_handler.js
new file mode 100644
index 0000000000..00e5af0c21
--- /dev/null
+++ b/netwerk/test/unit/test_substituting_protocol_handler.js
@@ -0,0 +1,64 @@
+"use strict";
+
+add_task(async function test_case_insensitive_substitutions() {
+ let resProto = Services.io
+ .getProtocolHandler("resource")
+ .QueryInterface(Ci.nsISubstitutingProtocolHandler);
+
+ let uri = Services.io.newFileURI(do_get_file("data"));
+
+ resProto.setSubstitution("FooBar", uri);
+ resProto.setSubstitutionWithFlags("BarBaz", uri, 0);
+
+ equal(
+ resProto.resolveURI(Services.io.newURI("resource://foobar/")),
+ uri.spec,
+ "Got correct resolved URI for setSubstitution"
+ );
+
+ equal(
+ resProto.resolveURI(Services.io.newURI("resource://foobar/")),
+ uri.spec,
+ "Got correct resolved URI for setSubstitutionWithFlags"
+ );
+
+ ok(
+ resProto.hasSubstitution("foobar"),
+ "hasSubstitution works with all-lower-case root"
+ );
+ ok(
+ resProto.hasSubstitution("FooBar"),
+ "hasSubstitution works with mixed-case root"
+ );
+
+ equal(
+ resProto.getSubstitution("foobar").spec,
+ uri.spec,
+ "getSubstitution works with all-lower-case root"
+ );
+ equal(
+ resProto.getSubstitution("FooBar").spec,
+ uri.spec,
+ "getSubstitution works with mixed-case root"
+ );
+
+ resProto.setSubstitution("foobar", null);
+ resProto.setSubstitution("barbaz", null);
+
+ Assert.throws(
+ () => resProto.resolveURI(Services.io.newURI("resource://foobar/")),
+ e => e.result == Cr.NS_ERROR_NOT_AVAILABLE,
+ "Correctly unregistered case-insensitive substitution in setSubstitution"
+ );
+ Assert.throws(
+ () => resProto.resolveURI(Services.io.newURI("resource://barbaz/")),
+ e => e.result == Cr.NS_ERROR_NOT_AVAILABLE,
+ "Correctly unregistered case-insensitive substitution in setSubstitutionWithFlags"
+ );
+
+ Assert.throws(
+ () => resProto.getSubstitution("foobar"),
+ e => e.result == Cr.NS_ERROR_NOT_AVAILABLE,
+ "foobar substitution has been removed"
+ );
+});
diff --git a/netwerk/test/unit/test_suspend_channel_before_connect.js b/netwerk/test/unit/test_suspend_channel_before_connect.js
new file mode 100644
index 0000000000..79a0c43236
--- /dev/null
+++ b/netwerk/test/unit/test_suspend_channel_before_connect.js
@@ -0,0 +1,93 @@
+"use strict";
+
+var CC = Components.Constructor;
+
+const ServerSocket = CC(
+ "@mozilla.org/network/server-socket;1",
+ "nsIServerSocket",
+ "init"
+);
+
+var obs = Services.obs;
+
+// A server that waits for a connect. If a channel is suspended it should not
+// try to connect to the server until it is is resumed or not try at all if it
+// is cancelled as in this test.
+function TestServer() {
+ this.listener = ServerSocket(-1, true, -1);
+ this.port = this.listener.port;
+ this.listener.asyncListen(this);
+}
+
+TestServer.prototype = {
+ onSocketAccepted(socket, trans) {
+ Assert.ok(false, "Socket should not have tried to connect!");
+ },
+
+ onStopListening(socket) {},
+
+ stop() {
+ try {
+ this.listener.close();
+ } catch (ignore) {}
+ },
+};
+
+var requestListenerObserver = {
+ QueryInterface: ChromeUtils.generateQI(["nsIObserver"]),
+
+ observe(subject, topic, data) {
+ if (
+ topic === "http-on-modify-request" &&
+ subject instanceof Ci.nsIHttpChannel
+ ) {
+ var chan = subject.QueryInterface(Ci.nsIHttpChannel);
+ chan.suspend();
+ obs.removeObserver(this, "http-on-modify-request");
+
+ // Timers are bad, but we need to wait to see that we are not trying to
+ // connect to the server. There are no other event since nothing should
+ // happen until we resume the channel.
+ let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ timer.initWithCallback(
+ () => {
+ chan.cancel(Cr.NS_BINDING_ABORTED);
+ chan.resume();
+ },
+ 1000,
+ Ci.nsITimer.TYPE_ONE_SHOT
+ );
+ }
+ },
+};
+
+var listener = {
+ onStartRequest: function test_onStartR(request) {},
+
+ onDataAvailable: function test_ODA() {
+ do_throw("Should not get any data!");
+ },
+
+ onStopRequest: function test_onStopR(request, status) {
+ executeSoon(run_next_test);
+ },
+};
+
+// Add observer and start a channel. Observer is going to suspend the channel on
+// "http-on-modify-request" even. If a channel is suspended so early it should
+// not try to connect at all until it is resumed. In this case we are going to
+// wait for some time and cancel the channel before resuming it.
+add_test(function testNoConnectChannelCanceledEarly() {
+ let serv = new TestServer();
+
+ obs.addObserver(requestListenerObserver, "http-on-modify-request");
+ var chan = NetUtil.newChannel({
+ uri: "http://localhost:" + serv.port,
+ loadUsingSystemPrincipal: true,
+ });
+ chan.asyncOpen(listener);
+
+ registerCleanupFunction(function () {
+ serv.stop();
+ });
+});
diff --git a/netwerk/test/unit/test_suspend_channel_on_authRetry.js b/netwerk/test/unit/test_suspend_channel_on_authRetry.js
new file mode 100644
index 0000000000..ad0b728b68
--- /dev/null
+++ b/netwerk/test/unit/test_suspend_channel_on_authRetry.js
@@ -0,0 +1,264 @@
+// This file tests async handling of a channel suspended in DoAuthRetry
+// notifying http-on-modify-request and http-on-before-connect observers.
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+ChromeUtils.defineLazyGetter(this, "URL", function () {
+ return "http://localhost:" + httpserv.identity.primaryPort;
+});
+
+var obs = Services.obs;
+
+var requestObserver = null;
+
+function AuthPrompt() {}
+
+AuthPrompt.prototype = {
+ user: "guest",
+ pass: "guest",
+
+ QueryInterface: ChromeUtils.generateQI(["nsIAuthPrompt"]),
+
+ prompt: function ap1_prompt(title, text, realm, save, defaultText, result) {
+ do_throw("unexpected prompt call");
+ },
+
+ promptUsernameAndPassword: function promptUP(
+ title,
+ text,
+ realm,
+ savePW,
+ user,
+ pw
+ ) {
+ user.value = this.user;
+ pw.value = this.pass;
+
+ obs.addObserver(requestObserver, "http-on-before-connect");
+ obs.addObserver(requestObserver, "http-on-modify-request");
+ return true;
+ },
+
+ promptPassword: function promptPW(title, text, realm, save, pwd) {
+ do_throw("unexpected promptPassword call");
+ },
+};
+
+function requestListenerObserver(
+ suspendOnBeforeConnect,
+ suspendOnModifyRequest
+) {
+ this.suspendOnModifyRequest = suspendOnModifyRequest;
+ this.suspendOnBeforeConnect = suspendOnBeforeConnect;
+}
+
+requestListenerObserver.prototype = {
+ suspendOnModifyRequest: false,
+ suspendOnBeforeConnect: false,
+ gotOnBeforeConnect: false,
+ resumeOnBeforeConnect: false,
+ gotOnModifyRequest: false,
+ resumeOnModifyRequest: false,
+ QueryInterface: ChromeUtils.generateQI(["nsIObserver"]),
+
+ observe(subject, topic, data) {
+ if (
+ topic === "http-on-before-connect" &&
+ subject instanceof Ci.nsIHttpChannel
+ ) {
+ if (this.suspendOnBeforeConnect) {
+ let chan = subject.QueryInterface(Ci.nsIHttpChannel);
+ executeSoon(() => {
+ this.resumeOnBeforeConnect = true;
+ chan.resume();
+ });
+ this.gotOnBeforeConnect = true;
+ chan.suspend();
+ }
+ } else if (
+ topic === "http-on-modify-request" &&
+ subject instanceof Ci.nsIHttpChannel
+ ) {
+ if (this.suspendOnModifyRequest) {
+ let chan = subject.QueryInterface(Ci.nsIHttpChannel);
+ executeSoon(() => {
+ this.resumeOnModifyRequest = true;
+ chan.resume();
+ });
+ this.gotOnModifyRequest = true;
+ chan.suspend();
+ }
+ }
+ },
+};
+
+function Requestor() {}
+
+Requestor.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["nsIInterfaceRequestor"]),
+
+ getInterface: function requestor_gi(iid) {
+ if (iid.equals(Ci.nsIAuthPrompt)) {
+ // Allow the prompt to store state by caching it here
+ if (!this.prompt) {
+ this.prompt = new AuthPrompt();
+ }
+ return this.prompt;
+ }
+
+ throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE);
+ },
+
+ prompt: null,
+};
+
+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);
+ if (requestObserver.suspendOnBeforeConnect) {
+ Assert.ok(
+ requestObserver.gotOnBeforeConnect &&
+ requestObserver.resumeOnBeforeConnect
+ );
+ }
+ if (requestObserver.suspendOnModifyRequest) {
+ Assert.ok(
+ requestObserver.gotOnModifyRequest &&
+ requestObserver.resumeOnModifyRequest
+ );
+ }
+ obs.removeObserver(requestObserver, "http-on-before-connect");
+ obs.removeObserver(requestObserver, "http-on-modify-request");
+ moveToNextTest();
+ },
+};
+
+function makeChan(url, loadingUrl) {
+ var principal = Services.scriptSecurityManager.createContentPrincipal(
+ Services.io.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_suspend_on_before_connect,
+ test_suspend_on_modify_request,
+ test_suspend_all,
+];
+
+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.start(-1);
+
+ tests[0]();
+}
+
+function test_suspend_on_auth(suspendOnBeforeConnect, suspendOnModifyRequest) {
+ var chan = makeChan(URL + "/auth", URL);
+ requestObserver = new requestListenerObserver(
+ suspendOnBeforeConnect,
+ suspendOnModifyRequest
+ );
+ chan.notificationCallbacks = new Requestor();
+ listener.expectedCode = 200; // OK
+ chan.asyncOpen(listener);
+
+ do_test_pending();
+}
+
+function test_suspend_on_before_connect() {
+ test_suspend_on_auth(true, false);
+}
+
+function test_suspend_on_modify_request() {
+ test_suspend_on_auth(false, true);
+}
+
+function test_suspend_all() {
+ test_suspend_on_auth(true, true);
+}
+
+// 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);
+}
diff --git a/netwerk/test/unit/test_suspend_channel_on_examine.js b/netwerk/test/unit/test_suspend_channel_on_examine.js
new file mode 100644
index 0000000000..7667026a56
--- /dev/null
+++ b/netwerk/test/unit/test_suspend_channel_on_examine.js
@@ -0,0 +1,76 @@
+// This file tests async handling of a channel suspended in http-on-modify-request.
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+var obs = Services.obs;
+
+var baseUrl;
+
+function responseHandler(metadata, response) {
+ var text = "testing";
+ response.setHeader("Content-Type", "text/plain", false);
+ response.setHeader("Set-Cookie", "chewy", false);
+ response.bodyOutputStream.write(text, text.length);
+}
+
+function onExamineListener(callback) {
+ obs.addObserver(
+ {
+ observe(subject, topic, data) {
+ obs.removeObserver(this, "http-on-examine-response");
+ callback(subject.QueryInterface(Ci.nsIHttpChannel));
+ },
+ },
+ "http-on-examine-response"
+ );
+}
+
+function startChannelRequest(uri, flags, callback) {
+ var chan = NetUtil.newChannel({
+ uri,
+ loadUsingSystemPrincipal: true,
+ });
+ chan.asyncOpen(new ChannelListener(callback, null, flags));
+}
+
+// We first make a request that we'll cancel asynchronously. The response will
+// still contain the set-cookie header. Then verify the cookie was not actually
+// retained.
+add_test(function testAsyncCancel() {
+ onExamineListener(chan => {
+ // Suspend the channel then yield to make this async.
+ chan.suspend();
+ Promise.resolve().then(() => {
+ chan.cancel(Cr.NS_BINDING_ABORTED);
+ chan.resume();
+ });
+ });
+ startChannelRequest(baseUrl, CL_EXPECT_FAILURE, (request, data, context) => {
+ Assert.ok(!data, "no response");
+
+ Assert.equal(
+ Services.cookies.countCookiesFromHost("localhost"),
+ 0,
+ "no cookies set"
+ );
+
+ executeSoon(run_next_test);
+ });
+});
+
+function run_test() {
+ var httpServer = new HttpServer();
+ httpServer.registerPathHandler("/", responseHandler);
+ httpServer.start(-1);
+
+ baseUrl = `http://localhost:${httpServer.identity.primaryPort}`;
+
+ run_next_test();
+
+ registerCleanupFunction(function () {
+ httpServer.stop(() => {});
+ });
+}
diff --git a/netwerk/test/unit/test_suspend_channel_on_examine_merged_response.js b/netwerk/test/unit/test_suspend_channel_on_examine_merged_response.js
new file mode 100644
index 0000000000..7496796500
--- /dev/null
+++ b/netwerk/test/unit/test_suspend_channel_on_examine_merged_response.js
@@ -0,0 +1,208 @@
+/* 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 async handling of a channel suspended in
+// notifying http-on-examine-merged-response observers.
+// Note that this test is developed based on test_bug482601.js.
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+var httpserv = null;
+var test_nr = 0;
+var buffer = "";
+var observerCalled = false;
+var channelResumed = false;
+
+var observer = {
+ QueryInterface: ChromeUtils.generateQI(["nsIObserver"]),
+
+ observe(subject, topic, data) {
+ if (
+ topic === "http-on-examine-merged-response" &&
+ subject instanceof Ci.nsIHttpChannel
+ ) {
+ var chan = subject.QueryInterface(Ci.nsIHttpChannel);
+ executeSoon(() => {
+ Assert.equal(channelResumed, false);
+ channelResumed = true;
+ chan.resume();
+ });
+ Assert.equal(observerCalled, false);
+ observerCalled = true;
+ chan.suspend();
+ }
+ },
+};
+
+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(channelResumed, true);
+ Assert.equal(observerCalled, true);
+ test_nr++;
+ do_timeout(0, do_test);
+ },
+};
+
+function run_test() {
+ httpserv = new HttpServer();
+ httpserv.registerPathHandler("/path/partial", path_partial);
+ httpserv.registerPathHandler("/path/cached", path_cached);
+ httpserv.start(-1);
+
+ Services.obs.addObserver(observer, "http-on-examine-merged-response");
+
+ do_timeout(0, do_test);
+ do_test_pending();
+}
+
+function do_test() {
+ if (test_nr < tests.length) {
+ tests[test_nr]();
+ } else {
+ Services.obs.removeObserver(observer, "http-on-examine-merged-response");
+ httpserv.stop(do_test_finished);
+ }
+}
+
+var tests = [test_partial, test_cached];
+
+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_partial() {
+ observerCalled = false;
+ channelResumed = false;
+ asyncOpenCacheEntry(
+ "http://localhost:" + httpserv.identity.primaryPort + "/path/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"
+ );
+
+ observerCalled = false;
+
+ var chan = makeChan(
+ "http://localhost:" + httpserv.identity.primaryPort + "/path/partial"
+ );
+ chan.asyncOpen(listener);
+}
+
+function test_cached() {
+ observerCalled = false;
+ channelResumed = false;
+ asyncOpenCacheEntry(
+ "http://localhost:" + httpserv.identity.primaryPort + "/path/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"
+ );
+
+ observerCalled = false;
+
+ var chan = makeChan(
+ "http://localhost:" + httpserv.identity.primaryPort + "/path/cached"
+ );
+ chan.loadFlags = Ci.nsIRequest.VALIDATE_ALWAYS;
+ chan.asyncOpen(listener);
+}
+
+// PATHS
+
+// /path/partial
+function path_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);
+}
+
+// /path/cached
+function path_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");
+}
diff --git a/netwerk/test/unit/test_suspend_channel_on_modified.js b/netwerk/test/unit/test_suspend_channel_on_modified.js
new file mode 100644
index 0000000000..397ffae881
--- /dev/null
+++ b/netwerk/test/unit/test_suspend_channel_on_modified.js
@@ -0,0 +1,177 @@
+// This file tests async handling of a channel suspended in http-on-modify-request.
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+var obs = Services.obs;
+
+var ios = Services.io;
+
+// baseUrl is always the initial connection attempt and is handled by
+// failResponseHandler since every test expects that request will either be
+// redirected or cancelled.
+var baseUrl;
+
+function failResponseHandler(metadata, response) {
+ var text = "failure response";
+ response.setHeader("Content-Type", "text/plain", false);
+ response.bodyOutputStream.write(text, text.length);
+ Assert.ok(false, "Received request when we shouldn't.");
+}
+
+function successResponseHandler(metadata, response) {
+ var text = "success response";
+ response.setHeader("Content-Type", "text/plain", false);
+ response.bodyOutputStream.write(text, text.length);
+ Assert.ok(true, "Received expected request.");
+}
+
+function onModifyListener(callback) {
+ obs.addObserver(
+ {
+ observe(subject, topic, data) {
+ obs.removeObserver(this, "http-on-modify-request");
+ callback(subject.QueryInterface(Ci.nsIHttpChannel));
+ },
+ },
+ "http-on-modify-request"
+ );
+}
+
+function startChannelRequest(uri, flags, expectedResponse = null) {
+ var chan = NetUtil.newChannel({
+ uri,
+ loadUsingSystemPrincipal: true,
+ });
+ chan.asyncOpen(
+ new ChannelListener(
+ (request, data, context) => {
+ if (expectedResponse) {
+ Assert.equal(data, expectedResponse);
+ } else {
+ Assert.ok(!data, "no response");
+ }
+ executeSoon(run_next_test);
+ },
+ null,
+ flags
+ )
+ );
+}
+
+add_test(function testSimpleRedirect() {
+ onModifyListener(chan => {
+ chan.redirectTo(ios.newURI(`${baseUrl}/success`));
+ });
+ startChannelRequest(baseUrl, undefined, "success response");
+});
+
+add_test(function testSimpleCancel() {
+ onModifyListener(chan => {
+ chan.cancel(Cr.NS_BINDING_ABORTED);
+ });
+ startChannelRequest(baseUrl, CL_EXPECT_FAILURE);
+});
+
+add_test(function testSimpleCancelRedirect() {
+ onModifyListener(chan => {
+ chan.redirectTo(ios.newURI(`${baseUrl}/fail`));
+ chan.cancel(Cr.NS_BINDING_ABORTED);
+ });
+ startChannelRequest(baseUrl, CL_EXPECT_FAILURE);
+});
+
+// Test a request that will get redirected asynchronously. baseUrl should
+// not be requested, we should receive the request for the redirectedUrl.
+add_test(function testAsyncRedirect() {
+ onModifyListener(chan => {
+ // Suspend the channel then yield to make this async.
+ chan.suspend();
+ Promise.resolve().then(() => {
+ chan.redirectTo(ios.newURI(`${baseUrl}/success`));
+ chan.resume();
+ });
+ });
+ startChannelRequest(baseUrl, undefined, "success response");
+});
+
+add_test(function testSyncRedirect() {
+ onModifyListener(chan => {
+ chan.suspend();
+ chan.redirectTo(ios.newURI(`${baseUrl}/success`));
+ Promise.resolve().then(() => {
+ chan.resume();
+ });
+ });
+ startChannelRequest(baseUrl, undefined, "success response");
+});
+
+add_test(function testAsyncCancel() {
+ onModifyListener(chan => {
+ // Suspend the channel then yield to make this async.
+ chan.suspend();
+ Promise.resolve().then(() => {
+ chan.cancel(Cr.NS_BINDING_ABORTED);
+ chan.resume();
+ });
+ });
+ startChannelRequest(baseUrl, CL_EXPECT_FAILURE);
+});
+
+add_test(function testSyncCancel() {
+ onModifyListener(chan => {
+ chan.suspend();
+ chan.cancel(Cr.NS_BINDING_ABORTED);
+ Promise.resolve().then(() => {
+ chan.resume();
+ });
+ });
+ startChannelRequest(baseUrl, CL_EXPECT_FAILURE);
+});
+
+// Test request that will get redirected and cancelled asynchronously,
+// ensure no connection is made.
+add_test(function testAsyncCancelRedirect() {
+ onModifyListener(chan => {
+ // Suspend the channel then yield to make this async.
+ chan.suspend();
+ Promise.resolve().then(() => {
+ chan.cancel(Cr.NS_BINDING_ABORTED);
+ chan.redirectTo(ios.newURI(`${baseUrl}/fail`));
+ chan.resume();
+ });
+ });
+ startChannelRequest(baseUrl, CL_EXPECT_FAILURE);
+});
+
+// Test a request that will get cancelled synchronously, ensure async redirect
+// is not made.
+add_test(function testSyncCancelRedirect() {
+ onModifyListener(chan => {
+ chan.suspend();
+ chan.cancel(Cr.NS_BINDING_ABORTED);
+ Promise.resolve().then(() => {
+ chan.redirectTo(ios.newURI(`${baseUrl}/fail`));
+ chan.resume();
+ });
+ });
+ startChannelRequest(baseUrl, CL_EXPECT_FAILURE);
+});
+
+function run_test() {
+ var httpServer = new HttpServer();
+ httpServer.registerPathHandler("/", failResponseHandler);
+ httpServer.registerPathHandler("/fail", failResponseHandler);
+ httpServer.registerPathHandler("/success", successResponseHandler);
+ httpServer.start(-1);
+
+ baseUrl = `http://localhost:${httpServer.identity.primaryPort}`;
+
+ run_next_test();
+
+ registerCleanupFunction(function () {
+ httpServer.stop(() => {});
+ });
+}
diff --git a/netwerk/test/unit/test_synthesized_response.js b/netwerk/test/unit/test_synthesized_response.js
new file mode 100644
index 0000000000..a598e649c4
--- /dev/null
+++ b/netwerk/test/unit/test_synthesized_response.js
@@ -0,0 +1,288 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+ChromeUtils.defineLazyGetter(this, "URL", function () {
+ return "http://localhost:" + httpServer.identity.primaryPort;
+});
+
+var httpServer = null;
+
+function isParentProcess() {
+ let appInfo = Cc["@mozilla.org/xre/app-info;1"];
+ return (
+ !appInfo ||
+ Services.appinfo.processType == Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT
+ );
+}
+
+if (isParentProcess()) {
+ // ensure the cache service is prepped when running the test
+ // We only do this in the main process, as the cache storage service leaks
+ // when instantiated in the content process.
+ Services.cache2;
+}
+
+var gotOnProgress;
+var gotOnStatus;
+
+function make_channel(url, body, cb) {
+ gotOnProgress = false;
+ gotOnStatus = false;
+ var chan = NetUtil.newChannel({
+ uri: url,
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+ chan.notificationCallbacks = {
+ numChecks: 0,
+ QueryInterface: ChromeUtils.generateQI([
+ "nsINetworkInterceptController",
+ "nsIInterfaceRequestor",
+ "nsIProgressEventSink",
+ ]),
+ getInterface(iid) {
+ return this.QueryInterface(iid);
+ },
+ onProgress(request, progress, progressMax) {
+ gotOnProgress = true;
+ },
+ onStatus(request, status, statusArg) {
+ gotOnStatus = true;
+ },
+ shouldPrepareForIntercept() {
+ Assert.equal(this.numChecks, 0);
+ this.numChecks++;
+ return true;
+ },
+ channelIntercepted(channel) {
+ channel.QueryInterface(Ci.nsIInterceptedChannel);
+ if (body) {
+ var synthesized = Cc[
+ "@mozilla.org/io/string-input-stream;1"
+ ].createInstance(Ci.nsIStringInputStream);
+ synthesized.data = body;
+
+ channel.startSynthesizedResponse(synthesized, null, null, "", false);
+ channel.finishSynthesizedResponse();
+ }
+ if (cb) {
+ cb(channel);
+ }
+ return {
+ dispatch() {},
+ };
+ },
+ };
+ return chan;
+}
+
+const REMOTE_BODY = "http handler body";
+const NON_REMOTE_BODY = "synthesized body";
+const NON_REMOTE_BODY_2 = "synthesized body #2";
+
+function bodyHandler(metadata, response) {
+ response.setHeader("Content-Type", "text/plain");
+ response.write(REMOTE_BODY);
+}
+
+function run_test() {
+ httpServer = new HttpServer();
+ httpServer.registerPathHandler("/body", bodyHandler);
+ httpServer.start(-1);
+
+ run_next_test();
+}
+
+function handle_synthesized_response(request, buffer) {
+ Assert.equal(buffer, NON_REMOTE_BODY);
+ Assert.ok(gotOnStatus);
+ Assert.ok(gotOnProgress);
+ run_next_test();
+}
+
+function handle_synthesized_response_2(request, buffer) {
+ Assert.equal(buffer, NON_REMOTE_BODY_2);
+ Assert.ok(gotOnStatus);
+ Assert.ok(gotOnProgress);
+ run_next_test();
+}
+
+function handle_remote_response(request, buffer) {
+ Assert.equal(buffer, REMOTE_BODY);
+ Assert.ok(gotOnStatus);
+ Assert.ok(gotOnProgress);
+ run_next_test();
+}
+
+// hit the network instead of synthesizing
+add_test(function () {
+ var chan = make_channel(URL + "/body", null, function (channel) {
+ channel.resetInterception(false);
+ });
+ chan.asyncOpen(new ChannelListener(handle_remote_response, null));
+});
+
+// synthesize a response
+add_test(function () {
+ var chan = make_channel(URL + "/body", NON_REMOTE_BODY);
+ chan.asyncOpen(
+ new ChannelListener(handle_synthesized_response, null, CL_ALLOW_UNKNOWN_CL)
+ );
+});
+
+// hit the network instead of synthesizing, to test that no previous synthesized
+// cache entry is used.
+add_test(function () {
+ var chan = make_channel(URL + "/body", null, function (channel) {
+ channel.resetInterception(false);
+ });
+ chan.asyncOpen(new ChannelListener(handle_remote_response, null));
+});
+
+// synthesize a different response to ensure no previous response is cached
+add_test(function () {
+ var chan = make_channel(URL + "/body", NON_REMOTE_BODY_2);
+ chan.asyncOpen(
+ new ChannelListener(
+ handle_synthesized_response_2,
+ null,
+ CL_ALLOW_UNKNOWN_CL
+ )
+ );
+});
+
+// ensure that the channel waits for a decision and synthesizes headers correctly
+add_test(function () {
+ var chan = make_channel(URL + "/body", null, function (channel) {
+ do_timeout(100, function () {
+ var synthesized = Cc[
+ "@mozilla.org/io/string-input-stream;1"
+ ].createInstance(Ci.nsIStringInputStream);
+ synthesized.data = NON_REMOTE_BODY;
+ channel.synthesizeHeader("Content-Length", NON_REMOTE_BODY.length);
+ channel.startSynthesizedResponse(synthesized, null, null, "", false);
+ channel.finishSynthesizedResponse();
+ });
+ });
+ chan.asyncOpen(new ChannelListener(handle_synthesized_response, null));
+});
+
+// ensure that the channel waits for a decision
+add_test(function () {
+ var chan = make_channel(URL + "/body", null, function (channel) {
+ do_timeout(100, function () {
+ channel.resetInterception(false);
+ });
+ });
+ chan.asyncOpen(new ChannelListener(handle_remote_response, null));
+});
+
+// ensure that the intercepted channel supports suspend/resume
+add_test(function () {
+ var chan = make_channel(URL + "/body", null, function (intercepted) {
+ var synthesized = Cc[
+ "@mozilla.org/io/string-input-stream;1"
+ ].createInstance(Ci.nsIStringInputStream);
+ synthesized.data = NON_REMOTE_BODY;
+
+ // set the content-type to ensure that the stream converter doesn't hold up notifications
+ // and cause the test to fail
+ intercepted.synthesizeHeader("Content-Type", "text/plain");
+ intercepted.startSynthesizedResponse(synthesized, null, null, "", false);
+ intercepted.finishSynthesizedResponse();
+ });
+ chan.asyncOpen(
+ new ChannelListener(
+ handle_synthesized_response,
+ null,
+ CL_ALLOW_UNKNOWN_CL | CL_SUSPEND | CL_EXPECT_3S_DELAY
+ )
+ );
+});
+
+// ensure that the intercepted channel can be cancelled
+add_test(function () {
+ var chan = make_channel(URL + "/body", null, function (intercepted) {
+ intercepted.cancelInterception(Cr.NS_BINDING_ABORTED);
+ });
+ chan.asyncOpen(new ChannelListener(run_next_test, null, CL_EXPECT_FAILURE));
+});
+
+// ensure that the channel can't be cancelled via nsIInterceptedChannel after making a decision
+add_test(function () {
+ var chan = make_channel(URL + "/body", null, function (channel) {
+ channel.resetInterception(false);
+ do_timeout(0, function () {
+ var gotexception = false;
+ try {
+ channel.cancelInterception();
+ } catch (x) {
+ gotexception = true;
+ }
+ Assert.ok(gotexception);
+ });
+ });
+ chan.asyncOpen(new ChannelListener(handle_remote_response, null));
+});
+
+// ensure that the intercepted channel can be canceled during the response
+add_test(function () {
+ var chan = make_channel(URL + "/body", null, function (intercepted) {
+ var synthesized = Cc[
+ "@mozilla.org/io/string-input-stream;1"
+ ].createInstance(Ci.nsIStringInputStream);
+ synthesized.data = NON_REMOTE_BODY;
+
+ let channel = intercepted.channel;
+ intercepted.startSynthesizedResponse(synthesized, null, null, "", false);
+ intercepted.finishSynthesizedResponse();
+ channel.cancel(Cr.NS_BINDING_ABORTED);
+ });
+ chan.asyncOpen(
+ new ChannelListener(
+ run_next_test,
+ null,
+ CL_EXPECT_FAILURE | CL_ALLOW_UNKNOWN_CL
+ )
+ );
+});
+
+// ensure that the intercepted channel can be canceled before the response
+add_test(function () {
+ var chan = make_channel(URL + "/body", null, function (intercepted) {
+ var synthesized = Cc[
+ "@mozilla.org/io/string-input-stream;1"
+ ].createInstance(Ci.nsIStringInputStream);
+ synthesized.data = NON_REMOTE_BODY;
+
+ intercepted.channel.cancel(Cr.NS_BINDING_ABORTED);
+
+ // This should not throw, but result in the channel firing callbacks
+ // with an error status.
+ intercepted.startSynthesizedResponse(synthesized, null, null, "", false);
+ intercepted.finishSynthesizedResponse();
+ });
+ chan.asyncOpen(
+ new ChannelListener(
+ run_next_test,
+ null,
+ CL_EXPECT_FAILURE | CL_ALLOW_UNKNOWN_CL
+ )
+ );
+});
+
+// Ensure that nsIInterceptedChannel.channelIntercepted() can return an error.
+// In this case we should automatically ResetInterception() and complete the
+// network request.
+add_test(function () {
+ var chan = make_channel(URL + "/body", null, function (channel) {
+ throw new Error("boom");
+ });
+ chan.asyncOpen(new ChannelListener(handle_remote_response, null));
+});
+
+add_test(function () {
+ httpServer.stop(run_next_test);
+});
diff --git a/netwerk/test/unit/test_throttlechannel.js b/netwerk/test/unit/test_throttlechannel.js
new file mode 100644
index 0000000000..5a0cfd1151
--- /dev/null
+++ b/netwerk/test/unit/test_throttlechannel.js
@@ -0,0 +1,48 @@
+// Test nsIThrottledInputChannel interface.
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+function test_handler(metadata, response) {
+ const originalBody = "the response";
+ response.setHeader("Content-Type", "text/html", false);
+ response.setStatusLine(metadata.httpVersion, 200, "OK");
+ response.bodyOutputStream.write(originalBody, originalBody.length);
+}
+
+function make_channel(url) {
+ return NetUtil.newChannel({
+ uri: url,
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+}
+
+function run_test() {
+ let httpserver = new HttpServer();
+ httpserver.start(-1);
+ const PORT = httpserver.identity.primaryPort;
+
+ httpserver.registerPathHandler("/testdir", test_handler);
+
+ let channel = make_channel("http://localhost:" + PORT + "/testdir");
+
+ let tq = Cc["@mozilla.org/network/throttlequeue;1"].createInstance(
+ Ci.nsIInputChannelThrottleQueue
+ );
+ tq.init(1000, 1000);
+
+ let tic = channel.QueryInterface(Ci.nsIThrottledInputChannel);
+ tic.throttleQueue = tq;
+
+ channel.asyncOpen(
+ new ChannelListener(() => {
+ ok(tq.bytesProcessed() > 0, "throttled queue processed some bytes");
+
+ httpserver.stop(do_test_finished);
+ })
+ );
+
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_throttlequeue.js b/netwerk/test/unit/test_throttlequeue.js
new file mode 100644
index 0000000000..4fa1eaa0e2
--- /dev/null
+++ b/netwerk/test/unit/test_throttlequeue.js
@@ -0,0 +1,25 @@
+// Test ThrottleQueue initialization.
+"use strict";
+
+function init(tq, mean, max) {
+ let threw = false;
+ try {
+ tq.init(mean, max);
+ } catch (e) {
+ threw = true;
+ }
+ return !threw;
+}
+
+function run_test() {
+ let tq = Cc["@mozilla.org/network/throttlequeue;1"].createInstance(
+ Ci.nsIInputChannelThrottleQueue
+ );
+
+ ok(!init(tq, 0, 50), "mean bytes cannot be 0");
+ ok(!init(tq, 50, 0), "max bytes cannot be 0");
+ ok(!init(tq, 0, 0), "mean and max bytes cannot be 0");
+ ok(!init(tq, 70, 20), "max cannot be less than mean");
+
+ ok(init(tq, 2, 2), "valid initialization");
+}
diff --git a/netwerk/test/unit/test_throttling.js b/netwerk/test/unit/test_throttling.js
new file mode 100644
index 0000000000..7627684dce
--- /dev/null
+++ b/netwerk/test/unit/test_throttling.js
@@ -0,0 +1,66 @@
+// Test nsIThrottledInputChannel interface.
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+function test_handler(metadata, response) {
+ const originalBody = "the response";
+ response.setHeader("Content-Type", "text/html", false);
+ response.setStatusLine(metadata.httpVersion, 200, "OK");
+ response.bodyOutputStream.write(originalBody, originalBody.length);
+}
+
+function make_channel(url) {
+ return NetUtil.newChannel({
+ uri: url,
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+}
+
+function run_test() {
+ let httpserver = new HttpServer();
+ httpserver.registerPathHandler("/testdir", test_handler);
+ httpserver.start(-1);
+
+ const PORT = httpserver.identity.primaryPort;
+ const size = 4096;
+
+ let sstream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(
+ Ci.nsIStringInputStream
+ );
+ sstream.data = "x".repeat(size);
+
+ let mime = Cc["@mozilla.org/network/mime-input-stream;1"].createInstance(
+ Ci.nsIMIMEInputStream
+ );
+ mime.addHeader("Content-Type", "multipart/form-data; boundary=zzzzz");
+ mime.setData(sstream);
+
+ let tq = Cc["@mozilla.org/network/throttlequeue;1"].createInstance(
+ Ci.nsIInputChannelThrottleQueue
+ );
+ // Make sure the request takes more than one read.
+ tq.init(100 + size / 2, 100 + size / 2);
+
+ let channel = make_channel("http://localhost:" + PORT + "/testdir");
+ channel
+ .QueryInterface(Ci.nsIUploadChannel)
+ .setUploadStream(mime, "", mime.available());
+ channel.requestMethod = "POST";
+
+ let tic = channel.QueryInterface(Ci.nsIThrottledInputChannel);
+ tic.throttleQueue = tq;
+
+ let startTime = Date.now();
+ channel.asyncOpen(
+ new ChannelListener(() => {
+ ok(Date.now() - startTime > 1000, "request took more than one second");
+
+ httpserver.stop(do_test_finished);
+ })
+ );
+
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_tldservice_nextsubdomain.js b/netwerk/test/unit/test_tldservice_nextsubdomain.js
new file mode 100644
index 0000000000..5a078aa809
--- /dev/null
+++ b/netwerk/test/unit/test_tldservice_nextsubdomain.js
@@ -0,0 +1,24 @@
+"use strict";
+
+function run_test() {
+ var tests = [
+ { data: "bar.foo.co.uk", result: "foo.co.uk" },
+ { data: "foo.bar.foo.co.uk", result: "bar.foo.co.uk" },
+ { data: "foo.co.uk", throw: true },
+ { data: "co.uk", throw: true },
+ { data: ".co.uk", throw: true },
+ { data: "com", throw: true },
+ { data: "tûlîp.foo.fr", result: "foo.fr" },
+ { data: "tûlîp.fôû.fr", result: "xn--f-xgav.fr" },
+ { data: "file://foo/bar", throw: true },
+ ];
+
+ tests.forEach(function (test) {
+ try {
+ var r = Services.eTLD.getNextSubDomain(test.data);
+ Assert.equal(r, test.result);
+ } catch (e) {
+ Assert.ok(test.throw);
+ }
+ });
+}
diff --git a/netwerk/test/unit/test_tls13_disabled.js b/netwerk/test/unit/test_tls13_disabled.js
new file mode 100644
index 0000000000..3bcb6333aa
--- /dev/null
+++ b/netwerk/test/unit/test_tls13_disabled.js
@@ -0,0 +1,93 @@
+/* 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 () => {
+ Services.prefs.clearUserPref("security.tls.version.max");
+ http3_clear_prefs();
+});
+
+let httpsUri;
+
+add_task(async function setup() {
+ let h2Port = Services.env.get("MOZHTTP2_PORT");
+ Assert.notEqual(h2Port, null);
+ Assert.notEqual(h2Port, "");
+ httpsUri = "https://foo.example.com:" + h2Port + "/";
+
+ await http3_setup_tests("h3");
+});
+
+let Listener = function () {};
+
+Listener.prototype = {
+ resumed: false,
+
+ onStartRequest: function testOnStartRequest(request) {
+ 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) {
+ let httpVersion = "";
+ try {
+ httpVersion = request.protocolVersion;
+ } catch (e) {}
+ if (this.expect_http3) {
+ Assert.equal(httpVersion, "h3");
+ } else {
+ Assert.notEqual(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) {
+ let chan = NetUtil.newChannel({
+ uri,
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+ chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI;
+ return chan;
+}
+
+async function test_http3_used(expect_http3) {
+ let listener = new Listener();
+ listener.expect_http3 = expect_http3;
+ let chan = makeChan(httpsUri);
+ await chanPromise(chan, listener);
+}
+
+add_task(async function test_tls13_pref() {
+ await test_http3_used(true);
+ // Try one more time.
+ await test_http3_used(true);
+
+ // Disable TLS1.3
+ Services.prefs.setIntPref("security.tls.version.max", 3);
+ await test_http3_used(false);
+ // Try one more time.
+ await test_http3_used(false);
+
+ // Enable TLS1.3
+ Services.obs.notifyObservers(null, "net:cancel-all-connections");
+ Services.prefs.setIntPref("security.tls.version.max", 4);
+ await test_http3_used(true);
+ // Try one more time.
+ await test_http3_used(true);
+});
diff --git a/netwerk/test/unit/test_tls_flags.js b/netwerk/test/unit/test_tls_flags.js
new file mode 100644
index 0000000000..876cd0ccea
--- /dev/null
+++ b/netwerk/test/unit/test_tls_flags.js
@@ -0,0 +1,248 @@
+// -*- 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";
+
+// a fork of test_be_conservative
+
+// Tests that nsIHttpChannelInternal.tlsFlags can be used to set the
+// client max version level. Flags can also be used to set the
+// level of intolerance rollback and to test out an experimental 1.3
+// hello, though they are not tested here.
+
+// Get a profile directory and ensure PSM initializes NSS.
+do_get_profile();
+Cc["@mozilla.org/psm;1"].getService(Ci.nsISupports);
+
+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, expectedVersion) {
+ this.input = input;
+ this.output = output;
+ this.expectedVersion = expectedVersion;
+ this.callbacks = [];
+ this.stopped = false;
+ }
+
+ onHandshakeDone(socket, status) {
+ info("TLS handshake done");
+ info(`TLS version used: ${status.tlsVersionUsed}`);
+ info(this.expectedVersion);
+ equal(
+ status.tlsVersionUsed,
+ this.expectedVersion,
+ "expected version check"
+ );
+ 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();
+ });
+ }
+}
+
+function startServer(
+ cert,
+ minServerVersion,
+ maxServerVersion,
+ expectedVersion
+) {
+ 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);
+
+ let listener = {
+ securityObservers: [],
+
+ onSocketAccepted(socket, transport) {
+ info("accepted TLS client connection");
+ let connectionInfo = transport.securityCallbacks.getInterface(
+ Ci.nsITLSServerConnectionInfo
+ );
+ let input = transport.openInputStream(0, 0, 0);
+ let output = transport.openOutputStream(0, 0, 0);
+ let securityObserver = new TLSServerSecurityObserver(
+ input,
+ output,
+ expectedVersion
+ );
+ 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();
+ });
+ },
+ };
+ tlsServer.asyncListen(listener);
+ return tlsServer;
+}
+
+const hostname = "example.com";
+
+function storeCertOverride(port, cert) {
+ let certOverrideService = Cc[
+ "@mozilla.org/security/certoverride;1"
+ ].getService(Ci.nsICertOverrideService);
+ certOverrideService.rememberValidityOverride(hostname, port, {}, cert, true);
+}
+
+function startClient(port, tlsFlags, expectSuccess) {
+ let req = new XMLHttpRequest();
+ req.open("GET", `https://${hostname}:${port}`);
+ let internalChannel = req.channel.QueryInterface(Ci.nsIHttpChannelInternal);
+ internalChannel.tlsFlags = tlsFlags;
+ 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 = getTestServerCertificate();
+
+ // server that accepts 1.1->1.3 and a client max 1.3. expect 1.3
+ info("TEST 1");
+ let server = startServer(
+ cert,
+ Ci.nsITLSClientStatus.TLS_VERSION_1_1,
+ Ci.nsITLSClientStatus.TLS_VERSION_1_3,
+ Ci.nsITLSClientStatus.TLS_VERSION_1_3
+ );
+ storeCertOverride(server.port, cert);
+ await startClient(server.port, 4, true /*should succeed*/);
+ server.close();
+
+ // server that accepts 1.1->1.3 and a client max 1.1. expect 1.1
+ info("TEST 2");
+ server = startServer(
+ cert,
+ Ci.nsITLSClientStatus.TLS_VERSION_1_1,
+ Ci.nsITLSClientStatus.TLS_VERSION_1_3,
+ Ci.nsITLSClientStatus.TLS_VERSION_1_1
+ );
+ storeCertOverride(server.port, cert);
+ await startClient(server.port, 2, true);
+ server.close();
+
+ // server that accepts 1.2->1.2 and a client max 1.3. expect 1.2
+ info("TEST 3");
+ server = startServer(
+ cert,
+ Ci.nsITLSClientStatus.TLS_VERSION_1_2,
+ Ci.nsITLSClientStatus.TLS_VERSION_1_2,
+ Ci.nsITLSClientStatus.TLS_VERSION_1_2
+ );
+ storeCertOverride(server.port, cert);
+ await startClient(server.port, 4, true);
+ server.close();
+
+ // server that accepts 1.2->1.2 and a client max 1.1. expect fail
+ info("TEST 4");
+ server = startServer(
+ cert,
+ Ci.nsITLSClientStatus.TLS_VERSION_1_2,
+ Ci.nsITLSClientStatus.TLS_VERSION_1_2,
+ 0
+ );
+ storeCertOverride(server.port, cert);
+ await startClient(server.port, 2, false);
+
+ server.close();
+});
+
+registerCleanupFunction(function () {
+ Services.prefs.clearUserPref("security.tls.version.max");
+ Services.prefs.clearUserPref("network.dns.localDomains");
+});
diff --git a/netwerk/test/unit/test_tls_flags_separate_connections.js b/netwerk/test/unit/test_tls_flags_separate_connections.js
new file mode 100644
index 0000000000..a67c15d8a6
--- /dev/null
+++ b/netwerk/test/unit/test_tls_flags_separate_connections.js
@@ -0,0 +1,117 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+ChromeUtils.defineLazyGetter(this, "URL", function () {
+ return "http://localhost:" + httpserv.identity.primaryPort;
+});
+
+// This unit test ensures connections with different tlsFlags have their own
+// connection pool. We verify this behavior by opening channels with different
+// tlsFlags, and their connection info's hash keys should be different.
+
+// In the first round of this test, we record the hash key for each connection.
+// In the second round, we check if each connection's hash key is consistent
+// and different from other connection's hash key.
+
+let httpserv = null;
+let gSecondRoundStarted = false;
+
+let randomFlagValues = [
+ 0x00000000,
+
+ 0xffffffff,
+
+ 0x12345678, 0x12345678,
+
+ 0x11111111, 0x22222222,
+
+ 0xaaaaaaaa, 0x77777777,
+
+ 0xbbbbbbbb, 0xcccccccc,
+];
+
+function handler(metadata, response) {
+ response.setHeader("Content-Type", "text/plain", false);
+ response.setHeader("Cache-Control", "no-cache", false);
+ response.setStatusLine(metadata.httpVersion, 200, "OK");
+ let body = "0123456789";
+ response.bodyOutputStream.write(body, body.length);
+}
+
+function makeChan(url, tlsFlags) {
+ let chan = NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true });
+ chan.QueryInterface(Ci.nsIHttpChannelInternal);
+ chan.tlsFlags = tlsFlags;
+
+ return chan;
+}
+
+let previousHashKeys = {};
+
+function Listener(tlsFlags) {
+ this.tlsFlags = tlsFlags;
+}
+
+let gTestsRun = 0;
+Listener.prototype = {
+ onStartRequest(request) {
+ request
+ .QueryInterface(Ci.nsIHttpChannel)
+ .QueryInterface(Ci.nsIHttpChannelInternal);
+
+ Assert.equal(request.tlsFlags, this.tlsFlags);
+
+ let hashKey = request.connectionInfoHashKey;
+ if (gSecondRoundStarted) {
+ // Compare the hash keys with the previous set ones.
+ // Hash keys should match if and only if their tlsFlags are the same.
+ for (let tlsFlags of randomFlagValues) {
+ if (tlsFlags == this.tlsFlags) {
+ Assert.equal(hashKey, previousHashKeys[tlsFlags]);
+ } else {
+ Assert.notEqual(hashKey, previousHashKeys[tlsFlags]);
+ }
+ }
+ } else {
+ // Set the hash keys in the first round.
+ previousHashKeys[this.tlsFlags] = hashKey;
+ }
+ },
+ onDataAvailable(request, stream, off, cnt) {
+ read_stream(stream, cnt);
+ },
+ onStopRequest() {
+ gTestsRun++;
+ if (gTestsRun == randomFlagValues.length) {
+ gTestsRun = 0;
+ if (gSecondRoundStarted) {
+ // The second round finishes.
+ httpserv.stop(do_test_finished);
+ } else {
+ // The first round finishes. Do the second round.
+ gSecondRoundStarted = true;
+ doTest();
+ }
+ }
+ },
+};
+
+function doTest() {
+ for (let tlsFlags of randomFlagValues) {
+ let chan = makeChan(URL, tlsFlags);
+ let listener = new Listener(tlsFlags);
+ chan.asyncOpen(listener);
+ }
+}
+
+function run_test() {
+ do_test_pending();
+ httpserv = new HttpServer();
+ httpserv.registerPathHandler("/", handler);
+ httpserv.start(-1);
+
+ doTest();
+}
diff --git a/netwerk/test/unit/test_tls_server.js b/netwerk/test/unit/test_tls_server.js
new file mode 100644
index 0000000000..5183f21fd7
--- /dev/null
+++ b/netwerk/test/unit/test_tls_server.js
@@ -0,0 +1,314 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Need profile dir to store the key / cert
+do_get_profile();
+// Ensure PSM is initialized
+Cc["@mozilla.org/psm;1"].getService(Ci.nsISupports);
+
+const { MockRegistrar } = ChromeUtils.importESModule(
+ "resource://testing-common/MockRegistrar.sys.mjs"
+);
+const certOverrideService = Cc[
+ "@mozilla.org/security/certoverride;1"
+].getService(Ci.nsICertOverrideService);
+const socketTransportService = Cc[
+ "@mozilla.org/network/socket-transport-service;1"
+].getService(Ci.nsISocketTransportService);
+
+const prefs = Services.prefs;
+
+function areCertsEqual(certA, certB) {
+ let derA = certA.getRawDER();
+ let derB = certB.getRawDER();
+ if (derA.length != derB.length) {
+ return false;
+ }
+ for (let i = 0; i < derA.length; i++) {
+ if (derA[i] != derB[i]) {
+ return false;
+ }
+ }
+ return true;
+}
+
+function startServer(
+ cert,
+ expectingPeerCert,
+ clientCertificateConfig,
+ expectedVersion,
+ expectedVersionStr
+) {
+ let tlsServer = Cc["@mozilla.org/network/tls-server-socket;1"].createInstance(
+ Ci.nsITLSServerSocket
+ );
+ tlsServer.init(-1, true, -1);
+ tlsServer.serverCert = cert;
+
+ let input, output;
+
+ let listener = {
+ onSocketAccepted(socket, transport) {
+ info("Accept TLS client connection");
+ let connectionInfo = transport.securityCallbacks.getInterface(
+ Ci.nsITLSServerConnectionInfo
+ );
+ connectionInfo.setSecurityObserver(listener);
+ input = transport.openInputStream(0, 0, 0);
+ output = transport.openOutputStream(0, 0, 0);
+ },
+ onHandshakeDone(socket, status) {
+ info("TLS handshake done");
+ if (expectingPeerCert) {
+ ok(!!status.peerCert, "Has peer cert");
+ ok(
+ areCertsEqual(status.peerCert, cert),
+ "Peer cert matches expected cert"
+ );
+ } else {
+ ok(!status.peerCert, "No peer cert (as expected)");
+ }
+
+ equal(
+ status.tlsVersionUsed,
+ expectedVersion,
+ "Using " + expectedVersionStr
+ );
+ let expectedCipher;
+ if (expectedVersion >= 772) {
+ expectedCipher = "TLS_AES_128_GCM_SHA256";
+ } else {
+ expectedCipher = "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256";
+ }
+ equal(status.cipherName, expectedCipher, "Using expected cipher");
+ equal(status.keyLength, 128, "Using 128-bit key");
+ equal(status.macLength, 128, "Using 128-bit MAC");
+
+ input.asyncWait(
+ {
+ onInputStreamReady(input1) {
+ NetUtil.asyncCopy(input1, output);
+ },
+ },
+ 0,
+ 0,
+ Services.tm.currentThread
+ );
+ },
+ onStopListening() {
+ info("onStopListening");
+ input.close();
+ output.close();
+ },
+ };
+
+ tlsServer.setSessionTickets(false);
+ tlsServer.setRequestClientCertificate(clientCertificateConfig);
+
+ tlsServer.asyncListen(listener);
+
+ return tlsServer;
+}
+
+function storeCertOverride(port, cert) {
+ certOverrideService.rememberValidityOverride(
+ "127.0.0.1",
+ port,
+ {},
+ cert,
+ true
+ );
+}
+
+function startClient(port, sendClientCert, expectingAlert, tlsVersion) {
+ gClientAuthDialogService.selectCertificate = sendClientCert;
+ let SSL_ERROR_BASE = Ci.nsINSSErrorsService.NSS_SSL_ERROR_BASE;
+ let SSL_ERROR_BAD_CERT_ALERT = SSL_ERROR_BASE + 17;
+ let SSL_ERROR_RX_CERTIFICATE_REQUIRED_ALERT = SSL_ERROR_BASE + 181;
+ let transport = socketTransportService.createTransport(
+ ["ssl"],
+ "127.0.0.1",
+ port,
+ null,
+ null
+ );
+ let input;
+ let output;
+
+ let inputDeferred = Promise.withResolvers();
+ let outputDeferred = Promise.withResolvers();
+
+ let handler = {
+ onTransportStatus(transport1, status) {
+ if (status === Ci.nsISocketTransport.STATUS_CONNECTED_TO) {
+ output.asyncWait(handler, 0, 0, Services.tm.currentThread);
+ }
+ },
+
+ onInputStreamReady(input1) {
+ try {
+ let data = NetUtil.readInputStreamToString(input1, input1.available());
+ equal(data, "HELLO", "Echoed data received");
+ input1.close();
+ output.close();
+ ok(!expectingAlert, "No cert alert expected");
+ inputDeferred.resolve();
+ } catch (e) {
+ let errorCode = -1 * (e.result & 0xffff);
+ if (expectingAlert) {
+ if (
+ tlsVersion == Ci.nsITLSClientStatus.TLS_VERSION_1_2 &&
+ errorCode == SSL_ERROR_BAD_CERT_ALERT
+ ) {
+ info("Got bad cert alert as expected for tls 1.2");
+ input1.close();
+ output.close();
+ inputDeferred.resolve();
+ return;
+ }
+ if (
+ tlsVersion == Ci.nsITLSClientStatus.TLS_VERSION_1_3 &&
+ errorCode == SSL_ERROR_RX_CERTIFICATE_REQUIRED_ALERT
+ ) {
+ info("Got cert required alert as expected for tls 1.3");
+ input1.close();
+ output.close();
+ inputDeferred.resolve();
+ return;
+ }
+ }
+ inputDeferred.reject(e);
+ }
+ },
+
+ onOutputStreamReady(output1) {
+ try {
+ output1.write("HELLO", 5);
+ info("Output to server written");
+ outputDeferred.resolve();
+ input = transport.openInputStream(0, 0, 0);
+ input.asyncWait(handler, 0, 0, Services.tm.currentThread);
+ } catch (e) {
+ let errorCode = -1 * (e.result & 0xffff);
+ if (errorCode == SSL_ERROR_BAD_CERT_ALERT) {
+ info("Server doesn't like client cert");
+ }
+ outputDeferred.reject(e);
+ }
+ },
+ };
+
+ transport.setEventSink(handler, Services.tm.currentThread);
+ output = transport.openOutputStream(0, 0, 0);
+
+ return Promise.all([inputDeferred.promise, outputDeferred.promise]);
+}
+
+// Replace the UI dialog that prompts the user to pick a client certificate.
+const gClientAuthDialogService = {
+ _selectCertificate: false,
+
+ set selectCertificate(value) {
+ this._selectCertificate = value;
+ },
+
+ chooseCertificate(hostname, certArray, loadContext, callback) {
+ if (this._selectCertificate) {
+ callback.certificateChosen(certArray[0], false);
+ } else {
+ callback.certificateChose(null, false);
+ }
+ },
+
+ QueryInterface: ChromeUtils.generateQI([Ci.nsIClientAuthDialogService]),
+};
+
+const ClientAuthDialogServiceContractID =
+ "@mozilla.org/security/ClientAuthDialogService;1";
+MockRegistrar.register(
+ ClientAuthDialogServiceContractID,
+ gClientAuthDialogService
+);
+
+const tests = [
+ {
+ expectingPeerCert: true,
+ clientCertificateConfig: Ci.nsITLSServerSocket.REQUIRE_ALWAYS,
+ sendClientCert: true,
+ expectingAlert: false,
+ },
+ {
+ expectingPeerCert: true,
+ clientCertificateConfig: Ci.nsITLSServerSocket.REQUIRE_ALWAYS,
+ sendClientCert: false,
+ expectingAlert: true,
+ },
+ {
+ expectingPeerCert: true,
+ clientCertificateConfig: Ci.nsITLSServerSocket.REQUEST_ALWAYS,
+ sendClientCert: true,
+ expectingAlert: false,
+ },
+ {
+ expectingPeerCert: false,
+ clientCertificateConfig: Ci.nsITLSServerSocket.REQUEST_ALWAYS,
+ sendClientCert: false,
+ expectingAlert: false,
+ },
+ {
+ expectingPeerCert: false,
+ clientCertificateConfig: Ci.nsITLSServerSocket.REQUEST_NEVER,
+ sendClientCert: true,
+ expectingAlert: false,
+ },
+ {
+ expectingPeerCert: false,
+ clientCertificateConfig: Ci.nsITLSServerSocket.REQUEST_NEVER,
+ sendClientCert: false,
+ expectingAlert: false,
+ },
+];
+
+const versions = [
+ {
+ prefValue: 3,
+ version: Ci.nsITLSClientStatus.TLS_VERSION_1_2,
+ versionStr: "TLS 1.2",
+ },
+ {
+ prefValue: 4,
+ version: Ci.nsITLSClientStatus.TLS_VERSION_1_3,
+ versionStr: "TLS 1.3",
+ },
+];
+
+add_task(async function () {
+ let cert = getTestServerCertificate();
+ ok(!!cert, "Got self-signed cert");
+ for (let v of versions) {
+ prefs.setIntPref("security.tls.version.max", v.prefValue);
+ for (let t of tests) {
+ let server = startServer(
+ cert,
+ t.expectingPeerCert,
+ t.clientCertificateConfig,
+ v.version,
+ v.versionStr
+ );
+ storeCertOverride(server.port, cert);
+ await startClient(
+ server.port,
+ t.sendClientCert,
+ t.expectingAlert,
+ v.version
+ );
+ server.close();
+ }
+ }
+});
+
+registerCleanupFunction(function () {
+ prefs.clearUserPref("security.tls.version.max");
+});
diff --git a/netwerk/test/unit/test_tls_server_multiple_clients.js b/netwerk/test/unit/test_tls_server_multiple_clients.js
new file mode 100644
index 0000000000..fdafd9744d
--- /dev/null
+++ b/netwerk/test/unit/test_tls_server_multiple_clients.js
@@ -0,0 +1,130 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Need profile dir to store the key / cert
+do_get_profile();
+// Ensure PSM is initialized
+Cc["@mozilla.org/psm;1"].getService(Ci.nsISupports);
+
+const certOverrideService = Cc[
+ "@mozilla.org/security/certoverride;1"
+].getService(Ci.nsICertOverrideService);
+const socketTransportService = Cc[
+ "@mozilla.org/network/socket-transport-service;1"
+].getService(Ci.nsISocketTransportService);
+
+function startServer(cert) {
+ let tlsServer = Cc["@mozilla.org/network/tls-server-socket;1"].createInstance(
+ Ci.nsITLSServerSocket
+ );
+ tlsServer.init(-1, true, -1);
+ tlsServer.serverCert = cert;
+
+ let input, output;
+
+ let listener = {
+ onSocketAccepted(socket, transport) {
+ info("Accept TLS client connection");
+ let connectionInfo = transport.securityCallbacks.getInterface(
+ Ci.nsITLSServerConnectionInfo
+ );
+ connectionInfo.setSecurityObserver(listener);
+ input = transport.openInputStream(0, 0, 0);
+ output = transport.openOutputStream(0, 0, 0);
+ },
+ onHandshakeDone(socket, status) {
+ info("TLS handshake done");
+
+ input.asyncWait(
+ {
+ onInputStreamReady(input1) {
+ NetUtil.asyncCopy(input1, output);
+ },
+ },
+ 0,
+ 0,
+ Services.tm.currentThread
+ );
+ },
+ onStopListening() {},
+ };
+
+ tlsServer.setSessionTickets(false);
+
+ tlsServer.asyncListen(listener);
+
+ return tlsServer.port;
+}
+
+function storeCertOverride(port, cert) {
+ certOverrideService.rememberValidityOverride(
+ "127.0.0.1",
+ port,
+ {},
+ cert,
+ true
+ );
+}
+
+function startClient(port) {
+ let transport = socketTransportService.createTransport(
+ ["ssl"],
+ "127.0.0.1",
+ port,
+ null,
+ null
+ );
+ let input;
+ let output;
+
+ let inputDeferred = Promise.withResolvers();
+ let outputDeferred = Promise.withResolvers();
+
+ let handler = {
+ onTransportStatus(transport1, status) {
+ if (status === Ci.nsISocketTransport.STATUS_CONNECTED_TO) {
+ output.asyncWait(handler, 0, 0, Services.tm.currentThread);
+ }
+ },
+
+ onInputStreamReady(input1) {
+ try {
+ let data = NetUtil.readInputStreamToString(input1, input1.available());
+ equal(data, "HELLO", "Echoed data received");
+ input1.close();
+ output.close();
+ inputDeferred.resolve();
+ } catch (e) {
+ inputDeferred.reject(e);
+ }
+ },
+
+ onOutputStreamReady(output1) {
+ try {
+ output1.write("HELLO", 5);
+ info("Output to server written");
+ outputDeferred.resolve();
+ input = transport.openInputStream(0, 0, 0);
+ input.asyncWait(handler, 0, 0, Services.tm.currentThread);
+ } catch (e) {
+ outputDeferred.reject(e);
+ }
+ },
+ };
+
+ transport.setEventSink(handler, Services.tm.currentThread);
+ output = transport.openOutputStream(0, 0, 0);
+
+ return Promise.all([inputDeferred.promise, outputDeferred.promise]);
+}
+
+add_task(async function () {
+ let cert = getTestServerCertificate();
+ ok(!!cert, "Got self-signed cert");
+ let port = startServer(cert);
+ storeCertOverride(port, cert);
+ await startClient(port);
+ await startClient(port);
+});
diff --git a/netwerk/test/unit/test_traceable_channel.js b/netwerk/test/unit/test_traceable_channel.js
new file mode 100644
index 0000000000..3390682bcf
--- /dev/null
+++ b/netwerk/test/unit/test_traceable_channel.js
@@ -0,0 +1,145 @@
+"use strict";
+
+// Test nsITraceableChannel interface.
+// Replace original listener with TracingListener that modifies body of HTTP
+// response. Make sure that body received by original channel's listener
+// is correctly modified.
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+var httpserver = new HttpServer();
+httpserver.start(-1);
+const PORT = httpserver.identity.primaryPort;
+
+var pipe = null;
+var streamSink = null;
+
+var originalBody = "original http response body";
+var gotOnStartRequest = false;
+
+function TracingListener() {}
+
+TracingListener.prototype = {
+ onStartRequest(request) {
+ dump("*** tracing listener onStartRequest\n");
+
+ gotOnStartRequest = true;
+
+ request.QueryInterface(Ci.nsIHttpChannelInternal);
+
+ // local/remote addresses broken in e10s: disable for now
+ 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);
+
+ // Make sure listener can't be replaced after OnStartRequest was called.
+ request.QueryInterface(Ci.nsITraceableChannel);
+ try {
+ var newListener = new TracingListener();
+ newListener.listener = request.setNewListener(newListener);
+ } catch (e) {
+ dump("TracingListener.onStartRequest swallowing exception: " + e + "\n");
+ return; // OK
+ }
+ do_throw("replaced channel's listener during onStartRequest.");
+ },
+
+ onStopRequest(request, statusCode) {
+ dump("*** tracing listener onStopRequest\n");
+
+ Assert.equal(gotOnStartRequest, true);
+
+ try {
+ var sin = Cc["@mozilla.org/scriptableinputstream;1"].createInstance(
+ Ci.nsIScriptableInputStream
+ );
+
+ streamSink.close();
+ var input = pipe.inputStream;
+ sin.init(input);
+ Assert.equal(sin.available(), originalBody.length);
+
+ var result = sin.read(originalBody.length);
+ Assert.equal(result, originalBody);
+
+ input.close();
+ } catch (e) {
+ dump("TracingListener.onStopRequest swallowing exception: " + e + "\n");
+ } finally {
+ httpserver.stop(do_test_finished);
+ }
+ },
+
+ QueryInterface: ChromeUtils.generateQI(["nsIRequestObserver"]),
+
+ listener: null,
+};
+
+function HttpResponseExaminer() {}
+
+HttpResponseExaminer.prototype = {
+ register() {
+ Services.obs.addObserver(this, "http-on-examine-response", true);
+ dump("Did HttpResponseExaminer.register\n");
+ },
+
+ // Replace channel's listener.
+ observe(subject, topic, data) {
+ dump("In HttpResponseExaminer.observe\n");
+ try {
+ subject.QueryInterface(Ci.nsITraceableChannel);
+
+ var tee = Cc["@mozilla.org/network/stream-listener-tee;1"].createInstance(
+ Ci.nsIStreamListenerTee
+ );
+ var newListener = new TracingListener();
+ pipe = Cc["@mozilla.org/pipe;1"].createInstance(Ci.nsIPipe);
+ pipe.init(false, false, 0, 0xffffffff, null);
+ streamSink = pipe.outputStream;
+
+ var originalListener = subject.setNewListener(tee);
+ tee.init(originalListener, streamSink, newListener);
+ } catch (e) {
+ do_throw("can't replace listener " + e);
+ }
+ dump("Did HttpResponseExaminer.observe\n");
+ },
+
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIObserver",
+ "nsISupportsWeakReference",
+ ]),
+};
+
+function test_handler(metadata, response) {
+ response.setHeader("Content-Type", "text/html", false);
+ response.setStatusLine(metadata.httpVersion, 200, "OK");
+ response.bodyOutputStream.write(originalBody, originalBody.length);
+}
+
+function make_channel(url) {
+ return NetUtil.newChannel({
+ uri: url,
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+}
+
+// Check if received body is correctly modified.
+function channel_finished(request, input, ctx) {
+ httpserver.stop(do_test_finished);
+}
+
+function run_test() {
+ var observer = new HttpResponseExaminer();
+ observer.register();
+
+ httpserver.registerPathHandler("/testdir", test_handler);
+
+ var channel = make_channel("http://localhost:" + PORT + "/testdir");
+ channel.asyncOpen(new ChannelListener(channel_finished));
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_trackingProtection_annotateChannels.js b/netwerk/test/unit/test_trackingProtection_annotateChannels.js
new file mode 100644
index 0000000000..a54005b8e2
--- /dev/null
+++ b/netwerk/test/unit/test_trackingProtection_annotateChannels.js
@@ -0,0 +1,391 @@
+"use strict";
+
+const { UrlClassifierTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/UrlClassifierTestUtils.sys.mjs"
+);
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+// This test supports both e10s and non-e10s mode. In non-e10s mode, this test
+// drives itself by creating a profile directory, setting up the URL classifier
+// test tables and adjusting the prefs which are necessary to do the testing.
+// In e10s mode however, some of these operations such as creating a profile
+// directory, setting up the URL classifier test tables and setting prefs
+// aren't supported in the content process, so we split the test into two
+// parts, the part testing the normal priority case by setting both prefs to
+// false (test_trackingProtection_annotateChannels_wrap1.js), and the part
+// testing the lowest priority case by setting both prefs to true
+// (test_trackingProtection_annotateChannels_wrap2.js). These wrapper scripts
+// are also in charge of creating the profile directory and setting up the URL
+// classifier test tables.
+//
+// Below where we need to take different actions based on the process type we're
+// in, we use runtime.processType to take the correct actions.
+
+const runtime = Services.appinfo;
+if (runtime.processType == runtime.PROCESS_TYPE_DEFAULT) {
+ do_get_profile();
+}
+
+const defaultTopWindowURI = NetUtil.newURI("http://www.example.com/");
+
+function listener(tracking, priority, throttleable, nextTest) {
+ this._tracking = tracking;
+ this._priority = priority;
+ this._throttleable = throttleable;
+ this._nextTest = nextTest;
+}
+listener.prototype = {
+ onStartRequest(request) {
+ Assert.equal(
+ request
+ .QueryInterface(Ci.nsIClassifiedChannel)
+ .isThirdPartyTrackingResource(),
+ this._tracking,
+ "tracking flag"
+ );
+ Assert.equal(
+ request.QueryInterface(Ci.nsISupportsPriority).priority,
+ this._priority,
+ "channel priority"
+ );
+ if (runtime.processType == runtime.PROCESS_TYPE_DEFAULT && this._tracking) {
+ Assert.equal(
+ !!(
+ request.QueryInterface(Ci.nsIClassOfService).classFlags &
+ Ci.nsIClassOfService.Throttleable
+ ),
+ this._throttleable,
+ "throttleable flag"
+ );
+ }
+ request.cancel(Cr.NS_ERROR_ABORT);
+ this._nextTest();
+ },
+ onDataAvailable: (request, stream, offset, count) => {},
+ onStopRequest: (request, status) => {},
+};
+
+var httpServer;
+var normalOrigin, trackingOrigin;
+var testPriorityMap;
+var currentTest;
+// When this test is running in e10s mode, the parent process is in charge of
+// setting the prefs for us, so here we merely read our prefs, and if they have
+// been set we skip the normal priority test and only test the lowest priority
+// case, and if it they have not been set we skip the lowest priority test and
+// only test the normal priority case.
+// In non-e10s mode, both of these will remain false and we adjust the prefs
+// ourselves and test both of the cases in one go.
+var skipNormalPriority = false,
+ skipLowestPriority = false;
+
+function setup_test() {
+ httpServer = new HttpServer();
+ httpServer.start(-1);
+ httpServer.identity.setPrimary(
+ "http",
+ "tracking.example.org",
+ httpServer.identity.primaryPort
+ );
+ httpServer.identity.add(
+ "http",
+ "example.org",
+ httpServer.identity.primaryPort
+ );
+ normalOrigin = "http://localhost:" + httpServer.identity.primaryPort;
+ trackingOrigin =
+ "http://tracking.example.org:" + httpServer.identity.primaryPort;
+
+ if (runtime.processType == runtime.PROCESS_TYPE_CONTENT) {
+ if (
+ Services.prefs.getBoolPref(
+ "privacy.trackingprotection.annotate_channels"
+ ) &&
+ Services.prefs.getBoolPref(
+ "privacy.trackingprotection.lower_network_priority"
+ )
+ ) {
+ skipNormalPriority = true;
+ } else {
+ skipLowestPriority = true;
+ }
+ }
+
+ runTests();
+}
+
+function doPriorityTest() {
+ if (!testPriorityMap.length) {
+ runTests();
+ return;
+ }
+
+ currentTest = testPriorityMap.shift();
+
+ // Let's be explicit about what we're testing!
+ Assert.ok(
+ "loadingPrincipal" in currentTest,
+ "check for incomplete test case"
+ );
+ Assert.ok("topWindowURI" in currentTest, "check for incomplete test case");
+
+ var channel = makeChannel(
+ currentTest.path,
+ currentTest.loadingPrincipal,
+ currentTest.topWindowURI
+ );
+ channel.asyncOpen(
+ new listener(
+ currentTest.expectedTracking,
+ currentTest.expectedPriority,
+ currentTest.expectedThrottleable,
+ doPriorityTest
+ )
+ );
+}
+
+function makeChannel(path, loadingPrincipal, topWindowURI) {
+ var chan;
+
+ if (loadingPrincipal) {
+ chan = NetUtil.newChannel({
+ uri: path,
+ loadingPrincipal,
+ securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER,
+ });
+ } else {
+ chan = NetUtil.newChannel({
+ uri: path,
+ loadUsingSystemPrincipal: true,
+ });
+ }
+ chan.QueryInterface(Ci.nsIHttpChannel);
+ chan.requestMethod = "GET";
+ if (topWindowURI) {
+ chan
+ .QueryInterface(Ci.nsIHttpChannelInternal)
+ .setTopWindowURIIfUnknown(topWindowURI);
+ }
+ return chan;
+}
+
+var tests = [
+ // Create the HTTP server.
+ setup_test,
+
+ // Add the test table into tracking protection table.
+ function addTestTrackers() {
+ if (runtime.processType == runtime.PROCESS_TYPE_DEFAULT) {
+ UrlClassifierTestUtils.addTestTrackers().then(() => {
+ runTests();
+ });
+ } else {
+ runTests();
+ }
+ },
+
+ // Annotations OFF, normal loading principal, topWinURI of example.com
+ // => trackers should not be de-prioritized
+ function setupAnnotationsOff() {
+ if (skipNormalPriority) {
+ runTests();
+ return;
+ }
+ if (runtime.processType == runtime.PROCESS_TYPE_DEFAULT) {
+ Services.prefs.setBoolPref(
+ "privacy.trackingprotection.annotate_channels",
+ false
+ );
+ Services.prefs.setBoolPref(
+ "privacy.trackingprotection.lower_network_priority",
+ false
+ );
+ }
+ var principal =
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ normalOrigin
+ );
+ testPriorityMap = [
+ {
+ path: normalOrigin + "/innocent.css",
+ loadingPrincipal: principal,
+ topWindowURI: defaultTopWindowURI,
+ expectedTracking: false,
+ expectedPriority: Ci.nsISupportsPriority.PRIORITY_NORMAL,
+ expectedThrottleable: false, // ignored since tracking==false
+ },
+ {
+ path: normalOrigin + "/innocent.js",
+ loadingPrincipal: principal,
+ topWindowURI: defaultTopWindowURI,
+ expectedTracking: false,
+ expectedPriority: Ci.nsISupportsPriority.PRIORITY_NORMAL,
+ expectedThrottleable: false, // ignored since tracking==false
+ },
+ {
+ path: trackingOrigin + "/evil.css",
+ loadingPrincipal: principal,
+ topWindowURI: defaultTopWindowURI,
+ expectedTracking: false,
+ expectedPriority: Ci.nsISupportsPriority.PRIORITY_NORMAL,
+ expectedThrottleable: false, // ignored since tracking==false
+ },
+ {
+ path: trackingOrigin + "/evil.js",
+ loadingPrincipal: principal,
+ topWindowURI: defaultTopWindowURI,
+ expectedTracking: false,
+ expectedPriority: Ci.nsISupportsPriority.PRIORITY_NORMAL,
+ expectedThrottleable: false, // ignored since tracking==false
+ },
+ ];
+ // We add the doPriorityTest test here so that it only gets injected in the
+ // test list if we're not skipping over this test.
+ tests.unshift(doPriorityTest);
+ runTests();
+ },
+
+ // Annotations ON, normal loading principal, topWinURI of example.com
+ // => trackers should be de-prioritized
+ function setupAnnotationsOn() {
+ if (skipLowestPriority) {
+ runTests();
+ return;
+ }
+ if (runtime.processType == runtime.PROCESS_TYPE_DEFAULT) {
+ Services.prefs.setBoolPref(
+ "privacy.trackingprotection.annotate_channels",
+ true
+ );
+ Services.prefs.setBoolPref(
+ "privacy.trackingprotection.lower_network_priority",
+ true
+ );
+ }
+ var principal =
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ normalOrigin
+ );
+ testPriorityMap = [
+ {
+ path: normalOrigin + "/innocent.css",
+ loadingPrincipal: principal,
+ topWindowURI: defaultTopWindowURI,
+ expectedTracking: false,
+ expectedPriority: Ci.nsISupportsPriority.PRIORITY_NORMAL,
+ expectedThrottleable: false, // ignored since tracking==false
+ },
+ {
+ path: normalOrigin + "/innocent.js",
+ loadingPrincipal: principal,
+ topWindowURI: defaultTopWindowURI,
+ expectedTracking: false,
+ expectedPriority: Ci.nsISupportsPriority.PRIORITY_NORMAL,
+ expectedThrottleable: false, // ignored since tracking==false
+ },
+ {
+ path: trackingOrigin + "/evil.css",
+ loadingPrincipal: principal,
+ topWindowURI: defaultTopWindowURI,
+ expectedTracking: true,
+ expectedPriority: Ci.nsISupportsPriority.PRIORITY_LOWEST,
+ expectedThrottleable: true,
+ },
+ {
+ path: trackingOrigin + "/evil.js",
+ loadingPrincipal: principal,
+ topWindowURI: defaultTopWindowURI,
+ expectedTracking: true,
+ expectedPriority: Ci.nsISupportsPriority.PRIORITY_LOWEST,
+ expectedThrottleable: true,
+ },
+ ];
+ // We add the doPriorityTest test here so that it only gets injected in the
+ // test list if we're not skipping over this test.
+ tests.unshift(doPriorityTest);
+ runTests();
+ },
+
+ // Annotations ON, system loading principal, topWinURI of example.com
+ // => trackers should not be de-prioritized
+ function setupAnnotationsOnSystemPrincipal() {
+ if (skipLowestPriority) {
+ runTests();
+ return;
+ }
+ if (runtime.processType == runtime.PROCESS_TYPE_DEFAULT) {
+ Services.prefs.setBoolPref(
+ "privacy.trackingprotection.annotate_channels",
+ true
+ );
+ Services.prefs.setBoolPref(
+ "privacy.trackingprotection.lower_network_priority",
+ true
+ );
+ }
+ testPriorityMap = [
+ {
+ path: normalOrigin + "/innocent.css",
+ loadingPrincipal: null, // system principal
+ topWindowURI: defaultTopWindowURI,
+ expectedTracking: false,
+ expectedPriority: Ci.nsISupportsPriority.PRIORITY_NORMAL,
+ expectedThrottleable: false, // ignored since tracking==false
+ },
+ {
+ path: normalOrigin + "/innocent.js",
+ loadingPrincipal: null, // system principal
+ topWindowURI: defaultTopWindowURI,
+ expectedTracking: false,
+ expectedPriority: Ci.nsISupportsPriority.PRIORITY_NORMAL,
+ expectedThrottleable: false, // ignored since tracking==false
+ },
+ {
+ path: trackingOrigin + "/evil.css",
+ loadingPrincipal: null, // system principal
+ topWindowURI: defaultTopWindowURI,
+ expectedTracking: false,
+ expectedPriority: Ci.nsISupportsPriority.PRIORITY_NORMAL,
+ expectedThrottleable: false,
+ },
+ {
+ path: trackingOrigin + "/evil.js",
+ loadingPrincipal: null, // system principal
+ topWindowURI: defaultTopWindowURI,
+ expectedTracking: false,
+ expectedPriority: Ci.nsISupportsPriority.PRIORITY_NORMAL,
+ expectedThrottleable: true,
+ },
+ ];
+ // We add the doPriorityTest test here so that it only gets injected in the
+ // test list if we're not skipping over this test.
+ tests.unshift(doPriorityTest);
+ runTests();
+ },
+
+ function cleanUp() {
+ httpServer.stop(do_test_finished);
+ if (runtime.processType == runtime.PROCESS_TYPE_DEFAULT) {
+ UrlClassifierTestUtils.cleanupTestTrackers();
+ }
+ runTests();
+ },
+];
+
+function runTests() {
+ if (!tests.length) {
+ do_test_finished();
+ return;
+ }
+
+ var test = tests.shift();
+ test();
+}
+
+function run_test() {
+ runTests();
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_trr.js b/netwerk/test/unit/test_trr.js
new file mode 100644
index 0000000000..52b36620b1
--- /dev/null
+++ b/netwerk/test/unit/test_trr.js
@@ -0,0 +1,946 @@
+"use strict";
+
+/* import-globals-from trr_common.js */
+
+const gDefaultPref = Services.prefs.getDefaultBranch("");
+
+SetParentalControlEnabled(false);
+
+function setup() {
+ Services.prefs.setBoolPref("network.dns.get-ttl", false);
+ h2Port = trr_test_setup();
+}
+
+setup();
+registerCleanupFunction(async () => {
+ trr_clear_prefs();
+ Services.prefs.clearUserPref("network.dns.get-ttl");
+});
+
+async function waitForConfirmation(expectedResponseIP, confirmationShouldFail) {
+ // Check that the confirmation eventually completes.
+ let count = 100;
+ while (count > 0) {
+ if (count == 50 || count == 10) {
+ // At these two points we do a longer timeout to account for a slow
+ // response on the server side. This is usually a problem on the Android
+ // because of the increased delay between the emulator and host.
+ await new Promise(resolve => do_timeout(100 * (100 / count), resolve));
+ }
+ let { inRecord } = await new TRRDNSListener(
+ `ip${count}.example.org`,
+ undefined,
+ false
+ );
+ inRecord.QueryInterface(Ci.nsIDNSAddrRecord);
+ let responseIP = inRecord.getNextAddrAsString();
+ Assert.ok(true, responseIP);
+ if (responseIP == expectedResponseIP) {
+ break;
+ }
+ count--;
+ }
+
+ if (confirmationShouldFail) {
+ Assert.equal(count, 0, "Confirmation did not finish after 100 iterations");
+ return;
+ }
+
+ Assert.greater(count, 0, "Finished confirmation before 100 iterations");
+}
+
+function setModeAndURI(mode, path) {
+ Services.prefs.setIntPref("network.trr.mode", mode);
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://${TRR_Domain}:${h2Port}/${path}`
+ );
+}
+
+function makeChan(url, mode, bypassCache) {
+ let chan = NetUtil.newChannel({
+ uri: url,
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+ chan.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE;
+ chan.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING;
+ chan.setTRRMode(mode);
+ return chan;
+}
+
+add_task(async function test_server_up() {
+ // This test checks that moz-http2.js running in node is working.
+ // This should always be the first test in this file (except for setup)
+ // otherwise we may encounter random failures when the http2 server is down.
+
+ await NodeServer.execute("bad_id", `"hello"`)
+ .then(() => ok(false, "expecting to throw"))
+ .catch(e => equal(e.message, "Error: could not find id"));
+});
+
+add_task(async function test_trr_flags() {
+ Services.prefs.setBoolPref("network.trr.fallback-on-zero-response", true);
+ Services.prefs.setIntPref("network.trr.request_timeout_ms", 10000);
+ Services.prefs.setIntPref(
+ "network.trr.request_timeout_mode_trronly_ms",
+ 10000
+ );
+
+ let httpserv = new HttpServer();
+ httpserv.registerPathHandler("/", function handler(metadata, response) {
+ let content = "ok";
+ response.setHeader("Content-Length", `${content.length}`);
+ response.bodyOutputStream.write(content, content.length);
+ });
+ httpserv.start(-1);
+
+ const URL = `http://example.com:${httpserv.identity.primaryPort}/`;
+
+ for (let mode of [0, 1, 2, 3, 4, 5]) {
+ setModeAndURI(mode, "doh?responseIP=127.0.0.1");
+ for (let flag of [
+ Ci.nsIRequest.TRR_DEFAULT_MODE,
+ Ci.nsIRequest.TRR_DISABLED_MODE,
+ Ci.nsIRequest.TRR_FIRST_MODE,
+ Ci.nsIRequest.TRR_ONLY_MODE,
+ ]) {
+ Services.dns.clearCache(true);
+ let chan = makeChan(URL, flag);
+ let expectTRR =
+ ([2, 3].includes(mode) && flag != Ci.nsIRequest.TRR_DISABLED_MODE) ||
+ (mode == 0 &&
+ [Ci.nsIRequest.TRR_FIRST_MODE, Ci.nsIRequest.TRR_ONLY_MODE].includes(
+ flag
+ ));
+
+ await new Promise(resolve =>
+ chan.asyncOpen(new ChannelListener(resolve))
+ );
+
+ equal(chan.getTRRMode(), flag);
+ equal(
+ expectTRR,
+ chan.QueryInterface(Ci.nsIHttpChannelInternal).isResolvedByTRR
+ );
+ }
+ }
+
+ await new Promise(resolve => httpserv.stop(resolve));
+ Services.prefs.clearUserPref("network.trr.fallback-on-zero-response");
+ Services.prefs.clearUserPref("network.trr.request_timeout_ms");
+ Services.prefs.clearUserPref("network.trr.request_timeout_mode_trronly_ms");
+});
+
+add_task(test_A_record);
+
+add_task(async function test_push() {
+ info("Verify DOH push");
+ Services.dns.clearCache(true);
+ info("Asking server to push us a record");
+ setModeAndURI(3, "doh?responseIP=5.5.5.5&push=true");
+
+ await new TRRDNSListener("first.example.com", "5.5.5.5");
+
+ // 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.
+ // Don't clear the cache, otherwise we lose the pushed record.
+ setModeAndURI(3, "404");
+
+ await new TRRDNSListener("push.example.org", "2018::2018");
+});
+
+add_task(test_AAAA_records);
+
+add_task(test_RFC1918);
+
+add_task(test_GET_ECS);
+
+add_task(test_timeout_mode3);
+
+add_task(test_trr_retry);
+
+add_task(test_strict_native_fallback);
+
+add_task(test_no_answers_fallback);
+
+add_task(test_404_fallback);
+
+add_task(test_mode_1_and_4);
+
+add_task(test_CNAME);
+
+add_task(test_name_mismatch);
+
+add_task(test_mode_2);
+
+add_task(test_excluded_domains);
+
+add_task(test_captiveportal_canonicalURL);
+
+add_task(test_parentalcontrols);
+
+// TRR-first check that DNS result is used if domain is part of the builtin-excluded-domains pref
+add_task(test_builtin_excluded_domains);
+
+add_task(test_excluded_domains_mode3);
+
+add_task(test25e);
+
+add_task(test_parentalcontrols_mode3);
+
+add_task(test_builtin_excluded_domains_mode3);
+
+add_task(count_cookies);
+
+add_task(test_connection_closed);
+
+add_task(async function test_clearCacheOnURIChange() {
+ info("Check that the TRR cache should be cleared by a pref change.");
+ Services.dns.clearCache(true);
+ Services.prefs.setBoolPref("network.trr.clear-cache-on-pref-change", true);
+ setModeAndURI(2, "doh?responseIP=7.7.7.7");
+
+ await new TRRDNSListener("bar.example.com", "7.7.7.7");
+
+ // The TRR cache should be cleared by this pref change.
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://localhost:${h2Port}/doh?responseIP=8.8.8.8`
+ );
+
+ await new TRRDNSListener("bar.example.com", "8.8.8.8");
+ Services.prefs.setBoolPref("network.trr.clear-cache-on-pref-change", false);
+});
+
+add_task(async function test_dnsSuffix() {
+ info("Checking that domains matching dns suffix list use Do53");
+ async function checkDnsSuffixInMode(mode) {
+ Services.dns.clearCache(true);
+ setModeAndURI(mode, "doh?responseIP=1.2.3.4&push=true");
+ await new TRRDNSListener("example.org", "1.2.3.4");
+ await new TRRDNSListener("push.example.org", "2018::2018");
+ await new TRRDNSListener("test.com", "1.2.3.4");
+
+ let networkLinkService = {
+ dnsSuffixList: ["example.org"],
+ QueryInterface: ChromeUtils.generateQI(["nsINetworkLinkService"]),
+ };
+ Services.obs.notifyObservers(
+ networkLinkService,
+ "network:dns-suffix-list-updated"
+ );
+ await new TRRDNSListener("test.com", "1.2.3.4");
+ if (Services.prefs.getBoolPref("network.trr.split_horizon_mitigations")) {
+ await new TRRDNSListener("example.org", "127.0.0.1");
+ // Also test that we don't use the pushed entry.
+ await new TRRDNSListener("push.example.org", "127.0.0.1");
+ } else {
+ await new TRRDNSListener("example.org", "1.2.3.4");
+ await new TRRDNSListener("push.example.org", "2018::2018");
+ }
+
+ // Attempt to clean up, just in case
+ networkLinkService.dnsSuffixList = [];
+ Services.obs.notifyObservers(
+ networkLinkService,
+ "network:dns-suffix-list-updated"
+ );
+ }
+
+ Services.prefs.setBoolPref("network.trr.split_horizon_mitigations", true);
+ await checkDnsSuffixInMode(2);
+ Services.prefs.setCharPref("network.trr.bootstrapAddr", "127.0.0.1");
+ await checkDnsSuffixInMode(3);
+ Services.prefs.setBoolPref("network.trr.split_horizon_mitigations", false);
+ // Test again with mitigations off
+ await checkDnsSuffixInMode(2);
+ await checkDnsSuffixInMode(3);
+ Services.prefs.clearUserPref("network.trr.split_horizon_mitigations");
+ Services.prefs.clearUserPref("network.trr.bootstrapAddr");
+});
+
+add_task(async function test_async_resolve_with_trr_server() {
+ info("Checking asyncResolveWithTrrServer");
+ Services.dns.clearCache(true);
+ Services.prefs.setIntPref("network.trr.mode", 0); // TRR-disabled
+
+ await new TRRDNSListener(
+ "bar_with_trr1.example.com",
+ "2.2.2.2",
+ true,
+ undefined,
+ `https://foo.example.com:${h2Port}/doh?responseIP=2.2.2.2`
+ );
+
+ // Test request without trr server, it should return a native dns response.
+ await new TRRDNSListener("bar_with_trr1.example.com", "127.0.0.1");
+
+ // Mode 2
+ Services.dns.clearCache(true);
+ setModeAndURI(2, "doh?responseIP=2.2.2.2");
+
+ await new TRRDNSListener(
+ "bar_with_trr2.example.com",
+ "3.3.3.3",
+ true,
+ undefined,
+ `https://foo.example.com:${h2Port}/doh?responseIP=3.3.3.3`
+ );
+
+ // Test request without trr server, it should return a response from trr server defined in the pref.
+ await new TRRDNSListener("bar_with_trr2.example.com", "2.2.2.2");
+
+ // Mode 3
+ Services.dns.clearCache(true);
+ setModeAndURI(3, "doh?responseIP=2.2.2.2");
+
+ await new TRRDNSListener(
+ "bar_with_trr3.example.com",
+ "3.3.3.3",
+ true,
+ undefined,
+ `https://foo.example.com:${h2Port}/doh?responseIP=3.3.3.3`
+ );
+
+ // Test request without trr server, it should return a response from trr server defined in the pref.
+ await new TRRDNSListener("bar_with_trr3.example.com", "2.2.2.2");
+
+ // Mode 5
+ Services.dns.clearCache(true);
+ setModeAndURI(5, "doh?responseIP=2.2.2.2");
+
+ // When dns is resolved in socket process, we can't set |expectEarlyFail| to true.
+ let inSocketProcess = mozinfo.socketprocess_networking;
+ await new TRRDNSListener(
+ "bar_with_trr3.example.com",
+ undefined,
+ false,
+ undefined,
+ `https://foo.example.com:${h2Port}/doh?responseIP=3.3.3.3`,
+ !inSocketProcess
+ );
+
+ // Call normal AsyncOpen, it will return result from the native resolver.
+ await new TRRDNSListener("bar_with_trr3.example.com", "127.0.0.1");
+
+ // Check that cache is ignored when server is different
+ Services.dns.clearCache(true);
+ setModeAndURI(3, "doh?responseIP=2.2.2.2");
+
+ await new TRRDNSListener("bar_with_trr4.example.com", "2.2.2.2", true);
+
+ // The record will be fetch again.
+ await new TRRDNSListener(
+ "bar_with_trr4.example.com",
+ "3.3.3.3",
+ true,
+ undefined,
+ `https://foo.example.com:${h2Port}/doh?responseIP=3.3.3.3`
+ );
+
+ // The record will be fetch again.
+ await new TRRDNSListener(
+ "bar_with_trr5.example.com",
+ "4.4.4.4",
+ true,
+ undefined,
+ `https://foo.example.com:${h2Port}/doh?responseIP=4.4.4.4`
+ );
+
+ // Check no fallback and no blocklisting upon failure
+ Services.dns.clearCache(true);
+ setModeAndURI(2, "doh?responseIP=2.2.2.2");
+
+ let { inStatus } = await new TRRDNSListener(
+ "bar_with_trr6.example.com",
+ undefined,
+ false,
+ undefined,
+ `https://foo.example.com:${h2Port}/404`
+ );
+ Assert.ok(
+ !Components.isSuccessCode(inStatus),
+ `${inStatus} should be an error code`
+ );
+
+ await new TRRDNSListener("bar_with_trr6.example.com", "2.2.2.2", true);
+
+ // Check that DoH push doesn't work
+ Services.dns.clearCache(true);
+ setModeAndURI(2, "doh?responseIP=2.2.2.2");
+
+ await new TRRDNSListener(
+ "bar_with_trr7.example.com",
+ "3.3.3.3",
+ true,
+ undefined,
+ `https://foo.example.com:${h2Port}/doh?responseIP=3.3.3.3&push=true`
+ );
+
+ // AsyncResoleWithTrrServer rejects server pushes and the entry for push.example.org
+ // shouldn't be neither in the default cache not in AsyncResoleWithTrrServer cache.
+ setModeAndURI(2, "404");
+
+ await new TRRDNSListener(
+ "push.example.org",
+ "3.3.3.3",
+ true,
+ undefined,
+ `https://foo.example.com:${h2Port}/doh?responseIP=3.3.3.3&push=true`
+ );
+
+ await new TRRDNSListener("push.example.org", "127.0.0.1");
+
+ // Check confirmation is ignored
+ Services.dns.clearCache(true);
+ setModeAndURI(2, "doh?responseIP=1::ffff");
+ Services.prefs.clearUserPref("network.trr.useGET");
+ Services.prefs.clearUserPref("network.trr.disable-ECS");
+ Services.prefs.setCharPref(
+ "network.trr.confirmationNS",
+ "confirm.example.com"
+ );
+
+ // AsyncResoleWithTrrServer will succeed
+ await new TRRDNSListener(
+ "bar_with_trr8.example.com",
+ "3.3.3.3",
+ true,
+ undefined,
+ `https://foo.example.com:${h2Port}/doh?responseIP=3.3.3.3`
+ );
+
+ Services.prefs.setCharPref("network.trr.confirmationNS", "skip");
+
+ // Bad port
+ Services.dns.clearCache(true);
+ setModeAndURI(2, "doh?responseIP=2.2.2.2");
+
+ ({ inStatus } = await new TRRDNSListener(
+ "only_once.example.com",
+ undefined,
+ false,
+ undefined,
+ `https://target.example.com:666/404`
+ ));
+ Assert.ok(
+ !Components.isSuccessCode(inStatus),
+ `${inStatus} should be an error code`
+ );
+
+ // // MOZ_LOG=sync,timestamp,nsHostResolver:5 We should not keep resolving only_once.example.com
+ // // TODO: find a way of automating this
+ // await new Promise(resolve => {});
+});
+
+add_task(test_fetch_time);
+
+add_task(async function test_content_encoding_gzip() {
+ info("Checking gzip content encoding");
+ Services.dns.clearCache(true);
+ Services.prefs.setBoolPref(
+ "network.trr.send_empty_accept-encoding_headers",
+ false
+ );
+ setModeAndURI(3, "doh?responseIP=2.2.2.2");
+
+ await new TRRDNSListener("bar.example.com", "2.2.2.2");
+ Services.prefs.clearUserPref(
+ "network.trr.send_empty_accept-encoding_headers"
+ );
+});
+
+add_task(async function test_redirect() {
+ info("Check handling of redirect");
+
+ // GET
+ Services.dns.clearCache(true);
+ setModeAndURI(3, "doh?redirect=4.4.4.4{&dns}");
+ Services.prefs.setBoolPref("network.trr.useGET", true);
+ Services.prefs.setBoolPref("network.trr.disable-ECS", true);
+
+ await new TRRDNSListener("ecs.example.com", "4.4.4.4");
+
+ // POST
+ Services.dns.clearCache(true);
+ Services.prefs.setBoolPref("network.trr.useGET", false);
+ setModeAndURI(3, "doh?redirect=4.4.4.4");
+
+ await new TRRDNSListener("bar.example.com", "4.4.4.4");
+
+ Services.prefs.clearUserPref("network.trr.useGET");
+ Services.prefs.clearUserPref("network.trr.disable-ECS");
+});
+
+// confirmationNS set without confirmed NS yet
+// checks that we properly fall back to DNS is confirmation is not ready yet,
+// and wait-for-confirmation pref is true
+add_task(async function test_confirmation() {
+ info("Checking that we fall back correctly when confirmation is pending");
+ Services.dns.clearCache(true);
+ Services.prefs.setBoolPref("network.trr.wait-for-confirmation", true);
+ setModeAndURI(2, "doh?responseIP=7.7.7.7&slowConfirm=true");
+ Services.prefs.setCharPref(
+ "network.trr.confirmationNS",
+ "confirm.example.com"
+ );
+
+ await new TRRDNSListener("example.org", "127.0.0.1");
+ await new Promise(resolve => do_timeout(1000, resolve));
+ await waitForConfirmation("7.7.7.7");
+
+ // Reset between each test to force re-confirm
+ Services.prefs.setCharPref("network.trr.confirmationNS", "skip");
+
+ info("Check that confirmation is skipped in mode 3");
+ // This is just a smoke test to make sure lookups succeed immediately
+ // in mode 3 without waiting for confirmation.
+ Services.dns.clearCache(true);
+ setModeAndURI(3, "doh?responseIP=1::ffff&slowConfirm=true");
+ Services.prefs.setCharPref(
+ "network.trr.confirmationNS",
+ "confirm.example.com"
+ );
+
+ await new TRRDNSListener("skipConfirmationForMode3.example.com", "1::ffff");
+
+ // Reset between each test to force re-confirm
+ Services.prefs.setCharPref("network.trr.confirmationNS", "skip");
+
+ Services.dns.clearCache(true);
+ Services.prefs.setBoolPref("network.trr.wait-for-confirmation", false);
+ setModeAndURI(2, "doh?responseIP=7.7.7.7&slowConfirm=true");
+ Services.prefs.setCharPref(
+ "network.trr.confirmationNS",
+ "confirm.example.com"
+ );
+
+ // DoH available immediately
+ await new TRRDNSListener("example.org", "7.7.7.7");
+
+ // Reset between each test to force re-confirm
+ Services.prefs.setCharPref("network.trr.confirmationNS", "skip");
+
+ // Fallback when confirmation fails
+ Services.dns.clearCache(true);
+ Services.prefs.setBoolPref("network.trr.wait-for-confirmation", true);
+ setModeAndURI(2, "404");
+ Services.prefs.setCharPref(
+ "network.trr.confirmationNS",
+ "confirm.example.com"
+ );
+
+ await waitForConfirmation("7.7.7.7", true);
+
+ await new TRRDNSListener("example.org", "127.0.0.1");
+
+ // Reset
+ Services.prefs.setCharPref("network.trr.confirmationNS", "skip");
+ Services.prefs.clearUserPref("network.trr.wait-for-confirmation");
+});
+
+add_task(test_fqdn);
+
+add_task(async function test_detected_uri() {
+ info("Test setDetectedTrrURI");
+ Services.dns.clearCache(true);
+ Services.prefs.setIntPref("network.trr.mode", 3);
+ Services.prefs.clearUserPref("network.trr.uri");
+ let defaultURI = gDefaultPref.getCharPref("network.trr.default_provider_uri");
+ gDefaultPref.setCharPref(
+ "network.trr.default_provider_uri",
+ `https://foo.example.com:${h2Port}/doh?responseIP=3.4.5.6`
+ );
+ await new TRRDNSListener("domainA.example.org.", "3.4.5.6");
+ Services.dns.setDetectedTrrURI(
+ `https://foo.example.com:${h2Port}/doh?responseIP=1.2.3.4`
+ );
+ await new TRRDNSListener("domainB.example.org.", "1.2.3.4");
+ gDefaultPref.setCharPref("network.trr.default_provider_uri", defaultURI);
+
+ // With a user-set doh uri this time.
+ Services.dns.clearCache(true);
+ setModeAndURI(2, "doh?responseIP=4.5.6.7");
+ await new TRRDNSListener("domainA.example.org.", "4.5.6.7");
+
+ // This should be a no-op, since we have a user-set URI
+ Services.dns.setDetectedTrrURI(
+ `https://foo.example.com:${h2Port}/doh?responseIP=1.2.3.4`
+ );
+ await new TRRDNSListener("domainB.example.org.", "4.5.6.7");
+
+ // Test network link status change
+ Services.dns.clearCache(true);
+ Services.prefs.setIntPref("network.trr.mode", 3);
+ Services.prefs.clearUserPref("network.trr.uri");
+ gDefaultPref.setCharPref(
+ "network.trr.default_provider_uri",
+ `https://foo.example.com:${h2Port}/doh?responseIP=3.4.5.6`
+ );
+ await new TRRDNSListener("domainA.example.org.", "3.4.5.6");
+ Services.dns.setDetectedTrrURI(
+ `https://foo.example.com:${h2Port}/doh?responseIP=1.2.3.4`
+ );
+ await new TRRDNSListener("domainB.example.org.", "1.2.3.4");
+
+ let networkLinkService = {
+ platformDNSIndications: 0,
+ QueryInterface: ChromeUtils.generateQI(["nsINetworkLinkService"]),
+ };
+
+ Services.obs.notifyObservers(
+ networkLinkService,
+ "network:link-status-changed",
+ "changed"
+ );
+
+ await new TRRDNSListener("domainC.example.org.", "3.4.5.6");
+
+ gDefaultPref.setCharPref("network.trr.default_provider_uri", defaultURI);
+});
+
+add_task(async function test_pref_changes() {
+ info("Testing pref change handling");
+ Services.prefs.clearUserPref("network.trr.uri");
+ let defaultURI = gDefaultPref.getCharPref("network.trr.default_provider_uri");
+
+ async function doThenCheckURI(closure, expectedURI, expectChange = true) {
+ let uriChanged;
+ if (expectChange) {
+ uriChanged = topicObserved("network:trr-uri-changed");
+ }
+ closure();
+ if (expectChange) {
+ await uriChanged;
+ }
+ equal(Services.dns.currentTrrURI, expectedURI);
+ }
+
+ // setting the default value of the pref should be reflected in the URI
+ await doThenCheckURI(() => {
+ gDefaultPref.setCharPref(
+ "network.trr.default_provider_uri",
+ `https://foo.example.com:${h2Port}/doh?default`
+ );
+ }, `https://foo.example.com:${h2Port}/doh?default`);
+
+ // the user set value should be reflected in the URI
+ await doThenCheckURI(() => {
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${h2Port}/doh?user`
+ );
+ }, `https://foo.example.com:${h2Port}/doh?user`);
+
+ // A user set pref is selected, so it should be chosen instead of the rollout
+ await doThenCheckURI(
+ () => {
+ Services.prefs.setCharPref(
+ "doh-rollout.uri",
+ `https://foo.example.com:${h2Port}/doh?rollout`
+ );
+ },
+ `https://foo.example.com:${h2Port}/doh?user`,
+ false
+ );
+
+ // There is no user set pref, so we go to the rollout pref
+ await doThenCheckURI(() => {
+ Services.prefs.clearUserPref("network.trr.uri");
+ }, `https://foo.example.com:${h2Port}/doh?rollout`);
+
+ // When the URI is set by the rollout addon, detection is allowed
+ await doThenCheckURI(() => {
+ Services.dns.setDetectedTrrURI(
+ `https://foo.example.com:${h2Port}/doh?detected`
+ );
+ }, `https://foo.example.com:${h2Port}/doh?detected`);
+
+ // Should switch back to the default provided by the rollout addon
+ await doThenCheckURI(() => {
+ let networkLinkService = {
+ platformDNSIndications: 0,
+ QueryInterface: ChromeUtils.generateQI(["nsINetworkLinkService"]),
+ };
+ Services.obs.notifyObservers(
+ networkLinkService,
+ "network:link-status-changed",
+ "changed"
+ );
+ }, `https://foo.example.com:${h2Port}/doh?rollout`);
+
+ // Again the user set pref should be chosen
+ await doThenCheckURI(() => {
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${h2Port}/doh?user`
+ );
+ }, `https://foo.example.com:${h2Port}/doh?user`);
+
+ // Detection should not work with a user set pref
+ await doThenCheckURI(
+ () => {
+ Services.dns.setDetectedTrrURI(
+ `https://foo.example.com:${h2Port}/doh?detected`
+ );
+ },
+ `https://foo.example.com:${h2Port}/doh?user`,
+ false
+ );
+
+ // Should stay the same on network changes
+ await doThenCheckURI(
+ () => {
+ let networkLinkService = {
+ platformDNSIndications: 0,
+ QueryInterface: ChromeUtils.generateQI(["nsINetworkLinkService"]),
+ };
+ Services.obs.notifyObservers(
+ networkLinkService,
+ "network:link-status-changed",
+ "changed"
+ );
+ },
+ `https://foo.example.com:${h2Port}/doh?user`,
+ false
+ );
+
+ // Restore the pref
+ gDefaultPref.setCharPref("network.trr.default_provider_uri", defaultURI);
+});
+
+add_task(async function test_dohrollout_mode() {
+ info("Testing doh-rollout.mode");
+ Services.prefs.clearUserPref("network.trr.mode");
+ Services.prefs.clearUserPref("doh-rollout.mode");
+
+ equal(Services.dns.currentTrrMode, 0);
+
+ async function doThenCheckMode(trrMode, rolloutMode, expectedMode, message) {
+ let modeChanged;
+ if (Services.dns.currentTrrMode != expectedMode) {
+ modeChanged = topicObserved("network:trr-mode-changed");
+ }
+
+ if (trrMode != undefined) {
+ Services.prefs.setIntPref("network.trr.mode", trrMode);
+ }
+
+ if (rolloutMode != undefined) {
+ Services.prefs.setIntPref("doh-rollout.mode", rolloutMode);
+ }
+
+ if (modeChanged) {
+ await modeChanged;
+ }
+ equal(Services.dns.currentTrrMode, expectedMode, message);
+ }
+
+ await doThenCheckMode(2, undefined, 2);
+ await doThenCheckMode(3, undefined, 3);
+ await doThenCheckMode(5, undefined, 5);
+ await doThenCheckMode(2, undefined, 2);
+ await doThenCheckMode(0, undefined, 0);
+ await doThenCheckMode(1, undefined, 5);
+ await doThenCheckMode(6, undefined, 5);
+
+ await doThenCheckMode(2, 0, 2);
+ await doThenCheckMode(2, 1, 2);
+ await doThenCheckMode(2, 2, 2);
+ await doThenCheckMode(2, 3, 2);
+ await doThenCheckMode(2, 5, 2);
+ await doThenCheckMode(3, 2, 3);
+ await doThenCheckMode(5, 2, 5);
+
+ Services.prefs.clearUserPref("network.trr.mode");
+ Services.prefs.clearUserPref("doh-rollout.mode");
+
+ await doThenCheckMode(undefined, 2, 2);
+ await doThenCheckMode(undefined, 3, 3);
+
+ // All modes that are not 0,2,3 are treated as 5
+ await doThenCheckMode(undefined, 5, 5);
+ await doThenCheckMode(undefined, 4, 5);
+ await doThenCheckMode(undefined, 6, 5);
+
+ await doThenCheckMode(undefined, 2, 2);
+ await doThenCheckMode(3, undefined, 3);
+
+ Services.prefs.clearUserPref("network.trr.mode");
+ equal(Services.dns.currentTrrMode, 2);
+ Services.prefs.clearUserPref("doh-rollout.mode");
+ equal(Services.dns.currentTrrMode, 0);
+});
+
+add_task(test_ipv6_trr_fallback);
+
+add_task(test_ipv4_trr_fallback);
+
+add_task(test_no_retry_without_doh);
+
+// This test checks that normally when the TRR mode goes from ON -> OFF
+// we purge the DNS cache (including TRR), so the entries aren't used on
+// networks where they shouldn't. For example - turning on a VPN.
+add_task(async function test_purge_trr_cache_on_mode_change() {
+ info("Checking that we purge cache when TRR is turned off");
+ Services.prefs.setBoolPref("network.trr.clear-cache-on-pref-change", true);
+
+ Services.prefs.setIntPref("network.trr.mode", 0);
+ Services.prefs.setIntPref("doh-rollout.mode", 2);
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${h2Port}/doh?responseIP=3.3.3.3`
+ );
+
+ await new TRRDNSListener("cached.example.com", "3.3.3.3");
+ Services.prefs.clearUserPref("doh-rollout.mode");
+
+ await new TRRDNSListener("cached.example.com", "127.0.0.1");
+
+ Services.prefs.setBoolPref("network.trr.clear-cache-on-pref-change", false);
+ Services.prefs.clearUserPref("doh-rollout.mode");
+});
+
+add_task(async function test_old_bootstrap_pref() {
+ Services.dns.clearCache(true);
+ // Note this is a remote address. Setting this pref should have no effect,
+ // as this is the old name for the bootstrap pref.
+ // If this were to be used, the test would crash when accessing a non-local
+ // IP address.
+ Services.prefs.setCharPref("network.trr.bootstrapAddress", "1.1.1.1");
+ setModeAndURI(Ci.nsIDNSService.MODE_TRRONLY, `doh?responseIP=4.4.4.4`);
+ await new TRRDNSListener("testytest.com", "4.4.4.4");
+});
+
+add_task(async function test_padding() {
+ setModeAndURI(Ci.nsIDNSService.MODE_TRRONLY, `doh`);
+ async function CheckPadding(
+ pad_length,
+ request,
+ none,
+ ecs,
+ padding,
+ ecsPadding
+ ) {
+ Services.prefs.setIntPref("network.trr.padding.length", pad_length);
+ Services.dns.clearCache(true);
+ Services.prefs.setBoolPref("network.trr.padding", false);
+ Services.prefs.setBoolPref("network.trr.disable-ECS", false);
+ await new TRRDNSListener(request, none);
+
+ Services.dns.clearCache(true);
+ Services.prefs.setBoolPref("network.trr.padding", false);
+ Services.prefs.setBoolPref("network.trr.disable-ECS", true);
+ await new TRRDNSListener(request, ecs);
+
+ Services.dns.clearCache(true);
+ Services.prefs.setBoolPref("network.trr.padding", true);
+ Services.prefs.setBoolPref("network.trr.disable-ECS", false);
+ await new TRRDNSListener(request, padding);
+
+ Services.dns.clearCache(true);
+ Services.prefs.setBoolPref("network.trr.padding", true);
+ Services.prefs.setBoolPref("network.trr.disable-ECS", true);
+ await new TRRDNSListener(request, ecsPadding);
+ }
+
+ // short domain name
+ await CheckPadding(
+ 16,
+ "a.pd",
+ "2.2.0.22",
+ "2.2.0.41",
+ "1.1.0.48",
+ "1.1.0.48"
+ );
+ await CheckPadding(256, "a.pd", "2.2.0.22", "2.2.0.41", "1.1.1.0", "1.1.1.0");
+
+ // medium domain name
+ await CheckPadding(
+ 16,
+ "has-padding.pd",
+ "2.2.0.32",
+ "2.2.0.51",
+ "1.1.0.48",
+ "1.1.0.64"
+ );
+ await CheckPadding(
+ 128,
+ "has-padding.pd",
+ "2.2.0.32",
+ "2.2.0.51",
+ "1.1.0.128",
+ "1.1.0.128"
+ );
+ await CheckPadding(
+ 80,
+ "has-padding.pd",
+ "2.2.0.32",
+ "2.2.0.51",
+ "1.1.0.80",
+ "1.1.0.80"
+ );
+
+ // long domain name
+ await CheckPadding(
+ 16,
+ "abcdefghijklmnopqrstuvwxyz0123456789.abcdefghijklmnopqrstuvwxyz0123456789.abcdefghijklmnopqrstuvwxyz0123456789.pd",
+ "2.2.0.131",
+ "2.2.0.150",
+ "1.1.0.160",
+ "1.1.0.160"
+ );
+ await CheckPadding(
+ 128,
+ "abcdefghijklmnopqrstuvwxyz0123456789.abcdefghijklmnopqrstuvwxyz0123456789.abcdefghijklmnopqrstuvwxyz0123456789.pd",
+ "2.2.0.131",
+ "2.2.0.150",
+ "1.1.1.0",
+ "1.1.1.0"
+ );
+ await CheckPadding(
+ 80,
+ "abcdefghijklmnopqrstuvwxyz0123456789.abcdefghijklmnopqrstuvwxyz0123456789.abcdefghijklmnopqrstuvwxyz0123456789.pd",
+ "2.2.0.131",
+ "2.2.0.150",
+ "1.1.0.160",
+ "1.1.0.160"
+ );
+});
+
+add_task(test_connection_reuse_and_cycling);
+
+// Can't test for socket process since telemetry is captured in different process.
+add_task(
+ { skip_if: () => mozinfo.socketprocess_networking },
+ async function test_trr_pb_telemetry() {
+ setModeAndURI(Ci.nsIDNSService.MODE_TRRONLY, `doh`);
+ Services.dns.clearCache(true);
+ Services.fog.initializeFOG();
+ Services.fog.testResetFOG();
+ await new TRRDNSListener("testytest.com", { expectedAnswer: "5.5.5.5" });
+
+ Assert.equal(
+ await Glean.networking.trrRequestCount.regular.testGetValue(),
+ 2
+ ); // One for IPv4 and one for IPv6.
+ Assert.equal(
+ await Glean.networking.trrRequestCount.private.testGetValue(),
+ null
+ );
+
+ await new TRRDNSListener("testytest.com", {
+ expectedAnswer: "5.5.5.5",
+ originAttributes: { privateBrowsingId: 1 },
+ });
+
+ Assert.equal(
+ await Glean.networking.trrRequestCount.regular.testGetValue(),
+ 2
+ );
+ Assert.equal(
+ await Glean.networking.trrRequestCount.private.testGetValue(),
+ 2
+ );
+ }
+);
diff --git a/netwerk/test/unit/test_trr_additional_section.js b/netwerk/test/unit/test_trr_additional_section.js
new file mode 100644
index 0000000000..bd8b300b96
--- /dev/null
+++ b/netwerk/test/unit/test_trr_additional_section.js
@@ -0,0 +1,380 @@
+/* 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";
+
+trr_test_setup();
+registerCleanupFunction(async () => {
+ trr_clear_prefs();
+});
+
+function makeChan(url) {
+ let chan = NetUtil.newChannel({
+ uri: url,
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+ return chan;
+}
+
+function channelOpenPromise(chan) {
+ return new Promise(resolve => {
+ function finish(req, buffer) {
+ resolve([req, buffer]);
+ }
+ chan.asyncOpen(new ChannelListener(finish));
+ });
+}
+
+let trrServer = new TRRServer();
+registerCleanupFunction(async () => {
+ await trrServer.stop();
+});
+add_task(async function setup_server() {
+ await trrServer.start();
+ dump(`port = ${trrServer.port()}\n`);
+ let chan = makeChan(`https://localhost:${trrServer.port()}/test?bla=some`);
+ let [, resp] = await channelOpenPromise(chan);
+ equal(resp, "<h1> 404 Path not found: /test</h1>");
+});
+
+add_task(async function test_parse_additional_section() {
+ Services.dns.clearCache(true);
+ Services.prefs.setIntPref("network.trr.mode", 3);
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${trrServer.port()}/dns-query`
+ );
+
+ await trrServer.registerDoHAnswers("something.foo", "A", {
+ answers: [
+ {
+ name: "something.foo",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "1.2.3.4",
+ },
+ ],
+ additionals: [
+ {
+ name: "else.foo",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "2.3.4.5",
+ },
+ ],
+ });
+
+ await new TRRDNSListener("something.foo", { expectedAnswer: "1.2.3.4" });
+ await new TRRDNSListener("else.foo", { expectedAnswer: "2.3.4.5" });
+
+ await trrServer.registerDoHAnswers("a.foo", "A", {
+ answers: [
+ {
+ name: "a.foo",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "1.2.3.4",
+ },
+ ],
+ additionals: [
+ {
+ name: "b.foo",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "2.3.4.5",
+ },
+ ],
+ });
+ await trrServer.registerDoHAnswers("b.foo", "A", {
+ answers: [
+ {
+ name: "b.foo",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "3.4.5.6",
+ },
+ ],
+ });
+
+ let req1 = new TRRDNSListener("a.foo", { expectedAnswer: "1.2.3.4" });
+
+ // A request for b.foo will be in progress by the time we parse the additional
+ // record. To keep things simple we don't end up saving the record, instead
+ // we wait for the in-progress request to complete.
+ // This check is also racy - if the response for a.foo completes before we make
+ // this request, we'll put the other IP in the cache. But that is very unlikely.
+ let req2 = new TRRDNSListener("b.foo", { expectedAnswer: "3.4.5.6" });
+
+ await Promise.all([req1, req2]);
+
+ // IPv6 additional
+ await trrServer.registerDoHAnswers("xyz.foo", "A", {
+ answers: [
+ {
+ name: "xyz.foo",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "1.2.3.4",
+ },
+ ],
+ additionals: [
+ {
+ name: "abc.foo",
+ ttl: 55,
+ type: "AAAA",
+ flush: false,
+ data: "::1:2:3:4",
+ },
+ ],
+ });
+
+ await new TRRDNSListener("xyz.foo", { expectedAnswer: "1.2.3.4" });
+ await new TRRDNSListener("abc.foo", { expectedAnswer: "::1:2:3:4" });
+
+ // IPv6 additional
+ await trrServer.registerDoHAnswers("ipv6.foo", "AAAA", {
+ answers: [
+ {
+ name: "ipv6.foo",
+ ttl: 55,
+ type: "AAAA",
+ flush: false,
+ data: "2001::a:b:c:d",
+ },
+ ],
+ additionals: [
+ {
+ name: "def.foo",
+ ttl: 55,
+ type: "AAAA",
+ flush: false,
+ data: "::a:b:c:d",
+ },
+ ],
+ });
+
+ await new TRRDNSListener("ipv6.foo", { expectedAnswer: "2001::a:b:c:d" });
+ await new TRRDNSListener("def.foo", { expectedAnswer: "::a:b:c:d" });
+
+ // IPv6 additional
+ await trrServer.registerDoHAnswers("ipv6b.foo", "AAAA", {
+ answers: [
+ {
+ name: "ipv6b.foo",
+ ttl: 55,
+ type: "AAAA",
+ flush: false,
+ data: "2001::a:b:c:d",
+ },
+ ],
+ additionals: [
+ {
+ name: "qqqq.foo",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "9.8.7.6",
+ },
+ ],
+ });
+
+ await new TRRDNSListener("ipv6b.foo", { expectedAnswer: "2001::a:b:c:d" });
+ await new TRRDNSListener("qqqq.foo", { expectedAnswer: "9.8.7.6" });
+
+ // Multiple IPs and multiple additional records
+ await trrServer.registerDoHAnswers("multiple.foo", "A", {
+ answers: [
+ {
+ name: "multiple.foo",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "9.9.9.9",
+ },
+ ],
+ additionals: [
+ {
+ // Should be ignored, because it should be in the answer section
+ name: "multiple.foo",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "1.1.1.1",
+ },
+ {
+ // Is ignored, because it should be in the answer section
+ name: "multiple.foo",
+ ttl: 55,
+ type: "AAAA",
+ flush: false,
+ data: "2001::a:b:c:d",
+ },
+ {
+ name: "yuiop.foo",
+ ttl: 55,
+ type: "AAAA",
+ flush: false,
+ data: "2001::a:b:c:d",
+ },
+ {
+ name: "yuiop.foo",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "1.2.3.4",
+ },
+ ],
+ });
+
+ let { inRecord } = await new TRRDNSListener("multiple.foo", {
+ expectedAnswer: "9.9.9.9",
+ });
+ let IPs = [];
+ inRecord.QueryInterface(Ci.nsIDNSAddrRecord);
+ inRecord.rewind();
+ while (inRecord.hasMore()) {
+ IPs.push(inRecord.getNextAddrAsString());
+ }
+ equal(IPs.length, 1);
+ equal(IPs[0], "9.9.9.9");
+ IPs = [];
+ ({ inRecord } = await new TRRDNSListener("yuiop.foo", {
+ expectedSuccess: false,
+ }));
+ inRecord.QueryInterface(Ci.nsIDNSAddrRecord);
+ inRecord.rewind();
+ while (inRecord.hasMore()) {
+ IPs.push(inRecord.getNextAddrAsString());
+ }
+ equal(IPs.length, 2);
+ equal(IPs[0], "2001::a:b:c:d");
+ equal(IPs[1], "1.2.3.4");
+});
+
+add_task(async function test_additional_after_resolve() {
+ await trrServer.registerDoHAnswers("first.foo", "A", {
+ answers: [
+ {
+ name: "first.foo",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "3.4.5.6",
+ },
+ ],
+ });
+ await new TRRDNSListener("first.foo", { expectedAnswer: "3.4.5.6" });
+
+ await trrServer.registerDoHAnswers("second.foo", "A", {
+ answers: [
+ {
+ name: "second.foo",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "1.2.3.4",
+ },
+ ],
+ additionals: [
+ {
+ name: "first.foo",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "2.3.4.5",
+ },
+ ],
+ });
+
+ await new TRRDNSListener("second.foo", { expectedAnswer: "1.2.3.4" });
+ await new TRRDNSListener("first.foo", { expectedAnswer: "2.3.4.5" });
+});
+
+// test for Bug - 1790075
+// Crash was observed when a DNS (using TRR) reply contains an additional
+// record field and this addditional record was previously unsuccessfully
+// resolved
+add_task(async function test_additional_cached_record_override() {
+ Services.dns.clearCache(true);
+ Services.prefs.setIntPref("network.trr.mode", 2);
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${trrServer.port()}/dns-query`
+ );
+
+ await new TRRDNSListener("else.foo", { expectedAnswer: "127.0.0.1" });
+
+ await trrServer.registerDoHAnswers("something.foo", "A", {
+ answers: [
+ {
+ name: "something.foo",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "1.2.3.4",
+ },
+ ],
+ additionals: [
+ {
+ name: "else.foo",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "2.3.4.5",
+ },
+ ],
+ });
+
+ await new TRRDNSListener("something.foo", { expectedAnswer: "1.2.3.4" });
+ await new TRRDNSListener("else.foo", { expectedAnswer: "2.3.4.5" });
+});
+
+add_task(async function test_ipv6_disabled() {
+ Services.prefs.setBoolPref("network.dns.disableIPv6", true);
+ await trrServer.registerDoHAnswers("ipv6.foo", "A", {
+ answers: [
+ {
+ name: "ipv6.foo",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "1.2.3.4",
+ },
+ ],
+ additionals: [
+ {
+ name: "sub.ipv6.foo",
+ ttl: 55,
+ type: "AAAA",
+ flush: false,
+ data: "::1:2:3:4",
+ },
+ ],
+ });
+
+ await new TRRDNSListener("ipv6.foo", { expectedAnswer: "1.2.3.4" });
+ await new TRRDNSListener("sub.ipv6.foo", { expectedSuccess: false });
+
+ await trrServer.registerDoHAnswers("direct.ipv6.foo", "AAAA", {
+ answers: [
+ {
+ name: "direct.ipv6.foo",
+ ttl: 55,
+ type: "AAAA",
+ flush: false,
+ data: "2001::a:b:c:d",
+ },
+ ],
+ });
+
+ await new TRRDNSListener("direct.ipv6.foo", { expectedSuccess: false });
+
+ Services.prefs.setBoolPref("network.dns.disableIPv6", false);
+});
diff --git a/netwerk/test/unit/test_trr_af_fallback.js b/netwerk/test/unit/test_trr_af_fallback.js
new file mode 100644
index 0000000000..c202e36702
--- /dev/null
+++ b/netwerk/test/unit/test_trr_af_fallback.js
@@ -0,0 +1,117 @@
+/* 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 gOverride = Cc["@mozilla.org/network/native-dns-override;1"].getService(
+ Ci.nsINativeDNSResolverOverride
+);
+
+trr_test_setup();
+registerCleanupFunction(async () => {
+ trr_clear_prefs();
+});
+
+let trrServer = null;
+add_task(async function start_trr_server() {
+ trrServer = new TRRServer();
+ registerCleanupFunction(async () => {
+ await trrServer.stop();
+ });
+ await trrServer.start();
+ dump(`port = ${trrServer.port()}\n`);
+ Services.prefs.setBoolPref("network.trr.skip-AAAA-when-not-supported", false);
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${trrServer.port()}/dns-query`
+ );
+ Services.prefs.setIntPref("network.trr.mode", Ci.nsIDNSService.MODE_TRRFIRST);
+});
+
+add_task(async function unspec_first() {
+ gOverride.clearOverrides();
+ Services.dns.clearCache(true);
+
+ gOverride.addIPOverride("example.org", "1.1.1.1");
+ gOverride.addIPOverride("example.org", "::1");
+
+ await trrServer.registerDoHAnswers("example.org", "A", {
+ answers: [
+ {
+ name: "example.org",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "1.2.3.4",
+ },
+ ],
+ });
+ // This first request gets cached. IPv6 response gets served from the cache
+ await new TRRDNSListener("example.org", { expectedAnswer: "1.2.3.4" });
+ await new TRRDNSListener("example.org", {
+ flags: Ci.nsIDNSService.RESOLVE_DISABLE_IPV6,
+ expectedAnswer: "1.2.3.4",
+ });
+ let { inStatus } = await new TRRDNSListener("example.org", {
+ flags: Ci.nsIDNSService.RESOLVE_DISABLE_IPV4,
+ expectedSuccess: false,
+ });
+ equal(inStatus, Cr.NS_ERROR_UNKNOWN_HOST);
+});
+
+add_task(async function A_then_AAAA_fails() {
+ gOverride.clearOverrides();
+ Services.dns.clearCache(true);
+
+ gOverride.addIPOverride("example.org", "1.1.1.1");
+ gOverride.addIPOverride("example.org", "::1");
+
+ await trrServer.registerDoHAnswers("example.org", "A", {
+ answers: [
+ {
+ name: "example.org",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "1.2.3.4",
+ },
+ ],
+ });
+ // We do individual IPv4/IPv6 requests - we expect IPv6 not to fallback to Do53 because we have an IPv4 record
+ await new TRRDNSListener("example.org", {
+ flags: Ci.nsIDNSService.RESOLVE_DISABLE_IPV6,
+ expectedAnswer: "1.2.3.4",
+ });
+ let { inStatus } = await new TRRDNSListener("example.org", {
+ flags: Ci.nsIDNSService.RESOLVE_DISABLE_IPV4,
+ expectedSuccess: false,
+ });
+ equal(inStatus, Cr.NS_ERROR_UNKNOWN_HOST);
+});
+
+add_task(async function just_AAAA_fails() {
+ gOverride.clearOverrides();
+ Services.dns.clearCache(true);
+
+ gOverride.addIPOverride("example.org", "1.1.1.1");
+ gOverride.addIPOverride("example.org", "::1");
+
+ await trrServer.registerDoHAnswers("example.org", "A", {
+ answers: [
+ {
+ name: "example.org",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "1.2.3.4",
+ },
+ ],
+ });
+ // We only do an IPv6 req - we expect IPv6 not to fallback to Do53 because we have an IPv4 record
+ let { inStatus } = await new TRRDNSListener("example.org", {
+ flags: Ci.nsIDNSService.RESOLVE_DISABLE_IPV4,
+ expectedSuccess: false,
+ });
+ equal(inStatus, Cr.NS_ERROR_UNKNOWN_HOST);
+});
diff --git a/netwerk/test/unit/test_trr_blocklist.js b/netwerk/test/unit/test_trr_blocklist.js
new file mode 100644
index 0000000000..8f9d3ffbc5
--- /dev/null
+++ b/netwerk/test/unit/test_trr_blocklist.js
@@ -0,0 +1,78 @@
+/* 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 override = Cc["@mozilla.org/network/native-dns-override;1"].getService(
+ Ci.nsINativeDNSResolverOverride
+);
+
+function setup() {
+ trr_test_setup();
+ Services.prefs.setBoolPref("network.trr.temp_blocklist", true);
+}
+setup();
+
+add_task(async function checkBlocklisting() {
+ let trrServer = new TRRServer();
+ registerCleanupFunction(async () => {
+ await trrServer.stop();
+ });
+ await trrServer.start();
+ info(`port = ${trrServer.port()}\n`);
+
+ Services.dns.clearCache(true);
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${trrServer.port()}/dns-query`
+ );
+ Services.prefs.setIntPref("network.trr.mode", Ci.nsIDNSService.MODE_TRRFIRST);
+
+ await trrServer.registerDoHAnswers("top.test.com", "NS", {});
+
+ override.addIPOverride("sub.top.test.com", "2.2.2.2");
+ override.addIPOverride("sub2.top.test.com", "2.2.2.2");
+ await new TRRDNSListener("sub.top.test.com", {
+ expectedAnswer: "2.2.2.2",
+ });
+ equal(await trrServer.requestCount("sub.top.test.com", "A"), 1);
+
+ // Clear the cache so that we need to consult the blocklist and not simply
+ // return the cached DNS record.
+ Services.dns.clearCache(true);
+ await new TRRDNSListener("sub.top.test.com", {
+ expectedAnswer: "2.2.2.2",
+ });
+ equal(
+ await trrServer.requestCount("sub.top.test.com", "A"),
+ 1,
+ "Request should go directly to native because result is still in blocklist"
+ );
+
+ // XXX(valentin): if this ever starts intermittently failing we need to add
+ // a sleep here. But the check for the parent NS should normally complete
+ // before the second subdomain request.
+ equal(
+ await trrServer.requestCount("top.test.com", "NS"),
+ 1,
+ "Should have checked parent domain"
+ );
+ await new TRRDNSListener("sub2.top.test.com", {
+ expectedAnswer: "2.2.2.2",
+ });
+ equal(await trrServer.requestCount("sub2.top.test.com", "A"), 0);
+
+ // The blocklist should instantly expire.
+ Services.prefs.setIntPref("network.trr.temp_blocklist_duration_sec", 0);
+ Services.dns.clearCache(true);
+ await new TRRDNSListener("sub.top.test.com", {
+ expectedAnswer: "2.2.2.2",
+ });
+ // blocklist expired. Do another check.
+ equal(
+ await trrServer.requestCount("sub.top.test.com", "A"),
+ 2,
+ "We should do another TRR request because the bloclist expired"
+ );
+});
diff --git a/netwerk/test/unit/test_trr_cancel.js b/netwerk/test/unit/test_trr_cancel.js
new file mode 100644
index 0000000000..60b65d7eaf
--- /dev/null
+++ b/netwerk/test/unit/test_trr_cancel.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";
+
+trr_test_setup();
+registerCleanupFunction(async () => {
+ trr_clear_prefs();
+});
+
+let trrServer = null;
+add_task(async function start_trr_server() {
+ trrServer = new TRRServer();
+ registerCleanupFunction(async () => {
+ await trrServer.stop();
+ });
+ await trrServer.start();
+ dump(`port = ${trrServer.port()}\n`);
+
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${trrServer.port()}/dns-query`
+ );
+ Services.prefs.setIntPref("network.trr.mode", Ci.nsIDNSService.MODE_TRRFIRST);
+});
+
+add_task(async function sanity_check() {
+ await trrServer.registerDoHAnswers("example.com", "A", {
+ answers: [
+ {
+ name: "example.com",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "1.2.3.4",
+ },
+ ],
+ });
+ // Simple check to see that TRR works.
+ await new TRRDNSListener("example.com", { expectedAnswer: "1.2.3.4" });
+});
+
+// Cancelling the request is not sync when using the socket process, so
+// we skip this test when it's enabled.
+add_task(
+ { skip_if: () => mozinfo.socketprocess_networking },
+ async function cancel_immediately() {
+ await trrServer.registerDoHAnswers("example.org", "A", {
+ answers: [
+ {
+ name: "example.org",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "2.3.4.5",
+ },
+ ],
+ });
+ let r1 = new TRRDNSListener("example.org", { expectedSuccess: false });
+ let r2 = new TRRDNSListener("example.org", { expectedAnswer: "2.3.4.5" });
+ r1.cancel();
+ let { inStatus } = await r1;
+ equal(inStatus, Cr.NS_ERROR_ABORT);
+ await r2;
+ equal(await trrServer.requestCount("example.org", "A"), 1);
+
+ // Now we cancel both of them
+ Services.dns.clearCache(true);
+ r1 = new TRRDNSListener("example.org", { expectedSuccess: false });
+ r2 = new TRRDNSListener("example.org", { expectedSuccess: false });
+ r1.cancel();
+ r2.cancel();
+ ({ inStatus } = await r1);
+ equal(inStatus, Cr.NS_ERROR_ABORT);
+ ({ inStatus } = await r2);
+ equal(inStatus, Cr.NS_ERROR_ABORT);
+ await new Promise(resolve => do_timeout(50, resolve));
+ equal(await trrServer.requestCount("example.org", "A"), 2);
+ }
+);
+
+add_task(async function cancel_delayed() {
+ Services.dns.clearCache(true);
+ await trrServer.registerDoHAnswers("example.com", "A", {
+ answers: [
+ {
+ name: "example.com",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "1.1.1.1",
+ },
+ ],
+ delay: 500,
+ });
+ let r1 = new TRRDNSListener("example.com", { expectedSuccess: false });
+ let r2 = new TRRDNSListener("example.com", { expectedAnswer: "1.1.1.1" });
+ await new Promise(resolve => do_timeout(50, resolve));
+ r1.cancel();
+ let { inStatus } = await r1;
+ equal(inStatus, Cr.NS_ERROR_ABORT);
+ await r2;
+});
+
+add_task(async function cancel_after_completed() {
+ Services.dns.clearCache(true);
+ await trrServer.registerDoHAnswers("example.com", "A", {
+ answers: [
+ {
+ name: "example.com",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "2.2.2.2",
+ },
+ ],
+ });
+ let r1 = new TRRDNSListener("example.com", { expectedAnswer: "2.2.2.2" });
+ await r1;
+ let r2 = new TRRDNSListener("example.com", { expectedAnswer: "2.2.2.2" });
+ // Check that cancelling r1 after it's complete does not affect r2 in any way.
+ r1.cancel();
+ await r2;
+});
+
+add_task(async function clearCacheWhileResolving() {
+ Services.dns.clearCache(true);
+ await trrServer.registerDoHAnswers("example.com", "A", {
+ answers: [
+ {
+ name: "example.com",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "3.3.3.3",
+ },
+ ],
+ delay: 500,
+ });
+ // Check that calling clearCache does not leave the request hanging.
+ let r1 = new TRRDNSListener("example.com", { expectedAnswer: "3.3.3.3" });
+ let r2 = new TRRDNSListener("example.com", { expectedAnswer: "3.3.3.3" });
+ Services.dns.clearCache(true);
+ await r1;
+ await r2;
+
+ // Also check the same for HTTPS records
+ await trrServer.registerDoHAnswers("httpsvc.com", "HTTPS", {
+ answers: [
+ {
+ name: "httpsvc.com",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 1,
+ name: "test.p1.com",
+ values: [{ key: "alpn", value: ["h2", "h3"] }],
+ },
+ },
+ ],
+ delay: 500,
+ });
+ let r3 = new TRRDNSListener("httpsvc.com", {
+ type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC,
+ });
+ let r4 = new TRRDNSListener("httpsvc.com", {
+ type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC,
+ });
+ Services.dns.clearCache(true);
+ await r3;
+ await r4;
+ equal(await trrServer.requestCount("httpsvc.com", "HTTPS"), 1);
+ Services.dns.clearCache(true);
+ await new TRRDNSListener("httpsvc.com", {
+ type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC,
+ });
+ equal(await trrServer.requestCount("httpsvc.com", "HTTPS"), 2);
+});
diff --git a/netwerk/test/unit/test_trr_case_sensitivity.js b/netwerk/test/unit/test_trr_case_sensitivity.js
new file mode 100644
index 0000000000..94bd79c6c4
--- /dev/null
+++ b/netwerk/test/unit/test_trr_case_sensitivity.js
@@ -0,0 +1,153 @@
+/* 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";
+
+trr_test_setup();
+registerCleanupFunction(async () => {
+ trr_clear_prefs();
+});
+
+function makeChan(url) {
+ let chan = NetUtil.newChannel({
+ uri: url,
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+ return chan;
+}
+
+function channelOpenPromise(chan) {
+ return new Promise(resolve => {
+ function finish(req, buffer) {
+ resolve([req, buffer]);
+ }
+ chan.asyncOpen(new ChannelListener(finish));
+ });
+}
+
+add_task(async function test_trr_casing() {
+ let trrServer = new TRRServer();
+ registerCleanupFunction(async () => {
+ await trrServer.stop();
+ });
+ await trrServer.start();
+ dump(`port = ${trrServer.port()}\n`);
+ let chan = makeChan(`https://localhost:${trrServer.port()}/test?bla=some`);
+ let [, resp] = await channelOpenPromise(chan);
+ equal(resp, "<h1> 404 Path not found: /test</h1>");
+
+ Services.dns.clearCache(true);
+ Services.prefs.setIntPref("network.trr.mode", 3);
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${trrServer.port()}/dns-query`
+ );
+
+ // This CNAME response goes to B.example.com (uppercased)
+ // It should be lowercased by the code
+ await trrServer.registerDoHAnswers("a.example.com", "A", {
+ answers: [
+ {
+ name: "a.example.com",
+ ttl: 55,
+ type: "CNAME",
+ flush: false,
+ data: "B.example.com",
+ },
+ ],
+ });
+ // Like in bug 1635566, the response for B.example.com will be lowercased
+ // by the server too -> b.example.com
+ // Requesting this resource would case the browser to reject the resource
+ await trrServer.registerDoHAnswers("B.example.com", "A", {
+ answers: [
+ {
+ name: "b.example.com",
+ ttl: 55,
+ type: "CNAME",
+ flush: false,
+ data: "c.example.com",
+ },
+ ],
+ });
+
+ // The browser should request this one
+ await trrServer.registerDoHAnswers("b.example.com", "A", {
+ answers: [
+ {
+ name: "b.example.com",
+ ttl: 55,
+ type: "CNAME",
+ flush: false,
+ data: "c.example.com",
+ },
+ ],
+ });
+ // Finally, it gets an IP
+ await trrServer.registerDoHAnswers("c.example.com", "A", {
+ answers: [
+ {
+ name: "c.example.com",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "1.2.3.4",
+ },
+ ],
+ });
+ await new TRRDNSListener("a.example.com", { expectedAnswer: "1.2.3.4" });
+
+ await trrServer.registerDoHAnswers("a.test.com", "A", {
+ answers: [
+ {
+ name: "a.test.com",
+ ttl: 55,
+ type: "CNAME",
+ flush: false,
+ data: "B.test.com",
+ },
+ ],
+ });
+ // We try this again, this time we explicitly make sure this resource
+ // is never used
+ await trrServer.registerDoHAnswers("B.test.com", "A", {
+ answers: [
+ {
+ name: "B.test.com",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "9.9.9.9",
+ },
+ ],
+ });
+ await trrServer.registerDoHAnswers("b.test.com", "A", {
+ answers: [
+ {
+ name: "b.test.com",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "8.8.8.8",
+ },
+ ],
+ });
+ await new TRRDNSListener("a.test.com", { expectedAnswer: "8.8.8.8" });
+
+ await trrServer.registerDoHAnswers("CAPITAL.COM", "A", {
+ answers: [
+ {
+ name: "capital.com",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "2.2.2.2",
+ },
+ ],
+ });
+ await new TRRDNSListener("CAPITAL.COM", { expectedAnswer: "2.2.2.2" });
+ await new TRRDNSListener("CAPITAL.COM.", { expectedAnswer: "2.2.2.2" });
+
+ await trrServer.stop();
+});
diff --git a/netwerk/test/unit/test_trr_cname_chain.js b/netwerk/test/unit/test_trr_cname_chain.js
new file mode 100644
index 0000000000..a3da592b7b
--- /dev/null
+++ b/netwerk/test/unit/test_trr_cname_chain.js
@@ -0,0 +1,231 @@
+/* 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 trrServer;
+
+function makeChan(url) {
+ let chan = NetUtil.newChannel({
+ uri: url,
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+ return chan;
+}
+
+function channelOpenPromise(chan) {
+ return new Promise(resolve => {
+ function finish(req, buffer) {
+ resolve([req, buffer]);
+ }
+ chan.asyncOpen(new ChannelListener(finish));
+ });
+}
+
+add_setup(async function setup() {
+ trr_test_setup();
+ registerCleanupFunction(async () => {
+ trr_clear_prefs();
+ });
+
+ trrServer = new TRRServer();
+ registerCleanupFunction(async () => {
+ await trrServer.stop();
+ });
+ await trrServer.start();
+ dump(`port = ${trrServer.port()}\n`);
+ let chan = makeChan(`https://localhost:${trrServer.port()}/test?bla=some`);
+ let [, resp] = await channelOpenPromise(chan);
+ equal(resp, "<h1> 404 Path not found: /test</h1>");
+
+ Services.dns.clearCache(true);
+ Services.prefs.setIntPref("network.trr.mode", 3);
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${trrServer.port()}/dns-query`
+ );
+});
+
+add_task(async function test_follow_cnames_same_response() {
+ await trrServer.registerDoHAnswers("something.foo", "A", {
+ answers: [
+ {
+ name: "something.foo",
+ ttl: 55,
+ type: "CNAME",
+ flush: false,
+ data: "other.foo",
+ },
+ {
+ name: "other.foo",
+ ttl: 55,
+ type: "CNAME",
+ flush: false,
+ data: "bla.foo",
+ },
+ {
+ name: "bla.foo",
+ ttl: 55,
+ type: "CNAME",
+ flush: false,
+ data: "xyz.foo",
+ },
+ {
+ name: "xyz.foo",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "1.2.3.4",
+ },
+ ],
+ });
+ let { inRecord } = await new TRRDNSListener("something.foo", {
+ expectedAnswer: "1.2.3.4",
+ flags: Ci.nsIDNSService.RESOLVE_CANONICAL_NAME,
+ });
+ equal(inRecord.QueryInterface(Ci.nsIDNSAddrRecord).canonicalName, "xyz.foo");
+
+ await trrServer.registerDoHAnswers("a.foo", "A", {
+ answers: [
+ {
+ name: "a.foo",
+ ttl: 55,
+ type: "CNAME",
+ flush: false,
+ data: "b.foo",
+ },
+ ],
+ });
+ await trrServer.registerDoHAnswers("b.foo", "A", {
+ answers: [
+ {
+ name: "b.foo",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "2.3.4.5",
+ },
+ ],
+ });
+ await new TRRDNSListener("a.foo", { expectedAnswer: "2.3.4.5" });
+});
+
+add_task(async function test_cname_nodata() {
+ // Test that we don't needlessly follow cname chains when the RA flag is set
+ // on the response.
+
+ await trrServer.registerDoHAnswers("first.foo", "A", {
+ flags: 0x80,
+ answers: [
+ {
+ name: "first.foo",
+ ttl: 55,
+ type: "CNAME",
+ flush: false,
+ data: "second.foo",
+ },
+ {
+ name: "second.foo",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "1.2.3.4",
+ },
+ ],
+ });
+ await trrServer.registerDoHAnswers("first.foo", "AAAA", {
+ flags: 0x80,
+ answers: [
+ {
+ name: "first.foo",
+ ttl: 55,
+ type: "CNAME",
+ flush: false,
+ data: "second.foo",
+ },
+ ],
+ });
+
+ await new TRRDNSListener("first.foo", { expectedAnswer: "1.2.3.4" });
+ equal(await trrServer.requestCount("first.foo", "A"), 1);
+ equal(await trrServer.requestCount("first.foo", "AAAA"), 1);
+ equal(await trrServer.requestCount("second.foo", "A"), 0);
+ equal(await trrServer.requestCount("second.foo", "AAAA"), 0);
+
+ await trrServer.registerDoHAnswers("first.bar", "A", {
+ answers: [
+ {
+ name: "first.bar",
+ ttl: 55,
+ type: "CNAME",
+ flush: false,
+ data: "second.bar",
+ },
+ {
+ name: "second.bar",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "1.2.3.4",
+ },
+ ],
+ });
+ await trrServer.registerDoHAnswers("first.bar", "AAAA", {
+ answers: [
+ {
+ name: "first.bar",
+ ttl: 55,
+ type: "CNAME",
+ flush: false,
+ data: "second.bar",
+ },
+ ],
+ });
+
+ await new TRRDNSListener("first.bar", { expectedAnswer: "1.2.3.4" });
+ equal(await trrServer.requestCount("first.bar", "A"), 1);
+ equal(await trrServer.requestCount("first.bar", "AAAA"), 1);
+ equal(await trrServer.requestCount("second.bar", "A"), 0); // addr included in first response
+ equal(await trrServer.requestCount("second.bar", "AAAA"), 1); // will follow cname because no flag is set
+
+ // Check that it also works for HTTPS records
+
+ await trrServer.registerDoHAnswers("first.bar", "HTTPS", {
+ flags: 0x80,
+ answers: [
+ {
+ name: "second.bar",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 1,
+ name: "h3pool",
+ values: [
+ { key: "alpn", value: ["h2", "h3"] },
+ { key: "no-default-alpn" },
+ { key: "port", value: 8888 },
+ { key: "ipv4hint", value: "1.2.3.4" },
+ { key: "echconfig", value: "123..." },
+ { key: "ipv6hint", value: "::1" },
+ ],
+ },
+ },
+ {
+ name: "first.bar",
+ ttl: 55,
+ type: "CNAME",
+ flush: false,
+ data: "second.bar",
+ },
+ ],
+ });
+
+ let { inStatus } = await new TRRDNSListener("first.bar", {
+ type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC,
+ });
+ Assert.ok(Components.isSuccessCode(inStatus), `${inStatus} should work`);
+ equal(await trrServer.requestCount("first.bar", "HTTPS"), 1);
+ equal(await trrServer.requestCount("second.bar", "HTTPS"), 0);
+});
diff --git a/netwerk/test/unit/test_trr_confirmation.js b/netwerk/test/unit/test_trr_confirmation.js
new file mode 100644
index 0000000000..6635f71377
--- /dev/null
+++ b/netwerk/test/unit/test_trr_confirmation.js
@@ -0,0 +1,401 @@
+/* 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 { TestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TestUtils.sys.mjs"
+);
+
+async function waitForConfirmationState(state, msToWait = 0) {
+ await TestUtils.waitForCondition(
+ () => Services.dns.currentTrrConfirmationState == state,
+ `Timed out waiting for ${state}. Currently ${Services.dns.currentTrrConfirmationState}`,
+ 1,
+ msToWait
+ );
+ equal(
+ Services.dns.currentTrrConfirmationState,
+ state,
+ "expected confirmation state"
+ );
+}
+
+const CONFIRM_OFF = 0;
+const CONFIRM_TRYING_OK = 1;
+const CONFIRM_OK = 2;
+const CONFIRM_FAILED = 3;
+const CONFIRM_TRYING_FAILED = 4;
+const CONFIRM_DISABLED = 5;
+
+function setup() {
+ trr_test_setup();
+ Services.prefs.setBoolPref("network.trr.skip-check-for-blocked-host", true);
+}
+
+setup();
+registerCleanupFunction(async () => {
+ trr_clear_prefs();
+ Services.prefs.clearUserPref("network.trr.skip-check-for-blocked-host");
+});
+
+let trrServer = null;
+add_task(async function start_trr_server() {
+ trrServer = new TRRServer();
+ registerCleanupFunction(async () => {
+ await trrServer.stop();
+ });
+ await trrServer.start();
+ dump(`port = ${trrServer.port()}\n`);
+
+ await trrServer.registerDoHAnswers(`faily.com`, "NS", {
+ answers: [
+ {
+ name: "faily.com",
+ ttl: 55,
+ type: "NS",
+ flush: false,
+ data: "ns.faily.com",
+ },
+ ],
+ });
+
+ for (let i = 0; i < 15; i++) {
+ await trrServer.registerDoHAnswers(`failing-domain${i}.faily.com`, "A", {
+ error: 600,
+ });
+ await trrServer.registerDoHAnswers(`failing-domain${i}.faily.com`, "AAAA", {
+ error: 600,
+ });
+ }
+});
+
+function trigger15Failures() {
+ // We need to clear the cache in case a previous call to this method
+ // put the results in the DNS cache.
+ Services.dns.clearCache(true);
+
+ let dnsRequests = [];
+ // There are actually two TRR requests sent for A and AAAA records, so doing
+ // DNS query 10 times should be enough to trigger confirmation process.
+ for (let i = 0; i < 10; i++) {
+ dnsRequests.push(
+ new TRRDNSListener(`failing-domain${i}.faily.com`, {
+ expectedAnswer: "127.0.0.1",
+ })
+ );
+ }
+
+ return Promise.all(dnsRequests);
+}
+
+async function registerNS(delay) {
+ return trrServer.registerDoHAnswers("confirm.example.com", "NS", {
+ answers: [
+ {
+ name: "confirm.example.com",
+ ttl: 55,
+ type: "NS",
+ flush: false,
+ data: "test.com",
+ },
+ ],
+ delay,
+ });
+}
+
+add_task(async function confirm_off() {
+ Services.prefs.setCharPref(
+ "network.trr.confirmationNS",
+ "confirm.example.com"
+ );
+ Services.prefs.setIntPref(
+ "network.trr.mode",
+ Ci.nsIDNSService.MODE_NATIVEONLY
+ );
+ equal(Services.dns.currentTrrConfirmationState, CONFIRM_OFF);
+ Services.prefs.setIntPref("network.trr.mode", Ci.nsIDNSService.MODE_TRROFF);
+ equal(Services.dns.currentTrrConfirmationState, CONFIRM_OFF);
+});
+
+add_task(async function confirm_disabled() {
+ Services.prefs.setCharPref(
+ "network.trr.confirmationNS",
+ "confirm.example.com"
+ );
+ Services.prefs.setIntPref("network.trr.mode", Ci.nsIDNSService.MODE_TRRONLY);
+ equal(Services.dns.currentTrrConfirmationState, CONFIRM_DISABLED);
+ Services.prefs.setCharPref("network.trr.confirmationNS", "skip");
+ Services.prefs.setIntPref("network.trr.mode", Ci.nsIDNSService.MODE_TRRFIRST);
+ equal(Services.dns.currentTrrConfirmationState, CONFIRM_DISABLED);
+});
+
+add_task(async function confirm_ok() {
+ Services.dns.clearCache(true);
+ Services.prefs.setCharPref(
+ "network.trr.confirmationNS",
+ "confirm.example.com"
+ );
+ await registerNS(0);
+ await trrServer.registerDoHAnswers("example.com", "A", {
+ answers: [
+ {
+ name: "example.com",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "1.2.3.4",
+ },
+ ],
+ });
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${trrServer.port()}/dns-query`
+ );
+ Services.prefs.setIntPref("network.trr.mode", Ci.nsIDNSService.MODE_TRRFIRST);
+ equal(
+ Services.dns.currentTrrConfirmationState,
+ CONFIRM_TRYING_OK,
+ "Should be CONFIRM_TRYING_OK"
+ );
+ await new TRRDNSListener("example.com", { expectedAnswer: "1.2.3.4" });
+ equal(await trrServer.requestCount("example.com", "A"), 1);
+ await waitForConfirmationState(CONFIRM_OK, 1000);
+
+ await registerNS(500);
+ Services.prefs.setIntPref(
+ "network.trr.mode",
+ Ci.nsIDNSService.MODE_NATIVEONLY
+ );
+ Services.prefs.setIntPref("network.trr.mode", Ci.nsIDNSService.MODE_TRRFIRST);
+ equal(
+ Services.dns.currentTrrConfirmationState,
+ CONFIRM_TRYING_OK,
+ "Should be CONFIRM_TRYING_OK"
+ );
+ await new Promise(resolve => do_timeout(100, resolve));
+ equal(
+ Services.dns.currentTrrConfirmationState,
+ CONFIRM_TRYING_OK,
+ "Confirmation should still be pending"
+ );
+ await waitForConfirmationState(CONFIRM_OK, 1000);
+});
+
+add_task(async function confirm_timeout() {
+ Services.prefs.setIntPref(
+ "network.trr.mode",
+ Ci.nsIDNSService.MODE_NATIVEONLY
+ );
+ equal(Services.dns.currentTrrConfirmationState, CONFIRM_OFF);
+ await registerNS(7000);
+ Services.prefs.setIntPref("network.trr.mode", Ci.nsIDNSService.MODE_TRRFIRST);
+ equal(
+ Services.dns.currentTrrConfirmationState,
+ CONFIRM_TRYING_OK,
+ "Should be CONFIRM_TRYING_OK"
+ );
+ await waitForConfirmationState(CONFIRM_FAILED, 7500);
+ // After the confirmation fails, a timer will periodically trigger a retry
+ // causing the state to go into CONFIRM_TRYING_FAILED.
+ await waitForConfirmationState(CONFIRM_TRYING_FAILED, 500);
+});
+
+add_task(async function confirm_fail_fast() {
+ Services.prefs.setIntPref(
+ "network.trr.mode",
+ Ci.nsIDNSService.MODE_NATIVEONLY
+ );
+ equal(Services.dns.currentTrrConfirmationState, CONFIRM_OFF);
+ await trrServer.registerDoHAnswers("confirm.example.com", "NS", {
+ error: 404,
+ });
+ Services.prefs.setIntPref("network.trr.mode", Ci.nsIDNSService.MODE_TRRFIRST);
+ equal(
+ Services.dns.currentTrrConfirmationState,
+ CONFIRM_TRYING_OK,
+ "Should be CONFIRM_TRYING_OK"
+ );
+ await waitForConfirmationState(CONFIRM_FAILED, 100);
+});
+
+add_task(async function multiple_failures() {
+ Services.prefs.setIntPref(
+ "network.trr.mode",
+ Ci.nsIDNSService.MODE_NATIVEONLY
+ );
+ equal(Services.dns.currentTrrConfirmationState, CONFIRM_OFF);
+
+ await registerNS(100);
+ Services.prefs.setIntPref("network.trr.mode", Ci.nsIDNSService.MODE_TRRFIRST);
+ equal(
+ Services.dns.currentTrrConfirmationState,
+ CONFIRM_TRYING_OK,
+ "Should be CONFIRM_TRYING_OK"
+ );
+ await waitForConfirmationState(CONFIRM_OK, 1000);
+ await registerNS(4000);
+ let failures = trigger15Failures();
+ await waitForConfirmationState(CONFIRM_TRYING_OK, 3000);
+ await failures;
+ // Check that failures during confirmation are ignored.
+ await trigger15Failures();
+ equal(
+ Services.dns.currentTrrConfirmationState,
+ CONFIRM_TRYING_OK,
+ "Should be CONFIRM_TRYING_OK"
+ );
+ await waitForConfirmationState(CONFIRM_OK, 4500);
+});
+
+add_task(async function test_connectivity_change() {
+ await registerNS(100);
+ Services.prefs.setIntPref(
+ "network.trr.mode",
+ Ci.nsIDNSService.MODE_NATIVEONLY
+ );
+ let confirmationCount = await trrServer.requestCount(
+ "confirm.example.com",
+ "NS"
+ );
+ Services.prefs.setIntPref("network.trr.mode", Ci.nsIDNSService.MODE_TRRFIRST);
+ equal(
+ Services.dns.currentTrrConfirmationState,
+ CONFIRM_TRYING_OK,
+ "Should be CONFIRM_TRYING_OK"
+ );
+ await waitForConfirmationState(CONFIRM_OK, 1000);
+ equal(
+ await trrServer.requestCount("confirm.example.com", "NS"),
+ confirmationCount + 1
+ );
+ Services.obs.notifyObservers(
+ null,
+ "network:captive-portal-connectivity",
+ "clear"
+ );
+ // This means a CP check completed successfully. But no CP was previously
+ // detected, so this is mostly a no-op.
+ equal(Services.dns.currentTrrConfirmationState, CONFIRM_OK);
+
+ Services.obs.notifyObservers(
+ null,
+ "network:captive-portal-connectivity",
+ "captive"
+ );
+ // This basically a successful CP login event. Wasn't captive before.
+ // Still treating as a no-op.
+ equal(Services.dns.currentTrrConfirmationState, CONFIRM_OK);
+
+ // This makes the TRR service set mCaptiveIsPassed=false
+ Services.obs.notifyObservers(
+ null,
+ "captive-portal-login",
+ "{type: 'captive-portal-login', id: 0, url: 'http://localhost/'}"
+ );
+
+ await registerNS(500);
+ let failures = trigger15Failures();
+ // The failure should cause us to go into CONFIRM_TRYING_OK and do an NS req
+ await waitForConfirmationState(CONFIRM_TRYING_OK, 3000);
+ await failures;
+
+ // The notification sets mCaptiveIsPassed=true then triggers an entirely new
+ // confirmation.
+ Services.obs.notifyObservers(
+ null,
+ "network:captive-portal-connectivity",
+ "clear"
+ );
+ // The notification should cause us to send a new confirmation request
+ equal(
+ Services.dns.currentTrrConfirmationState,
+ CONFIRM_TRYING_OK,
+ "Should be CONFIRM_TRYING_OK"
+ );
+ await waitForConfirmationState(CONFIRM_OK, 1000);
+ // two extra confirmation events should have been received by the server
+ equal(
+ await trrServer.requestCount("confirm.example.com", "NS"),
+ confirmationCount + 3
+ );
+});
+
+add_task(async function test_network_change() {
+ let confirmationCount = await trrServer.requestCount(
+ "confirm.example.com",
+ "NS"
+ );
+ equal(Services.dns.currentTrrConfirmationState, CONFIRM_OK);
+
+ Services.obs.notifyObservers(null, "network:link-status-changed", "up");
+ equal(Services.dns.currentTrrConfirmationState, CONFIRM_OK);
+ equal(
+ await trrServer.requestCount("confirm.example.com", "NS"),
+ confirmationCount
+ );
+
+ let failures = trigger15Failures();
+ // The failure should cause us to go into CONFIRM_TRYING_OK and do an NS req
+ await waitForConfirmationState(CONFIRM_TRYING_OK, 3000);
+ await failures;
+ // The network up event should reset the confirmation to TRYING_OK and do
+ // another NS req
+ Services.obs.notifyObservers(null, "network:link-status-changed", "up");
+ equal(Services.dns.currentTrrConfirmationState, CONFIRM_TRYING_OK);
+ await waitForConfirmationState(CONFIRM_OK, 1000);
+ // two extra confirmation events should have been received by the server
+ equal(
+ await trrServer.requestCount("confirm.example.com", "NS"),
+ confirmationCount + 2
+ );
+});
+
+add_task(async function test_uri_pref_change() {
+ let confirmationCount = await trrServer.requestCount(
+ "confirm.example.com",
+ "NS"
+ );
+ equal(Services.dns.currentTrrConfirmationState, CONFIRM_OK);
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${trrServer.port()}/dns-query?changed`
+ );
+ equal(Services.dns.currentTrrConfirmationState, CONFIRM_TRYING_OK);
+ await waitForConfirmationState(CONFIRM_OK, 1000);
+ equal(
+ await trrServer.requestCount("confirm.example.com", "NS"),
+ confirmationCount + 1
+ );
+});
+
+add_task(async function test_autodetected_uri() {
+ const defaultPrefBranch = Services.prefs.getDefaultBranch("");
+ let defaultURI = defaultPrefBranch.getCharPref(
+ "network.trr.default_provider_uri"
+ );
+ defaultPrefBranch.setCharPref(
+ "network.trr.default_provider_uri",
+ `https://foo.example.com:${trrServer.port()}/dns-query?changed`
+ );
+ // For setDetectedTrrURI to work we must pretend we are using the default.
+ Services.prefs.clearUserPref("network.trr.uri");
+ await waitForConfirmationState(CONFIRM_OK, 1000);
+ let confirmationCount = await trrServer.requestCount(
+ "confirm.example.com",
+ "NS"
+ );
+ Services.dns.setDetectedTrrURI(
+ `https://foo.example.com:${trrServer.port()}/dns-query?changed2`
+ );
+ equal(Services.dns.currentTrrConfirmationState, CONFIRM_TRYING_OK);
+ await waitForConfirmationState(CONFIRM_OK, 1000);
+ equal(
+ await trrServer.requestCount("confirm.example.com", "NS"),
+ confirmationCount + 1
+ );
+
+ // reset the default URI
+ defaultPrefBranch.setCharPref("network.trr.default_provider_uri", defaultURI);
+});
diff --git a/netwerk/test/unit/test_trr_decoding.js b/netwerk/test/unit/test_trr_decoding.js
new file mode 100644
index 0000000000..a2b80ec773
--- /dev/null
+++ b/netwerk/test/unit/test_trr_decoding.js
@@ -0,0 +1,56 @@
+/* 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";
+
+trr_test_setup();
+registerCleanupFunction(async () => {
+ trr_clear_prefs();
+});
+
+let trrServer = null;
+add_setup(async function start_trr_server() {
+ trrServer = new TRRServer();
+ registerCleanupFunction(async () => {
+ await trrServer.stop();
+ });
+ await trrServer.start();
+ dump(`port = ${trrServer.port()}\n`);
+
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${trrServer.port()}/dns-query`
+ );
+ Services.prefs.setIntPref("network.trr.mode", Ci.nsIDNSService.MODE_TRRONLY);
+});
+
+add_task(async function ignoreUnknownTypes() {
+ Services.dns.clearCache(true);
+ await trrServer.registerDoHAnswers("abc.def.ced.com", "A", {
+ answers: [
+ {
+ name: "abc.def.ced.com",
+ ttl: 55,
+ type: "DNAME",
+ flush: false,
+ data: "def.ced.com.test",
+ },
+ {
+ name: "abc.def.ced.com",
+ ttl: 55,
+ type: "CNAME",
+ flush: false,
+ data: "abc.def.ced.com.test",
+ },
+ {
+ name: "abc.def.ced.com.test",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "3.3.3.3",
+ },
+ ],
+ });
+ await new TRRDNSListener("abc.def.ced.com", { expectedAnswer: "3.3.3.3" });
+});
diff --git a/netwerk/test/unit/test_trr_domain.js b/netwerk/test/unit/test_trr_domain.js
new file mode 100644
index 0000000000..b8b6c460eb
--- /dev/null
+++ b/netwerk/test/unit/test_trr_domain.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";
+
+// This test simmulates intermittent native DNS functionality.
+// We verify that we don't use the negative DNS record for the DoH server.
+// The first resolve of foo.example.com fails, so we expect TRR not to work.
+// Immediately after the native DNS starts working, it should connect to the
+// TRR server and start working.
+
+const override = Cc["@mozilla.org/network/native-dns-override;1"].getService(
+ Ci.nsINativeDNSResolverOverride
+);
+
+function setup() {
+ trr_test_setup();
+ Services.prefs.clearUserPref("network.trr.bootstrapAddr");
+ Services.prefs.clearUserPref("network.dns.native-is-localhost");
+}
+setup();
+
+registerCleanupFunction(async () => {
+ trr_clear_prefs();
+ override.clearOverrides();
+});
+
+add_task(async function intermittent_dns_mode3() {
+ override.addIPOverride("foo.example.com", "N/A");
+ let trrServer = new TRRServer();
+ registerCleanupFunction(async () => {
+ await trrServer.stop();
+ });
+ await trrServer.start();
+ info(`port = ${trrServer.port()}\n`);
+ Services.dns.clearCache(true);
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${trrServer.port()}/dns-query`
+ );
+ Services.prefs.setIntPref("network.trr.mode", Ci.nsIDNSService.MODE_TRRONLY);
+ await trrServer.registerDoHAnswers("example.com", "A", {
+ answers: [
+ {
+ name: "example.com",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "1.2.3.4",
+ },
+ ],
+ });
+ let { inStatus } = await new TRRDNSListener("example.com", {
+ expectedSuccess: false,
+ });
+ equal(inStatus, Cr.NS_ERROR_UNKNOWN_HOST);
+ await trrServer.registerDoHAnswers("example.org", "A", {
+ answers: [
+ {
+ name: "example.org",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "1.2.3.4",
+ },
+ ],
+ });
+ override.addIPOverride("foo.example.com", "127.0.0.1");
+ await new TRRDNSListener("example.org", { expectedAnswer: "1.2.3.4" });
+ await trrServer.stop();
+});
+
+add_task(async function intermittent_dns_mode2() {
+ override.addIPOverride("foo.example.com", "N/A");
+ let trrServer = new TRRServer();
+ registerCleanupFunction(async () => {
+ await trrServer.stop();
+ });
+ await trrServer.start();
+ info(`port = ${trrServer.port()}\n`);
+
+ Services.dns.clearCache(true);
+ Services.prefs.setIntPref(
+ "network.trr.mode",
+ Ci.nsIDNSService.MODE_NATIVEONLY
+ );
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${trrServer.port()}/dns-query`
+ );
+ Services.prefs.setIntPref("network.trr.mode", Ci.nsIDNSService.MODE_TRRFIRST);
+ await trrServer.registerDoHAnswers("example.com", "A", {
+ answers: [
+ {
+ name: "example.com",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "1.1.1.1",
+ },
+ ],
+ });
+ override.addIPOverride("example.com", "2.2.2.2");
+ await new TRRDNSListener("example.com", {
+ expectedAnswer: "2.2.2.2",
+ });
+ await trrServer.registerDoHAnswers("example.org", "A", {
+ answers: [
+ {
+ name: "example.org",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "1.2.3.4",
+ },
+ ],
+ });
+ override.addIPOverride("example.org", "3.3.3.3");
+ override.addIPOverride("foo.example.com", "127.0.0.1");
+ await new TRRDNSListener("example.org", { expectedAnswer: "1.2.3.4" });
+ await trrServer.stop();
+});
diff --git a/netwerk/test/unit/test_trr_enterprise_policy.js b/netwerk/test/unit/test_trr_enterprise_policy.js
new file mode 100644
index 0000000000..e96753d554
--- /dev/null
+++ b/netwerk/test/unit/test_trr_enterprise_policy.js
@@ -0,0 +1,93 @@
+/* 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";
+
+trr_test_setup();
+registerCleanupFunction(async () => {
+ trr_clear_prefs();
+});
+
+const { updateAppInfo } = ChromeUtils.importESModule(
+ "resource://testing-common/AppInfo.sys.mjs"
+);
+
+updateAppInfo({
+ name: "XPCShell",
+ ID: "xpcshell@tests.mozilla.org",
+ version: "48",
+ platformVersion: "48",
+});
+
+const { EnterprisePolicyTesting } = ChromeUtils.importESModule(
+ "resource://testing-common/EnterprisePolicyTesting.sys.mjs"
+);
+
+// This initializes the policy engine for xpcshell tests
+let policies = Cc["@mozilla.org/enterprisepolicies;1"].getService(
+ Ci.nsIObserver
+);
+policies.observe(null, "policies-startup", null);
+
+add_task(async function test_enterprise_policy_unlocked() {
+ await EnterprisePolicyTesting.setupPolicyEngineWithJson({
+ policies: {
+ DNSOverHTTPS: {
+ Enabled: false,
+ ProviderURL: "https://example.org/provider",
+ ExcludedDomains: ["example.com", "example.org"],
+ },
+ },
+ });
+
+ equal(Services.prefs.getIntPref("network.trr.mode"), 5);
+ equal(Services.prefs.prefIsLocked("network.trr.mode"), false);
+ equal(
+ Services.prefs.getStringPref("network.trr.uri"),
+ "https://example.org/provider"
+ );
+ equal(Services.prefs.prefIsLocked("network.trr.uri"), false);
+ equal(
+ Services.prefs.getStringPref("network.trr.excluded-domains"),
+ "example.com,example.org"
+ );
+ equal(Services.prefs.prefIsLocked("network.trr.excluded-domains"), false);
+ equal(Services.dns.currentTrrMode, 5);
+ equal(Services.dns.currentTrrURI, "https://example.org/provider");
+ Services.dns.setDetectedTrrURI("https://autodetect.example.com/provider");
+ equal(Services.dns.currentTrrMode, 5);
+ equal(Services.dns.currentTrrURI, "https://example.org/provider");
+});
+
+add_task(async function test_enterprise_policy_locked() {
+ // Read dns.currentTrrMode to make DNS service initialized earlier.
+ info("Services.dns.currentTrrMode:" + Services.dns.currentTrrMode);
+ await EnterprisePolicyTesting.setupPolicyEngineWithJson({
+ policies: {
+ DNSOverHTTPS: {
+ Enabled: true,
+ ProviderURL: "https://example.com/provider",
+ ExcludedDomains: ["example.com", "example.org"],
+ Locked: true,
+ },
+ },
+ });
+
+ equal(Services.prefs.getIntPref("network.trr.mode"), 2);
+ equal(Services.prefs.prefIsLocked("network.trr.mode"), true);
+ equal(
+ Services.prefs.getStringPref("network.trr.uri"),
+ "https://example.com/provider"
+ );
+ equal(Services.prefs.prefIsLocked("network.trr.uri"), true);
+ equal(
+ Services.prefs.getStringPref("network.trr.excluded-domains"),
+ "example.com,example.org"
+ );
+ equal(Services.prefs.prefIsLocked("network.trr.excluded-domains"), true);
+ equal(Services.dns.currentTrrMode, 2);
+ equal(Services.dns.currentTrrURI, "https://example.com/provider");
+ Services.dns.setDetectedTrrURI("https://autodetect.example.com/provider");
+ equal(Services.dns.currentTrrURI, "https://example.com/provider");
+});
diff --git a/netwerk/test/unit/test_trr_extended_error.js b/netwerk/test/unit/test_trr_extended_error.js
new file mode 100644
index 0000000000..68606c93f8
--- /dev/null
+++ b/netwerk/test/unit/test_trr_extended_error.js
@@ -0,0 +1,319 @@
+/* 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";
+
+trr_test_setup();
+registerCleanupFunction(async () => {
+ trr_clear_prefs();
+});
+
+function makeChan(url) {
+ let chan = NetUtil.newChannel({
+ uri: url,
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+ return chan;
+}
+
+function channelOpenPromise(chan) {
+ return new Promise(resolve => {
+ function finish(req, buffer) {
+ resolve([req, buffer]);
+ }
+ chan.asyncOpen(new ChannelListener(finish));
+ });
+}
+
+let trrServer;
+add_task(async function setup() {
+ trrServer = new TRRServer();
+ registerCleanupFunction(async () => {
+ await trrServer.stop();
+ });
+ await trrServer.start();
+ dump(`port = ${trrServer.port()}\n`);
+ let chan = makeChan(`https://localhost:${trrServer.port()}/test?bla=some`);
+ let [, resp] = await channelOpenPromise(chan);
+ equal(resp, "<h1> 404 Path not found: /test</h1>");
+
+ Services.dns.clearCache(true);
+ Services.prefs.setIntPref("network.trr.mode", 2);
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${trrServer.port()}/dns-query`
+ );
+});
+
+add_task(async function test_extended_error_1() {
+ await trrServer.registerDoHAnswers("something.foo", "A", {
+ answers: [
+ {
+ name: "something.foo",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "1.2.3.4",
+ },
+ ],
+ });
+
+ await new TRRDNSListener("something.foo", { expectedAnswer: "1.2.3.4" });
+
+ await trrServer.registerDoHAnswers("a.foo", "A", {
+ answers: [],
+ additionals: [
+ {
+ name: ".",
+ type: "OPT",
+ class: "IN",
+ options: [
+ {
+ code: "EDNS_ERROR",
+ extended_error: 17, // Filtered
+ text: "DNSSec bogus",
+ },
+ ],
+ },
+ ],
+ flags: 2, // SERVFAIL
+ });
+
+ // Check that we don't fall back to DNS
+ let { inStatus } = await new TRRDNSListener("a.foo", {
+ expectedSuccess: false,
+ });
+ Assert.ok(
+ !Components.isSuccessCode(inStatus),
+ `${inStatus} should be an error code`
+ );
+});
+
+add_task(async function test_extended_error_filtered() {
+ await trrServer.registerDoHAnswers("b.foo", "A", {
+ answers: [],
+ additionals: [
+ {
+ name: ".",
+ type: "OPT",
+ class: "IN",
+ options: [
+ {
+ code: "EDNS_ERROR",
+ extended_error: 17, // Filtered
+ text: "Filtered",
+ },
+ ],
+ },
+ ],
+ });
+
+ // Check that we don't fall back to DNS
+ let { inStatus } = await new TRRDNSListener("b.foo", {
+ expectedSuccess: false,
+ });
+ Assert.ok(
+ !Components.isSuccessCode(inStatus),
+ `${inStatus} should be an error code`
+ );
+});
+
+add_task(async function test_extended_error_not_ready() {
+ await trrServer.registerDoHAnswers("c.foo", "A", {
+ answers: [],
+ additionals: [
+ {
+ name: ".",
+ type: "OPT",
+ class: "IN",
+ options: [
+ {
+ code: "EDNS_ERROR",
+ extended_error: 14, // Not ready
+ text: "Not ready",
+ },
+ ],
+ },
+ ],
+ });
+
+ // For this code it's OK to fallback
+ await new TRRDNSListener("c.foo", { expectedAnswer: "127.0.0.1" });
+});
+
+add_task(async function ipv6_answer_and_delayed_ipv4_error() {
+ // AAAA comes back immediately.
+ // A EDNS_ERROR comes back later, with a delay
+ await trrServer.registerDoHAnswers("delay1.com", "AAAA", {
+ answers: [
+ {
+ name: "delay1.com",
+ ttl: 55,
+ type: "AAAA",
+ flush: false,
+ data: "::a:b:c:d",
+ },
+ ],
+ });
+ await trrServer.registerDoHAnswers("delay1.com", "A", {
+ answers: [],
+ additionals: [
+ {
+ name: ".",
+ type: "OPT",
+ class: "IN",
+ options: [
+ {
+ code: "EDNS_ERROR",
+ extended_error: 17, // Filtered
+ text: "Filtered",
+ },
+ ],
+ },
+ ],
+ delay: 200,
+ });
+
+ // Check that we don't fall back to DNS
+ await new TRRDNSListener("delay1.com", { expectedAnswer: "::a:b:c:d" });
+});
+
+add_task(async function ipv4_error_and_delayed_ipv6_answer() {
+ // AAAA comes back immediately delay
+ // A EDNS_ERROR comes back immediately
+ await trrServer.registerDoHAnswers("delay2.com", "AAAA", {
+ answers: [
+ {
+ name: "delay2.com",
+ ttl: 55,
+ type: "AAAA",
+ flush: false,
+ data: "::a:b:c:d",
+ },
+ ],
+ delay: 200,
+ });
+ await trrServer.registerDoHAnswers("delay2.com", "A", {
+ answers: [],
+ additionals: [
+ {
+ name: ".",
+ type: "OPT",
+ class: "IN",
+ options: [
+ {
+ code: "EDNS_ERROR",
+ extended_error: 17, // Filtered
+ text: "Filtered",
+ },
+ ],
+ },
+ ],
+ });
+
+ // Check that we don't fall back to DNS
+ await new TRRDNSListener("delay2.com", { expectedAnswer: "::a:b:c:d" });
+});
+
+add_task(async function ipv4_answer_and_delayed_ipv6_error() {
+ // A comes back immediately.
+ // AAAA EDNS_ERROR comes back later, with a delay
+ await trrServer.registerDoHAnswers("delay3.com", "A", {
+ answers: [
+ {
+ name: "delay3.com",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "1.2.3.4",
+ },
+ ],
+ });
+ await trrServer.registerDoHAnswers("delay3.com", "AAAA", {
+ answers: [],
+ additionals: [
+ {
+ name: ".",
+ type: "OPT",
+ class: "IN",
+ options: [
+ {
+ code: "EDNS_ERROR",
+ extended_error: 17, // Filtered
+ text: "Filtered",
+ },
+ ],
+ },
+ ],
+ delay: 200,
+ });
+
+ // Check that we don't fall back to DNS
+ await new TRRDNSListener("delay3.com", { expectedAnswer: "1.2.3.4" });
+});
+
+add_task(async function delayed_ipv4_answer_and_ipv6_error() {
+ // A comes back with delay.
+ // AAAA EDNS_ERROR comes immediately
+ await trrServer.registerDoHAnswers("delay4.com", "A", {
+ answers: [
+ {
+ name: "delay4.com",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "1.2.3.4",
+ },
+ ],
+ delay: 200,
+ });
+ await trrServer.registerDoHAnswers("delay4.com", "AAAA", {
+ answers: [],
+ additionals: [
+ {
+ name: ".",
+ type: "OPT",
+ class: "IN",
+ options: [
+ {
+ code: "EDNS_ERROR",
+ extended_error: 17, // Filtered
+ text: "Filtered",
+ },
+ ],
+ },
+ ],
+ });
+
+ // Check that we don't fall back to DNS
+ await new TRRDNSListener("delay4.com", { expectedAnswer: "1.2.3.4" });
+});
+
+add_task(async function test_only_ipv4_extended_error() {
+ Services.prefs.setBoolPref("network.dns.disableIPv6", true);
+ await trrServer.registerDoHAnswers("only.com", "A", {
+ answers: [],
+ additionals: [
+ {
+ name: ".",
+ type: "OPT",
+ class: "IN",
+ options: [
+ {
+ code: "EDNS_ERROR",
+ extended_error: 17, // Filtered
+ text: "Filtered",
+ },
+ ],
+ },
+ ],
+ });
+ let { inStatus } = await new TRRDNSListener("only.com", {
+ expectedSuccess: false,
+ });
+ Assert.ok(
+ !Components.isSuccessCode(inStatus),
+ `${inStatus} should be an error code`
+ );
+});
diff --git a/netwerk/test/unit/test_trr_https_fallback.js b/netwerk/test/unit/test_trr_https_fallback.js
new file mode 100644
index 0000000000..23c5adedaa
--- /dev/null
+++ b/netwerk/test/unit/test_trr_https_fallback.js
@@ -0,0 +1,1105 @@
+/* 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 { TestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TestUtils.sys.mjs"
+);
+
+let h2Port;
+let h3Port;
+let h3NoResponsePort;
+let trrServer;
+
+const certOverrideService = Cc[
+ "@mozilla.org/security/certoverride;1"
+].getService(Ci.nsICertOverrideService);
+
+add_setup(async function setup() {
+ trr_test_setup();
+
+ h2Port = Services.env.get("MOZHTTP2_PORT");
+ Assert.notEqual(h2Port, null);
+ Assert.notEqual(h2Port, "");
+
+ h3Port = Services.env.get("MOZHTTP3_PORT");
+ Assert.notEqual(h3Port, null);
+ Assert.notEqual(h3Port, "");
+
+ h3NoResponsePort = Services.env.get("MOZHTTP3_PORT_NO_RESPONSE");
+ Assert.notEqual(h3NoResponsePort, null);
+ Assert.notEqual(h3NoResponsePort, "");
+
+ 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);
+
+ registerCleanupFunction(async () => {
+ trr_clear_prefs();
+ Services.prefs.clearUserPref("network.dns.upgrade_with_https_rr");
+ Services.prefs.clearUserPref("network.dns.use_https_rr_as_altsvc");
+ Services.prefs.clearUserPref("network.dns.echconfig.enabled");
+ Services.prefs.clearUserPref(
+ "network.dns.echconfig.fallback_to_origin_when_all_failed"
+ );
+ Services.prefs.clearUserPref("network.dns.httpssvc.reset_exclustion_list");
+ Services.prefs.clearUserPref("network.http.http3.enable");
+ Services.prefs.clearUserPref(
+ "network.dns.httpssvc.http3_fast_fallback_timeout"
+ );
+ Services.prefs.clearUserPref("network.http.speculative-parallel-limit");
+ Services.prefs.clearUserPref("network.dns.localDomains");
+ Services.prefs.clearUserPref("network.dns.http3_echconfig.enabled");
+ if (trrServer) {
+ await trrServer.stop();
+ }
+ });
+
+ if (mozinfo.socketprocess_networking) {
+ Services.dns; // Needed to trigger socket process.
+ await TestUtils.waitForCondition(() => Services.io.socketProcessLaunched);
+ }
+
+ Services.prefs.setIntPref("network.trr.mode", Ci.nsIDNSService.MODE_TRRFIRST);
+});
+
+function makeChan(url) {
+ let chan = NetUtil.newChannel({
+ uri: url,
+ loadUsingSystemPrincipal: true,
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT,
+ }).QueryInterface(Ci.nsIHttpChannel);
+ 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));
+ });
+}
+
+// Test if we can fallback to the last record sucessfully.
+add_task(async function testFallbackToTheLastRecord() {
+ 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("test.fallback.com", "HTTPS", {
+ answers: [
+ {
+ name: "test.fallback.com",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 1,
+ name: "test.fallback1.com",
+ values: [
+ { key: "alpn", value: ["h2", "h3-26"] },
+ { key: "echconfig", value: "123..." },
+ ],
+ },
+ },
+ {
+ name: "test.fallback.com",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 4,
+ name: "foo.example.com",
+ values: [
+ { key: "alpn", value: ["h2", "h3-26"] },
+ { key: "port", value: h2Port },
+ { key: "echconfig", value: "456..." },
+ ],
+ },
+ },
+ {
+ name: "test.fallback.com",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 3,
+ name: "test.fallback3.com",
+ values: [
+ { key: "alpn", value: ["h2", "h3-26"] },
+ { key: "echconfig", value: "456..." },
+ ],
+ },
+ },
+ {
+ name: "test.fallback.com",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 2,
+ name: "test.fallback2.com",
+ values: [
+ { key: "alpn", value: ["h2", "h3-26"] },
+ { key: "echconfig", value: "456..." },
+ ],
+ },
+ },
+ ],
+ });
+
+ await new TRRDNSListener("test.fallback.com", {
+ type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC,
+ });
+
+ let chan = makeChan(`https://test.fallback.com:${h2Port}/server-timing`);
+ let [req] = await channelOpenPromise(chan);
+ // Test if this request is done by h2.
+ Assert.equal(req.getResponseHeader("x-connection-http2"), "yes");
+
+ await trrServer.stop();
+});
+
+add_task(async function testFallbackToTheOrigin() {
+ trrServer = new TRRServer();
+ await trrServer.start();
+ Services.prefs.setIntPref("network.trr.mode", 3);
+ Services.prefs.setBoolPref(
+ "network.dns.echconfig.fallback_to_origin_when_all_failed",
+ true
+ );
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${trrServer.port()}/dns-query`
+ );
+
+ // All records are not able to use to connect, so we fallback to the origin
+ // one.
+ await trrServer.registerDoHAnswers("test.foo.com", "HTTPS", {
+ answers: [
+ {
+ name: "test.foo.com",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 1,
+ name: "test.foo1.com",
+ values: [
+ { key: "alpn", value: ["h2", "h3-26"] },
+ { key: "echconfig", value: "123..." },
+ ],
+ },
+ },
+ {
+ name: "test.foo.com",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 3,
+ name: "test.foo3.com",
+ values: [
+ { key: "alpn", value: ["h2", "h3-26"] },
+ { key: "echconfig", value: "456..." },
+ ],
+ },
+ },
+ {
+ name: "test.foo.com",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 2,
+ name: "test.foo2.com",
+ values: [
+ { key: "alpn", value: ["h2", "h3-26"] },
+ { key: "echconfig", value: "456..." },
+ ],
+ },
+ },
+ ],
+ });
+
+ await trrServer.registerDoHAnswers("test.foo.com", "A", {
+ answers: [
+ {
+ name: "test.foo.com",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "127.0.0.1",
+ },
+ ],
+ });
+
+ await new TRRDNSListener("test.foo.com", {
+ type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC,
+ });
+
+ let chan = makeChan(`https://test.foo.com:${h2Port}/server-timing`);
+ let [req] = await channelOpenPromise(chan);
+ // Test if this request is done by h2.
+ Assert.equal(req.getResponseHeader("x-connection-http2"), "yes");
+
+ await trrServer.stop();
+});
+
+// Test when all records are failed and network.dns.echconfig.fallback_to_origin
+// is false. In this case, the connection is always failed.
+add_task(async function testAllRecordsFailed() {
+ 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`
+ );
+ Services.prefs.setBoolPref(
+ "network.dns.echconfig.fallback_to_origin_when_all_failed",
+ false
+ );
+
+ await trrServer.registerDoHAnswers("test.bar.com", "HTTPS", {
+ answers: [
+ {
+ name: "test.bar.com",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 1,
+ name: "test.bar1.com",
+ values: [
+ { key: "alpn", value: ["h2", "h3-26"] },
+ { key: "echconfig", value: "123..." },
+ ],
+ },
+ },
+ {
+ name: "test.bar.com",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 3,
+ name: "test.bar3.com",
+ values: [
+ { key: "alpn", value: ["h2", "h3-26"] },
+ { key: "echconfig", value: "456..." },
+ ],
+ },
+ },
+ {
+ name: "test.bar.com",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 2,
+ name: "test.bar2.com",
+ values: [
+ { key: "alpn", value: ["h2", "h3-26"] },
+ { key: "echconfig", value: "456..." },
+ ],
+ },
+ },
+ ],
+ });
+
+ await new TRRDNSListener("test.bar.com", {
+ type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC,
+ });
+
+ // This channel should be failed.
+ let chan = makeChan(`https://test.bar.com:${h2Port}/server-timing`);
+ await channelOpenPromise(chan, CL_EXPECT_LATE_FAILURE | CL_ALLOW_UNKNOWN_CL);
+
+ await trrServer.stop();
+});
+
+// Test when all records have no echConfig, we directly fallback to the origin
+// one.
+add_task(async function testFallbackToTheOrigin2() {
+ 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`
+ );
+
+ await trrServer.registerDoHAnswers("test.example.com", "HTTPS", {
+ answers: [
+ {
+ name: "test.example.com",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 1,
+ name: "test.example1.com",
+ values: [{ key: "alpn", value: ["h2", "h3-26"] }],
+ },
+ },
+ {
+ name: "test.example.com",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 3,
+ name: "test.example3.com",
+ values: [{ key: "alpn", value: ["h2", "h3-26"] }],
+ },
+ },
+ ],
+ });
+
+ await new TRRDNSListener("test.example.com", {
+ type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC,
+ });
+
+ let chan = makeChan(`https://test.example.com:${h2Port}/server-timing`);
+ await channelOpenPromise(chan, CL_EXPECT_LATE_FAILURE | CL_ALLOW_UNKNOWN_CL);
+
+ await trrServer.registerDoHAnswers("test.example.com", "A", {
+ answers: [
+ {
+ name: "test.example.com",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "127.0.0.1",
+ },
+ ],
+ });
+
+ chan = makeChan(`https://test.example.com:${h2Port}/server-timing`);
+ await channelOpenPromise(chan);
+
+ await trrServer.stop();
+});
+
+// Test when some records have echConfig and some not, we directly fallback to
+// the origin one.
+add_task(async function testFallbackToTheOrigin3() {
+ Services.dns.clearCache(true);
+
+ 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`
+ );
+
+ await trrServer.registerDoHAnswers("vulnerable.com", "A", {
+ answers: [
+ {
+ name: "vulnerable.com",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "127.0.0.1",
+ },
+ ],
+ });
+
+ await trrServer.registerDoHAnswers("vulnerable.com", "HTTPS", {
+ answers: [
+ {
+ name: "vulnerable.com",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 1,
+ name: "vulnerable1.com",
+ values: [
+ { key: "alpn", value: ["h2", "h3-26"] },
+ { key: "echconfig", value: "456..." },
+ ],
+ },
+ },
+ {
+ name: "vulnerable.com",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 2,
+ name: "vulnerable2.com",
+ values: [
+ { key: "alpn", value: ["h2", "h3-26"] },
+ { key: "echconfig", value: "456..." },
+ ],
+ },
+ },
+ {
+ name: "vulnerable.com",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 3,
+ name: "vulnerable3.com",
+ values: [{ key: "alpn", value: ["h2", "h3-26"] }],
+ },
+ },
+ ],
+ });
+
+ await new TRRDNSListener("vulnerable.com", {
+ type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC,
+ });
+
+ let chan = makeChan(`https://vulnerable.com:${h2Port}/server-timing`);
+ await channelOpenPromise(chan);
+
+ await trrServer.stop();
+});
+
+add_task(async function testResetExclusionList() {
+ 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`
+ );
+ Services.prefs.setBoolPref(
+ "network.dns.httpssvc.reset_exclustion_list",
+ false
+ );
+
+ await trrServer.registerDoHAnswers("test.reset.com", "HTTPS", {
+ answers: [
+ {
+ name: "test.reset.com",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 1,
+ name: "test.reset1.com",
+ values: [
+ { key: "alpn", value: ["h2", "h3-26"] },
+ { key: "port", value: h2Port },
+ { key: "echconfig", value: "456..." },
+ ],
+ },
+ },
+ {
+ name: "test.reset.com",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 2,
+ name: "test.reset2.com",
+ values: [
+ { key: "alpn", value: ["h2", "h3-26"] },
+ { key: "echconfig", value: "456..." },
+ ],
+ },
+ },
+ ],
+ });
+
+ await new TRRDNSListener("test.reset.com", {
+ type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC,
+ });
+
+ // After this request, test.reset1.com and test.reset2.com should be both in
+ // the exclusion list.
+ let chan = makeChan(`https://test.reset.com:${h2Port}/server-timing`);
+ await channelOpenPromise(chan, CL_EXPECT_LATE_FAILURE | CL_ALLOW_UNKNOWN_CL);
+
+ // This request should be also failed, because all records are excluded.
+ chan = makeChan(`https://test.reset.com:${h2Port}/server-timing`);
+ await channelOpenPromise(chan, CL_EXPECT_LATE_FAILURE | CL_ALLOW_UNKNOWN_CL);
+
+ await trrServer.registerDoHAnswers("test.reset1.com", "A", {
+ answers: [
+ {
+ name: "test.reset1.com",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "127.0.0.1",
+ },
+ ],
+ });
+
+ Services.prefs.setBoolPref(
+ "network.dns.httpssvc.reset_exclustion_list",
+ true
+ );
+
+ // After enable network.dns.httpssvc.reset_exclustion_list and register
+ // A record for test.reset1.com, this request should be succeeded.
+ chan = makeChan(`https://test.reset.com:${h2Port}/server-timing`);
+ await channelOpenPromise(chan);
+
+ await trrServer.stop();
+});
+
+// Simply test if we can connect to H3 server.
+add_task(async function testH3Connection() {
+ 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`
+ );
+ Services.prefs.setBoolPref("network.http.http3.enable", true);
+
+ Services.prefs.setIntPref(
+ "network.dns.httpssvc.http3_fast_fallback_timeout",
+ 100
+ );
+
+ await trrServer.registerDoHAnswers("test.h3.com", "HTTPS", {
+ answers: [
+ {
+ name: "test.h3.com",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 1,
+ name: "www.h3.com",
+ values: [
+ { key: "alpn", value: "h3-29" },
+ { key: "port", value: h3Port },
+ { key: "echconfig", value: "456..." },
+ ],
+ },
+ },
+ ],
+ });
+
+ await trrServer.registerDoHAnswers("www.h3.com", "A", {
+ answers: [
+ {
+ name: "www.h3.com",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "127.0.0.1",
+ },
+ ],
+ });
+
+ await new TRRDNSListener("test.h3.com", {
+ type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC,
+ });
+
+ let chan = makeChan(`https://test.h3.com`);
+ let [req] = await channelOpenPromise(chan);
+ Assert.equal(req.protocolVersion, "h3-29");
+ let internal = req.QueryInterface(Ci.nsIHttpChannelInternal);
+ Assert.equal(internal.remotePort, h3Port);
+
+ await trrServer.stop();
+});
+
+add_task(async function testFastfallbackToH2() {
+ 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`
+ );
+ Services.prefs.setBoolPref("network.http.http3.enable", true);
+ // Use a short timeout to make sure the fast fallback timer will be triggered.
+ Services.prefs.setIntPref(
+ "network.dns.httpssvc.http3_fast_fallback_timeout",
+ 1
+ );
+ Services.prefs.setCharPref(
+ "network.dns.localDomains",
+ "test.fastfallback1.com"
+ );
+
+ await trrServer.registerDoHAnswers("test.fastfallback.com", "HTTPS", {
+ answers: [
+ {
+ name: "test.fastfallback.com",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 1,
+ name: "test.fastfallback1.com",
+ values: [
+ { key: "alpn", value: "h3-29" },
+ { key: "port", value: h3NoResponsePort },
+ { 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.fastfallback2.com", "A", {
+ answers: [
+ {
+ name: "test.fastfallback2.com",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "127.0.0.1",
+ },
+ ],
+ });
+
+ await new TRRDNSListener("test.fastfallback.com", {
+ type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC,
+ });
+
+ 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);
+
+ // Use a longer timeout to test the case that the timer is canceled.
+ Services.prefs.setIntPref(
+ "network.dns.httpssvc.http3_fast_fallback_timeout",
+ 5000
+ );
+
+ chan = makeChan(`https://test.fastfallback.com/server-timing`);
+ [req] = await channelOpenPromise(chan);
+ Assert.equal(req.protocolVersion, "h2");
+ internal = req.QueryInterface(Ci.nsIHttpChannelInternal);
+ Assert.equal(internal.remotePort, h2Port);
+
+ await trrServer.stop();
+});
+
+// Test when we fail to establish H3 connection.
+add_task(async function testFailedH3Connection() {
+ trrServer = new TRRServer();
+ await trrServer.start();
+ Services.dns.clearCache(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.enable", true);
+ Services.prefs.setIntPref(
+ "network.dns.httpssvc.http3_fast_fallback_timeout",
+ 0
+ );
+
+ await trrServer.registerDoHAnswers("test.h3.org", "HTTPS", {
+ answers: [
+ {
+ name: "test.h3.org",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 1,
+ name: "www.h3.org",
+ values: [
+ { key: "alpn", value: "h3-29" },
+ { key: "port", value: h3Port },
+ { key: "echconfig", value: "456..." },
+ ],
+ },
+ },
+ ],
+ });
+
+ await new TRRDNSListener("test.h3.org", {
+ type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC,
+ });
+
+ let chan = makeChan(`https://test.h3.org`);
+ await channelOpenPromise(chan, CL_EXPECT_LATE_FAILURE | CL_ALLOW_UNKNOWN_CL);
+
+ await trrServer.stop();
+});
+
+// Test we don't use the service mode record whose domain is in
+// http3 excluded list.
+add_task(async function testHttp3ExcludedList() {
+ trrServer = new TRRServer();
+ await trrServer.start();
+ Services.dns.clearCache(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.enable", true);
+ Services.prefs.setIntPref(
+ "network.dns.httpssvc.http3_fast_fallback_timeout",
+ 0
+ );
+
+ Services.prefs.setCharPref(
+ "network.http.http3.alt-svc-mapping-for-testing",
+ "www.h3_fail.org;h3-29=:" + h3Port
+ );
+
+ // This will fail because there is no address record for www.h3_fail.org.
+ let chan = makeChan(`https://www.h3_fail.org`);
+ await channelOpenPromise(chan, CL_EXPECT_LATE_FAILURE | CL_ALLOW_UNKNOWN_CL);
+
+ // Now www.h3_fail.org should be already excluded, so the second record
+ // foo.example.com will be selected.
+ await trrServer.registerDoHAnswers("test.h3_excluded.org", "HTTPS", {
+ answers: [
+ {
+ name: "test.h3_excluded.org",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 1,
+ name: "www.h3_fail.org",
+ values: [
+ { key: "alpn", value: "h3-29" },
+ { key: "port", value: h3Port },
+ ],
+ },
+ },
+ {
+ name: "test.h3_excluded.org",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 2,
+ name: "foo.example.com",
+ values: [
+ { key: "alpn", value: "h3-29" },
+ { key: "port", value: h3Port },
+ ],
+ },
+ },
+ ],
+ });
+
+ await new TRRDNSListener("test.h3_excluded.org", {
+ type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC,
+ });
+
+ chan = makeChan(`https://test.h3_excluded.org`);
+ let [req] = await channelOpenPromise(chan);
+ Assert.equal(req.protocolVersion, "h3-29");
+ let internal = req.QueryInterface(Ci.nsIHttpChannelInternal);
+ Assert.equal(internal.remotePort, h3Port);
+
+ await trrServer.stop();
+});
+
+add_task(async function testAllRecordsInHttp3ExcludedList() {
+ trrServer = new TRRServer();
+ await trrServer.start();
+ Services.dns.clearCache(true);
+ Services.prefs.setIntPref("network.trr.mode", 3);
+ Services.prefs.setBoolPref("network.dns.http3_echconfig.enabled", true);
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${trrServer.port()}/dns-query`
+ );
+ Services.prefs.setBoolPref("network.http.http3.enable", true);
+ Services.prefs.setIntPref(
+ "network.dns.httpssvc.http3_fast_fallback_timeout",
+ 0
+ );
+
+ Services.prefs.setCharPref(
+ "network.http.http3.alt-svc-mapping-for-testing",
+ "www.h3_fail1.org;h3-29=:" + h3Port
+ );
+
+ await trrServer.registerDoHAnswers("www.h3_all_excluded.org", "A", {
+ answers: [
+ {
+ name: "www.h3_all_excluded.org",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "127.0.0.1",
+ },
+ ],
+ });
+
+ // Test we can connect to www.h3_all_excluded.org sucessfully.
+ let chan = makeChan(
+ `https://www.h3_all_excluded.org:${h2Port}/server-timing`
+ );
+
+ let [req] = await channelOpenPromise(chan);
+
+ // Test if this request is done by h2.
+ Assert.equal(req.getResponseHeader("x-connection-http2"), "yes");
+
+ // This will fail because there is no address record for www.h3_fail1.org.
+ chan = makeChan(`https://www.h3_fail1.org`);
+ await channelOpenPromise(chan, CL_EXPECT_LATE_FAILURE | CL_ALLOW_UNKNOWN_CL);
+
+ Services.prefs.setCharPref(
+ "network.http.http3.alt-svc-mapping-for-testing",
+ "www.h3_fail2.org;h3-29=:" + h3Port
+ );
+
+ // This will fail because there is no address record for www.h3_fail2.org.
+ chan = makeChan(`https://www.h3_fail2.org`);
+ await channelOpenPromise(chan, CL_EXPECT_LATE_FAILURE | CL_ALLOW_UNKNOWN_CL);
+
+ await trrServer.registerDoHAnswers("www.h3_all_excluded.org", "HTTPS", {
+ answers: [
+ {
+ name: "www.h3_all_excluded.org",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 1,
+ name: "www.h3_fail1.org",
+ values: [
+ { key: "alpn", value: "h3-29" },
+ { key: "port", value: h3Port },
+ { key: "echconfig", value: "456..." },
+ ],
+ },
+ },
+ {
+ name: "www.h3_all_excluded.org",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 2,
+ name: "www.h3_fail2.org",
+ values: [
+ { key: "alpn", value: "h3-29" },
+ { key: "port", value: h3Port },
+ { key: "echconfig", value: "456..." },
+ ],
+ },
+ },
+ ],
+ });
+
+ await new TRRDNSListener("www.h3_all_excluded.org", {
+ type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC,
+ });
+
+ Services.dns.clearCache(true);
+ Services.prefs.setIntPref("network.http.speculative-parallel-limit", 0);
+ Services.obs.notifyObservers(null, "net:prune-all-connections");
+
+ // All HTTPS RRs are in http3 excluded list and all records are failed to
+ // connect, so don't fallback to the origin one.
+ chan = makeChan(`https://www.h3_all_excluded.org:${h2Port}/server-timing`);
+ await channelOpenPromise(chan, CL_EXPECT_LATE_FAILURE | CL_ALLOW_UNKNOWN_CL);
+
+ await trrServer.registerDoHAnswers("www.h3_fail1.org", "A", {
+ answers: [
+ {
+ name: "www.h3_fail1.org",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "127.0.0.1",
+ },
+ ],
+ });
+
+ // The the case that when all records are in http3 excluded list, we still
+ // give the first record one more shot.
+ chan = makeChan(`https://www.h3_all_excluded.org`);
+ [req] = await channelOpenPromise(chan);
+ Assert.equal(req.protocolVersion, "h3-29");
+ let internal = req.QueryInterface(Ci.nsIHttpChannelInternal);
+ Assert.equal(internal.remotePort, h3Port);
+
+ await trrServer.stop();
+});
+
+WebSocketListener.prototype = {
+ onAcknowledge(aContext, aSize) {},
+ onBinaryMessageAvailable(aContext, aMsg) {},
+ onMessageAvailable(aContext, aMsg) {},
+ onServerClose(aContext, aCode, aReason) {},
+ onStart(aContext) {
+ this.finish();
+ },
+ onStop(aContext, aStatusCode) {},
+};
+
+add_task(async function testUpgradeNotUsingHTTPSRR() {
+ 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`
+ );
+
+ await trrServer.registerDoHAnswers("test.ws.com", "HTTPS", {
+ answers: [
+ {
+ name: "test.ws.com",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 1,
+ name: "test.ws1.com",
+ values: [{ key: "port", value: ["8888"] }],
+ },
+ },
+ ],
+ });
+
+ await new TRRDNSListener("test.ws.com", {
+ type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC,
+ });
+
+ await trrServer.registerDoHAnswers("test.ws.com", "A", {
+ answers: [
+ {
+ name: "test.ws.com",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "127.0.0.1",
+ },
+ ],
+ });
+
+ let wssUri = "wss://test.ws.com:" + h2Port + "/websocket";
+ let chan = Cc["@mozilla.org/network/protocol;1?name=wss"].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_DOCUMENT
+ );
+
+ var uri = Services.io.newURI(wssUri);
+ var wsListener = new WebSocketListener();
+ certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData(
+ false
+ );
+ await new Promise(resolve => {
+ wsListener.finish = resolve;
+ chan.asyncOpen(uri, wssUri, {}, 0, wsListener, null);
+ certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData(
+ true
+ );
+ });
+
+ await trrServer.stop();
+});
+
+// Test if we fallback to h2 with echConfig.
+add_task(async function testFallbackToH2WithEchConfig() {
+ trrServer = new TRRServer();
+ await trrServer.start();
+ Services.dns.clearCache(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.enable", true);
+ Services.prefs.setIntPref(
+ "network.dns.httpssvc.http3_fast_fallback_timeout",
+ 0
+ );
+
+ await trrServer.registerDoHAnswers("test.fallback.org", "HTTPS", {
+ answers: [
+ {
+ name: "test.fallback.org",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 1,
+ name: "test.fallback.org",
+ values: [
+ { key: "alpn", value: ["h2", "h3-29"] },
+ { key: "port", value: h2Port },
+ { key: "echconfig", value: "456..." },
+ ],
+ },
+ },
+ ],
+ });
+
+ await trrServer.registerDoHAnswers("test.fallback.org", "A", {
+ answers: [
+ {
+ name: "test.fallback.org",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "127.0.0.1",
+ },
+ ],
+ });
+
+ await new TRRDNSListener("test.fallback.org", {
+ type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC,
+ });
+
+ await new TRRDNSListener("test.fallback.org", "127.0.0.1");
+
+ let chan = makeChan(`https://test.fallback.org/server-timing`);
+ let [req] = await channelOpenPromise(chan);
+ // Test if this request is done by h2.
+ Assert.equal(req.getResponseHeader("x-connection-http2"), "yes");
+
+ await trrServer.stop();
+});
diff --git a/netwerk/test/unit/test_trr_httpssvc.js b/netwerk/test/unit/test_trr_httpssvc.js
new file mode 100644
index 0000000000..6c1db447a8
--- /dev/null
+++ b/netwerk/test/unit/test_trr_httpssvc.js
@@ -0,0 +1,728 @@
+/* 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 { TestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TestUtils.sys.mjs"
+);
+
+let h2Port;
+let trrServer;
+
+function inChildProcess() {
+ return Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT;
+}
+
+add_setup(async function setup() {
+ if (inChildProcess()) {
+ return;
+ }
+
+ trr_test_setup();
+ h2Port = Services.env.get("MOZHTTP2_PORT");
+ Assert.notEqual(h2Port, null);
+ Assert.notEqual(h2Port, "");
+
+ registerCleanupFunction(async () => {
+ trr_clear_prefs();
+ Services.prefs.clearUserPref("network.dns.port_prefixed_qname_https_rr");
+ await trrServer.stop();
+ });
+
+ if (mozinfo.socketprocess_networking) {
+ Services.dns; // Needed to trigger socket process.
+ await TestUtils.waitForCondition(() => Services.io.socketProcessLaunched);
+ }
+
+ Services.prefs.setIntPref("network.trr.mode", 3);
+});
+
+add_task(async function testHTTPSSVC() {
+ // use the h2 server as DOH provider
+ if (!inChildProcess()) {
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ "https://foo.example.com:" + h2Port + "/httpssvc"
+ );
+ }
+
+ let { inRecord } = await new TRRDNSListener("test.httpssvc.com", {
+ type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC,
+ });
+ let answer = inRecord.QueryInterface(Ci.nsIDNSHTTPSSVCRecord).records;
+ Assert.equal(answer[0].priority, 1);
+ Assert.equal(answer[0].name, "h3pool");
+ Assert.equal(answer[0].values.length, 7);
+ Assert.deepEqual(
+ answer[0].values[0].QueryInterface(Ci.nsISVCParamAlpn).alpn,
+ ["h2", "h3"],
+ "got correct answer"
+ );
+ Assert.ok(
+ answer[0].values[1].QueryInterface(Ci.nsISVCParamNoDefaultAlpn),
+ "got correct answer"
+ );
+ Assert.equal(
+ answer[0].values[2].QueryInterface(Ci.nsISVCParamPort).port,
+ 8888,
+ "got correct answer"
+ );
+ Assert.equal(
+ answer[0].values[3].QueryInterface(Ci.nsISVCParamIPv4Hint).ipv4Hint[0]
+ .address,
+ "1.2.3.4",
+ "got correct answer"
+ );
+ Assert.equal(
+ answer[0].values[4].QueryInterface(Ci.nsISVCParamEchConfig).echconfig,
+ "123...",
+ "got correct answer"
+ );
+ Assert.equal(
+ answer[0].values[5].QueryInterface(Ci.nsISVCParamIPv6Hint).ipv6Hint[0]
+ .address,
+ "::1",
+ "got correct answer"
+ );
+ Assert.equal(
+ answer[0].values[6].QueryInterface(Ci.nsISVCParamODoHConfig).ODoHConfig,
+ "456...",
+ "got correct answer"
+ );
+ Assert.equal(answer[1].priority, 2);
+ Assert.equal(answer[1].name, "test.httpssvc.com");
+ Assert.equal(answer[1].values.length, 5);
+ Assert.deepEqual(
+ answer[1].values[0].QueryInterface(Ci.nsISVCParamAlpn).alpn,
+ ["h2"],
+ "got correct answer"
+ );
+ Assert.equal(
+ answer[1].values[1].QueryInterface(Ci.nsISVCParamIPv4Hint).ipv4Hint[0]
+ .address,
+ "1.2.3.4",
+ "got correct answer"
+ );
+ Assert.equal(
+ answer[1].values[1].QueryInterface(Ci.nsISVCParamIPv4Hint).ipv4Hint[1]
+ .address,
+ "5.6.7.8",
+ "got correct answer"
+ );
+ Assert.equal(
+ answer[1].values[2].QueryInterface(Ci.nsISVCParamEchConfig).echconfig,
+ "abc...",
+ "got correct answer"
+ );
+ Assert.equal(
+ answer[1].values[3].QueryInterface(Ci.nsISVCParamIPv6Hint).ipv6Hint[0]
+ .address,
+ "::1",
+ "got correct answer"
+ );
+ Assert.equal(
+ answer[1].values[3].QueryInterface(Ci.nsISVCParamIPv6Hint).ipv6Hint[1]
+ .address,
+ "fe80::794f:6d2c:3d5e:7836",
+ "got correct answer"
+ );
+ Assert.equal(
+ answer[1].values[4].QueryInterface(Ci.nsISVCParamODoHConfig).ODoHConfig,
+ "def...",
+ "got correct answer"
+ );
+ Assert.equal(answer[2].priority, 3);
+ Assert.equal(answer[2].name, "hello");
+ Assert.equal(answer[2].values.length, 0);
+});
+
+add_task(async function test_aliasform() {
+ trrServer = new TRRServer();
+ await trrServer.start();
+ dump(`port = ${trrServer.port()}\n`);
+
+ if (inChildProcess()) {
+ do_send_remote_message("mode3-port", trrServer.port());
+ await do_await_remote_message("mode3-port-done");
+ } else {
+ Services.prefs.setIntPref("network.trr.mode", 3);
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${trrServer.port()}/dns-query`
+ );
+ }
+
+ // Make sure that HTTPS AliasForm is only treated as a CNAME for HTTPS requests
+ await trrServer.registerDoHAnswers("test1.com", "A", {
+ answers: [
+ {
+ name: "test1.com",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 0,
+ name: "something1.com",
+ values: [],
+ },
+ },
+ ],
+ });
+ await trrServer.registerDoHAnswers("something1.com", "A", {
+ answers: [
+ {
+ name: "something1.com",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "1.2.3.4",
+ },
+ ],
+ });
+
+ {
+ let { inStatus } = await new TRRDNSListener("test1.com", {
+ expectedSuccess: false,
+ });
+ Assert.ok(
+ !Components.isSuccessCode(inStatus),
+ `${inStatus} should be an error code`
+ );
+ }
+
+ // Test that HTTPS priority = 0 (AliasForm) behaves like a CNAME
+ await trrServer.registerDoHAnswers("test.com", "HTTPS", {
+ answers: [
+ {
+ name: "test.com",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 0,
+ name: "something.com",
+ values: [],
+ },
+ },
+ ],
+ });
+ await trrServer.registerDoHAnswers("something.com", "HTTPS", {
+ answers: [
+ {
+ name: "something.com",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 1,
+ name: "h3pool",
+ values: [{ key: "alpn", value: ["h2", "h3"] }],
+ },
+ },
+ ],
+ });
+
+ {
+ let { inStatus, inRecord } = await new TRRDNSListener("test.com", {
+ type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC,
+ expectedSuccess: false,
+ });
+ Assert.ok(Components.isSuccessCode(inStatus), `${inStatus} should succeed`);
+ let answer = inRecord.QueryInterface(Ci.nsIDNSHTTPSSVCRecord).records;
+ Assert.equal(answer[0].priority, 1);
+ Assert.equal(answer[0].name, "h3pool");
+ }
+
+ // Test a chain of HTTPSSVC AliasForm and CNAMEs
+ await trrServer.registerDoHAnswers("x.com", "HTTPS", {
+ answers: [
+ {
+ name: "x.com",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 0,
+ name: "y.com",
+ values: [],
+ },
+ },
+ ],
+ });
+ await trrServer.registerDoHAnswers("y.com", "HTTPS", {
+ answers: [
+ {
+ name: "y.com",
+ type: "CNAME",
+ ttl: 55,
+ class: "IN",
+ flush: false,
+ data: "z.com",
+ },
+ ],
+ });
+ await trrServer.registerDoHAnswers("z.com", "HTTPS", {
+ answers: [
+ {
+ name: "z.com",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 0,
+ name: "target.com",
+ values: [],
+ },
+ },
+ ],
+ });
+ await trrServer.registerDoHAnswers("target.com", "HTTPS", {
+ answers: [
+ {
+ name: "target.com",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 1,
+ name: "h3pool",
+ values: [{ key: "alpn", value: ["h2", "h3"] }],
+ },
+ },
+ ],
+ });
+
+ let { inStatus, inRecord } = await new TRRDNSListener("x.com", {
+ type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC,
+ expectedSuccess: false,
+ });
+ Assert.ok(Components.isSuccessCode(inStatus), `${inStatus} should succeed`);
+ let answer = inRecord.QueryInterface(Ci.nsIDNSHTTPSSVCRecord).records;
+ Assert.equal(answer[0].priority, 1);
+ Assert.equal(answer[0].name, "h3pool");
+
+ // We get a ServiceForm instead of a A answer, CNAME or AliasForm
+ await trrServer.registerDoHAnswers("no-ip-host.com", "A", {
+ answers: [
+ {
+ name: "no-ip-host.com",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 1,
+ name: "h3pool",
+ values: [
+ { key: "alpn", value: ["h2", "h3"] },
+ { key: "no-default-alpn" },
+ { key: "port", value: 8888 },
+ { key: "ipv4hint", value: "1.2.3.4" },
+ { key: "echconfig", value: "123..." },
+ { key: "ipv6hint", value: "::1" },
+ ],
+ },
+ },
+ ],
+ });
+
+ ({ inStatus } = await new TRRDNSListener("no-ip-host.com", {
+ expectedSuccess: false,
+ }));
+ Assert.ok(
+ !Components.isSuccessCode(inStatus),
+ `${inStatus} should be an error code`
+ );
+
+ // Test CNAME/AliasForm loop
+ await trrServer.registerDoHAnswers("loop.com", "HTTPS", {
+ answers: [
+ {
+ name: "loop.com",
+ type: "CNAME",
+ ttl: 55,
+ class: "IN",
+ flush: false,
+ data: "loop2.com",
+ },
+ ],
+ });
+ await trrServer.registerDoHAnswers("loop2.com", "HTTPS", {
+ answers: [
+ {
+ name: "loop2.com",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 0,
+ name: "loop.com",
+ values: [],
+ },
+ },
+ ],
+ });
+
+ // Make sure these are the first requests
+ Assert.equal(await trrServer.requestCount("loop.com", "HTTPS"), 0);
+ Assert.equal(await trrServer.requestCount("loop2.com", "HTTPS"), 0);
+
+ ({ inStatus } = await new TRRDNSListener("loop.com", {
+ type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC,
+ expectedSuccess: false,
+ }));
+ Assert.ok(
+ !Components.isSuccessCode(inStatus),
+ `${inStatus} should be an error code`
+ );
+ // Make sure the error was actually triggered by a loop.
+ Assert.greater(await trrServer.requestCount("loop.com", "HTTPS"), 2);
+ Assert.greater(await trrServer.requestCount("loop2.com", "HTTPS"), 2);
+
+ // Alias form for .
+ await trrServer.registerDoHAnswers("empty.com", "A", {
+ answers: [
+ {
+ name: "empty.com",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 0,
+ name: "", // This is not allowed
+ values: [],
+ },
+ },
+ ],
+ });
+
+ ({ inStatus } = await new TRRDNSListener("empty.com", {
+ expectedSuccess: false,
+ }));
+ Assert.ok(
+ !Components.isSuccessCode(inStatus),
+ `${inStatus} should be an error code`
+ );
+
+ // We should ignore ServiceForm if an AliasForm record is also present
+ await trrServer.registerDoHAnswers("multi.com", "HTTPS", {
+ answers: [
+ {
+ name: "multi.com",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 1,
+ name: "h3pool",
+ values: [
+ { key: "alpn", value: ["h2", "h3"] },
+ { key: "no-default-alpn" },
+ { key: "port", value: 8888 },
+ { key: "ipv4hint", value: "1.2.3.4" },
+ { key: "echconfig", value: "123..." },
+ { key: "ipv6hint", value: "::1" },
+ ],
+ },
+ },
+ {
+ name: "multi.com",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 0,
+ name: "example.com",
+ values: [],
+ },
+ },
+ ],
+ });
+
+ let { inStatus: inStatus2 } = await new TRRDNSListener("multi.com", {
+ type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC,
+ expectedSuccess: false,
+ });
+ Assert.ok(
+ !Components.isSuccessCode(inStatus2),
+ `${inStatus2} should be an error code`
+ );
+
+ // the svcparam keys are in reverse order
+ await trrServer.registerDoHAnswers("order.com", "HTTPS", {
+ answers: [
+ {
+ name: "order.com",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 1,
+ name: "h3pool",
+ values: [
+ { key: "ipv6hint", value: "::1" },
+ { key: "echconfig", value: "123..." },
+ { key: "ipv4hint", value: "1.2.3.4" },
+ { key: "port", value: 8888 },
+ { key: "no-default-alpn" },
+ { key: "alpn", value: ["h2", "h3"] },
+ ],
+ },
+ },
+ ],
+ });
+
+ ({ inStatus: inStatus2 } = await new TRRDNSListener("order.com", {
+ type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC,
+ expectedSuccess: false,
+ }));
+ Assert.ok(
+ !Components.isSuccessCode(inStatus2),
+ `${inStatus2} should be an error code`
+ );
+
+ // duplicate svcparam keys
+ await trrServer.registerDoHAnswers("duplicate.com", "HTTPS", {
+ answers: [
+ {
+ name: "duplicate.com",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 1,
+ name: "h3pool",
+ values: [
+ { key: "alpn", value: ["h2", "h3"] },
+ { key: "alpn", value: ["h2", "h3", "h4"] },
+ ],
+ },
+ },
+ ],
+ });
+
+ ({ inStatus: inStatus2 } = await new TRRDNSListener("duplicate.com", {
+ type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC,
+ expectedSuccess: false,
+ }));
+ Assert.ok(
+ !Components.isSuccessCode(inStatus2),
+ `${inStatus2} should be an error code`
+ );
+
+ // mandatory svcparam
+ await trrServer.registerDoHAnswers("mandatory.com", "HTTPS", {
+ answers: [
+ {
+ name: "mandatory.com",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 1,
+ name: "h3pool",
+ values: [
+ { key: "mandatory", value: ["key100"] },
+ { key: "alpn", value: ["h2", "h3"] },
+ { key: "key100" },
+ ],
+ },
+ },
+ ],
+ });
+
+ ({ inStatus: inStatus2 } = await new TRRDNSListener("mandatory.com", {
+ type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC,
+ expectedSuccess: false,
+ }));
+ Assert.ok(!Components.isSuccessCode(inStatus2), `${inStatus2} should fail`);
+
+ // mandatory svcparam
+ await trrServer.registerDoHAnswers("mandatory2.com", "HTTPS", {
+ answers: [
+ {
+ name: "mandatory2.com",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 1,
+ name: "h3pool",
+ values: [
+ {
+ key: "mandatory",
+ value: [
+ "alpn",
+ "no-default-alpn",
+ "port",
+ "ipv4hint",
+ "echconfig",
+ "ipv6hint",
+ ],
+ },
+ { key: "alpn", value: ["h2", "h3"] },
+ { key: "no-default-alpn" },
+ { key: "port", value: 8888 },
+ { key: "ipv4hint", value: "1.2.3.4" },
+ { key: "echconfig", value: "123..." },
+ { key: "ipv6hint", value: "::1" },
+ ],
+ },
+ },
+ ],
+ });
+
+ ({ inStatus: inStatus2 } = await new TRRDNSListener("mandatory2.com", {
+ type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC,
+ }));
+
+ Assert.ok(Components.isSuccessCode(inStatus2), `${inStatus2} should succeed`);
+
+ // alias-mode with . targetName
+ await trrServer.registerDoHAnswers("no-alias.com", "HTTPS", {
+ answers: [
+ {
+ name: "no-alias.com",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 0,
+ name: ".",
+ values: [],
+ },
+ },
+ ],
+ });
+
+ ({ inStatus: inStatus2 } = await new TRRDNSListener("no-alias.com", {
+ type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC,
+ expectedSuccess: false,
+ }));
+
+ Assert.ok(!Components.isSuccessCode(inStatus2), `${inStatus2} should fail`);
+
+ // service-mode with . targetName
+ await trrServer.registerDoHAnswers("service.com", "HTTPS", {
+ answers: [
+ {
+ name: "service.com",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 1,
+ name: ".",
+ values: [{ key: "alpn", value: ["h2", "h3"] }],
+ },
+ },
+ ],
+ });
+
+ ({ inRecord, inStatus: inStatus2 } = await new TRRDNSListener("service.com", {
+ type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC,
+ }));
+ Assert.ok(Components.isSuccessCode(inStatus2), `${inStatus2} should work`);
+ answer = inRecord.QueryInterface(Ci.nsIDNSHTTPSSVCRecord).records;
+ Assert.equal(answer[0].priority, 1);
+ Assert.equal(answer[0].name, "service.com");
+});
+
+add_task(async function testNegativeResponse() {
+ let { inStatus } = await new TRRDNSListener("negative_test.com", {
+ type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC,
+ expectedSuccess: false,
+ });
+ Assert.ok(
+ !Components.isSuccessCode(inStatus),
+ `${inStatus} should be an error code`
+ );
+
+ await trrServer.registerDoHAnswers("negative_test.com", "HTTPS", {
+ answers: [
+ {
+ name: "negative_test.com",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 1,
+ name: "negative_test.com",
+ values: [{ key: "alpn", value: ["h2", "h3"] }],
+ },
+ },
+ ],
+ });
+
+ // Should still be failed because a negative response is from DNS cache.
+ ({ inStatus } = await new TRRDNSListener("negative_test.com", {
+ type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC,
+ expectedSuccess: false,
+ }));
+ Assert.ok(
+ !Components.isSuccessCode(inStatus),
+ `${inStatus} should be an error code`
+ );
+
+ if (inChildProcess()) {
+ do_send_remote_message("clearCache");
+ await do_await_remote_message("clearCache-done");
+ } else {
+ Services.dns.clearCache(true);
+ }
+
+ let inRecord;
+ ({ inRecord, inStatus } = await new TRRDNSListener("negative_test.com", {
+ type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC,
+ }));
+ Assert.ok(Components.isSuccessCode(inStatus), `${inStatus} should work`);
+ let answer = inRecord.QueryInterface(Ci.nsIDNSHTTPSSVCRecord).records;
+ Assert.equal(answer[0].priority, 1);
+ Assert.equal(answer[0].name, "negative_test.com");
+});
+
+add_task(async function testPortPrefixedName() {
+ if (inChildProcess()) {
+ do_send_remote_message("set-port-prefixed-pref");
+ await do_await_remote_message("set-port-prefixed-pref-done");
+ } else {
+ Services.prefs.setBoolPref(
+ "network.dns.port_prefixed_qname_https_rr",
+ true
+ );
+ }
+
+ await trrServer.registerDoHAnswers(
+ "_4433._https.port_prefix.test.com",
+ "HTTPS",
+ {
+ answers: [
+ {
+ name: "_4433._https.port_prefix.test.com",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 1,
+ name: "port_prefix.test1.com",
+ values: [{ key: "alpn", value: ["h2", "h3"] }],
+ },
+ },
+ ],
+ }
+ );
+
+ let { inRecord, inStatus } = await new TRRDNSListener(
+ "port_prefix.test.com",
+ {
+ type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC,
+ port: 4433,
+ }
+ );
+ Assert.ok(Components.isSuccessCode(inStatus), `${inStatus} should work`);
+ let answer = inRecord.QueryInterface(Ci.nsIDNSHTTPSSVCRecord).records;
+ Assert.equal(answer[0].priority, 1);
+ Assert.equal(answer[0].name, "port_prefix.test1.com");
+ await trrServer.stop();
+});
diff --git a/netwerk/test/unit/test_trr_nat64.js b/netwerk/test/unit/test_trr_nat64.js
new file mode 100644
index 0000000000..aa41f2a611
--- /dev/null
+++ b/netwerk/test/unit/test_trr_nat64.js
@@ -0,0 +1,120 @@
+/* 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 override = Cc["@mozilla.org/network/native-dns-override;1"].getService(
+ Ci.nsINativeDNSResolverOverride
+);
+
+trr_test_setup();
+registerCleanupFunction(async () => {
+ Services.prefs.clearUserPref("network.connectivity-service.nat64-prefix");
+ override.clearOverrides();
+ trr_clear_prefs();
+});
+
+/**
+ * Waits for an observer notification to fire.
+ *
+ * @param {String} topic The notification topic.
+ * @returns {Promise} A promise that fulfills when the notification is fired.
+ */
+function promiseObserverNotification(topic, matchFunc) {
+ return new Promise((resolve, reject) => {
+ Services.obs.addObserver(function observe(subject, topic1, data) {
+ let matches = typeof matchFunc != "function" || matchFunc(subject, data);
+ if (!matches) {
+ return;
+ }
+ Services.obs.removeObserver(observe, topic);
+ resolve({ subject, data });
+ }, topic);
+ });
+}
+
+function makeChan(url) {
+ let chan = NetUtil.newChannel({
+ uri: url,
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+ return chan;
+}
+
+function channelOpenPromise(chan) {
+ return new Promise(resolve => {
+ function finish(req, buffer) {
+ resolve([req, buffer]);
+ }
+ chan.asyncOpen(new ChannelListener(finish));
+ });
+}
+
+add_task(async function test_add_nat64_prefix_to_trr() {
+ let trrServer = new TRRServer();
+ registerCleanupFunction(async () => {
+ await trrServer.stop();
+ });
+ await trrServer.start();
+ dump(`port = ${trrServer.port()}\n`);
+ let chan = makeChan(`https://localhost:${trrServer.port()}/test?bla=some`);
+ let [, resp] = await channelOpenPromise(chan);
+ equal(resp, "<h1> 404 Path not found: /test</h1>");
+ Services.dns.clearCache(true);
+ override.addIPOverride("ipv4only.arpa", "fe80::9b2b:c000:00aa");
+ Services.prefs.setCharPref(
+ "network.connectivity-service.nat64-prefix",
+ "ae80::3b1b:c343:1133"
+ );
+
+ let topic = "network:connectivity-service:dns-checks-complete";
+ if (mozinfo.socketprocess_networking) {
+ topic += "-from-socket-process";
+ }
+ let notification = promiseObserverNotification(topic);
+ Services.obs.notifyObservers(null, "network:captive-portal-connectivity");
+ await notification;
+
+ Services.prefs.setIntPref("network.trr.mode", 2);
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${trrServer.port()}/dns-query`
+ );
+
+ await trrServer.registerDoHAnswers("xyz.foo", "A", {
+ answers: [
+ {
+ name: "xyz.foo",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "1.2.3.4",
+ },
+ ],
+ });
+ let { inRecord } = await new TRRDNSListener("xyz.foo", {
+ expectedSuccess: false,
+ });
+
+ inRecord.QueryInterface(Ci.nsIDNSAddrRecord);
+ Assert.equal(
+ inRecord.getNextAddrAsString(),
+ "1.2.3.4",
+ `Checking that native IPv4 addresses have higher priority.`
+ );
+
+ Assert.equal(
+ inRecord.getNextAddrAsString(),
+ "ae80::3b1b:102:304",
+ `Checking the manually entered NAT64-prefixed address is in the middle.`
+ );
+
+ Assert.equal(
+ inRecord.getNextAddrAsString(),
+ "fe80::9b2b:102:304",
+ `Checking that the NAT64-prefixed address is appended at the back.`
+ );
+
+ await trrServer.stop();
+});
diff --git a/netwerk/test/unit/test_trr_noPrefetch.js b/netwerk/test/unit/test_trr_noPrefetch.js
new file mode 100644
index 0000000000..ca3188a370
--- /dev/null
+++ b/netwerk/test/unit/test_trr_noPrefetch.js
@@ -0,0 +1,174 @@
+/* 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 trrServer = null;
+add_setup(async function setup() {
+ if (Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT) {
+ return;
+ }
+
+ trr_test_setup();
+ Services.prefs.setBoolPref("network.dns.disablePrefetch", true);
+ registerCleanupFunction(async () => {
+ Services.prefs.clearUserPref("network.dns.disablePrefetch");
+ trr_clear_prefs();
+ });
+
+ trrServer = new TRRServer();
+ registerCleanupFunction(async () => {
+ await trrServer.stop();
+ });
+ await trrServer.start();
+
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${trrServer.port()}/dns-query`
+ );
+ Services.prefs.setIntPref("network.trr.mode", Ci.nsIDNSService.MODE_TRRONLY);
+
+ let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(
+ Ci.nsIX509CertDB
+ );
+ addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u");
+
+ // We need to define both A and AAAA responses, otherwise
+ // we might race and pick up the skip reason for the other request.
+ await trrServer.registerDoHAnswers(`myfoo.test`, "A", {
+ answers: [],
+ });
+ await trrServer.registerDoHAnswers(`myfoo.test`, "AAAA", {
+ answers: [],
+ });
+
+ // myfoo2.test will return sever error as it's not defined
+
+ // return nxdomain for this one
+ await trrServer.registerDoHAnswers(`myfoo3.test`, "A", {
+ flags: 0x03,
+ answers: [],
+ });
+ await trrServer.registerDoHAnswers(`myfoo3.test`, "AAAA", {
+ flags: 0x03,
+ answers: [],
+ });
+
+ await trrServer.registerDoHAnswers(`alt1.example.com`, "A", {
+ answers: [
+ {
+ name: "alt1.example.com",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "127.0.0.1",
+ },
+ ],
+ });
+});
+
+add_task(async function test_failure() {
+ let req = await new Promise(resolve => {
+ let chan = NetUtil.newChannel({
+ uri: `http://myfoo.test/`,
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+ chan.asyncOpen(new ChannelListener(resolve, null, CL_EXPECT_FAILURE));
+ });
+
+ equal(req.status, Cr.NS_ERROR_UNKNOWN_HOST);
+ equal(
+ req.QueryInterface(Ci.nsIHttpChannelInternal).effectiveTRRMode,
+ Ci.nsIRequest.TRR_ONLY_MODE
+ );
+ equal(
+ req.QueryInterface(Ci.nsIHttpChannelInternal).trrSkipReason,
+ Ci.nsITRRSkipReason.TRR_NO_ANSWERS
+ );
+
+ req = await new Promise(resolve => {
+ let chan = NetUtil.newChannel({
+ uri: `http://myfoo2.test/`,
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+ chan.asyncOpen(new ChannelListener(resolve, null, CL_EXPECT_FAILURE));
+ });
+
+ equal(req.status, Cr.NS_ERROR_UNKNOWN_HOST);
+ equal(
+ req.QueryInterface(Ci.nsIHttpChannelInternal).effectiveTRRMode,
+ Ci.nsIRequest.TRR_ONLY_MODE
+ );
+ equal(
+ req.QueryInterface(Ci.nsIHttpChannelInternal).trrSkipReason,
+ Ci.nsITRRSkipReason.TRR_RCODE_FAIL
+ );
+
+ req = await new Promise(resolve => {
+ let chan = NetUtil.newChannel({
+ uri: `http://myfoo3.test/`,
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+ chan.asyncOpen(new ChannelListener(resolve, null, CL_EXPECT_FAILURE));
+ });
+
+ equal(req.status, Cr.NS_ERROR_UNKNOWN_HOST);
+ equal(
+ req.QueryInterface(Ci.nsIHttpChannelInternal).effectiveTRRMode,
+ Ci.nsIRequest.TRR_ONLY_MODE
+ );
+ equal(
+ req.QueryInterface(Ci.nsIHttpChannelInternal).trrSkipReason,
+ Ci.nsITRRSkipReason.TRR_NXDOMAIN
+ );
+});
+
+add_task(async function test_success() {
+ let httpServer = new NodeHTTP2Server();
+ await httpServer.start();
+ await httpServer.registerPathHandler("/", (req, resp) => {
+ resp.writeHead(200);
+ resp.end("done");
+ });
+ registerCleanupFunction(async () => {
+ await httpServer.stop();
+ });
+
+ let req = await new Promise(resolve => {
+ let chan = NetUtil.newChannel({
+ uri: `https://alt1.example.com:${httpServer.port()}/`,
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+ chan.asyncOpen(new ChannelListener(resolve, null, CL_ALLOW_UNKNOWN_CL));
+ });
+
+ equal(req.status, Cr.NS_OK);
+ equal(
+ req.QueryInterface(Ci.nsIHttpChannelInternal).effectiveTRRMode,
+ Ci.nsIRequest.TRR_ONLY_MODE
+ );
+ equal(
+ req.QueryInterface(Ci.nsIHttpChannelInternal).trrSkipReason,
+ Ci.nsITRRSkipReason.TRR_OK
+ );
+
+ // Another request to check connection reuse
+ req = await new Promise(resolve => {
+ let chan = NetUtil.newChannel({
+ uri: `https://alt1.example.com:${httpServer.port()}/second`,
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+ chan.asyncOpen(new ChannelListener(resolve, null, CL_ALLOW_UNKNOWN_CL));
+ });
+
+ equal(req.status, Cr.NS_OK);
+ equal(
+ req.QueryInterface(Ci.nsIHttpChannelInternal).effectiveTRRMode,
+ Ci.nsIRequest.TRR_ONLY_MODE
+ );
+ equal(
+ req.QueryInterface(Ci.nsIHttpChannelInternal).trrSkipReason,
+ Ci.nsITRRSkipReason.TRR_OK
+ );
+});
diff --git a/netwerk/test/unit/test_trr_proxy.js b/netwerk/test/unit/test_trr_proxy.js
new file mode 100644
index 0000000000..962971d103
--- /dev/null
+++ b/netwerk/test/unit/test_trr_proxy.js
@@ -0,0 +1,156 @@
+// These are globlas defined for proxy servers, in ProxyAutoConfig.cpp. See
+// PACGlobalFunctions
+/* globals dnsResolve, alert */
+
+/* This test checks that using a PAC script still works when TRR is on.
+ Steps:
+ - Set the pac script
+ - Do a request to make sure that the script is loaded
+ - Set the TRR mode
+ - Make a request that would lead to running the PAC script
+ We run these steps for TRR mode 2 and 3, and with fetchOffMainThread = true/false
+*/
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+const { MockRegistrar } = ChromeUtils.importESModule(
+ "resource://testing-common/MockRegistrar.sys.mjs"
+);
+
+registerCleanupFunction(async () => {
+ Services.prefs.clearUserPref("network.proxy.type");
+ Services.prefs.clearUserPref("network.proxy.parse_pac_on_socket_process");
+ trr_clear_prefs();
+});
+
+function FindProxyForURL(url, host) {
+ alert(`PAC resolving: ${host}`);
+ alert(dnsResolve(host));
+ return "DIRECT";
+}
+
+ChromeUtils.defineLazyGetter(this, "systemSettings", function () {
+ return {
+ QueryInterface: ChromeUtils.generateQI(["nsISystemProxySettings"]),
+
+ mainThreadOnly: true,
+ PACURI: `data:application/x-ns-proxy-autoconfig;charset=utf-8,${encodeURIComponent(
+ FindProxyForURL.toString()
+ )}`,
+ getProxyForURI(aURI) {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
+ },
+ };
+});
+
+const override = Cc["@mozilla.org/network/native-dns-override;1"].getService(
+ Ci.nsINativeDNSResolverOverride
+);
+
+async function do_test_pac_dnsResolve() {
+ Services.prefs.setCharPref("network.trr.confirmationNS", "skip");
+ Services.console.reset();
+ // Create a console listener.
+ let consolePromise = new Promise(resolve => {
+ let listener = {
+ observe(message) {
+ // Ignore unexpected messages.
+ if (!(message instanceof Ci.nsIConsoleMessage)) {
+ return;
+ }
+
+ if (message.message.includes("PAC file installed from")) {
+ Services.console.unregisterListener(listener);
+ resolve();
+ }
+ },
+ };
+
+ Services.console.registerListener(listener);
+ });
+
+ MockRegistrar.register(
+ "@mozilla.org/system-proxy-settings;1",
+ systemSettings
+ );
+ Services.prefs.setIntPref(
+ "network.proxy.type",
+ Ci.nsIProtocolProxyService.PROXYCONFIG_SYSTEM
+ );
+
+ let httpserv = new HttpServer();
+ httpserv.registerPathHandler("/", function handler(metadata, response) {
+ let content = "ok";
+ response.setHeader("Content-Length", `${content.length}`);
+ response.bodyOutputStream.write(content, content.length);
+ });
+ httpserv.start(-1);
+
+ Services.prefs.setBoolPref("network.dns.native-is-localhost", false);
+ Services.prefs.setIntPref("network.trr.mode", 0); // Disable TRR until the PAC is loaded
+ override.addIPOverride("example.org", "127.0.0.1");
+ let chan = NetUtil.newChannel({
+ uri: `http://example.org:${httpserv.identity.primaryPort}/`,
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+ await new Promise(resolve => chan.asyncOpen(new ChannelListener(resolve)));
+ await consolePromise;
+
+ let h2Port = Services.env.get("MOZHTTP2_PORT");
+ Assert.notEqual(h2Port, null);
+ Assert.notEqual(h2Port, "");
+
+ override.addIPOverride("foo.example.com", "127.0.0.1");
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${h2Port}/doh?responseIP=127.0.0.1`
+ );
+
+ trr_test_setup();
+
+ async function test_with(DOMAIN, trrMode, fetchOffMainThread) {
+ Services.prefs.setIntPref("network.trr.mode", trrMode); // TRR first
+ Services.prefs.setBoolPref(
+ "network.trr.fetch_off_main_thread",
+ fetchOffMainThread
+ );
+ override.addIPOverride(DOMAIN, "127.0.0.1");
+
+ chan = NetUtil.newChannel({
+ uri: `http://${DOMAIN}:${httpserv.identity.primaryPort}/`,
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+ await new Promise(resolve => chan.asyncOpen(new ChannelListener(resolve)));
+
+ await override.clearHostOverride(DOMAIN);
+ }
+
+ await test_with("test1.com", 2, true);
+ await test_with("test2.com", 3, true);
+ await test_with("test3.com", 2, false);
+ await test_with("test4.com", 3, false);
+ await httpserv.stop();
+}
+
+add_task(async function test_pac_dnsResolve() {
+ Services.prefs.setBoolPref(
+ "network.proxy.parse_pac_on_socket_process",
+ false
+ );
+
+ await do_test_pac_dnsResolve();
+
+ if (mozinfo.socketprocess_networking) {
+ info("run test again");
+ Services.prefs.clearUserPref("network.proxy.type");
+ trr_clear_prefs();
+ Services.prefs.setBoolPref(
+ "network.proxy.parse_pac_on_socket_process",
+ true
+ );
+ Services.prefs.setIntPref("network.proxy.type", 2);
+ Services.prefs.setIntPref("network.proxy.type", 0);
+ await do_test_pac_dnsResolve();
+ }
+});
diff --git a/netwerk/test/unit/test_trr_proxy_auth.js b/netwerk/test/unit/test_trr_proxy_auth.js
new file mode 100644
index 0000000000..f339d82dea
--- /dev/null
+++ b/netwerk/test/unit/test_trr_proxy_auth.js
@@ -0,0 +1,122 @@
+/* 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 */
+/* import-globals-from head_servers.js */
+
+function setup() {
+ trr_test_setup();
+ Services.prefs.setBoolPref("network.trr.fetch_off_main_thread", true);
+}
+
+setup();
+registerCleanupFunction(async () => {
+ trr_clear_prefs();
+});
+
+function AuthPrompt() {}
+
+AuthPrompt.prototype = {
+ user: "guest",
+ pass: "guest",
+
+ QueryInterface: ChromeUtils.generateQI(["nsIAuthPrompt2"]),
+
+ promptAuth: function ap_promptAuth(channel, level, authInfo) {
+ authInfo.username = this.user;
+ authInfo.password = this.pass;
+
+ return true;
+ },
+
+ asyncPromptAuth: function ap_async(chan, cb, ctx, lvl, info) {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
+ },
+};
+
+function Requestor() {}
+
+Requestor.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["nsIInterfaceRequestor"]),
+
+ getInterface: function requestor_gi(iid) {
+ if (iid.equals(Ci.nsIAuthPrompt2)) {
+ // Allow the prompt to store state by caching it here
+ if (!this.prompt) {
+ this.prompt = new AuthPrompt();
+ }
+ return this.prompt;
+ }
+
+ throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE);
+ },
+
+ prompt: null,
+};
+
+// Test if we successfully retry TRR request on main thread.
+add_task(async function test_trr_proxy_auth() {
+ let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(
+ Ci.nsIX509CertDB
+ );
+ addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u");
+ addCertFromFile(certdb, "proxy-ca.pem", "CTu,u,u");
+
+ let 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`
+ );
+
+ await trrServer.registerDoHAnswers("test.proxy.com", "A", {
+ answers: [
+ {
+ name: "test.proxy.com",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "3.3.3.3",
+ },
+ ],
+ });
+
+ await new TRRDNSListener("test.proxy.com", "3.3.3.3");
+
+ let proxy = new NodeHTTP2ProxyServer();
+ await proxy.start(0, true);
+ registerCleanupFunction(async () => {
+ await proxy.stop();
+ await trrServer.stop();
+ });
+
+ let authTriggered = false;
+ let observer = {
+ QueryInterface: ChromeUtils.generateQI(["nsIObserver"]),
+ observe(aSubject, aTopic, aData) {
+ if (aTopic == "http-on-examine-response") {
+ Services.obs.removeObserver(observer, "http-on-examine-response");
+ let channel = aSubject.QueryInterface(Ci.nsIChannel);
+ channel.notificationCallbacks = new Requestor();
+ if (
+ channel.URI.spec.startsWith(
+ `https://foo.example.com:${trrServer.port()}/dns-query`
+ )
+ ) {
+ authTriggered = true;
+ }
+ }
+ },
+ };
+ Services.obs.addObserver(observer, "http-on-examine-response");
+
+ Services.dns.clearCache(true);
+ await new TRRDNSListener("test.proxy.com", "3.3.3.3");
+ Assert.ok(authTriggered);
+});
diff --git a/netwerk/test/unit/test_trr_strict_mode.js b/netwerk/test/unit/test_trr_strict_mode.js
new file mode 100644
index 0000000000..c4f8706fd5
--- /dev/null
+++ b/netwerk/test/unit/test_trr_strict_mode.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/. */
+
+"use strict";
+
+function setup() {
+ trr_test_setup();
+}
+setup();
+
+add_task(async function checkBlocklisting() {
+ Services.prefs.setBoolPref("network.trr.temp_blocklist", true);
+ Services.prefs.setBoolPref("network.trr.strict_native_fallback", true);
+
+ let trrServer = new TRRServer();
+ registerCleanupFunction(async () => {
+ await trrServer.stop();
+ });
+ await trrServer.start();
+ info(`port = ${trrServer.port()}\n`);
+
+ Services.dns.clearCache(true);
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${trrServer.port()}/dns-query`
+ );
+ Services.prefs.setIntPref("network.trr.mode", Ci.nsIDNSService.MODE_TRRFIRST);
+
+ // Check that we properly fallback to native DNS for a variety of DNS rcodes
+ for (let i = 0; i <= 5; i++) {
+ info(`testing rcode=${i}`);
+ await trrServer.registerDoHAnswers(`sub${i}.blocklisted.com`, "A", {
+ flags: i,
+ });
+ await trrServer.registerDoHAnswers(`sub${i}.blocklisted.com`, "AAAA", {
+ flags: i,
+ });
+
+ await new TRRDNSListener(`sub${i}.blocklisted.com`, {
+ expectedAnswer: "127.0.0.1",
+ });
+ Services.dns.clearCache(true);
+ await new TRRDNSListener(`sub${i}.blocklisted.com`, {
+ expectedAnswer: "127.0.0.1",
+ });
+ await new TRRDNSListener(`sub.sub${i}.blocklisted.com`, {
+ expectedAnswer: "127.0.0.1",
+ });
+ Services.dns.clearCache(true);
+ }
+});
diff --git a/netwerk/test/unit/test_trr_telemetry.js b/netwerk/test/unit/test_trr_telemetry.js
new file mode 100644
index 0000000000..fff6f07774
--- /dev/null
+++ b/netwerk/test/unit/test_trr_telemetry.js
@@ -0,0 +1,118 @@
+"use strict";
+
+/* import-globals-from trr_common.js */
+
+// Allow telemetry probes which may otherwise be disabled for some
+// applications (e.g. Thunderbird).
+Services.prefs.setBoolPref(
+ "toolkit.telemetry.testing.overrideProductsCheck",
+ true
+);
+
+const { TelemetryTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TelemetryTestUtils.sys.mjs"
+);
+
+function setup() {
+ h2Port = trr_test_setup();
+}
+
+setup();
+registerCleanupFunction(async () => {
+ trr_clear_prefs();
+});
+
+async function trrLookup(mode, rolloutMode) {
+ let hist = TelemetryTestUtils.getAndClearKeyedHistogram(
+ "TRR_SKIP_REASON_TRR_FIRST2"
+ );
+
+ if (rolloutMode) {
+ info("Testing doh-rollout.mode");
+ setModeAndURI(0, "doh?responseIP=2.2.2.2");
+ Services.prefs.setIntPref("doh-rollout.mode", rolloutMode);
+ } else {
+ setModeAndURI(mode, "doh?responseIP=2.2.2.2");
+ }
+
+ Services.dns.clearCache(true);
+ await new TRRDNSListener("test.example.com", "2.2.2.2");
+ let expectedKey = `(other)_${mode}`;
+ if (mode == 0) {
+ expectedKey = "(other)";
+ }
+
+ let snapshot = hist.snapshot();
+ await TestUtils.waitForCondition(() => {
+ snapshot = hist.snapshot();
+ info("snapshot:" + JSON.stringify(snapshot));
+ return snapshot;
+ });
+ TelemetryTestUtils.assertKeyedHistogramValue(
+ hist,
+ expectedKey,
+ Ci.nsITRRSkipReason.TRR_OK,
+ 1
+ );
+}
+
+add_task(async function test_trr_lookup_mode_2() {
+ await trrLookup(Ci.nsIDNSService.MODE_TRRFIRST);
+});
+
+add_task(async function test_trr_lookup_mode_3() {
+ await trrLookup(Ci.nsIDNSService.MODE_TRRONLY);
+});
+
+add_task(async function test_trr_lookup_mode_0() {
+ await trrLookup(
+ Ci.nsIDNSService.MODE_NATIVEONLY,
+ Ci.nsIDNSService.MODE_TRRFIRST
+ );
+});
+
+async function trrByTypeLookup(trrURI, expectedSuccess, expectedSkipReason) {
+ Services.prefs.setIntPref(
+ "doh-rollout.mode",
+ Ci.nsIDNSService.MODE_NATIVEONLY
+ );
+
+ let hist = TelemetryTestUtils.getAndClearKeyedHistogram(
+ "TRR_RELEVANT_SKIP_REASON_TRR_FIRST_TYPE_REC"
+ );
+
+ setModeAndURI(Ci.nsIDNSService.MODE_TRRFIRST, trrURI);
+
+ Services.dns.clearCache(true);
+ await new TRRDNSListener("test.httpssvc.com", {
+ type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC,
+ expectedSuccess,
+ });
+ let expectedKey = `(other)_2`;
+
+ let snapshot = hist.snapshot();
+ await TestUtils.waitForCondition(() => {
+ snapshot = hist.snapshot();
+ info("snapshot:" + JSON.stringify(snapshot));
+ return snapshot;
+ });
+
+ TelemetryTestUtils.assertKeyedHistogramValue(
+ hist,
+ expectedKey,
+ expectedSkipReason,
+ 1
+ );
+}
+
+add_task(async function test_trr_by_type_lookup_success() {
+ await trrByTypeLookup("httpssvc", true, Ci.nsITRRSkipReason.TRR_OK);
+});
+
+add_task(async function test_trr_by_type_lookup_fail() {
+ await trrByTypeLookup(
+ "doh?responseIP=none",
+ false,
+ Ci.nsITRRSkipReason.TRR_NO_ANSWERS
+ );
+});
diff --git a/netwerk/test/unit/test_trr_ttl.js b/netwerk/test/unit/test_trr_ttl.js
new file mode 100644
index 0000000000..36cd0d4a5a
--- /dev/null
+++ b/netwerk/test/unit/test_trr_ttl.js
@@ -0,0 +1,60 @@
+/* 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";
+
+trr_test_setup();
+registerCleanupFunction(async () => {
+ trr_clear_prefs();
+});
+
+let trrServer = null;
+add_task(async function setup() {
+ trrServer = new TRRServer();
+ registerCleanupFunction(async () => {
+ await trrServer.stop();
+ });
+ await trrServer.start();
+ dump(`port = ${trrServer.port()}\n`);
+
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${trrServer.port()}/dns-query`
+ );
+ Services.prefs.setIntPref("network.trr.mode", Ci.nsIDNSService.MODE_TRRFIRST);
+});
+
+add_task(async function check_ttl_works() {
+ await trrServer.registerDoHAnswers("example.com", "A", {
+ answers: [
+ {
+ name: "example.com",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "1.2.3.4",
+ },
+ ],
+ });
+ let { inRecord } = await new TRRDNSListener("example.com", {
+ expectedAnswer: "1.2.3.4",
+ });
+ equal(inRecord.QueryInterface(Ci.nsIDNSAddrRecord).ttl, 55);
+ await trrServer.registerDoHAnswers("example.org", "A", {
+ answers: [
+ {
+ name: "example.org",
+ ttl: 999,
+ type: "A",
+ flush: false,
+ data: "1.2.3.4",
+ },
+ ],
+ });
+ // Simple check to see that TRR works.
+ ({ inRecord } = await new TRRDNSListener("example.org", {
+ expectedAnswer: "1.2.3.4",
+ }));
+ equal(inRecord.QueryInterface(Ci.nsIDNSAddrRecord).ttl, 999);
+});
diff --git a/netwerk/test/unit/test_trr_with_proxy.js b/netwerk/test/unit/test_trr_with_proxy.js
new file mode 100644
index 0000000000..7d238adb98
--- /dev/null
+++ b/netwerk/test/unit/test_trr_with_proxy.js
@@ -0,0 +1,210 @@
+/* This test checks that a TRRServiceChannel can connect to the server with
+ a proxy.
+ Steps:
+ - Setup the proxy (PAC, proxy filter, and system proxy settings)
+ - Test when "network.trr.async_connInfo" is false. In this case, every
+ TRRServicChannel waits for the proxy info to be resolved.
+ - Test when "network.trr.async_connInfo" is true. In this case, every
+ TRRServicChannel uses an already created connection info to connect.
+ - The test test_trr_uri_change() is about checking if trr connection info
+ is updated correctly when trr uri changed.
+*/
+
+"use strict";
+
+var { setTimeout } = ChromeUtils.importESModule(
+ "resource://gre/modules/Timer.sys.mjs"
+);
+
+/* import-globals-from trr_common.js */
+
+let filter;
+let systemProxySettings;
+let trrProxy;
+const pps = Cc["@mozilla.org/network/protocol-proxy-service;1"].getService();
+
+function setup() {
+ h2Port = trr_test_setup();
+ SetParentalControlEnabled(false);
+}
+
+setup();
+registerCleanupFunction(async () => {
+ trr_clear_prefs();
+ Services.prefs.clearUserPref("network.proxy.type");
+ Services.prefs.clearUserPref("network.proxy.autoconfig_url");
+ Services.prefs.clearUserPref("network.trr.async_connInfo");
+ if (trrProxy) {
+ await trrProxy.stop();
+ }
+});
+
+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) {
+ cb.onProxyFilterResult(
+ pps.newProxyInfo(
+ this._type,
+ this._host,
+ this._port,
+ "",
+ "",
+ this._flags,
+ 1000,
+ null
+ )
+ );
+ }
+}
+
+async function doTest(proxySetup, delay) {
+ info("Verifying a basic A record");
+ Services.dns.clearCache(true);
+ // Close all previous connections.
+ Services.obs.notifyObservers(null, "net:cancel-all-connections");
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => setTimeout(resolve, 1000));
+
+ setModeAndURI(2, "doh?responseIP=2.2.2.2"); // TRR-first
+
+ trrProxy = new TRRProxy();
+ await trrProxy.start(h2Port);
+ info("port=" + trrProxy.port);
+
+ await proxySetup(trrProxy.port);
+
+ if (delay) {
+ await new Promise(resolve => do_timeout(delay, resolve));
+ }
+
+ await new TRRDNSListener("bar.example.com", "2.2.2.2");
+
+ // A non-zero request count indicates that TRR requests are being routed
+ // through the proxy.
+ Assert.ok(
+ (await trrProxy.request_count()) >= 1,
+ `Request count should be at least 1`
+ );
+
+ // clean up
+ Services.prefs.clearUserPref("network.proxy.type");
+ Services.prefs.clearUserPref("network.proxy.autoconfig_url");
+ if (filter) {
+ pps.unregisterFilter(filter);
+ filter = null;
+ }
+ if (systemProxySettings) {
+ MockRegistrar.unregister(systemProxySettings);
+ systemProxySettings = null;
+ }
+
+ await trrProxy.stop();
+ trrProxy = null;
+}
+
+add_task(async function test_trr_proxy() {
+ async function setupPACWithDataURL(proxyPort) {
+ var pac = `data:text/plain, function FindProxyForURL(url, host) { return "HTTPS foo.example.com:${proxyPort}";}`;
+ Services.prefs.setIntPref("network.proxy.type", 2);
+ Services.prefs.setCharPref("network.proxy.autoconfig_url", pac);
+ }
+
+ async function setupPACWithHttpURL(proxyPort) {
+ let httpserv = new HttpServer();
+ httpserv.registerPathHandler("/", function handler(metadata, response) {
+ response.setStatusLine(metadata.httpVersion, 200, "OK");
+ let content = `function FindProxyForURL(url, host) { return "HTTPS foo.example.com:${proxyPort}";}`;
+ response.setHeader("Content-Length", `${content.length}`);
+ response.bodyOutputStream.write(content, content.length);
+ });
+ httpserv.start(-1);
+ Services.prefs.setIntPref("network.proxy.type", 2);
+ let pacUri = `http://127.0.0.1:${httpserv.identity.primaryPort}/`;
+ Services.prefs.setCharPref("network.proxy.autoconfig_url", pacUri);
+
+ function consoleMessageObserved() {
+ return new Promise(resolve => {
+ let listener = {
+ QueryInterface: ChromeUtils.generateQI(["nsIConsoleListener"]),
+ observe(msg) {
+ if (msg == `PAC file installed from ${pacUri}`) {
+ Services.console.unregisterListener(listener);
+ resolve();
+ }
+ },
+ };
+ Services.console.registerListener(listener);
+ });
+ }
+
+ await consoleMessageObserved();
+ }
+
+ async function setupProxyFilter(proxyPort) {
+ filter = new ProxyFilter("https", "foo.example.com", proxyPort, 0);
+ pps.registerFilter(filter, 10);
+ }
+
+ async function setupSystemProxySettings(proxyPort) {
+ systemProxySettings = {
+ QueryInterface: ChromeUtils.generateQI(["nsISystemProxySettings"]),
+ mainThreadOnly: true,
+ PACURI: null,
+ getProxyForURI: (aSpec, aScheme, aHost, aPort) => {
+ return `HTTPS foo.example.com:${proxyPort}`;
+ },
+ };
+
+ MockRegistrar.register(
+ "@mozilla.org/system-proxy-settings;1",
+ systemProxySettings
+ );
+
+ Services.prefs.setIntPref(
+ "network.proxy.type",
+ Ci.nsIProtocolProxyService.PROXYCONFIG_SYSTEM
+ );
+
+ // simulate that system proxy setting is changed.
+ pps.notifyProxyConfigChangedInternal();
+ }
+
+ Services.prefs.setBoolPref("network.trr.async_connInfo", false);
+ await doTest(setupPACWithDataURL);
+ await doTest(setupPACWithDataURL, 1000);
+ await doTest(setupPACWithHttpURL);
+ await doTest(setupPACWithHttpURL, 1000);
+ await doTest(setupProxyFilter);
+ await doTest(setupProxyFilter, 1000);
+ await doTest(setupSystemProxySettings);
+ await doTest(setupSystemProxySettings, 1000);
+
+ Services.prefs.setBoolPref("network.trr.async_connInfo", true);
+ await doTest(setupPACWithDataURL);
+ await doTest(setupPACWithDataURL, 1000);
+ await doTest(setupPACWithHttpURL);
+ await doTest(setupPACWithHttpURL, 1000);
+ await doTest(setupProxyFilter);
+ await doTest(setupProxyFilter, 1000);
+ await doTest(setupSystemProxySettings);
+ await doTest(setupSystemProxySettings, 1000);
+});
+
+add_task(async function test_trr_uri_change() {
+ Services.prefs.setIntPref("network.proxy.type", 0);
+ Services.prefs.setBoolPref("network.trr.async_connInfo", true);
+ Services.dns.clearCache(true);
+ setModeAndURI(2, "doh?responseIP=2.2.2.2", "127.0.0.1");
+
+ await new TRRDNSListener("car.example.com", "127.0.0.1");
+
+ Services.dns.clearCache(true);
+ setModeAndURI(2, "doh?responseIP=2.2.2.2");
+ await new TRRDNSListener("car.example.net", "2.2.2.2");
+});
diff --git a/netwerk/test/unit/test_udp_multicast.js b/netwerk/test/unit/test_udp_multicast.js
new file mode 100644
index 0000000000..202a4a1d99
--- /dev/null
+++ b/netwerk/test/unit/test_udp_multicast.js
@@ -0,0 +1,99 @@
+// Bug 960397: UDP multicast options
+"use strict";
+
+var { Constructor: CC } = Components;
+
+const UDPSocket = CC(
+ "@mozilla.org/network/udp-socket;1",
+ "nsIUDPSocket",
+ "init"
+);
+const ADDRESS_TEST1 = "224.0.0.200";
+const ADDRESS_TEST2 = "224.0.0.201";
+const ADDRESS_TEST3 = "224.0.0.202";
+const ADDRESS_TEST4 = "224.0.0.203";
+
+const TIMEOUT = 2000;
+
+function createSocketAndJoin(addr) {
+ let socket = new UDPSocket(
+ -1,
+ false,
+ Services.scriptSecurityManager.getSystemPrincipal()
+ );
+ socket.joinMulticast(addr);
+ return socket;
+}
+
+function sendPing(socket, addr) {
+ let ping = "ping";
+ let rawPing = new TextEncoder().encode(ping);
+
+ return new Promise((resolve, reject) => {
+ socket.asyncListen({
+ onPacketReceived(s, message) {
+ info("Received on port " + socket.port);
+ Assert.equal(message.data, ping);
+ socket.close();
+ resolve(message.data);
+ },
+ onStopListening(sock, status) {},
+ });
+
+ info("Multicast send to port " + socket.port);
+ socket.send(addr, socket.port, rawPing, rawPing.length);
+
+ // Timers are bad, but it seems like the only way to test *not* getting a
+ // packet.
+ let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ timer.initWithCallback(
+ () => {
+ socket.close();
+ reject();
+ },
+ TIMEOUT,
+ Ci.nsITimer.TYPE_ONE_SHOT
+ );
+ });
+}
+
+add_test(() => {
+ info("Joining multicast group");
+ let socket = createSocketAndJoin(ADDRESS_TEST1);
+ sendPing(socket, ADDRESS_TEST1).then(run_next_test, () =>
+ do_throw("Joined group, but no packet received")
+ );
+});
+
+// Disabled on Windows11 for frequent intermittent failures.
+// See bug 1760123.
+add_test({ skip_if: () => mozinfo.win11_2009 }, () => {
+ info("Disabling multicast loopback");
+ let socket = createSocketAndJoin(ADDRESS_TEST2);
+ socket.multicastLoopback = false;
+ sendPing(socket, ADDRESS_TEST2).then(
+ () => do_throw("Loopback disabled, but still got a packet"),
+ run_next_test
+ );
+});
+
+// This fails locally on windows 11.
+add_test({ skip_if: () => mozinfo.win11_2009 }, () => {
+ info("Changing multicast interface");
+ let socket = createSocketAndJoin(ADDRESS_TEST3);
+ socket.multicastInterface = "127.0.0.1";
+ sendPing(socket, ADDRESS_TEST3).then(
+ () => do_throw("Changed interface, but still got a packet"),
+ run_next_test
+ );
+});
+
+add_test(() => {
+ info("Leaving multicast group");
+ let socket = createSocketAndJoin(ADDRESS_TEST4);
+ socket.leaveMulticast(ADDRESS_TEST4);
+ sendPing(socket, ADDRESS_TEST4).then(
+ () => do_throw("Left group, but still got a packet"),
+ run_next_test
+ );
+});
diff --git a/netwerk/test/unit/test_udpsocket.js b/netwerk/test/unit/test_udpsocket.js
new file mode 100644
index 0000000000..340fe6aa13
--- /dev/null
+++ b/netwerk/test/unit/test_udpsocket.js
@@ -0,0 +1,89 @@
+/* -*- Mode: Javascript; 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 HELLO_WORLD = "Hello World";
+const EMPTY_MESSAGE = "";
+
+add_test(function test_udp_message_raw_data() {
+ info("test for nsIUDPMessage.rawData");
+
+ let socket = Cc["@mozilla.org/network/udp-socket;1"].createInstance(
+ Ci.nsIUDPSocket
+ );
+
+ socket.init(-1, true, Services.scriptSecurityManager.getSystemPrincipal());
+ info("Port assigned : " + socket.port);
+ socket.asyncListen({
+ QueryInterface: ChromeUtils.generateQI(["nsIUDPSocketListener"]),
+ onPacketReceived(aSocket, aMessage) {
+ let recv_data = String.fromCharCode.apply(null, aMessage.rawData);
+ Assert.equal(recv_data, HELLO_WORLD);
+ Assert.equal(recv_data, aMessage.data);
+ socket.close();
+ run_next_test();
+ },
+ onStopListening(aSocket, aStatus) {},
+ });
+
+ let rawData = new Uint8Array(HELLO_WORLD.length);
+ for (let i = 0; i < HELLO_WORLD.length; i++) {
+ rawData[i] = HELLO_WORLD.charCodeAt(i);
+ }
+ let written = socket.send("127.0.0.1", socket.port, rawData);
+ Assert.equal(written, HELLO_WORLD.length);
+});
+
+add_test(function test_udp_send_stream() {
+ info("test for nsIUDPSocket.sendBinaryStream");
+
+ let socket = Cc["@mozilla.org/network/udp-socket;1"].createInstance(
+ Ci.nsIUDPSocket
+ );
+
+ socket.init(-1, true, Services.scriptSecurityManager.getSystemPrincipal());
+ socket.asyncListen({
+ QueryInterface: ChromeUtils.generateQI(["nsIUDPSocketListener"]),
+ onPacketReceived(aSocket, aMessage) {
+ let recv_data = String.fromCharCode.apply(null, aMessage.rawData);
+ Assert.equal(recv_data, HELLO_WORLD);
+ socket.close();
+ run_next_test();
+ },
+ onStopListening(aSocket, aStatus) {},
+ });
+
+ let stream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(
+ Ci.nsIStringInputStream
+ );
+ stream.setData(HELLO_WORLD, HELLO_WORLD.length);
+ socket.sendBinaryStream("127.0.0.1", socket.port, stream);
+});
+
+add_test(function test_udp_message_zero_length() {
+ info("test for nsIUDPMessage with zero length");
+
+ let socket = Cc["@mozilla.org/network/udp-socket;1"].createInstance(
+ Ci.nsIUDPSocket
+ );
+
+ socket.init(-1, true, Services.scriptSecurityManager.getSystemPrincipal());
+ info("Port assigned : " + socket.port);
+ socket.asyncListen({
+ QueryInterface: ChromeUtils.generateQI(["nsIUDPSocketListener"]),
+ onPacketReceived(aSocket, aMessage) {
+ let recv_data = String.fromCharCode.apply(null, aMessage.rawData);
+ Assert.equal(recv_data, EMPTY_MESSAGE);
+ Assert.equal(recv_data, aMessage.data);
+ socket.close();
+ run_next_test();
+ },
+ onStopListening(aSocket, aStatus) {},
+ });
+
+ let rawData = new Uint8Array(EMPTY_MESSAGE.length);
+ let written = socket.send("127.0.0.1", socket.port, rawData);
+ Assert.equal(written, EMPTY_MESSAGE.length);
+});
diff --git a/netwerk/test/unit/test_udpsocket_offline.js b/netwerk/test/unit/test_udpsocket_offline.js
new file mode 100644
index 0000000000..080f54281d
--- /dev/null
+++ b/netwerk/test/unit/test_udpsocket_offline.js
@@ -0,0 +1,144 @@
+/* -*- 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";
+
+add_test(function test_ipv4_any() {
+ let socket = Cc["@mozilla.org/network/udp-socket;1"].createInstance(
+ Ci.nsIUDPSocket
+ );
+
+ Assert.throws(() => {
+ socket.init(-1, false, Services.scriptSecurityManager.getSystemPrincipal());
+ }, /NS_ERROR_OFFLINE/);
+
+ run_next_test();
+});
+
+add_test(function test_ipv6_any() {
+ let socket = Cc["@mozilla.org/network/udp-socket;1"].createInstance(
+ Ci.nsIUDPSocket
+ );
+
+ Assert.throws(() => {
+ socket.init2("::", -1, Services.scriptSecurityManager.getSystemPrincipal());
+ }, /NS_ERROR_OFFLINE/);
+
+ run_next_test();
+});
+
+add_test(function test_ipv4() {
+ let socket = Cc["@mozilla.org/network/udp-socket;1"].createInstance(
+ Ci.nsIUDPSocket
+ );
+
+ Assert.throws(() => {
+ socket.init2(
+ "240.0.0.1",
+ -1,
+ Services.scriptSecurityManager.getSystemPrincipal()
+ );
+ }, /NS_ERROR_OFFLINE/);
+
+ run_next_test();
+});
+
+add_test(function test_ipv6() {
+ let socket = Cc["@mozilla.org/network/udp-socket;1"].createInstance(
+ Ci.nsIUDPSocket
+ );
+
+ Assert.throws(() => {
+ socket.init2(
+ "2001:db8::1",
+ -1,
+ Services.scriptSecurityManager.getSystemPrincipal()
+ );
+ }, /NS_ERROR_OFFLINE/);
+
+ run_next_test();
+});
+
+add_test(function test_ipv4_loopback() {
+ let socket = Cc["@mozilla.org/network/udp-socket;1"].createInstance(
+ Ci.nsIUDPSocket
+ );
+
+ try {
+ socket.init2(
+ "127.0.0.1",
+ -1,
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ true
+ );
+ } catch (e) {
+ Assert.ok(false, "unexpected exception: " + e);
+ }
+
+ // Now with localhost connections disabled in offline mode.
+ Services.prefs.setBoolPref("network.disable-localhost-when-offline", true);
+ socket = Cc["@mozilla.org/network/udp-socket;1"].createInstance(
+ Ci.nsIUDPSocket
+ );
+
+ Assert.throws(() => {
+ socket.init2(
+ "127.0.0.1",
+ -1,
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ true
+ );
+ }, /NS_ERROR_OFFLINE/);
+
+ Services.prefs.setBoolPref("network.disable-localhost-when-offline", false);
+
+ run_next_test();
+});
+
+add_test(function test_ipv6_loopback() {
+ let socket = Cc["@mozilla.org/network/udp-socket;1"].createInstance(
+ Ci.nsIUDPSocket
+ );
+
+ try {
+ socket.init2(
+ "::1",
+ -1,
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ true
+ );
+ } catch (e) {
+ Assert.ok(false, "unexpected exception: " + e);
+ }
+
+ // Now with localhost connections disabled in offline mode.
+ Services.prefs.setBoolPref("network.disable-localhost-when-offline", true);
+ socket = Cc["@mozilla.org/network/udp-socket;1"].createInstance(
+ Ci.nsIUDPSocket
+ );
+
+ Assert.throws(() => {
+ socket.init2(
+ "::1",
+ -1,
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ true
+ );
+ }, /NS_ERROR_OFFLINE/);
+
+ Services.prefs.setBoolPref("network.disable-localhost-when-offline", false);
+
+ run_next_test();
+});
+
+function run_test() {
+ // jshint ignore:line
+ Services.io.offline = true;
+ Services.prefs.setBoolPref("network.disable-localhost-when-offline", false);
+ registerCleanupFunction(() => {
+ Services.io.offline = false;
+ Services.prefs.clearUserPref("network.disable-localhost-when-offline");
+ });
+ run_next_test();
+}
diff --git a/netwerk/test/unit/test_unescapestring.js b/netwerk/test/unit/test_unescapestring.js
new file mode 100644
index 0000000000..9853d2c0af
--- /dev/null
+++ b/netwerk/test/unit/test_unescapestring.js
@@ -0,0 +1,35 @@
+"use strict";
+
+const ONLY_NONASCII = Ci.nsINetUtil.ESCAPE_URL_ONLY_NONASCII;
+const SKIP_CONTROL = Ci.nsINetUtil.ESCAPE_URL_SKIP_CONTROL;
+
+var tests = [
+ ["foo", "foo", 0],
+ ["foo%20bar", "foo bar", 0],
+ ["foo%2zbar", "foo%2zbar", 0],
+ ["foo%", "foo%", 0],
+ ["%zzfoo", "%zzfoo", 0],
+ ["foo%z", "foo%z", 0],
+ ["foo%00bar", "foo\x00bar", 0],
+ ["foo%ffbar", "foo\xffbar", 0],
+ ["%00%1b%20%61%7f%80%ff", "%00%1b%20%61%7f\x80\xff", ONLY_NONASCII],
+ ["%00%1b%20%61%7f%80%ff", "%00%1b a%7f\x80\xff", SKIP_CONTROL],
+ [
+ "%00%1b%20%61%7f%80%ff",
+ "%00%1b%20%61%7f\x80\xff",
+ ONLY_NONASCII | SKIP_CONTROL,
+ ],
+ // Test that we do not drop the high-bytes of a UTF-16 string.
+ ["\u30a8\u30c9", "\xe3\x82\xa8\xe3\x83\x89", 0],
+];
+
+function run_test() {
+ for (var i = 0; i < tests.length; ++i) {
+ dump("Test " + i + " (" + tests[i][0] + ", " + tests[i][2] + ")\n");
+ Assert.equal(
+ Services.io.unescapeString(tests[i][0], tests[i][2]),
+ tests[i][1]
+ );
+ }
+ dump(tests.length + " tests passed\n");
+}
diff --git a/netwerk/test/unit/test_unix_domain.js b/netwerk/test/unit/test_unix_domain.js
new file mode 100644
index 0000000000..221721dc38
--- /dev/null
+++ b/netwerk/test/unit/test_unix_domain.js
@@ -0,0 +1,699 @@
+// Exercise Unix domain sockets.
+"use strict";
+
+var CC = Components.Constructor;
+
+const UnixServerSocket = CC(
+ "@mozilla.org/network/server-socket;1",
+ "nsIServerSocket",
+ "initWithFilename"
+);
+const UnixAbstractServerSocket = CC(
+ "@mozilla.org/network/server-socket;1",
+ "nsIServerSocket",
+ "initWithAbstractAddress"
+);
+
+const ScriptableInputStream = CC(
+ "@mozilla.org/scriptableinputstream;1",
+ "nsIScriptableInputStream",
+ "init"
+);
+
+const socketTransportService = Cc[
+ "@mozilla.org/network/socket-transport-service;1"
+].getService(Ci.nsISocketTransportService);
+
+const threadManager = Cc["@mozilla.org/thread-manager;1"].getService();
+
+const allPermissions = parseInt("777", 8);
+
+function run_test() {
+ // If we're on Windows, simply check for graceful failure.
+ if (mozinfo.os == "win") {
+ test_not_supported();
+ return;
+ }
+
+ // The xpcshell temp directory on Android doesn't seem to let us create
+ // Unix domain sockets. (Perhaps it's a FAT filesystem?)
+ if (mozinfo.os != "android") {
+ add_test(test_echo);
+ add_test(test_name_too_long);
+ add_test(test_no_directory);
+ add_test(test_no_such_socket);
+ add_test(test_address_in_use);
+ add_test(test_file_in_way);
+ add_test(test_create_permission);
+ add_test(test_connect_permission);
+ add_test(test_long_socket_name);
+ add_test(test_keep_when_offline);
+ }
+
+ if (mozinfo.os == "android" || mozinfo.os == "linux") {
+ add_test(test_abstract_address_socket);
+ }
+
+ run_next_test();
+}
+
+// Check that creating a Unix domain socket fails gracefully on Windows.
+function test_not_supported() {
+ let socketName = do_get_tempdir();
+ socketName.append("socket");
+ info("creating socket: " + socketName.path);
+
+ do_check_throws_nsIException(
+ () => new UnixServerSocket(socketName, allPermissions, -1),
+ "NS_ERROR_SOCKET_ADDRESS_NOT_SUPPORTED"
+ );
+
+ do_check_throws_nsIException(
+ () => socketTransportService.createUnixDomainTransport(socketName),
+ "NS_ERROR_SOCKET_ADDRESS_NOT_SUPPORTED"
+ );
+}
+
+// Actually exchange data with Unix domain sockets.
+function test_echo() {
+ let log = "";
+
+ let socketName = do_get_tempdir();
+ socketName.append("socket");
+
+ // Create a server socket, listening for connections.
+ info("creating socket: " + socketName.path);
+ let server = new UnixServerSocket(socketName, allPermissions, -1);
+ server.asyncListen({
+ onSocketAccepted(aServ, aTransport) {
+ info("called test_echo's onSocketAccepted");
+ log += "a";
+
+ Assert.equal(aServ, server);
+
+ let connection = aTransport;
+
+ // Check the server socket's self address.
+ let connectionSelfAddr = connection.getScriptableSelfAddr();
+ Assert.equal(connectionSelfAddr.family, Ci.nsINetAddr.FAMILY_LOCAL);
+ Assert.equal(connectionSelfAddr.address, socketName.path);
+
+ // The client socket is anonymous, so the server transport should
+ // have an empty peer address.
+ Assert.equal(connection.host, "");
+ Assert.equal(connection.port, 0);
+ let connectionPeerAddr = connection.getScriptablePeerAddr();
+ Assert.equal(connectionPeerAddr.family, Ci.nsINetAddr.FAMILY_LOCAL);
+ Assert.equal(connectionPeerAddr.address, "");
+
+ let serverAsyncInput = connection
+ .openInputStream(0, 0, 0)
+ .QueryInterface(Ci.nsIAsyncInputStream);
+ let serverOutput = connection.openOutputStream(0, 0, 0);
+
+ serverAsyncInput.asyncWait(
+ function (aStream) {
+ info("called test_echo's server's onInputStreamReady");
+ let serverScriptableInput = new ScriptableInputStream(aStream);
+
+ // Receive data from the client, and send back a response.
+ Assert.equal(
+ serverScriptableInput.readBytes(17),
+ "Mervyn Murgatroyd"
+ );
+ info("server has read message from client");
+ serverOutput.write("Ruthven Murgatroyd", 18);
+ info("server has written to client");
+ },
+ 0,
+ 0,
+ threadManager.currentThread
+ );
+ },
+
+ onStopListening(aServ, aStatus) {
+ info("called test_echo's onStopListening");
+ log += "s";
+
+ Assert.equal(aServ, server);
+ Assert.equal(log, "acs");
+
+ run_next_test();
+ },
+ });
+
+ // Create a client socket, and connect to the server.
+ let client = socketTransportService.createUnixDomainTransport(socketName);
+ Assert.equal(client.host, socketName.path);
+ Assert.equal(client.port, 0);
+
+ let clientAsyncInput = client
+ .openInputStream(0, 0, 0)
+ .QueryInterface(Ci.nsIAsyncInputStream);
+ let clientInput = new ScriptableInputStream(clientAsyncInput);
+ let clientOutput = client.openOutputStream(0, 0, 0);
+
+ clientOutput.write("Mervyn Murgatroyd", 17);
+ info("client has written to server");
+
+ clientAsyncInput.asyncWait(
+ function (aStream) {
+ info("called test_echo's client's onInputStreamReady");
+ log += "c";
+
+ Assert.equal(aStream, clientAsyncInput);
+
+ // Now that the connection has been established, we can check the
+ // transport's self and peer addresses.
+ let clientSelfAddr = client.getScriptableSelfAddr();
+ Assert.equal(clientSelfAddr.family, Ci.nsINetAddr.FAMILY_LOCAL);
+ Assert.equal(clientSelfAddr.address, "");
+
+ Assert.equal(client.host, socketName.path); // re-check, but hey
+ let clientPeerAddr = client.getScriptablePeerAddr();
+ Assert.equal(clientPeerAddr.family, Ci.nsINetAddr.FAMILY_LOCAL);
+ Assert.equal(clientPeerAddr.address, socketName.path);
+
+ Assert.equal(clientInput.readBytes(18), "Ruthven Murgatroyd");
+ info("client has read message from server");
+
+ server.close();
+ },
+ 0,
+ 0,
+ threadManager.currentThread
+ );
+}
+
+// Create client and server sockets using a path that's too long.
+function test_name_too_long() {
+ let socketName = do_get_tempdir();
+ // The length limits on all the systems NSPR supports are a bit past 100.
+ socketName.append(new Array(1000).join("x"));
+
+ // The length must be checked before we ever make any system calls --- we
+ // have to create the sockaddr first --- so it's unambiguous which error
+ // we should get here.
+
+ do_check_throws_nsIException(
+ () => new UnixServerSocket(socketName, 0, -1),
+ "NS_ERROR_FILE_NAME_TOO_LONG"
+ );
+
+ // Unlike most other client socket errors, this one gets reported
+ // immediately, as we can't even initialize the sockaddr with the given
+ // name.
+ do_check_throws_nsIException(
+ () => socketTransportService.createUnixDomainTransport(socketName),
+ "NS_ERROR_FILE_NAME_TOO_LONG"
+ );
+
+ run_next_test();
+}
+
+// Try creating a socket in a directory that doesn't exist.
+function test_no_directory() {
+ let socketName = do_get_tempdir();
+ socketName.append("missing");
+ socketName.append("socket");
+
+ do_check_throws_nsIException(
+ () => new UnixServerSocket(socketName, 0, -1),
+ "NS_ERROR_FILE_NOT_FOUND"
+ );
+
+ run_next_test();
+}
+
+// Try connecting to a server socket that isn't there.
+function test_no_such_socket() {
+ let socketName = do_get_tempdir();
+ socketName.append("nonexistent-socket");
+
+ let client = socketTransportService.createUnixDomainTransport(socketName);
+ let clientAsyncInput = client
+ .openInputStream(0, 0, 0)
+ .QueryInterface(Ci.nsIAsyncInputStream);
+ clientAsyncInput.asyncWait(
+ function (aStream) {
+ info("called test_no_such_socket's onInputStreamReady");
+
+ Assert.equal(aStream, clientAsyncInput);
+
+ // nsISocketTransport puts off actually creating sockets as long as
+ // possible, so the error in connecting doesn't actually show up until
+ // this point.
+ do_check_throws_nsIException(
+ () => clientAsyncInput.available(),
+ "NS_ERROR_FILE_NOT_FOUND"
+ );
+
+ clientAsyncInput.close();
+ client.close(Cr.NS_OK);
+
+ run_next_test();
+ },
+ 0,
+ 0,
+ threadManager.currentThread
+ );
+}
+
+// Creating a socket with a name that another socket is already using is an
+// error.
+function test_address_in_use() {
+ let socketName = do_get_tempdir();
+ socketName.append("socket-in-use");
+
+ // Create one server socket.
+ new UnixServerSocket(socketName, allPermissions, -1);
+
+ // Now try to create another with the same name.
+ do_check_throws_nsIException(
+ () => new UnixServerSocket(socketName, allPermissions, -1),
+ "NS_ERROR_SOCKET_ADDRESS_IN_USE"
+ );
+
+ run_next_test();
+}
+
+// Creating a socket with a name that is already a file is an error.
+function test_file_in_way() {
+ let socketName = do_get_tempdir();
+ socketName.append("file_in_way");
+
+ // Create a file with the given name.
+ socketName.create(Ci.nsIFile.NORMAL_FILE_TYPE, allPermissions);
+
+ // Try to create a socket with the same name.
+ do_check_throws_nsIException(
+ () => new UnixServerSocket(socketName, allPermissions, -1),
+ "NS_ERROR_SOCKET_ADDRESS_IN_USE"
+ );
+
+ // Try to create a socket under a name that uses that as a parent directory.
+ socketName.append("socket");
+ do_check_throws_nsIException(
+ () => new UnixServerSocket(socketName, 0, -1),
+ "NS_ERROR_FILE_NOT_DIRECTORY"
+ );
+
+ run_next_test();
+}
+
+// It is not permitted to create a socket in a directory which we are not
+// permitted to execute, or create files in.
+function test_create_permission() {
+ let dirName = do_get_tempdir();
+ dirName.append("unfriendly");
+
+ let socketName = dirName.clone();
+ socketName.append("socket");
+
+ // The test harness has difficulty cleaning things up if we don't make
+ // everything writable before we're done.
+ try {
+ // Create a directory which we are not permitted to search.
+ dirName.create(Ci.nsIFile.DIRECTORY_TYPE, 0);
+
+ // Try to create a socket in that directory. Because Linux returns EACCES
+ // when a 'connect' fails because of a local firewall rule,
+ // nsIServerSocket returns NS_ERROR_CONNECTION_REFUSED in this case.
+ do_check_throws_nsIException(
+ () => new UnixServerSocket(socketName, allPermissions, -1),
+ "NS_ERROR_CONNECTION_REFUSED"
+ );
+
+ // Grant read and execute permission, but not write permission on the directory.
+ dirName.permissions = parseInt("0555", 8);
+
+ // This should also fail; we need write permission.
+ do_check_throws_nsIException(
+ () => new UnixServerSocket(socketName, allPermissions, -1),
+ "NS_ERROR_CONNECTION_REFUSED"
+ );
+ } finally {
+ // Make the directory writable, so the test harness can clean it up.
+ dirName.permissions = allPermissions;
+ }
+
+ // This should succeed, since we now have all the permissions on the
+ // directory we could want.
+ do_check_instanceof(
+ new UnixServerSocket(socketName, allPermissions, -1),
+ Ci.nsIServerSocket
+ );
+
+ run_next_test();
+}
+
+// To connect to a Unix domain socket, we need search permission on the
+// directories containing it, and some kind of permission or other on the
+// socket itself.
+function test_connect_permission() {
+ // This test involves a lot of callbacks, but they're written out so that
+ // the actual control flow proceeds from top to bottom.
+ let log = "";
+
+ // Create a directory which we are permitted to search - at first.
+ let dirName = do_get_tempdir();
+ dirName.append("inhospitable");
+ dirName.create(Ci.nsIFile.DIRECTORY_TYPE, allPermissions);
+
+ let socketName = dirName.clone();
+ socketName.append("socket");
+
+ // Create a server socket in that directory, listening for connections,
+ // and accessible.
+ let server = new UnixServerSocket(socketName, allPermissions, -1);
+ server.asyncListen({
+ onSocketAccepted: socketAccepted,
+ onStopListening: stopListening,
+ });
+
+ // Make the directory unsearchable.
+ dirName.permissions = 0;
+
+ let client3;
+
+ let client1 = socketTransportService.createUnixDomainTransport(socketName);
+ let client1AsyncInput = client1
+ .openInputStream(0, 0, 0)
+ .QueryInterface(Ci.nsIAsyncInputStream);
+ client1AsyncInput.asyncWait(
+ function () {
+ info("called test_connect_permission's client1's onInputStreamReady");
+ log += "1";
+
+ // nsISocketTransport puts off actually creating sockets as long as
+ // possible, so the error doesn't actually show up until this point.
+ do_check_throws_nsIException(
+ () => client1AsyncInput.available(),
+ "NS_ERROR_CONNECTION_REFUSED"
+ );
+
+ client1AsyncInput.close();
+ client1.close(Cr.NS_OK);
+
+ // Make the directory searchable, but make the socket inaccessible.
+ dirName.permissions = allPermissions;
+ socketName.permissions = 0;
+
+ let client2 =
+ socketTransportService.createUnixDomainTransport(socketName);
+ let client2AsyncInput = client2
+ .openInputStream(0, 0, 0)
+ .QueryInterface(Ci.nsIAsyncInputStream);
+ client2AsyncInput.asyncWait(
+ function () {
+ info("called test_connect_permission's client2's onInputStreamReady");
+ log += "2";
+
+ do_check_throws_nsIException(
+ () => client2AsyncInput.available(),
+ "NS_ERROR_CONNECTION_REFUSED"
+ );
+
+ client2AsyncInput.close();
+ client2.close(Cr.NS_OK);
+
+ // Now make everything accessible, and try one last time.
+ socketName.permissions = allPermissions;
+
+ client3 =
+ socketTransportService.createUnixDomainTransport(socketName);
+
+ let client3Output = client3.openOutputStream(0, 0, 0);
+ client3Output.write("Hanratty", 8);
+
+ let client3AsyncInput = client3
+ .openInputStream(0, 0, 0)
+ .QueryInterface(Ci.nsIAsyncInputStream);
+ client3AsyncInput.asyncWait(
+ client3InputStreamReady,
+ 0,
+ 0,
+ threadManager.currentThread
+ );
+ },
+ 0,
+ 0,
+ threadManager.currentThread
+ );
+ },
+ 0,
+ 0,
+ threadManager.currentThread
+ );
+
+ function socketAccepted(aServ, aTransport) {
+ info("called test_connect_permission's onSocketAccepted");
+ log += "a";
+
+ let serverInput = aTransport
+ .openInputStream(0, 0, 0)
+ .QueryInterface(Ci.nsIAsyncInputStream);
+ let serverOutput = aTransport.openOutputStream(0, 0, 0);
+
+ serverInput.asyncWait(
+ function (aStream) {
+ info(
+ "called test_connect_permission's socketAccepted's onInputStreamReady"
+ );
+ log += "i";
+
+ // Receive data from the client, and send back a response.
+ let serverScriptableInput = new ScriptableInputStream(serverInput);
+ Assert.equal(serverScriptableInput.readBytes(8), "Hanratty");
+ serverOutput.write("Ferlingatti", 11);
+ },
+ 0,
+ 0,
+ threadManager.currentThread
+ );
+ }
+
+ function client3InputStreamReady(aStream) {
+ info("called client3's onInputStreamReady");
+ log += "3";
+
+ let client3Input = new ScriptableInputStream(aStream);
+
+ Assert.equal(client3Input.readBytes(11), "Ferlingatti");
+
+ client3.close(Cr.NS_OK);
+ server.close();
+ }
+
+ function stopListening(aServ, aStatus) {
+ info("called test_connect_permission's server's stopListening");
+ log += "s";
+
+ Assert.equal(log, "12ai3s");
+
+ run_next_test();
+ }
+}
+
+// Creating a socket with a long filename doesn't crash.
+function test_long_socket_name() {
+ let socketName = do_get_tempdir();
+ socketName.append(new Array(10000).join("long"));
+
+ // Try to create a server socket with the long name.
+ do_check_throws_nsIException(
+ () => new UnixServerSocket(socketName, allPermissions, -1),
+ "NS_ERROR_FILE_NAME_TOO_LONG"
+ );
+
+ // Try to connect to a socket with the long name.
+ do_check_throws_nsIException(
+ () => socketTransportService.createUnixDomainTransport(socketName),
+ "NS_ERROR_FILE_NAME_TOO_LONG"
+ );
+
+ run_next_test();
+}
+
+// Going offline should not shut down Unix domain sockets.
+function test_keep_when_offline() {
+ let log = "";
+
+ let socketName = do_get_tempdir();
+ socketName.append("keep-when-offline");
+
+ // Create a listening socket.
+ let listener = new UnixServerSocket(socketName, allPermissions, -1);
+ listener.asyncListen({ onSocketAccepted: onAccepted, onStopListening });
+
+ // Connect a client socket to the listening socket.
+ let client = socketTransportService.createUnixDomainTransport(socketName);
+ let clientOutput = client.openOutputStream(0, 0, 0);
+ let clientInput = client.openInputStream(0, 0, 0);
+ clientInput.asyncWait(clientReady, 0, 0, threadManager.currentThread);
+ let clientScriptableInput = new ScriptableInputStream(clientInput);
+
+ let server, serverInput, serverScriptableInput, serverOutput;
+
+ // How many times has the server invited the client to go first?
+ let count = 0;
+
+ // The server accepted connection callback.
+ function onAccepted(aListener, aServer) {
+ info("test_keep_when_offline: onAccepted called");
+ log += "a";
+ Assert.equal(aListener, listener);
+ server = aServer;
+
+ // Prepare to receive messages from the client.
+ serverInput = server.openInputStream(0, 0, 0);
+ serverInput.asyncWait(serverReady, 0, 0, threadManager.currentThread);
+ serverScriptableInput = new ScriptableInputStream(serverInput);
+
+ // Start a conversation with the client.
+ serverOutput = server.openOutputStream(0, 0, 0);
+ serverOutput.write("After you, Alphonse!", 20);
+ count++;
+ }
+
+ // The client has seen its end of the socket close.
+ function clientReady(aStream) {
+ log += "c";
+ info("test_keep_when_offline: clientReady called: " + log);
+ Assert.equal(aStream, clientInput);
+
+ // If the connection has been closed, end the conversation and stop listening.
+ let available;
+ try {
+ available = clientInput.available();
+ } catch (ex) {
+ do_check_instanceof(ex, Ci.nsIException);
+ Assert.equal(ex.result, Cr.NS_BASE_STREAM_CLOSED);
+
+ info("client received end-of-stream; closing client output stream");
+ log += ")";
+
+ client.close(Cr.NS_OK);
+
+ // Now both output streams have been closed, and both input streams
+ // have received the close notification. Stop listening for
+ // connections.
+ listener.close();
+ }
+
+ if (available) {
+ // Check the message from the server.
+ Assert.equal(clientScriptableInput.readBytes(20), "After you, Alphonse!");
+
+ // Write our response to the server.
+ clientOutput.write("No, after you, Gaston!", 22);
+
+ // Ask to be called again, when more input arrives.
+ clientInput.asyncWait(clientReady, 0, 0, threadManager.currentThread);
+ }
+ }
+
+ function serverReady(aStream) {
+ log += "s";
+ info("test_keep_when_offline: serverReady called: " + log);
+ Assert.equal(aStream, serverInput);
+
+ // Check the message from the client.
+ Assert.equal(serverScriptableInput.readBytes(22), "No, after you, Gaston!");
+
+ // This should not shut things down: Unix domain sockets should
+ // remain open in offline mode.
+ if (count == 5) {
+ Services.io.offline = true;
+ log += "o";
+ }
+
+ if (count < 10) {
+ // Insist.
+ serverOutput.write("After you, Alphonse!", 20);
+ count++;
+
+ // As long as the input stream is open, always ask to be called again
+ // when more input arrives.
+ serverInput.asyncWait(serverReady, 0, 0, threadManager.currentThread);
+ } else if (count == 10) {
+ // After sending ten times and receiving ten replies, we're not
+ // going to send any more. Close the server's output stream; the
+ // client's input stream should see this.
+ info("closing server transport");
+ server.close(Cr.NS_OK);
+ log += "(";
+ }
+ }
+
+ // We have stopped listening.
+ function onStopListening(aServ, aStatus) {
+ info("test_keep_when_offline: onStopListening called");
+ log += "L";
+ Assert.equal(log, "acscscscscsocscscscscs(c)L");
+
+ Assert.equal(aServ, listener);
+ Assert.equal(aStatus, Cr.NS_BINDING_ABORTED);
+
+ run_next_test();
+ }
+}
+
+function test_abstract_address_socket() {
+ const socketname = "abstractsocket";
+ let server = new UnixAbstractServerSocket(socketname, -1);
+ server.asyncListen({
+ onSocketAccepted: (aServ, aTransport) => {
+ let serverInput = aTransport
+ .openInputStream(0, 0, 0)
+ .QueryInterface(Ci.nsIAsyncInputStream);
+ let serverOutput = aTransport.openOutputStream(0, 0, 0);
+
+ serverInput.asyncWait(
+ aStream => {
+ info(
+ "called test_abstract_address_socket's onSocketAccepted's onInputStreamReady"
+ );
+
+ // Receive data from the client, and send back a response.
+ let serverScriptableInput = new ScriptableInputStream(serverInput);
+ Assert.equal(serverScriptableInput.readBytes(9), "ping ping");
+ serverOutput.write("pong", 4);
+ },
+ 0,
+ 0,
+ threadManager.currentThread
+ );
+ },
+ onStopListening: (aServ, aTransport) => {},
+ });
+
+ let client =
+ socketTransportService.createUnixDomainAbstractAddressTransport(socketname);
+ Assert.equal(client.host, socketname);
+ Assert.equal(client.port, 0);
+ let clientInput = client
+ .openInputStream(0, 0, 0)
+ .QueryInterface(Ci.nsIAsyncInputStream);
+ let clientOutput = client.openOutputStream(0, 0, 0);
+
+ clientOutput.write("ping ping", 9);
+
+ clientInput.asyncWait(
+ aStream => {
+ let clientScriptInput = new ScriptableInputStream(clientInput);
+ let available = clientScriptInput.available();
+ if (available) {
+ Assert.equal(clientScriptInput.readBytes(4), "pong");
+
+ client.close(Cr.NS_OK);
+ server.close(Cr.NS_OK);
+
+ run_next_test();
+ }
+ },
+ 0,
+ 0,
+ threadManager.currentThread
+ );
+}
diff --git a/netwerk/test/unit/test_uri_mutator.js b/netwerk/test/unit/test_uri_mutator.js
new file mode 100644
index 0000000000..fb9228fc5b
--- /dev/null
+++ b/netwerk/test/unit/test_uri_mutator.js
@@ -0,0 +1,48 @@
+/* -*- 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";
+
+function standardMutator() {
+ return Cc["@mozilla.org/network/standard-url-mutator;1"].createInstance(
+ Ci.nsIURIMutator
+ );
+}
+
+add_task(async function test_simple_setter_chaining() {
+ let uri = standardMutator()
+ .setSpec("http://example.com/")
+ .setQuery("hello")
+ .setRef("bla")
+ .setScheme("ftp")
+ .finalize();
+ equal(uri.spec, "ftp://example.com/?hello#bla");
+});
+
+add_task(async function test_qi_behaviour() {
+ let uri = standardMutator()
+ .setSpec("http://example.com/")
+ .QueryInterface(Ci.nsIURI);
+ equal(uri.spec, "http://example.com/");
+
+ Assert.throws(
+ () => {
+ uri = standardMutator().QueryInterface(Ci.nsIURI);
+ },
+ /NS_NOINTERFACE/,
+ "mutator doesn't QI if it holds no URI"
+ );
+
+ let mutator = standardMutator().setSpec("http://example.com/path");
+ uri = mutator.QueryInterface(Ci.nsIURI);
+ equal(uri.spec, "http://example.com/path");
+ Assert.throws(
+ () => {
+ uri = mutator.QueryInterface(Ci.nsIURI);
+ },
+ /NS_NOINTERFACE/,
+ "Second QueryInterface should fail"
+ );
+});
diff --git a/netwerk/test/unit/test_use_httpssvc.js b/netwerk/test/unit/test_use_httpssvc.js
new file mode 100644
index 0000000000..31174b3374
--- /dev/null
+++ b/netwerk/test/unit/test_use_httpssvc.js
@@ -0,0 +1,240 @@
+/* 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 h2Port;
+
+const certOverrideService = Cc[
+ "@mozilla.org/security/certoverride;1"
+].getService(Ci.nsICertOverrideService);
+const { TestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TestUtils.sys.mjs"
+);
+
+add_setup(async function setup() {
+ trr_test_setup();
+
+ h2Port = Services.env.get("MOZHTTP2_PORT");
+ Assert.notEqual(h2Port, null);
+ Assert.notEqual(h2Port, "");
+
+ Services.prefs.setBoolPref("network.dns.upgrade_with_https_rr", true);
+ Services.prefs.setBoolPref("network.dns.use_https_rr_as_altsvc", true);
+
+ registerCleanupFunction(() => {
+ trr_clear_prefs();
+ Services.prefs.clearUserPref("network.dns.upgrade_with_https_rr");
+ Services.prefs.clearUserPref("network.dns.use_https_rr_as_altsvc");
+ });
+
+ if (mozinfo.socketprocess_networking) {
+ Services.dns; // Needed to trigger socket process.
+ await TestUtils.waitForCondition(() => Services.io.socketProcessLaunched);
+ }
+
+ Services.prefs.setIntPref("network.trr.mode", 2); // TRR first
+});
+
+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));
+ });
+}
+
+// This is for testing when the HTTPSSVC record is not available when
+// the transaction is added in connection manager.
+add_task(async function testUseHTTPSSVCForHttpsUpgrade() {
+ // use the h2 server as DOH provider
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ "https://foo.example.com:" + h2Port + "/httpssvc_as_altsvc"
+ );
+ Services.dns.clearCache(true);
+
+ certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData(
+ true
+ );
+
+ let chan = makeChan(`https://test.httpssvc.com:8080/`);
+ let [req] = await channelOpenPromise(chan);
+ Assert.equal(req.getResponseHeader("x-connection-http2"), "yes");
+
+ certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData(
+ false
+ );
+});
+
+class EventSinkListener {
+ getInterface(iid) {
+ if (iid.equals(Ci.nsIChannelEventSink)) {
+ return this;
+ }
+ throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE);
+ }
+ 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() {
+ // use the h2 server as DOH provider
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ "https://foo.example.com:" + h2Port + "/httpssvc_as_altsvc"
+ );
+ Services.dns.clearCache(true);
+
+ certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData(
+ true
+ );
+
+ // At this time, the DataStorage is not ready, so MaybeUseHTTPSRRForUpgrade()
+ // is called from the callback of NS_ShouldSecureUpgrade().
+ let chan = makeChan(`http://test.httpssvc.com:80/`);
+ let listener = new EventSinkListener();
+ chan.notificationCallbacks = listener;
+
+ let [req] = await channelOpenPromise(chan);
+
+ req.QueryInterface(Ci.nsIHttpChannel);
+ Assert.equal(req.getResponseHeader("x-connection-http2"), "yes");
+
+ // At this time, the DataStorage is ready, so MaybeUseHTTPSRRForUpgrade()
+ // is called from nsHttpChannel::OnBeforeConnect().
+ chan = makeChan(`http://test.httpssvc.com:80/`);
+ listener = new EventSinkListener();
+ chan.notificationCallbacks = listener;
+
+ [req] = await channelOpenPromise(chan);
+
+ req.QueryInterface(Ci.nsIHttpChannel);
+ Assert.equal(req.getResponseHeader("x-connection-http2"), "yes");
+
+ certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData(
+ false
+ );
+});
+
+// This is for testing when the HTTPSSVC record is already available before
+// the transaction is added in connection manager.
+add_task(async function testUseHTTPSSVC() {
+ // use the h2 server as DOH provider
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ "https://foo.example.com:" + h2Port + "/httpssvc_as_altsvc"
+ );
+
+ // Do DNS resolution before creating the channel, so the HTTPSSVC record will
+ // be resolved from the cache.
+ await new TRRDNSListener("test.httpssvc.com", {
+ type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC,
+ });
+
+ // We need to skip the security check, since our test cert is signed for
+ // foo.example.com, not test.httpssvc.com.
+ certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData(
+ true
+ );
+
+ let chan = makeChan(`https://test.httpssvc.com:8888`);
+ let [req] = await channelOpenPromise(chan);
+ // Test if this request is done by h2.
+ Assert.equal(req.getResponseHeader("x-connection-http2"), "yes");
+
+ certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData(
+ false
+ );
+});
+
+// Test if we can successfully fallback to the original host and port.
+add_task(async function testFallback() {
+ 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.fallback.com", "A", {
+ answers: [
+ {
+ name: "test.fallback.com",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "127.0.0.1",
+ },
+ ],
+ });
+ // Use a wrong port number 8888, so this connection will be refused.
+ await trrServer.registerDoHAnswers("test.fallback.com", "HTTPS", {
+ answers: [
+ {
+ name: "test.fallback.com",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 1,
+ name: "foo.example.com",
+ values: [{ key: "port", value: 8888 }],
+ },
+ },
+ ],
+ });
+
+ let { inRecord } = await new TRRDNSListener("test.fallback.com", {
+ type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC,
+ });
+
+ let record = inRecord
+ .QueryInterface(Ci.nsIDNSHTTPSSVCRecord)
+ .GetServiceModeRecord(false, false);
+ Assert.equal(record.priority, 1);
+ Assert.equal(record.name, "foo.example.com");
+
+ certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData(
+ true
+ );
+
+ // When the connection with port 8888 failed, the correct h2Port will be used
+ // to connect again.
+ let chan = makeChan(`https://test.fallback.com:${h2Port}`);
+ let [req] = await channelOpenPromise(chan);
+ // Test if this request is done by h2.
+ Assert.equal(req.getResponseHeader("x-connection-http2"), "yes");
+
+ certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData(
+ false
+ );
+});
diff --git a/netwerk/test/unit/test_verify_traffic.js b/netwerk/test/unit/test_verify_traffic.js
new file mode 100644
index 0000000000..be41223642
--- /dev/null
+++ b/netwerk/test/unit/test_verify_traffic.js
@@ -0,0 +1,110 @@
+/* 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 */
+/* import-globals-from head_servers.js */
+
+function makeChan(uri) {
+ let chan = NetUtil.newChannel({
+ uri,
+ 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]);
+ }
+ chan.asyncOpen(new ChannelListener(finish, null, flags));
+ });
+}
+
+async function registerSimplePathHandler(server, path) {
+ return server.registerPathHandler(path, (req, resp) => {
+ resp.writeHead(200);
+ resp.end("done");
+ });
+}
+
+add_task(async function test_verify_traffic_for_http2() {
+ Services.prefs.setBoolPref(
+ "network.http.http2.move_to_pending_list_after_network_change",
+ true
+ );
+
+ // Bug 1878505: It seems when HTTPS RR is enabled, a speculative
+ // connection that waits to receive a HTTPS response will receive it
+ // after the actual connection is established, leading to an extra
+ // connection being established.
+ Services.prefs.setIntPref("network.http.speculative-parallel-limit", 0);
+
+ let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(
+ Ci.nsIX509CertDB
+ );
+ addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u");
+
+ let server = new NodeHTTP2Server();
+ await server.start();
+ registerCleanupFunction(async () => {
+ Services.prefs.clearUserPref(
+ "network.http.http2.move_to_pending_list_after_network_change"
+ );
+ await server.stop();
+ });
+
+ try {
+ await server.registerPathHandler("/longDelay", (req, resp) => {
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout, no-undef
+ setTimeout(function () {
+ resp.writeHead(200);
+ resp.end("done");
+ }, 8000);
+ });
+ } catch (e) {}
+
+ await registerSimplePathHandler(server, "/test");
+
+ // Send some requests and check if we have only one h2 session.
+ for (let i = 0; i < 2; i++) {
+ let chan = makeChan(`https://localhost:${server.port()}/test`);
+ await channelOpenPromise(chan, CL_ALLOW_UNKNOWN_CL);
+ }
+ let sessionCount = await server.sessionCount();
+ Assert.equal(sessionCount, 1);
+
+ let res = await new Promise(resolve => {
+ // Create a request that takes 8s to finish.
+ let chan = makeChan(`https://localhost:${server.port()}/longDelay`);
+ chan.asyncOpen(new ChannelListener(resolve, null, CL_ALLOW_UNKNOWN_CL));
+
+ // Send a network change event to trigger VerifyTraffic(). After this,
+ // the connnection will be put in the pending list.
+ // We'll crate a new connection for the new request.
+ Services.obs.notifyObservers(
+ null,
+ "network:link-status-changed",
+ "changed"
+ );
+
+ // This request will use the new connection.
+ let chan1 = makeChan(`https://localhost:${server.port()}/test`);
+ chan1.asyncOpen(new ChannelListener(() => {}, null, CL_ALLOW_UNKNOWN_CL));
+ });
+
+ // The connection in the pending list should be still working.
+ Assert.equal(res.status, Cr.NS_OK);
+ Assert.equal(res.QueryInterface(Ci.nsIHttpChannel).responseStatus, 200);
+
+ sessionCount = await server.sessionCount();
+ Assert.equal(sessionCount, 2);
+
+ await server.stop();
+});
diff --git a/netwerk/test/unit/test_websocket_500k.js b/netwerk/test/unit/test_websocket_500k.js
new file mode 100644
index 0000000000..94a324cca0
--- /dev/null
+++ b/netwerk/test/unit/test_websocket_500k.js
@@ -0,0 +1,222 @@
+/* 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 */
+/* import-globals-from head_servers.js */
+
+ChromeUtils.defineESModuleGetters(this, {
+ ObjectUtils: "resource://gre/modules/ObjectUtils.sys.mjs",
+});
+
+add_setup(async function () {
+ Services.prefs.setCharPref("network.dns.localDomains", "foo.example.com");
+
+ let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(
+ Ci.nsIX509CertDB
+ );
+ addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u");
+ addCertFromFile(certdb, "proxy-ca.pem", "CTu,u,u");
+});
+
+registerCleanupFunction(async () => {
+ Services.prefs.clearUserPref("network.dns.localDomains");
+});
+
+async function channelOpenPromise(url, msg) {
+ let conn = new WebSocketConnection();
+ await conn.open(url);
+ conn.send(msg);
+ let res = await conn.receiveMessages();
+ conn.close();
+ let { status } = await conn.finished();
+ return [status, res];
+}
+
+async function sendDataAndCheck(url) {
+ let data = "a".repeat(500000);
+ let [status, res] = await channelOpenPromise(url, data);
+ Assert.equal(status, Cr.NS_OK);
+ // Use "ObjectUtils.deepEqual" directly to avoid printing data.
+ Assert.ok(ObjectUtils.deepEqual(res, [data]));
+}
+
+add_task(async function test_h2_websocket_500k() {
+ Services.prefs.setBoolPref("network.http.http2.websockets", true);
+ let wss = new NodeWebSocketHttp2Server();
+ await wss.start();
+ registerCleanupFunction(async () => wss.stop());
+
+ Assert.notEqual(wss.port(), null);
+ await wss.registerMessageHandler((data, ws) => {
+ ws.send(data);
+ });
+ let url = `wss://foo.example.com:${wss.port()}`;
+ await sendDataAndCheck(url);
+});
+
+// h1.1 direct
+add_task(async function test_h1_websocket_direct() {
+ let wss = new NodeWebSocketServer();
+ await wss.start();
+ registerCleanupFunction(async () => wss.stop());
+ Assert.notEqual(wss.port(), null);
+ await wss.registerMessageHandler((data, ws) => {
+ ws.send(data);
+ });
+ let url = `wss://localhost:${wss.port()}`;
+ await sendDataAndCheck(url);
+});
+
+// ws h1.1 with insecure h1.1 proxy
+add_task(async function test_h1_ws_with_h1_insecure_proxy() {
+ Services.prefs.setBoolPref("network.http.http2.websockets", false);
+ let proxy = new NodeHTTPProxyServer();
+ await proxy.start();
+
+ let wss = new NodeWebSocketServer();
+ await wss.start();
+
+ registerCleanupFunction(async () => {
+ await wss.stop();
+ await proxy.stop();
+ });
+
+ Assert.notEqual(wss.port(), null);
+
+ await wss.registerMessageHandler((data, ws) => {
+ ws.send(data);
+ });
+ let url = `wss://localhost:${wss.port()}`;
+ await sendDataAndCheck(url);
+});
+
+// h1 server with secure h1.1 proxy
+add_task(async function test_h1_ws_with_secure_h1_proxy() {
+ let proxy = new NodeHTTPSProxyServer();
+ await proxy.start();
+
+ let wss = new NodeWebSocketServer();
+ await wss.start();
+ registerCleanupFunction(async () => {
+ await wss.stop();
+ await proxy.stop();
+ });
+
+ Assert.notEqual(wss.port(), null);
+ await wss.registerMessageHandler((data, ws) => {
+ ws.send(data);
+ });
+
+ let url = `wss://localhost:${wss.port()}`;
+ await sendDataAndCheck(url);
+
+ await proxy.stop();
+});
+
+// ws h1.1 with h2 proxy
+add_task(async function test_h1_ws_with_h2_proxy() {
+ Services.prefs.setBoolPref("network.http.http2.websockets", false);
+
+ let proxy = new NodeHTTP2ProxyServer();
+ await proxy.start();
+
+ let wss = new NodeWebSocketServer();
+ await wss.start();
+
+ registerCleanupFunction(async () => {
+ await wss.stop();
+ await proxy.stop();
+ });
+
+ Assert.notEqual(wss.port(), null);
+ await wss.registerMessageHandler((data, ws) => {
+ ws.send(data);
+ });
+
+ let url = `wss://localhost:${wss.port()}`;
+ await sendDataAndCheck(url);
+
+ await proxy.stop();
+});
+
+// ws h2 with insecure h1.1 proxy
+add_task(async function test_h2_ws_with_h1_insecure_proxy() {
+ Services.prefs.setBoolPref("network.http.http2.websockets", true);
+
+ let proxy = new NodeHTTPProxyServer();
+ await proxy.start();
+
+ let wss = new NodeWebSocketHttp2Server();
+ await wss.start();
+
+ registerCleanupFunction(async () => {
+ await wss.stop();
+ await proxy.stop();
+ });
+
+ Assert.notEqual(wss.port(), null);
+ await wss.registerMessageHandler((data, ws) => {
+ ws.send(data);
+ });
+
+ let url = `wss://localhost:${wss.port()}`;
+ await sendDataAndCheck(url);
+
+ await proxy.stop();
+});
+
+add_task(async function test_h2_ws_with_h1_secure_proxy() {
+ Services.prefs.setBoolPref("network.http.http2.websockets", true);
+
+ let proxy = new NodeHTTPSProxyServer();
+ await proxy.start();
+
+ let wss = new NodeWebSocketHttp2Server();
+ await wss.start();
+
+ registerCleanupFunction(async () => {
+ await wss.stop();
+ await proxy.stop();
+ });
+
+ Assert.notEqual(wss.port(), null);
+ await wss.registerMessageHandler((data, ws) => {
+ ws.send(data);
+ });
+
+ let url = `wss://localhost:${wss.port()}`;
+ await sendDataAndCheck(url);
+
+ await proxy.stop();
+});
+
+// ws h2 with secure h2 proxy
+add_task(async function test_h2_ws_with_h2_proxy() {
+ Services.prefs.setBoolPref("network.http.http2.websockets", true);
+
+ let proxy = new NodeHTTP2ProxyServer();
+ await proxy.start(); // start and register proxy "filter"
+
+ let wss = new NodeWebSocketHttp2Server();
+ await wss.start(); // init port
+
+ registerCleanupFunction(async () => {
+ await wss.stop();
+ await proxy.stop();
+ });
+
+ Assert.notEqual(wss.port(), null);
+ await wss.registerMessageHandler((data, ws) => {
+ ws.send(data);
+ });
+
+ let url = `wss://localhost:${wss.port()}`;
+ await sendDataAndCheck(url);
+
+ await proxy.stop();
+});
diff --git a/netwerk/test/unit/test_websocket_fails.js b/netwerk/test/unit/test_websocket_fails.js
new file mode 100644
index 0000000000..9acb1bfcd2
--- /dev/null
+++ b/netwerk/test/unit/test_websocket_fails.js
@@ -0,0 +1,194 @@
+/* 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 */
+/* import-globals-from head_servers.js */
+/* import-globals-from head_websocket.js */
+
+var CC = Components.Constructor;
+const ServerSocket = CC(
+ "@mozilla.org/network/server-socket;1",
+ "nsIServerSocket",
+ "init"
+);
+
+let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(
+ Ci.nsIX509CertDB
+);
+
+add_setup(() => {
+ Services.prefs.setBoolPref("network.http.http2.websockets", true);
+});
+
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("network.http.http2.websockets");
+});
+
+// TLS handshake to the end server fails - no proxy
+async function test_tls_fail_on_direct_ws_server_handshake() {
+ // no cert and no proxy
+ let wss = new NodeWebSocketServer();
+ await wss.start();
+ registerCleanupFunction(async () => {
+ await wss.stop();
+ });
+
+ Assert.notEqual(wss.port(), null);
+
+ let chan = makeWebSocketChan();
+ let url = `wss://localhost:${wss.port()}`;
+ const msg = "test tls handshake with direct ws server fails";
+ let [status] = await openWebSocketChannelPromise(chan, url, msg);
+
+ // can be two errors, seems to be a race between:
+ // * overwriting the WebSocketChannel status with NS_ERROR_NET_RESET and
+ // * getting the original 805A1FF3 // SEC_ERROR_UNKNOWN_ISSUER
+ if (status == 2152398930) {
+ Assert.equal(status, 0x804b0052); // NS_ERROR_NET_INADEQUATE_SECURITY
+ } else {
+ // occasionally this happens
+ Assert.equal(status, 0x804b0057); // NS_ERROR_WEBSOCKET_CONNECTION_REFUSED
+ }
+}
+
+// TLS handshake to proxy fails
+async function test_tls_fail_on_proxy_handshake() {
+ // we have ws cert, but no proxy cert
+ addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u");
+
+ let proxy = new NodeHTTPSProxyServer();
+ await proxy.start();
+
+ let wss = new NodeWebSocketServer();
+ await wss.start();
+
+ registerCleanupFunction(async () => {
+ await wss.stop();
+ await proxy.stop();
+ });
+
+ Assert.notEqual(wss.port(), null);
+
+ let chan = makeWebSocketChan();
+ let url = `wss://localhost:${wss.port()}`;
+ const msg = "test tls failure on proxy handshake";
+ let [status] = await openWebSocketChannelPromise(chan, url, msg);
+
+ // see above test for details on why 2 cases here
+ if (status == 2152398930) {
+ Assert.equal(status, 0x804b0052); // NS_ERROR_NET_INADEQUATE_SECURITY
+ } else {
+ Assert.equal(status, 0x804b0057); // NS_ERROR_WEBSOCKET_CONNECTION_REFUSED
+ }
+
+ await proxy.stop();
+}
+
+// the ws server does not respond (closed port)
+async function test_non_responsive_ws_server_closed_port() {
+ // ws server cert already added in previous test
+
+ // no ws server listening (closed port)
+ let randomPort = 666; // "random" port
+ let chan = makeWebSocketChan();
+ let url = `wss://localhost:${randomPort}`;
+ const msg = "test non-responsive ws server closed port";
+ let [status] = await openWebSocketChannelPromise(chan, url, msg);
+ Assert.equal(status, 0x804b0057); // NS_ERROR_WEBSOCKET_CONNECTION_REFUSED
+}
+
+// no ws response from server (ie. no ws server, use tcp server to open port)
+async function test_non_responsive_ws_server_open_port() {
+ // we are expecting the timeout in this test, so lets shorten to 1s
+ Services.prefs.setIntPref("network.websocket.timeout.open", 1);
+
+ // ws server cert already added in previous test
+
+ // use a tcp server to test open port, not a ws server
+ var server = ServerSocket(-1, true, -1); // port, loopback, default-backlog
+ var port = server.port;
+ info("server: listening on " + server.port);
+ server.asyncListen({});
+
+ // queue cleanup after all tests
+ registerCleanupFunction(() => {
+ server.close();
+ Services.prefs.clearUserPref("network.websocket.timeout.open");
+ });
+
+ // try ws connection
+ let chan = makeWebSocketChan();
+ let url = `wss://localhost:${port}`;
+ const msg = "test non-responsive ws server open port";
+ let [status] = await openWebSocketChannelPromise(chan, url, msg);
+ Assert.equal(status, Cr.NS_ERROR_NET_TIMEOUT_EXTERNAL); // we will timeout
+ Services.prefs.clearUserPref("network.websocket.timeout.open");
+}
+
+// proxy does not respond
+async function test_proxy_doesnt_respond() {
+ Services.prefs.setIntPref("network.websocket.timeout.open", 1);
+ Services.prefs.setBoolPref("network.http.http2.websockets", false);
+ // ws cert added in previous test, add proxy cert
+ addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u");
+ addCertFromFile(certdb, "proxy-ca.pem", "CTu,u,u");
+
+ info("spinning up proxy");
+ let proxy = new NodeHTTPSProxyServer();
+ await proxy.start();
+
+ // route traffic through non-existant proxy
+ const pps = Cc["@mozilla.org/network/protocol-proxy-service;1"].getService();
+ let randomPort = proxy.port() + 1;
+ var filter = new NodeProxyFilter(
+ proxy.protocol(),
+ "localhost",
+ randomPort,
+ 0
+ );
+ pps.registerFilter(filter, 10);
+
+ registerCleanupFunction(async () => {
+ await proxy.stop();
+ Services.prefs.clearUserPref("network.websocket.timeout.open");
+ });
+
+ // setup the websocket server
+ info("spinning up websocket server");
+ let wss = new NodeWebSocketServer();
+ await wss.start();
+ registerCleanupFunction(() => {
+ wss.stop();
+ });
+ Assert.notEqual(wss.port(), null);
+ await wss.registerMessageHandler((data, ws) => {
+ ws.send(data);
+ });
+
+ info("creating and connecting websocket");
+ let url = `wss://localhost:${wss.port()}`;
+ let conn = new WebSocketConnection();
+ conn.open(url); // do not await, we don't expect a fully opened channel
+
+ // check proxy info
+ info("checking proxy info");
+ let proxyInfoPromise = conn.getProxyInfo();
+ let proxyInfo = await proxyInfoPromise;
+ Assert.equal(proxyInfo.type, "https"); // let's be sure that failure is not "direct"
+
+ // we fail to connect via proxy, as expected
+ let { status } = await conn.finished();
+ info("stats: " + status);
+ Assert.equal(status, 0x804b0057); // NS_ERROR_WEBSOCKET_CONNECTION_REFUSED
+}
+
+add_task(test_tls_fail_on_direct_ws_server_handshake);
+add_task(test_tls_fail_on_proxy_handshake);
+add_task(test_non_responsive_ws_server_closed_port);
+add_task(test_non_responsive_ws_server_open_port);
+add_task(test_proxy_doesnt_respond);
diff --git a/netwerk/test/unit/test_websocket_fails_2.js b/netwerk/test/unit/test_websocket_fails_2.js
new file mode 100644
index 0000000000..57c9b321ad
--- /dev/null
+++ b/netwerk/test/unit/test_websocket_fails_2.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";
+
+/* import-globals-from head_cache.js */
+/* import-globals-from head_cookies.js */
+/* import-globals-from head_channels.js */
+/* import-globals-from head_servers.js */
+/* import-globals-from head_websocket.js */
+
+let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(
+ Ci.nsIX509CertDB
+);
+
+add_setup(() => {
+ Services.prefs.setBoolPref("network.http.http2.websockets", true);
+});
+
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("network.http.http2.websockets");
+});
+
+// TLS handshake to the end server fails with proxy
+async function test_tls_fail_on_ws_server_over_proxy() {
+ // we are expecting a timeout, so lets shorten how long we must wait
+ Services.prefs.setIntPref("network.websocket.timeout.open", 1);
+
+ // no cert to ws server
+ addCertFromFile(certdb, "proxy-ca.pem", "CTu,u,u");
+
+ let proxy = new NodeHTTPSProxyServer();
+ await proxy.start();
+
+ let wss = new NodeWebSocketServer();
+ await wss.start();
+
+ registerCleanupFunction(async () => {
+ await wss.stop();
+ await proxy.stop();
+ Services.prefs.clearUserPref("network.websocket.timeout.open");
+ });
+
+ Assert.notEqual(wss.port(), null);
+ await wss.registerMessageHandler((data, ws) => {
+ ws.send(data);
+ });
+
+ let chan = makeWebSocketChan();
+ let url = `wss://localhost:${wss.port()}`;
+ const msg = "test tls fail on ws server over proxy";
+ let [status] = await openWebSocketChannelPromise(chan, url, msg);
+
+ Assert.equal(status, Cr.NS_ERROR_NET_TIMEOUT_EXTERNAL);
+}
+add_task(test_tls_fail_on_ws_server_over_proxy);
diff --git a/netwerk/test/unit/test_websocket_offline.js b/netwerk/test/unit/test_websocket_offline.js
new file mode 100644
index 0000000000..1f13879dbc
--- /dev/null
+++ b/netwerk/test/unit/test_websocket_offline.js
@@ -0,0 +1,51 @@
+/* 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";
+
+// checking to make sure we don't hang as per 1038304
+// offline so url isn't impt
+var url = "ws://localhost";
+var chan;
+var offlineStatus;
+
+var listener = {
+ onAcknowledge(aContext, aSize) {},
+ onBinaryMessageAvailable(aContext, aMsg) {},
+ onMessageAvailable(aContext, aMsg) {},
+ onServerClose(aContext, aCode, aReason) {},
+ onStart(aContext) {
+ // onStart is not called when a connection fails
+ Assert.ok(false);
+ },
+ onStop(aContext, aStatusCode) {
+ Assert.notEqual(aStatusCode, Cr.NS_OK);
+ Services.io.offline = offlineStatus;
+ do_test_finished();
+ },
+};
+
+function run_test() {
+ offlineStatus = Services.io.offline;
+ Services.io.offline = true;
+
+ try {
+ 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 = Services.io.newURI(url);
+ chan.asyncOpen(uri, url, {}, 0, listener, null);
+ do_test_pending();
+ } catch (x) {
+ dump("throwing " + x);
+ do_throw(x);
+ }
+}
diff --git a/netwerk/test/unit/test_websocket_server.js b/netwerk/test/unit/test_websocket_server.js
new file mode 100644
index 0000000000..a33413a02f
--- /dev/null
+++ b/netwerk/test/unit/test_websocket_server.js
@@ -0,0 +1,317 @@
+/* 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 */
+/* import-globals-from head_servers.js */
+
+const certOverrideService = Cc[
+ "@mozilla.org/security/certoverride;1"
+].getService(Ci.nsICertOverrideService);
+
+add_setup(async function setup() {
+ certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData(
+ true
+ );
+
+ registerCleanupFunction(async () => {
+ certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData(
+ false
+ );
+ });
+});
+
+function makeChan(uri) {
+ let chan = NetUtil.newChannel({
+ uri,
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+ chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI;
+ return chan;
+}
+
+function httpChannelOpenPromise(chan, flags) {
+ return new Promise(resolve => {
+ function finish(req, buffer) {
+ resolve([req, buffer]);
+ }
+ chan.asyncOpen(new ChannelListener(finish, null, flags));
+ });
+}
+
+async function channelOpenPromise(url, msg) {
+ let conn = new WebSocketConnection();
+ await conn.open(url);
+ conn.send(msg);
+ let res = await conn.receiveMessages();
+ conn.close();
+ let { status } = await conn.finished();
+ return [status, res];
+}
+
+// h1.1 direct
+async function test_h1_websocket_direct() {
+ let wss = new NodeWebSocketServer();
+ await wss.start();
+ registerCleanupFunction(async () => wss.stop());
+ Assert.notEqual(wss.port(), null);
+ await wss.registerMessageHandler((data, ws) => {
+ ws.send(data);
+ });
+ let url = `wss://localhost:${wss.port()}`;
+ const msg = "test websocket";
+
+ let conn = new WebSocketConnection();
+ await conn.open(url);
+ conn.send(msg);
+ let mess1 = await conn.receiveMessages();
+ Assert.deepEqual(mess1, [msg]);
+
+ // Now send 3 more, and check that we received all of them
+ conn.send(msg);
+ conn.send(msg);
+ conn.send(msg);
+ let mess2 = [];
+ while (mess2.length < 3) {
+ // receive could return 1, 2 or all 3 replies.
+ mess2 = mess2.concat(await conn.receiveMessages());
+ }
+ Assert.deepEqual(mess2, [msg, msg, msg]);
+
+ conn.close();
+ let { status } = await conn.finished();
+
+ Assert.equal(status, Cr.NS_OK);
+}
+
+// h1 server with secure h1.1 proxy
+async function test_h1_ws_with_secure_h1_proxy() {
+ let proxy = new NodeHTTPSProxyServer();
+ await proxy.start();
+
+ let wss = new NodeWebSocketServer();
+ await wss.start();
+
+ registerCleanupFunction(async () => {
+ await wss.stop();
+ await proxy.stop();
+ });
+
+ Assert.notEqual(wss.port(), null);
+ await wss.registerMessageHandler((data, ws) => {
+ ws.send(data);
+ });
+
+ let url = `wss://localhost:${wss.port()}`;
+ const msg = "test h1.1 websocket with h1.1 secure proxy";
+ let [status, res] = await channelOpenPromise(url, msg);
+ Assert.equal(status, Cr.NS_OK);
+ Assert.deepEqual(res, [msg]);
+
+ await proxy.stop();
+}
+
+async function test_h2_websocket_direct() {
+ Services.prefs.setBoolPref("network.http.http2.websockets", true);
+ let wss = new NodeWebSocketHttp2Server();
+ await wss.start();
+ registerCleanupFunction(async () => wss.stop());
+
+ Assert.notEqual(wss.port(), null);
+ await wss.registerMessageHandler((data, ws) => {
+ ws.send(data);
+ });
+ let url = `wss://localhost:${wss.port()}`;
+ const msg = "test h2 websocket h2 direct";
+ let [status, res] = await channelOpenPromise(url, msg);
+ Assert.equal(status, Cr.NS_OK);
+ Assert.deepEqual(res, [msg]);
+}
+
+// ws h1.1 with insecure h1.1 proxy
+async function test_h1_ws_with_h1_insecure_proxy() {
+ Services.prefs.setBoolPref("network.http.http2.websockets", false);
+ let proxy = new NodeHTTPProxyServer();
+ await proxy.start();
+
+ let wss = new NodeWebSocketServer();
+ await wss.start();
+
+ registerCleanupFunction(async () => {
+ await wss.stop();
+ await proxy.stop();
+ });
+
+ Assert.notEqual(wss.port(), null);
+
+ await wss.registerMessageHandler((data, ws) => {
+ ws.send(data);
+ });
+ let url = `wss://localhost:${wss.port()}`;
+ const msg = "test h1 websocket with h1 insecure proxy";
+ let [status, res] = await channelOpenPromise(url, msg);
+ Assert.equal(status, Cr.NS_OK);
+ Assert.deepEqual(res, [msg]);
+
+ await proxy.stop();
+}
+
+// ws h1.1 with h2 proxy
+async function test_h1_ws_with_h2_proxy() {
+ Services.prefs.setBoolPref("network.http.http2.websockets", false);
+
+ let proxy = new NodeHTTP2ProxyServer();
+ await proxy.start();
+
+ let wss = new NodeWebSocketServer();
+ await wss.start();
+
+ registerCleanupFunction(async () => {
+ await wss.stop();
+ await proxy.stop();
+ });
+
+ Assert.notEqual(wss.port(), null);
+ await wss.registerMessageHandler((data, ws) => {
+ ws.send(data);
+ });
+
+ let url = `wss://localhost:${wss.port()}`;
+ const msg = "test h1 websocket with h2 proxy";
+ let [status, res] = await channelOpenPromise(url, msg);
+ Assert.equal(status, Cr.NS_OK);
+ Assert.deepEqual(res, [msg]);
+
+ await proxy.stop();
+}
+
+// ws h2 with insecure h1.1 proxy
+async function test_h2_ws_with_h1_insecure_proxy() {
+ Services.prefs.setBoolPref("network.http.http2.websockets", true);
+
+ let proxy = new NodeHTTPProxyServer();
+ await proxy.start();
+
+ registerCleanupFunction(async () => {
+ await wss.stop();
+ await proxy.stop();
+ });
+
+ let wss = new NodeWebSocketHttp2Server();
+ await wss.start();
+ Assert.notEqual(wss.port(), null);
+ await wss.registerMessageHandler((data, ws) => {
+ ws.send(data);
+ });
+
+ let url = `wss://localhost:${wss.port()}`;
+ const msg = "test h2 websocket with h1 insecure proxy";
+ let [status, res] = await channelOpenPromise(url, msg);
+ Assert.equal(status, Cr.NS_OK);
+ Assert.deepEqual(res, [msg]);
+
+ await proxy.stop();
+}
+
+// ws h2 with secure h1 proxy
+async function test_h2_ws_with_h1_secure_proxy() {
+ Services.prefs.setBoolPref("network.http.http2.websockets", true);
+
+ let proxy = new NodeHTTPSProxyServer();
+ await proxy.start();
+
+ let wss = new NodeWebSocketHttp2Server();
+ await wss.start();
+
+ registerCleanupFunction(async () => {
+ await wss.stop();
+ await proxy.stop();
+ });
+
+ Assert.notEqual(wss.port(), null);
+ await wss.registerMessageHandler((data, ws) => {
+ ws.send(data);
+ });
+
+ let url = `wss://localhost:${wss.port()}`;
+ const msg = "test h2 websocket with h1 secure proxy";
+ let [status, res] = await channelOpenPromise(url, msg);
+ Assert.equal(status, Cr.NS_OK);
+ Assert.deepEqual(res, [msg]);
+
+ await proxy.stop();
+}
+
+// ws h2 with secure h2 proxy
+async function test_h2_ws_with_h2_proxy() {
+ Services.prefs.setBoolPref("network.http.http2.websockets", true);
+
+ let proxy = new NodeHTTP2ProxyServer();
+ await proxy.start(); // start and register proxy "filter"
+
+ let wss = new NodeWebSocketHttp2Server();
+ await wss.start(); // init port
+
+ registerCleanupFunction(async () => {
+ await wss.stop();
+ await proxy.stop();
+ });
+
+ Assert.notEqual(wss.port(), null);
+ await wss.registerMessageHandler((data, ws) => {
+ ws.send(data);
+ });
+
+ let url = `wss://localhost:${wss.port()}`;
+ const msg = "test h2 websocket with h2 proxy";
+ let [status, res] = await channelOpenPromise(url, msg);
+ Assert.equal(status, Cr.NS_OK);
+ Assert.deepEqual(res, [msg]);
+
+ await proxy.stop();
+}
+
+async function test_bug_1848013() {
+ Services.prefs.setBoolPref("network.http.http2.websockets", true);
+
+ let proxy = new NodeHTTPProxyServer();
+ await proxy.start();
+
+ registerCleanupFunction(async () => {
+ await wss.stop();
+ await proxy.stop();
+ });
+
+ let wss = new NodeWebSocketHttp2Server();
+ await wss.start();
+ Assert.notEqual(wss.port(), null);
+ await wss.registerMessageHandler((data, ws) => {
+ ws.send(data);
+ });
+
+ // To create a h2 connection before the websocket one.
+ let chan = makeChan(`https://localhost:${wss.port()}/`);
+ await httpChannelOpenPromise(chan, CL_ALLOW_UNKNOWN_CL);
+
+ let url = `wss://localhost:${wss.port()}`;
+ const msg = "test h2 websocket with h1 insecure proxy";
+ let [status, res] = await channelOpenPromise(url, msg);
+ Assert.equal(status, Cr.NS_OK);
+ Assert.deepEqual(res, [msg]);
+
+ await proxy.stop();
+}
+
+add_task(test_h1_websocket_direct);
+add_task(test_h2_websocket_direct);
+add_task(test_h1_ws_with_secure_h1_proxy);
+add_task(test_h1_ws_with_h1_insecure_proxy);
+add_task(test_h1_ws_with_h2_proxy);
+add_task(test_h2_ws_with_h1_insecure_proxy);
+add_task(test_h2_ws_with_h1_secure_proxy);
+add_task(test_h2_ws_with_h2_proxy);
+add_task(test_bug_1848013);
diff --git a/netwerk/test/unit/test_websocket_server_multiclient.js b/netwerk/test/unit/test_websocket_server_multiclient.js
new file mode 100644
index 0000000000..33e730acbf
--- /dev/null
+++ b/netwerk/test/unit/test_websocket_server_multiclient.js
@@ -0,0 +1,141 @@
+/* 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 */
+/* import-globals-from head_servers.js */
+/* import-globals-from head_websocket.js */
+
+// These test should basically match the ones in test_websocket_server.js,
+// but with multiple websocket clients making requests on the same server
+
+const certOverrideService = Cc[
+ "@mozilla.org/security/certoverride;1"
+].getService(Ci.nsICertOverrideService);
+
+// setup
+add_setup(async function setup() {
+ // turn off cert checking for these tests
+ certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData(
+ true
+ );
+});
+
+// append cleanup to cleanup queue
+registerCleanupFunction(async () => {
+ certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData(
+ false
+ );
+ Services.prefs.clearUserPref("network.http.http2.websockets");
+});
+
+async function spinup_and_check(proxy_kind, ws_kind) {
+ let ws_h2 = true;
+ if (ws_kind == NodeWebSocketServer) {
+ info("not h2 ws");
+ ws_h2 = false;
+ }
+ Services.prefs.setBoolPref("network.http.http2.websockets", ws_h2);
+
+ let proxy;
+ if (proxy_kind) {
+ proxy = new proxy_kind();
+ await proxy.start();
+ registerCleanupFunction(async () => proxy.stop());
+ }
+
+ let wss = new ws_kind();
+ await wss.start();
+ registerCleanupFunction(async () => wss.stop());
+
+ Assert.notEqual(wss.port(), null);
+ await wss.registerMessageHandler((data, ws) => {
+ ws.send(data);
+ });
+ let url = `wss://localhost:${wss.port()}`;
+
+ let conn1 = new WebSocketConnection();
+ await conn1.open(url);
+
+ let conn2 = new WebSocketConnection();
+ await conn2.open(url);
+
+ conn1.send("msg1");
+ conn2.send("msg2");
+
+ let mess2 = await conn2.receiveMessages();
+ Assert.deepEqual(mess2, ["msg2"]);
+
+ conn1.send("msg1 again");
+ let mess1 = [];
+ while (mess1.length < 2) {
+ // receive could return only the first or both replies.
+ mess1 = mess1.concat(await conn1.receiveMessages());
+ }
+ Assert.deepEqual(mess1, ["msg1", "msg1 again"]);
+
+ conn1.close();
+ conn2.close();
+ Assert.deepEqual({ status: Cr.NS_OK }, await conn1.finished());
+ Assert.deepEqual({ status: Cr.NS_OK }, await conn2.finished());
+ await wss.stop();
+
+ if (proxy_kind) {
+ await proxy.stop();
+ }
+}
+
+// h1.1 direct
+async function test_h1_websocket_direct() {
+ await spinup_and_check(null, NodeWebSocketServer);
+}
+
+// h2 direct
+async function test_h2_websocket_direct() {
+ await spinup_and_check(null, NodeWebSocketHttp2Server);
+}
+
+// ws h1.1 with secure h1.1 proxy
+async function test_h1_ws_with_secure_h1_proxy() {
+ await spinup_and_check(NodeHTTPSProxyServer, NodeWebSocketServer);
+}
+
+// ws h1.1 with insecure h1.1 proxy
+async function test_h1_ws_with_insecure_h1_proxy() {
+ await spinup_and_check(NodeHTTPProxyServer, NodeWebSocketServer);
+}
+
+// ws h1.1 with h2 proxy
+async function test_h1_ws_with_h2_proxy() {
+ await spinup_and_check(NodeHTTP2ProxyServer, NodeWebSocketServer);
+}
+
+// ws h2 with insecure h1.1 proxy
+async function test_h2_ws_with_insecure_h1_proxy() {
+ await spinup_and_check(NodeHTTPProxyServer, NodeWebSocketHttp2Server);
+}
+
+// ws h2 with secure h1 proxy
+async function test_h2_ws_with_secure_h1_proxy() {
+ await spinup_and_check(NodeHTTPSProxyServer, NodeWebSocketHttp2Server);
+}
+
+// ws h2 with secure h2 proxy
+async function test_h2_ws_with_h2_proxy() {
+ await spinup_and_check(NodeHTTP2ProxyServer, NodeWebSocketHttp2Server);
+}
+
+add_task(test_h1_websocket_direct);
+add_task(test_h2_websocket_direct);
+add_task(test_h1_ws_with_secure_h1_proxy);
+add_task(test_h1_ws_with_insecure_h1_proxy);
+add_task(test_h1_ws_with_h2_proxy);
+
+// any multi-client test with h2 websocket and any kind of proxy will fail/hang
+add_task(test_h2_ws_with_insecure_h1_proxy);
+add_task(test_h2_ws_with_secure_h1_proxy);
+add_task(test_h2_ws_with_h2_proxy);
diff --git a/netwerk/test/unit/test_websocket_with_h3_active.js b/netwerk/test/unit/test_websocket_with_h3_active.js
new file mode 100644
index 0000000000..f9ed2b08a0
--- /dev/null
+++ b/netwerk/test/unit/test_websocket_with_h3_active.js
@@ -0,0 +1,97 @@
+/* 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";
+
+registerCleanupFunction(async () => {
+ http3_clear_prefs();
+});
+
+let wssUri;
+let httpsUri;
+
+add_task(async function pre_setup() {
+ let h2Port = Services.env.get("MOZHTTP2_PORT");
+ Assert.notEqual(h2Port, null);
+ Assert.notEqual(h2Port, "");
+
+ wssUri = "wss://foo.example.com:" + h2Port + "/websocket";
+ httpsUri = "https://foo.example.com:" + h2Port + "/";
+ Services.prefs.setBoolPref("network.http.http3.support_version1", true);
+});
+
+add_task(async function setup() {
+ await http3_setup_tests("h3");
+});
+
+WebSocketListener.prototype = {
+ onAcknowledge(aContext, aSize) {},
+ onBinaryMessageAvailable(aContext, aMsg) {},
+ onMessageAvailable(aContext, aMsg) {},
+ onServerClose(aContext, aCode, aReason) {},
+ onStart(aContext) {
+ this.finish();
+ },
+ onStop(aContext, aStatusCode) {},
+};
+
+function makeH2Chan() {
+ 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 open_wss_when_h3_is_active() {
+ // Make an active connection using HTTP/3
+ let chanHttp1 = makeH2Chan(httpsUri);
+ await new Promise(resolve => {
+ chanHttp1.asyncOpen(
+ new ChannelListener(request => {
+ let httpVersion = "";
+ try {
+ httpVersion = request.protocolVersion;
+ } catch (e) {}
+ Assert.equal(httpVersion, "h3");
+ resolve();
+ })
+ );
+ });
+
+ // Now try to connect ot a WebSocket on the same port -> this should not loop
+ // see bug 1717360.
+ let chan = Cc["@mozilla.org/network/protocol;1?name=wss"].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 = Services.io.newURI(wssUri);
+ var wsListener = new WebSocketListener();
+ await new Promise(resolve => {
+ wsListener.finish = resolve;
+ chan.asyncOpen(uri, wssUri, {}, 0, wsListener, null);
+ });
+
+ // Try to use https protocol, it should sttill use HTTP/3
+ let chanHttp2 = makeH2Chan(httpsUri);
+ await new Promise(resolve => {
+ chanHttp2.asyncOpen(
+ new ChannelListener(request => {
+ let httpVersion = "";
+ try {
+ httpVersion = request.protocolVersion;
+ } catch (e) {}
+ Assert.equal(httpVersion, "h3");
+ resolve();
+ })
+ );
+ });
+});
diff --git a/netwerk/test/unit/test_webtransport_simple.js b/netwerk/test/unit/test_webtransport_simple.js
new file mode 100644
index 0000000000..bd99654bc3
--- /dev/null
+++ b/netwerk/test/unit/test_webtransport_simple.js
@@ -0,0 +1,460 @@
+//
+// Simple WebTransport test
+//
+
+/* import-globals-from head_webtransport.js */
+
+"use strict";
+
+var h3Port;
+var host;
+
+var { setTimeout } = ChromeUtils.importESModule(
+ "resource://gre/modules/Timer.sys.mjs"
+);
+
+registerCleanupFunction(async () => {
+ Services.prefs.clearUserPref("network.dns.localDomains");
+ Services.prefs.clearUserPref("network.webtransport.datagrams.enabled");
+ Services.prefs.clearUserPref(
+ "network.http.http3.alt-svc-mapping-for-testing"
+ );
+});
+
+add_task(async function setup() {
+ await http3_setup_tests("h3");
+
+ Services.prefs.setBoolPref("network.webtransport.datagrams.enabled", true);
+
+ h3Port = Services.env.get("MOZHTTP3_PORT");
+ Assert.notEqual(h3Port, null);
+ Assert.notEqual(h3Port, "");
+ host = "foo.example.com:" + h3Port;
+});
+
+function makeChan(url) {
+ let chan = NetUtil.newChannel({
+ uri: url,
+ loadUsingSystemPrincipal: true,
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT,
+ }).QueryInterface(Ci.nsIHttpChannel);
+ chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI;
+ return chan;
+}
+
+function channelOpenPromise(chan, flags) {
+ // eslint-disable-next-line no-async-promise-executor
+ return new Promise(async resolve => {
+ function finish(req, buffer) {
+ resolve([req, buffer]);
+ }
+ let internal = chan.QueryInterface(Ci.nsIHttpChannelInternal);
+ internal.setWaitForHTTPSSVCRecord();
+
+ chan.asyncOpen(new ChannelListener(finish, null, flags));
+ });
+}
+
+function bytesFromString(str) {
+ return new TextEncoder().encode(str);
+}
+
+add_task(async function test_wt_datagram() {
+ let webTransport = NetUtil.newWebTransport();
+ let listener = new WebTransportListener().QueryInterface(
+ Ci.WebTransportSessionEventListener
+ );
+
+ let pReady = new Promise(resolve => {
+ listener.ready = resolve;
+ });
+ let pSize = new Promise(resolve => {
+ listener.onMaxDatagramSize = resolve;
+ });
+ let pOutcome = new Promise(resolve => {
+ listener.onDatagramOutcome = resolve;
+ });
+
+ webTransport.asyncConnect(
+ NetUtil.newURI(`https://${host}/success`),
+ [],
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ listener
+ );
+
+ await pReady;
+
+ webTransport.getMaxDatagramSize();
+ let size = await pSize;
+ info("max size:" + size);
+
+ let rawData = new Uint8Array(size);
+ rawData.fill(42);
+
+ webTransport.sendDatagram(rawData, 1);
+ let { id, outcome } = await pOutcome;
+ Assert.equal(id, 1);
+ Assert.equal(outcome, Ci.WebTransportSessionEventListener.SENT);
+
+ let chan = makeChan(`https://${host}/get_webtransport_datagram`);
+ let [req, buffer] = await channelOpenPromise(chan);
+ Assert.equal(req.protocolVersion, "h3");
+
+ Assert.deepEqual(bytesFromString(buffer), rawData);
+
+ webTransport.getMaxDatagramSize();
+ size = await pSize;
+ info("max size:" + size);
+
+ rawData = new Uint8Array(size + 1);
+ webTransport.sendDatagram(rawData, 2);
+
+ pOutcome = new Promise(resolve => {
+ listener.onDatagramOutcome = resolve;
+ });
+ ({ id, outcome } = await pOutcome);
+ Assert.equal(id, 2);
+ Assert.equal(
+ outcome,
+ Ci.WebTransportSessionEventListener.DROPPED_TOO_MUCH_DATA
+ );
+
+ webTransport.closeSession(0, "");
+});
+
+add_task(async function test_connect_wt() {
+ let webTransport = NetUtil.newWebTransport();
+
+ await new Promise(resolve => {
+ let listener = new WebTransportListener().QueryInterface(
+ Ci.WebTransportSessionEventListener
+ );
+ listener.ready = resolve;
+
+ webTransport.asyncConnect(
+ NetUtil.newURI(`https://${host}/success`),
+ [],
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ listener
+ );
+ });
+
+ webTransport.closeSession(0, "");
+});
+
+add_task(async function test_redirect_wt() {
+ let webTransport = NetUtil.newWebTransport();
+
+ await new Promise(resolve => {
+ let listener = new WebTransportListener().QueryInterface(
+ Ci.WebTransportSessionEventListener
+ );
+
+ listener.closed = resolve;
+
+ webTransport.asyncConnect(
+ NetUtil.newURI(`https://${host}/redirect`),
+ [],
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ listener
+ );
+ });
+});
+
+add_task(async function test_reject() {
+ let webTransport = NetUtil.newWebTransport();
+
+ await new Promise(resolve => {
+ let listener = new WebTransportListener().QueryInterface(
+ Ci.WebTransportSessionEventListener
+ );
+ listener.closed = resolve;
+
+ webTransport.asyncConnect(
+ NetUtil.newURI(`https://${host}/reject`),
+ [],
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ listener
+ );
+ });
+});
+
+async function test_closed(path) {
+ let webTransport = NetUtil.newWebTransport();
+
+ let listener = new WebTransportListener().QueryInterface(
+ Ci.WebTransportSessionEventListener
+ );
+
+ let pReady = new Promise(resolve => {
+ listener.ready = resolve;
+ });
+ let pClose = new Promise(resolve => {
+ listener.closed = resolve;
+ });
+ webTransport.asyncConnect(
+ NetUtil.newURI(`https://${host}${path}`),
+ [],
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ listener
+ );
+
+ await pReady;
+ await pClose;
+}
+
+add_task(async function test_closed_0ms() {
+ await test_closed("/closeafter0ms");
+});
+
+add_task(async function test_closed_100ms() {
+ await test_closed("/closeafter100ms");
+});
+
+add_task(async function test_wt_stream_create() {
+ let webTransport = NetUtil.newWebTransport().QueryInterface(
+ Ci.nsIWebTransport
+ );
+
+ await new Promise(resolve => {
+ let listener = new WebTransportListener().QueryInterface(
+ Ci.WebTransportSessionEventListener
+ );
+ listener.ready = resolve;
+
+ webTransport.asyncConnect(
+ NetUtil.newURI(`https://${host}/success`),
+ [],
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ listener
+ );
+ });
+
+ await streamCreatePromise(webTransport, true);
+ await streamCreatePromise(webTransport, false);
+
+ webTransport.closeSession(0, "");
+});
+
+add_task(async function test_wt_stream_send_and_stats() {
+ let webTransport = NetUtil.newWebTransport().QueryInterface(
+ Ci.nsIWebTransport
+ );
+
+ await new Promise(resolve => {
+ let listener = new WebTransportListener().QueryInterface(
+ Ci.WebTransportSessionEventListener
+ );
+ listener.ready = resolve;
+
+ webTransport.asyncConnect(
+ NetUtil.newURI(`https://${host}/success`),
+ [],
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ listener
+ );
+ });
+
+ let stream = await streamCreatePromise(webTransport, false);
+ let outputStream = stream.outputStream;
+
+ let data = "1234567890ABC";
+ outputStream.write(data, data.length);
+
+ // We need some time to send the packet out.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => setTimeout(resolve, 2000));
+
+ let stats = await sendStreamStatsPromise(stream);
+ Assert.equal(stats.bytesSent, data.length);
+
+ webTransport.closeSession(0, "");
+});
+
+add_task(async function test_wt_receive_stream_and_stats() {
+ let webTransport = NetUtil.newWebTransport().QueryInterface(
+ Ci.nsIWebTransport
+ );
+
+ let listener = new WebTransportListener().QueryInterface(
+ Ci.WebTransportSessionEventListener
+ );
+
+ let pReady = new Promise(resolve => {
+ listener.ready = resolve;
+ });
+ let pStreamReady = new Promise(resolve => {
+ listener.streamAvailable = resolve;
+ });
+ webTransport.asyncConnect(
+ NetUtil.newURI(`https://${host}/create_unidi_stream_and_hello`),
+ [],
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ listener
+ );
+
+ await pReady;
+ let stream = await pStreamReady;
+
+ let data = await new Promise(resolve => {
+ let handler = new inputStreamReader().QueryInterface(
+ Ci.nsIInputStreamCallback
+ );
+ handler.finish = resolve;
+ let inputStream = stream.inputStream;
+ inputStream.asyncWait(handler, 0, 0, Services.tm.currentThread);
+ });
+
+ info("data: " + data);
+ Assert.equal(data, "qwerty");
+
+ let stats = await receiveStreamStatsPromise(stream);
+ Assert.equal(stats.bytesReceived, data.length);
+
+ stream.sendStopSending(0);
+
+ webTransport.closeSession(0, "");
+});
+
+add_task(async function test_wt_outgoing_bidi_stream() {
+ let webTransport = NetUtil.newWebTransport().QueryInterface(
+ Ci.nsIWebTransport
+ );
+
+ await new Promise(resolve => {
+ let listener = new WebTransportListener().QueryInterface(
+ Ci.WebTransportSessionEventListener
+ );
+ listener.ready = resolve;
+
+ webTransport.asyncConnect(
+ NetUtil.newURI(`https://${host}/success`),
+ [],
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ listener
+ );
+ });
+
+ let stream = await streamCreatePromise(webTransport, true);
+ let outputStream = stream.outputStream;
+
+ let data = "1234567";
+ outputStream.write(data, data.length);
+
+ let received = await new Promise(resolve => {
+ let handler = new inputStreamReader().QueryInterface(
+ Ci.nsIInputStreamCallback
+ );
+ handler.finish = resolve;
+ let inputStream = stream.inputStream;
+ inputStream.asyncWait(handler, 0, 0, Services.tm.currentThread);
+ });
+
+ info("received: " + received);
+ Assert.equal(received, data);
+
+ let stats = await sendStreamStatsPromise(stream);
+ Assert.equal(stats.bytesSent, data.length);
+
+ stats = await receiveStreamStatsPromise(stream);
+ Assert.equal(stats.bytesReceived, data.length);
+
+ webTransport.closeSession(0, "");
+});
+
+add_task(async function test_wt_incoming_bidi_stream() {
+ let webTransport = NetUtil.newWebTransport().QueryInterface(
+ Ci.nsIWebTransport
+ );
+
+ let listener = new WebTransportListener().QueryInterface(
+ Ci.WebTransportSessionEventListener
+ );
+
+ let pReady = new Promise(resolve => {
+ listener.ready = resolve;
+ });
+ let pStreamReady = new Promise(resolve => {
+ listener.streamAvailable = resolve;
+ });
+ webTransport.asyncConnect(
+ NetUtil.newURI(`https://${host}/create_bidi_stream`),
+ [],
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ listener
+ );
+
+ await pReady;
+ let stream = await pStreamReady;
+
+ let outputStream = stream.outputStream;
+
+ let data = "12345678";
+ outputStream.write(data, data.length);
+
+ let received = await new Promise(resolve => {
+ let handler = new inputStreamReader().QueryInterface(
+ Ci.nsIInputStreamCallback
+ );
+ handler.finish = resolve;
+ let inputStream = stream.inputStream;
+ inputStream.asyncWait(handler, 0, 0, Services.tm.currentThread);
+ });
+
+ info("received: " + received);
+ Assert.equal(received, data);
+
+ let stats = await sendStreamStatsPromise(stream);
+ Assert.equal(stats.bytesSent, data.length);
+
+ stats = await receiveStreamStatsPromise(stream);
+ Assert.equal(stats.bytesReceived, data.length);
+
+ webTransport.closeSession(0, "");
+});
+
+async function createWebTransportAndConnect() {
+ let webTransport = NetUtil.newWebTransport();
+
+ await new Promise(resolve => {
+ let listener = new WebTransportListener().QueryInterface(
+ Ci.WebTransportSessionEventListener
+ );
+ listener.ready = resolve;
+
+ webTransport.asyncConnect(
+ NetUtil.newURI(`https://${host}/success`),
+ [],
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ listener
+ );
+ });
+
+ return webTransport;
+}
+
+add_task(async function test_multple_webtransport_connnection() {
+ let webTransports = [];
+ for (let i = 0; i < 3; i++) {
+ let transport = await createWebTransportAndConnect();
+ webTransports.push(transport);
+ }
+
+ let first = webTransports[0];
+ await streamCreatePromise(first, true);
+
+ for (let i = 0; i < 3; i++) {
+ webTransports[i].closeSession(0, "");
+ }
+});
diff --git a/netwerk/test/unit/test_xmlhttprequest.js b/netwerk/test/unit/test_xmlhttprequest.js
new file mode 100644
index 0000000000..7363a54cfd
--- /dev/null
+++ b/netwerk/test/unit/test_xmlhttprequest.js
@@ -0,0 +1,57 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+var httpserver = new HttpServer();
+var testpath = "/simple";
+var httpbody = "<?xml version='1.0' ?><root>0123456789</root>";
+
+function createXHR(async) {
+ var xhr = new XMLHttpRequest();
+ xhr.open(
+ "GET",
+ "http://localhost:" + httpserver.identity.primaryPort + testpath,
+ async
+ );
+ return xhr;
+}
+
+function checkResults(xhr) {
+ if (xhr.readyState != 4) {
+ return false;
+ }
+
+ Assert.equal(xhr.status, 200);
+ Assert.equal(xhr.responseText, httpbody);
+
+ var root_node = xhr.responseXML.getElementsByTagName("root").item(0);
+ Assert.equal(root_node.firstChild.data, "0123456789");
+ return true;
+}
+
+function run_test() {
+ httpserver.registerPathHandler(testpath, serverHandler);
+ httpserver.start(-1);
+
+ // Test sync XHR sending
+ var sync = createXHR(false);
+ sync.send(null);
+ checkResults(sync);
+
+ // Test async XHR sending
+ let async = createXHR(true);
+ async.addEventListener("readystatechange", function (event) {
+ if (checkResults(async)) {
+ httpserver.stop(do_test_finished);
+ }
+ });
+ async.send(null);
+ do_test_pending();
+}
+
+function serverHandler(metadata, response) {
+ response.setHeader("Content-Type", "text/xml", false);
+ response.bodyOutputStream.write(httpbody, httpbody.length);
+}
diff --git a/netwerk/test/unit/trr_common.js b/netwerk/test/unit/trr_common.js
new file mode 100644
index 0000000000..2ddd556983
--- /dev/null
+++ b/netwerk/test/unit/trr_common.js
@@ -0,0 +1,1235 @@
+/* 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_trr.js */
+/* import-globals-from head_http3.js */
+
+const { TestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TestUtils.sys.mjs"
+);
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+const TRR_Domain = "foo.example.com";
+
+const { MockRegistrar } = ChromeUtils.importESModule(
+ "resource://testing-common/MockRegistrar.sys.mjs"
+);
+
+const gOverride = Cc["@mozilla.org/network/native-dns-override;1"].getService(
+ Ci.nsINativeDNSResolverOverride
+);
+
+async function SetParentalControlEnabled(aEnabled) {
+ let parentalControlsService = {
+ parentalControlsEnabled: aEnabled,
+ QueryInterface: ChromeUtils.generateQI(["nsIParentalControlsService"]),
+ };
+ let cid = MockRegistrar.register(
+ "@mozilla.org/parental-controls-service;1",
+ parentalControlsService
+ );
+ Services.dns.reloadParentalControlEnabled();
+ MockRegistrar.unregister(cid);
+}
+
+let runningOHTTPTests = false;
+let h2Port;
+
+function setModeAndURIForODoH(mode, path) {
+ Services.prefs.setIntPref("network.trr.mode", mode);
+ if (path.substr(0, 4) == "doh?") {
+ path = path.replace("doh?", "odoh?");
+ }
+
+ Services.prefs.setCharPref("network.trr.odoh.target_path", `${path}`);
+}
+
+function setModeAndURIForOHTTP(mode, path, domain) {
+ Services.prefs.setIntPref("network.trr.mode", mode);
+ if (domain) {
+ Services.prefs.setCharPref(
+ "network.trr.ohttp.uri",
+ `https://${domain}:${h2Port}/${path}`
+ );
+ } else {
+ Services.prefs.setCharPref(
+ "network.trr.ohttp.uri",
+ `https://${TRR_Domain}:${h2Port}/${path}`
+ );
+ }
+}
+
+function setModeAndURI(mode, path, domain) {
+ if (runningOHTTPTests) {
+ setModeAndURIForOHTTP(mode, path, domain);
+ } else {
+ Services.prefs.setIntPref("network.trr.mode", mode);
+ if (domain) {
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://${domain}:${h2Port}/${path}`
+ );
+ } else {
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://${TRR_Domain}:${h2Port}/${path}`
+ );
+ }
+ }
+}
+
+async function test_A_record() {
+ info("Verifying a basic A record");
+ Services.dns.clearCache(true);
+ setModeAndURI(2, "doh?responseIP=2.2.2.2"); // TRR-first
+ await new TRRDNSListener("bar.example.com", "2.2.2.2");
+
+ info("Verifying a basic A record - without bootstrapping");
+ Services.dns.clearCache(true);
+ setModeAndURI(3, "doh?responseIP=3.3.3.3"); // TRR-only
+
+ // Clear bootstrap address and add DoH endpoint hostname to local domains
+ Services.prefs.clearUserPref("network.trr.bootstrapAddr");
+ Services.prefs.setCharPref("network.dns.localDomains", TRR_Domain);
+
+ await new TRRDNSListener("bar.example.com", "3.3.3.3");
+
+ Services.prefs.setCharPref("network.trr.bootstrapAddr", "127.0.0.1");
+ Services.prefs.clearUserPref("network.dns.localDomains");
+
+ info("Verify that the cached record is used when DoH endpoint is down");
+ // Don't clear the cache. That is what we're checking.
+ setModeAndURI(3, "404");
+
+ await new TRRDNSListener("bar.example.com", "3.3.3.3");
+ info("verify working credentials in DOH request");
+ Services.dns.clearCache(true);
+ setModeAndURI(3, "doh?responseIP=4.4.4.4&auth=true");
+ Services.prefs.setCharPref("network.trr.credentials", "user:password");
+
+ await new TRRDNSListener("bar.example.com", "4.4.4.4");
+
+ info("Verify failing credentials in DOH request");
+ Services.dns.clearCache(true);
+ setModeAndURI(3, "doh?responseIP=4.4.4.4&auth=true");
+ Services.prefs.setCharPref("network.trr.credentials", "evil:person");
+
+ let { inStatus } = await new TRRDNSListener(
+ "wrong.example.com",
+ undefined,
+ false
+ );
+ Assert.ok(
+ !Components.isSuccessCode(inStatus),
+ `${inStatus} should be an error code`
+ );
+
+ Services.prefs.clearUserPref("network.trr.credentials");
+}
+
+async function test_AAAA_records() {
+ info("Verifying AAAA record");
+
+ Services.dns.clearCache(true);
+ setModeAndURI(3, "doh?responseIP=2020:2020::2020&delayIPv4=100");
+
+ await new TRRDNSListener("aaaa.example.com", "2020:2020::2020");
+
+ Services.dns.clearCache(true);
+ setModeAndURI(3, "doh?responseIP=2020:2020::2020&delayIPv6=100");
+
+ await new TRRDNSListener("aaaa.example.com", "2020:2020::2020");
+
+ Services.dns.clearCache(true);
+ setModeAndURI(3, "doh?responseIP=2020:2020::2020");
+
+ await new TRRDNSListener("aaaa.example.com", "2020:2020::2020");
+}
+
+async function test_RFC1918() {
+ info("Verifying that RFC1918 address from the server is rejected by default");
+ Services.dns.clearCache(true);
+ setModeAndURI(3, "doh?responseIP=192.168.0.1");
+
+ let { inStatus } = await new TRRDNSListener(
+ "rfc1918.example.com",
+ undefined,
+ false
+ );
+
+ Assert.ok(
+ !Components.isSuccessCode(inStatus),
+ `${inStatus} should be an error code`
+ );
+ setModeAndURI(3, "doh?responseIP=::ffff:192.168.0.1");
+ ({ inStatus } = await new TRRDNSListener(
+ "rfc1918-ipv6.example.com",
+ undefined,
+ false
+ ));
+ Assert.ok(
+ !Components.isSuccessCode(inStatus),
+ `${inStatus} should be an error code`
+ );
+
+ info("Verify RFC1918 address from the server is fine when told so");
+ Services.dns.clearCache(true);
+ setModeAndURI(3, "doh?responseIP=192.168.0.1");
+ Services.prefs.setBoolPref("network.trr.allow-rfc1918", true);
+ await new TRRDNSListener("rfc1918.example.com", "192.168.0.1");
+ setModeAndURI(3, "doh?responseIP=::ffff:192.168.0.1");
+
+ await new TRRDNSListener("rfc1918-ipv6.example.com", "::ffff:192.168.0.1");
+
+ Services.prefs.clearUserPref("network.trr.allow-rfc1918");
+}
+
+async function test_GET_ECS() {
+ info("Verifying resolution via GET with ECS disabled");
+ Services.dns.clearCache(true);
+ // The template part should be discarded
+ setModeAndURI(3, "doh{?dns}");
+ Services.prefs.setBoolPref("network.trr.useGET", true);
+ Services.prefs.setBoolPref("network.trr.disable-ECS", true);
+
+ await new TRRDNSListener("ecs.example.com", "5.5.5.5");
+
+ info("Verifying resolution via GET with ECS enabled");
+ Services.dns.clearCache(true);
+ setModeAndURI(3, "doh");
+ Services.prefs.setBoolPref("network.trr.disable-ECS", false);
+
+ await new TRRDNSListener("get.example.com", "5.5.5.5");
+
+ Services.prefs.clearUserPref("network.trr.useGET");
+ Services.prefs.clearUserPref("network.trr.disable-ECS");
+}
+
+async function test_timeout_mode3() {
+ info("Verifying that a short timeout causes failure with a slow server");
+ Services.dns.clearCache(true);
+ // First, mode 3.
+ setModeAndURI(3, "doh?noResponse=true");
+ Services.prefs.setIntPref("network.trr.request_timeout_ms", 10);
+ Services.prefs.setIntPref("network.trr.request_timeout_mode_trronly_ms", 10);
+
+ let { inStatus } = await new TRRDNSListener(
+ "timeout.example.com",
+ undefined,
+ false
+ );
+ Assert.ok(
+ !Components.isSuccessCode(inStatus),
+ `${inStatus} should be an error code`
+ );
+
+ // Now for mode 2
+ Services.dns.clearCache(true);
+ setModeAndURI(2, "doh?noResponse=true");
+
+ await new TRRDNSListener("timeout.example.com", "127.0.0.1"); // Should fallback
+
+ Services.prefs.clearUserPref("network.trr.request_timeout_ms");
+ Services.prefs.clearUserPref("network.trr.request_timeout_mode_trronly_ms");
+}
+
+async function test_trr_retry() {
+ Services.dns.clearCache(true);
+ Services.prefs.setBoolPref("network.trr.strict_native_fallback", false);
+
+ info("Test fallback to native");
+ Services.prefs.setBoolPref("network.trr.retry_on_recoverable_errors", false);
+ setModeAndURI(2, "doh?noResponse=true");
+ Services.prefs.setIntPref("network.trr.request_timeout_ms", 10);
+ Services.prefs.setIntPref("network.trr.request_timeout_mode_trronly_ms", 10);
+
+ await new TRRDNSListener("timeout.example.com", {
+ expectedAnswer: "127.0.0.1",
+ });
+
+ Services.prefs.clearUserPref("network.trr.request_timeout_ms");
+ Services.prefs.clearUserPref("network.trr.request_timeout_mode_trronly_ms");
+
+ info("Test Retry Success");
+ Services.prefs.setBoolPref("network.trr.retry_on_recoverable_errors", true);
+
+ let chan = makeChan(
+ `https://foo.example.com:${h2Port}/reset-doh-request-count`,
+ Ci.nsIRequest.TRR_DISABLED_MODE
+ );
+ await new Promise(resolve =>
+ chan.asyncOpen(new ChannelListener(resolve, null))
+ );
+
+ setModeAndURI(2, "doh?responseIP=2.2.2.2&retryOnDecodeFailure=true");
+ await new TRRDNSListener("retry_ok.example.com", "2.2.2.2");
+
+ info("Test Retry Failed");
+ Services.dns.clearCache(true);
+ setModeAndURI(2, "doh?responseIP=2.2.2.2&corruptedAnswer=true");
+ await new TRRDNSListener("retry_ng.example.com", "127.0.0.1");
+}
+
+async function test_strict_native_fallback() {
+ Services.dns.clearCache(true);
+ Services.prefs.setBoolPref("network.trr.retry_on_recoverable_errors", true);
+ Services.prefs.setBoolPref("network.trr.strict_native_fallback", true);
+
+ info("First a timeout case");
+ setModeAndURI(2, "doh?noResponse=true");
+ Services.prefs.setIntPref("network.trr.request_timeout_ms", 10);
+ Services.prefs.setIntPref("network.trr.request_timeout_mode_trronly_ms", 10);
+ Services.prefs.setIntPref(
+ "network.trr.strict_fallback_request_timeout_ms",
+ 10
+ );
+
+ Services.prefs.setBoolPref(
+ "network.trr.strict_native_fallback_allow_timeouts",
+ false
+ );
+
+ let { inStatus } = await new TRRDNSListener(
+ "timeout.example.com",
+ undefined,
+ false
+ );
+ Assert.ok(
+ !Components.isSuccessCode(inStatus),
+ `${inStatus} should be an error code`
+ );
+ Services.dns.clearCache(true);
+ await new TRRDNSListener("timeout.example.com", undefined, false);
+
+ Services.dns.clearCache(true);
+ Services.prefs.setBoolPref(
+ "network.trr.strict_native_fallback_allow_timeouts",
+ true
+ );
+ await new TRRDNSListener("timeout.example.com", {
+ expectedAnswer: "127.0.0.1",
+ });
+
+ Services.prefs.setBoolPref(
+ "network.trr.strict_native_fallback_allow_timeouts",
+ false
+ );
+
+ info("Now a connection error");
+ Services.dns.clearCache(true);
+ setModeAndURI(2, "doh?responseIP=2.2.2.2");
+ Services.prefs.clearUserPref("network.trr.request_timeout_ms");
+ Services.prefs.clearUserPref("network.trr.request_timeout_mode_trronly_ms");
+ Services.prefs.clearUserPref(
+ "network.trr.strict_fallback_request_timeout_ms"
+ );
+ ({ inStatus } = await new TRRDNSListener("closeme.com", undefined, false));
+ Assert.ok(
+ !Components.isSuccessCode(inStatus),
+ `${inStatus} should be an error code`
+ );
+
+ info("Now a decode error");
+ Services.dns.clearCache(true);
+ setModeAndURI(2, "doh?responseIP=2.2.2.2&corruptedAnswer=true");
+ ({ inStatus } = await new TRRDNSListener(
+ "bar.example.com",
+ undefined,
+ false
+ ));
+ Assert.ok(
+ !Components.isSuccessCode(inStatus),
+ `${inStatus} should be an error code`
+ );
+
+ if (!mozinfo.socketprocess_networking) {
+ // Confirmation state isn't passed cross-process.
+ info("Now with confirmation failed - should fallback");
+ Services.dns.clearCache(true);
+ setModeAndURI(2, "doh?responseIP=2.2.2.2&corruptedAnswer=true");
+ Services.prefs.setCharPref("network.trr.confirmationNS", "example.com");
+ await TestUtils.waitForCondition(
+ // 3 => CONFIRM_FAILED, 4 => CONFIRM_TRYING_FAILED
+ () =>
+ Services.dns.currentTrrConfirmationState == 3 ||
+ Services.dns.currentTrrConfirmationState == 4,
+ `Timed out waiting for confirmation failure. Currently ${Services.dns.currentTrrConfirmationState}`,
+ 1,
+ 5000
+ );
+ await new TRRDNSListener("bar.example.com", "127.0.0.1"); // Should fallback
+ }
+
+ info("Now a successful case.");
+ Services.dns.clearCache(true);
+ setModeAndURI(2, "doh?responseIP=2.2.2.2");
+ if (!mozinfo.socketprocess_networking) {
+ // Only need to reset confirmation state if we messed with it before.
+ Services.prefs.setCharPref("network.trr.confirmationNS", "skip");
+ await TestUtils.waitForCondition(
+ // 5 => CONFIRM_DISABLED
+ () => Services.dns.currentTrrConfirmationState == 5,
+ `Timed out waiting for confirmation disabled. Currently ${Services.dns.currentTrrConfirmationState}`,
+ 1,
+ 5000
+ );
+ }
+ await new TRRDNSListener("bar.example.com", "2.2.2.2");
+
+ info("Now without strict fallback mode, timeout case");
+ Services.dns.clearCache(true);
+ setModeAndURI(2, "doh?noResponse=true");
+ Services.prefs.setIntPref("network.trr.request_timeout_ms", 10);
+ Services.prefs.setIntPref("network.trr.request_timeout_mode_trronly_ms", 10);
+ Services.prefs.setIntPref(
+ "network.trr.strict_fallback_request_timeout_ms",
+ 10
+ );
+ Services.prefs.setBoolPref("network.trr.strict_native_fallback", false);
+
+ await new TRRDNSListener("timeout.example.com", "127.0.0.1"); // Should fallback
+
+ info("Now a connection error");
+ Services.dns.clearCache(true);
+ setModeAndURI(2, "doh?responseIP=2.2.2.2");
+ Services.prefs.clearUserPref("network.trr.request_timeout_ms");
+ Services.prefs.clearUserPref("network.trr.request_timeout_mode_trronly_ms");
+ Services.prefs.clearUserPref(
+ "network.trr.strict_fallback_request_timeout_ms"
+ );
+ await new TRRDNSListener("closeme.com", "127.0.0.1"); // Should fallback
+
+ info("Now a decode error");
+ Services.dns.clearCache(true);
+ setModeAndURI(2, "doh?responseIP=2.2.2.2&corruptedAnswer=true");
+ await new TRRDNSListener("bar.example.com", "127.0.0.1"); // Should fallback
+
+ Services.prefs.setBoolPref("network.trr.strict_native_fallback", false);
+ Services.prefs.clearUserPref("network.trr.request_timeout_ms");
+ Services.prefs.clearUserPref("network.trr.request_timeout_mode_trronly_ms");
+ Services.prefs.clearUserPref(
+ "network.trr.strict_fallback_request_timeout_ms"
+ );
+}
+
+async function test_no_answers_fallback() {
+ info("Verfiying that we correctly fallback to Do53 when no answers from DoH");
+ Services.dns.clearCache(true);
+ setModeAndURI(2, "doh?responseIP=none"); // TRR-first
+
+ await new TRRDNSListener("confirm.example.com", "127.0.0.1");
+
+ info("Now in strict mode - no fallback");
+ Services.prefs.setBoolPref("network.trr.strict_native_fallback", true);
+ Services.dns.clearCache(true);
+ await new TRRDNSListener("confirm.example.com", "127.0.0.1");
+ Services.prefs.setBoolPref("network.trr.strict_native_fallback", false);
+}
+
+async function test_404_fallback() {
+ info("Verfiying that we correctly fallback to Do53 when DoH sends 404");
+ Services.dns.clearCache(true);
+ setModeAndURI(2, "404"); // TRR-first
+
+ await new TRRDNSListener("test404.example.com", "127.0.0.1");
+
+ info("Now in strict mode - no fallback");
+ Services.prefs.setBoolPref("network.trr.strict_native_fallback", true);
+ Services.dns.clearCache(true);
+ let { inStatus } = await new TRRDNSListener("test404.example.com", {
+ expectedSuccess: false,
+ });
+ Assert.ok(
+ !Components.isSuccessCode(inStatus),
+ `${inStatus} should be an error code`
+ );
+ Services.prefs.setBoolPref("network.trr.strict_native_fallback", false);
+}
+
+async function test_mode_1_and_4() {
+ info("Verifying modes 1 and 4 are treated as TRR-off");
+ for (let mode of [1, 4]) {
+ Services.dns.clearCache(true);
+ setModeAndURI(mode, "doh?responseIP=2.2.2.2");
+ Assert.equal(
+ Services.dns.currentTrrMode,
+ 5,
+ "Effective TRR mode should be 5"
+ );
+ }
+}
+
+async function test_CNAME() {
+ info("Checking that we follow a CNAME correctly");
+ Services.dns.clearCache(true);
+ // The dns-cname path alternates between sending us a CNAME pointing to
+ // another domain, and an A record. If we follow the cname correctly, doing
+ // a lookup with this path as the DoH URI should resolve to that A record.
+ setModeAndURI(3, "dns-cname");
+
+ await new TRRDNSListener("cname.example.com", "99.88.77.66");
+
+ info("Verifying that we bail out when we're thrown into a CNAME loop");
+ Services.dns.clearCache(true);
+ // First mode 3.
+ setModeAndURI(3, "doh?responseIP=none&cnameloop=true");
+
+ let { inStatus } = await new TRRDNSListener(
+ "test18.example.com",
+ undefined,
+ false
+ );
+ Assert.ok(
+ !Components.isSuccessCode(inStatus),
+ `${inStatus} should be an error code`
+ );
+
+ // Now mode 2.
+ Services.dns.clearCache(true);
+ setModeAndURI(2, "doh?responseIP=none&cnameloop=true");
+
+ await new TRRDNSListener("test20.example.com", "127.0.0.1"); // Should fallback
+
+ info("Check that we correctly handle CNAME bundled with an A record");
+ Services.dns.clearCache(true);
+ // "dns-cname-a" path causes server to send a CNAME as well as an A record
+ setModeAndURI(3, "dns-cname-a");
+
+ await new TRRDNSListener("cname-a.example.com", "9.8.7.6");
+}
+
+async function test_name_mismatch() {
+ info("Verify that records that don't match the requested name are rejected");
+ Services.dns.clearCache(true);
+ // Setting hostname param tells server to always send record for bar.example.com
+ // regardless of what was requested.
+ setModeAndURI(3, "doh?hostname=mismatch.example.com");
+
+ let { inStatus } = await new TRRDNSListener(
+ "bar.example.com",
+ undefined,
+ false
+ );
+ Assert.ok(
+ !Components.isSuccessCode(inStatus),
+ `${inStatus} should be an error code`
+ );
+}
+
+async function test_mode_2() {
+ info("Checking that TRR result is used in mode 2");
+ Services.dns.clearCache(true);
+ setModeAndURI(2, "doh?responseIP=192.192.192.192");
+ Services.prefs.setCharPref("network.trr.excluded-domains", "");
+ Services.prefs.setCharPref("network.trr.builtin-excluded-domains", "");
+
+ await new TRRDNSListener("bar.example.com", "192.192.192.192");
+
+ info("Now in strict mode");
+ Services.prefs.setBoolPref("network.trr.strict_native_fallback", true);
+ Services.dns.clearCache(true);
+ await new TRRDNSListener("bar.example.com", "192.192.192.192");
+ Services.prefs.setBoolPref("network.trr.strict_native_fallback", false);
+}
+
+async function test_excluded_domains() {
+ info("Checking that Do53 is used for names in excluded-domains list");
+ for (let strictMode of [true, false]) {
+ info("Strict mode: " + strictMode);
+ Services.prefs.setBoolPref(
+ "network.trr.strict_native_fallback",
+ strictMode
+ );
+ Services.dns.clearCache(true);
+ setModeAndURI(2, "doh?responseIP=192.192.192.192");
+ Services.prefs.setCharPref(
+ "network.trr.excluded-domains",
+ "bar.example.com"
+ );
+
+ await new TRRDNSListener("bar.example.com", "127.0.0.1"); // Do53 result
+
+ Services.dns.clearCache(true);
+ Services.prefs.setCharPref("network.trr.excluded-domains", "example.com");
+
+ await new TRRDNSListener("bar.example.com", "127.0.0.1");
+
+ Services.dns.clearCache(true);
+ Services.prefs.setCharPref(
+ "network.trr.excluded-domains",
+ "foo.test.com, bar.example.com"
+ );
+ await new TRRDNSListener("bar.example.com", "127.0.0.1");
+
+ Services.dns.clearCache(true);
+ Services.prefs.setCharPref(
+ "network.trr.excluded-domains",
+ "bar.example.com, foo.test.com"
+ );
+
+ await new TRRDNSListener("bar.example.com", "127.0.0.1");
+
+ Services.prefs.clearUserPref("network.trr.excluded-domains");
+ }
+}
+
+function topicObserved(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);
+ });
+}
+
+async function test_captiveportal_canonicalURL() {
+ info("Check that captivedetect.canonicalURL is resolved via native DNS");
+ for (let strictMode of [true, false]) {
+ info("Strict mode: " + strictMode);
+ Services.prefs.setBoolPref(
+ "network.trr.strict_native_fallback",
+ strictMode
+ );
+ Services.dns.clearCache(true);
+ setModeAndURI(2, "doh?responseIP=2.2.2.2");
+
+ const cpServer = new HttpServer();
+ cpServer.registerPathHandler(
+ "/cp",
+ function handleRawData(request, response) {
+ response.setHeader("Content-Type", "text/plain", false);
+ response.setHeader("Cache-Control", "no-cache", false);
+ response.bodyOutputStream.write("data", 4);
+ }
+ );
+ cpServer.start(-1);
+ cpServer.identity.setPrimary(
+ "http",
+ "detectportal.firefox.com",
+ cpServer.identity.primaryPort
+ );
+ let cpPromise = topicObserved("captive-portal-login");
+
+ Services.prefs.setCharPref(
+ "captivedetect.canonicalURL",
+ `http://detectportal.firefox.com:${cpServer.identity.primaryPort}/cp`
+ );
+ Services.prefs.setBoolPref("network.captive-portal-service.testMode", true);
+ Services.prefs.setBoolPref("network.captive-portal-service.enabled", true);
+
+ // The captive portal has to have used native DNS, otherwise creating
+ // a socket to a non-local IP would trigger a crash.
+ await cpPromise;
+ // Simply resolving the captive portal domain should still use TRR
+ await new TRRDNSListener("detectportal.firefox.com", "2.2.2.2");
+
+ Services.prefs.clearUserPref("network.captive-portal-service.enabled");
+ Services.prefs.clearUserPref("network.captive-portal-service.testMode");
+ Services.prefs.clearUserPref("captivedetect.canonicalURL");
+
+ await new Promise(resolve => cpServer.stop(resolve));
+ }
+}
+
+async function test_parentalcontrols() {
+ info("Check that DoH isn't used when parental controls are enabled");
+ Services.dns.clearCache(true);
+ setModeAndURI(2, "doh?responseIP=2.2.2.2");
+ await SetParentalControlEnabled(true);
+ await new TRRDNSListener("www.example.com", "127.0.0.1");
+ await SetParentalControlEnabled(false);
+
+ info("Now in strict mode");
+ Services.prefs.setBoolPref("network.trr.strict_native_fallback", true);
+ Services.dns.clearCache(true);
+ setModeAndURI(2, "doh?responseIP=2.2.2.2");
+ await SetParentalControlEnabled(true);
+ await new TRRDNSListener("www.example.com", "127.0.0.1");
+ await SetParentalControlEnabled(false);
+ Services.prefs.setBoolPref("network.trr.strict_native_fallback", false);
+}
+
+async function test_builtin_excluded_domains() {
+ info("Verifying Do53 is used for domains in builtin-excluded-domians list");
+ for (let strictMode of [true, false]) {
+ info("Strict mode: " + strictMode);
+ Services.prefs.setBoolPref(
+ "network.trr.strict_native_fallback",
+ strictMode
+ );
+ Services.dns.clearCache(true);
+ setModeAndURI(2, "doh?responseIP=2.2.2.2");
+
+ Services.prefs.setCharPref("network.trr.excluded-domains", "");
+ Services.prefs.setCharPref(
+ "network.trr.builtin-excluded-domains",
+ "bar.example.com"
+ );
+ await new TRRDNSListener("bar.example.com", "127.0.0.1");
+
+ Services.dns.clearCache(true);
+ Services.prefs.setCharPref(
+ "network.trr.builtin-excluded-domains",
+ "example.com"
+ );
+ await new TRRDNSListener("bar.example.com", "127.0.0.1");
+
+ Services.dns.clearCache(true);
+ Services.prefs.setCharPref(
+ "network.trr.builtin-excluded-domains",
+ "foo.test.com, bar.example.com"
+ );
+ await new TRRDNSListener("bar.example.com", "127.0.0.1");
+ await new TRRDNSListener("foo.test.com", "127.0.0.1");
+ }
+}
+
+async function test_excluded_domains_mode3() {
+ info("Checking Do53 is used for names in excluded-domains list in mode 3");
+ Services.dns.clearCache(true);
+ setModeAndURI(3, "doh?responseIP=192.192.192.192");
+ Services.prefs.setCharPref("network.trr.excluded-domains", "");
+ Services.prefs.setCharPref("network.trr.builtin-excluded-domains", "");
+
+ await new TRRDNSListener("excluded", "192.192.192.192", true);
+
+ Services.dns.clearCache(true);
+ Services.prefs.setCharPref("network.trr.excluded-domains", "excluded");
+
+ await new TRRDNSListener("excluded", "127.0.0.1");
+
+ // Test .local
+ Services.dns.clearCache(true);
+ Services.prefs.setCharPref("network.trr.excluded-domains", "excluded,local");
+
+ await new TRRDNSListener("test.local", "127.0.0.1");
+
+ // Test .other
+ Services.dns.clearCache(true);
+ Services.prefs.setCharPref(
+ "network.trr.excluded-domains",
+ "excluded,local,other"
+ );
+
+ await new TRRDNSListener("domain.other", "127.0.0.1");
+}
+
+async function test25e() {
+ info("Check captivedetect.canonicalURL is resolved via native DNS in mode 3");
+ Services.dns.clearCache(true);
+ setModeAndURI(3, "doh?responseIP=192.192.192.192");
+
+ const cpServer = new HttpServer();
+ cpServer.registerPathHandler(
+ "/cp",
+ function handleRawData(request, response) {
+ response.setHeader("Content-Type", "text/plain", false);
+ response.setHeader("Cache-Control", "no-cache", false);
+ response.bodyOutputStream.write("data", 4);
+ }
+ );
+ cpServer.start(-1);
+ cpServer.identity.setPrimary(
+ "http",
+ "detectportal.firefox.com",
+ cpServer.identity.primaryPort
+ );
+ let cpPromise = topicObserved("captive-portal-login");
+
+ Services.prefs.setCharPref(
+ "captivedetect.canonicalURL",
+ `http://detectportal.firefox.com:${cpServer.identity.primaryPort}/cp`
+ );
+ Services.prefs.setBoolPref("network.captive-portal-service.testMode", true);
+ Services.prefs.setBoolPref("network.captive-portal-service.enabled", true);
+
+ // The captive portal has to have used native DNS, otherwise creating
+ // a socket to a non-local IP would trigger a crash.
+ await cpPromise;
+ // // Simply resolving the captive portal domain should still use TRR
+ await new TRRDNSListener("detectportal.firefox.com", "192.192.192.192");
+
+ Services.prefs.clearUserPref("network.captive-portal-service.enabled");
+ Services.prefs.clearUserPref("network.captive-portal-service.testMode");
+ Services.prefs.clearUserPref("captivedetect.canonicalURL");
+
+ await new Promise(resolve => cpServer.stop(resolve));
+}
+
+async function test_parentalcontrols_mode3() {
+ info("Check DoH isn't used when parental controls are enabled in mode 3");
+ Services.dns.clearCache(true);
+ setModeAndURI(3, "doh?responseIP=192.192.192.192");
+ await SetParentalControlEnabled(true);
+ await new TRRDNSListener("www.example.com", "127.0.0.1");
+ await SetParentalControlEnabled(false);
+}
+
+async function test_builtin_excluded_domains_mode3() {
+ info("Check Do53 used for domains in builtin-excluded-domians list, mode 3");
+ Services.dns.clearCache(true);
+ setModeAndURI(3, "doh?responseIP=192.192.192.192");
+ Services.prefs.setCharPref("network.trr.excluded-domains", "");
+ Services.prefs.setCharPref(
+ "network.trr.builtin-excluded-domains",
+ "excluded"
+ );
+
+ await new TRRDNSListener("excluded", "127.0.0.1");
+
+ // Test .local
+ Services.dns.clearCache(true);
+ Services.prefs.setCharPref(
+ "network.trr.builtin-excluded-domains",
+ "excluded,local"
+ );
+
+ await new TRRDNSListener("test.local", "127.0.0.1");
+
+ // Test .other
+ Services.dns.clearCache(true);
+ Services.prefs.setCharPref(
+ "network.trr.builtin-excluded-domains",
+ "excluded,local,other"
+ );
+
+ await new TRRDNSListener("domain.other", "127.0.0.1");
+}
+
+async function count_cookies() {
+ info("Check that none of the requests have set any cookies.");
+ Assert.equal(Services.cookies.countCookiesFromHost("example.com"), 0);
+ Assert.equal(Services.cookies.countCookiesFromHost("foo.example.com."), 0);
+}
+
+async function test_connection_closed() {
+ info("Check we handle it correctly when the connection is closed");
+ Services.dns.clearCache(true);
+ setModeAndURI(3, "doh?responseIP=2.2.2.2");
+ Services.prefs.setCharPref("network.trr.excluded-domains", "");
+ // We don't need to wait for 30 seconds for the request to fail
+ Services.prefs.setIntPref("network.trr.request_timeout_mode_trronly_ms", 500);
+ // bootstrap
+ Services.prefs.clearUserPref("network.dns.localDomains");
+ Services.prefs.setCharPref("network.trr.bootstrapAddr", "127.0.0.1");
+
+ await new TRRDNSListener("bar.example.com", "2.2.2.2");
+
+ // makes the TRR connection shut down.
+ let { inStatus } = await new TRRDNSListener("closeme.com", undefined, false);
+ Assert.ok(
+ !Components.isSuccessCode(inStatus),
+ `${inStatus} should be an error code`
+ );
+ await new TRRDNSListener("bar2.example.com", "2.2.2.2");
+
+ // No bootstrap this time
+ Services.prefs.clearUserPref("network.trr.bootstrapAddr");
+
+ Services.dns.clearCache(true);
+ Services.prefs.setCharPref("network.trr.excluded-domains", "excluded,local");
+ Services.prefs.setCharPref("network.dns.localDomains", TRR_Domain);
+
+ await new TRRDNSListener("bar.example.com", "2.2.2.2");
+
+ // makes the TRR connection shut down.
+ ({ inStatus } = await new TRRDNSListener("closeme.com", undefined, false));
+ Assert.ok(
+ !Components.isSuccessCode(inStatus),
+ `${inStatus} should be an error code`
+ );
+ await new TRRDNSListener("bar2.example.com", "2.2.2.2");
+
+ // No local domains either
+ Services.dns.clearCache(true);
+ Services.prefs.setCharPref("network.trr.excluded-domains", "excluded");
+ Services.prefs.clearUserPref("network.dns.localDomains");
+ Services.prefs.clearUserPref("network.trr.bootstrapAddr");
+
+ await new TRRDNSListener("bar.example.com", "2.2.2.2");
+
+ // makes the TRR connection shut down.
+ ({ inStatus } = await new TRRDNSListener("closeme.com", undefined, false));
+ Assert.ok(
+ !Components.isSuccessCode(inStatus),
+ `${inStatus} should be an error code`
+ );
+ await new TRRDNSListener("bar2.example.com", "2.2.2.2");
+
+ // Now make sure that even in mode 3 without a bootstrap address
+ // we are able to restart the TRR connection if it drops - the TRR service
+ // channel will use regular DNS to resolve the TRR address.
+ Services.dns.clearCache(true);
+ Services.prefs.setCharPref("network.trr.excluded-domains", "");
+ Services.prefs.setCharPref("network.trr.builtin-excluded-domains", "");
+ Services.prefs.clearUserPref("network.dns.localDomains");
+ Services.prefs.clearUserPref("network.trr.bootstrapAddr");
+
+ await new TRRDNSListener("bar.example.com", "2.2.2.2");
+
+ // makes the TRR connection shut down.
+ ({ inStatus } = await new TRRDNSListener("closeme.com", undefined, false));
+ Assert.ok(
+ !Components.isSuccessCode(inStatus),
+ `${inStatus} should be an error code`
+ );
+ Services.dns.clearCache(true);
+ await new TRRDNSListener("bar2.example.com", "2.2.2.2");
+
+ // This test exists to document what happens when we're in TRR only mode
+ // and we don't set a bootstrap address. We use DNS to resolve the
+ // initial URI, but if the connection fails, we don't fallback to DNS
+ Services.dns.clearCache(true);
+ setModeAndURI(2, "doh?responseIP=9.9.9.9");
+ Services.prefs.setCharPref("network.dns.localDomains", "closeme.com");
+ Services.prefs.clearUserPref("network.trr.bootstrapAddr");
+
+ await new TRRDNSListener("bar.example.com", "9.9.9.9");
+
+ // makes the TRR connection shut down. Should fallback to DNS
+ await new TRRDNSListener("closeme.com", "127.0.0.1");
+ // TRR should be back up again
+ await new TRRDNSListener("bar2.example.com", "9.9.9.9");
+}
+
+async function test_fetch_time() {
+ info("Verifying timing");
+ Services.dns.clearCache(true);
+ setModeAndURI(2, "doh?responseIP=2.2.2.2&delayIPv4=20");
+
+ await new TRRDNSListener("bar_time.example.com", "2.2.2.2", true, 20);
+
+ // gets an error from DoH. It will fall back to regular DNS. The TRR timing should be 0.
+ Services.dns.clearCache(true);
+ setModeAndURI(2, "404&delayIPv4=20");
+
+ await new TRRDNSListener("bar_time1.example.com", "127.0.0.1", true, 0);
+
+ // check an excluded domain. It should fall back to regular DNS. The TRR timing should be 0.
+ Services.prefs.setCharPref(
+ "network.trr.excluded-domains",
+ "bar_time2.example.com"
+ );
+ for (let strictMode of [true, false]) {
+ info("Strict mode: " + strictMode);
+ Services.prefs.setBoolPref(
+ "network.trr.strict_native_fallback",
+ strictMode
+ );
+ Services.dns.clearCache(true);
+ setModeAndURI(2, "doh?responseIP=2.2.2.2&delayIPv4=20");
+ await new TRRDNSListener("bar_time2.example.com", "127.0.0.1", true, 0);
+ }
+
+ Services.prefs.setCharPref("network.trr.excluded-domains", "");
+
+ // verify RFC1918 address from the server is rejected and the TRR timing will be not set because the response will be from the native resolver.
+ Services.dns.clearCache(true);
+ setModeAndURI(2, "doh?responseIP=192.168.0.1&delayIPv4=20");
+ await new TRRDNSListener("rfc1918_time.example.com", "127.0.0.1", true, 0);
+}
+
+async function test_fqdn() {
+ info("Test that we handle FQDN encoding and decoding properly");
+ Services.dns.clearCache(true);
+ setModeAndURI(3, "doh?responseIP=9.8.7.6");
+
+ await new TRRDNSListener("fqdn.example.org.", "9.8.7.6");
+
+ // GET
+ Services.dns.clearCache(true);
+ Services.prefs.setBoolPref("network.trr.useGET", true);
+ await new TRRDNSListener("fqdn_get.example.org.", "9.8.7.6");
+
+ Services.prefs.clearUserPref("network.trr.useGET");
+}
+
+async function test_ipv6_trr_fallback() {
+ info("Testing fallback with ipv6");
+ Services.dns.clearCache(true);
+
+ setModeAndURI(2, "doh?responseIP=4.4.4.4");
+ const override = Cc["@mozilla.org/network/native-dns-override;1"].getService(
+ Ci.nsINativeDNSResolverOverride
+ );
+ gOverride.addIPOverride("ipv6.host.com", "1:1::2");
+
+ // Should not fallback to Do53 because A request for ipv6.host.com returns
+ // 4.4.4.4
+ let { inStatus } = await new TRRDNSListener("ipv6.host.com", {
+ flags: Ci.nsIDNSService.RESOLVE_DISABLE_IPV4,
+ expectedSuccess: false,
+ });
+ equal(inStatus, Cr.NS_ERROR_UNKNOWN_HOST);
+
+ // This time both requests fail, so we do fall back
+ Services.dns.clearCache(true);
+ setModeAndURI(2, "doh?responseIP=none");
+ await new TRRDNSListener("ipv6.host.com", "1:1::2");
+
+ info("In strict mode, the lookup should fail when both reqs fail.");
+ Services.dns.clearCache(true);
+ Services.prefs.setBoolPref("network.trr.strict_native_fallback", true);
+ setModeAndURI(2, "doh?responseIP=none");
+ await new TRRDNSListener("ipv6.host.com", "1:1::2");
+ Services.prefs.setBoolPref("network.trr.strict_native_fallback", false);
+
+ override.clearOverrides();
+}
+
+async function test_ipv4_trr_fallback() {
+ info("Testing fallback with ipv4");
+ Services.dns.clearCache(true);
+
+ setModeAndURI(2, "doh?responseIP=1:2::3");
+ const override = Cc["@mozilla.org/network/native-dns-override;1"].getService(
+ Ci.nsINativeDNSResolverOverride
+ );
+ gOverride.addIPOverride("ipv4.host.com", "3.4.5.6");
+
+ // Should not fallback to Do53 because A request for ipv4.host.com returns
+ // 1:2::3
+ let { inStatus } = await new TRRDNSListener("ipv4.host.com", {
+ flags: Ci.nsIDNSService.RESOLVE_DISABLE_IPV6,
+ expectedSuccess: false,
+ });
+ equal(inStatus, Cr.NS_ERROR_UNKNOWN_HOST);
+
+ // This time both requests fail, so we do fall back
+ Services.dns.clearCache(true);
+ setModeAndURI(2, "doh?responseIP=none");
+ await new TRRDNSListener("ipv4.host.com", "3.4.5.6");
+
+ // No fallback with strict mode.
+ Services.dns.clearCache(true);
+ Services.prefs.setBoolPref("network.trr.strict_native_fallback", true);
+ setModeAndURI(2, "doh?responseIP=none");
+ await new TRRDNSListener("ipv4.host.com", "3.4.5.6");
+ Services.prefs.setBoolPref("network.trr.strict_native_fallback", false);
+
+ override.clearOverrides();
+}
+
+async function test_no_retry_without_doh() {
+ info("Bug 1648147 - if the TRR returns 0.0.0.0 we should not retry with DNS");
+ Services.prefs.setBoolPref("network.trr.fallback-on-zero-response", false);
+
+ async function test(url, ip) {
+ setModeAndURI(2, `doh?responseIP=${ip}`);
+
+ // Requests to 0.0.0.0 are usually directed to localhost, so let's use a port
+ // we know isn't being used - 666 (Doom)
+ let chan = makeChan(url, Ci.nsIRequest.TRR_DEFAULT_MODE);
+ let statusCounter = {
+ statusCount: {},
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIInterfaceRequestor",
+ "nsIProgressEventSink",
+ ]),
+ getInterface(iid) {
+ return this.QueryInterface(iid);
+ },
+ onProgress(request, progress, progressMax) {},
+ onStatus(request, status, statusArg) {
+ this.statusCount[status] = 1 + (this.statusCount[status] || 0);
+ },
+ };
+ chan.notificationCallbacks = statusCounter;
+ await new Promise(resolve =>
+ chan.asyncOpen(new ChannelListener(resolve, null, CL_EXPECT_FAILURE))
+ );
+ equal(
+ statusCounter.statusCount[0x4b000b],
+ 1,
+ "Expecting only one instance of NS_NET_STATUS_RESOLVED_HOST"
+ );
+ equal(
+ statusCounter.statusCount[0x4b0007],
+ 1,
+ "Expecting only one instance of NS_NET_STATUS_CONNECTING_TO"
+ );
+ }
+
+ for (let strictMode of [true, false]) {
+ info("Strict mode: " + strictMode);
+ Services.prefs.setBoolPref(
+ "network.trr.strict_native_fallback",
+ strictMode
+ );
+ await test(`http://unknown.ipv4.stuff:666/path`, "0.0.0.0");
+ await test(`http://unknown.ipv6.stuff:666/path`, "::");
+ }
+}
+
+async function test_connection_reuse_and_cycling() {
+ Services.dns.clearCache(true);
+ Services.prefs.setIntPref("network.trr.request_timeout_ms", 500);
+ Services.prefs.setIntPref(
+ "network.trr.strict_fallback_request_timeout_ms",
+ 500
+ );
+ Services.prefs.setIntPref("network.trr.request_timeout_mode_trronly_ms", 500);
+
+ setModeAndURI(2, `doh?responseIP=9.8.7.6`);
+ Services.prefs.setBoolPref("network.trr.strict_native_fallback", true);
+ Services.prefs.setCharPref("network.trr.confirmationNS", "example.com");
+ await TestUtils.waitForCondition(
+ // 2 => CONFIRM_OK
+ () => Services.dns.currentTrrConfirmationState == 2,
+ `Timed out waiting for confirmation success. Currently ${Services.dns.currentTrrConfirmationState}`,
+ 1,
+ 5000
+ );
+
+ // Setting conncycle=true in the URI. Server will start logging reqs.
+ // We will do a specific sequence of lookups, then fetch the log from
+ // the server and check that it matches what we'd expect.
+ setModeAndURI(2, `doh?responseIP=9.8.7.6&conncycle=true`);
+ await TestUtils.waitForCondition(
+ // 2 => CONFIRM_OK
+ () => Services.dns.currentTrrConfirmationState == 2,
+ `Timed out waiting for confirmation success. Currently ${Services.dns.currentTrrConfirmationState}`,
+ 1,
+ 5000
+ );
+ // Confirmation upon uri-change will have created one req.
+
+ // Two reqs for each bar1 and bar2 - A + AAAA.
+ await new TRRDNSListener("bar1.example.org.", "9.8.7.6");
+ await new TRRDNSListener("bar2.example.org.", "9.8.7.6");
+ // Total so far: (1) + 2 + 2 = 5
+
+ // Two reqs that fail, one Confirmation req, two retried reqs that succeed.
+ await new TRRDNSListener("newconn.example.org.", "9.8.7.6");
+ await TestUtils.waitForCondition(
+ // 2 => CONFIRM_OK
+ () => Services.dns.currentTrrConfirmationState == 2,
+ `Timed out waiting for confirmation success. Currently ${Services.dns.currentTrrConfirmationState}`,
+ 1,
+ 5000
+ );
+ // Total so far: (5) + 2 + 1 + 2 = 10
+
+ // Two reqs for each bar3 and bar4 .
+ await new TRRDNSListener("bar3.example.org.", "9.8.7.6");
+ await new TRRDNSListener("bar4.example.org.", "9.8.7.6");
+ // Total so far: (10) + 2 + 2 = 14.
+
+ // Two reqs that fail, one Confirmation req, two retried reqs that succeed.
+ await new TRRDNSListener("newconn2.example.org.", "9.8.7.6");
+ await TestUtils.waitForCondition(
+ // 2 => CONFIRM_OK
+ () => Services.dns.currentTrrConfirmationState == 2,
+ `Timed out waiting for confirmation success. Currently ${Services.dns.currentTrrConfirmationState}`,
+ 1,
+ 5000
+ );
+ // Total so far: (14) + 2 + 1 + 2 = 19
+
+ // Two reqs for each bar5 and bar6 .
+ await new TRRDNSListener("bar5.example.org.", "9.8.7.6");
+ await new TRRDNSListener("bar6.example.org.", "9.8.7.6");
+ // Total so far: (19) + 2 + 2 = 23
+
+ let chan = makeChan(
+ `https://foo.example.com:${h2Port}/get-doh-req-port-log`,
+ Ci.nsIRequest.TRR_DISABLED_MODE
+ );
+ let dohReqPortLog = await new Promise(resolve =>
+ chan.asyncOpen(
+ new ChannelListener((stuff, buffer) => {
+ resolve(JSON.parse(buffer));
+ })
+ )
+ );
+
+ // Since the actual ports seen will vary at runtime, we use placeholders
+ // instead in our expected output definition. For example, if two entries
+ // both have "port1", it means they both should have the same port in the
+ // server's log.
+ // For reqs that fail and trigger a Confirmation + retry, the retried reqs
+ // might not re-use the new connection created for Confirmation due to a
+ // race, so we have an extra alternate expected port for them. This lets
+ // us test that they use *a* new port even if it's not *the* new port.
+ // Subsequent lookups are not affected, they will use the same conn as
+ // the Confirmation req.
+ let expectedLogTemplate = [
+ ["example.com", "port1"],
+ ["bar1.example.org", "port1"],
+ ["bar1.example.org", "port1"],
+ ["bar2.example.org", "port1"],
+ ["bar2.example.org", "port1"],
+ ["newconn.example.org", "port1"],
+ ["newconn.example.org", "port1"],
+ ["example.com", "port2"],
+ ["newconn.example.org", "port2"],
+ ["newconn.example.org", "port2"],
+ ["bar3.example.org", "port2"],
+ ["bar3.example.org", "port2"],
+ ["bar4.example.org", "port2"],
+ ["bar4.example.org", "port2"],
+ ["newconn2.example.org", "port2"],
+ ["newconn2.example.org", "port2"],
+ ["example.com", "port3"],
+ ["newconn2.example.org", "port3"],
+ ["newconn2.example.org", "port3"],
+ ["bar5.example.org", "port3"],
+ ["bar5.example.org", "port3"],
+ ["bar6.example.org", "port3"],
+ ["bar6.example.org", "port3"],
+ ];
+
+ if (expectedLogTemplate.length != dohReqPortLog.length) {
+ // This shouldn't happen, and if it does, we'll fail the assertion
+ // below. But first dump the whole server-side log to help with
+ // debugging should we see a failure. Most likely cause would be
+ // that another consumer of TRR happened to make a request while
+ // the test was running and polluted the log.
+ info(dohReqPortLog);
+ }
+
+ equal(
+ expectedLogTemplate.length,
+ dohReqPortLog.length,
+ "Correct number of req log entries"
+ );
+
+ let seenPorts = new Set();
+ // This is essentially a symbol table - as we iterate through the log
+ // we will assign the actual seen port numbers to the placeholders.
+ let seenPortsByExpectedPort = new Map();
+
+ for (let i = 0; i < expectedLogTemplate.length; i++) {
+ let expectedName = expectedLogTemplate[i][0];
+ let expectedPort = expectedLogTemplate[i][1];
+ let seenName = dohReqPortLog[i][0];
+ let seenPort = dohReqPortLog[i][1];
+ info(`Checking log entry. Name: ${seenName}, Port: ${seenPort}`);
+ equal(expectedName, seenName, "Name matches for entry " + i);
+ if (!seenPortsByExpectedPort.has(expectedPort)) {
+ ok(!seenPorts.has(seenPort), "Port should not have been previously used");
+ seenPorts.add(seenPort);
+ seenPortsByExpectedPort.set(expectedPort, seenPort);
+ } else {
+ equal(
+ seenPort,
+ seenPortsByExpectedPort.get(expectedPort),
+ "Connection was reused as expected"
+ );
+ }
+ }
+}
diff --git a/netwerk/test/unit/xpcshell.toml b/netwerk/test/unit/xpcshell.toml
new file mode 100644
index 0000000000..dd5957abdb
--- /dev/null
+++ b/netwerk/test/unit/xpcshell.toml
@@ -0,0 +1,1270 @@
+[DEFAULT]
+head = "head_channels.js head_cache.js head_cache2.js head_cookies.js head_servers.js head_trr.js head_http3.js head_telemetry.js head_websocket.js head_webtransport.js"
+support-files = [
+ "http2-ca.pem",
+ "proxy-ca.pem",
+ "client-cert.p12",
+ "data/cookies_v10.sqlite",
+ "data/image.png",
+ "data/system_root.lnk",
+ "data/test_psl.txt",
+ "data/test_readline1.txt",
+ "data/test_readline2.txt",
+ "data/test_readline3.txt",
+ "data/test_readline4.txt",
+ "data/test_readline5.txt",
+ "data/test_readline6.txt",
+ "data/test_readline7.txt",
+ "data/test_readline8.txt",
+ "data/signed_win.exe",
+ "socks_client_subprocess.js",
+ "test_link.desktop",
+ "test_link.url",
+ "test_link.lnk",
+ "../../dns/effective_tld_names.dat",
+ "test_alt-data_cross_process.js",
+ "trr_common.js",
+ "test_http3_prio_helpers.js",
+ "http2_test_common.js",
+]
+
+# dom.serviceWorkers.enabled is currently set to false in StaticPrefList.yaml
+# and enabled individually by app prefs, so for the xpcshell tests that involve
+# interception, we need to explicitly enable the pref.
+# Consider enabling it in StaticPrefList.yaml
+# https://bugzilla.mozilla.org/show_bug.cgi?id=1816325
+# Several tests rely on redirecting to data: URIs, which was allowed for a long
+# time but now forbidden. So we enable it just for these tests.
+prefs = [
+ "dom.serviceWorkers.enabled=true",
+ "network.allow_redirect_to_data=true",
+]
+
+["test_1073747.js"]
+
+["test_304_headers.js"]
+
+["test_304_responses.js"]
+
+["test_307_redirect.js"]
+
+["test_421.js"]
+
+["test_MIME_params.js"]
+
+["test_NetUtil.js"]
+
+["test_SuperfluousAuth.js"]
+
+["test_URIs.js"]
+# Intermittent time-outs on Android, bug 1285020
+requesttimeoutfactor = 2
+
+["test_URIs2.js"]
+# Intermittent time-outs on Android, bug 1285020
+requesttimeoutfactor = 2
+
+["test_XHR_redirects.js"]
+
+["test_about_networking.js"]
+
+["test_about_protocol.js"]
+
+["test_aboutblank.js"]
+
+["test_addr_in_use_error.js"]
+
+["test_alt-data_closeWithStatus.js"]
+
+["test_alt-data_overwrite.js"]
+
+["test_alt-data_simple.js"]
+skip-if = ["os == 'win'"] # Bug 1760081
+run-sequentially = "very high failure rate in parallel"
+
+["test_alt-data_stream.js"]
+
+["test_alt-data_too_big.js"]
+
+["test_altsvc.js"]
+run-sequentially = "node server exceptions dont replay well"
+
+["test_altsvc_http3.js"]
+skip-if = [
+ "os == 'android'",
+ "os == 'win' && msix", # Bug 1807931
+]
+run-sequentially = "http3server"
+
+["test_altsvc_pref.js"]
+skip-if = [
+ "os == 'android'",
+ "os == 'win' && msix", # https://bugzilla.mozilla.org/show_bug.cgi?id=1807931
+]
+
+["test_anonymous-coalescing.js"]
+
+["test_auth_dialog_permission.js"]
+
+["test_auth_jar.js"]
+
+["test_auth_multiple.js"]
+
+["test_auth_proxy.js"]
+
+["test_authentication.js"]
+
+["test_authpromptwrapper.js"]
+
+["test_backgroundfilesaver.js"]
+
+["test_be_conservative.js"]
+firefox-appdir = "browser"
+
+["test_be_conservative_error_handling.js"]
+firefox-appdir = "browser"
+
+["test_bhttp.js"]
+
+["test_blob_channelname.js"]
+
+["test_brotli_decoding.js"]
+
+["test_brotli_http.js"]
+
+["test_brotli_unknown_content_type.js"]
+
+["test_bug203271.js"]
+
+["test_bug248970_cache.js"]
+
+["test_bug248970_cookie.js"]
+
+["test_bug261425.js"]
+
+["test_bug263127.js"]
+
+["test_bug282432.js"]
+
+["test_bug321706.js"]
+
+["test_bug331825.js"]
+
+["test_bug336501.js"]
+
+["test_bug337744.js"]
+
+["test_bug368702.js"]
+
+["test_bug369787.js"]
+
+["test_bug371473.js"]
+
+["test_bug376844.js"]
+
+["test_bug376865.js"]
+
+["test_bug379034.js"]
+
+["test_bug380994.js"]
+
+["test_bug388281.js"]
+
+["test_bug396389.js"]
+
+["test_bug401564.js"]
+
+["test_bug411952.js"]
+
+["test_bug412457.js"]
+skip-if = ["appname == 'thunderbird'"]
+
+["test_bug412945.js"]
+
+["test_bug414122.js"]
+
+["test_bug427957.js"]
+
+["test_bug429347.js"]
+
+["test_bug455311.js"]
+
+["test_bug464591.js"]
+skip-if = ["appname == 'thunderbird'"]
+
+["test_bug468426.js"]
+
+["test_bug468594.js"]
+
+["test_bug470716.js"]
+
+["test_bug477578.js"]
+
+["test_bug479413.js"]
+
+["test_bug479485.js"]
+
+["test_bug482601.js"]
+
+["test_bug482934.js"]
+
+["test_bug490095.js"]
+
+["test_bug504014.js"]
+
+["test_bug510359.js"]
+
+["test_bug526789.js"]
+
+["test_bug528292.js"]
+
+["test_bug536324_64bit_content_length.js"]
+
+["test_bug540566.js"]
+
+["test_bug553970.js"]
+
+["test_bug561042.js"]
+
+["test_bug561276.js"]
+
+["test_bug580508.js"]
+
+["test_bug586908.js"]
+
+["test_bug596443.js"]
+
+["test_bug618835.js"]
+
+["test_bug633743.js"]
+
+["test_bug650522.js"]
+
+["test_bug650995.js"]
+
+["test_bug652761.js"]
+
+["test_bug654926.js"]
+
+["test_bug654926_doom_and_read.js"]
+
+["test_bug654926_test_seek.js"]
+
+["test_bug659569.js"]
+
+["test_bug660066.js"]
+
+["test_bug667087.js"]
+
+["test_bug667818.js"]
+
+["test_bug667907.js"]
+
+["test_bug669001.js"]
+
+["test_bug770243.js"]
+
+["test_bug812167.js"]
+
+["test_bug826063.js"]
+
+["test_bug856978.js"]
+
+["test_bug894586.js"]
+# Allocating 4GB might actually succeed on 64 bit machines
+skip-if = ["bits != 32"]
+
+["test_bug935499.js"]
+
+["test_bug1064258.js"]
+
+["test_bug1177909.js"]
+
+["test_bug1195415.js"]
+
+["test_bug1218029.js"]
+
+["test_bug1279246.js"]
+
+["test_bug1312774_http1.js"]
+
+["test_bug1312782_http1.js"]
+skip-if = ["os == 'android'"] # Bug 1700483
+
+["test_bug1355539_http1.js"]
+
+["test_bug1378385_http1.js"]
+
+["test_bug1411316_http1.js"]
+
+["test_bug1527293.js"]
+
+["test_bug1683176.js"]
+skip-if = [
+ "os == 'android'",
+ "!debug",
+ "os == 'win' && socketprocess_networking",
+]
+
+["test_bug1725766.js"]
+skip-if = ["os == 'android'"] # skip because of bug 1589327
+
+["test_cache-control_request.js"]
+
+["test_cache-entry-id.js"]
+
+["test_cache2-00-service-get.js"]
+
+["test_cache2-01-basic.js"]
+
+["test_cache2-01a-basic-readonly.js"]
+
+["test_cache2-01b-basic-datasize.js"]
+
+["test_cache2-01c-basic-hasmeta-only.js"]
+
+["test_cache2-01d-basic-not-wanted.js"]
+
+["test_cache2-01e-basic-bypass-if-busy.js"]
+
+["test_cache2-01f-basic-openTruncate.js"]
+
+["test_cache2-02-open-non-existing.js"]
+
+["test_cache2-02b-open-non-existing-and-doom.js"]
+
+["test_cache2-03-oncacheentryavail-throws.js"]
+
+["test_cache2-04-oncacheentryavail-throws2x.js"]
+
+["test_cache2-05-visit.js"]
+
+["test_cache2-06-pb-mode.js"]
+
+["test_cache2-07-visit-memory.js"]
+
+["test_cache2-07a-open-memory.js"]
+
+["test_cache2-08-evict-disk-by-memory-storage.js"]
+
+["test_cache2-09-evict-disk-by-uri.js"]
+
+["test_cache2-10-evict-direct.js"]
+
+["test_cache2-10b-evict-direct-immediate.js"]
+
+["test_cache2-11-evict-memory.js"]
+
+["test_cache2-12-evict-disk.js"]
+
+["test_cache2-13-evict-non-existing.js"]
+
+["test_cache2-14-concurent-readers.js"]
+
+["test_cache2-14b-concurent-readers-complete.js"]
+
+["test_cache2-15-conditional-304.js"]
+
+["test_cache2-16-conditional-200.js"]
+
+["test_cache2-17-evict-all.js"]
+
+["test_cache2-18-not-valid.js"]
+
+["test_cache2-19-range-206.js"]
+
+["test_cache2-20-range-200.js"]
+
+["test_cache2-21-anon-storage.js"]
+
+["test_cache2-22-anon-visit.js"]
+
+["test_cache2-23-read-over-chunk.js"]
+
+["test_cache2-24-exists.js"]
+
+["test_cache2-25-chunk-memory-limit.js"]
+
+["test_cache2-26-no-outputstream-open.js"]
+
+["test_cache2-27-force-valid-for.js"]
+
+["test_cache2-28-last-access-attrs.js"]
+# This test will be fixed in bug 1067931
+skip-if = ["true"]
+
+["test_cache2-28a-OPEN_SECRETLY.js"]
+# This test will be fixed in bug 1067931
+skip-if = ["true"]
+
+["test_cache2-29a-concurrent_read_resumable_entry_size_zero.js"]
+
+["test_cache2-29b-concurrent_read_non-resumable_entry_size_zero.js"]
+
+["test_cache2-29c-concurrent_read_half-interrupted.js"]
+
+["test_cache2-29d-concurrent_read_half-corrupted-206.js"]
+
+["test_cache2-29e-concurrent_read_half-non-206-response.js"]
+
+["test_cache2-30a-entry-pinning.js"]
+
+["test_cache2-30b-pinning-storage-clear.js"]
+
+["test_cache2-30c-pinning-deferred-doom.js"]
+
+["test_cache2-30d-pinning-WasEvicted-API.js"]
+
+["test_cache2-31-visit-all.js"]
+
+["test_cache2-32-clear-origin.js"]
+
+["test_cache_204_response.js"]
+
+["test_cache_jar.js"]
+
+["test_cacheflags.js"]
+
+["test_captive_portal_service.js"]
+run-sequentially = "node server exceptions dont replay well"
+
+["test_cert_info.js"]
+
+["test_cert_verification_failure.js"]
+run-sequentially = "node server exceptions dont replay well"
+
+["test_channel_close.js"]
+skip-if = ["os == 'win' && socketprocess_networking && !debug"]
+
+["test_channel_long_domain.js"]
+
+["test_channel_priority.js"]
+
+["test_chunked_responses.js"]
+prefs = ["security.allow_eval_with_system_principal=true"]
+
+["test_client_auth_with_proxy.js"]
+skip-if = ["os == 'android'"]
+
+["test_coaleasing_h2_and_h3_connection.js"]
+skip-if = [
+ "os == 'android'",
+ "os == 'win' && msix",
+] # https://bugzilla.mozilla.org/show_bug.cgi?id=1808049
+run-sequentially = "http3server"
+
+["test_compareURIs.js"]
+
+["test_compressappend.js"]
+
+["test_connection_based_auth.js"]
+
+["test_content_encoding_gzip.js"]
+
+["test_content_length_underrun.js"]
+
+["test_content_sniffer.js"]
+
+["test_cookie_blacklist.js"]
+
+["test_cookie_header.js"]
+
+["test_cookie_ipv6.js"]
+
+["test_cookie_partitioned_attribute.js"]
+
+["test_cookiejars.js"]
+
+["test_cookiejars_safebrowsing.js"]
+
+["test_cookies_async_failure.js"]
+skip-if = ["os == 'linux' && bits == 64 && !debug"] #Bug 1553353
+
+["test_cookies_partition_counting.js"]
+
+["test_cookies_privatebrowsing.js"]
+
+["test_cookies_profile_close.js"]
+skip-if = ["os == 'android'"] # Bug 1700483
+
+["test_cookies_purge_counting.js"]
+
+["test_cookies_purge_counting_per_host.js"]
+
+["test_cookies_read.js"]
+
+["test_cookies_sync_failure.js"]
+
+["test_cookies_thirdparty.js"]
+
+["test_cookies_thirdparty_session.js"]
+
+["test_cookies_upgrade_10.js"]
+
+["test_data_protocol.js"]
+
+["test_defaultURI.js"]
+
+["test_dns_by_type_resolve.js"]
+
+["test_dns_cancel.js"]
+skip-if = ["verify"]
+
+["test_dns_disable_ipv4.js"]
+
+["test_dns_disable_ipv6.js"]
+
+["test_dns_disabled.js"]
+
+["test_dns_localredirect.js"]
+
+["test_dns_offline.js"]
+
+["test_dns_onion.js"]
+
+["test_dns_originAttributes.js"]
+
+["test_dns_override.js"]
+
+["test_dns_override_for_localhost.js"]
+
+["test_dns_proxy_bypass.js"]
+
+["test_dns_retry.js"]
+skip-if = [
+ "os == 'mac'", # server on a local ipv6 is not started on mac
+ "socketprocess_networking",
+] # bug 1760106
+run-sequentially = "node server exceptions dont replay well"
+
+["test_dns_service.js"]
+
+["test_domain_eviction.js"]
+
+["test_dooh.js"]
+head = "head_channels.js head_cache.js head_cookies.js head_servers.js head_trr.js head_http3.js trr_common.js"
+run-sequentially = "node server exceptions dont replay well"
+skip-if = ["socketprocess_networking"]
+
+["test_doomentry.js"]
+
+["test_duplicate_headers.js"]
+
+["test_early_hint_listener.js"]
+skip-if = ["os == 'win' && msix"] # https://bugzilla.mozilla.org/show_bug.cgi?id=1807931
+
+["test_early_hint_listener_http2.js"]
+run-sequentially = "node server exceptions dont replay well"
+
+["test_ech_grease.js"]
+firefox-appdir = "browser"
+skip-if = ["tsan && socketprocess_networking"] # Bug 1808236
+
+["test_event_sink.js"]
+
+["test_eviction.js"]
+
+["test_extract_charset_from_content_type.js"]
+
+["test_file_protocol.js"]
+
+["test_filestreams.js"]
+
+["test_freshconnection.js"]
+
+["test_getHost.js"]
+
+["test_gio_protocol.js"]
+run-if = ["os == 'linux'"]
+
+["test_gre_resources.js"]
+
+["test_h2proxy_connection_limit.js"]
+run-sequentially = "node server exceptions dont replay well"
+
+["test_head.js"]
+
+["test_head_request_no_response_body.js"]
+
+["test_header_Accept-Language.js"]
+
+["test_header_Accept-Language_case.js"]
+
+["test_header_Server_Timing.js"]
+run-sequentially = "node server exceptions dont replay well"
+
+["test_headers.js"]
+
+["test_hostnameIsLocalIPAddress.js"]
+
+["test_hostnameIsSharedIPAddress.js"]
+
+["test_hpke_config_manager.js"]
+skip-if = ["!nightly_build"] # OHTTP Config manager not currently shipped to release.
+
+["test_http1-proxy.js"]
+
+["test_http2-proxy-failing.js"]
+run-sequentially = "node server exceptions dont replay well"
+
+["test_http2-proxy.js"]
+run-sequentially = "one http2 node proxy is used for all tests, this test is using global session counter"
+skip-if = ["os == 'android'"]
+
+["test_http2.js"]
+run-sequentially = "node server exceptions dont replay well"
+head = "head_channels.js head_cache.js head_cookies.js head_servers.js head_trr.js head_http3.js http2_test_common.js"
+
+["test_http2_with_proxy.js"]
+run-sequentially = "node server exceptions dont replay well"
+head = "head_channels.js head_cache.js head_cookies.js head_servers.js head_trr.js head_http3.js http2_test_common.js"
+
+["test_http3.js"]
+skip-if = [
+ "os == 'android'", # bug 1622901
+ "os == 'win' && msix", # https://bugzilla.mozilla.org/show_bug.cgi?id=1807931
+]
+run-sequentially = "http3server"
+
+["test_http3_0rtt.js"]
+skip-if = [
+ "os == 'win'",
+ "os == 'android'",
+]
+
+["test_http3_421.js"]
+skip-if = [
+ "os == 'android'",
+ "os == 'win' && msix", # https://bugzilla.mozilla.org/show_bug.cgi?id=1807931
+]
+run-sequentially = "http3server"
+
+["test_http3_alt_svc.js"]
+skip-if = [
+ "os == 'android'",
+ "os == 'win' && msix", # https://bugzilla.mozilla.org/show_bug.cgi?id=1807931
+]
+run-sequentially = "http3server"
+
+["test_http3_coalescing.js"]
+skip-if = [
+ "os == 'android'",
+ "socketprocess_networking",
+ "os == 'win' && msix", # https://bugzilla.mozilla.org/show_bug.cgi?id=1808049
+ "apple_silicon", # https://bugzilla.mozilla.org/show_bug.cgi?id=1866067
+]
+run-sequentially = "node server exceptions dont replay well"
+
+["test_http3_direct_proxy.js"]
+skip-if = [
+ "os == 'android'",
+ "os == 'win' && msix", # https://bugzilla.mozilla.org/show_bug.cgi?id=1808049
+]
+run-sequentially = "node server exceptions dont replay well"
+
+["test_http3_dns_retry.js"]
+skip-if = [
+ "os == 'android'",
+ "os == 'win' && msix",
+]
+run-sequentially = "node server exceptions dont replay well"
+
+["test_http3_early_hint_listener.js"]
+skip-if = [
+ "os == 'android'",
+ "os == 'linux'", # Bug 1773916
+ "os == 'win' && msix", # https://bugzilla.mozilla.org/show_bug.cgi?id=1808049
+]
+run-sequentially = "http3server"
+
+["test_http3_error_before_connect.js"]
+skip-if = [
+ "os == 'android'",
+ "os == 'win' && msix", # https://bugzilla.mozilla.org/show_bug.cgi?id=1807931
+]
+run-sequentially = "node server exceptions dont replay well"
+
+["test_http3_fast_fallback.js"]
+skip-if = [
+ "os == 'win'",
+ "os == 'android'",
+]
+run-sequentially = "node server exceptions dont replay well"
+
+["test_http3_fatal_stream_error.js"]
+skip-if = [
+ "os == 'android'",
+ "os == 'win' && msix", # https://bugzilla.mozilla.org/show_bug.cgi?id=1807931
+]
+run-sequentially = "node server exceptions dont replay well"
+
+["test_http3_large_post.js"]
+skip-if = [
+ "os == 'win'",
+ "os == 'android'",
+]
+
+["test_http3_large_post_telemetry.js"]
+disabled = "bug 1771744 - telemetry probe expired"
+# skip-if =
+# asan
+# tsan
+# os == 'win'
+# os == 'android'
+# socketprocess_networking
+
+["test_http3_perf.js"]
+skip-if = [
+ "os == 'android'",
+ "os == 'win' && msix", # https://bugzilla.mozilla.org/show_bug.cgi?id=1807931
+]
+run-sequentially = "http3server"
+
+["test_http3_prio_disabled.js"]
+skip-if = [
+ "os == 'android'",
+ "os == 'win' && msix", # https://bugzilla.mozilla.org/show_bug.cgi?id=1807931
+]
+run-sequentially = "http3server"
+
+["test_http3_prio_enabled.js"]
+skip-if = [
+ "os == 'android'",
+ "os == 'win' && msix", # https://bugzilla.mozilla.org/show_bug.cgi?id=1807931
+]
+run-sequentially = "http3server"
+
+["test_http3_server.js"]
+skip-if = [
+ "verify",
+ "os == 'android'",
+ "os == 'win' && msix", # https://bugzilla.mozilla.org/show_bug.cgi?id=1808049
+]
+run-sequentially = "node server exceptions dont replay well"
+
+["test_http3_server_not_existing.js"]
+skip-if = ["os == 'android'"]
+run-sequentially = "node server exceptions dont replay well"
+
+["test_http3_trans_close.js"]
+skip-if = [
+ "os == 'android'",
+ "os == 'win' && msix", # https://bugzilla.mozilla.org/show_bug.cgi?id=1807931
+]
+run-sequentially = "http3server"
+
+["test_http3_version1.js"]
+skip-if = [
+ "os == 'win'",
+ "os == 'android'",
+]
+run-sequentially = "node server exceptions dont replay well"
+
+["test_httpResponseTimeout.js"]
+skip-if = ["os == 'win' && socketprocess_networking"]
+
+["test_http_408_retry.js"]
+
+["test_http_headers.js"]
+
+["test_http_server_timing.js"]
+
+["test_http_sfv.js"]
+
+["test_httpauth.js"]
+
+["test_httpcancel.js"]
+
+["test_https_rr_ech_prefs.js"]
+skip-if = ["os == 'android'"]
+run-sequentially = "node server exceptions dont replay well"
+
+["test_https_rr_sorted_alpn.js"]
+skip-if = ["os == 'android'"]
+run-sequentially = "node server exceptions dont replay well"
+
+["test_httpssvc_ech_with_alpn.js"]
+skip-if = [
+ "os == 'android'", # bug 1622901
+ "os == 'mac' && !debug",
+ "asan",
+ "os == 'win' && msix", # https://bugzilla.mozilla.org/show_bug.cgi?id=1808048
+]
+run-sequentially = "node server exceptions dont replay well"
+
+["test_httpssvc_https_upgrade.js"]
+
+["test_httpssvc_iphint.js"]
+run-sequentially = "node server exceptions dont replay well"
+
+["test_httpssvc_priority.js"]
+run-sequentially = "node server exceptions dont replay well"
+
+["test_httpssvc_retry_with_ech.js"]
+skip-if = [
+ "os == 'android'", # bug 1622901
+ "os == 'mac' && !debug",
+ "asan",
+ "os == 'win' && msix", # https://bugzilla.mozilla.org/show_bug.cgi?id=1808048
+]
+run-sequentially = "node server exceptions dont replay well"
+
+["test_httpssvc_retry_without_ech.js"]
+skip-if = ["os == 'win' && msix"] # https://bugzilla.mozilla.org/show_bug.cgi?id=1808048
+run-sequentially = "node server exceptions dont replay well"
+
+["test_httpsuspend.js"]
+
+["test_idn_blacklist.js"]
+
+["test_idn_spoof.js"]
+
+["test_idn_urls.js"]
+
+["test_idna2008.js"]
+
+["test_idnservice.js"]
+
+["test_immutable.js"]
+run-sequentially = "node server exceptions dont replay well"
+
+["test_inhibit_caching.js"]
+
+["test_ioservice.js"]
+
+["test_large_port.js"]
+
+["test_loadgroup_cancel.js"]
+
+["test_localhost_offline.js"]
+
+["test_localstreams.js"]
+
+["test_mismatch_last-modified.js"]
+
+["test_mozTXTToHTMLConv.js"]
+
+["test_multipart_byteranges.js"]
+
+["test_multipart_streamconv-byte-by-byte.js"]
+
+["test_multipart_streamconv.js"]
+
+["test_multipart_streamconv_empty.js"]
+
+["test_multipart_streamconv_missing_boundary_lead_dashes.js"]
+
+["test_multipart_streamconv_missing_lead_boundary.js"]
+
+["test_nestedabout_serialize.js"]
+
+["test_net_addr.js"]
+# Bug 732363: test fails on windows for unknown reasons.
+skip-if = ["os == 'win'"]
+
+["test_network_connectivity_service.js"]
+
+["test_networking_over_socket_process.js"]
+skip-if = [
+ "os == 'android'",
+ "!socketprocess_networking",
+]
+run-sequentially = "node server exceptions dont replay well"
+
+["test_no_cookies_after_last_pb_exit.js"]
+
+["test_node_execute.js"]
+
+["test_nojsredir.js"]
+
+["test_non_ipv4_hostname_ending_in_number_cookie_db.js"]
+
+["test_nsIBufferedOutputStream_writeFrom_block.js"]
+
+["test_ntlm_authentication.js"]
+
+["test_ntlm_proxy_and_web_auth.js"]
+
+["test_ntlm_proxy_auth.js"]
+
+["test_ntlm_web_auth.js"]
+
+["test_oblivious_http.js"]
+
+["test_obs-fold.js"]
+
+["test_offline_status.js"]
+
+["test_ohttp.js"]
+
+["test_orb_empty_header.js"]
+
+["test_origin.js"]
+
+["test_original_sent_received_head.js"]
+
+["test_pac_reload_after_network_change.js"]
+
+["test_parse_content_type.js"]
+
+["test_partial_response_entry_size_smart_shrink.js"]
+
+["test_permmgr.js"]
+
+["test_ping_aboutnetworking.js"]
+skip-if = ["verify && os == 'mac'"]
+
+["test_plaintext_sniff.js"]
+skip-if = ["true"] # Causes sporatic oranges
+
+["test_port_remapping.js"]
+skip-if = ["os == 'win' && socketprocess_networking"]
+
+["test_post.js"]
+
+["test_predictor.js"]
+
+["test_private_cookie_changed.js"]
+
+["test_private_necko_channel.js"]
+
+["test_progress.js"]
+
+["test_progress_no_proxy_and_proxy.js"]
+skip-if = [
+ "os == 'win'",
+ "os == 'android'",
+]
+run-sequentially = "node server exceptions dont replay well"
+
+["test_protocolproxyservice-async-filters.js"]
+
+["test_protocolproxyservice.js"]
+skip-if = [
+ "apple_silicon", # bug 1707738
+ "tsan && socketprocess_networking", # Bug 1808235
+]
+
+["test_proxy-failover_canceled.js"]
+
+["test_proxy-failover_passing.js"]
+
+["test_proxy-replace_canceled.js"]
+
+["test_proxy-replace_passing.js"]
+
+["test_proxy-slow-upload.js"]
+
+["test_proxy_cancel.js"]
+run-sequentially = "node server exceptions dont replay well"
+
+["test_proxy_pac.js"]
+
+["test_proxyconnect.js"]
+skip-if = [
+ "tsan",
+ "socketprocess_networking", # Bug 1614708
+]
+
+["test_psl.js"]
+
+["test_race_cache_with_network.js"]
+skip-if = [
+ "os == 'win' && !debug", # Bug 1866777
+]
+
+["test_range_requests.js"]
+
+["test_rcwn_always_cache_new_content.js"]
+
+["test_rcwn_interrupted.js"]
+
+["test_readline.js"]
+
+["test_redirect-caching_canceled.js"]
+
+["test_redirect-caching_failure.js"]
+
+["test_redirect-caching_passing.js"]
+
+["test_redirect_baduri.js"]
+
+["test_redirect_canceled.js"]
+
+["test_redirect_different-protocol.js"]
+
+["test_redirect_failure.js"]
+
+["test_redirect_from_script.js"]
+
+["test_redirect_from_script_after-open_passing.js"]
+
+["test_redirect_history.js"]
+
+["test_redirect_loop.js"]
+
+["test_redirect_passing.js"]
+
+["test_redirect_protocol_telemetry.js"]
+
+["test_redirect_veto.js"]
+
+["test_reentrancy.js"]
+
+["test_referrer.js"]
+
+["test_referrer_cross_origin.js"]
+
+["test_referrer_policy.js"]
+
+["test_reopen.js"]
+
+["test_reply_without_content_type.js"]
+
+["test_resumable_channel.js"]
+
+["test_resumable_truncate.js"]
+
+["test_retry_0rtt.js"]
+skip-if = [
+ "verify && os == 'android'",
+ "os == 'win' && msix", # https://bugzilla.mozilla.org/show_bug.cgi?id=1808048
+]
+run-sequentially = "tlsserver uses fixed port"
+
+["test_safeoutputstream.js"]
+
+["test_safeoutputstream_append.js"]
+
+["test_schema_13_db.js"]
+
+["test_schema_12_migration.js"]
+
+["test_schema_10_migration.js"]
+
+["test_schema_2_migration.js"]
+
+["test_schema_3_migration.js"]
+
+["test_separate_connections.js"]
+
+["test_servers.js"]
+
+["test_signature_extraction.js"]
+skip-if = ["os != 'win'"]
+
+["test_simple.js"]
+
+["test_sockettransportsvc_available.js"]
+
+["test_socks.js"]
+skip-if = [
+ "os == 'mac' && debug", #Bug 1140656
+ "os == 'win' && msix", # https://bugzilla.mozilla.org/show_bug.cgi?id=1808049
+]
+# Bug 675039: test fails consistently on Android
+fail-if = ["os == 'android'"]
+# http2 unit tests require us to have node available to run the spdy and http2 server
+
+["test_speculative_connect.js"]
+
+["test_stale-while-revalidate_loop.js"]
+
+["test_stale-while-revalidate_max-age-0.js"]
+
+["test_stale-while-revalidate_negative.js"]
+
+["test_stale-while-revalidate_positive.js"]
+
+["test_standardurl.js"]
+
+["test_standardurl_default_port.js"]
+
+["test_standardurl_port.js"]
+
+["test_streamcopier.js"]
+
+["test_substituting_protocol_handler.js"]
+
+["test_suspend_channel_before_connect.js"]
+
+["test_suspend_channel_on_authRetry.js"]
+
+["test_suspend_channel_on_examine.js"]
+
+["test_suspend_channel_on_examine_merged_response.js"]
+
+["test_suspend_channel_on_modified.js"]
+
+["test_synthesized_response.js"]
+
+["test_throttlechannel.js"]
+
+["test_throttlequeue.js"]
+
+["test_throttling.js"]
+
+["test_tldservice_nextsubdomain.js"]
+
+["test_tls13_disabled.js"]
+skip-if = [
+ "os == 'android'",
+ "verify && os == 'win'",
+ "os == 'win' && msix", # https://bugzilla.mozilla.org/show_bug.cgi?id=1808049
+]
+run-sequentially = "node server exceptions dont replay well"
+
+["test_tls_flags.js"]
+skip-if = ["os == 'android' && processor == 'x86_64'"]
+
+["test_tls_flags_separate_connections.js"]
+
+["test_tls_server.js"]
+firefox-appdir = "browser"
+
+["test_tls_server_multiple_clients.js"]
+
+["test_traceable_channel.js"]
+
+["test_trackingProtection_annotateChannels.js"]
+
+["test_trr.js"]
+head = "head_channels.js head_cache.js head_cookies.js head_servers.js head_trr.js head_http3.js trr_common.js"
+run-sequentially = "very high failure rate in parallel"
+
+["test_trr_additional_section.js"]
+run-sequentially = "node server exceptions dont replay well"
+
+["test_trr_af_fallback.js"]
+
+["test_trr_blocklist.js"]
+run-sequentially = "node server exceptions dont replay well"
+
+["test_trr_cancel.js"]
+run-sequentially = "node server exceptions dont replay well"
+
+["test_trr_case_sensitivity.js"]
+run-sequentially = "node server exceptions dont replay well"
+
+["test_trr_cname_chain.js"]
+run-sequentially = "node server exceptions dont replay well"
+
+["test_trr_confirmation.js"]
+skip-if = [
+ "socketprocess_networking", # confirmation state isn't passed cross-process
+ "appname == 'thunderbird'", # bug 1760097
+]
+run-sequentially = "node server exceptions dont replay well"
+
+["test_trr_decoding.js"]
+
+["test_trr_domain.js"]
+
+["test_trr_enterprise_policy.js"]
+firefox-appdir = "browser" # needed for resource:///modules/policies/schema.sys.mjs to be registered
+skip-if = [
+ "os == 'android'",
+ "socketprocess_networking",
+]
+
+["test_trr_extended_error.js"]
+run-sequentially = "node server exceptions dont replay well"
+
+["test_trr_https_fallback.js"]
+skip-if = [
+ "asan",
+ "tsan",
+ "os == 'win'",
+ "os == 'android'",
+]
+run-sequentially = "node server exceptions dont replay well"
+
+["test_trr_httpssvc.js"]
+run-sequentially = "node server exceptions dont replay well"
+
+["test_trr_nat64.js"]
+run-sequentially = "node server exceptions dont replay well"
+
+["test_trr_noPrefetch.js"]
+
+["test_trr_proxy.js"]
+
+["test_trr_proxy_auth.js"]
+skip-if = [
+ "os == 'android'",
+ "socketprocess_networking",
+]
+
+["test_trr_strict_mode.js"]
+
+["test_trr_telemetry.js"]
+head = "head_channels.js head_cache.js head_cookies.js head_servers.js head_trr.js head_http3.js trr_common.js"
+skip-if = [
+ "os == 'android'",
+ "socketprocess_networking",
+]
+
+["test_trr_ttl.js"]
+
+["test_trr_with_proxy.js"]
+head = "head_channels.js head_cache.js head_cookies.js head_servers.js head_trr.js trr_common.js"
+skip-if = [
+ "os == 'android'",
+ "socketprocess_networking", # Bug 1808233
+]
+run-sequentially = "node server exceptions dont replay well"
+
+["test_udp_multicast.js"]
+
+["test_udpsocket.js"]
+
+["test_udpsocket_offline.js"]
+
+["test_unescapestring.js"]
+
+["test_unix_domain.js"]
+
+["test_uri_mutator.js"]
+
+["test_use_httpssvc.js"]
+run-sequentially = "node server exceptions dont replay well"
+
+["test_verify_traffic.js"]
+
+["test_websocket_500k.js"]
+skip-if = ["verify"]
+run-sequentially = "node server exceptions dont replay well"
+
+["test_websocket_fails.js"]
+run-sequentially = "node server exceptions dont replay well"
+skip-if = ["os == 'android' && verify"] # Bug 1804101
+
+["test_websocket_fails_2.js"]
+run-sequentially = "node server exceptions dont replay well"
+
+["test_websocket_offline.js"]
+
+["test_websocket_server.js"]
+run-sequentially = "node server exceptions dont replay well"
+
+["test_websocket_server_multiclient.js"]
+run-sequentially = "node server exceptions dont replay well"
+
+["test_websocket_with_h3_active.js"]
+skip-if = [
+ "os == 'android'",
+ "verify && os == 'win'",
+ "os == 'win' && msix", # https://bugzilla.mozilla.org/show_bug.cgi?id=1808049
+]
+run-sequentially = "node server exceptions dont replay well"
+
+["test_webtransport_simple.js"]
+# This test will be fixed in bug 1796556
+skip-if = [
+ "os == 'android'",
+ "os == 'win' && msix", # https://bugzilla.mozilla.org/show_bug.cgi?id=1807931
+ "verify && os == 'win'",
+ "socketprocess_networking",
+]
+
+["test_xmlhttprequest.js"]