summaryrefslogtreecommitdiffstats
path: root/netwerk/test/unit
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
commit2aa4a82499d4becd2284cdb482213d541b8804dd (patch)
treeb80bf8bf13c3766139fbacc530efd0dd9d54394c /netwerk/test/unit
parentInitial commit. (diff)
downloadfirefox-upstream.tar.xz
firefox-upstream.zip
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'netwerk/test/unit')
-rw-r--r--netwerk/test/unit/CA.key.pem30
-rw-r--r--netwerk/test/unit/client_cert_chooser.js27
-rw-r--r--netwerk/test/unit/client_cert_chooser.manifest2
-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.js167
-rw-r--r--netwerk/test/unit/head_cache2.js428
-rw-r--r--netwerk/test/unit/head_channels.js456
-rw-r--r--netwerk/test/unit/head_cookies.js973
-rw-r--r--netwerk/test/unit/head_http3.js105
-rw-r--r--netwerk/test/unit/head_trr.js364
-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/moz.build8
-rw-r--r--netwerk/test/unit/perftest.ini1
-rw-r--r--netwerk/test/unit/socks_client_subprocess.js101
-rw-r--r--netwerk/test/unit/test_1073747.js42
-rw-r--r--netwerk/test/unit/test_304_responses.js106
-rw-r--r--netwerk/test/unit/test_307_redirect.js96
-rw-r--r--netwerk/test/unit/test_421.js64
-rw-r--r--netwerk/test/unit/test_MIME_params.js784
-rw-r--r--netwerk/test/unit/test_NetUtil.js818
-rw-r--r--netwerk/test/unit/test_SuperfluousAuth.js99
-rw-r--r--netwerk/test/unit/test_URIs.js1042
-rw-r--r--netwerk/test/unit/test_URIs2.js906
-rw-r--r--netwerk/test/unit/test_XHR_redirects.js273
-rw-r--r--netwerk/test/unit/test_about_networking.js121
-rw-r--r--netwerk/test/unit/test_about_protocol.js57
-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.js174
-rw-r--r--netwerk/test/unit/test_alt-data_cross_process.js143
-rw-r--r--netwerk/test/unit/test_alt-data_overwrite.js203
-rw-r--r--netwerk/test/unit/test_alt-data_simple.js196
-rw-r--r--netwerk/test/unit/test_alt-data_stream.js153
-rw-r--r--netwerk/test/unit/test_alt-data_too_big.js113
-rw-r--r--netwerk/test/unit/test_altsvc.js519
-rw-r--r--netwerk/test/unit/test_altsvc_http3.js496
-rw-r--r--netwerk/test/unit/test_altsvc_pref.js139
-rw-r--r--netwerk/test/unit/test_anonymous-coalescing.js186
-rw-r--r--netwerk/test/unit/test_auth_dialog_permission.js283
-rw-r--r--netwerk/test/unit/test_auth_jar.js97
-rw-r--r--netwerk/test/unit/test_auth_proxy.js465
-rw-r--r--netwerk/test/unit/test_authentication.js754
-rw-r--r--netwerk/test/unit/test_authpromptwrapper.js233
-rw-r--r--netwerk/test/unit/test_backgroundfilesaver.js775
-rw-r--r--netwerk/test/unit/test_be_conservative.js247
-rw-r--r--netwerk/test/unit/test_be_conservative_error_handling.js242
-rw-r--r--netwerk/test/unit/test_blob_channelname.js42
-rw-r--r--netwerk/test/unit/test_brotli_http.js44
-rw-r--r--netwerk/test/unit/test_bug1064258.js142
-rw-r--r--netwerk/test/unit/test_bug1177909.js194
-rw-r--r--netwerk/test/unit/test_bug1195415.js163
-rw-r--r--netwerk/test/unit/test_bug1218029.js116
-rw-r--r--netwerk/test/unit/test_bug1279246.js98
-rw-r--r--netwerk/test/unit/test_bug1312774_http1.js153
-rw-r--r--netwerk/test/unit/test_bug1312782_http1.js206
-rw-r--r--netwerk/test/unit/test_bug1355539_http1.js207
-rw-r--r--netwerk/test/unit/test_bug1378385_http1.js202
-rw-r--r--netwerk/test/unit/test_bug1411316_http1.js117
-rw-r--r--netwerk/test/unit/test_bug1527293.js92
-rw-r--r--netwerk/test/unit/test_bug1683176.js104
-rw-r--r--netwerk/test/unit/test_bug203271.js248
-rw-r--r--netwerk/test/unit/test_bug248970_cache.js168
-rw-r--r--netwerk/test/unit/test_bug248970_cookie.js150
-rw-r--r--netwerk/test/unit/test_bug261425.js37
-rw-r--r--netwerk/test/unit/test_bug263127.js58
-rw-r--r--netwerk/test/unit/test_bug282432.js38
-rw-r--r--netwerk/test/unit/test_bug321706.js12
-rw-r--r--netwerk/test/unit/test_bug331825.js41
-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_bug365133.js131
-rw-r--r--netwerk/test/unit/test_bug368702.js153
-rw-r--r--netwerk/test/unit/test_bug369787.js71
-rw-r--r--netwerk/test/unit/test_bug371473.js40
-rw-r--r--netwerk/test/unit/test_bug376844.js23
-rw-r--r--netwerk/test/unit/test_bug376865.js23
-rw-r--r--netwerk/test/unit/test_bug379034.js21
-rw-r--r--netwerk/test/unit/test_bug380994.js26
-rw-r--r--netwerk/test/unit/test_bug388281.js42
-rw-r--r--netwerk/test/unit/test_bug396389.js71
-rw-r--r--netwerk/test/unit/test_bug401564.js44
-rw-r--r--netwerk/test/unit/test_bug411952.js53
-rw-r--r--netwerk/test/unit/test_bug412457.js117
-rw-r--r--netwerk/test/unit/test_bug412945.js42
-rw-r--r--netwerk/test/unit/test_bug414122.js62
-rw-r--r--netwerk/test/unit/test_bug427957.js106
-rw-r--r--netwerk/test/unit/test_bug429347.js75
-rw-r--r--netwerk/test/unit/test_bug455311.js128
-rw-r--r--netwerk/test/unit/test_bug464591.js100
-rw-r--r--netwerk/test/unit/test_bug468426.js129
-rw-r--r--netwerk/test/unit/test_bug468594.js176
-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.js61
-rw-r--r--netwerk/test/unit/test_bug479485.js65
-rw-r--r--netwerk/test/unit/test_bug482601.js262
-rw-r--r--netwerk/test/unit/test_bug482934.js189
-rw-r--r--netwerk/test/unit/test_bug484684.js139
-rw-r--r--netwerk/test/unit/test_bug490095.js158
-rw-r--r--netwerk/test/unit/test_bug504014.js74
-rw-r--r--netwerk/test/unit/test_bug510359.js102
-rw-r--r--netwerk/test/unit/test_bug515583.js82
-rw-r--r--netwerk/test/unit/test_bug526789.js287
-rw-r--r--netwerk/test/unit/test_bug528292.js93
-rw-r--r--netwerk/test/unit/test_bug536324_64bit_content_length.js64
-rw-r--r--netwerk/test/unit/test_bug540566.js24
-rw-r--r--netwerk/test/unit/test_bug543805.js136
-rw-r--r--netwerk/test/unit/test_bug553970.js53
-rw-r--r--netwerk/test/unit/test_bug561042.js42
-rw-r--r--netwerk/test/unit/test_bug561276.js64
-rw-r--r--netwerk/test/unit/test_bug580508.js33
-rw-r--r--netwerk/test/unit/test_bug586908.js104
-rw-r--r--netwerk/test/unit/test_bug596443.js115
-rw-r--r--netwerk/test/unit/test_bug618835.js122
-rw-r--r--netwerk/test/unit/test_bug633743.js193
-rw-r--r--netwerk/test/unit/test_bug650522.js34
-rw-r--r--netwerk/test/unit/test_bug650995.js189
-rw-r--r--netwerk/test/unit/test_bug652761.js19
-rw-r--r--netwerk/test/unit/test_bug654926.js102
-rw-r--r--netwerk/test/unit/test_bug654926_doom_and_read.js90
-rw-r--r--netwerk/test/unit/test_bug654926_test_seek.js76
-rw-r--r--netwerk/test/unit/test_bug659569.js58
-rw-r--r--netwerk/test/unit/test_bug660066.js56
-rw-r--r--netwerk/test/unit/test_bug667087.js32
-rw-r--r--netwerk/test/unit/test_bug667818.js50
-rw-r--r--netwerk/test/unit/test_bug667907.js86
-rw-r--r--netwerk/test/unit/test_bug669001.js179
-rw-r--r--netwerk/test/unit/test_bug767025.js315
-rw-r--r--netwerk/test/unit/test_bug770243.js246
-rw-r--r--netwerk/test/unit/test_bug812167.js141
-rw-r--r--netwerk/test/unit/test_bug826063.js91
-rw-r--r--netwerk/test/unit/test_bug856978.js135
-rw-r--r--netwerk/test/unit/test_bug894586.js157
-rw-r--r--netwerk/test/unit/test_bug935499.js10
-rw-r--r--netwerk/test/unit/test_cache-control_request.js447
-rw-r--r--netwerk/test/unit/test_cache-entry-id.js215
-rw-r--r--netwerk/test/unit/test_cache2-00-service-get.js19
-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.js49
-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-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.js53
-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.js18
-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.js66
-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.js57
-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.js74
-rw-r--r--netwerk/test/unit/test_cache2-29b-concurrent_read_non-resumable_entry_size_zero.js78
-rw-r--r--netwerk/test/unit/test_cache2-29c-concurrent_read_half-interrupted.js93
-rw-r--r--netwerk/test/unit/test_cache2-29d-concurrent_read_half-corrupted-206.js93
-rw-r--r--netwerk/test/unit/test_cache2-29e-concurrent_read_half-non-206-response.js88
-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.js188
-rw-r--r--netwerk/test/unit/test_cache2-30d-pinning-WasEvicted-API.js151
-rw-r--r--netwerk/test/unit/test_cache2-31-visit-all.js88
-rw-r--r--netwerk/test/unit/test_cache2-32-clear-origin.js68
-rw-r--r--netwerk/test/unit/test_cacheForOfflineUse_no-store.js104
-rw-r--r--netwerk/test/unit/test_cache_204_response.js60
-rw-r--r--netwerk/test/unit/test_cache_jar.js104
-rw-r--r--netwerk/test/unit/test_cacheflags.js436
-rw-r--r--netwerk/test/unit/test_captive_portal_service.js207
-rw-r--r--netwerk/test/unit/test_channel_close.js68
-rw-r--r--netwerk/test/unit/test_channel_priority.js97
-rw-r--r--netwerk/test/unit/test_chunked_responses.js172
-rw-r--r--netwerk/test/unit/test_compareURIs.js60
-rw-r--r--netwerk/test/unit/test_compressappend.js99
-rw-r--r--netwerk/test/unit/test_content_encoding_gzip.js210
-rw-r--r--netwerk/test/unit/test_content_length_underrun.js280
-rw-r--r--netwerk/test/unit/test_content_sniffer.js163
-rw-r--r--netwerk/test/unit/test_cookie_blacklist.js39
-rw-r--r--netwerk/test/unit/test_cookie_header.js113
-rw-r--r--netwerk/test/unit/test_cookie_ipv6.js52
-rw-r--r--netwerk/test/unit/test_cookiejars.js172
-rw-r--r--netwerk/test/unit/test_cookiejars_safebrowsing.js238
-rw-r--r--netwerk/test/unit/test_cookies_async_failure.js508
-rw-r--r--netwerk/test/unit/test_cookies_persistence.js76
-rw-r--r--netwerk/test/unit/test_cookies_privatebrowsing.js139
-rw-r--r--netwerk/test/unit/test_cookies_profile_close.js112
-rw-r--r--netwerk/test/unit/test_cookies_read.js114
-rw-r--r--netwerk/test/unit/test_cookies_sync_failure.js344
-rw-r--r--netwerk/test/unit/test_cookies_thirdparty.js162
-rw-r--r--netwerk/test/unit/test_cookies_thirdparty_session.js71
-rw-r--r--netwerk/test/unit/test_cookies_upgrade_10.js59
-rw-r--r--netwerk/test/unit/test_data_protocol.js91
-rw-r--r--netwerk/test/unit/test_defaultURI.js185
-rw-r--r--netwerk/test/unit/test_disabled_ftp.js21
-rw-r--r--netwerk/test/unit/test_dns_by_type_resolve.js180
-rw-r--r--netwerk/test/unit/test_dns_cancel.js125
-rw-r--r--netwerk/test/unit/test_dns_disable_ipv4.js55
-rw-r--r--netwerk/test/unit/test_dns_disable_ipv6.js56
-rw-r--r--netwerk/test/unit/test_dns_disabled.js94
-rw-r--r--netwerk/test/unit/test_dns_localredirect.js68
-rw-r--r--netwerk/test/unit/test_dns_offline.js113
-rw-r--r--netwerk/test/unit/test_dns_onion.js82
-rw-r--r--netwerk/test/unit/test_dns_originAttributes.js99
-rw-r--r--netwerk/test/unit/test_dns_override.js311
-rw-r--r--netwerk/test/unit/test_dns_override_for_localhost.js92
-rw-r--r--netwerk/test/unit/test_dns_proxy_bypass.js82
-rw-r--r--netwerk/test/unit/test_dns_service.js68
-rw-r--r--netwerk/test/unit/test_domain_eviction.js185
-rw-r--r--netwerk/test/unit/test_doomentry.js108
-rw-r--r--netwerk/test/unit/test_duplicate_headers.js558
-rw-r--r--netwerk/test/unit/test_event_sink.js195
-rw-r--r--netwerk/test/unit/test_eviction.js252
-rw-r--r--netwerk/test/unit/test_extract_charset_from_content_type.js245
-rw-r--r--netwerk/test/unit/test_fallback_no-cache-entry_canceled.js131
-rw-r--r--netwerk/test/unit/test_fallback_no-cache-entry_passing.js130
-rw-r--r--netwerk/test/unit/test_fallback_redirect-to-different-origin_canceled.js133
-rw-r--r--netwerk/test/unit/test_fallback_redirect-to-different-origin_passing.js132
-rw-r--r--netwerk/test/unit/test_fallback_request-error_canceled.js140
-rw-r--r--netwerk/test/unit/test_fallback_request-error_passing.js136
-rw-r--r--netwerk/test/unit/test_fallback_response-error_canceled.js133
-rw-r--r--netwerk/test/unit/test_fallback_response-error_passing.js132
-rw-r--r--netwerk/test/unit/test_file_protocol.js280
-rw-r--r--netwerk/test/unit/test_filestreams.js306
-rw-r--r--netwerk/test/unit/test_freshconnection.js30
-rw-r--r--netwerk/test/unit/test_getHost.js63
-rw-r--r--netwerk/test/unit/test_gre_resources.js32
-rw-r--r--netwerk/test/unit/test_gzipped_206.js167
-rw-r--r--netwerk/test/unit/test_head.js169
-rw-r--r--netwerk/test/unit/test_head_request_no_response_body.js76
-rw-r--r--netwerk/test/unit/test_header_Accept-Language.js101
-rw-r--r--netwerk/test/unit/test_header_Accept-Language_case.js53
-rw-r--r--netwerk/test/unit/test_header_Server_Timing.js67
-rw-r--r--netwerk/test/unit/test_headers.js175
-rw-r--r--netwerk/test/unit/test_hostnameIsLocalIPAddress.js41
-rw-r--r--netwerk/test/unit/test_hostnameIsSharedIPAddress.js21
-rw-r--r--netwerk/test/unit/test_http1-proxy.js229
-rw-r--r--netwerk/test/unit/test_http2-proxy.js615
-rw-r--r--netwerk/test/unit/test_http2.js1442
-rw-r--r--netwerk/test/unit/test_http3.js569
-rw-r--r--netwerk/test/unit/test_http3_421.js175
-rw-r--r--netwerk/test/unit/test_http3_alt_svc.js129
-rw-r--r--netwerk/test/unit/test_http3_error_before_connect.js123
-rw-r--r--netwerk/test/unit/test_http3_fast_fallback.js597
-rw-r--r--netwerk/test/unit/test_http3_fatal_stream_error.js149
-rw-r--r--netwerk/test/unit/test_http3_large_post.js101
-rw-r--r--netwerk/test/unit/test_http3_perf.js262
-rw-r--r--netwerk/test/unit/test_http3_server_not_existing.js123
-rw-r--r--netwerk/test/unit/test_http3_trans_close.js84
-rw-r--r--netwerk/test/unit/test_httpResponseTimeout.js162
-rw-r--r--netwerk/test/unit/test_http_headers.js75
-rw-r--r--netwerk/test/unit/test_http_sfv.js590
-rw-r--r--netwerk/test/unit/test_httpauth.js204
-rw-r--r--netwerk/test/unit/test_httpcancel.js260
-rw-r--r--netwerk/test/unit/test_httpssvc_https_upgrade.js298
-rw-r--r--netwerk/test/unit/test_httpssvc_iphint.js301
-rw-r--r--netwerk/test/unit/test_httpssvc_priority.js235
-rw-r--r--netwerk/test/unit/test_httpssvc_retry_with_ech.js288
-rw-r--r--netwerk/test/unit/test_httpssvc_retry_without_ech.js223
-rw-r--r--netwerk/test/unit/test_httpsuspend.js84
-rw-r--r--netwerk/test/unit/test_idn_blacklist.js173
-rw-r--r--netwerk/test/unit/test_idn_urls.js441
-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.js167
-rw-r--r--netwerk/test/unit/test_inhibit_caching.js92
-rw-r--r--netwerk/test/unit/test_ioservice.js19
-rw-r--r--netwerk/test/unit/test_large_port.js68
-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.js94
-rw-r--r--netwerk/test/unit/test_localstreams.js90
-rw-r--r--netwerk/test/unit/test_mismatch_last-modified.js155
-rw-r--r--netwerk/test/unit/test_mozTXTToHTMLConv.js395
-rw-r--r--netwerk/test/unit/test_multipart_byteranges.js141
-rw-r--r--netwerk/test/unit/test_multipart_streamconv-byte-by-byte.js113
-rw-r--r--netwerk/test/unit/test_multipart_streamconv.js98
-rw-r--r--netwerk/test/unit/test_multipart_streamconv_empty.js68
-rw-r--r--netwerk/test/unit/test_multipart_streamconv_missing_boundary_lead_dashes.js90
-rw-r--r--netwerk/test/unit/test_multipart_streamconv_missing_lead_boundary.js90
-rw-r--r--netwerk/test/unit/test_nestedabout_serialize.js41
-rw-r--r--netwerk/test/unit/test_net_addr.js222
-rw-r--r--netwerk/test/unit/test_network_activity.js61
-rw-r--r--netwerk/test/unit/test_network_connectivity_service.js215
-rw-r--r--netwerk/test/unit/test_no_cookies_after_last_pb_exit.js134
-rw-r--r--netwerk/test/unit/test_node_execute.js87
-rw-r--r--netwerk/test/unit/test_nojsredir.js65
-rw-r--r--netwerk/test/unit/test_nsIBufferedOutputStream_writeFrom_block.js193
-rw-r--r--netwerk/test/unit/test_obs-fold.js73
-rw-r--r--netwerk/test/unit/test_offline_status.js19
-rw-r--r--netwerk/test/unit/test_offlinecache_custom-directory.js165
-rw-r--r--netwerk/test/unit/test_origin.js330
-rw-r--r--netwerk/test/unit/test_original_sent_received_head.js246
-rw-r--r--netwerk/test/unit/test_parse_content_type.js365
-rw-r--r--netwerk/test/unit/test_partial_response_entry_size_smart_shrink.js104
-rw-r--r--netwerk/test/unit/test_permmgr.js131
-rw-r--r--netwerk/test/unit/test_ping_aboutnetworking.js105
-rw-r--r--netwerk/test/unit/test_pinned_app_cache.js302
-rw-r--r--netwerk/test/unit/test_plaintext_sniff.js209
-rw-r--r--netwerk/test/unit/test_port_remapping.js48
-rw-r--r--netwerk/test/unit/test_post.js139
-rw-r--r--netwerk/test/unit/test_predictor.js771
-rw-r--r--netwerk/test/unit/test_private_cookie_changed.js44
-rw-r--r--netwerk/test/unit/test_private_necko_channel.js56
-rw-r--r--netwerk/test/unit/test_progress.js132
-rw-r--r--netwerk/test/unit/test_protocolproxyservice-async-filters.js440
-rw-r--r--netwerk/test/unit/test_protocolproxyservice.js1049
-rw-r--r--netwerk/test/unit/test_proxy-failover_canceled.js58
-rw-r--r--netwerk/test/unit/test_proxy-failover_passing.js46
-rw-r--r--netwerk/test/unit/test_proxy-replace_canceled.js58
-rw-r--r--netwerk/test/unit/test_proxy-replace_passing.js46
-rw-r--r--netwerk/test/unit/test_proxyconnect.js358
-rw-r--r--netwerk/test/unit/test_psl.js45
-rw-r--r--netwerk/test/unit/test_race_cache_with_network.js284
-rw-r--r--netwerk/test/unit/test_range_requests.js527
-rw-r--r--netwerk/test/unit/test_rcwn_always_cache_new_content.js115
-rw-r--r--netwerk/test/unit/test_readline.js104
-rw-r--r--netwerk/test/unit/test_redirect-caching_canceled.js62
-rw-r--r--netwerk/test/unit/test_redirect-caching_failure.js82
-rw-r--r--netwerk/test/unit/test_redirect-caching_passing.js54
-rw-r--r--netwerk/test/unit/test_redirect_baduri.js42
-rw-r--r--netwerk/test/unit/test_redirect_canceled.js48
-rw-r--r--netwerk/test/unit/test_redirect_different-protocol.js54
-rw-r--r--netwerk/test/unit/test_redirect_failure.js60
-rw-r--r--netwerk/test/unit/test_redirect_from_script.js249
-rw-r--r--netwerk/test/unit/test_redirect_from_script_after-open_passing.js249
-rw-r--r--netwerk/test/unit/test_redirect_history.js74
-rw-r--r--netwerk/test/unit/test_redirect_loop.js86
-rw-r--r--netwerk/test/unit/test_redirect_passing.js53
-rw-r--r--netwerk/test/unit/test_redirect_protocol_telemetry.js66
-rw-r--r--netwerk/test/unit/test_reentrancy.js107
-rw-r--r--netwerk/test/unit/test_referrer.js247
-rw-r--r--netwerk/test/unit/test_referrer_cross_origin.js319
-rw-r--r--netwerk/test/unit/test_referrer_policy.js143
-rw-r--r--netwerk/test/unit/test_reopen.js147
-rw-r--r--netwerk/test/unit/test_reply_without_content_type.js150
-rw-r--r--netwerk/test/unit/test_resumable_channel.js424
-rw-r--r--netwerk/test/unit/test_resumable_truncate.js93
-rw-r--r--netwerk/test/unit/test_safeoutputstream.js72
-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_2_migration.js303
-rw-r--r--netwerk/test/unit/test_schema_3_migration.js170
-rw-r--r--netwerk/test/unit/test_separate_connections.js102
-rw-r--r--netwerk/test/unit/test_signature_extraction.js215
-rw-r--r--netwerk/test/unit/test_simple.js69
-rw-r--r--netwerk/test/unit/test_sockettransportsvc_available.js11
-rw-r--r--netwerk/test/unit/test_socks.js519
-rw-r--r--netwerk/test/unit/test_speculative_connect.js368
-rw-r--r--netwerk/test/unit/test_stale-while-revalidate_loop.js46
-rw-r--r--netwerk/test/unit/test_stale-while-revalidate_max-age-0.js111
-rw-r--r--netwerk/test/unit/test_stale-while-revalidate_negative.js90
-rw-r--r--netwerk/test/unit/test_stale-while-revalidate_positive.js111
-rw-r--r--netwerk/test/unit/test_standardurl.js1299
-rw-r--r--netwerk/test/unit/test_standardurl_default_port.js61
-rw-r--r--netwerk/test/unit/test_standardurl_port.js71
-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.js99
-rw-r--r--netwerk/test/unit/test_suspend_channel_on_authRetry.js276
-rw-r--r--netwerk/test/unit/test_suspend_channel_on_examine.js77
-rw-r--r--netwerk/test/unit/test_suspend_channel_on_examine_merged_response.js212
-rw-r--r--netwerk/test/unit/test_suspend_channel_on_modified.js181
-rw-r--r--netwerk/test/unit/test_synthesized_response.js294
-rw-r--r--netwerk/test/unit/test_throttlechannel.js46
-rw-r--r--netwerk/test/unit/test_throttlequeue.js25
-rw-r--r--netwerk/test/unit/test_throttling.js64
-rw-r--r--netwerk/test/unit/test_tldservice_nextsubdomain.js28
-rw-r--r--netwerk/test/unit/test_tls_flags.js274
-rw-r--r--netwerk/test/unit/test_tls_flags_separate_connections.js119
-rw-r--r--netwerk/test/unit/test_tls_server.js301
-rw-r--r--netwerk/test/unit/test_tls_server_multiple_clients.js152
-rw-r--r--netwerk/test/unit/test_traceable_channel.js145
-rw-r--r--netwerk/test/unit/test_trackingProtection_annotateChannels.js388
-rw-r--r--netwerk/test/unit/test_trr.js2136
-rw-r--r--netwerk/test/unit/test_trr_additional_section.js314
-rw-r--r--netwerk/test/unit/test_trr_case_sensitivity.js143
-rw-r--r--netwerk/test/unit/test_trr_cname_chain.js110
-rw-r--r--netwerk/test/unit/test_trr_extended_error.js340
-rw-r--r--netwerk/test/unit/test_trr_https_fallback.js1090
-rw-r--r--netwerk/test/unit/test_trr_httpssvc.js669
-rw-r--r--netwerk/test/unit/test_trr_nat64.js121
-rw-r--r--netwerk/test/unit/test_trr_proxy.js133
-rw-r--r--netwerk/test/unit/test_udp_multicast.js114
-rw-r--r--netwerk/test/unit/test_udpsocket.js89
-rw-r--r--netwerk/test/unit/test_udpsocket_offline.js108
-rw-r--r--netwerk/test/unit/test_unescapestring.js34
-rw-r--r--netwerk/test/unit/test_unix_domain.js705
-rw-r--r--netwerk/test/unit/test_uri_mutator.js48
-rw-r--r--netwerk/test/unit/test_use_httpssvc.js325
-rw-r--r--netwerk/test/unit/test_websocket_offline.js51
-rw-r--r--netwerk/test/unit/test_xmlhttprequest.js55
-rw-r--r--netwerk/test/unit/xpcshell.ini517
428 files changed, 69303 insertions, 0 deletions
diff --git a/netwerk/test/unit/CA.key.pem b/netwerk/test/unit/CA.key.pem
new file mode 100644
index 0000000000..2153c9dd56
--- /dev/null
+++ b/netwerk/test/unit/CA.key.pem
@@ -0,0 +1,30 @@
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIIFDjBABgkqhkiG9w0BBQ0wMzAbBgkqhkiG9w0BBQwwDgQIoXjtIGzP1OkCAggA
+MBQGCCqGSIb3DQMHBAg7NkhJaDJEVwSCBMhYUI4JRAIdvJtCmP7lKl30QR+HG4JC
+9gUJflQrCsa1WhxKCHa7VXzqlItQTu0FTMwn9GRFUOtqpY8ZE6XJFuvzKPox3RMa
+1TMod6v3NS3KIs6/l2Pb2HcGtcMzOJVei1nwtjsT5fvq36x3eoKzgqd9l0fLcvlD
+wf+byzgY0Nsau+ER8DWy65jXF8bPsQQcKTc+U+p4moO3UuXG4+Pnd8ooSaM4X2on
+1jIYDU1aFoSDUvze8+MvQCD32QLuO63iK7ox4sFharG7KucYqeWCihDx5rlGaVGB
+5647v4oHRysEdLVTkU12mIC/Hx/yPXcLhHYmawmnYwEoh1S+wd7rOo9Wn/l16NTK
+8BcDuvfM8km4T5oO/UFaNDIBLBQsNM5sNHDYFDlhmR4x6d5nXeERJ6DQbvhQtgnV
+bTtT9h24rsC8Irflz/abcvTvqqp8I1+gYEzmhgDRUgp9zAPZUoH3E4DKk5rVgApR
+ARX9Y88S7k/OBnU8r+cT+0CjsusbbIv5W2nAFqEX9jMend0cHzYvq3m6v1Jqxjfn
+kQRP1n+SagmAPBIAzy1wSHGV43+COk6FB+blfAGbO55lLglEM9PLH7Nnl0XrPtaE
+dXx5RTtdBnb349Ow8H3WnleTfKspUbIVNyM48aPaXJu6Y784pUXDOC13ISFVbOew
+dPr/s/GoHgBUIm9gxkhNQYUlcSNrJCyJ6bqvrYbOmVQRusO/SaM6ozY8wFL8LDnS
+GeXmg3dAslHhuaHlFN7atF7rBtTWPsH+oQdHNKcLDK7nYq45v8VfjPUrWPfYc2nB
+l+zT4LozY3VPfPW7BG2zVBTyxXkiynz0w7tJaN/HokZGAUDqWXqjSceJqc9Q4XAG
+slIxbxkfxEJUEmJ2wHEnure6T0dJOIfbJzkCqWAeJjkrbI5mdKLuXFj94VgSlfK2
+iq3J20/5HVdHqoVGRZ5rxBUIaVEgSXB3/+9C/M0U0uxx23zxRmVkMGdhhCqXQRh/
+jFUkBzq4x3yibxJW3fRe7jXEJdo1DAAfgBnDvCUWH7lRX8hDkx6OIX4ZS4D7Va0j
+ogSC04IdZWxOP3YJ4gGwx8vvgHWnBLyFfmdFnfHXUr9A8HDDJQTupYg25PDUGHla
+SxukgOYdQ2O6jUCW0TYeUzX7y/P/Za93kWJp7XqA4v76fQ+C9d3CZT/TY0CqNgxB
+C5+PWRGvxtcy+Bne8QYCJhvNPEhfgFa9fU3Rd4w43lvTb9rsy7eBR3jJKdPLKExJ
+zEPIgVUGaMf0lawL0fIgoUI5Q3kRCmrswkTK9kr4+rSA//p0NralnZtHCWRvgs9W
+Lg4hkf1vXxsa9f16Nk6PxqU/OnJmhTnTnv9MzFoX3Sce2neD86H5c7tdguySbrsj
+5fww64rH1UwHhn/i49i3hkseax48gOAZPA8rl+L70FS8dXLpHOm4ihmv6ubVjr82
+yOxi4WmaoXfmOPBgOgGhz1nTFAaetwfhZIsgEtysuWAOsApOUyjlD/wM25988bAa
+m5FwslUGLWQfBIV1N9PC+Q0ui1ywRuLoKHNiKDSE+T5iOuv2Yf7du4nncoM/ANmU
+FnWJL3Aj1VE/O+OeUyuNEPWLHvVX5TChe5mFXZO4bXfTR4tgdJJ15HWf4LKMQdcl
+BEA=
+-----END ENCRYPTED PRIVATE KEY-----
diff --git a/netwerk/test/unit/client_cert_chooser.js b/netwerk/test/unit/client_cert_chooser.js
new file mode 100644
index 0000000000..5078c68fc6
--- /dev/null
+++ b/netwerk/test/unit/client_cert_chooser.js
@@ -0,0 +1,27 @@
+// -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+"use strict";
+
+const { ComponentUtils } = ChromeUtils.import(
+ "resource://gre/modules/ComponentUtils.jsm"
+);
+
+var Prompter = {
+ QueryInterface: ChromeUtils.generateQI(["nsIPrompt"]),
+ alert() {}, // Do nothing when asked to show an alert
+};
+
+function WindowWatcherService() {}
+WindowWatcherService.prototype = {
+ classID: Components.ID("{01ae923c-81bb-45db-b860-d423b0fc4fe1}"),
+ QueryInterface: ChromeUtils.generateQI(["nsIWindowWatcher"]),
+
+ getNewPrompter() {
+ return Prompter;
+ },
+};
+
+this.NSGetFactory = ComponentUtils.generateNSGetFactory([WindowWatcherService]);
diff --git a/netwerk/test/unit/client_cert_chooser.manifest b/netwerk/test/unit/client_cert_chooser.manifest
new file mode 100644
index 0000000000..e604c92d0e
--- /dev/null
+++ b/netwerk/test/unit/client_cert_chooser.manifest
@@ -0,0 +1,2 @@
+component {01ae923c-81bb-45db-b860-d423b0fc4fe1} cert_dialog.js
+contract @mozilla.org/embedcomp/window-watcher;1 {01ae923c-81bb-45db-b860-d423b0fc4fe1}
diff --git a/netwerk/test/unit/data/cookies_v10.sqlite b/netwerk/test/unit/data/cookies_v10.sqlite
new file mode 100644
index 0000000000..2301731f8e
--- /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..388cb184a2
--- /dev/null
+++ b/netwerk/test/unit/head_cache.js
@@ -0,0 +1,167 @@
+"use strict";
+
+var { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+var _CSvc;
+function get_cache_service() {
+ if (_CSvc) {
+ return _CSvc;
+ }
+
+ return (_CSvc = Cc["@mozilla.org/netwerk/cache-storage-service;1"].getService(
+ Ci.nsICacheStorageService
+ ));
+}
+
+function evict_cache_entries(where) {
+ var clearDisk = !where || where == "disk" || where == "all";
+ var clearMem = !where || where == "memory" || where == "all";
+ var clearAppCache = where == "appcache";
+
+ var svc = get_cache_service();
+ var storage;
+
+ if (clearMem) {
+ storage = svc.memoryCacheStorage(Services.loadContextInfo.default);
+ storage.asyncEvictStorage(null);
+ }
+
+ if (clearDisk) {
+ storage = svc.diskCacheStorage(Services.loadContextInfo.default, false);
+ storage.asyncEvictStorage(null);
+ }
+
+ if (clearAppCache) {
+ storage = svc.appCacheStorage(Services.loadContextInfo.default, null);
+ storage.asyncEvictStorage(null);
+ }
+}
+
+function createURI(urispec) {
+ var ioServ = Cc["@mozilla.org/network/io-service;1"].getService(
+ Ci.nsIIOService
+ );
+ return ioServ.newURI(urispec);
+}
+
+function getCacheStorage(where, lci, appcache) {
+ if (!lci) {
+ lci = Services.loadContextInfo.default;
+ }
+ var svc = get_cache_service();
+ switch (where) {
+ case "disk":
+ return svc.diskCacheStorage(lci, false);
+ case "memory":
+ return svc.memoryCacheStorage(lci);
+ case "appcache":
+ return svc.appCacheStorage(lci, appcache);
+ case "pin":
+ return svc.pinningCacheStorage(lci);
+ }
+ return null;
+}
+
+function asyncOpenCacheEntry(key, where, flags, lci, callback, appcache) {
+ key = createURI(key);
+
+ function CacheListener() {}
+ CacheListener.prototype = {
+ _appCache: appcache,
+
+ QueryInterface: ChromeUtils.generateQI(["nsICacheEntryOpenCallback"]),
+
+ onCacheEntryCheck(entry, appCache) {
+ if (typeof callback === "object") {
+ return callback.onCacheEntryCheck(entry, appCache);
+ }
+ return Ci.nsICacheEntryOpenCallback.ENTRY_WANTED;
+ },
+
+ onCacheEntryAvailable(entry, isnew, appCache, status) {
+ if (typeof callback === "object") {
+ // Root us at the callback
+ callback.__cache_listener_root = this;
+ callback.onCacheEntryAvailable(entry, isnew, appCache, status);
+ } else {
+ callback(status, entry, appCache);
+ }
+ },
+
+ run() {
+ var storage = getCacheStorage(where, lci, this._appCache);
+ storage.asyncOpenURI(key, "", flags, this);
+ },
+ };
+
+ new CacheListener().run();
+}
+
+function syncWithCacheIOThread(callback, force) {
+ if (force) {
+ asyncOpenCacheEntry(
+ "http://nonexistententry/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_READONLY,
+ null,
+ function(status, entry) {
+ Assert.equal(status, Cr.NS_ERROR_CACHE_KEY_NOT_FOUND);
+ callback();
+ }
+ );
+ } else {
+ callback();
+ }
+}
+
+function get_device_entry_count(where, lci, continuation) {
+ var storage = getCacheStorage(where, lci);
+ if (!storage) {
+ continuation(-1, 0);
+ return;
+ }
+
+ var visitor = {
+ onCacheStorageInfo(entryCount, consumption) {
+ executeSoon(function() {
+ continuation(entryCount, consumption);
+ });
+ },
+ };
+
+ // get the device entry count
+ storage.asyncVisitStorage(visitor, false);
+}
+
+function asyncCheckCacheEntryPresence(
+ key,
+ where,
+ shouldExist,
+ continuation,
+ appCache
+) {
+ asyncOpenCacheEntry(
+ key,
+ where,
+ Ci.nsICacheStorage.OPEN_READONLY,
+ null,
+ function(status, entry) {
+ if (shouldExist) {
+ dump("TEST-INFO | checking cache key " + key + " exists @ " + where);
+ Assert.equal(status, Cr.NS_OK);
+ Assert.ok(!!entry);
+ } else {
+ dump(
+ "TEST-INFO | checking cache key " + key + " doesn't exist @ " + where
+ );
+ Assert.equal(status, Cr.NS_ERROR_CACHE_KEY_NOT_FOUND);
+ Assert.equal(null, entry);
+ }
+ continuation();
+ },
+ appCache
+ );
+}
diff --git a/netwerk/test/unit/head_cache2.js b/netwerk/test/unit/head_cache2.js
new file mode 100644
index 0000000000..5a531498ca
--- /dev/null
+++ b/netwerk/test/unit/head_cache2.js
@@ -0,0 +1,428 @@
+/* import-globals-from head_cache.js */
+/* import-globals-from head_channels.js */
+
+"use strict";
+
+var callbacks = [];
+
+// Expect an existing entry
+const NORMAL = 0;
+// Expect a new entry
+const NEW = 1 << 0;
+// Return early from onCacheEntryCheck and set the callback to state it expects onCacheEntryCheck to happen
+const NOTVALID = 1 << 1;
+// Throw from onCacheEntryAvailable
+const THROWAVAIL = 1 << 2;
+// Open entry for reading-only
+const READONLY = 1 << 3;
+// Expect the entry to not be found
+const NOTFOUND = 1 << 4;
+// Return ENTRY_NEEDS_REVALIDATION from onCacheEntryCheck
+const REVAL = 1 << 5;
+// Return ENTRY_PARTIAL from onCacheEntryCheck, in combo with NEW or RECREATE bypasses check for emptiness of the entry
+const PARTIAL = 1 << 6;
+// Expect the entry is doomed, i.e. the output stream should not be possible to open
+const DOOMED = 1 << 7;
+// Don't trigger the go-on callback until the entry is written
+const WAITFORWRITE = 1 << 8;
+// Don't write data (i.e. don't open output stream)
+const METAONLY = 1 << 9;
+// Do recreation of an existing cache entry
+const RECREATE = 1 << 10;
+// Do not give me the entry
+const NOTWANTED = 1 << 11;
+// Tell the cache to wait for the entry to be completely written first
+const COMPLETE = 1 << 12;
+// Don't write meta/data and don't set valid in the callback, consumer will do it manually
+const DONTFILL = 1 << 13;
+// Used in combination with METAONLY, don't call setValid() on the entry after metadata has been set
+const DONTSETVALID = 1 << 14;
+// Notify before checking the data, useful for proper callback ordering checks
+const NOTIFYBEFOREREAD = 1 << 15;
+// It's allowed to not get an existing entry (result of opening is undetermined)
+const MAYBE_NEW = 1 << 16;
+
+var log_c2 = true;
+function LOG_C2(o, m) {
+ if (!log_c2) {
+ return;
+ }
+ if (!m) {
+ dump("TEST-INFO | CACHE2: " + o + "\n");
+ } else {
+ dump(
+ "TEST-INFO | CACHE2: callback #" +
+ o.order +
+ "(" +
+ (o.workingData ? o.workingData.substr(0, 10) : "---") +
+ ") " +
+ m +
+ "\n"
+ );
+ }
+}
+
+function pumpReadStream(inputStream, goon) {
+ if (inputStream.isNonBlocking()) {
+ // non-blocking stream, must read via pump
+ var pump = Cc["@mozilla.org/network/input-stream-pump;1"].createInstance(
+ Ci.nsIInputStreamPump
+ );
+ pump.init(inputStream, 0, 0, true);
+ var data = "";
+ pump.asyncRead({
+ onStartRequest(aRequest) {},
+ onDataAvailable(aRequest, aInputStream, aOffset, aCount) {
+ var wrapper = Cc["@mozilla.org/scriptableinputstream;1"].createInstance(
+ Ci.nsIScriptableInputStream
+ );
+ wrapper.init(aInputStream);
+ var str = wrapper.read(wrapper.available());
+ LOG_C2("reading data '" + str.substring(0, 5) + "'");
+ data += str;
+ },
+ onStopRequest(aRequest, aStatusCode) {
+ LOG_C2("done reading data: " + aStatusCode);
+ Assert.equal(aStatusCode, Cr.NS_OK);
+ goon(data);
+ },
+ });
+ } else {
+ // blocking stream
+ var data = read_stream(inputStream, inputStream.available());
+ goon(data);
+ }
+}
+
+OpenCallback.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["nsICacheEntryOpenCallback"]),
+ onCacheEntryCheck(entry, appCache) {
+ LOG_C2(this, "onCacheEntryCheck");
+ Assert.ok(!this.onCheckPassed);
+ this.onCheckPassed = true;
+
+ if (this.behavior & NOTVALID) {
+ LOG_C2(this, "onCacheEntryCheck DONE, return ENTRY_WANTED");
+ return Ci.nsICacheEntryOpenCallback.ENTRY_WANTED;
+ }
+
+ if (this.behavior & NOTWANTED) {
+ LOG_C2(this, "onCacheEntryCheck DONE, return ENTRY_NOT_WANTED");
+ return Ci.nsICacheEntryOpenCallback.ENTRY_NOT_WANTED;
+ }
+
+ Assert.equal(entry.getMetaDataElement("meto"), this.workingMetadata);
+
+ // check for sane flag combination
+ Assert.notEqual(this.behavior & (REVAL | PARTIAL), REVAL | PARTIAL);
+
+ if (this.behavior & (REVAL | PARTIAL)) {
+ LOG_C2(this, "onCacheEntryCheck DONE, return ENTRY_NEEDS_REVALIDATION");
+ return Ci.nsICacheEntryOpenCallback.ENTRY_NEEDS_REVALIDATION;
+ }
+
+ if (this.behavior & COMPLETE) {
+ LOG_C2(
+ this,
+ "onCacheEntryCheck DONE, return RECHECK_AFTER_WRITE_FINISHED"
+ );
+ // Specific to the new backend because of concurrent read/write:
+ // when a consumer returns RECHECK_AFTER_WRITE_FINISHED from onCacheEntryCheck
+ // the cache calls this callback again after the entry write has finished.
+ // This gives the consumer a chance to recheck completeness of the entry
+ // again.
+ // Thus, we reset state as onCheck would have never been called.
+ this.onCheckPassed = false;
+ // Don't return RECHECK_AFTER_WRITE_FINISHED on second call of onCacheEntryCheck.
+ this.behavior &= ~COMPLETE;
+ return Ci.nsICacheEntryOpenCallback.RECHECK_AFTER_WRITE_FINISHED;
+ }
+
+ LOG_C2(this, "onCacheEntryCheck DONE, return ENTRY_WANTED");
+ return Ci.nsICacheEntryOpenCallback.ENTRY_WANTED;
+ },
+ onCacheEntryAvailable(entry, isnew, appCache, status) {
+ if (this.behavior & MAYBE_NEW && isnew) {
+ this.behavior |= NEW;
+ }
+
+ LOG_C2(this, "onCacheEntryAvailable, " + this.behavior);
+ Assert.ok(!this.onAvailPassed);
+ this.onAvailPassed = true;
+
+ Assert.equal(isnew, !!(this.behavior & NEW));
+
+ if (this.behavior & (NOTFOUND | NOTWANTED)) {
+ Assert.equal(status, Cr.NS_ERROR_CACHE_KEY_NOT_FOUND);
+ Assert.ok(!entry);
+ if (this.behavior & THROWAVAIL) {
+ this.throwAndNotify(entry);
+ }
+ this.goon(entry);
+ } else if (this.behavior & (NEW | RECREATE)) {
+ Assert.ok(!!entry);
+
+ if (this.behavior & RECREATE) {
+ entry = entry.recreate();
+ Assert.ok(!!entry);
+ }
+
+ if (this.behavior & THROWAVAIL) {
+ this.throwAndNotify(entry);
+ }
+
+ if (!(this.behavior & WAITFORWRITE)) {
+ this.goon(entry);
+ }
+
+ if (!(this.behavior & PARTIAL)) {
+ try {
+ entry.getMetaDataElement("meto");
+ Assert.ok(false);
+ } catch (ex) {}
+ }
+
+ if (this.behavior & DONTFILL) {
+ Assert.equal(false, this.behavior & WAITFORWRITE);
+ return;
+ }
+
+ var self = this;
+ executeSoon(function() {
+ // emulate network latency
+ entry.setMetaDataElement("meto", self.workingMetadata);
+ entry.metaDataReady();
+ if (self.behavior & METAONLY) {
+ // Since forcing GC/CC doesn't trigger OnWriterClosed, we have to set the entry valid manually :(
+ if (!(self.behavior & DONTSETVALID)) {
+ entry.setValid();
+ }
+
+ entry.close();
+ if (self.behavior & WAITFORWRITE) {
+ self.goon(entry);
+ }
+
+ return;
+ }
+ executeSoon(function() {
+ // emulate more network latency
+ if (self.behavior & DOOMED) {
+ LOG_C2(self, "checking doom state");
+ try {
+ var os = entry.openOutputStream(0, -1);
+ // Unfortunately, in the undetermined state we cannot even check whether the entry
+ // is actually doomed or not.
+ os.close();
+ Assert.ok(!!(self.behavior & MAYBE_NEW));
+ } catch (ex) {
+ Assert.ok(true);
+ }
+ if (self.behavior & WAITFORWRITE) {
+ self.goon(entry);
+ }
+ return;
+ }
+
+ var offset = self.behavior & PARTIAL ? entry.dataSize : 0;
+ LOG_C2(self, "openOutputStream @ " + offset);
+ var os = entry.openOutputStream(offset, -1);
+ LOG_C2(self, "writing data");
+ var wrt = os.write(self.workingData, self.workingData.length);
+ Assert.equal(wrt, self.workingData.length);
+ os.close();
+ if (self.behavior & WAITFORWRITE) {
+ self.goon(entry);
+ }
+
+ entry.close();
+ });
+ });
+ } else {
+ // NORMAL
+ Assert.ok(!!entry);
+ Assert.equal(entry.getMetaDataElement("meto"), this.workingMetadata);
+ if (this.behavior & THROWAVAIL) {
+ this.throwAndNotify(entry);
+ }
+ if (this.behavior & NOTIFYBEFOREREAD) {
+ this.goon(entry, true);
+ }
+
+ var self = this;
+ pumpReadStream(entry.openInputStream(0), function(data) {
+ Assert.equal(data, self.workingData);
+ self.onDataCheckPassed = true;
+ LOG_C2(self, "entry read done");
+ self.goon(entry);
+ entry.close();
+ });
+ }
+ },
+ selfCheck() {
+ LOG_C2(this, "selfCheck");
+
+ Assert.ok(this.onCheckPassed || this.behavior & MAYBE_NEW);
+ Assert.ok(this.onAvailPassed);
+ Assert.ok(this.onDataCheckPassed || this.behavior & MAYBE_NEW);
+ },
+ throwAndNotify(entry) {
+ LOG_C2(this, "Throwing");
+ var self = this;
+ executeSoon(function() {
+ LOG_C2(self, "Notifying");
+ self.goon(entry);
+ });
+ throw Components.Exception("", Cr.NS_ERROR_FAILURE);
+ },
+};
+
+function OpenCallback(behavior, workingMetadata, workingData, goon) {
+ this.behavior = behavior;
+ this.workingMetadata = workingMetadata;
+ this.workingData = workingData;
+ this.goon = goon;
+ this.onCheckPassed =
+ (!!(behavior & (NEW | RECREATE)) || !workingMetadata) &&
+ !(behavior & NOTVALID);
+ this.onAvailPassed = false;
+ this.onDataCheckPassed =
+ !!(behavior & (NEW | RECREATE | NOTWANTED)) || !workingMetadata;
+ callbacks.push(this);
+ this.order = callbacks.length;
+}
+
+VisitCallback.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["nsICacheStorageVisitor"]),
+ onCacheStorageInfo(num, consumption) {
+ LOG_C2(this, "onCacheStorageInfo: num=" + num + ", size=" + consumption);
+ Assert.equal(this.num, num);
+ Assert.equal(this.consumption, consumption);
+ if (!this.entries) {
+ this.notify();
+ }
+ },
+ onCacheEntryInfo(
+ aURI,
+ aIdEnhance,
+ aDataSize,
+ aFetchCount,
+ aLastModifiedTime,
+ aExpirationTime,
+ aPinned,
+ aInfo
+ ) {
+ var key = (aIdEnhance ? aIdEnhance + ":" : "") + aURI.asciiSpec;
+ LOG_C2(this, "onCacheEntryInfo: key=" + key);
+
+ function findCacheIndex(element) {
+ if (typeof element === "string") {
+ return element === key;
+ } else if (typeof element === "object") {
+ return (
+ element.uri === key &&
+ element.lci.isAnonymous === aInfo.isAnonymous &&
+ ChromeUtils.isOriginAttributesEqual(
+ element.lci.originAttributes,
+ aInfo.originAttributes
+ )
+ );
+ }
+
+ return false;
+ }
+
+ Assert.ok(!!this.entries);
+
+ var index = this.entries.findIndex(findCacheIndex);
+ Assert.ok(index > -1);
+
+ this.entries.splice(index, 1);
+ },
+ onCacheEntryVisitCompleted() {
+ LOG_C2(this, "onCacheEntryVisitCompleted");
+ if (this.entries) {
+ Assert.equal(this.entries.length, 0);
+ }
+ this.notify();
+ },
+ notify() {
+ Assert.ok(!!this.goon);
+ var goon = this.goon;
+ this.goon = null;
+ executeSoon(goon);
+ },
+ selfCheck() {
+ Assert.ok(!this.entries || !this.entries.length);
+ },
+};
+
+function VisitCallback(num, consumption, entries, goon) {
+ this.num = num;
+ this.consumption = consumption;
+ this.entries = entries;
+ this.goon = goon;
+ callbacks.push(this);
+ this.order = callbacks.length;
+}
+
+EvictionCallback.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["nsICacheEntryDoomCallback"]),
+ onCacheEntryDoomed(result) {
+ Assert.equal(this.expectedSuccess, result == Cr.NS_OK);
+ this.goon();
+ },
+ selfCheck() {},
+};
+
+function EvictionCallback(success, goon) {
+ this.expectedSuccess = success;
+ this.goon = goon;
+ callbacks.push(this);
+ this.order = callbacks.length;
+}
+
+MultipleCallbacks.prototype = {
+ fired() {
+ if (--this.pending == 0) {
+ var self = this;
+ if (this.delayed) {
+ executeSoon(function() {
+ self.goon();
+ });
+ } else {
+ this.goon();
+ }
+ }
+ },
+ add() {
+ ++this.pending;
+ },
+};
+
+function MultipleCallbacks(number, goon, delayed) {
+ this.pending = number;
+ this.goon = goon;
+ this.delayed = delayed;
+}
+
+function wait_for_cache_index(continue_func) {
+ // This callback will not fire before the index is in the ready state. nsICacheStorage.exists() will
+ // no longer throw after this point.
+ get_cache_service().asyncGetDiskConsumption({
+ onNetworkCacheDiskConsumption() {
+ continue_func();
+ },
+ // eslint-disable-next-line mozilla/use-chromeutils-generateqi
+ QueryInterface() {
+ return this;
+ },
+ });
+}
+
+function finish_cache2_test() {
+ callbacks.forEach(function(callback, index) {
+ callback.selfCheck();
+ });
+ do_test_finished();
+}
diff --git a/netwerk/test/unit/head_channels.js b/netwerk/test/unit/head_channels.js
new file mode 100644
index 0000000000..8761032238
--- /dev/null
+++ b/netwerk/test/unit/head_channels.js
@@ -0,0 +1,456 @@
+/**
+ * Read count bytes from stream and return as a String object
+ */
+
+/* import-globals-from head_cache.js */
+/* import-globals-from head_cookies.js */
+
+function read_stream(stream, count) {
+ /* assume stream has non-ASCII data */
+ var wrapper = Cc["@mozilla.org/binaryinputstream;1"].createInstance(
+ Ci.nsIBinaryInputStream
+ );
+ wrapper.setInputStream(stream);
+ /* JS methods can be called with a maximum of 65535 arguments, and input
+ streams don't have to return all the data they make .available() when
+ asked to .read() that number of bytes. */
+ var data = [];
+ while (count > 0) {
+ var bytes = wrapper.readByteArray(Math.min(65535, count));
+ data.push(String.fromCharCode.apply(null, bytes));
+ count -= bytes.length;
+ if (bytes.length == 0) {
+ do_throw("Nothing read from input stream!");
+ }
+ }
+ return data.join("");
+}
+
+const CL_EXPECT_FAILURE = 0x1;
+const CL_EXPECT_GZIP = 0x2;
+const CL_EXPECT_3S_DELAY = 0x4;
+const CL_SUSPEND = 0x8;
+const CL_ALLOW_UNKNOWN_CL = 0x10;
+const CL_EXPECT_LATE_FAILURE = 0x20;
+const CL_FROM_CACHE = 0x40; // Response must be from the cache
+const CL_NOT_FROM_CACHE = 0x80; // Response must NOT be from the cache
+const CL_IGNORE_CL = 0x100; // don't bother to verify the content-length
+
+const SUSPEND_DELAY = 3000;
+
+/**
+ * A stream listener that calls a callback function with a specified
+ * context and the received data when the channel is loaded.
+ *
+ * Signature of the closure:
+ * void closure(in nsIRequest request, in ACString data, in JSObject context);
+ *
+ * This listener makes sure that various parts of the channel API are
+ * implemented correctly and that the channel's status is a success code
+ * (you can pass CL_EXPECT_FAILURE or CL_EXPECT_LATE_FAILURE as flags
+ * to allow a failure code)
+ *
+ * Note that it also requires a valid content length on the channel and
+ * is thus not fully generic.
+ */
+function ChannelListener(closure, ctx, flags) {
+ this._closure = closure;
+ this._closurectx = ctx;
+ this._flags = flags;
+ this._isFromCache = false;
+ this._cacheEntryId = undefined;
+}
+ChannelListener.prototype = {
+ _closure: null,
+ _closurectx: null,
+ _buffer: "",
+ _got_onstartrequest: false,
+ _got_onstoprequest: false,
+ _contentLen: -1,
+ _lastEvent: 0,
+
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIStreamListener",
+ "nsIRequestObserver",
+ ]),
+
+ onStartRequest(request) {
+ try {
+ if (this._got_onstartrequest) {
+ do_throw("Got second onStartRequest event!");
+ }
+ this._got_onstartrequest = true;
+ this._lastEvent = Date.now();
+
+ try {
+ this._isFromCache = request
+ .QueryInterface(Ci.nsICacheInfoChannel)
+ .isFromCache();
+ } catch (e) {}
+
+ var thrown = false;
+ try {
+ this._cacheEntryId = request
+ .QueryInterface(Ci.nsICacheInfoChannel)
+ .getCacheEntryId();
+ } catch (e) {
+ thrown = true;
+ }
+ if (this._isFromCache && thrown) {
+ do_throw("Should get a CacheEntryId");
+ } else if (!this._isFromCache && !thrown) {
+ do_throw("Shouldn't get a CacheEntryId");
+ }
+
+ request.QueryInterface(Ci.nsIChannel);
+ try {
+ this._contentLen = request.contentLength;
+ } catch (ex) {
+ if (!(this._flags & (CL_EXPECT_FAILURE | CL_ALLOW_UNKNOWN_CL))) {
+ do_throw("Could not get contentLength");
+ }
+ }
+ if (!request.isPending()) {
+ do_throw("request reports itself as not pending from onStartRequest!");
+ }
+ if (
+ this._contentLen == -1 &&
+ !(this._flags & (CL_EXPECT_FAILURE | CL_ALLOW_UNKNOWN_CL))
+ ) {
+ do_throw("Content length is unknown in onStartRequest!");
+ }
+
+ if (this._flags & CL_FROM_CACHE) {
+ request.QueryInterface(Ci.nsICachingChannel);
+ if (!request.isFromCache()) {
+ do_throw("Response is not from the cache (CL_FROM_CACHE)");
+ }
+ }
+ if (this._flags & CL_NOT_FROM_CACHE) {
+ request.QueryInterface(Ci.nsICachingChannel);
+ if (request.isFromCache()) {
+ do_throw("Response is from the cache (CL_NOT_FROM_CACHE)");
+ }
+ }
+
+ if (this._flags & CL_SUSPEND) {
+ request.suspend();
+ do_timeout(SUSPEND_DELAY, function() {
+ request.resume();
+ });
+ }
+ } catch (ex) {
+ do_throw("Error in onStartRequest: " + ex);
+ }
+ },
+
+ onDataAvailable(request, stream, offset, count) {
+ try {
+ let current = Date.now();
+
+ if (!this._got_onstartrequest) {
+ do_throw("onDataAvailable without onStartRequest event!");
+ }
+ if (this._got_onstoprequest) {
+ do_throw("onDataAvailable after onStopRequest event!");
+ }
+ if (!request.isPending()) {
+ do_throw("request reports itself as not pending from onDataAvailable!");
+ }
+ if (this._flags & CL_EXPECT_FAILURE) {
+ do_throw("Got data despite expecting a failure");
+ }
+
+ if (
+ current - this._lastEvent >= SUSPEND_DELAY &&
+ !(this._flags & CL_EXPECT_3S_DELAY)
+ ) {
+ do_throw("Data received after significant unexpected delay");
+ } else if (
+ current - this._lastEvent < SUSPEND_DELAY &&
+ this._flags & CL_EXPECT_3S_DELAY
+ ) {
+ do_throw("Data received sooner than expected");
+ } else if (
+ current - this._lastEvent >= SUSPEND_DELAY &&
+ this._flags & CL_EXPECT_3S_DELAY
+ ) {
+ this._flags &= ~CL_EXPECT_3S_DELAY;
+ } // No more delays expected
+
+ this._buffer = this._buffer.concat(read_stream(stream, count));
+ this._lastEvent = current;
+ } catch (ex) {
+ do_throw("Error in onDataAvailable: " + ex);
+ }
+ },
+
+ onStopRequest(request, status) {
+ try {
+ var success = Components.isSuccessCode(status);
+ if (!this._got_onstartrequest) {
+ do_throw("onStopRequest without onStartRequest event!");
+ }
+ if (this._got_onstoprequest) {
+ do_throw("Got second onStopRequest event!");
+ }
+ this._got_onstoprequest = true;
+ if (
+ this._flags & (CL_EXPECT_FAILURE | CL_EXPECT_LATE_FAILURE) &&
+ success
+ ) {
+ do_throw(
+ "Should have failed to load URL (status is " +
+ status.toString(16) +
+ ")"
+ );
+ } else if (
+ !(this._flags & (CL_EXPECT_FAILURE | CL_EXPECT_LATE_FAILURE)) &&
+ !success
+ ) {
+ do_throw("Failed to load URL: " + status.toString(16));
+ }
+ if (status != request.status) {
+ do_throw("request.status does not match status arg to onStopRequest!");
+ }
+ if (request.isPending()) {
+ do_throw("request reports itself as pending from onStopRequest!");
+ }
+ if (
+ !(
+ this._flags &
+ (CL_EXPECT_FAILURE | CL_EXPECT_LATE_FAILURE | CL_IGNORE_CL)
+ ) &&
+ !(this._flags & CL_EXPECT_GZIP) &&
+ this._contentLen != -1
+ ) {
+ Assert.equal(this._buffer.length, this._contentLen);
+ }
+ } catch (ex) {
+ do_throw("Error in onStopRequest: " + ex);
+ }
+ try {
+ this._closure(
+ request,
+ this._buffer,
+ this._closurectx,
+ this._isFromCache,
+ this._cacheEntryId
+ );
+ this._closurectx = null;
+ } catch (ex) {
+ do_throw("Error in closure function: " + ex);
+ }
+ },
+};
+
+var ES_ABORT_REDIRECT = 0x01;
+
+function ChannelEventSink(flags) {
+ this._flags = flags;
+}
+
+ChannelEventSink.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["nsIInterfaceRequestor"]),
+
+ getInterface(iid) {
+ if (iid.equals(Ci.nsIChannelEventSink)) {
+ return this;
+ }
+ throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE);
+ },
+
+ asyncOnChannelRedirect(oldChannel, newChannel, flags, callback) {
+ if (this._flags & ES_ABORT_REDIRECT) {
+ throw Components.Exception("", Cr.NS_BINDING_ABORTED);
+ }
+
+ callback.onRedirectVerifyCallback(Cr.NS_OK);
+ },
+};
+
+/**
+ * A helper class to construct origin attributes.
+ */
+function OriginAttributes(inIsolatedMozBrowser, privateId) {
+ this.inIsolatedMozBrowser = inIsolatedMozBrowser;
+ this.privateBrowsingId = privateId;
+}
+OriginAttributes.prototype = {
+ inIsolatedMozBrowser: false,
+ privateBrowsingId: 0,
+};
+
+function readFile(file) {
+ let fstream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(
+ Ci.nsIFileInputStream
+ );
+ fstream.init(file, -1, 0, 0);
+ let data = NetUtil.readInputStreamToString(fstream, fstream.available());
+ fstream.close();
+ return data;
+}
+
+function addCertFromFile(certdb, filename, trustString) {
+ let certFile = do_get_file(filename, false);
+ let pem = readFile(certFile)
+ .replace(/-----BEGIN CERTIFICATE-----/, "")
+ .replace(/-----END CERTIFICATE-----/, "")
+ .replace(/[\r\n]/g, "");
+ certdb.addCertFromBase64(pem, trustString);
+}
+
+// Helper code to test nsISerializable
+function serialize_to_escaped_string(obj) {
+ let objectOutStream = Cc["@mozilla.org/binaryoutputstream;1"].createInstance(
+ Ci.nsIObjectOutputStream
+ );
+ let pipe = Cc["@mozilla.org/pipe;1"].createInstance(Ci.nsIPipe);
+ pipe.init(false, false, 0, 0xffffffff, null);
+ objectOutStream.setOutputStream(pipe.outputStream);
+ objectOutStream.writeCompoundObject(obj, Ci.nsISupports, true);
+ objectOutStream.close();
+
+ let objectInStream = Cc["@mozilla.org/binaryinputstream;1"].createInstance(
+ Ci.nsIObjectInputStream
+ );
+ objectInStream.setInputStream(pipe.inputStream);
+ let data = [];
+ // This reads all the data from the stream until an error occurs.
+ while (true) {
+ try {
+ let bytes = objectInStream.readByteArray(1);
+ data.push(String.fromCharCode.apply(null, bytes));
+ } catch (e) {
+ break;
+ }
+ }
+ return escape(data.join(""));
+}
+
+function deserialize_from_escaped_string(str) {
+ let payload = unescape(str);
+ let data = [];
+ let i = 0;
+ while (i < payload.length) {
+ data.push(payload.charCodeAt(i++));
+ }
+
+ let objectOutStream = Cc["@mozilla.org/binaryoutputstream;1"].createInstance(
+ Ci.nsIObjectOutputStream
+ );
+ let pipe = Cc["@mozilla.org/pipe;1"].createInstance(Ci.nsIPipe);
+ pipe.init(false, false, 0, 0xffffffff, null);
+ objectOutStream.setOutputStream(pipe.outputStream);
+ objectOutStream.writeByteArray(data);
+ objectOutStream.close();
+
+ let objectInStream = Cc["@mozilla.org/binaryinputstream;1"].createInstance(
+ Ci.nsIObjectInputStream
+ );
+ objectInStream.setInputStream(pipe.inputStream);
+ return objectInStream.readObject(true);
+}
+
+// Copied from head_psm.js.
+function add_tls_server_setup(serverBinName, certsPath, addDefaultRoot = true) {
+ add_test(function() {
+ _setupTLSServerTest(serverBinName, certsPath, addDefaultRoot);
+ });
+}
+
+// Do not call this directly; use add_tls_server_setup
+function _setupTLSServerTest(serverBinName, certsPath, addDefaultRoot) {
+ asyncStartTLSTestServer(serverBinName, certsPath, addDefaultRoot).then(
+ run_next_test
+ );
+}
+
+async function asyncStartTLSTestServer(
+ serverBinName,
+ certsPath,
+ addDefaultRoot
+) {
+ const { HttpServer } = ChromeUtils.import(
+ "resource://testing-common/httpd.js"
+ );
+ let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(
+ Ci.nsIX509CertDB
+ );
+ // The trusted CA that is typically used for "good" certificates.
+ if (addDefaultRoot) {
+ addCertFromFile(certdb, `${certsPath}/test-ca.pem`, "CTu,u,u");
+ }
+
+ const CALLBACK_PORT = 8444;
+
+ let envSvc = Cc["@mozilla.org/process/environment;1"].getService(
+ Ci.nsIEnvironment
+ );
+ let greBinDir = Services.dirsvc.get("GreBinD", Ci.nsIFile);
+ envSvc.set("DYLD_LIBRARY_PATH", greBinDir.path);
+ // TODO(bug 1107794): Android libraries are in /data/local/xpcb, but "GreBinD"
+ // does not return this path on Android, so hard code it here.
+ envSvc.set("LD_LIBRARY_PATH", greBinDir.path + ":/data/local/xpcb");
+ envSvc.set("MOZ_TLS_SERVER_DEBUG_LEVEL", "3");
+ envSvc.set("MOZ_TLS_SERVER_CALLBACK_PORT", CALLBACK_PORT);
+
+ let httpServer = new HttpServer();
+ let serverReady = new Promise(resolve => {
+ httpServer.registerPathHandler("/", function handleServerCallback(
+ aRequest,
+ aResponse
+ ) {
+ aResponse.setStatusLine(aRequest.httpVersion, 200, "OK");
+ aResponse.setHeader("Content-Type", "text/plain");
+ let responseBody = "OK!";
+ aResponse.bodyOutputStream.write(responseBody, responseBody.length);
+ executeSoon(function() {
+ httpServer.stop(resolve);
+ });
+ });
+ httpServer.start(CALLBACK_PORT);
+ });
+
+ let serverBin = _getBinaryUtil(serverBinName);
+ let process = Cc["@mozilla.org/process/util;1"].createInstance(Ci.nsIProcess);
+ process.init(serverBin);
+ let certDir = do_get_file(certsPath, false);
+ Assert.ok(certDir.exists(), `certificate folder (${certsPath}) should exist`);
+ // Using "sql:" causes the SQL DB to be used so we can run tests on Android.
+ process.run(false, ["sql:" + certDir.path, Services.appinfo.processID], 2);
+
+ registerCleanupFunction(function() {
+ process.kill();
+ });
+
+ await serverReady;
+}
+
+function _getBinaryUtil(binaryUtilName) {
+ let utilBin = Services.dirsvc.get("GreD", Ci.nsIFile);
+ // On macOS, GreD is .../Contents/Resources, and most binary utilities
+ // are located there, but certutil is in GreBinD (or .../Contents/MacOS),
+ // so we have to change the path accordingly.
+ if (binaryUtilName === "certutil") {
+ utilBin = Services.dirsvc.get("GreBinD", Ci.nsIFile);
+ }
+ utilBin.append(binaryUtilName + mozinfo.bin_suffix);
+ // If we're testing locally, the above works. If not, the server executable
+ // is in another location.
+ if (!utilBin.exists()) {
+ utilBin = Services.dirsvc.get("CurWorkD", Ci.nsIFile);
+ while (utilBin.path.includes("xpcshell")) {
+ utilBin = utilBin.parent;
+ }
+ utilBin.append("bin");
+ utilBin.append(binaryUtilName + mozinfo.bin_suffix);
+ }
+ // But maybe we're on Android, where binaries are in /data/local/xpcb.
+ if (!utilBin.exists()) {
+ utilBin.initWithPath("/data/local/xpcb/");
+ utilBin.append(binaryUtilName);
+ }
+ Assert.ok(utilBin.exists(), `Binary util ${binaryUtilName} should exist`);
+ return utilBin;
+}
diff --git a/netwerk/test/unit/head_cookies.js b/netwerk/test/unit/head_cookies.js
new file mode 100644
index 0000000000..feb6939b4f
--- /dev/null
+++ b/netwerk/test/unit/head_cookies.js
@@ -0,0 +1,973 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/* import-globals-from head_cache.js */
+
+"use strict";
+
+const { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm");
+const { CookieXPCShellUtils } = ChromeUtils.import(
+ "resource://testing-common/CookieXPCShellUtils.jsm"
+);
+
+// Don't pick up default permissions from profile.
+Services.prefs.setCharPref("permissions.manager.defaultsUrl", "");
+
+CookieXPCShellUtils.init(this);
+
+XPCOMUtils.defineLazyServiceGetter(
+ Services,
+ "cookiesvc",
+ "@mozilla.org/cookieService;1",
+ "nsICookieService"
+);
+XPCOMUtils.defineLazyServiceGetter(
+ Services,
+ "cookiemgr",
+ "@mozilla.org/cookiemanager;1",
+ "nsICookieManager"
+);
+
+XPCOMUtils.defineLazyServiceGetter(
+ Services,
+ "etld",
+ "@mozilla.org/network/effective-tld-service;1",
+ "nsIEffectiveTLDService"
+);
+
+function do_check_throws(f, result, stack) {
+ if (!stack) {
+ stack = Components.stack.caller;
+ }
+
+ try {
+ f();
+ } catch (exc) {
+ if (exc.result == result) {
+ return;
+ }
+ do_throw("expected result " + result + ", caught " + exc, stack);
+ }
+ do_throw("expected result " + result + ", none thrown", stack);
+}
+
+// Helper to step a generator function and catch a StopIteration exception.
+function do_run_generator(generator) {
+ try {
+ generator.next();
+ } catch (e) {
+ do_throw("caught exception " + e, Components.stack.caller);
+ }
+}
+
+// Helper to finish a generator function test.
+function do_finish_generator_test(generator) {
+ executeSoon(function() {
+ generator.return();
+ do_test_finished();
+ });
+}
+
+function _observer(generator, topic) {
+ Services.obs.addObserver(this, topic);
+
+ this.generator = generator;
+ this.topic = topic;
+}
+
+_observer.prototype = {
+ observe(subject, topic, data) {
+ Assert.equal(this.topic, topic);
+
+ Services.obs.removeObserver(this, this.topic);
+
+ // Continue executing the generator function.
+ if (this.generator) {
+ do_run_generator(this.generator);
+ }
+
+ this.generator = null;
+ this.topic = null;
+ },
+};
+
+// Close the cookie database. If a generator is supplied, it will be invoked
+// once the close is complete.
+function do_close_profile(generator) {
+ // Register an observer for db close.
+ new _observer(generator, "cookie-db-closed");
+
+ // Close the db.
+ let service = Services.cookies.QueryInterface(Ci.nsIObserver);
+ service.observe(null, "profile-before-change", "shutdown-persist");
+}
+
+function _promise_observer(topic) {
+ Services.obs.addObserver(this, topic);
+
+ this.topic = topic;
+ return new Promise(resolve => (this.resolve = resolve));
+}
+
+_promise_observer.prototype = {
+ observe(subject, topic, data) {
+ Assert.equal(this.topic, topic);
+
+ Services.obs.removeObserver(this, this.topic);
+ if (this.resolve) {
+ this.resolve();
+ }
+
+ this.resolve = null;
+ this.topic = null;
+ },
+};
+
+// Close the cookie database. And resolve a promise.
+function promise_close_profile() {
+ // Register an observer for db close.
+ let promise = new _promise_observer("cookie-db-closed");
+
+ // Close the db.
+ let service = Services.cookies.QueryInterface(Ci.nsIObserver);
+ service.observe(null, "profile-before-change", "shutdown-persist");
+
+ return promise;
+}
+
+// Load the cookie database.
+function promise_load_profile() {
+ // Register an observer for read completion.
+ let promise = new _promise_observer("cookie-db-read");
+
+ // Load the profile.
+ let service = Services.cookies.QueryInterface(Ci.nsIObserver);
+ service.observe(null, "profile-do-change", "");
+
+ return promise;
+}
+
+// Load the cookie database. If a generator is supplied, it will be invoked
+// once the load is complete.
+function do_load_profile(generator) {
+ // Register an observer for read completion.
+ new _observer(generator, "cookie-db-read");
+
+ // Load the profile.
+ let service = Services.cookies.QueryInterface(Ci.nsIObserver);
+ service.observe(null, "profile-do-change", "");
+}
+
+// Set a single session cookie using http and test the cookie count
+// against 'expected'
+function do_set_single_http_cookie(uri, channel, expected) {
+ Services.cookiesvc.setCookieStringFromHttp(uri, "foo=bar", channel);
+ Assert.equal(Services.cookiemgr.countCookiesFromHost(uri.host), expected);
+}
+
+// Set two cookies; via document.channel and via http request.
+async function do_set_cookies(uri, channel, session, expected) {
+ let suffix = session ? "" : "; max-age=1000";
+
+ // via document.cookie
+ const thirdPartyUrl = "http://third.com/";
+ const contentPage = await CookieXPCShellUtils.loadContentPage(thirdPartyUrl);
+ await contentPage.spawn(
+ {
+ cookie: "can=has" + suffix,
+ url: uri.spec,
+ },
+ async obj => {
+ // eslint-disable-next-line no-undef
+ await new content.Promise(resolve => {
+ // eslint-disable-next-line no-undef
+ const ifr = content.document.createElement("iframe");
+ // eslint-disable-next-line no-undef
+ content.document.body.appendChild(ifr);
+ ifr.src = obj.url;
+ ifr.onload = () => {
+ ifr.contentDocument.cookie = obj.cookie;
+ resolve();
+ };
+ });
+ }
+ );
+ await contentPage.close();
+
+ Assert.equal(Services.cookiemgr.countCookiesFromHost(uri.host), expected[0]);
+
+ // via http request
+ Services.cookiesvc.setCookieStringFromHttp(uri, "hot=dog" + suffix, channel);
+ Assert.equal(Services.cookiemgr.countCookiesFromHost(uri.host), expected[1]);
+}
+
+function do_count_cookies() {
+ return Services.cookiemgr.cookies.length;
+}
+
+// Helper object to store cookie data.
+function Cookie(
+ name,
+ value,
+ host,
+ path,
+ expiry,
+ lastAccessed,
+ creationTime,
+ isSession,
+ isSecure,
+ isHttpOnly,
+ inBrowserElement = false,
+ originAttributes = {},
+ sameSite = Ci.nsICookie.SAMESITE_NONE,
+ rawSameSite = Ci.nsICookie.SAMESITE_NONE,
+ schemeMap = Ci.nsICookie.SCHEME_UNSET
+) {
+ this.name = name;
+ this.value = value;
+ this.host = host;
+ this.path = path;
+ this.expiry = expiry;
+ this.lastAccessed = lastAccessed;
+ this.creationTime = creationTime;
+ this.isSession = isSession;
+ this.isSecure = isSecure;
+ this.isHttpOnly = isHttpOnly;
+ this.inBrowserElement = inBrowserElement;
+ this.originAttributes = originAttributes;
+ this.sameSite = sameSite;
+ this.rawSameSite = rawSameSite;
+ this.schemeMap = schemeMap;
+
+ let strippedHost = host.charAt(0) == "." ? host.slice(1) : host;
+
+ try {
+ this.baseDomain = Services.etld.getBaseDomainFromHost(strippedHost);
+ } catch (e) {
+ if (
+ e.result == Cr.NS_ERROR_HOST_IS_IP_ADDRESS ||
+ e.result == Cr.NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS
+ ) {
+ this.baseDomain = strippedHost;
+ }
+ }
+}
+
+// Object representing a database connection and associated statements. The
+// implementation varies depending on schema version.
+function CookieDatabaseConnection(file, schema) {
+ // Manually generate a cookies.sqlite file with appropriate rows, columns,
+ // and schema version. If it already exists, just set up our statements.
+ let exists = file.exists();
+
+ this.db = Services.storage.openDatabase(file);
+ this.schema = schema;
+ if (!exists) {
+ this.db.schemaVersion = schema;
+ }
+
+ switch (schema) {
+ case 1: {
+ if (!exists) {
+ this.db.executeSimpleSQL(
+ "CREATE TABLE moz_cookies ( \
+ id INTEGER PRIMARY KEY, \
+ name TEXT, \
+ value TEXT, \
+ host TEXT, \
+ path TEXT, \
+ expiry INTEGER, \
+ isSecure INTEGER, \
+ isHttpOnly INTEGER)"
+ );
+ }
+
+ this.stmtInsert = this.db.createStatement(
+ "INSERT INTO moz_cookies ( \
+ id, \
+ name, \
+ value, \
+ host, \
+ path, \
+ expiry, \
+ isSecure, \
+ isHttpOnly) \
+ VALUES ( \
+ :id, \
+ :name, \
+ :value, \
+ :host, \
+ :path, \
+ :expiry, \
+ :isSecure, \
+ :isHttpOnly)"
+ );
+
+ this.stmtDelete = this.db.createStatement(
+ "DELETE FROM moz_cookies WHERE id = :id"
+ );
+
+ break;
+ }
+
+ case 2: {
+ if (!exists) {
+ this.db.executeSimpleSQL(
+ "CREATE TABLE moz_cookies ( \
+ id INTEGER PRIMARY KEY, \
+ name TEXT, \
+ value TEXT, \
+ host TEXT, \
+ path TEXT, \
+ expiry INTEGER, \
+ lastAccessed INTEGER, \
+ isSecure INTEGER, \
+ isHttpOnly INTEGER)"
+ );
+ }
+
+ this.stmtInsert = this.db.createStatement(
+ "INSERT OR REPLACE INTO moz_cookies ( \
+ id, \
+ name, \
+ value, \
+ host, \
+ path, \
+ expiry, \
+ lastAccessed, \
+ isSecure, \
+ isHttpOnly) \
+ VALUES ( \
+ :id, \
+ :name, \
+ :value, \
+ :host, \
+ :path, \
+ :expiry, \
+ :lastAccessed, \
+ :isSecure, \
+ :isHttpOnly)"
+ );
+
+ this.stmtDelete = this.db.createStatement(
+ "DELETE FROM moz_cookies WHERE id = :id"
+ );
+
+ this.stmtUpdate = this.db.createStatement(
+ "UPDATE moz_cookies SET lastAccessed = :lastAccessed WHERE id = :id"
+ );
+
+ break;
+ }
+
+ case 3: {
+ if (!exists) {
+ this.db.executeSimpleSQL(
+ "CREATE TABLE moz_cookies ( \
+ id INTEGER PRIMARY KEY, \
+ baseDomain TEXT, \
+ name TEXT, \
+ value TEXT, \
+ host TEXT, \
+ path TEXT, \
+ expiry INTEGER, \
+ lastAccessed INTEGER, \
+ isSecure INTEGER, \
+ isHttpOnly INTEGER)"
+ );
+
+ this.db.executeSimpleSQL(
+ "CREATE INDEX moz_basedomain ON moz_cookies (baseDomain)"
+ );
+ }
+
+ this.stmtInsert = this.db.createStatement(
+ "INSERT INTO moz_cookies ( \
+ id, \
+ baseDomain, \
+ name, \
+ value, \
+ host, \
+ path, \
+ expiry, \
+ lastAccessed, \
+ isSecure, \
+ isHttpOnly) \
+ VALUES ( \
+ :id, \
+ :baseDomain, \
+ :name, \
+ :value, \
+ :host, \
+ :path, \
+ :expiry, \
+ :lastAccessed, \
+ :isSecure, \
+ :isHttpOnly)"
+ );
+
+ this.stmtDelete = this.db.createStatement(
+ "DELETE FROM moz_cookies WHERE id = :id"
+ );
+
+ this.stmtUpdate = this.db.createStatement(
+ "UPDATE moz_cookies SET lastAccessed = :lastAccessed WHERE id = :id"
+ );
+
+ break;
+ }
+
+ case 4: {
+ if (!exists) {
+ this.db.executeSimpleSQL(
+ "CREATE TABLE moz_cookies ( \
+ id INTEGER PRIMARY KEY, \
+ baseDomain TEXT, \
+ name TEXT, \
+ value TEXT, \
+ host TEXT, \
+ path TEXT, \
+ expiry INTEGER, \
+ lastAccessed INTEGER, \
+ creationTime INTEGER, \
+ isSecure INTEGER, \
+ isHttpOnly INTEGER \
+ CONSTRAINT moz_uniqueid UNIQUE (name, host, path))"
+ );
+
+ this.db.executeSimpleSQL(
+ "CREATE INDEX moz_basedomain ON moz_cookies (baseDomain)"
+ );
+
+ this.db.executeSimpleSQL("PRAGMA journal_mode = WAL");
+ }
+
+ this.stmtInsert = this.db.createStatement(
+ "INSERT INTO moz_cookies ( \
+ baseDomain, \
+ name, \
+ value, \
+ host, \
+ path, \
+ expiry, \
+ lastAccessed, \
+ creationTime, \
+ isSecure, \
+ isHttpOnly) \
+ VALUES ( \
+ :baseDomain, \
+ :name, \
+ :value, \
+ :host, \
+ :path, \
+ :expiry, \
+ :lastAccessed, \
+ :creationTime, \
+ :isSecure, \
+ :isHttpOnly)"
+ );
+
+ this.stmtDelete = this.db.createStatement(
+ "DELETE FROM moz_cookies \
+ WHERE name = :name AND host = :host AND path = :path"
+ );
+
+ this.stmtUpdate = this.db.createStatement(
+ "UPDATE moz_cookies SET lastAccessed = :lastAccessed \
+ WHERE name = :name AND host = :host AND path = :path"
+ );
+
+ break;
+ }
+
+ case 10: {
+ if (!exists) {
+ this.db.executeSimpleSQL(
+ "CREATE TABLE moz_cookies ( \
+ id INTEGER PRIMARY KEY, \
+ baseDomain TEXT, \
+ originAttributes TEXT NOT NULL DEFAULT '', \
+ name TEXT, \
+ value TEXT, \
+ host TEXT, \
+ path TEXT, \
+ expiry INTEGER, \
+ lastAccessed INTEGER, \
+ creationTime INTEGER, \
+ isSecure INTEGER, \
+ isHttpOnly INTEGER, \
+ inBrowserElement INTEGER DEFAULT 0, \
+ sameSite INTEGER DEFAULT 0, \
+ rawSameSite INTEGER DEFAULT 0, \
+ CONSTRAINT moz_uniqueid UNIQUE (name, host, path, originAttributes))"
+ );
+
+ this.db.executeSimpleSQL(
+ "CREATE INDEX moz_basedomain ON moz_cookies (baseDomain)"
+ );
+
+ this.db.executeSimpleSQL("PRAGMA journal_mode = WAL");
+ this.db.executeSimpleSQL("PRAGMA wal_autocheckpoint = 16");
+ }
+
+ this.stmtInsert = this.db.createStatement(
+ "INSERT INTO moz_cookies ( \
+ name, \
+ value, \
+ host, \
+ baseDomain, \
+ path, \
+ expiry, \
+ lastAccessed, \
+ creationTime, \
+ isSecure, \
+ isHttpOnly, \
+ inBrowserElement, \
+ originAttributes, \
+ sameSite, \
+ rawSameSite \
+ ) VALUES ( \
+ :name, \
+ :value, \
+ :host, \
+ :baseDomain, \
+ :path, \
+ :expiry, \
+ :lastAccessed, \
+ :creationTime, \
+ :isSecure, \
+ :isHttpOnly, \
+ :inBrowserElement, \
+ :originAttributes, \
+ :sameSite, \
+ :rawSameSite)"
+ );
+
+ this.stmtDelete = this.db.createStatement(
+ "DELETE FROM moz_cookies \
+ WHERE name = :name AND host = :host AND path = :path AND \
+ originAttributes = :originAttributes"
+ );
+
+ this.stmtUpdate = this.db.createStatement(
+ "UPDATE moz_cookies SET lastAccessed = :lastAccessed \
+ WHERE name = :name AND host = :host AND path = :path AND \
+ originAttributes = :originAttributes"
+ );
+
+ break;
+ }
+
+ case 11: {
+ if (!exists) {
+ this.db.executeSimpleSQL(
+ "CREATE TABLE moz_cookies ( \
+ id INTEGER PRIMARY KEY, \
+ originAttributes TEXT NOT NULL DEFAULT '', \
+ name TEXT, \
+ value TEXT, \
+ host TEXT, \
+ path TEXT, \
+ expiry INTEGER, \
+ lastAccessed INTEGER, \
+ creationTime INTEGER, \
+ isSecure INTEGER, \
+ isHttpOnly INTEGER, \
+ inBrowserElement INTEGER DEFAULT 0, \
+ sameSite INTEGER DEFAULT 0, \
+ rawSameSite INTEGER DEFAULT 0, \
+ CONSTRAINT moz_uniqueid UNIQUE (name, host, path, originAttributes))"
+ );
+
+ this.db.executeSimpleSQL("PRAGMA journal_mode = WAL");
+ this.db.executeSimpleSQL("PRAGMA wal_autocheckpoint = 16");
+ }
+
+ this.stmtInsert = this.db.createStatement(
+ "INSERT INTO moz_cookies ( \
+ name, \
+ value, \
+ host, \
+ path, \
+ expiry, \
+ lastAccessed, \
+ creationTime, \
+ isSecure, \
+ isHttpOnly, \
+ inBrowserElement, \
+ originAttributes, \
+ sameSite, \
+ rawSameSite \
+ ) VALUES ( \
+ :name, \
+ :value, \
+ :host, \
+ :path, \
+ :expiry, \
+ :lastAccessed, \
+ :creationTime, \
+ :isSecure, \
+ :isHttpOnly, \
+ :inBrowserElement, \
+ :originAttributes, \
+ :sameSite, \
+ :rawSameSite)"
+ );
+
+ this.stmtDelete = this.db.createStatement(
+ "DELETE FROM moz_cookies \
+ WHERE name = :name AND host = :host AND path = :path AND \
+ originAttributes = :originAttributes"
+ );
+
+ this.stmtUpdate = this.db.createStatement(
+ "UPDATE moz_cookies SET lastAccessed = :lastAccessed \
+ WHERE name = :name AND host = :host AND path = :path AND \
+ originAttributes = :originAttributes"
+ );
+
+ break;
+ }
+
+ case 12: {
+ if (!exists) {
+ this.db.executeSimpleSQL(
+ "CREATE TABLE moz_cookies ( \
+ id INTEGER PRIMARY KEY, \
+ originAttributes TEXT NOT NULL DEFAULT '', \
+ name TEXT, \
+ value TEXT, \
+ host TEXT, \
+ path TEXT, \
+ expiry INTEGER, \
+ lastAccessed INTEGER, \
+ creationTime INTEGER, \
+ isSecure INTEGER, \
+ isHttpOnly INTEGER, \
+ inBrowserElement INTEGER DEFAULT 0, \
+ sameSite INTEGER DEFAULT 0, \
+ rawSameSite INTEGER DEFAULT 0, \
+ schemeMap INTEGER DEFAULT 0, \
+ CONSTRAINT moz_uniqueid UNIQUE (name, host, path, originAttributes))"
+ );
+
+ this.db.executeSimpleSQL("PRAGMA journal_mode = WAL");
+ this.db.executeSimpleSQL("PRAGMA wal_autocheckpoint = 16");
+ }
+
+ this.stmtInsert = this.db.createStatement(
+ "INSERT INTO moz_cookies ( \
+ name, \
+ value, \
+ host, \
+ path, \
+ expiry, \
+ lastAccessed, \
+ creationTime, \
+ isSecure, \
+ isHttpOnly, \
+ inBrowserElement, \
+ originAttributes, \
+ sameSite, \
+ rawSameSite, \
+ schemeMap \
+ ) VALUES ( \
+ :name, \
+ :value, \
+ :host, \
+ :path, \
+ :expiry, \
+ :lastAccessed, \
+ :creationTime, \
+ :isSecure, \
+ :isHttpOnly, \
+ :inBrowserElement, \
+ :originAttributes, \
+ :sameSite, \
+ :rawSameSite, \
+ :schemeMap)"
+ );
+
+ this.stmtDelete = this.db.createStatement(
+ "DELETE FROM moz_cookies \
+ WHERE name = :name AND host = :host AND path = :path AND \
+ originAttributes = :originAttributes"
+ );
+
+ this.stmtUpdate = this.db.createStatement(
+ "UPDATE moz_cookies SET lastAccessed = :lastAccessed \
+ WHERE name = :name AND host = :host AND path = :path AND \
+ originAttributes = :originAttributes"
+ );
+
+ break;
+ }
+
+ default:
+ do_throw("unrecognized schemaVersion!");
+ }
+}
+
+CookieDatabaseConnection.prototype = {
+ insertCookie(cookie) {
+ if (!(cookie instanceof Cookie)) {
+ do_throw("not a cookie");
+ }
+
+ switch (this.schema) {
+ case 1:
+ this.stmtInsert.bindByName("id", cookie.creationTime);
+ this.stmtInsert.bindByName("name", cookie.name);
+ this.stmtInsert.bindByName("value", cookie.value);
+ this.stmtInsert.bindByName("host", cookie.host);
+ this.stmtInsert.bindByName("path", cookie.path);
+ this.stmtInsert.bindByName("expiry", cookie.expiry);
+ this.stmtInsert.bindByName("isSecure", cookie.isSecure);
+ this.stmtInsert.bindByName("isHttpOnly", cookie.isHttpOnly);
+ break;
+
+ case 2:
+ this.stmtInsert.bindByName("id", cookie.creationTime);
+ this.stmtInsert.bindByName("name", cookie.name);
+ this.stmtInsert.bindByName("value", cookie.value);
+ this.stmtInsert.bindByName("host", cookie.host);
+ this.stmtInsert.bindByName("path", cookie.path);
+ this.stmtInsert.bindByName("expiry", cookie.expiry);
+ this.stmtInsert.bindByName("lastAccessed", cookie.lastAccessed);
+ this.stmtInsert.bindByName("isSecure", cookie.isSecure);
+ this.stmtInsert.bindByName("isHttpOnly", cookie.isHttpOnly);
+ break;
+
+ case 3:
+ this.stmtInsert.bindByName("id", cookie.creationTime);
+ this.stmtInsert.bindByName("baseDomain", cookie.baseDomain);
+ this.stmtInsert.bindByName("name", cookie.name);
+ this.stmtInsert.bindByName("value", cookie.value);
+ this.stmtInsert.bindByName("host", cookie.host);
+ this.stmtInsert.bindByName("path", cookie.path);
+ this.stmtInsert.bindByName("expiry", cookie.expiry);
+ this.stmtInsert.bindByName("lastAccessed", cookie.lastAccessed);
+ this.stmtInsert.bindByName("isSecure", cookie.isSecure);
+ this.stmtInsert.bindByName("isHttpOnly", cookie.isHttpOnly);
+ break;
+
+ case 4:
+ this.stmtInsert.bindByName("baseDomain", cookie.baseDomain);
+ this.stmtInsert.bindByName("name", cookie.name);
+ this.stmtInsert.bindByName("value", cookie.value);
+ this.stmtInsert.bindByName("host", cookie.host);
+ this.stmtInsert.bindByName("path", cookie.path);
+ this.stmtInsert.bindByName("expiry", cookie.expiry);
+ this.stmtInsert.bindByName("lastAccessed", cookie.lastAccessed);
+ this.stmtInsert.bindByName("creationTime", cookie.creationTime);
+ this.stmtInsert.bindByName("isSecure", cookie.isSecure);
+ this.stmtInsert.bindByName("isHttpOnly", cookie.isHttpOnly);
+ break;
+
+ case 10:
+ this.stmtInsert.bindByName("name", cookie.name);
+ this.stmtInsert.bindByName("value", cookie.value);
+ this.stmtInsert.bindByName("host", cookie.host);
+ this.stmtInsert.bindByName("baseDomain", cookie.baseDomain);
+ this.stmtInsert.bindByName("path", cookie.path);
+ this.stmtInsert.bindByName("expiry", cookie.expiry);
+ this.stmtInsert.bindByName("lastAccessed", cookie.lastAccessed);
+ this.stmtInsert.bindByName("creationTime", cookie.creationTime);
+ this.stmtInsert.bindByName("isSecure", cookie.isSecure);
+ this.stmtInsert.bindByName("isHttpOnly", cookie.isHttpOnly);
+ this.stmtInsert.bindByName("inBrowserElement", cookie.inBrowserElement);
+ this.stmtInsert.bindByName(
+ "originAttributes",
+ ChromeUtils.originAttributesToSuffix(cookie.originAttributes)
+ );
+ this.stmtInsert.bindByName("sameSite", cookie.sameSite);
+ this.stmtInsert.bindByName("rawSameSite", cookie.rawSameSite);
+ break;
+
+ case 11:
+ this.stmtInsert.bindByName("name", cookie.name);
+ this.stmtInsert.bindByName("value", cookie.value);
+ this.stmtInsert.bindByName("host", cookie.host);
+ this.stmtInsert.bindByName("path", cookie.path);
+ this.stmtInsert.bindByName("expiry", cookie.expiry);
+ this.stmtInsert.bindByName("lastAccessed", cookie.lastAccessed);
+ this.stmtInsert.bindByName("creationTime", cookie.creationTime);
+ this.stmtInsert.bindByName("isSecure", cookie.isSecure);
+ this.stmtInsert.bindByName("isHttpOnly", cookie.isHttpOnly);
+ this.stmtInsert.bindByName("inBrowserElement", cookie.inBrowserElement);
+ this.stmtInsert.bindByName(
+ "originAttributes",
+ ChromeUtils.originAttributesToSuffix(cookie.originAttributes)
+ );
+ this.stmtInsert.bindByName("sameSite", cookie.sameSite);
+ this.stmtInsert.bindByName("rawSameSite", cookie.rawSameSite);
+ break;
+
+ case 12:
+ this.stmtInsert.bindByName("name", cookie.name);
+ this.stmtInsert.bindByName("value", cookie.value);
+ this.stmtInsert.bindByName("host", cookie.host);
+ this.stmtInsert.bindByName("path", cookie.path);
+ this.stmtInsert.bindByName("expiry", cookie.expiry);
+ this.stmtInsert.bindByName("lastAccessed", cookie.lastAccessed);
+ this.stmtInsert.bindByName("creationTime", cookie.creationTime);
+ this.stmtInsert.bindByName("isSecure", cookie.isSecure);
+ this.stmtInsert.bindByName("isHttpOnly", cookie.isHttpOnly);
+ this.stmtInsert.bindByName("inBrowserElement", cookie.inBrowserElement);
+ this.stmtInsert.bindByName(
+ "originAttributes",
+ ChromeUtils.originAttributesToSuffix(cookie.originAttributes)
+ );
+ this.stmtInsert.bindByName("sameSite", cookie.sameSite);
+ this.stmtInsert.bindByName("rawSameSite", cookie.rawSameSite);
+ this.stmtInsert.bindByName("schemeMap", cookie.schemeMap);
+ break;
+
+ default:
+ do_throw("unrecognized schemaVersion!");
+ }
+
+ do_execute_stmt(this.stmtInsert);
+ },
+
+ deleteCookie(cookie) {
+ if (!(cookie instanceof Cookie)) {
+ do_throw("not a cookie");
+ }
+
+ switch (this.db.schemaVersion) {
+ case 1:
+ case 2:
+ case 3:
+ this.stmtDelete.bindByName("id", cookie.creationTime);
+ break;
+
+ case 4:
+ this.stmtDelete.bindByName("name", cookie.name);
+ this.stmtDelete.bindByName("host", cookie.host);
+ this.stmtDelete.bindByName("path", cookie.path);
+ break;
+
+ case 10:
+ case 11:
+ case 12:
+ this.stmtDelete.bindByName("name", cookie.name);
+ this.stmtDelete.bindByName("host", cookie.host);
+ this.stmtDelete.bindByName("path", cookie.path);
+ this.stmtDelete.bindByName(
+ "originAttributes",
+ ChromeUtils.originAttributesToSuffix(cookie.originAttributes)
+ );
+ break;
+
+ default:
+ do_throw("unrecognized schemaVersion!");
+ }
+
+ do_execute_stmt(this.stmtDelete);
+ },
+
+ updateCookie(cookie) {
+ if (!(cookie instanceof Cookie)) {
+ do_throw("not a cookie");
+ }
+
+ switch (this.db.schemaVersion) {
+ case 1:
+ do_throw("can't update a schema 1 cookie!");
+ break;
+ case 2:
+ case 3:
+ this.stmtUpdate.bindByName("id", cookie.creationTime);
+ this.stmtUpdate.bindByName("lastAccessed", cookie.lastAccessed);
+ break;
+
+ case 4:
+ this.stmtDelete.bindByName("name", cookie.name);
+ this.stmtDelete.bindByName("host", cookie.host);
+ this.stmtDelete.bindByName("path", cookie.path);
+ this.stmtUpdate.bindByName("name", cookie.name);
+ this.stmtUpdate.bindByName("host", cookie.host);
+ this.stmtUpdate.bindByName("path", cookie.path);
+ this.stmtUpdate.bindByName("lastAccessed", cookie.lastAccessed);
+ break;
+
+ case 10:
+ case 11:
+ case 12:
+ this.stmtDelete.bindByName("name", cookie.name);
+ this.stmtDelete.bindByName("host", cookie.host);
+ this.stmtDelete.bindByName("path", cookie.path);
+ this.stmtDelete.bindByName(
+ "originAttributes",
+ ChromeUtils.originAttributesToSuffix(cookie.originAttributes)
+ );
+ this.stmtUpdate.bindByName("name", cookie.name);
+ this.stmtUpdate.bindByName("host", cookie.host);
+ this.stmtUpdate.bindByName("path", cookie.path);
+ this.stmtUpdate.bindByName(
+ "originAttributes",
+ ChromeUtils.originAttributesToSuffix(cookie.originAttributes)
+ );
+ this.stmtUpdate.bindByName("lastAccessed", cookie.lastAccessed);
+ break;
+
+ default:
+ do_throw("unrecognized schemaVersion!");
+ }
+
+ do_execute_stmt(this.stmtUpdate);
+ },
+
+ close() {
+ this.stmtInsert.finalize();
+ this.stmtDelete.finalize();
+ if (this.stmtUpdate) {
+ this.stmtUpdate.finalize();
+ }
+ this.db.close();
+
+ this.stmtInsert = null;
+ this.stmtDelete = null;
+ this.stmtUpdate = null;
+ this.db = null;
+ },
+};
+
+function do_get_cookie_file(profile) {
+ let file = profile.clone();
+ file.append("cookies.sqlite");
+ return file;
+}
+
+// Count the cookies from 'host' in a database. If 'host' is null, count all
+// cookies.
+function do_count_cookies_in_db(connection, host) {
+ let select = null;
+ if (host) {
+ select = connection.createStatement(
+ "SELECT COUNT(1) FROM moz_cookies WHERE host = :host"
+ );
+ select.bindByName("host", host);
+ } else {
+ select = connection.createStatement("SELECT COUNT(1) FROM moz_cookies");
+ }
+
+ select.executeStep();
+ let result = select.getInt32(0);
+ select.reset();
+ select.finalize();
+ return result;
+}
+
+// Execute 'stmt', ensuring that we reset it if it throws.
+function do_execute_stmt(stmt) {
+ try {
+ stmt.executeStep();
+ stmt.reset();
+ } catch (e) {
+ stmt.reset();
+ throw e;
+ }
+}
diff --git a/netwerk/test/unit/head_http3.js b/netwerk/test/unit/head_http3.js
new file mode 100644
index 0000000000..9fffa72ea4
--- /dev/null
+++ b/netwerk/test/unit/head_http3.js
@@ -0,0 +1,105 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* import-globals-from head_channels.js */
+/* import-globals-from head_cookies.js */
+
+async function http3_setup_tests() {
+ let env = Cc["@mozilla.org/process/environment;1"].getService(
+ Ci.nsIEnvironment
+ );
+
+ let h3Port = env.get("MOZHTTP3_PORT");
+ Assert.notEqual(h3Port, null);
+ Assert.notEqual(h3Port, "");
+ let h3Route = "foo.example.com:" + h3Port;
+ do_get_profile();
+
+ Services.prefs.setBoolPref("network.http.http3.enabled", true);
+ Services.prefs.setCharPref("network.dns.localDomains", "foo.example.com");
+ Services.prefs.setBoolPref("network.dns.disableIPv6", true);
+ Services.prefs.setCharPref(
+ "network.http.http3.alt-svc-mapping-for-testing",
+ "foo.example.com;h3-27=:" + h3Port
+ );
+
+ let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(
+ Ci.nsIX509CertDB
+ );
+ addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u");
+
+ await setup_altsvc("https://foo.example.com/", h3Route);
+}
+
+function makeChan(uri) {
+ let chan = NetUtil.newChannel({
+ uri,
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+ chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI;
+ return chan;
+}
+
+let CheckHttp3Listener = function() {};
+
+CheckHttp3Listener.prototype = {
+ expectedRoute: "",
+
+ onStartRequest: function testOnStartRequest(request) {},
+
+ onDataAvailable: function testOnDataAvailable(request, stream, off, cnt) {
+ read_stream(stream, cnt);
+ },
+
+ onStopRequest: function testOnStopRequest(request, status) {
+ let routed = "NA";
+ try {
+ routed = request.getRequestHeader("Alt-Used");
+ } catch (e) {}
+ dump("routed is " + routed + "\n");
+
+ if (routed == this.expectedRoute) {
+ let httpVersion = "";
+ try {
+ httpVersion = request.protocolVersion;
+ } catch (e) {}
+ Assert.equal(httpVersion, "h3");
+ this.finish(true);
+ } else {
+ dump("try again to get alt svc mapping\n");
+ this.finish(false);
+ }
+ },
+};
+
+async function setup_altsvc(uri, expectedRoute) {
+ let result = false;
+ do {
+ let chan = makeChan(uri);
+ let listener = new CheckHttp3Listener();
+ listener.expectedRoute = expectedRoute;
+ result = await altsvcSetupPromise(chan, listener);
+ dump("results=" + result);
+ } while (result === false);
+}
+
+function altsvcSetupPromise(chan, listener) {
+ return new Promise(resolve => {
+ function finish(result) {
+ resolve(result);
+ }
+ listener.finish = finish;
+ chan.asyncOpen(listener);
+ });
+}
+
+function http3_clear_prefs() {
+ Services.prefs.clearUserPref("network.http.http3.enabled");
+ Services.prefs.clearUserPref("network.dns.localDomains");
+ Services.prefs.clearUserPref("network.dns.disableIPv6");
+ Services.prefs.clearUserPref(
+ "network.http.http3.alt-svc-mapping-for-testing"
+ );
+ dump("cleanup done\n");
+}
diff --git a/netwerk/test/unit/head_trr.js b/netwerk/test/unit/head_trr.js
new file mode 100644
index 0000000000..bc488ec6cc
--- /dev/null
+++ b/netwerk/test/unit/head_trr.js
@@ -0,0 +1,364 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from head_cache.js */
+/* import-globals-from head_cookies.js */
+/* import-globals-from head_channels.js */
+
+/* globals require, __dirname, global, Buffer */
+
+const { NodeServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+/// Sets the TRR related prefs and adds the certificate we use for the HTTP2
+/// server.
+function trr_test_setup() {
+ dump("start!\n");
+
+ // Set to allow the cert presented by our H2 server
+ do_get_profile();
+
+ Services.prefs.setBoolPref("network.http.spdy.enabled", true);
+ Services.prefs.setBoolPref("network.http.spdy.enabled.http2", true);
+ // the TRR server is on 127.0.0.1
+ Services.prefs.setCharPref("network.trr.bootstrapAddress", "127.0.0.1");
+
+ // use the h2 server as DOH provider
+ // make all native resolve calls "secretly" resolve localhost instead
+ Services.prefs.setBoolPref("network.dns.native-is-localhost", true);
+
+ // 0 - off, 1 - reserved, 2 - TRR first, 3 - TRR only, 4 - reserved
+ Services.prefs.setIntPref("network.trr.mode", 2); // TRR first
+ Services.prefs.setBoolPref("network.trr.wait-for-portal", false);
+ // By default wait for all responses before notifying the listeners.
+ Services.prefs.setBoolPref("network.trr.wait-for-A-and-AAAA", true);
+ // don't confirm that TRR is working, just go!
+ Services.prefs.setCharPref("network.trr.confirmationNS", "skip");
+ // some tests rely on the cache not being cleared on pref change.
+ // we specifically test that this works
+ Services.prefs.setBoolPref("network.trr.clear-cache-on-pref-change", false);
+
+ // The moz-http2 cert is for foo.example.com and is signed by http2-ca.pem
+ // so add that cert to the trust list as a signing cert. // the foo.example.com domain name.
+ let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(
+ Ci.nsIX509CertDB
+ );
+ addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u");
+}
+
+/// Clears the prefs that we're likely to set while testing TRR code
+function trr_clear_prefs() {
+ Services.prefs.clearUserPref("network.trr.mode");
+ Services.prefs.clearUserPref("network.trr.uri");
+ Services.prefs.clearUserPref("network.trr.credentials");
+ Services.prefs.clearUserPref("network.trr.wait-for-portal");
+ Services.prefs.clearUserPref("network.trr.allow-rfc1918");
+ Services.prefs.clearUserPref("network.trr.useGET");
+ Services.prefs.clearUserPref("network.trr.confirmationNS");
+ Services.prefs.clearUserPref("network.trr.bootstrapAddress");
+ Services.prefs.clearUserPref("network.trr.blacklist-duration");
+ Services.prefs.clearUserPref("network.trr.request_timeout_ms");
+ Services.prefs.clearUserPref("network.trr.request_timeout_mode_trronly_ms");
+ Services.prefs.clearUserPref("network.trr.disable-ECS");
+ Services.prefs.clearUserPref("network.trr.early-AAAA");
+ Services.prefs.clearUserPref("network.trr.skip-AAAA-when-not-supported");
+ Services.prefs.clearUserPref("network.trr.wait-for-A-and-AAAA");
+ Services.prefs.clearUserPref("network.trr.excluded-domains");
+ Services.prefs.clearUserPref("network.trr.builtin-excluded-domains");
+ Services.prefs.clearUserPref("network.trr.clear-cache-on-pref-change");
+ Services.prefs.clearUserPref("network.trr.fetch_off_main_thread");
+ Services.prefs.clearUserPref("captivedetect.canonicalURL");
+
+ Services.prefs.clearUserPref("network.http.spdy.enabled");
+ Services.prefs.clearUserPref("network.http.spdy.enabled.http2");
+ Services.prefs.clearUserPref("network.dns.localDomains");
+ Services.prefs.clearUserPref("network.dns.native-is-localhost");
+ Services.prefs.clearUserPref(
+ "network.trr.send_empty_accept-encoding_headers"
+ );
+}
+
+/// This class sends a DNS query and can be awaited as a promise to get the
+/// response.
+class TRRDNSListener {
+ constructor(name, options = {}) {
+ this.name = name;
+ this.options = options;
+ this.expectedAnswer = options.expectedAnswer ?? undefined;
+ this.expectedSuccess = options.expectedSuccess ?? true;
+ this.delay = options.delay;
+ this.promise = new Promise(resolve => {
+ this.resolve = resolve;
+ });
+ let trrServer = options.trrServer || "";
+
+ const dns = Cc["@mozilla.org/network/dns-service;1"].getService(
+ Ci.nsIDNSService
+ );
+ const threadManager = Cc["@mozilla.org/thread-manager;1"].getService(
+ Ci.nsIThreadManager
+ );
+ const currentThread = threadManager.currentThread;
+
+ let resolverInfo =
+ trrServer == "" ? null : dns.newTRRResolverInfo(trrServer);
+ try {
+ this.request = dns.asyncResolve(
+ name,
+ Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT,
+ this.options.flags || 0,
+ resolverInfo,
+ this,
+ currentThread,
+ {} // defaultOriginAttributes
+ );
+ Assert.ok(!options.expectEarlyFail);
+ } catch (e) {
+ Assert.ok(options.expectEarlyFail);
+ this.resolve([e]);
+ }
+ }
+
+ onLookupComplete(inRequest, inRecord, inStatus) {
+ Assert.ok(
+ inRequest == this.request,
+ "Checking that this is the correct callback"
+ );
+
+ // If we don't expect success here, just resolve and the caller will
+ // decide what to do with the results.
+ if (!this.expectedSuccess) {
+ this.resolve([inRequest, inRecord, inStatus]);
+ return;
+ }
+
+ Assert.equal(inStatus, Cr.NS_OK, "Checking status");
+ inRecord.QueryInterface(Ci.nsIDNSAddrRecord);
+ let answer = inRecord.getNextAddrAsString();
+ Assert.equal(
+ answer,
+ this.expectedAnswer,
+ `Checking result for ${this.name}`
+ );
+ inRecord.rewind(); // In case the caller also checks the addresses
+
+ if (this.delay !== undefined) {
+ Assert.greaterOrEqual(
+ inRecord.trrFetchDurationNetworkOnly,
+ this.delay,
+ `the response should take at least ${this.delay}`
+ );
+
+ Assert.greaterOrEqual(
+ inRecord.trrFetchDuration,
+ this.delay,
+ `the response should take at least ${this.delay}`
+ );
+
+ if (this.delay == 0) {
+ // The response timing should be really 0
+ Assert.equal(
+ inRecord.trrFetchDurationNetworkOnly,
+ 0,
+ `the response time should be 0`
+ );
+
+ Assert.equal(
+ inRecord.trrFetchDuration,
+ this.delay,
+ `the response time should be 0`
+ );
+ }
+ }
+
+ this.resolve([inRequest, inRecord, inStatus]);
+ }
+
+ QueryInterface(aIID) {
+ if (aIID.equals(Ci.nsIDNSListener) || aIID.equals(Ci.nsISupports)) {
+ return this;
+ }
+ throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE);
+ }
+
+ // Implement then so we can await this as a promise.
+ then() {
+ return this.promise.then.apply(this.promise, arguments);
+ }
+}
+
+/// Implements a basic HTTP2 server
+class TRRServerCode {
+ static async startServer(port) {
+ const fs = require("fs");
+ const options = {
+ key: fs.readFileSync(__dirname + "/http2-cert.key"),
+ cert: fs.readFileSync(__dirname + "/http2-cert.pem"),
+ };
+
+ const url = require("url");
+ global.path_handlers = {};
+ global.handler = (req, resp) => {
+ const path = req.headers[global.http2.constants.HTTP2_HEADER_PATH];
+ let u = url.parse(req.url, true);
+ let handler = global.path_handlers[u.pathname];
+ if (handler) {
+ return handler(req, resp, u);
+ }
+
+ // Didn't find a handler for this path.
+ let response = `<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);
+ };
+
+ // key: string "name/type"
+ // value: array [answer1, answer2]
+ global.dns_query_answers = {};
+
+ global.http2 = require("http2");
+ global.server = global.http2.createSecureServer(options, global.handler);
+
+ await global.server.listen(port);
+
+ global.dnsPacket = require(`${__dirname}/../dns-packet`);
+ global.ip = require(`${__dirname}/../node-ip`);
+
+ return global.server.address().port;
+ }
+}
+
+/// This is the default handler for /dns-query
+/// It implements basic functionality for parsing the DoH packet, then
+/// queries global.dns_query_answers for available answers for the DNS query.
+function trrQueryHandler(req, resp, url) {
+ let requestBody = Buffer.from("");
+ let method = req.headers[global.http2.constants.HTTP2_HEADER_METHOD];
+ let contentLength = req.headers["content-length"];
+
+ if (method == "POST") {
+ req.on("data", chunk => {
+ requestBody = Buffer.concat([requestBody, chunk]);
+ if (requestBody.length == contentLength) {
+ return processRequest(req, resp, requestBody);
+ }
+ });
+ } else if (method == "GET") {
+ if (!url.query.dns) {
+ resp.writeHead(400);
+ resp.end("Missing dns parameter");
+ return;
+ }
+
+ requestBody = Buffer.from(url.query.dns, "base64");
+ return processRequest(req, resp, requestBody);
+ } else {
+ // unexpected method.
+ resp.writeHead(405);
+ resp.end("Unexpected method");
+ }
+
+ function processRequest(req, resp, payload) {
+ let dnsQuery = global.dnsPacket.decode(payload);
+ let response =
+ global.dns_query_answers[
+ `${dnsQuery.questions[0].name}/${dnsQuery.questions[0].type}`
+ ] || {};
+
+ let flags = global.dnsPacket.RECURSION_DESIRED;
+ if (
+ (!response.answers || !response.answers.length) &&
+ response.additionals &&
+ response.additionals.length > 0
+ ) {
+ flags |= global.dnsPacket.rcodes.toRcode("SERVFAIL");
+ }
+ let buf = global.dnsPacket.encode({
+ type: "response",
+ id: dnsQuery.id,
+ flags,
+ questions: dnsQuery.questions,
+ answers: response.answers || [],
+ additionals: response.additionals || [],
+ });
+
+ let writeResponse = (resp, buf) => {
+ resp.setHeader("Content-Length", buf.length);
+ resp.writeHead(200, { "Content-Type": "application/dns-message" });
+ resp.write(buf);
+ resp.end("");
+ };
+
+ if (response.delay) {
+ setTimeout(
+ arg => {
+ writeResponse(arg[0], arg[1]);
+ },
+ response.delay,
+ [resp, buf]
+ );
+ return;
+ }
+
+ writeResponse(resp, buf);
+ }
+}
+
+// A convenient wrapper around NodeServer
+class TRRServer {
+ /// Starts the server
+ /// @port - default 0
+ /// when provided, will attempt to listen on that port.
+ async start(port = 0) {
+ this.processId = await NodeServer.fork();
+
+ await this.execute(TRRServerCode);
+ this.port = await this.execute(`TRRServerCode.startServer(${port})`);
+ await this.registerPathHandler("/dns-query", trrQueryHandler);
+ }
+
+ /// Executes a command in the context of the node server
+ async execute(command) {
+ return NodeServer.execute(this.processId, command);
+ }
+
+ /// Stops the server
+ async stop() {
+ if (this.processId) {
+ await NodeServer.kill(this.processId);
+ this.processId = undefined;
+ }
+ }
+
+ /// @path : string - the path on the server that we're handling. ex: /path
+ /// @handler : function(req, resp, url) - function that processes request and
+ /// emits a response.
+ async registerPathHandler(path, handler) {
+ return this.execute(
+ `global.path_handlers["${path}"] = ${handler.toString()}`
+ );
+ }
+
+ /// @name : string - name we're providing answers for. eg: foo.example.com
+ /// @type : string - the DNS query type. eg: "A", "AAAA", "CNAME", etc
+ /// @answers : array - array of answers (hashmap) that dnsPacket can parse
+ /// eg: [{
+ /// name: "bar.example.com",
+ /// ttl: 55,
+ /// type: "A",
+ /// flush: false,
+ /// data: "1.2.3.4",
+ /// }]
+ async registerDoHAnswers(name, type, answers, additionals, delay = 0) {
+ let text = `global.dns_query_answers["${name}/${type}"] = ${JSON.stringify({
+ answers,
+ additionals,
+ delay,
+ })}`;
+ return this.execute(text);
+ }
+}
diff --git a/netwerk/test/unit/http2-ca.pem b/netwerk/test/unit/http2-ca.pem
new file mode 100644
index 0000000000..ef5a801720
--- /dev/null
+++ b/netwerk/test/unit/http2-ca.pem
@@ -0,0 +1,18 @@
+-----BEGIN CERTIFICATE-----
+MIIC1DCCAbygAwIBAgIURZvN7yVqFNwThGHASoy1OlOGvOMwDQYJKoZIhvcNAQEL
+BQAwGTEXMBUGA1UEAwwOIEhUVFAyIFRlc3QgQ0EwIhgPMjAxNzAxMDEwMDAwMDBa
+GA8yMDI3MDEwMTAwMDAwMFowGTEXMBUGA1UEAwwOIEhUVFAyIFRlc3QgQ0EwggEi
+MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC6iFGoRI4W1kH9braIBjYQPTwT
+2erkNUq07PVoV2wke8HHJajg2B+9sZwGm24ahvJr4q9adWtqZHEIeqVap0WH9xzV
+JJwCfs1D/B5p0DggKZOrIMNJ5Nu5TMJrbA7tFYIP8X6taRqx0wI6iypB7qdw4A8N
+jf1mCyuwJJKkfbmIYXmQsVeQPdI7xeC4SB+oN9OIQ+8nFthVt2Zaqn4CkC86exCA
+BiTMHGyXrZZhW7filhLAdTGjDJHdtMr3/K0dJdMJ77kXDqdo4bN7LyJvaeO0ipVh
+He4m1iWdq5EITjbLHCQELL8Wiy/l8Y+ZFzG4s/5JI/pyUcQx1QOs2hgKNe2NAgMB
+AAGjEDAOMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBADyDiQnKjsvR
+NrOk0aqgJ8XgK/IgJXFLbAVivjBLwnJGEkwxrFtC14mpTrPuXw9AybhroMjinq4Y
+cNYTFuTE34k0fZEU8d60J/Tpfd1i0EB8+oUPuqOn+N29/LeHPAnkDJdOZye3w0U+
+StAI79WqUYQaKIG7qLnt60dQwBte12uvbuPaB3mREIfDXOKcjLBdZHL1waWjtzUX
+z2E91VIdpvJGfEfXC3fIe1uO9Jh/E9NVWci84+njkNsl+OyBfOJ8T+pV3SHfWedp
+Zbjwh6UTukIuc3mW0rS/qZOa2w3HQaO53BMbluo0w1+cscOepsATld2HHvSiHB+0
+K8SWFRHdBOU=
+-----END CERTIFICATE-----
diff --git a/netwerk/test/unit/http2-ca.pem.certspec b/netwerk/test/unit/http2-ca.pem.certspec
new file mode 100644
index 0000000000..46f62e3fbc
--- /dev/null
+++ b/netwerk/test/unit/http2-ca.pem.certspec
@@ -0,0 +1,4 @@
+issuer: HTTP2 Test CA
+subject: HTTP2 Test CA
+validity:20170101-20270101
+extension:basicConstraints:cA,
diff --git a/netwerk/test/unit/moz.build b/netwerk/test/unit/moz.build
new file mode 100644
index 0000000000..2e6708eaf6
--- /dev/null
+++ b/netwerk/test/unit/moz.build
@@ -0,0 +1,8 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+# Temporarily disabled. See bug 1256495.
+# GeneratedTestCertificate('http2-ca.pem')
diff --git a/netwerk/test/unit/perftest.ini b/netwerk/test/unit/perftest.ini
new file mode 100644
index 0000000000..789d6ae3e9
--- /dev/null
+++ b/netwerk/test/unit/perftest.ini
@@ -0,0 +1 @@
+[test_http3_perf.js]
diff --git a/netwerk/test/unit/socks_client_subprocess.js b/netwerk/test/unit/socks_client_subprocess.js
new file mode 100644
index 0000000000..1ae0e31490
--- /dev/null
+++ b/netwerk/test/unit/socks_client_subprocess.js
@@ -0,0 +1,101 @@
+/* global arguments */
+
+"use strict";
+
+var CC = Components.Constructor;
+
+const BinaryInputStream = CC(
+ "@mozilla.org/binaryinputstream;1",
+ "nsIBinaryInputStream",
+ "setInputStream"
+);
+const ProtocolProxyService = CC(
+ "@mozilla.org/network/protocol-proxy-service;1",
+ "nsIProtocolProxyService"
+);
+var sts = Cc["@mozilla.org/network/socket-transport-service;1"].getService(
+ Ci.nsISocketTransportService
+);
+
+function waitForStream(stream, streamType) {
+ return new Promise((resolve, reject) => {
+ stream = stream.QueryInterface(streamType);
+ if (!stream) {
+ reject("stream didn't implement given stream type");
+ }
+ let currentThread = Cc["@mozilla.org/thread-manager;1"].getService()
+ .currentThread;
+ stream.asyncWait(
+ stream => {
+ resolve(stream);
+ },
+ 0,
+ 0,
+ currentThread
+ );
+ });
+}
+
+async function launchConnection(
+ socks_vers,
+ socks_port,
+ dest_host,
+ dest_port,
+ dns
+) {
+ let pi_flags = 0;
+ if (dns == "remote") {
+ pi_flags = Ci.nsIProxyInfo.TRANSPARENT_PROXY_RESOLVES_HOST;
+ }
+
+ let pps = new ProtocolProxyService();
+ let pi = pps.newProxyInfo(
+ socks_vers,
+ "localhost",
+ socks_port,
+ "",
+ "",
+ pi_flags,
+ -1,
+ null
+ );
+ let trans = sts.createTransport([], dest_host, dest_port, pi);
+ let input = trans.openInputStream(0, 0, 0);
+ let output = trans.openOutputStream(0, 0, 0);
+ input = await waitForStream(input, Ci.nsIAsyncInputStream);
+ let bin = new BinaryInputStream(input);
+ let data = bin.readBytes(5);
+ let response;
+ if (data == "PING!") {
+ print("client: got ping, sending pong.");
+ response = "PONG!";
+ } else {
+ print("client: wrong data from server:", data);
+ response = "Error: wrong data received.";
+ }
+ output = await waitForStream(output, Ci.nsIAsyncOutputStream);
+ output.write(response, response.length);
+ output.close();
+ input.close();
+}
+
+async function run(args) {
+ for (let arg of args) {
+ print("client: running test", arg);
+ let test = arg.split("|");
+ await launchConnection(
+ test[0],
+ parseInt(test[1]),
+ test[2],
+ parseInt(test[3]),
+ test[4]
+ );
+ }
+}
+
+var satisfied = false;
+run(arguments).then(() => (satisfied = true));
+var mainThread = Cc["@mozilla.org/thread-manager;1"].getService().mainThread;
+while (!satisfied) {
+ mainThread.processNextEvent(true);
+}
diff --git a/netwerk/test/unit/test_1073747.js b/netwerk/test/unit/test_1073747.js
new file mode 100644
index 0000000000..b4df481330
--- /dev/null
+++ b/netwerk/test/unit/test_1073747.js
@@ -0,0 +1,42 @@
+// Test based on submitted one from Peter B Shalimoff
+
+"use strict";
+
+var test = function(s, funcName) {
+ function Arg() {}
+ Arg.prototype.toString = function() {
+ info("Testing " + funcName + " with null args");
+ return this.value;
+ };
+ // create a generic arg lits of null, -1, and 10 nulls
+ var args = [s, -1];
+ for (var i = 0; i < 10; ++i) {
+ args.push(new Arg());
+ }
+ var up = Cc["@mozilla.org/network/url-parser;1?auth=maybe"].getService(
+ Ci.nsIURLParser
+ );
+ try {
+ up[funcName].apply(up, args);
+ return args;
+ } catch (x) {
+ Assert.ok(true); // make sure it throws an exception instead of crashing
+ return x;
+ }
+};
+var s = null;
+var funcs = [
+ "parseAuthority",
+ "parseFileName",
+ "parseFilePath",
+ "parsePath",
+ "parseServerInfo",
+ "parseURL",
+ "parseUserInfo",
+];
+
+function run_test() {
+ funcs.forEach(function(f) {
+ test(s, f);
+ });
+}
diff --git a/netwerk/test/unit/test_304_responses.js b/netwerk/test/unit/test_304_responses.js
new file mode 100644
index 0000000000..fee8a23fd5
--- /dev/null
+++ b/netwerk/test/unit/test_304_responses.js
@@ -0,0 +1,106 @@
+"use strict";
+// https://bugzilla.mozilla.org/show_bug.cgi?id=761228
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+XPCOMUtils.defineLazyGetter(this, "URL", function() {
+ return "http://localhost:" + httpServer.identity.primaryPort;
+});
+
+var httpServer = null;
+const testFileName = "test_customConditionalRequest_304";
+const basePath = "/" + testFileName + "/";
+
+XPCOMUtils.defineLazyGetter(this, "baseURI", function() {
+ return URL + basePath;
+});
+
+const unexpected304 = "unexpected304";
+const existingCached304 = "existingCached304";
+
+function make_uri(url) {
+ var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
+ return ios.newURI(url);
+}
+
+function make_channel(url) {
+ return NetUtil.newChannel({
+ uri: url,
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+}
+
+function clearCache() {
+ var service = Cc["@mozilla.org/netwerk/cache-storage-service;1"].getService(
+ Ci.nsICacheStorageService
+ );
+ service.clear();
+}
+
+function alwaysReturn304Handler(metadata, response) {
+ response.setStatusLine(metadata.httpVersion, 304, "Not Modified");
+ response.setHeader("Returned-From-Handler", "1");
+}
+
+function run_test() {
+ evict_cache_entries();
+
+ httpServer = new HttpServer();
+ httpServer.registerPathHandler(
+ basePath + unexpected304,
+ alwaysReturn304Handler
+ );
+ httpServer.registerPathHandler(
+ basePath + existingCached304,
+ alwaysReturn304Handler
+ );
+ httpServer.start(-1);
+ run_next_test();
+}
+
+function finish_test(request, buffer) {
+ httpServer.stop(do_test_finished);
+}
+
+function consume304(request, buffer) {
+ request.QueryInterface(Ci.nsIHttpChannel);
+ Assert.equal(request.responseStatus, 304);
+ Assert.equal(request.getResponseHeader("Returned-From-Handler"), "1");
+ run_next_test();
+}
+
+// Test that we return a 304 response to the caller when we are not expecting
+// a 304 response (i.e. when the server shouldn't have sent us one).
+add_test(function test_unexpected_304() {
+ var chan = make_channel(baseURI + unexpected304);
+ chan.asyncOpen(new ChannelListener(consume304, null));
+});
+
+// Test that we can cope with a 304 response that was (erroneously) stored in
+// the cache.
+add_test(function test_304_stored_in_cache() {
+ asyncOpenCacheEntry(
+ baseURI + existingCached304,
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ function(entryStatus, cacheEntry) {
+ cacheEntry.setMetaDataElement("request-method", "GET");
+ cacheEntry.setMetaDataElement(
+ "response-head",
+ // eslint-disable-next-line no-useless-concat
+ "HTTP/1.1 304 Not Modified\r\n" + "\r\n"
+ );
+ cacheEntry.metaDataReady();
+ cacheEntry.close();
+
+ var chan = make_channel(baseURI + existingCached304);
+
+ // make it a custom conditional request
+ chan.QueryInterface(Ci.nsIHttpChannel);
+ chan.setRequestHeader("If-None-Match", '"foo"', false);
+
+ chan.asyncOpen(new ChannelListener(consume304, null));
+ }
+ );
+});
diff --git a/netwerk/test/unit/test_307_redirect.js b/netwerk/test/unit/test_307_redirect.js
new file mode 100644
index 0000000000..84295a071f
--- /dev/null
+++ b/netwerk/test/unit/test_307_redirect.js
@@ -0,0 +1,96 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+XPCOMUtils.defineLazyGetter(this, "URL", function() {
+ return "http://localhost:" + httpserver.identity.primaryPort;
+});
+
+XPCOMUtils.defineLazyGetter(this, "uri", function() {
+ return URL + "/redirect";
+});
+
+XPCOMUtils.defineLazyGetter(this, "noRedirectURI", function() {
+ return URL + "/content";
+});
+
+var httpserver = null;
+
+function make_channel(url) {
+ return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true });
+}
+
+const requestBody = "request body";
+
+function redirectHandler(metadata, response) {
+ response.setStatusLine(metadata.httpVersion, 307, "Moved Temporarily");
+ response.setHeader("Location", noRedirectURI, false);
+}
+
+function contentHandler(metadata, response) {
+ response.setHeader("Content-Type", "text/plain");
+ response.bodyOutputStream.writeFrom(
+ metadata.bodyInputStream,
+ metadata.bodyInputStream.available()
+ );
+}
+
+function noRedirectStreamObserver(request, buffer) {
+ Assert.equal(buffer, requestBody);
+ var chan = make_channel(uri);
+ var uploadStream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(
+ Ci.nsIStringInputStream
+ );
+ uploadStream.setData(requestBody, requestBody.length);
+ chan
+ .QueryInterface(Ci.nsIUploadChannel)
+ .setUploadStream(uploadStream, "text/plain", -1);
+ chan.asyncOpen(new ChannelListener(noHeaderStreamObserver, null));
+}
+
+function noHeaderStreamObserver(request, buffer) {
+ Assert.equal(buffer, requestBody);
+ var chan = make_channel(uri);
+ var uploadStream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(
+ Ci.nsIStringInputStream
+ );
+ var streamBody =
+ "Content-Type: text/plain\r\n" +
+ "Content-Length: " +
+ requestBody.length +
+ "\r\n\r\n" +
+ requestBody;
+ uploadStream.setData(streamBody, streamBody.length);
+ chan
+ .QueryInterface(Ci.nsIUploadChannel)
+ .setUploadStream(uploadStream, "", -1);
+ chan.asyncOpen(new ChannelListener(headerStreamObserver, null));
+}
+
+function headerStreamObserver(request, buffer) {
+ Assert.equal(buffer, requestBody);
+ httpserver.stop(do_test_finished);
+}
+
+function run_test() {
+ httpserver = new HttpServer();
+ httpserver.registerPathHandler("/redirect", redirectHandler);
+ httpserver.registerPathHandler("/content", contentHandler);
+ httpserver.start(-1);
+
+ var prefs = Cc["@mozilla.org/preferences-service;1"].getService(
+ Ci.nsIPrefBranch
+ );
+ prefs.setBoolPref("network.http.prompt-temp-redirect", false);
+
+ var chan = make_channel(noRedirectURI);
+ var uploadStream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(
+ Ci.nsIStringInputStream
+ );
+ uploadStream.setData(requestBody, requestBody.length);
+ chan
+ .QueryInterface(Ci.nsIUploadChannel)
+ .setUploadStream(uploadStream, "text/plain", -1);
+ chan.asyncOpen(new ChannelListener(noRedirectStreamObserver, null));
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_421.js b/netwerk/test/unit/test_421.js
new file mode 100644
index 0000000000..9ee0e1cb04
--- /dev/null
+++ b/netwerk/test/unit/test_421.js
@@ -0,0 +1,64 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+XPCOMUtils.defineLazyGetter(this, "URL", function() {
+ return "http://localhost:" + httpserver.identity.primaryPort;
+});
+
+var httpserver = new HttpServer();
+var testpath = "/421";
+var httpbody = "0123456789";
+var channel;
+var ios;
+
+function run_test() {
+ setup_test();
+ do_test_pending();
+}
+
+function setup_test() {
+ httpserver.registerPathHandler(testpath, serverHandler);
+ httpserver.start(-1);
+
+ channel = setupChannel(testpath);
+
+ channel.asyncOpen(new ChannelListener(checkRequestResponse, channel));
+}
+
+function setupChannel(path) {
+ var chan = NetUtil.newChannel({
+ uri: URL + path,
+ loadUsingSystemPrincipal: true,
+ });
+ chan.QueryInterface(Ci.nsIHttpChannel);
+ chan.requestMethod = "GET";
+ return chan;
+}
+
+var iters = 0;
+
+function serverHandler(metadata, response) {
+ response.setHeader("Content-Type", "text/plain", false);
+
+ if (!iters) {
+ response.setStatusLine("1.1", 421, "Not Authoritative " + iters);
+ } else {
+ response.setStatusLine("1.1", 200, "OK");
+ }
+ ++iters;
+
+ response.bodyOutputStream.write(httpbody, httpbody.length);
+}
+
+function checkRequestResponse(request, data, context) {
+ Assert.equal(channel.responseStatus, 200);
+ Assert.equal(channel.responseStatusText, "OK");
+ Assert.ok(channel.requestSucceeded);
+
+ Assert.equal(channel.contentType, "text/plain");
+ Assert.equal(channel.contentLength, httpbody.length);
+ Assert.equal(data, httpbody);
+
+ httpserver.stop(do_test_finished);
+}
diff --git a/netwerk/test/unit/test_MIME_params.js b/netwerk/test/unit/test_MIME_params.js
new file mode 100644
index 0000000000..786d4fc48f
--- /dev/null
+++ b/netwerk/test/unit/test_MIME_params.js
@@ -0,0 +1,784 @@
+/**
+ * Tests for parsing header fields using the syntax used in
+ * Content-Disposition and Content-Type
+ *
+ * See also https://bugzilla.mozilla.org/show_bug.cgi?id=609667
+ */
+
+"use strict";
+
+var BS = "\\";
+var DQUOTE = '"';
+
+// Test array:
+// - element 0: "Content-Disposition" header to test
+// under MIME (email):
+// - element 1: correct value returned for disposition-type (empty param name)
+// - element 2: correct value for filename returned
+// under HTTP:
+// (currently supports continuations; expected results without continuations
+// are commented out for now)
+// - element 3: correct value returned for disposition-type (empty param name)
+// - element 4: correct value for filename returned
+//
+// 3 and 4 may be left out if they are identical
+
+var tests = [
+ // No filename parameter: return nothing
+ ["attachment;", "attachment", Cr.NS_ERROR_INVALID_ARG],
+
+ // basic
+ ["attachment; filename=basic", "attachment", "basic"],
+
+ // extended
+ ["attachment; filename*=UTF-8''extended", "attachment", "extended"],
+
+ // prefer extended to basic (bug 588781)
+ [
+ "attachment; filename=basic; filename*=UTF-8''extended",
+ "attachment",
+ "extended",
+ ],
+
+ // prefer extended to basic (bug 588781)
+ [
+ "attachment; filename*=UTF-8''extended; filename=basic",
+ "attachment",
+ "extended",
+ ],
+
+ // use first basic value (invalid; error recovery)
+ ["attachment; filename=first; filename=wrong", "attachment", "first"],
+
+ // old school bad HTTP servers: missing 'attachment' or 'inline'
+ // (invalid; error recovery)
+ ["filename=old", "filename=old", "old"],
+
+ ["attachment; filename*=UTF-8''extended", "attachment", "extended"],
+
+ // continuations not part of RFC 5987 (bug 610054)
+ [
+ "attachment; filename*0=foo; filename*1=bar",
+ "attachment",
+ "foobar",
+ /* "attachment", Cr.NS_ERROR_INVALID_ARG */
+ ],
+
+ // Return first continuation (invalid; error recovery)
+ [
+ "attachment; filename*0=first; filename*0=wrong; filename=basic",
+ "attachment",
+ "first",
+ /* "attachment", "basic" */
+ ],
+
+ // Only use correctly ordered continuations (invalid; error recovery)
+ [
+ "attachment; filename*0=first; filename*1=second; filename*0=wrong",
+ "attachment",
+ "firstsecond",
+ /* "attachment", Cr.NS_ERROR_INVALID_ARG */
+ ],
+
+ // prefer continuation to basic (unless RFC 5987)
+ [
+ "attachment; filename=basic; filename*0=foo; filename*1=bar",
+ "attachment",
+ "foobar",
+ /* "attachment", "basic" */
+ ],
+
+ // Prefer extended to basic and/or (broken or not) continuation
+ // (invalid; error recovery)
+ [
+ "attachment; filename=basic; filename*0=first; filename*0=wrong; filename*=UTF-8''extended",
+ "attachment",
+ "extended",
+ ],
+
+ // RFC 2231 not clear on correct outcome: we prefer non-continued extended
+ // (invalid; error recovery)
+ [
+ "attachment; filename=basic; filename*=UTF-8''extended; filename*0=foo; filename*1=bar",
+ "attachment",
+ "extended",
+ ],
+
+ // Gaps should result in returning only value until gap hit
+ // (invalid; error recovery)
+ [
+ "attachment; filename*0=foo; filename*2=bar",
+ "attachment",
+ "foo",
+ /* "attachment", Cr.NS_ERROR_INVALID_ARG */
+ ],
+
+ // Don't allow leading 0's (*01) (invalid; error recovery)
+ [
+ "attachment; filename*0=foo; filename*01=bar",
+ "attachment",
+ "foo",
+ /* "attachment", Cr.NS_ERROR_INVALID_ARG */
+ ],
+
+ // continuations should prevail over non-extended (unless RFC 5987)
+ [
+ "attachment; filename=basic; filename*0*=UTF-8''multi;\r\n" +
+ " filename*1=line;\r\n" +
+ " filename*2*=%20extended",
+ "attachment",
+ "multiline extended",
+ /* "attachment", "basic" */
+ ],
+
+ // Gaps should result in returning only value until gap hit
+ // (invalid; error recovery)
+ [
+ "attachment; filename=basic; filename*0*=UTF-8''multi;\r\n" +
+ " filename*1=line;\r\n" +
+ " filename*3*=%20extended",
+ "attachment",
+ "multiline",
+ /* "attachment", "basic" */
+ ],
+
+ // First series, only please, and don't slurp up higher elements (*2 in this
+ // case) from later series into earlier one (invalid; error recovery)
+ [
+ "attachment; filename=basic; filename*0*=UTF-8''multi;\r\n" +
+ " filename*1=line;\r\n" +
+ " filename*0*=UTF-8''wrong;\r\n" +
+ " filename*1=bad;\r\n" +
+ " filename*2=evil",
+ "attachment",
+ "multiline",
+ /* "attachment", "basic" */
+ ],
+
+ // RFC 2231 not clear on correct outcome: we prefer non-continued extended
+ // (invalid; error recovery)
+ [
+ "attachment; filename=basic; filename*0=UTF-8''multi\r\n;" +
+ " filename*=UTF-8''extended;\r\n" +
+ " filename*1=line;\r\n" +
+ " filename*2*=%20extended",
+ "attachment",
+ "extended",
+ ],
+
+ // sneaky: if unescaped, make sure we leave UTF-8'' in value
+ [
+ "attachment; filename*0=UTF-8''unescaped;\r\n" +
+ " filename*1*=%20so%20includes%20UTF-8''%20in%20value",
+ "attachment",
+ "UTF-8''unescaped so includes UTF-8'' in value",
+ /* "attachment", Cr.NS_ERROR_INVALID_ARG */
+ ],
+
+ // sneaky: if unescaped, make sure we leave UTF-8'' in value
+ [
+ "attachment; filename=basic; filename*0=UTF-8''unescaped;\r\n" +
+ " filename*1*=%20so%20includes%20UTF-8''%20in%20value",
+ "attachment",
+ "UTF-8''unescaped so includes UTF-8'' in value",
+ /* "attachment", "basic" */
+ ],
+
+ // Prefer basic over invalid continuation
+ // (invalid; error recovery)
+ [
+ "attachment; filename=basic; filename*1=multi;\r\n" +
+ " filename*2=line;\r\n" +
+ " filename*3*=%20extended",
+ "attachment",
+ "basic",
+ ],
+
+ // support digits over 10
+ [
+ "attachment; filename=basic; filename*0*=UTF-8''0;\r\n" +
+ " filename*1=1; filename*2=2;filename*3=3;filename*4=4;filename*5=5;\r\n" +
+ " filename*6=6; filename*7=7;filename*8=8;filename*9=9;filename*10=a;\r\n" +
+ " filename*11=b; filename*12=c;filename*13=d;filename*14=e;filename*15=f\r\n",
+ "attachment",
+ "0123456789abcdef",
+ /* "attachment", "basic" */
+ ],
+
+ // support digits over 10 (detect gaps)
+ [
+ "attachment; filename=basic; filename*0*=UTF-8''0;\r\n" +
+ " filename*1=1; filename*2=2;filename*3=3;filename*4=4;filename*5=5;\r\n" +
+ " filename*6=6; filename*7=7;filename*8=8;filename*9=9;filename*10=a;\r\n" +
+ " filename*11=b; filename*12=c;filename*14=e\r\n",
+ "attachment",
+ "0123456789abc",
+ /* "attachment", "basic" */
+ ],
+
+ // return nothing: invalid
+ // (invalid; error recovery)
+ [
+ "attachment; filename*1=multi;\r\n" +
+ " filename*2=line;\r\n" +
+ " filename*3*=%20extended",
+ "attachment",
+ Cr.NS_ERROR_INVALID_ARG,
+ ],
+
+ // Bug 272541: Empty disposition type treated as "attachment"
+
+ // sanity check
+ [
+ "attachment; filename=foo.html",
+ "attachment",
+ "foo.html",
+ "attachment",
+ "foo.html",
+ ],
+
+ // the actual bug
+ [
+ "; filename=foo.html",
+ Cr.NS_ERROR_FIRST_HEADER_FIELD_COMPONENT_EMPTY,
+ "foo.html",
+ Cr.NS_ERROR_FIRST_HEADER_FIELD_COMPONENT_EMPTY,
+ "foo.html",
+ ],
+
+ // regression check, but see bug 671204
+ [
+ "filename=foo.html",
+ "filename=foo.html",
+ "foo.html",
+ "filename=foo.html",
+ "foo.html",
+ ],
+
+ // Bug 384571: RFC 2231 parameters not decoded when appearing in reversed order
+
+ // check ordering
+ [
+ "attachment; filename=basic; filename*0*=UTF-8''0;\r\n" +
+ " filename*1=1; filename*2=2;filename*3=3;filename*4=4;filename*5=5;\r\n" +
+ " filename*6=6; filename*7=7;filename*8=8;filename*9=9;filename*10=a;\r\n" +
+ " filename*11=b; filename*12=c;filename*13=d;filename*15=f;filename*14=e;\r\n",
+ "attachment",
+ "0123456789abcdef",
+ /* "attachment", "basic" */
+ ],
+
+ // check non-digits in sequence numbers
+ [
+ "attachment; filename=basic; filename*0*=UTF-8''0;\r\n" +
+ " filename*1a=1\r\n",
+ "attachment",
+ "0",
+ /* "attachment", "basic" */
+ ],
+
+ // check duplicate sequence numbers
+ [
+ "attachment; filename=basic; filename*0*=UTF-8''0;\r\n" +
+ " filename*0=bad; filename*1=1;\r\n",
+ "attachment",
+ "0",
+ /* "attachment", "basic" */
+ ],
+
+ // check overflow
+ [
+ "attachment; filename=basic; filename*0*=UTF-8''0;\r\n" +
+ " filename*11111111111111111111111111111111111111111111111111111111111=1",
+ "attachment",
+ "0",
+ /* "attachment", "basic" */
+ ],
+
+ // check underflow
+ [
+ // eslint-disable-next-line no-useless-concat
+ "attachment; filename=basic; filename*0*=UTF-8''0;\r\n" + " filename*-1=1",
+ "attachment",
+ "0",
+ /* "attachment", "basic" */
+ ],
+
+ // check mixed token/quoted-string
+ [
+ 'attachment; filename=basic; filename*0="0";\r\n' +
+ " filename*1=1;\r\n" +
+ " filename*2*=%32",
+ "attachment",
+ "012",
+ /* "attachment", "basic" */
+ ],
+
+ // check empty sequence number
+ [
+ "attachment; filename=basic; filename**=UTF-8''0\r\n",
+ "attachment",
+ "basic",
+ "attachment",
+ "basic",
+ ],
+
+ // Bug 419157: ensure that a MIME parameter with no charset information
+ // fallbacks to Latin-1
+
+ [
+ "attachment;filename=IT839\x04\xB5(m8)2.pdf;",
+ "attachment",
+ "IT839\u0004\u00b5(m8)2.pdf",
+ ],
+
+ // Bug 588389: unescaping backslashes in quoted string parameters
+
+ // '\"', should be parsed as '"'
+ [
+ "attachment; filename=" + DQUOTE + (BS + DQUOTE) + DQUOTE,
+ "attachment",
+ DQUOTE,
+ ],
+
+ // 'a\"b', should be parsed as 'a"b'
+ [
+ "attachment; filename=" + DQUOTE + "a" + (BS + DQUOTE) + "b" + DQUOTE,
+ "attachment",
+ "a" + DQUOTE + "b",
+ ],
+
+ // '\x', should be parsed as 'x'
+ ["attachment; filename=" + DQUOTE + (BS + "x") + DQUOTE, "attachment", "x"],
+
+ // test empty param (quoted-string)
+ ["attachment; filename=" + DQUOTE + DQUOTE, "attachment", ""],
+
+ // test empty param
+ ["attachment; filename=", "attachment", ""],
+
+ // Bug 601933: RFC 2047 does not apply to parameters (at least in HTTP)
+ [
+ "attachment; filename==?ISO-8859-1?Q?foo-=E4.html?=",
+ "attachment",
+ "foo-\u00e4.html",
+ /* "attachment", "=?ISO-8859-1?Q?foo-=E4.html?=" */
+ ],
+
+ [
+ 'attachment; filename="=?ISO-8859-1?Q?foo-=E4.html?="',
+ "attachment",
+ "foo-\u00e4.html",
+ /* "attachment", "=?ISO-8859-1?Q?foo-=E4.html?=" */
+ ],
+
+ // format sent by GMail as of 2012-07-23 (5987 overrides 2047)
+ [
+ "attachment; filename=\"=?ISO-8859-1?Q?foo-=E4.html?=\"; filename*=UTF-8''5987",
+ "attachment",
+ "5987",
+ ],
+
+ // Bug 651185: double quotes around 2231/5987 encoded param
+ // Change reverted to backwards compat issues with various web services,
+ // such as OWA (Bug 703015), plus similar problems in Thunderbird. If this
+ // is tried again in the future, email probably needs to be special-cased.
+
+ // sanity check
+ ["attachment; filename*=utf-8''%41", "attachment", "A"],
+
+ // the actual bug
+ [
+ "attachment; filename*=" + DQUOTE + "utf-8''%41" + DQUOTE,
+ "attachment",
+ "A",
+ ],
+ // previously with the fix for 651185:
+ // "attachment", Cr.NS_ERROR_INVALID_ARG],
+
+ // Bug 670333: Content-Disposition parser does not require presence of "="
+ // in params
+
+ // sanity check
+ ["attachment; filename*=UTF-8''foo-%41.html", "attachment", "foo-A.html"],
+
+ // the actual bug
+ [
+ "attachment; filename *=UTF-8''foo-%41.html",
+ "attachment",
+ Cr.NS_ERROR_INVALID_ARG,
+ ],
+
+ // the actual bug, without 2231/5987 encoding
+ ["attachment; filename X", "attachment", Cr.NS_ERROR_INVALID_ARG],
+
+ // sanity check with WS on both sides
+ ["attachment; filename = foo-A.html", "attachment", "foo-A.html"],
+
+ // Bug 685192: in RFC2231/5987 encoding, a missing charset field should be
+ // treated as error
+
+ // the actual bug
+ ["attachment; filename*=''foo", "attachment", "foo"],
+ // previously with the fix for 692574:
+ // "attachment", Cr.NS_ERROR_INVALID_ARG],
+
+ // sanity check
+ ["attachment; filename*=a''foo", "attachment", "foo"],
+
+ // Bug 692574: RFC2231/5987 decoding should not tolerate missing single
+ // quotes
+
+ // one missing
+ ["attachment; filename*=UTF-8'foo-%41.html", "attachment", "foo-A.html"],
+ // previously with the fix for 692574:
+ // "attachment", Cr.NS_ERROR_INVALID_ARG],
+
+ // both missing
+ ["attachment; filename*=foo-%41.html", "attachment", "foo-A.html"],
+ // previously with the fix for 692574:
+ // "attachment", Cr.NS_ERROR_INVALID_ARG],
+
+ // make sure fallback works
+ [
+ "attachment; filename*=UTF-8'foo-%41.html; filename=bar.html",
+ "attachment",
+ "foo-A.html",
+ ],
+ // previously with the fix for 692574:
+ // "attachment", "bar.html"],
+
+ // Bug 693806: RFC2231/5987 encoding: charset information should be treated
+ // as authoritative
+
+ // UTF-8 labeled ISO-8859-1
+ ["attachment; filename*=ISO-8859-1''%c3%a4", "attachment", "\u00c3\u00a4"],
+
+ // UTF-8 labeled ISO-8859-1, but with octets not allowed in ISO-8859-1
+ // accepts x82, understands it as Win1252, maps it to Unicode \u20a1
+ [
+ "attachment; filename*=ISO-8859-1''%e2%82%ac",
+ "attachment",
+ "\u00e2\u201a\u00ac",
+ ],
+
+ // defective UTF-8
+ ["attachment; filename*=UTF-8''A%e4B", "attachment", Cr.NS_ERROR_INVALID_ARG],
+
+ // defective UTF-8, with fallback
+ [
+ "attachment; filename*=UTF-8''A%e4B; filename=fallback",
+ "attachment",
+ "fallback",
+ ],
+
+ // defective UTF-8 (continuations), with fallback
+ [
+ "attachment; filename*0*=UTF-8''A%e4B; filename=fallback",
+ "attachment",
+ "fallback",
+ ],
+
+ // check that charsets aren't mixed up
+ [
+ "attachment; filename*0*=ISO-8859-15''euro-sign%3d%a4; filename*=ISO-8859-1''currency-sign%3d%a4",
+ "attachment",
+ "currency-sign=\u00a4",
+ ],
+
+ // same as above, except reversed
+ [
+ "attachment; filename*=ISO-8859-1''currency-sign%3d%a4; filename*0*=ISO-8859-15''euro-sign%3d%a4",
+ "attachment",
+ "currency-sign=\u00a4",
+ ],
+
+ // Bug 704989: add workaround for broken Outlook Web App (OWA)
+ // attachment handling
+
+ ['attachment; filename*="a%20b"', "attachment", "a b"],
+
+ // Bug 717121: crash nsMIMEHeaderParamImpl::DoParameterInternal
+
+ ['attachment; filename="', "attachment", ""],
+
+ // We used to read past string if last param w/o = and ;
+ // Note: was only detected on windows PGO builds
+ ["attachment; filename=foo; trouble", "attachment", "foo"],
+
+ // Same, followed by space, hits another case
+ ["attachment; filename=foo; trouble ", "attachment", "foo"],
+
+ ["attachment", "attachment", Cr.NS_ERROR_INVALID_ARG],
+
+ // Bug 730574: quoted-string in RFC2231-continuations not handled
+
+ [
+ 'attachment; filename=basic; filename*0="foo"; filename*1="\\b\\a\\r.html"',
+ "attachment",
+ "foobar.html",
+ /* "attachment", "basic" */
+ ],
+
+ // unmatched escape char
+ [
+ 'attachment; filename=basic; filename*0="foo"; filename*1="\\b\\a\\',
+ "attachment",
+ "fooba\\",
+ /* "attachment", "basic" */
+ ],
+
+ // Bug 732369: Content-Disposition parser does not require presence of ";" between params
+ // optimally, this would not even return the disposition type "attachment"
+
+ [
+ "attachment; extension=bla filename=foo",
+ "attachment",
+ Cr.NS_ERROR_INVALID_ARG,
+ ],
+
+ // Bug 1440677 - spaces inside filenames ought to be quoted, but too many
+ // servers do the wrong thing and most browsers accept this, so we were
+ // forced to do the same for compat.
+ ["attachment; filename=foo extension=bla", "attachment", "foo extension=bla"],
+
+ ["attachment filename=foo", "attachment", Cr.NS_ERROR_INVALID_ARG],
+
+ // Bug 777687: handling of broken %escapes
+
+ ["attachment; filename*=UTF-8''f%oo; filename=bar", "attachment", "bar"],
+
+ ["attachment; filename*=UTF-8''foo%; filename=bar", "attachment", "bar"],
+
+ // Bug 783502 - xpcshell test netwerk/test/unit/test_MIME_params.js fails on AddressSanitizer
+ ['attachment; filename="\\b\\a\\', "attachment", "ba\\"],
+
+ // Bug 1412213 - do continue to parse, behind an empty parameter
+ ["attachment; ; filename=foo", "attachment", "foo"],
+
+ // Bug 1412213 - do continue to parse, behind a parameter w/o =
+ ["attachment; badparameter; filename=foo", "attachment", "foo"],
+
+ // Bug 1440677 - spaces inside filenames ought to be quoted, but too many
+ // servers do the wrong thing and most browsers accept this, so we were
+ // forced to do the same for compat.
+ ["attachment; filename=foo bar.html", "attachment", "foo bar.html"],
+ // Note: we keep the tab character, but later validation will replace with a space,
+ // as file systems do not like tab characters.
+ ["attachment; filename=foo\tbar.html", "attachment", "foo\tbar.html"],
+ // Newlines get stripped completely (in practice, http header parsing may
+ // munge these into spaces before they get to us, but we should check we deal
+ // with them either way):
+ ["attachment; filename=foo\nbar.html", "attachment", "foobar.html"],
+ ["attachment; filename=foo\r\nbar.html", "attachment", "foobar.html"],
+ ["attachment; filename=foo\rbar.html", "attachment", "foobar.html"],
+
+ // Trailing rubbish shouldn't matter:
+ ["attachment; filename=foo bar; garbage", "attachment", "foo bar"],
+ ["attachment; filename=foo bar; extension=blah", "attachment", "foo bar"],
+
+ // Check that whitespace processing can't crash.
+ ["attachment; filename = ", "attachment", ""],
+];
+
+var rfc5987paramtests = [
+ [
+ // basic test
+ "UTF-8'language'value",
+ "value",
+ "language",
+ Cr.NS_OK,
+ ],
+ [
+ // percent decoding
+ "UTF-8''1%202",
+ "1 2",
+ "",
+ Cr.NS_OK,
+ ],
+ [
+ // UTF-8
+ "UTF-8''%c2%a3%20and%20%e2%82%ac%20rates",
+ "\u00a3 and \u20ac rates",
+ "",
+ Cr.NS_OK,
+ ],
+ [
+ // missing charset
+ "''abc",
+ "",
+ "",
+ Cr.NS_ERROR_INVALID_ARG,
+ ],
+ [
+ // ISO-8859-1: unsupported
+ "ISO-8859-1''%A3%20rates",
+ "",
+ "",
+ Cr.NS_ERROR_INVALID_ARG,
+ ],
+ [
+ // unknown charset
+ "foo''abc",
+ "",
+ "",
+ Cr.NS_ERROR_INVALID_ARG,
+ ],
+ [
+ // missing component
+ "abc",
+ "",
+ "",
+ Cr.NS_ERROR_INVALID_ARG,
+ ],
+ [
+ // missing component
+ "'abc",
+ "",
+ "",
+ Cr.NS_ERROR_INVALID_ARG,
+ ],
+ [
+ // illegal chars
+ "UTF-8''a b",
+ "",
+ "",
+ Cr.NS_ERROR_INVALID_ARG,
+ ],
+ [
+ // broken % escapes
+ "UTF-8''a%zz",
+ "",
+ "",
+ Cr.NS_ERROR_INVALID_ARG,
+ ],
+ [
+ // broken % escapes
+ "UTF-8''a%b",
+ "",
+ "",
+ Cr.NS_ERROR_INVALID_ARG,
+ ],
+ [
+ // broken % escapes
+ "UTF-8''a%",
+ "",
+ "",
+ Cr.NS_ERROR_INVALID_ARG,
+ ],
+ [
+ // broken UTF-8
+ "UTF-8''%A3%20rates",
+ "",
+ "",
+ 0x8050000e /* NS_ERROR_UDEC_ILLEGALINPUT */,
+ ],
+];
+
+function do_tests(whichRFC) {
+ var mhp = Cc["@mozilla.org/network/mime-hdrparam;1"].getService(
+ Ci.nsIMIMEHeaderParam
+ );
+
+ var unused = { value: null };
+
+ for (var i = 0; i < tests.length; ++i) {
+ dump("Testing #" + i + ": " + tests[i] + "\n");
+
+ // check disposition type
+ var expectedDt =
+ tests[i].length == 3 || whichRFC == 0 ? tests[i][1] : tests[i][3];
+
+ try {
+ var result;
+
+ if (whichRFC == 0) {
+ result = mhp.getParameter(tests[i][0], "", "UTF-8", true, unused);
+ } else {
+ result = mhp.getParameterHTTP(tests[i][0], "", "UTF-8", true, unused);
+ }
+
+ Assert.equal(result, expectedDt);
+ } catch (e) {
+ // Tests can also succeed by expecting to fail with given error code
+ if (e.result) {
+ // Allow following tests to run by catching exception from do_check_eq()
+ try {
+ Assert.equal(e.result, expectedDt);
+ } catch (e) {}
+ }
+ continue;
+ }
+
+ // check filename parameter
+ var expectedFn =
+ tests[i].length == 3 || whichRFC == 0 ? tests[i][2] : tests[i][4];
+
+ try {
+ var result;
+
+ if (whichRFC == 0) {
+ result = mhp.getParameter(
+ tests[i][0],
+ "filename",
+ "UTF-8",
+ true,
+ unused
+ );
+ } else {
+ result = mhp.getParameterHTTP(
+ tests[i][0],
+ "filename",
+ "UTF-8",
+ true,
+ unused
+ );
+ }
+
+ Assert.equal(result, expectedFn);
+ } catch (e) {
+ // Tests can also succeed by expecting to fail with given error code
+ if (e.result) {
+ // Allow following tests to run by catching exception from do_check_eq()
+ try {
+ Assert.equal(e.result, expectedFn);
+ } catch (e) {}
+ }
+ continue;
+ }
+ }
+}
+
+function test_decode5987Param() {
+ var mhp = Cc["@mozilla.org/network/mime-hdrparam;1"].getService(
+ Ci.nsIMIMEHeaderParam
+ );
+
+ for (var i = 0; i < rfc5987paramtests.length; ++i) {
+ dump("Testing #" + i + ": " + rfc5987paramtests[i] + "\n");
+
+ var lang = {};
+ try {
+ var decoded = mhp.decodeRFC5987Param(rfc5987paramtests[i][0], lang);
+ if (rfc5987paramtests[i][3] == Cr.NS_OK) {
+ Assert.equal(rfc5987paramtests[i][1], decoded);
+ Assert.equal(rfc5987paramtests[i][2], lang.value);
+ } else {
+ Assert.equal(rfc5987paramtests[i][3], "instead got: " + decoded);
+ }
+ } catch (e) {
+ Assert.equal(rfc5987paramtests[i][3], e.result);
+ }
+ }
+}
+
+function run_test() {
+ // Test RFC 2231 (complete header field values)
+ do_tests(0);
+
+ // Test RFC 5987 (complete header field values)
+ do_tests(1);
+
+ // tests for RFC5987 parameter parsing
+ test_decode5987Param();
+}
diff --git a/netwerk/test/unit/test_NetUtil.js b/netwerk/test/unit/test_NetUtil.js
new file mode 100644
index 0000000000..9670b3ba19
--- /dev/null
+++ b/netwerk/test/unit/test_NetUtil.js
@@ -0,0 +1,818 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+ * vim: sw=2 ts=2 sts=2 et
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * This file tests the methods on NetUtil.jsm.
+ */
+
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+// We need the profile directory so the test harness will clean up our test
+// files.
+do_get_profile();
+
+const OUTPUT_STREAM_CONTRACT_ID = "@mozilla.org/network/file-output-stream;1";
+const SAFE_OUTPUT_STREAM_CONTRACT_ID =
+ "@mozilla.org/network/safe-file-output-stream;1";
+
+////////////////////////////////////////////////////////////////////////////////
+//// Helper Methods
+
+/**
+ * Reads the contents of a file and returns it as a string.
+ *
+ * @param aFile
+ * The file to return from.
+ * @return the contents of the file in the form of a string.
+ */
+function getFileContents(aFile) {
+ "use strict";
+
+ let fstream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(
+ Ci.nsIFileInputStream
+ );
+ fstream.init(aFile, -1, 0, 0);
+
+ let cstream = Cc["@mozilla.org/intl/converter-input-stream;1"].createInstance(
+ Ci.nsIConverterInputStream
+ );
+ cstream.init(fstream, "UTF-8", 0, 0);
+
+ let string = {};
+ cstream.readString(-1, string);
+ cstream.close();
+ return string.value;
+}
+
+/**
+ * Tests asynchronously writing a file using NetUtil.asyncCopy.
+ *
+ * @param aContractId
+ * The contract ID to use for the output stream
+ * @param aDeferOpen
+ * Whether to use DEFER_OPEN in the output stream.
+ */
+function async_write_file(aContractId, aDeferOpen) {
+ do_test_pending();
+
+ // First, we need an output file to write to.
+ let file = Cc["@mozilla.org/file/directory_service;1"]
+ .getService(Ci.nsIProperties)
+ .get("ProfD", Ci.nsIFile);
+ file.append("NetUtil-async-test-file.tmp");
+ file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o666);
+
+ // Then, we need an output stream to our output file.
+ let ostream = Cc[aContractId].createInstance(Ci.nsIFileOutputStream);
+ ostream.init(
+ file,
+ -1,
+ -1,
+ aDeferOpen ? Ci.nsIFileOutputStream.DEFER_OPEN : 0
+ );
+
+ // Finally, we need an input stream to take data from.
+ const TEST_DATA = "this is a test string";
+ let istream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(
+ Ci.nsIStringInputStream
+ );
+ istream.setData(TEST_DATA, TEST_DATA.length);
+
+ NetUtil.asyncCopy(istream, ostream, function(aResult) {
+ // Make sure the copy was successful!
+ Assert.ok(Components.isSuccessCode(aResult));
+
+ // Check the file contents.
+ Assert.equal(TEST_DATA, getFileContents(file));
+
+ // Finish the test.
+ do_test_finished();
+ run_next_test();
+ });
+}
+
+////////////////////////////////////////////////////////////////////////////////
+//// Tests
+
+// Test NetUtil.asyncCopy for all possible buffering scenarios
+function test_async_copy() {
+ // Create a data sample
+ function make_sample(text) {
+ let data = [];
+ for (let i = 0; i <= 100; ++i) {
+ data.push(text);
+ }
+ return data.join();
+ }
+
+ // Create an input buffer holding some data
+ function make_input(isBuffered, data) {
+ if (isBuffered) {
+ // String input streams are buffered
+ let istream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(
+ Ci.nsIStringInputStream
+ );
+ istream.setData(data, data.length);
+ return istream;
+ }
+
+ // File input streams are not buffered, so let's create a file
+ let file = Cc["@mozilla.org/file/directory_service;1"]
+ .getService(Ci.nsIProperties)
+ .get("ProfD", Ci.nsIFile);
+ file.append("NetUtil-asyncFetch-test-file.tmp");
+ file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o666);
+
+ let ostream = Cc[
+ "@mozilla.org/network/file-output-stream;1"
+ ].createInstance(Ci.nsIFileOutputStream);
+ ostream.init(file, -1, -1, 0);
+ ostream.write(data, data.length);
+ ostream.close();
+
+ let istream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(
+ Ci.nsIFileInputStream
+ );
+ istream.init(file, -1, 0, 0);
+
+ return istream;
+ }
+
+ // Create an output buffer holding some data
+ function make_output(isBuffered) {
+ let file = Cc["@mozilla.org/file/directory_service;1"]
+ .getService(Ci.nsIProperties)
+ .get("ProfD", Ci.nsIFile);
+ file.append("NetUtil-asyncFetch-test-file.tmp");
+ file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o666);
+
+ let ostream = Cc[
+ "@mozilla.org/network/file-output-stream;1"
+ ].createInstance(Ci.nsIFileOutputStream);
+ ostream.init(file, -1, -1, 0);
+
+ if (!isBuffered) {
+ return { file, sink: ostream };
+ }
+
+ let bstream = Cc[
+ "@mozilla.org/network/buffered-output-stream;1"
+ ].createInstance(Ci.nsIBufferedOutputStream);
+ bstream.init(ostream, 256);
+ return { file, sink: bstream };
+ }
+ (async function() {
+ do_test_pending();
+ for (let bufferedInput of [true, false]) {
+ for (let bufferedOutput of [true, false]) {
+ let text =
+ "test_async_copy with " +
+ (bufferedInput ? "buffered input" : "unbuffered input") +
+ ", " +
+ (bufferedOutput ? "buffered output" : "unbuffered output");
+ info(text);
+ let TEST_DATA = "[" + make_sample(text) + "]";
+ let source = make_input(bufferedInput, TEST_DATA);
+ let { file, sink } = make_output(bufferedOutput);
+ let result = await new Promise(resolve => {
+ NetUtil.asyncCopy(source, sink, resolve);
+ });
+
+ // Make sure the copy was successful!
+ if (!Components.isSuccessCode(result)) {
+ do_throw(new Components.Exception("asyncCopy error", result));
+ }
+
+ // Check the file contents.
+ Assert.equal(TEST_DATA, getFileContents(file));
+ }
+ }
+
+ do_test_finished();
+ run_next_test();
+ })();
+}
+
+function test_async_write_file() {
+ async_write_file(OUTPUT_STREAM_CONTRACT_ID);
+}
+
+function test_async_write_file_deferred() {
+ async_write_file(OUTPUT_STREAM_CONTRACT_ID, true);
+}
+
+function test_async_write_file_safe() {
+ async_write_file(SAFE_OUTPUT_STREAM_CONTRACT_ID);
+}
+
+function test_async_write_file_safe_deferred() {
+ async_write_file(SAFE_OUTPUT_STREAM_CONTRACT_ID, true);
+}
+
+function test_newURI_no_spec_throws() {
+ try {
+ NetUtil.newURI();
+ do_throw("should throw!");
+ } catch (e) {
+ Assert.equal(e.result, Cr.NS_ERROR_INVALID_ARG);
+ }
+
+ run_next_test();
+}
+
+function test_newURI() {
+ let ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
+
+ // Check that we get the same URI back from the IO service and the utility
+ // method.
+ const TEST_URI = "http://mozilla.org";
+ let iosURI = ios.newURI(TEST_URI);
+ let NetUtilURI = NetUtil.newURI(TEST_URI);
+ Assert.ok(iosURI.equals(NetUtilURI));
+
+ run_next_test();
+}
+
+function test_newURI_takes_nsIFile() {
+ let ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
+
+ // Create a test file that we can pass into NetUtil.newURI
+ let file = Cc["@mozilla.org/file/directory_service;1"]
+ .getService(Ci.nsIProperties)
+ .get("ProfD", Ci.nsIFile);
+ file.append("NetUtil-test-file.tmp");
+
+ // Check that we get the same URI back from the IO service and the utility
+ // method.
+ let iosURI = ios.newFileURI(file);
+ let NetUtilURI = NetUtil.newURI(file);
+ Assert.ok(iosURI.equals(NetUtilURI));
+
+ run_next_test();
+}
+
+function test_asyncFetch_no_channel() {
+ try {
+ NetUtil.asyncFetch(null, function() {});
+ do_throw("should throw!");
+ } catch (e) {
+ Assert.equal(e.result, Cr.NS_ERROR_INVALID_ARG);
+ }
+
+ run_next_test();
+}
+
+function test_asyncFetch_no_callback() {
+ try {
+ NetUtil.asyncFetch({});
+ do_throw("should throw!");
+ } catch (e) {
+ Assert.equal(e.result, Cr.NS_ERROR_INVALID_ARG);
+ }
+
+ run_next_test();
+}
+
+function test_asyncFetch_with_nsIChannel() {
+ const TEST_DATA = "this is a test string";
+
+ // Start the http server, and register our handler.
+ let server = new HttpServer();
+ server.registerPathHandler("/test", function(aRequest, aResponse) {
+ aResponse.setStatusLine(aRequest.httpVersion, 200, "OK");
+ aResponse.setHeader("Content-Type", "text/plain", false);
+ aResponse.write(TEST_DATA);
+ });
+ server.start(-1);
+
+ // Create our channel.
+ let channel = NetUtil.newChannel({
+ uri: "http://localhost:" + server.identity.primaryPort + "/test",
+ loadUsingSystemPrincipal: true,
+ });
+
+ // Open our channel asynchronously.
+ NetUtil.asyncFetch(channel, function(aInputStream, aResult) {
+ // Check that we had success.
+ Assert.ok(Components.isSuccessCode(aResult));
+
+ // Check that we got the right data.
+ Assert.equal(aInputStream.available(), TEST_DATA.length);
+ let is = Cc["@mozilla.org/scriptableinputstream;1"].createInstance(
+ Ci.nsIScriptableInputStream
+ );
+ is.init(aInputStream);
+ let result = is.read(TEST_DATA.length);
+ Assert.equal(TEST_DATA, result);
+
+ server.stop(run_next_test);
+ });
+}
+
+function test_asyncFetch_with_nsIURI() {
+ const TEST_DATA = "this is a test string";
+
+ // Start the http server, and register our handler.
+ let server = new HttpServer();
+ server.registerPathHandler("/test", function(aRequest, aResponse) {
+ aResponse.setStatusLine(aRequest.httpVersion, 200, "OK");
+ aResponse.setHeader("Content-Type", "text/plain", false);
+ aResponse.write(TEST_DATA);
+ });
+ server.start(-1);
+
+ // Create our URI.
+ let uri = NetUtil.newURI(
+ "http://localhost:" + server.identity.primaryPort + "/test"
+ );
+
+ // Open our URI asynchronously.
+ NetUtil.asyncFetch(
+ {
+ uri,
+ loadUsingSystemPrincipal: true,
+ },
+ function(aInputStream, aResult) {
+ // Check that we had success.
+ Assert.ok(Components.isSuccessCode(aResult));
+
+ // Check that we got the right data.
+ Assert.equal(aInputStream.available(), TEST_DATA.length);
+ let is = Cc["@mozilla.org/scriptableinputstream;1"].createInstance(
+ Ci.nsIScriptableInputStream
+ );
+ is.init(aInputStream);
+ let result = is.read(TEST_DATA.length);
+ Assert.equal(TEST_DATA, result);
+
+ server.stop(run_next_test);
+ },
+ null, // aLoadingNode
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ null, // aTriggeringPrincipal
+ Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ Ci.nsIContentPolicy.TYPE_OTHER
+ );
+}
+
+function test_asyncFetch_with_string() {
+ const TEST_DATA = "this is a test string";
+
+ // Start the http server, and register our handler.
+ let server = new HttpServer();
+ server.registerPathHandler("/test", function(aRequest, aResponse) {
+ aResponse.setStatusLine(aRequest.httpVersion, 200, "OK");
+ aResponse.setHeader("Content-Type", "text/plain", false);
+ aResponse.write(TEST_DATA);
+ });
+ server.start(-1);
+
+ // Open our location asynchronously.
+ NetUtil.asyncFetch(
+ {
+ uri: "http://localhost:" + server.identity.primaryPort + "/test",
+ loadUsingSystemPrincipal: true,
+ },
+ function(aInputStream, aResult) {
+ // Check that we had success.
+ Assert.ok(Components.isSuccessCode(aResult));
+
+ // Check that we got the right data.
+ Assert.equal(aInputStream.available(), TEST_DATA.length);
+ let is = Cc["@mozilla.org/scriptableinputstream;1"].createInstance(
+ Ci.nsIScriptableInputStream
+ );
+ is.init(aInputStream);
+ let result = is.read(TEST_DATA.length);
+ Assert.equal(TEST_DATA, result);
+
+ server.stop(run_next_test);
+ },
+ null, // aLoadingNode
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ null, // aTriggeringPrincipal
+ Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ Ci.nsIContentPolicy.TYPE_OTHER
+ );
+}
+
+function test_asyncFetch_with_nsIFile() {
+ const TEST_DATA = "this is a test string";
+
+ // First we need a file to read from.
+ let file = Cc["@mozilla.org/file/directory_service;1"]
+ .getService(Ci.nsIProperties)
+ .get("ProfD", Ci.nsIFile);
+ file.append("NetUtil-asyncFetch-test-file.tmp");
+ file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o666);
+
+ // Write the test data to the file.
+ let ostream = Cc["@mozilla.org/network/file-output-stream;1"].createInstance(
+ Ci.nsIFileOutputStream
+ );
+ ostream.init(file, -1, -1, 0);
+ ostream.write(TEST_DATA, TEST_DATA.length);
+
+ // Sanity check to make sure the data was written.
+ Assert.equal(TEST_DATA, getFileContents(file));
+
+ // Open our file asynchronously.
+ // Note that this causes main-tread I/O and should be avoided in production.
+ NetUtil.asyncFetch(
+ {
+ uri: NetUtil.newURI(file),
+ loadUsingSystemPrincipal: true,
+ },
+ function(aInputStream, aResult) {
+ // Check that we had success.
+ Assert.ok(Components.isSuccessCode(aResult));
+
+ // Check that we got the right data.
+ Assert.equal(aInputStream.available(), TEST_DATA.length);
+ let is = Cc["@mozilla.org/scriptableinputstream;1"].createInstance(
+ Ci.nsIScriptableInputStream
+ );
+ is.init(aInputStream);
+ let result = is.read(TEST_DATA.length);
+ Assert.equal(TEST_DATA, result);
+
+ run_next_test();
+ },
+ null, // aLoadingNode
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ null, // aTriggeringPrincipal
+ Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ Ci.nsIContentPolicy.TYPE_OTHER
+ );
+}
+
+function test_asyncFetch_with_nsIInputString() {
+ const TEST_DATA = "this is a test string";
+ let istream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(
+ Ci.nsIStringInputStream
+ );
+ istream.setData(TEST_DATA, TEST_DATA.length);
+
+ // Read the input stream asynchronously.
+ NetUtil.asyncFetch(
+ istream,
+ function(aInputStream, aResult) {
+ // Check that we had success.
+ Assert.ok(Components.isSuccessCode(aResult));
+
+ // Check that we got the right data.
+ Assert.equal(aInputStream.available(), TEST_DATA.length);
+ Assert.equal(
+ NetUtil.readInputStreamToString(aInputStream, TEST_DATA.length),
+ TEST_DATA
+ );
+
+ run_next_test();
+ },
+ null, // aLoadingNode
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ null, // aTriggeringPrincipal
+ Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ Ci.nsIContentPolicy.TYPE_OTHER
+ );
+}
+
+function test_asyncFetch_does_not_block() {
+ // Create our channel that has no data.
+ let channel = NetUtil.newChannel({
+ uri: "data:text/plain,",
+ loadUsingSystemPrincipal: true,
+ });
+
+ // Open our channel asynchronously.
+ NetUtil.asyncFetch(channel, function(aInputStream, aResult) {
+ // Check that we had success.
+ Assert.ok(Components.isSuccessCode(aResult));
+
+ // Check that reading a byte throws that the stream was closed (as opposed
+ // saying it would block).
+ let is = Cc["@mozilla.org/scriptableinputstream;1"].createInstance(
+ Ci.nsIScriptableInputStream
+ );
+ is.init(aInputStream);
+ try {
+ is.read(1);
+ do_throw("should throw!");
+ } catch (e) {
+ Assert.equal(e.result, Cr.NS_BASE_STREAM_CLOSED);
+ }
+
+ run_next_test();
+ });
+}
+
+function test_newChannel_no_specifier() {
+ try {
+ NetUtil.newChannel();
+ do_throw("should throw!");
+ } catch (e) {
+ Assert.equal(e.result, Cr.NS_ERROR_INVALID_ARG);
+ }
+
+ run_next_test();
+}
+
+function test_newChannel_with_string() {
+ const TEST_SPEC = "http://mozilla.org";
+
+ // Check that we get the same URI back from channel the IO service creates and
+ // the channel the utility method creates.
+ let ios = Services.io;
+ let iosChannel = ios.newChannel(
+ TEST_SPEC,
+ null,
+ null,
+ null, // aLoadingNode
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ null, // aTriggeringPrincipal
+ Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ Ci.nsIContentPolicy.TYPE_OTHER
+ );
+ let NetUtilChannel = NetUtil.newChannel({
+ uri: TEST_SPEC,
+ loadUsingSystemPrincipal: true,
+ });
+ Assert.ok(iosChannel.URI.equals(NetUtilChannel.URI));
+
+ run_next_test();
+}
+
+function test_newChannel_with_nsIURI() {
+ const TEST_SPEC = "http://mozilla.org";
+
+ // Check that we get the same URI back from channel the IO service creates and
+ // the channel the utility method creates.
+ let uri = NetUtil.newURI(TEST_SPEC);
+ let iosChannel = Services.io.newChannelFromURI(
+ uri,
+ null, // aLoadingNode
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ null, // aTriggeringPrincipal
+ Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ Ci.nsIContentPolicy.TYPE_OTHER
+ );
+ let NetUtilChannel = NetUtil.newChannel({
+ uri,
+ loadUsingSystemPrincipal: true,
+ });
+ Assert.ok(iosChannel.URI.equals(NetUtilChannel.URI));
+
+ run_next_test();
+}
+
+function test_newChannel_with_options() {
+ let uri = "data:text/plain,";
+
+ let iosChannel = Services.io.newChannelFromURI(
+ NetUtil.newURI(uri),
+ null, // aLoadingNode
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ null, // aTriggeringPrincipal
+ Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ Ci.nsIContentPolicy.TYPE_OTHER
+ );
+
+ function checkEqualToIOSChannel(channel) {
+ Assert.ok(iosChannel.URI.equals(channel.URI));
+ }
+
+ checkEqualToIOSChannel(
+ NetUtil.newChannel({
+ uri,
+ loadingPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+ securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER,
+ })
+ );
+
+ checkEqualToIOSChannel(
+ NetUtil.newChannel({
+ uri,
+ loadUsingSystemPrincipal: true,
+ })
+ );
+
+ run_next_test();
+}
+
+function test_newChannel_with_wrong_options() {
+ let uri = "data:text/plain,";
+ let systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal();
+
+ Assert.throws(() => {
+ NetUtil.newChannel({ uri, loadUsingSystemPrincipal: true }, null, null);
+ }, /requires a single object argument/);
+
+ Assert.throws(() => {
+ NetUtil.newChannel({ loadUsingSystemPrincipal: true });
+ }, /requires the 'uri' property/);
+
+ Assert.throws(() => {
+ NetUtil.newChannel({ uri, loadingNode: true });
+ }, /requires the 'securityFlags'/);
+
+ Assert.throws(() => {
+ NetUtil.newChannel({ uri, securityFlags: 0 });
+ }, /requires at least one of the 'loadingNode'/);
+
+ Assert.throws(() => {
+ NetUtil.newChannel({
+ uri,
+ loadingPrincipal: systemPrincipal,
+ securityFlags: 0,
+ });
+ }, /requires the 'contentPolicyType'/);
+
+ Assert.throws(() => {
+ NetUtil.newChannel({
+ uri,
+ loadUsingSystemPrincipal: systemPrincipal,
+ });
+ }, /to be 'true' or 'undefined'/);
+
+ Assert.throws(() => {
+ NetUtil.newChannel({
+ uri,
+ loadingPrincipal: systemPrincipal,
+ loadUsingSystemPrincipal: true,
+ });
+ }, /does not accept 'loadUsingSystemPrincipal'/);
+
+ run_next_test();
+}
+
+function test_readInputStreamToString() {
+ const TEST_DATA = "this is a test string\0 with an embedded null";
+ let istream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(
+ Ci.nsISupportsCString
+ );
+ istream.data = TEST_DATA;
+
+ Assert.equal(
+ NetUtil.readInputStreamToString(istream, TEST_DATA.length),
+ TEST_DATA
+ );
+
+ run_next_test();
+}
+
+function test_readInputStreamToString_no_input_stream() {
+ try {
+ NetUtil.readInputStreamToString("hi", 2);
+ do_throw("should throw!");
+ } catch (e) {
+ Assert.equal(e.result, Cr.NS_ERROR_INVALID_ARG);
+ }
+
+ run_next_test();
+}
+
+function test_readInputStreamToString_no_bytes_arg() {
+ const TEST_DATA = "this is a test string";
+ let istream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(
+ Ci.nsIStringInputStream
+ );
+ istream.setData(TEST_DATA, TEST_DATA.length);
+
+ try {
+ NetUtil.readInputStreamToString(istream);
+ do_throw("should throw!");
+ } catch (e) {
+ Assert.equal(e.result, Cr.NS_ERROR_INVALID_ARG);
+ }
+
+ run_next_test();
+}
+
+function test_readInputStreamToString_blocking_stream() {
+ let pipe = Cc["@mozilla.org/pipe;1"].createInstance(Ci.nsIPipe);
+ pipe.init(true, true, 0, 0, null);
+
+ try {
+ NetUtil.readInputStreamToString(pipe.inputStream, 10);
+ do_throw("should throw!");
+ } catch (e) {
+ Assert.equal(e.result, Cr.NS_BASE_STREAM_WOULD_BLOCK);
+ }
+ run_next_test();
+}
+
+function test_readInputStreamToString_too_many_bytes() {
+ const TEST_DATA = "this is a test string";
+ let istream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(
+ Ci.nsIStringInputStream
+ );
+ istream.setData(TEST_DATA, TEST_DATA.length);
+
+ try {
+ NetUtil.readInputStreamToString(istream, TEST_DATA.length + 10);
+ do_throw("should throw!");
+ } catch (e) {
+ Assert.equal(e.result, Cr.NS_ERROR_FAILURE);
+ }
+
+ run_next_test();
+}
+
+function test_readInputStreamToString_with_charset() {
+ const TEST_DATA = "\uff10\uff11\uff12\uff13";
+ const TEST_DATA_UTF8 = "\xef\xbc\x90\xef\xbc\x91\xef\xbc\x92\xef\xbc\x93";
+ const TEST_DATA_SJIS = "\x82\x4f\x82\x50\x82\x51\x82\x52";
+
+ let istream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(
+ Ci.nsIStringInputStream
+ );
+
+ istream.setData(TEST_DATA_UTF8, TEST_DATA_UTF8.length);
+ Assert.equal(
+ NetUtil.readInputStreamToString(istream, TEST_DATA_UTF8.length, {
+ charset: "UTF-8",
+ }),
+ TEST_DATA
+ );
+
+ istream.setData(TEST_DATA_SJIS, TEST_DATA_SJIS.length);
+ Assert.equal(
+ NetUtil.readInputStreamToString(istream, TEST_DATA_SJIS.length, {
+ charset: "Shift_JIS",
+ }),
+ TEST_DATA
+ );
+
+ run_next_test();
+}
+
+function test_readInputStreamToString_invalid_sequence() {
+ const TEST_DATA = "\ufffd\ufffd\ufffd\ufffd";
+ const TEST_DATA_UTF8 = "\xaa\xaa\xaa\xaa";
+
+ let istream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(
+ Ci.nsIStringInputStream
+ );
+
+ istream.setData(TEST_DATA_UTF8, TEST_DATA_UTF8.length);
+ try {
+ NetUtil.readInputStreamToString(istream, TEST_DATA_UTF8.length, {
+ charset: "UTF-8",
+ });
+ do_throw("should throw!");
+ } catch (e) {
+ Assert.equal(e.result, Cr.NS_ERROR_ILLEGAL_INPUT);
+ }
+
+ istream.setData(TEST_DATA_UTF8, TEST_DATA_UTF8.length);
+ Assert.equal(
+ NetUtil.readInputStreamToString(istream, TEST_DATA_UTF8.length, {
+ charset: "UTF-8",
+ replacement: Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER,
+ }),
+ TEST_DATA
+ );
+
+ run_next_test();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+//// Test Runner
+
+[
+ test_async_copy,
+ test_async_write_file,
+ test_async_write_file_deferred,
+ test_async_write_file_safe,
+ test_async_write_file_safe_deferred,
+ test_newURI_no_spec_throws,
+ test_newURI,
+ test_newURI_takes_nsIFile,
+ test_asyncFetch_no_channel,
+ test_asyncFetch_no_callback,
+ test_asyncFetch_with_nsIChannel,
+ test_asyncFetch_with_nsIURI,
+ test_asyncFetch_with_string,
+ test_asyncFetch_with_nsIFile,
+ test_asyncFetch_with_nsIInputString,
+ test_asyncFetch_does_not_block,
+ test_newChannel_no_specifier,
+ test_newChannel_with_string,
+ test_newChannel_with_nsIURI,
+ test_newChannel_with_options,
+ test_newChannel_with_wrong_options,
+ test_readInputStreamToString,
+ test_readInputStreamToString_no_input_stream,
+ test_readInputStreamToString_no_bytes_arg,
+ test_readInputStreamToString_blocking_stream,
+ test_readInputStreamToString_too_many_bytes,
+ test_readInputStreamToString_with_charset,
+ test_readInputStreamToString_invalid_sequence,
+].forEach(f => add_test(f));
+var index = 0;
diff --git a/netwerk/test/unit/test_SuperfluousAuth.js b/netwerk/test/unit/test_SuperfluousAuth.js
new file mode 100644
index 0000000000..fa50e385cd
--- /dev/null
+++ b/netwerk/test/unit/test_SuperfluousAuth.js
@@ -0,0 +1,99 @@
+/*
+
+Create two http requests with the same URL in which has a user name. We allow
+first http request to be loaded and saved in the cache, so the second request
+will be served from the cache. However, we disallow loading by returning 1
+in the prompt service. In the end, the second request will be failed.
+
+*/
+
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+const { MockRegistrar } = ChromeUtils.import(
+ "resource://testing-common/MockRegistrar.jsm"
+);
+
+var httpProtocolHandler = Cc[
+ "@mozilla.org/network/protocol;1?name=http"
+].getService(Ci.nsIHttpProtocolHandler);
+
+XPCOMUtils.defineLazyGetter(this, "URL", function() {
+ return "http://foo@localhost:" + httpServer.identity.primaryPort;
+});
+
+var httpServer = null;
+
+const gMockPromptService = {
+ firstTimeCalled: false,
+ confirmExBC() {
+ if (!this.firstTimeCalled) {
+ this.firstTimeCalled = true;
+ return 0;
+ }
+
+ return 1;
+ },
+
+ QueryInterface: ChromeUtils.generateQI(["nsIPromptService"]),
+};
+
+var gMockPromptServiceCID = MockRegistrar.register(
+ "@mozilla.org/embedcomp/prompt-service;1",
+ gMockPromptService
+);
+
+registerCleanupFunction(() => {
+ MockRegistrar.unregister(gMockPromptServiceCID);
+});
+
+function makeChan(uri) {
+ let chan = NetUtil.newChannel({
+ uri,
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+ chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI;
+ return chan;
+}
+
+const responseBody = "body";
+
+function contentHandler(metadata, response) {
+ response.setHeader("Content-Type", "text/plain");
+ response.setHeader("ETag", "Just testing");
+ response.setHeader("Cache-Control", "max-age=99999");
+ response.setHeader("Content-Length", "" + responseBody.length);
+ response.bodyOutputStream.write(responseBody, responseBody.length);
+}
+
+function run_test() {
+ do_get_profile();
+
+ Services.prefs.setBoolPref("network.http.rcwn.enabled", false);
+
+ httpServer = new HttpServer();
+ httpServer.registerPathHandler("/content", contentHandler);
+ httpServer.start(-1);
+
+ httpProtocolHandler.EnsureHSTSDataReady().then(function() {
+ var chan1 = makeChan(URL + "/content");
+ chan1.asyncOpen(new ChannelListener(firstTimeThrough, null));
+ var chan2 = makeChan(URL + "/content");
+ chan2.asyncOpen(
+ new ChannelListener(secondTimeThrough, null, CL_EXPECT_FAILURE)
+ );
+ });
+
+ do_test_pending();
+}
+
+function firstTimeThrough(request, buffer) {
+ Assert.equal(buffer, responseBody);
+ Assert.ok(gMockPromptService.firstTimeCalled, "Prompt service invoked");
+}
+
+function secondTimeThrough(request, buffer) {
+ Assert.equal(request.status, Cr.NS_ERROR_ABORT);
+ httpServer.stop(do_test_finished);
+}
diff --git a/netwerk/test/unit/test_URIs.js b/netwerk/test/unit/test_URIs.js
new file mode 100644
index 0000000000..df55ebe157
--- /dev/null
+++ b/netwerk/test/unit/test_URIs.js
@@ -0,0 +1,1042 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+var gIoService = Cc["@mozilla.org/network/io-service;1"].getService(
+ Ci.nsIIOService
+);
+
+// Run by: cd objdir; make -C netwerk/test/ xpcshell-tests
+// or: cd objdir; make SOLO_FILE="test_URIs.js" -C netwerk/test/ check-one
+
+// See also test_URIs2.js.
+
+// Relevant RFCs: 1738, 1808, 2396, 3986 (newer than the code)
+// http://greenbytes.de/tech/webdav/rfc3986.html#rfc.section.5.4
+// http://greenbytes.de/tech/tc/uris/
+
+// TEST DATA
+// ---------
+var gTests = [
+ {
+ spec: "about:blank",
+ scheme: "about",
+ prePath: "about:",
+ pathQueryRef: "blank",
+ ref: "",
+ nsIURL: false,
+ nsINestedURI: true,
+ immutable: true,
+ },
+ {
+ spec: "about:foobar",
+ scheme: "about",
+ prePath: "about:",
+ pathQueryRef: "foobar",
+ ref: "",
+ nsIURL: false,
+ nsINestedURI: false,
+ immutable: true,
+ },
+ {
+ spec: "chrome://foobar/somedir/somefile.xml",
+ scheme: "chrome",
+ prePath: "chrome://foobar",
+ pathQueryRef: "/somedir/somefile.xml",
+ ref: "",
+ nsIURL: true,
+ nsINestedURI: false,
+ immutable: true,
+ },
+ {
+ spec: "data:text/html;charset=utf-8,<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:",
+ pathQueryRef: "//mozilla.org/",
+ ref: "",
+ nsIURL: false,
+ nsINestedURI: false,
+ },
+ {
+ spec: "http://www.example.com/",
+ scheme: "http",
+ prePath: "http://www.example.com",
+ pathQueryRef: "/",
+ ref: "",
+ nsIURL: true,
+ nsINestedURI: false,
+ },
+ {
+ spec: "http://www.exa\nmple.com/",
+ scheme: "http",
+ prePath: "http://www.example.com",
+ pathQueryRef: "/",
+ ref: "",
+ nsIURL: true,
+ nsINestedURI: false,
+ },
+ {
+ spec: "http://10.32.4.239/",
+ scheme: "http",
+ prePath: "http://10.32.4.239",
+ host: "10.32.4.239",
+ pathQueryRef: "/",
+ ref: "",
+ nsIURL: true,
+ nsINestedURI: false,
+ },
+ {
+ spec: "http://[::192.9.5.5]/ipng",
+ scheme: "http",
+ prePath: "http://[::c009:505]",
+ host: "::c009:505",
+ pathQueryRef: "/ipng",
+ ref: "",
+ nsIURL: true,
+ nsINestedURI: false,
+ },
+ {
+ spec: "http://[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]:8888/index.html",
+ scheme: "http",
+ prePath: "http://[fedc:ba98:7654:3210:fedc:ba98:7654:3210]:8888",
+ host: "fedc:ba98:7654:3210:fedc:ba98:7654:3210",
+ port: 8888,
+ pathQueryRef: "/index.html",
+ ref: "",
+ nsIURL: true,
+ nsINestedURI: false,
+ },
+ {
+ spec: "http://bar:foo@www.mozilla.org:8080/pub/mozilla.org/README.html",
+ scheme: "http",
+ prePath: "http://bar:foo@www.mozilla.org:8080",
+ port: 8080,
+ username: "bar",
+ password: "foo",
+ host: "www.mozilla.org",
+ pathQueryRef: "/pub/mozilla.org/README.html",
+ ref: "",
+ nsIURL: true,
+ nsINestedURI: false,
+ },
+ {
+ spec: "jar:resource://!/",
+ scheme: "jar",
+ prePath: "jar:",
+ pathQueryRef: "resource:///!/",
+ ref: "",
+ nsIURL: true,
+ nsINestedURI: true,
+ },
+ {
+ spec: "jar:resource://gre/chrome.toolkit.jar!/",
+ scheme: "jar",
+ prePath: "jar:",
+ pathQueryRef: "resource://gre/chrome.toolkit.jar!/",
+ ref: "",
+ nsIURL: true,
+ nsINestedURI: true,
+ },
+ {
+ spec: "mailto:webmaster@mozilla.com",
+ scheme: "mailto",
+ prePath: "mailto:",
+ pathQueryRef: "webmaster@mozilla.com",
+ ref: "",
+ nsIURL: false,
+ nsINestedURI: false,
+ },
+ {
+ spec: "javascript:new Date()",
+ scheme: "javascript",
+ prePath: "javascript:",
+ pathQueryRef: "new Date()",
+ ref: "",
+ nsIURL: false,
+ nsINestedURI: false,
+ },
+ {
+ spec: "blob:123456",
+ scheme: "blob",
+ prePath: "blob:",
+ pathQueryRef: "123456",
+ ref: "",
+ nsIURL: false,
+ nsINestedURI: false,
+ immutable: true,
+ },
+ {
+ spec: "place:sort=8&maxResults=10",
+ scheme: "place",
+ prePath: "place:",
+ pathQueryRef: "sort=8&maxResults=10",
+ ref: "",
+ nsIURL: false,
+ nsINestedURI: false,
+ },
+ {
+ spec: "resource://gre/",
+ scheme: "resource",
+ prePath: "resource://gre",
+ pathQueryRef: "/",
+ ref: "",
+ nsIURL: true,
+ nsINestedURI: false,
+ },
+ {
+ spec: "resource://gre/components/",
+ scheme: "resource",
+ prePath: "resource://gre",
+ pathQueryRef: "/components/",
+ ref: "",
+ nsIURL: true,
+ nsINestedURI: false,
+ },
+
+ // Adding more? Consider adding to test_URIs2.js instead, so that neither
+ // test runs for *too* long, risking timeouts on slow platforms.
+];
+
+var gHashSuffixes = ["#", "#myRef", "#myRef?a=b", "#myRef#", "#myRef#x:yz"];
+
+// TEST HELPER FUNCTIONS
+// ---------------------
+function do_info(text, stack) {
+ if (!stack) {
+ stack = Components.stack.caller;
+ }
+
+ dump(
+ "\n" +
+ "TEST-INFO | " +
+ stack.filename +
+ " | [" +
+ stack.name +
+ " : " +
+ stack.lineNumber +
+ "] " +
+ text +
+ "\n"
+ );
+}
+
+// Checks that the URIs satisfy equals(), in both possible orderings.
+// Also checks URI.equalsExceptRef(), because equal URIs should also be equal
+// when we ignore the ref.
+//
+// The third argument is optional. If the client passes a third argument
+// (e.g. todo_check_true), we'll use that in lieu of ok.
+function do_check_uri_eq(aURI1, aURI2, aCheckTrueFunc = ok) {
+ do_info("(uri equals check: '" + aURI1.spec + "' == '" + aURI2.spec + "')");
+ aCheckTrueFunc(aURI1.equals(aURI2));
+ do_info("(uri equals check: '" + aURI2.spec + "' == '" + aURI1.spec + "')");
+ aCheckTrueFunc(aURI2.equals(aURI1));
+
+ // (Only take the extra step of testing 'equalsExceptRef' when we expect the
+ // URIs to really be equal. In 'todo' cases, the URIs may or may not be
+ // equal when refs are ignored - there's no way of knowing in general.)
+ if (aCheckTrueFunc == ok) {
+ do_check_uri_eqExceptRef(aURI1, aURI2, aCheckTrueFunc);
+ }
+}
+
+// Checks that the URIs satisfy equalsExceptRef(), in both possible orderings.
+//
+// The third argument is optional. If the client passes a third argument
+// (e.g. todo_check_true), we'll use that in lieu of ok.
+function do_check_uri_eqExceptRef(aURI1, aURI2, aCheckTrueFunc = ok) {
+ do_info(
+ "(uri equalsExceptRef check: '" + aURI1.spec + "' == '" + aURI2.spec + "')"
+ );
+ aCheckTrueFunc(aURI1.equalsExceptRef(aURI2));
+ do_info(
+ "(uri equalsExceptRef check: '" + aURI2.spec + "' == '" + aURI1.spec + "')"
+ );
+ aCheckTrueFunc(aURI2.equalsExceptRef(aURI1));
+}
+
+// Checks that the given property on aURI matches the corresponding property
+// in the test bundle (or matches some function of that corresponding property,
+// if aTestFunctor is passed in).
+function do_check_property(aTest, aURI, aPropertyName, aTestFunctor) {
+ if (aTest[aPropertyName]) {
+ var expectedVal = aTestFunctor
+ ? aTestFunctor(aTest[aPropertyName])
+ : aTest[aPropertyName];
+
+ do_info(
+ "testing " +
+ aPropertyName +
+ " of " +
+ (aTestFunctor ? "modified '" : "'") +
+ aTest.spec +
+ "' is '" +
+ expectedVal +
+ "'"
+ );
+ Assert.equal(aURI[aPropertyName], expectedVal);
+ }
+}
+
+// Test that a given URI parses correctly into its various components.
+function do_test_uri_basic(aTest) {
+ var URI;
+
+ do_info(
+ "Basic tests for " +
+ aTest.spec +
+ " relative URI: " +
+ (aTest.relativeURI === undefined ? "(none)" : aTest.relativeURI)
+ );
+
+ try {
+ URI = NetUtil.newURI(aTest.spec);
+ } catch (e) {
+ do_info("Caught error on parse of" + aTest.spec + " Error: " + e.result);
+ if (aTest.fail) {
+ Assert.equal(e.result, aTest.result);
+ return;
+ }
+ do_throw(e.result);
+ }
+
+ if (aTest.relativeURI) {
+ var relURI;
+
+ try {
+ relURI = gIoService.newURI(aTest.relativeURI, null, URI);
+ } catch (e) {
+ do_info(
+ "Caught error on Relative parse of " +
+ aTest.spec +
+ " + " +
+ aTest.relativeURI +
+ " Error: " +
+ e.result
+ );
+ if (aTest.relativeFail) {
+ Assert.equal(e.result, aTest.relativeFail);
+ return;
+ }
+ do_throw(e.result);
+ }
+ do_info(
+ "relURI.pathQueryRef = " +
+ relURI.pathQueryRef +
+ ", was " +
+ URI.pathQueryRef
+ );
+ URI = relURI;
+ do_info("URI.pathQueryRef now = " + URI.pathQueryRef);
+ }
+
+ // Sanity-check
+ do_info("testing " + aTest.spec + " equals a clone of itself");
+ do_check_uri_eq(URI, URI.mutate().finalize());
+ do_check_uri_eqExceptRef(
+ URI,
+ URI.mutate()
+ .setRef("")
+ .finalize()
+ );
+ do_info("testing " + aTest.spec + " instanceof nsIURL");
+ Assert.equal(URI instanceof Ci.nsIURL, aTest.nsIURL);
+ do_info("testing " + aTest.spec + " instanceof nsINestedURI");
+ Assert.equal(URI instanceof Ci.nsINestedURI, aTest.nsINestedURI);
+
+ do_info(
+ "testing that " +
+ aTest.spec +
+ " throws or returns false " +
+ "from equals(null)"
+ );
+ // XXXdholbert At some point it'd probably be worth making this behavior
+ // (throwing vs. returning false) consistent across URI implementations.
+ var threw = false;
+ var isEqualToNull;
+ try {
+ isEqualToNull = URI.equals(null);
+ } catch (e) {
+ threw = true;
+ }
+ Assert.ok(threw || !isEqualToNull);
+
+ // Check the various components
+ do_check_property(aTest, URI, "scheme");
+ do_check_property(aTest, URI, "prePath");
+ do_check_property(aTest, URI, "pathQueryRef");
+ do_check_property(aTest, URI, "query");
+ do_check_property(aTest, URI, "ref");
+ do_check_property(aTest, URI, "port");
+ do_check_property(aTest, URI, "username");
+ do_check_property(aTest, URI, "password");
+ do_check_property(aTest, URI, "host");
+ do_check_property(aTest, URI, "specIgnoringRef");
+ if ("hasRef" in aTest) {
+ do_info("testing hasref: " + aTest.hasRef + " vs " + URI.hasRef);
+ Assert.equal(aTest.hasRef, URI.hasRef);
+ }
+}
+
+// Test that a given URI parses correctly when we add a given ref to the end
+function do_test_uri_with_hash_suffix(aTest, aSuffix) {
+ do_info("making sure caller is using suffix that starts with '#'");
+ Assert.equal(aSuffix[0], "#");
+
+ var origURI = NetUtil.newURI(aTest.spec);
+ var testURI;
+
+ if (aTest.relativeURI) {
+ try {
+ origURI = gIoService.newURI(aTest.relativeURI, null, origURI);
+ } catch (e) {
+ do_info(
+ "Caught error on Relative parse of " +
+ aTest.spec +
+ " + " +
+ aTest.relativeURI +
+ " Error: " +
+ e.result
+ );
+ return;
+ }
+ try {
+ testURI = gIoService.newURI(aSuffix, null, origURI);
+ } catch (e) {
+ do_info(
+ "Caught error adding suffix to " +
+ aTest.spec +
+ " + " +
+ aTest.relativeURI +
+ ", suffix " +
+ aSuffix +
+ " Error: " +
+ e.result
+ );
+ return;
+ }
+ } else {
+ testURI = NetUtil.newURI(aTest.spec + aSuffix);
+ }
+
+ do_info(
+ "testing " +
+ aTest.spec +
+ " with '" +
+ aSuffix +
+ "' appended " +
+ "equals a clone of itself"
+ );
+ do_check_uri_eq(testURI, testURI.mutate().finalize());
+
+ do_info(
+ "testing " +
+ aTest.spec +
+ " doesn't equal self with '" +
+ aSuffix +
+ "' appended"
+ );
+
+ Assert.ok(!origURI.equals(testURI));
+
+ do_info(
+ "testing " +
+ aTest.spec +
+ " is equalExceptRef to self with '" +
+ aSuffix +
+ "' appended"
+ );
+ do_check_uri_eqExceptRef(origURI, testURI);
+
+ Assert.equal(testURI.hasRef, true);
+
+ if (!origURI.ref) {
+ // These tests fail if origURI has a ref
+ do_info(
+ "testing setRef('') on " +
+ testURI.spec +
+ " is equal to no-ref version but not equal to ref version"
+ );
+ var cloneNoRef = testURI
+ .mutate()
+ .setRef("")
+ .finalize(); // we used to clone here.
+ do_info("cloneNoRef: " + cloneNoRef.spec + " hasRef: " + cloneNoRef.hasRef);
+ do_info("testURI: " + testURI.spec + " hasRef: " + testURI.hasRef);
+ do_check_uri_eq(cloneNoRef, origURI);
+ Assert.ok(!cloneNoRef.equals(testURI));
+
+ do_info(
+ "testing cloneWithNewRef on " +
+ testURI.spec +
+ " with an empty ref is equal to no-ref version but not equal to ref version"
+ );
+ var cloneNewRef = testURI
+ .mutate()
+ .setRef("")
+ .finalize();
+ do_check_uri_eq(cloneNewRef, origURI);
+ do_check_uri_eq(cloneNewRef, cloneNoRef);
+ Assert.ok(!cloneNewRef.equals(testURI));
+
+ do_info(
+ "testing cloneWithNewRef on " +
+ origURI.spec +
+ " with the same new ref is equal to ref version and not equal to no-ref version"
+ );
+ cloneNewRef = origURI
+ .mutate()
+ .setRef(aSuffix)
+ .finalize();
+ do_check_uri_eq(cloneNewRef, testURI);
+ Assert.ok(cloneNewRef.equals(testURI));
+ }
+
+ do_check_property(aTest, testURI, "scheme");
+ do_check_property(aTest, testURI, "prePath");
+ if (!origURI.ref) {
+ // These don't work if it's a ref already because '+' doesn't give the right result
+ do_check_property(aTest, testURI, "pathQueryRef", function(aStr) {
+ return aStr + aSuffix;
+ });
+ do_check_property(aTest, testURI, "ref", function(aStr) {
+ return aSuffix.substr(1);
+ });
+ }
+}
+
+// Tests various ways of setting & clearing a ref on a URI.
+function do_test_mutate_ref(aTest, aSuffix) {
+ do_info("making sure caller is using suffix that starts with '#'");
+ Assert.equal(aSuffix[0], "#");
+
+ var refURIWithSuffix = NetUtil.newURI(aTest.spec + aSuffix);
+ var refURIWithoutSuffix = NetUtil.newURI(aTest.spec);
+
+ var testURI = NetUtil.newURI(aTest.spec);
+
+ // First: Try setting .ref to our suffix
+ do_info(
+ "testing that setting .ref on " +
+ aTest.spec +
+ " to '" +
+ aSuffix +
+ "' does what we expect"
+ );
+ testURI = testURI
+ .mutate()
+ .setRef(aSuffix)
+ .finalize();
+ do_check_uri_eq(testURI, refURIWithSuffix);
+ do_check_uri_eqExceptRef(testURI, refURIWithoutSuffix);
+
+ // Now try setting .ref but leave off the initial hash (expect same result)
+ var suffixLackingHash = aSuffix.substr(1);
+ if (suffixLackingHash) {
+ // (skip this our suffix was *just* a #)
+ do_info(
+ "testing that setting .ref on " +
+ aTest.spec +
+ " to '" +
+ suffixLackingHash +
+ "' does what we expect"
+ );
+ testURI = testURI
+ .mutate()
+ .setRef(suffixLackingHash)
+ .finalize();
+ do_check_uri_eq(testURI, refURIWithSuffix);
+ do_check_uri_eqExceptRef(testURI, refURIWithoutSuffix);
+ }
+
+ // Now, clear .ref (should get us back the original spec)
+ do_info(
+ "testing that clearing .ref on " + testURI.spec + " does what we expect"
+ );
+ testURI = testURI
+ .mutate()
+ .setRef("")
+ .finalize();
+ do_check_uri_eq(testURI, refURIWithoutSuffix);
+ do_check_uri_eqExceptRef(testURI, refURIWithSuffix);
+
+ if (!aTest.relativeURI) {
+ // TODO: These tests don't work as-is for relative URIs.
+
+ // Now try setting .spec directly (including suffix) and then clearing .ref
+ var specWithSuffix = aTest.spec + aSuffix;
+ do_info(
+ "testing that setting spec to " +
+ specWithSuffix +
+ " and then clearing ref does what we expect"
+ );
+
+ testURI = testURI
+ .mutate()
+ .setSpec(specWithSuffix)
+ .setRef("")
+ .finalize();
+ do_check_uri_eq(testURI, refURIWithoutSuffix);
+ do_check_uri_eqExceptRef(testURI, refURIWithSuffix);
+
+ // XXX nsIJARURI throws an exception in SetPath(), so skip it for next part.
+ if (!(testURI instanceof Ci.nsIJARURI)) {
+ // Now try setting .pathQueryRef directly (including suffix) and then clearing .ref
+ // (same as above, but with now with .pathQueryRef instead of .spec)
+ testURI = NetUtil.newURI(aTest.spec);
+
+ var pathWithSuffix = aTest.pathQueryRef + aSuffix;
+ do_info(
+ "testing that setting path to " +
+ pathWithSuffix +
+ " and then clearing ref does what we expect"
+ );
+ testURI = testURI
+ .mutate()
+ .setPathQueryRef(pathWithSuffix)
+ .setRef("")
+ .finalize();
+ do_check_uri_eq(testURI, refURIWithoutSuffix);
+ do_check_uri_eqExceptRef(testURI, refURIWithSuffix);
+
+ // Also: make sure that clearing .pathQueryRef also clears .ref
+ testURI = testURI
+ .mutate()
+ .setPathQueryRef(pathWithSuffix)
+ .finalize();
+ do_info(
+ "testing that clearing path from " +
+ pathWithSuffix +
+ " also clears .ref"
+ );
+ testURI = testURI
+ .mutate()
+ .setPathQueryRef("")
+ .finalize();
+ Assert.equal(testURI.ref, "");
+ }
+ }
+}
+
+// Check that changing nested/about URIs works correctly.
+add_task(function check_nested_mutations() {
+ // nsNestedAboutURI
+ let uri1 = gIoService.newURI("about:blank#");
+ let uri2 = gIoService.newURI("about:blank");
+ let uri3 = uri1
+ .mutate()
+ .setRef("")
+ .finalize();
+ do_check_uri_eq(uri3, uri2);
+ uri3 = uri2
+ .mutate()
+ .setRef("#")
+ .finalize();
+ do_check_uri_eq(uri3, uri1);
+
+ uri1 = gIoService.newURI("about:blank?something");
+ uri2 = gIoService.newURI("about:blank");
+ uri3 = uri1
+ .mutate()
+ .setQuery("")
+ .finalize();
+ do_check_uri_eq(uri3, uri2);
+ uri3 = uri2
+ .mutate()
+ .setQuery("something")
+ .finalize();
+ do_check_uri_eq(uri3, uri1);
+
+ uri1 = gIoService.newURI("about:blank?query#ref");
+ uri2 = gIoService.newURI("about:blank");
+ uri3 = uri1
+ .mutate()
+ .setPathQueryRef("blank")
+ .finalize();
+ do_check_uri_eq(uri3, uri2);
+ uri3 = uri2
+ .mutate()
+ .setPathQueryRef("blank?query#ref")
+ .finalize();
+ do_check_uri_eq(uri3, uri1);
+
+ // nsSimpleNestedURI
+ uri1 = gIoService.newURI("view-source:http://example.com/path#");
+ uri2 = gIoService.newURI("view-source:http://example.com/path");
+ uri3 = uri1
+ .mutate()
+ .setRef("")
+ .finalize();
+ do_check_uri_eq(uri3, uri2);
+ uri3 = uri2
+ .mutate()
+ .setRef("#")
+ .finalize();
+ do_check_uri_eq(uri3, uri1);
+
+ uri1 = gIoService.newURI("view-source:http://example.com/path?something");
+ uri2 = gIoService.newURI("view-source:http://example.com/path");
+ uri3 = uri1
+ .mutate()
+ .setQuery("")
+ .finalize();
+ do_check_uri_eq(uri3, uri2);
+ uri3 = uri2
+ .mutate()
+ .setQuery("something")
+ .finalize();
+ do_check_uri_eq(uri3, uri1);
+
+ uri1 = gIoService.newURI("view-source:http://example.com/path?query#ref");
+ uri2 = gIoService.newURI("view-source:http://example.com/path");
+ uri3 = uri1
+ .mutate()
+ .setPathQueryRef("path")
+ .finalize();
+ do_check_uri_eq(uri3, uri2);
+ uri3 = uri2
+ .mutate()
+ .setPathQueryRef("path?query#ref")
+ .finalize();
+ do_check_uri_eq(uri3, uri1);
+
+ uri1 = gIoService.newURI("view-source:about:blank#");
+ uri2 = gIoService.newURI("view-source:about:blank");
+ uri3 = uri1
+ .mutate()
+ .setRef("")
+ .finalize();
+ do_check_uri_eq(uri3, uri2);
+ uri3 = uri2
+ .mutate()
+ .setRef("#")
+ .finalize();
+ do_check_uri_eq(uri3, uri1);
+
+ uri1 = gIoService.newURI("view-source:about:blank?something");
+ uri2 = gIoService.newURI("view-source:about:blank");
+ uri3 = uri1
+ .mutate()
+ .setQuery("")
+ .finalize();
+ do_check_uri_eq(uri3, uri2);
+ uri3 = uri2
+ .mutate()
+ .setQuery("something")
+ .finalize();
+ do_check_uri_eq(uri3, uri1);
+
+ uri1 = gIoService.newURI("view-source:about:blank?query#ref");
+ uri2 = gIoService.newURI("view-source:about:blank");
+ uri3 = uri1
+ .mutate()
+ .setPathQueryRef("blank")
+ .finalize();
+ do_check_uri_eq(uri3, uri2);
+ uri3 = uri2
+ .mutate()
+ .setPathQueryRef("blank?query#ref")
+ .finalize();
+ do_check_uri_eq(uri3, uri1);
+});
+
+add_task(function check_space_escaping() {
+ let uri = gIoService.newURI("data:text/plain,hello%20world#space hash");
+ Assert.equal(uri.spec, "data:text/plain,hello%20world#space%20hash");
+ uri = gIoService.newURI("data:text/plain,hello%20world#space%20hash");
+ Assert.equal(uri.spec, "data:text/plain,hello%20world#space%20hash");
+ uri = gIoService.newURI("data:text/plain,hello world#space%20hash");
+ Assert.equal(uri.spec, "data:text/plain,hello world#space%20hash");
+ uri = gIoService.newURI("data:text/plain,hello world#space hash");
+ Assert.equal(uri.spec, "data:text/plain,hello world#space%20hash");
+ uri = gIoService.newURI("http://example.com/test path#test path");
+ uri = gIoService.newURI("http://example.com/test%20path#test%20path");
+});
+
+add_task(function check_schemeIsNull() {
+ let uri = gIoService.newURI("data:text/plain,aaa");
+ Assert.ok(!uri.schemeIs(null));
+ uri = gIoService.newURI("http://example.com");
+ Assert.ok(!uri.schemeIs(null));
+ uri = gIoService.newURI("dummyscheme://example.com");
+ Assert.ok(!uri.schemeIs(null));
+ uri = gIoService.newURI("jar:resource://gre/chrome.toolkit.jar!/");
+ Assert.ok(!uri.schemeIs(null));
+ uri = gIoService.newURI("moz-icon://.unknown?size=32");
+ Assert.ok(!uri.schemeIs(null));
+});
+
+// Check that characters in the query of moz-extension aren't improperly unescaped (Bug 1547882)
+add_task(function check_mozextension_query() {
+ let uri = gIoService.newURI(
+ "moz-extension://a7d1572e-3beb-4d93-a920-c408fa09e8ea/_source/holding.html"
+ );
+ uri = uri
+ .mutate()
+ .setQuery("u=https%3A%2F%2Fnews.ycombinator.com%2F")
+ .finalize();
+ Assert.equal(uri.query, "u=https%3A%2F%2Fnews.ycombinator.com%2F");
+ uri = gIoService.newURI(
+ "moz-extension://a7d1572e-3beb-4d93-a920-c408fa09e8ea/_source/holding.html?u=https%3A%2F%2Fnews.ycombinator.com%2F"
+ );
+ Assert.equal(
+ uri.spec,
+ "moz-extension://a7d1572e-3beb-4d93-a920-c408fa09e8ea/_source/holding.html?u=https%3A%2F%2Fnews.ycombinator.com%2F"
+ );
+ Assert.equal(uri.query, "u=https%3A%2F%2Fnews.ycombinator.com%2F");
+});
+
+add_task(function check_resolve() {
+ let base = gIoService.newURI("http://example.com");
+ let uri = gIoService.newURI("tel::+371 27028456", "utf-8", base);
+ Assert.equal(uri.spec, "tel::+371 27028456");
+});
+
+add_task(function test_extra_protocols() {
+ // dweb://
+ let url = gIoService.newURI("dweb://example.com/test");
+ Assert.equal(url.host, "example.com");
+
+ // dat://
+ url = gIoService.newURI(
+ "dat://41f8a987cfeba80a037e51cc8357d513b62514de36f2f9b3d3eeec7a8fb3b5a5/"
+ );
+ Assert.equal(
+ url.host,
+ "41f8a987cfeba80a037e51cc8357d513b62514de36f2f9b3d3eeec7a8fb3b5a5"
+ );
+ url = gIoService.newURI("dat://example.com/test");
+ Assert.equal(url.host, "example.com");
+
+ // ipfs://
+ url = gIoService.newURI(
+ "ipfs://bafybeiccfclkdtucu6y4yc5cpr6y3yuinr67svmii46v5cfcrkp47ihehy/frontend/license.txt"
+ );
+ Assert.equal(url.scheme, "ipfs");
+ Assert.equal(
+ url.host,
+ "bafybeiccfclkdtucu6y4yc5cpr6y3yuinr67svmii46v5cfcrkp47ihehy"
+ );
+ Assert.equal(url.filePath, "/frontend/license.txt");
+
+ // ipns://
+ url = gIoService.newURI("ipns://peerdium.gozala.io/index.html");
+ Assert.equal(url.scheme, "ipns");
+ Assert.equal(url.host, "peerdium.gozala.io");
+ Assert.equal(url.filePath, "/index.html");
+
+ // ssb://
+ url = gIoService.newURI("ssb://scuttlebutt.nz/index.html");
+ Assert.equal(url.scheme, "ssb");
+ Assert.equal(url.host, "scuttlebutt.nz");
+ Assert.equal(url.filePath, "/index.html");
+
+ // wtp://
+ url = gIoService.newURI(
+ "wtp://951ead31d09e4049fc1f21f137e233dd0589fcbd/blog/vim-tips/"
+ );
+ Assert.equal(url.scheme, "wtp");
+ Assert.equal(url.host, "951ead31d09e4049fc1f21f137e233dd0589fcbd");
+ Assert.equal(url.filePath, "/blog/vim-tips/");
+});
+
+// TEST MAIN FUNCTION
+// ------------------
+add_task(function mainTest() {
+ // UTF-8 check - From bug 622981
+ // ASCII
+ let base = gIoService.newURI("http://example.org/xenia?");
+ let resolved = gIoService.newURI("?x", null, base);
+ let expected = gIoService.newURI("http://example.org/xenia?x");
+ do_info(
+ "Bug 662981: ACSII - comparing " + resolved.spec + " and " + expected.spec
+ );
+ Assert.ok(resolved.equals(expected));
+
+ // UTF-8 character "è"
+ // Bug 622981 was triggered by an empty query string
+ base = gIoService.newURI("http://example.org/xènia?");
+ resolved = gIoService.newURI("?x", null, base);
+ expected = gIoService.newURI("http://example.org/xènia?x");
+ do_info(
+ "Bug 662981: UTF8 - comparing " + resolved.spec + " and " + expected.spec
+ );
+ Assert.ok(resolved.equals(expected));
+
+ gTests.forEach(function(aTest) {
+ // Check basic URI functionality
+ do_test_uri_basic(aTest);
+
+ if (!aTest.fail) {
+ // Try adding various #-prefixed strings to the ends of the URIs
+ gHashSuffixes.forEach(function(aSuffix) {
+ do_test_uri_with_hash_suffix(aTest, aSuffix);
+ if (!aTest.immutable) {
+ do_test_mutate_ref(aTest, aSuffix);
+ }
+ });
+
+ // For URIs that we couldn't mutate above due to them being immutable:
+ // Now we check that they're actually immutable.
+ if (aTest.immutable) {
+ Assert.ok(aTest.immutable);
+ }
+ }
+ });
+});
+
+function check_round_trip_serialization(spec) {
+ dump(`checking ${spec}\n`);
+ let uri = gIoService.newURI(spec);
+ let str = serialize_to_escaped_string(uri);
+ let other = deserialize_from_escaped_string(str).QueryInterface(Ci.nsIURI);
+ equal(other.spec, uri.spec);
+}
+
+add_task(function test_iconURI_serialization() {
+ // URIs taken from test_moz_icon_uri.js
+
+ let tests = [
+ "moz-icon://foo.html?contentType=bar&size=button&state=normal",
+ "moz-icon://foo.html?size=3",
+ "moz-icon://stock/foo",
+ "moz-icon:file://foo.txt",
+ "moz-icon://file://foo.txt",
+ ];
+
+ tests.forEach(str => check_round_trip_serialization(str));
+});
+
+add_task(function test_jarURI_serialization() {
+ check_round_trip_serialization("jar:http://example.com/bar.jar!/");
+});
diff --git a/netwerk/test/unit/test_URIs2.js b/netwerk/test/unit/test_URIs2.js
new file mode 100644
index 0000000000..997f63658e
--- /dev/null
+++ b/netwerk/test/unit/test_URIs2.js
@@ -0,0 +1,906 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+"use strict";
+
+var gIoService = Cc["@mozilla.org/network/io-service;1"].getService(
+ Ci.nsIIOService
+);
+
+// Run by: cd objdir; make -C netwerk/test/ xpcshell-tests
+// or: cd objdir; make SOLO_FILE="test_URIs2.js" -C netwerk/test/ check-one
+
+// This is a clone of test_URIs.js, with a different set of test data in gTests.
+// The original test data in test_URIs.js was split between test_URIs and test_URIs2.js
+// because test_URIs.js was running for too long on slow platforms, causing
+// intermittent timeouts.
+
+// Relevant RFCs: 1738, 1808, 2396, 3986 (newer than the code)
+// http://greenbytes.de/tech/webdav/rfc3986.html#rfc.section.5.4
+// http://greenbytes.de/tech/tc/uris/
+
+// TEST DATA
+// ---------
+var gTests = [
+ {
+ spec: "view-source:about:blank",
+ scheme: "view-source",
+ prePath: "view-source:",
+ pathQueryRef: "about:blank",
+ ref: "",
+ nsIURL: false,
+ nsINestedURI: true,
+ immutable: true,
+ },
+ {
+ spec: "view-source:http://www.mozilla.org/",
+ scheme: "view-source",
+ prePath: "view-source:",
+ pathQueryRef: "http://www.mozilla.org/",
+ ref: "",
+ nsIURL: false,
+ nsINestedURI: true,
+ immutable: true,
+ },
+ {
+ spec: "x-external:",
+ scheme: "x-external",
+ prePath: "x-external:",
+ pathQueryRef: "",
+ ref: "",
+ nsIURL: false,
+ nsINestedURI: false,
+ },
+ {
+ spec: "x-external:abc",
+ scheme: "x-external",
+ prePath: "x-external:",
+ pathQueryRef: "abc",
+ ref: "",
+ nsIURL: false,
+ nsINestedURI: false,
+ },
+ {
+ spec: "http://www2.example.com/",
+ relativeURI: "a/b/c/d",
+ scheme: "http",
+ prePath: "http://www2.example.com",
+ pathQueryRef: "/a/b/c/d",
+ ref: "",
+ nsIURL: true,
+ nsINestedURI: false,
+ },
+ // relative URL testcases from http://greenbytes.de/tech/webdav/rfc3986.html#rfc.section.5.4
+ {
+ spec: "http://a/b/c/d;p?q",
+ relativeURI: "g:h",
+ scheme: "g",
+ prePath: "g:",
+ pathQueryRef: "h",
+ ref: "",
+ nsIURL: false,
+ nsINestedURI: false,
+ },
+ {
+ spec: "http://a/b/c/d;p?q",
+ relativeURI: "g",
+ scheme: "http",
+ prePath: "http://a",
+ pathQueryRef: "/b/c/g",
+ ref: "",
+ nsIURL: true,
+ nsINestedURI: false,
+ },
+ {
+ spec: "http://a/b/c/d;p?q",
+ relativeURI: "./g",
+ scheme: "http",
+ prePath: "http://a",
+ pathQueryRef: "/b/c/g",
+ ref: "",
+ nsIURL: true,
+ nsINestedURI: false,
+ },
+ {
+ spec: "http://a/b/c/d;p?q",
+ relativeURI: "g/",
+ scheme: "http",
+ prePath: "http://a",
+ pathQueryRef: "/b/c/g/",
+ ref: "",
+ nsIURL: true,
+ nsINestedURI: false,
+ },
+ {
+ spec: "http://a/b/c/d;p?q",
+ relativeURI: "/g",
+ scheme: "http",
+ prePath: "http://a",
+ pathQueryRef: "/g",
+ ref: "",
+ nsIURL: true,
+ nsINestedURI: false,
+ },
+ {
+ spec: "http://a/b/c/d;p?q",
+ relativeURI: "?y",
+ scheme: "http",
+ prePath: "http://a",
+ pathQueryRef: "/b/c/d;p?y",
+ ref: "", // fix
+ nsIURL: true,
+ nsINestedURI: false,
+ },
+ {
+ spec: "http://a/b/c/d;p?q",
+ relativeURI: "g?y",
+ scheme: "http",
+ prePath: "http://a",
+ pathQueryRef: "/b/c/g?y",
+ ref: "", // fix
+ specIgnoringRef: "http://a/b/c/g?y",
+ hasRef: false,
+ nsIURL: true,
+ nsINestedURI: false,
+ },
+ {
+ spec: "http://a/b/c/d;p?q",
+ relativeURI: "#s",
+ scheme: "http",
+ prePath: "http://a",
+ pathQueryRef: "/b/c/d;p?q#s",
+ ref: "s", // fix
+ specIgnoringRef: "http://a/b/c/d;p?q",
+ hasRef: true,
+ nsIURL: true,
+ nsINestedURI: false,
+ },
+ {
+ spec: "http://a/b/c/d;p?q",
+ relativeURI: "g#s",
+ scheme: "http",
+ prePath: "http://a",
+ pathQueryRef: "/b/c/g#s",
+ ref: "s",
+ nsIURL: true,
+ nsINestedURI: false,
+ },
+ {
+ spec: "http://a/b/c/d;p?q",
+ relativeURI: "g?y#s",
+ scheme: "http",
+ prePath: "http://a",
+ pathQueryRef: "/b/c/g?y#s",
+ ref: "s",
+ nsIURL: true,
+ nsINestedURI: false,
+ },
+ /*
+ Bug xxxxxx - we return a path of b/c/;x
+ { spec: "http://a/b/c/d;p?q",
+ relativeURI: ";x",
+ scheme: "http",
+ prePath: "http://a",
+ pathQueryRef: "/b/c/d;x",
+ ref: "",
+ nsIURL: true, nsINestedURI: false },
+ */
+ {
+ spec: "http://a/b/c/d;p?q",
+ relativeURI: "g;x",
+ scheme: "http",
+ prePath: "http://a",
+ pathQueryRef: "/b/c/g;x",
+ ref: "",
+ nsIURL: true,
+ nsINestedURI: false,
+ },
+ {
+ spec: "http://a/b/c/d;p?q",
+ relativeURI: "g;x?y#s",
+ scheme: "http",
+ prePath: "http://a",
+ pathQueryRef: "/b/c/g;x?y#s",
+ ref: "s",
+ nsIURL: true,
+ nsINestedURI: false,
+ },
+ /*
+ Can't easily specify a relative URI of "" to the test code
+ { spec: "http://a/b/c/d;p?q",
+ relativeURI: "",
+ scheme: "http",
+ prePath: "http://a",
+ pathQueryRef: "/b/c/d",
+ ref: "",
+ nsIURL: true, nsINestedURI: false },
+ */
+ {
+ spec: "http://a/b/c/d;p?q",
+ relativeURI: ".",
+ scheme: "http",
+ prePath: "http://a",
+ pathQueryRef: "/b/c/",
+ ref: "",
+ nsIURL: true,
+ nsINestedURI: false,
+ },
+ {
+ spec: "http://a/b/c/d;p?q",
+ relativeURI: "./",
+ scheme: "http",
+ prePath: "http://a",
+ pathQueryRef: "/b/c/",
+ ref: "",
+ nsIURL: true,
+ nsINestedURI: false,
+ },
+ {
+ spec: "http://a/b/c/d;p?q",
+ relativeURI: "..",
+ scheme: "http",
+ prePath: "http://a",
+ pathQueryRef: "/b/",
+ ref: "",
+ nsIURL: true,
+ nsINestedURI: false,
+ },
+ {
+ spec: "http://a/b/c/d;p?q",
+ relativeURI: "../",
+ scheme: "http",
+ prePath: "http://a",
+ pathQueryRef: "/b/",
+ ref: "",
+ nsIURL: true,
+ nsINestedURI: false,
+ },
+ {
+ spec: "http://a/b/c/d;p?q",
+ relativeURI: "../g",
+ scheme: "http",
+ prePath: "http://a",
+ pathQueryRef: "/b/g",
+ ref: "",
+ nsIURL: true,
+ nsINestedURI: false,
+ },
+ {
+ spec: "http://a/b/c/d;p?q",
+ relativeURI: "../..",
+ scheme: "http",
+ prePath: "http://a",
+ pathQueryRef: "/",
+ ref: "",
+ nsIURL: true,
+ nsINestedURI: false,
+ },
+ {
+ spec: "http://a/b/c/d;p?q",
+ relativeURI: "../../",
+ scheme: "http",
+ prePath: "http://a",
+ pathQueryRef: "/",
+ ref: "",
+ nsIURL: true,
+ nsINestedURI: false,
+ },
+ {
+ spec: "http://a/b/c/d;p?q",
+ relativeURI: "../../g",
+ scheme: "http",
+ prePath: "http://a",
+ pathQueryRef: "/g",
+ ref: "",
+ nsIURL: true,
+ nsINestedURI: false,
+ },
+
+ // abnormal examples
+ {
+ spec: "http://a/b/c/d;p?q",
+ relativeURI: "../../../g",
+ scheme: "http",
+ prePath: "http://a",
+ pathQueryRef: "/g",
+ ref: "",
+ nsIURL: true,
+ nsINestedURI: false,
+ },
+ {
+ spec: "http://a/b/c/d;p?q",
+ relativeURI: "../../../../g",
+ scheme: "http",
+ prePath: "http://a",
+ pathQueryRef: "/g",
+ ref: "",
+ nsIURL: true,
+ nsINestedURI: false,
+ },
+
+ // coalesce
+ {
+ spec: "http://a/b/c/d;p?q",
+ relativeURI: "/./g",
+ scheme: "http",
+ prePath: "http://a",
+ pathQueryRef: "/g",
+ ref: "",
+ nsIURL: true,
+ nsINestedURI: false,
+ },
+ {
+ spec: "http://a/b/c/d;p?q",
+ relativeURI: "/../g",
+ scheme: "http",
+ prePath: "http://a",
+ pathQueryRef: "/g",
+ ref: "",
+ nsIURL: true,
+ nsINestedURI: false,
+ },
+ {
+ spec: "http://a/b/c/d;p?q",
+ relativeURI: "g.",
+ scheme: "http",
+ prePath: "http://a",
+ pathQueryRef: "/b/c/g.",
+ ref: "",
+ nsIURL: true,
+ nsINestedURI: false,
+ },
+ {
+ spec: "http://a/b/c/d;p?q",
+ relativeURI: ".g",
+ scheme: "http",
+ prePath: "http://a",
+ pathQueryRef: "/b/c/.g",
+ ref: "",
+ nsIURL: true,
+ nsINestedURI: false,
+ },
+ {
+ spec: "http://a/b/c/d;p?q",
+ relativeURI: "g..",
+ scheme: "http",
+ prePath: "http://a",
+ pathQueryRef: "/b/c/g..",
+ ref: "",
+ nsIURL: true,
+ nsINestedURI: false,
+ },
+ {
+ spec: "http://a/b/c/d;p?q",
+ relativeURI: "..g",
+ scheme: "http",
+ prePath: "http://a",
+ pathQueryRef: "/b/c/..g",
+ ref: "",
+ nsIURL: true,
+ nsINestedURI: false,
+ },
+ {
+ spec: "http://a/b/c/d;p?q",
+ relativeURI: ".",
+ scheme: "http",
+ prePath: "http://a",
+ pathQueryRef: "/b/c/",
+ ref: "",
+ nsIURL: true,
+ nsINestedURI: false,
+ },
+ {
+ spec: "http://a/b/c/d;p?q",
+ relativeURI: "./../g",
+ scheme: "http",
+ prePath: "http://a",
+ pathQueryRef: "/b/g",
+ ref: "",
+ nsIURL: true,
+ nsINestedURI: false,
+ },
+ {
+ spec: "http://a/b/c/d;p?q",
+ relativeURI: "./g/.",
+ scheme: "http",
+ prePath: "http://a",
+ pathQueryRef: "/b/c/g/",
+ ref: "",
+ nsIURL: true,
+ nsINestedURI: false,
+ },
+ {
+ spec: "http://a/b/c/d;p?q",
+ relativeURI: "g/./h",
+ scheme: "http",
+ prePath: "http://a",
+ pathQueryRef: "/b/c/g/h",
+ ref: "",
+ nsIURL: true,
+ nsINestedURI: false,
+ },
+ {
+ spec: "http://a/b/c/d;p?q",
+ relativeURI: "g/../h",
+ scheme: "http",
+ prePath: "http://a",
+ pathQueryRef: "/b/c/h",
+ ref: "", // fix
+ nsIURL: true,
+ nsINestedURI: false,
+ },
+ {
+ spec: "http://a/b/c/d;p?q",
+ relativeURI: "g;x=1/./y",
+ scheme: "http",
+ prePath: "http://a",
+ pathQueryRef: "/b/c/g;x=1/y",
+ ref: "",
+ nsIURL: true,
+ nsINestedURI: false,
+ },
+ {
+ spec: "http://a/b/c/d;p?q",
+ relativeURI: "g;x=1/../y",
+ scheme: "http",
+ prePath: "http://a",
+ pathQueryRef: "/b/c/y",
+ ref: "",
+ nsIURL: true,
+ nsINestedURI: false,
+ },
+ // protocol-relative http://tools.ietf.org/html/rfc3986#section-4.2
+ {
+ spec: "http://www2.example.com/",
+ relativeURI: "//www3.example2.com/bar",
+ scheme: "http",
+ prePath: "http://www3.example2.com",
+ pathQueryRef: "/bar",
+ ref: "",
+ nsIURL: true,
+ nsINestedURI: false,
+ },
+ {
+ spec: "https://www2.example.com/",
+ relativeURI: "//www3.example2.com/bar",
+ scheme: "https",
+ prePath: "https://www3.example2.com",
+ pathQueryRef: "/bar",
+ ref: "",
+ nsIURL: true,
+ nsINestedURI: false,
+ },
+];
+
+var gHashSuffixes = ["#", "#myRef", "#myRef?a=b", "#myRef#", "#myRef#x:yz"];
+
+// TEST HELPER FUNCTIONS
+// ---------------------
+function do_info(text, stack) {
+ if (!stack) {
+ stack = Components.stack.caller;
+ }
+
+ dump(
+ "\n" +
+ "TEST-INFO | " +
+ stack.filename +
+ " | [" +
+ stack.name +
+ " : " +
+ stack.lineNumber +
+ "] " +
+ text +
+ "\n"
+ );
+}
+
+// Checks that the URIs satisfy equals(), in both possible orderings.
+// Also checks URI.equalsExceptRef(), because equal URIs should also be equal
+// when we ignore the ref.
+//
+// The third argument is optional. If the client passes a third argument
+// (e.g. todo_check_true), we'll use that in lieu of ok.
+function do_check_uri_eq(aURI1, aURI2, aCheckTrueFunc = ok) {
+ do_info("(uri equals check: '" + aURI1.spec + "' == '" + aURI2.spec + "')");
+ aCheckTrueFunc(aURI1.equals(aURI2));
+ do_info("(uri equals check: '" + aURI2.spec + "' == '" + aURI1.spec + "')");
+ aCheckTrueFunc(aURI2.equals(aURI1));
+
+ // (Only take the extra step of testing 'equalsExceptRef' when we expect the
+ // URIs to really be equal. In 'todo' cases, the URIs may or may not be
+ // equal when refs are ignored - there's no way of knowing in general.)
+ if (aCheckTrueFunc == ok) {
+ do_check_uri_eqExceptRef(aURI1, aURI2, aCheckTrueFunc);
+ }
+}
+
+// Checks that the URIs satisfy equalsExceptRef(), in both possible orderings.
+//
+// The third argument is optional. If the client passes a third argument
+// (e.g. todo_check_true), we'll use that in lieu of ok.
+function do_check_uri_eqExceptRef(aURI1, aURI2, aCheckTrueFunc = ok) {
+ do_info(
+ "(uri equalsExceptRef check: '" + aURI1.spec + "' == '" + aURI2.spec + "')"
+ );
+ aCheckTrueFunc(aURI1.equalsExceptRef(aURI2));
+ do_info(
+ "(uri equalsExceptRef check: '" + aURI2.spec + "' == '" + aURI1.spec + "')"
+ );
+ aCheckTrueFunc(aURI2.equalsExceptRef(aURI1));
+}
+
+// Checks that the given property on aURI matches the corresponding property
+// in the test bundle (or matches some function of that corresponding property,
+// if aTestFunctor is passed in).
+function do_check_property(aTest, aURI, aPropertyName, aTestFunctor) {
+ if (aTest[aPropertyName]) {
+ var expectedVal = aTestFunctor
+ ? aTestFunctor(aTest[aPropertyName])
+ : aTest[aPropertyName];
+
+ do_info(
+ "testing " +
+ aPropertyName +
+ " of " +
+ (aTestFunctor ? "modified '" : "'") +
+ aTest.spec +
+ "' is '" +
+ expectedVal +
+ "'"
+ );
+ Assert.equal(aURI[aPropertyName], expectedVal);
+ }
+}
+
+// Test that a given URI parses correctly into its various components.
+function do_test_uri_basic(aTest) {
+ var URI;
+
+ do_info(
+ "Basic tests for " + aTest.spec + " relative URI: " + aTest.relativeURI
+ );
+
+ try {
+ URI = NetUtil.newURI(aTest.spec);
+ } catch (e) {
+ do_info("Caught error on parse of" + aTest.spec + " Error: " + e.result);
+ if (aTest.fail) {
+ Assert.equal(e.result, aTest.result);
+ return;
+ }
+ do_throw(e.result);
+ }
+
+ if (aTest.relativeURI) {
+ var relURI;
+
+ try {
+ relURI = gIoService.newURI(aTest.relativeURI, null, URI);
+ } catch (e) {
+ do_info(
+ "Caught error on Relative parse of " +
+ aTest.spec +
+ " + " +
+ aTest.relativeURI +
+ " Error: " +
+ e.result
+ );
+ if (aTest.relativeFail) {
+ Assert.equal(e.result, aTest.relativeFail);
+ return;
+ }
+ do_throw(e.result);
+ }
+ do_info(
+ "relURI.pathQueryRef = " +
+ relURI.pathQueryRef +
+ ", was " +
+ URI.pathQueryRef
+ );
+ URI = relURI;
+ do_info("URI.pathQueryRef now = " + URI.pathQueryRef);
+ }
+
+ // Sanity-check
+ do_info("testing " + aTest.spec + " equals a clone of itself");
+ do_check_uri_eq(URI, URI.mutate().finalize());
+ do_check_uri_eqExceptRef(
+ URI,
+ URI.mutate()
+ .setRef("")
+ .finalize()
+ );
+ do_info("testing " + aTest.spec + " instanceof nsIURL");
+ Assert.equal(URI instanceof Ci.nsIURL, aTest.nsIURL);
+ do_info("testing " + aTest.spec + " instanceof nsINestedURI");
+ Assert.equal(URI instanceof Ci.nsINestedURI, aTest.nsINestedURI);
+
+ do_info(
+ "testing that " +
+ aTest.spec +
+ " throws or returns false " +
+ "from equals(null)"
+ );
+ // XXXdholbert At some point it'd probably be worth making this behavior
+ // (throwing vs. returning false) consistent across URI implementations.
+ var threw = false;
+ var isEqualToNull;
+ try {
+ isEqualToNull = URI.equals(null);
+ } catch (e) {
+ threw = true;
+ }
+ Assert.ok(threw || !isEqualToNull);
+
+ // Check the various components
+ do_check_property(aTest, URI, "scheme");
+ do_check_property(aTest, URI, "prePath");
+ do_check_property(aTest, URI, "pathQueryRef");
+ do_check_property(aTest, URI, "query");
+ do_check_property(aTest, URI, "ref");
+ do_check_property(aTest, URI, "port");
+ do_check_property(aTest, URI, "username");
+ do_check_property(aTest, URI, "password");
+ do_check_property(aTest, URI, "host");
+ do_check_property(aTest, URI, "specIgnoringRef");
+ if ("hasRef" in aTest) {
+ do_info("testing hasref: " + aTest.hasRef + " vs " + URI.hasRef);
+ Assert.equal(aTest.hasRef, URI.hasRef);
+ }
+}
+
+// Test that a given URI parses correctly when we add a given ref to the end
+function do_test_uri_with_hash_suffix(aTest, aSuffix) {
+ do_info("making sure caller is using suffix that starts with '#'");
+ Assert.equal(aSuffix[0], "#");
+
+ var origURI = NetUtil.newURI(aTest.spec);
+ var testURI;
+
+ if (aTest.relativeURI) {
+ try {
+ origURI = gIoService.newURI(aTest.relativeURI, null, origURI);
+ } catch (e) {
+ do_info(
+ "Caught error on Relative parse of " +
+ aTest.spec +
+ " + " +
+ aTest.relativeURI +
+ " Error: " +
+ e.result
+ );
+ return;
+ }
+ try {
+ testURI = gIoService.newURI(aSuffix, null, origURI);
+ } catch (e) {
+ do_info(
+ "Caught error adding suffix to " +
+ aTest.spec +
+ " + " +
+ aTest.relativeURI +
+ ", suffix " +
+ aSuffix +
+ " Error: " +
+ e.result
+ );
+ return;
+ }
+ } else {
+ testURI = NetUtil.newURI(aTest.spec + aSuffix);
+ }
+
+ do_info(
+ "testing " +
+ aTest.spec +
+ " with '" +
+ aSuffix +
+ "' appended " +
+ "equals a clone of itself"
+ );
+ do_check_uri_eq(testURI, testURI.mutate().finalize());
+
+ do_info(
+ "testing " +
+ aTest.spec +
+ " doesn't equal self with '" +
+ aSuffix +
+ "' appended"
+ );
+
+ Assert.ok(!origURI.equals(testURI));
+
+ do_info(
+ "testing " +
+ aTest.spec +
+ " is equalExceptRef to self with '" +
+ aSuffix +
+ "' appended"
+ );
+ do_check_uri_eqExceptRef(origURI, testURI);
+
+ Assert.equal(testURI.hasRef, true);
+
+ if (!origURI.ref) {
+ // These tests fail if origURI has a ref
+ do_info(
+ "testing cloneIgnoringRef on " +
+ testURI.spec +
+ " is equal to no-ref version but not equal to ref version"
+ );
+ var cloneNoRef = testURI
+ .mutate()
+ .setRef("")
+ .finalize();
+ do_check_uri_eq(cloneNoRef, origURI);
+ Assert.ok(!cloneNoRef.equals(testURI));
+ }
+
+ do_check_property(aTest, testURI, "scheme");
+ do_check_property(aTest, testURI, "prePath");
+ if (!origURI.ref) {
+ // These don't work if it's a ref already because '+' doesn't give the right result
+ do_check_property(aTest, testURI, "pathQueryRef", function(aStr) {
+ return aStr + aSuffix;
+ });
+ do_check_property(aTest, testURI, "ref", function(aStr) {
+ return aSuffix.substr(1);
+ });
+ }
+}
+
+// Tests various ways of setting & clearing a ref on a URI.
+function do_test_mutate_ref(aTest, aSuffix) {
+ do_info("making sure caller is using suffix that starts with '#'");
+ Assert.equal(aSuffix[0], "#");
+
+ var refURIWithSuffix = NetUtil.newURI(aTest.spec + aSuffix);
+ var refURIWithoutSuffix = NetUtil.newURI(aTest.spec);
+
+ var testURI = NetUtil.newURI(aTest.spec);
+
+ // First: Try setting .ref to our suffix
+ do_info(
+ "testing that setting .ref on " +
+ aTest.spec +
+ " to '" +
+ aSuffix +
+ "' does what we expect"
+ );
+ testURI = testURI
+ .mutate()
+ .setRef(aSuffix)
+ .finalize();
+ do_check_uri_eq(testURI, refURIWithSuffix);
+ do_check_uri_eqExceptRef(testURI, refURIWithoutSuffix);
+
+ // Now try setting .ref but leave off the initial hash (expect same result)
+ var suffixLackingHash = aSuffix.substr(1);
+ if (suffixLackingHash) {
+ // (skip this our suffix was *just* a #)
+ do_info(
+ "testing that setting .ref on " +
+ aTest.spec +
+ " to '" +
+ suffixLackingHash +
+ "' does what we expect"
+ );
+ testURI = testURI
+ .mutate()
+ .setRef(suffixLackingHash)
+ .finalize();
+ do_check_uri_eq(testURI, refURIWithSuffix);
+ do_check_uri_eqExceptRef(testURI, refURIWithoutSuffix);
+ }
+
+ // Now, clear .ref (should get us back the original spec)
+ do_info(
+ "testing that clearing .ref on " + testURI.spec + " does what we expect"
+ );
+ testURI = testURI
+ .mutate()
+ .setRef("")
+ .finalize();
+ do_check_uri_eq(testURI, refURIWithoutSuffix);
+ do_check_uri_eqExceptRef(testURI, refURIWithSuffix);
+
+ if (!aTest.relativeURI) {
+ // TODO: These tests don't work as-is for relative URIs.
+
+ // Now try setting .spec directly (including suffix) and then clearing .ref
+ var specWithSuffix = aTest.spec + aSuffix;
+ do_info(
+ "testing that setting spec to " +
+ specWithSuffix +
+ " and then clearing ref does what we expect"
+ );
+ testURI = testURI
+ .mutate()
+ .setSpec(specWithSuffix)
+ .setRef("")
+ .finalize();
+ do_check_uri_eq(testURI, refURIWithoutSuffix);
+ do_check_uri_eqExceptRef(testURI, refURIWithSuffix);
+
+ // XXX nsIJARURI throws an exception in SetPath(), so skip it for next part.
+ if (!(testURI instanceof Ci.nsIJARURI)) {
+ // Now try setting .pathQueryRef directly (including suffix) and then clearing .ref
+ // (same as above, but with now with .pathQueryRef instead of .spec)
+ testURI = NetUtil.newURI(aTest.spec);
+
+ var pathWithSuffix = aTest.pathQueryRef + aSuffix;
+ do_info(
+ "testing that setting path to " +
+ pathWithSuffix +
+ " and then clearing ref does what we expect"
+ );
+ testURI = testURI
+ .mutate()
+ .setPathQueryRef(pathWithSuffix)
+ .setRef("")
+ .finalize();
+ do_check_uri_eq(testURI, refURIWithoutSuffix);
+ do_check_uri_eqExceptRef(testURI, refURIWithSuffix);
+
+ // Also: make sure that clearing .pathQueryRef also clears .ref
+ testURI = testURI
+ .mutate()
+ .setPathQueryRef(pathWithSuffix)
+ .finalize();
+ do_info(
+ "testing that clearing path from " +
+ pathWithSuffix +
+ " also clears .ref"
+ );
+ testURI = testURI
+ .mutate()
+ .setPathQueryRef("")
+ .finalize();
+ Assert.equal(testURI.ref, "");
+ }
+ }
+}
+
+// TEST MAIN FUNCTION
+// ------------------
+function run_test() {
+ // UTF-8 check - From bug 622981
+ // ASCII
+ let base = gIoService.newURI("http://example.org/xenia?");
+ let resolved = gIoService.newURI("?x", null, base);
+ let expected = gIoService.newURI("http://example.org/xenia?x");
+ do_info(
+ "Bug 662981: ACSII - comparing " + resolved.spec + " and " + expected.spec
+ );
+ Assert.ok(resolved.equals(expected));
+
+ // UTF-8 character "è"
+ // Bug 622981 was triggered by an empty query string
+ base = gIoService.newURI("http://example.org/xènia?");
+ resolved = gIoService.newURI("?x", null, base);
+ expected = gIoService.newURI("http://example.org/xènia?x");
+ do_info(
+ "Bug 662981: UTF8 - comparing " + resolved.spec + " and " + expected.spec
+ );
+ Assert.ok(resolved.equals(expected));
+
+ gTests.forEach(function(aTest) {
+ // Check basic URI functionality
+ do_test_uri_basic(aTest);
+
+ if (!aTest.fail) {
+ // Try adding various #-prefixed strings to the ends of the URIs
+ gHashSuffixes.forEach(function(aSuffix) {
+ do_test_uri_with_hash_suffix(aTest, aSuffix);
+ if (!aTest.immutable) {
+ do_test_mutate_ref(aTest, aSuffix);
+ }
+ });
+
+ // For URIs that we couldn't mutate above due to them being immutable:
+ // Now we check that they're actually immutable.
+ if (aTest.immutable) {
+ Assert.ok(aTest.immutable);
+ }
+ }
+ });
+}
diff --git a/netwerk/test/unit/test_XHR_redirects.js b/netwerk/test/unit/test_XHR_redirects.js
new file mode 100644
index 0000000000..dfe037d0ec
--- /dev/null
+++ b/netwerk/test/unit/test_XHR_redirects.js
@@ -0,0 +1,273 @@
+// This file tests whether XmlHttpRequests correctly handle redirects,
+// including rewriting POSTs to GETs (on 301/302/303), as well as
+// prompting for redirects of other unsafe methods (such as PUTs, DELETEs,
+// etc--see HttpBaseChannel::IsSafeMethod). Since no prompting is possible
+// in xpcshell, we get an error for prompts, and the request fails.
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+const { Preferences } = ChromeUtils.import(
+ "resource://gre/modules/Preferences.jsm"
+);
+
+var sSame;
+var sOther;
+var sRedirectPromptPref;
+
+const BUGID = "676059";
+const OTHERBUGID = "696849";
+
+XPCOMUtils.defineLazyGetter(this, "pSame", function() {
+ return sSame.identity.primaryPort;
+});
+XPCOMUtils.defineLazyGetter(this, "pOther", function() {
+ return sOther.identity.primaryPort;
+});
+
+function createXHR(async, method, path) {
+ var xhr = new XMLHttpRequest();
+ xhr.open(method, "http://localhost:" + pSame + path, async);
+ return xhr;
+}
+
+function checkResults(xhr, method, status, unsafe) {
+ if (unsafe) {
+ if (sRedirectPromptPref) {
+ // The method is null if we prompt for unsafe redirects
+ method = null;
+ } else {
+ // The status code is 200 when we don't prompt for unsafe redirects
+ status = 200;
+ }
+ }
+
+ if (xhr.readyState != 4) {
+ return false;
+ }
+ Assert.equal(xhr.status, status);
+
+ if (status == 200) {
+ // if followed then check for echoed method name
+ Assert.equal(xhr.getResponseHeader("X-Received-Method"), method);
+ }
+
+ return true;
+}
+
+function run_test() {
+ // start servers
+ sSame = new HttpServer();
+
+ // same-origin redirects
+ sSame.registerPathHandler(
+ "/bug" + BUGID + "-redirect301",
+ bug676059redirect301
+ );
+ sSame.registerPathHandler(
+ "/bug" + BUGID + "-redirect302",
+ bug676059redirect302
+ );
+ sSame.registerPathHandler(
+ "/bug" + BUGID + "-redirect303",
+ bug676059redirect303
+ );
+ sSame.registerPathHandler(
+ "/bug" + BUGID + "-redirect307",
+ bug676059redirect307
+ );
+ sSame.registerPathHandler(
+ "/bug" + BUGID + "-redirect308",
+ bug676059redirect308
+ );
+
+ // cross-origin redirects
+ sSame.registerPathHandler(
+ "/bug" + OTHERBUGID + "-redirect301",
+ bug696849redirect301
+ );
+ sSame.registerPathHandler(
+ "/bug" + OTHERBUGID + "-redirect302",
+ bug696849redirect302
+ );
+ sSame.registerPathHandler(
+ "/bug" + OTHERBUGID + "-redirect303",
+ bug696849redirect303
+ );
+ sSame.registerPathHandler(
+ "/bug" + OTHERBUGID + "-redirect307",
+ bug696849redirect307
+ );
+ sSame.registerPathHandler(
+ "/bug" + OTHERBUGID + "-redirect308",
+ bug696849redirect308
+ );
+
+ // same-origin target
+ sSame.registerPathHandler("/bug" + BUGID + "-target", echoMethod);
+ sSame.start(-1);
+
+ // cross-origin target
+ sOther = new HttpServer();
+ sOther.registerPathHandler("/bug" + OTHERBUGID + "-target", echoMethod);
+ sOther.start(-1);
+
+ // format: redirectType, methodToSend, redirectedMethod, finalStatus
+ // redirectType sets the URI the initial request goes to
+ // methodToSend is the HTTP method to send
+ // redirectedMethod is the method to use for the redirect, if any
+ // finalStatus is 200 when the redirect takes place, redirectType otherwise
+
+ // Note that unsafe methods should not follow the redirect automatically
+ // Of the methods below, DELETE, POST and PUT are unsafe
+
+ sRedirectPromptPref = Preferences.get("network.http.prompt-temp-redirect");
+ // Following Bug 677754 we don't prompt for unsafe redirects
+
+ // same-origin variant
+ var tests = [
+ // 301: rewrite just POST
+ [301, "DELETE", "DELETE", 301, true],
+ [301, "GET", "GET", 200, false],
+ [301, "HEAD", "HEAD", 200, false],
+ [301, "POST", "GET", 200, false],
+ [301, "PUT", "PUT", 301, true],
+ [301, "PROPFIND", "PROPFIND", 200, false],
+ // 302: see 301
+ [302, "DELETE", "DELETE", 302, true],
+ [302, "GET", "GET", 200, false],
+ [302, "HEAD", "HEAD", 200, false],
+ [302, "POST", "GET", 200, false],
+ [302, "PUT", "PUT", 302, true],
+ [302, "PROPFIND", "PROPFIND", 200, false],
+ // 303: rewrite to GET except HEAD
+ [303, "DELETE", "GET", 200, false],
+ [303, "GET", "GET", 200, false],
+ [303, "HEAD", "HEAD", 200, false],
+ [303, "POST", "GET", 200, false],
+ [303, "PUT", "GET", 200, false],
+ [303, "PROPFIND", "GET", 200, false],
+ // 307: never rewrite
+ [307, "DELETE", "DELETE", 307, true],
+ [307, "GET", "GET", 200, false],
+ [307, "HEAD", "HEAD", 200, false],
+ [307, "POST", "POST", 307, true],
+ [307, "PUT", "PUT", 307, true],
+ [307, "PROPFIND", "PROPFIND", 200, false],
+ // 308: never rewrite
+ [308, "DELETE", "DELETE", 308, true],
+ [308, "GET", "GET", 200, false],
+ [308, "HEAD", "HEAD", 200, false],
+ [308, "POST", "POST", 308, true],
+ [308, "PUT", "PUT", 308, true],
+ [308, "PROPFIND", "PROPFIND", 200, false],
+ ];
+
+ // cross-origin variant
+ var othertests = tests; // for now these have identical results
+
+ var xhr;
+
+ for (var i = 0; i < tests.length; ++i) {
+ dump("Testing " + tests[i] + "\n");
+ xhr = createXHR(
+ false,
+ tests[i][1],
+ "/bug" + BUGID + "-redirect" + tests[i][0]
+ );
+ xhr.send(null);
+ checkResults(xhr, tests[i][2], tests[i][3], tests[i][4]);
+ }
+
+ for (var i = 0; i < othertests.length; ++i) {
+ dump("Testing " + othertests[i] + " (cross-origin)\n");
+ xhr = createXHR(
+ false,
+ othertests[i][1],
+ "/bug" + OTHERBUGID + "-redirect" + othertests[i][0]
+ );
+ xhr.send(null);
+ checkResults(xhr, othertests[i][2], tests[i][3], tests[i][4]);
+ }
+
+ sSame.stop(do_test_finished);
+ sOther.stop(do_test_finished);
+}
+
+function redirect(metadata, response, status, port, bugid) {
+ // set a proper reason string to avoid confusion when looking at the
+ // HTTP messages
+ var reason;
+ if (status == 301) {
+ reason = "Moved Permanently";
+ } else if (status == 302) {
+ reason = "Found";
+ } else if (status == 303) {
+ reason = "See Other";
+ } else if (status == 307) {
+ reason = "Temporary Redirect";
+ } else if (status == 308) {
+ reason = "Permanent Redirect";
+ }
+
+ response.setStatusLine(metadata.httpVersion, status, reason);
+ response.setHeader(
+ "Location",
+ "http://localhost:" + port + "/bug" + bugid + "-target"
+ );
+}
+
+// PATH HANDLER FOR /bug676059-redirect301
+function bug676059redirect301(metadata, response) {
+ redirect(metadata, response, 301, pSame, BUGID);
+}
+
+// PATH HANDLER FOR /bug696849-redirect301
+function bug696849redirect301(metadata, response) {
+ redirect(metadata, response, 301, pOther, OTHERBUGID);
+}
+
+// PATH HANDLER FOR /bug676059-redirect302
+function bug676059redirect302(metadata, response) {
+ redirect(metadata, response, 302, pSame, BUGID);
+}
+
+// PATH HANDLER FOR /bug696849-redirect302
+function bug696849redirect302(metadata, response) {
+ redirect(metadata, response, 302, pOther, OTHERBUGID);
+}
+
+// PATH HANDLER FOR /bug676059-redirect303
+function bug676059redirect303(metadata, response) {
+ redirect(metadata, response, 303, pSame, BUGID);
+}
+
+// PATH HANDLER FOR /bug696849-redirect303
+function bug696849redirect303(metadata, response) {
+ redirect(metadata, response, 303, pOther, OTHERBUGID);
+}
+
+// PATH HANDLER FOR /bug676059-redirect307
+function bug676059redirect307(metadata, response) {
+ redirect(metadata, response, 307, pSame, BUGID);
+}
+
+// PATH HANDLER FOR /bug676059-redirect308
+function bug676059redirect308(metadata, response) {
+ redirect(metadata, response, 308, pSame, BUGID);
+}
+
+// PATH HANDLER FOR /bug696849-redirect307
+function bug696849redirect307(metadata, response) {
+ redirect(metadata, response, 307, pOther, OTHERBUGID);
+}
+
+// PATH HANDLER FOR /bug696849-redirect308
+function bug696849redirect308(metadata, response) {
+ redirect(metadata, response, 308, pOther, OTHERBUGID);
+}
+
+// Echo the request method in "X-Received-Method" header field
+function echoMethod(metadata, response) {
+ response.setStatusLine(metadata.httpVersion, 200, "OK");
+ response.setHeader("X-Received-Method", metadata.method);
+}
diff --git a/netwerk/test/unit/test_about_networking.js b/netwerk/test/unit/test_about_networking.js
new file mode 100644
index 0000000000..186e2a539a
--- /dev/null
+++ b/netwerk/test/unit/test_about_networking.js
@@ -0,0 +1,121 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+const gDashboard = Cc["@mozilla.org/network/dashboard;1"].getService(
+ Ci.nsIDashboard
+);
+
+const gServerSocket = Cc["@mozilla.org/network/server-socket;1"].createInstance(
+ Ci.nsIServerSocket
+);
+const gHttpServer = new HttpServer();
+
+add_test(function test_http() {
+ gDashboard.requestHttpConnections(function(data) {
+ let found = false;
+ for (let i = 0; i < data.connections.length; i++) {
+ if (data.connections[i].host == "localhost") {
+ found = true;
+ break;
+ }
+ }
+ Assert.equal(found, true);
+
+ run_next_test();
+ });
+});
+
+add_test(function test_dns() {
+ gDashboard.requestDNSInfo(function(data) {
+ let found = false;
+ for (let i = 0; i < data.entries.length; i++) {
+ if (data.entries[i].hostname == "localhost") {
+ found = true;
+ break;
+ }
+ }
+ Assert.equal(found, true);
+
+ do_test_pending();
+ gHttpServer.stop(do_test_finished);
+
+ run_next_test();
+ });
+});
+
+add_test(function test_sockets() {
+ // TODO: enable this test in bug 1581892.
+ if (mozinfo.socketprocess_networking) {
+ info("skip test_sockets");
+ run_next_test();
+ return;
+ }
+
+ let sts = Cc["@mozilla.org/network/socket-transport-service;1"].getService(
+ Ci.nsISocketTransportService
+ );
+ let threadManager = Cc["@mozilla.org/thread-manager;1"].getService();
+
+ let transport = sts.createTransport(
+ [],
+ "127.0.0.1",
+ gServerSocket.port,
+ null
+ );
+ let listener = {
+ onTransportStatus(aTransport, aStatus, aProgress, aProgressMax) {
+ if (aStatus == Ci.nsISocketTransport.STATUS_CONNECTED_TO) {
+ gDashboard.requestSockets(function(data) {
+ gServerSocket.close();
+ let found = false;
+ for (let i = 0; i < data.sockets.length; i++) {
+ if (data.sockets[i].host == "127.0.0.1") {
+ found = true;
+ break;
+ }
+ }
+ Assert.equal(found, true);
+
+ run_next_test();
+ });
+ }
+ },
+ };
+ transport.setEventSink(listener, threadManager.currentThread);
+
+ transport.openOutputStream(Ci.nsITransport.OPEN_BLOCKING, 0, 0);
+});
+
+function run_test() {
+ Services.prefs.setBoolPref(
+ "network.cookieJarSettings.unblocked_for_testing",
+ true
+ );
+
+ // We always resolve localhost as it's hardcoded without the following pref:
+ Services.prefs.setBoolPref("network.proxy.allow_hijacking_localhost", true);
+
+ let ioService = Cc["@mozilla.org/network/io-service;1"].getService(
+ Ci.nsIIOService
+ );
+
+ gHttpServer.start(-1);
+
+ let uri = ioService.newURI(
+ "http://localhost:" + gHttpServer.identity.primaryPort
+ );
+ let channel = NetUtil.newChannel({ uri, loadUsingSystemPrincipal: true });
+
+ channel.open();
+
+ gServerSocket.init(-1, true, -1);
+ Services.prefs.clearUserPref("network.proxy.allow_hijacking_localhost");
+
+ run_next_test();
+}
diff --git a/netwerk/test/unit/test_about_protocol.js b/netwerk/test/unit/test_about_protocol.js
new file mode 100644
index 0000000000..2da50acad3
--- /dev/null
+++ b/netwerk/test/unit/test_about_protocol.js
@@ -0,0 +1,57 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+var unsafeAboutModule = {
+ QueryInterface: ChromeUtils.generateQI(["nsIAboutModule"]),
+ newChannel(aURI, aLoadInfo) {
+ var uri = Services.io.newURI("about:blank");
+ let chan = Services.io.newChannelFromURIWithLoadInfo(uri, aLoadInfo);
+ chan.owner = Services.scriptSecurityManager.getSystemPrincipal();
+ return chan;
+ },
+ getURIFlags(aURI) {
+ return Ci.nsIAboutModule.URI_SAFE_FOR_UNTRUSTED_CONTENT;
+ },
+};
+
+var factory = {
+ createInstance(aOuter, aIID) {
+ if (aOuter) {
+ throw Components.Exception("", Cr.NS_ERROR_NO_AGGREGATION);
+ }
+ return unsafeAboutModule.QueryInterface(aIID);
+ },
+ lockFactory(aLock) {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
+ },
+ QueryInterface: ChromeUtils.generateQI(["nsIFactory"]),
+};
+
+function run_test() {
+ let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
+ let classID = Cc["@mozilla.org/uuid-generator;1"]
+ .getService(Ci.nsIUUIDGenerator)
+ .generateUUID();
+ registrar.registerFactory(
+ classID,
+ "",
+ "@mozilla.org/network/protocol/about;1?what=unsafe",
+ factory
+ );
+
+ let aboutUnsafeChan = NetUtil.newChannel({
+ uri: "about:unsafe",
+ loadUsingSystemPrincipal: true,
+ });
+
+ Assert.equal(
+ null,
+ aboutUnsafeChan.owner,
+ "URI_SAFE_FOR_UNTRUSTED_CONTENT channel has no owner"
+ );
+
+ registrar.unregisterFactory(classID, factory);
+}
diff --git a/netwerk/test/unit/test_aboutblank.js b/netwerk/test/unit/test_aboutblank.js
new file mode 100644
index 0000000000..2d5c92f095
--- /dev/null
+++ b/netwerk/test/unit/test_aboutblank.js
@@ -0,0 +1,32 @@
+"use strict";
+
+function run_test() {
+ var base = NetUtil.newURI("http://www.example.com");
+ var about1 = NetUtil.newURI("about:blank");
+ var about2 = NetUtil.newURI("about:blank", null, base);
+
+ var chan1 = NetUtil.newChannel({
+ uri: about1,
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIPropertyBag2);
+
+ var chan2 = NetUtil.newChannel({
+ uri: about2,
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIPropertyBag2);
+
+ var haveProp = false;
+ var propVal = null;
+ try {
+ propVal = chan1.getPropertyAsInterface("baseURI", Ci.nsIURI);
+ haveProp = true;
+ } catch (e) {
+ if (e.result != Cr.NS_ERROR_NOT_AVAILABLE) {
+ throw e;
+ }
+ // Property shouldn't be there.
+ }
+ Assert.equal(propVal, null);
+ Assert.equal(haveProp, false);
+ Assert.equal(chan2.getPropertyAsInterface("baseURI", Ci.nsIURI), base);
+}
diff --git a/netwerk/test/unit/test_addr_in_use_error.js b/netwerk/test/unit/test_addr_in_use_error.js
new file mode 100644
index 0000000000..d25860e896
--- /dev/null
+++ b/netwerk/test/unit/test_addr_in_use_error.js
@@ -0,0 +1,36 @@
+// Opening a second listening socket on the same address as an extant
+// socket should elicit NS_ERROR_SOCKET_ADDRESS_IN_USE on non-Windows
+// machines.
+
+"use strict";
+
+var CC = Components.Constructor;
+
+const ServerSocket = CC(
+ "@mozilla.org/network/server-socket;1",
+ "nsIServerSocket",
+ "init"
+);
+
+function testAddrInUse() {
+ // Windows lets us have as many sockets listening on the same address as
+ // we like, evidently.
+ if (mozinfo.os == "win") {
+ return;
+ }
+
+ // Create listening socket:
+ // any port (-1), loopback only (true), default backlog (-1)
+ let listener = ServerSocket(-1, true, -1);
+ Assert.ok(listener instanceof Ci.nsIServerSocket);
+
+ // Try to create another listening socket on the same port, whatever that was.
+ do_check_throws_nsIException(
+ () => ServerSocket(listener.port, true, -1),
+ "NS_ERROR_SOCKET_ADDRESS_IN_USE"
+ );
+}
+
+function run_test() {
+ testAddrInUse();
+}
diff --git a/netwerk/test/unit/test_alt-data_closeWithStatus.js b/netwerk/test/unit/test_alt-data_closeWithStatus.js
new file mode 100644
index 0000000000..766601ee5f
--- /dev/null
+++ b/netwerk/test/unit/test_alt-data_closeWithStatus.js
@@ -0,0 +1,174 @@
+/**
+ * Test for the "alternative data stream" - closing the stream with an error.
+ *
+ * - we load a URL with preference for an alt data (check what we get is the raw data,
+ * since there was nothing previously cached)
+ * - we store something in alt data (using the asyncWait method)
+ * - then we abort the operation calling closeWithStatus()
+ * - we flush the HTTP cache
+ * - we reload the same URL using a new channel, again prefering the alt data be loaded
+ * - again we receive the data from the server.
+ */
+
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+XPCOMUtils.defineLazyGetter(this, "URL", function() {
+ return "http://localhost:" + httpServer.identity.primaryPort + "/content";
+});
+
+var httpServer = null;
+
+// needs to be rooted
+var cacheFlushObserver = (cacheFlushObserver = {
+ observe() {
+ cacheFlushObserver = null;
+ readServerContentAgain();
+ },
+});
+
+var currentThread = null;
+
+function make_channel(url, callback, ctx) {
+ return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true });
+}
+
+function inChildProcess() {
+ return (
+ Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime)
+ .processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT
+ );
+}
+
+const responseContent = "response body";
+const responseContent2 = "response body 2";
+const altContent = "!@#$%^&*()";
+const altContentType = "text/binary";
+
+var servedNotModified = false;
+var shouldPassRevalidation = true;
+
+var cache_storage = null;
+
+function contentHandler(metadata, response) {
+ response.setHeader("Content-Type", "text/plain");
+ response.setHeader("Cache-Control", "no-cache");
+ response.setHeader("ETag", "test-etag1");
+
+ try {
+ var etag = metadata.getHeader("If-None-Match");
+ } catch (ex) {
+ var etag = "";
+ }
+
+ if (etag == "test-etag1" && shouldPassRevalidation) {
+ response.setStatusLine(metadata.httpVersion, 304, "Not Modified");
+ servedNotModified = true;
+ } else {
+ var content = shouldPassRevalidation ? responseContent : responseContent2;
+ response.bodyOutputStream.write(content, content.length);
+ }
+}
+
+function check_has_alt_data_in_index(aHasAltData, callback) {
+ if (inChildProcess()) {
+ callback();
+ return;
+ }
+
+ syncWithCacheIOThread(() => {
+ var hasAltData = {};
+ cache_storage.getCacheIndexEntryAttrs(createURI(URL), "", hasAltData, {});
+ Assert.equal(hasAltData.value, aHasAltData);
+ callback();
+ }, true);
+}
+
+function run_test() {
+ do_get_profile();
+ httpServer = new HttpServer();
+ httpServer.registerPathHandler("/content", contentHandler);
+ httpServer.start(-1);
+ do_test_pending();
+
+ if (!inChildProcess()) {
+ cache_storage = getCacheStorage("disk");
+ wait_for_cache_index(asyncOpen);
+ } else {
+ asyncOpen();
+ }
+}
+
+function asyncOpen() {
+ var chan = make_channel(URL);
+
+ var cc = chan.QueryInterface(Ci.nsICacheInfoChannel);
+ cc.preferAlternativeDataType(altContentType, "", true);
+
+ chan.asyncOpen(new ChannelListener(readServerContent, null));
+}
+
+function readServerContent(request, buffer) {
+ var cc = request.QueryInterface(Ci.nsICacheInfoChannel);
+
+ Assert.equal(buffer, responseContent);
+ Assert.equal(cc.alternativeDataType, "");
+ check_has_alt_data_in_index(false, () => {
+ if (!inChildProcess()) {
+ currentThread = Services.tm.currentThread;
+ }
+
+ executeSoon(() => {
+ var os = cc.openAlternativeOutputStream(
+ altContentType,
+ altContent.length
+ );
+
+ var aos = os.QueryInterface(Ci.nsIAsyncOutputStream);
+ aos.asyncWait(
+ _ => {
+ os.write(altContent, altContent.length);
+ aos.closeWithStatus(Cr.NS_ERROR_FAILURE);
+ executeSoon(flushAndReadServerContentAgain);
+ },
+ 0,
+ 0,
+ currentThread
+ );
+ });
+ });
+}
+
+function flushAndReadServerContentAgain() {
+ // We need to do a GC pass to ensure the cache entry has been freed.
+ gc();
+ if (!inChildProcess()) {
+ Services.cache2
+ .QueryInterface(Ci.nsICacheTesting)
+ .flush(cacheFlushObserver);
+ } else {
+ do_send_remote_message("flush");
+ do_await_remote_message("flushed").then(() => {
+ readServerContentAgain();
+ });
+ }
+}
+
+function readServerContentAgain() {
+ var chan = make_channel(URL);
+ var cc = chan.QueryInterface(Ci.nsICacheInfoChannel);
+ cc.preferAlternativeDataType("dummy1", "text/javascript", true);
+ cc.preferAlternativeDataType(altContentType, "text/plain", true);
+ cc.preferAlternativeDataType("dummy2", "", true);
+
+ chan.asyncOpen(new ChannelListener(readServerContentAgainCB, null));
+}
+
+function readServerContentAgainCB(request, buffer) {
+ var cc = request.QueryInterface(Ci.nsICacheInfoChannel);
+
+ Assert.equal(buffer, responseContent);
+ Assert.equal(cc.alternativeDataType, "");
+ check_has_alt_data_in_index(false, () => httpServer.stop(do_test_finished));
+}
diff --git a/netwerk/test/unit/test_alt-data_cross_process.js b/netwerk/test/unit/test_alt-data_cross_process.js
new file mode 100644
index 0000000000..05959a54af
--- /dev/null
+++ b/netwerk/test/unit/test_alt-data_cross_process.js
@@ -0,0 +1,143 @@
+/**
+ * Test for the "alternative data stream" stored withing a cache entry.
+ *
+ * - we load a URL with preference for an alt data (check what we get is the raw data,
+ * since there was nothing previously cached)
+ * - we store the alt data along the channel (to the cache entry)
+ * - we flush the HTTP cache
+ * - we reload the same URL using a new channel, again prefering the alt data be loaded
+ * - this time the alt data must arive
+ */
+
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+XPCOMUtils.defineLazyGetter(this, "URL", function() {
+ return "http://localhost:" + httpServer.identity.primaryPort + "/content";
+});
+
+var httpServer = null;
+
+function make_channel(url, callback, ctx) {
+ return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true });
+}
+
+function inChildProcess() {
+ return (
+ Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime)
+ .processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT
+ );
+}
+
+const responseContent = "response body";
+const responseContent2 = "response body 2";
+const altContent = "!@#$%^&*()";
+const altContentType = "text/binary";
+
+var servedNotModified = false;
+var shouldPassRevalidation = true;
+
+var cache_storage = null;
+
+function contentHandler(metadata, response) {
+ response.setHeader("Content-Type", "text/plain");
+ response.setHeader("Cache-Control", "no-cache");
+ response.setHeader("ETag", "test-etag1");
+
+ try {
+ var etag = metadata.getHeader("If-None-Match");
+ } catch (ex) {
+ var etag = "";
+ }
+
+ if (etag == "test-etag1" && shouldPassRevalidation) {
+ response.setStatusLine(metadata.httpVersion, 304, "Not Modified");
+ servedNotModified = true;
+ } else {
+ var content = shouldPassRevalidation ? responseContent : responseContent2;
+ response.bodyOutputStream.write(content, content.length);
+ }
+}
+
+function check_has_alt_data_in_index(aHasAltData, callback) {
+ if (inChildProcess()) {
+ callback();
+ return;
+ }
+
+ syncWithCacheIOThread(() => {
+ var hasAltData = {};
+ cache_storage.getCacheIndexEntryAttrs(createURI(URL), "", hasAltData, {});
+ Assert.equal(hasAltData.value, aHasAltData);
+ callback();
+ }, true);
+}
+
+function run_test() {
+ httpServer = new HttpServer();
+ httpServer.registerPathHandler("/content", contentHandler);
+ httpServer.start(-1);
+ do_test_pending();
+
+ asyncOpen();
+}
+
+function asyncOpen() {
+ var chan = make_channel(URL);
+
+ var cc = chan.QueryInterface(Ci.nsICacheInfoChannel);
+ cc.preferAlternativeDataType(altContentType, "", true);
+
+ chan.asyncOpen(new ChannelListener(readServerContent, null));
+}
+
+function readServerContent(request, buffer) {
+ var cc = request.QueryInterface(Ci.nsICacheInfoChannel);
+
+ Assert.equal(buffer, responseContent);
+ Assert.equal(cc.alternativeDataType, "");
+ check_has_alt_data_in_index(false, () => {
+ executeSoon(() => {
+ var os = cc.openAlternativeOutputStream(
+ altContentType,
+ altContent.length
+ );
+ os.write(altContent, altContent.length);
+ os.close();
+
+ executeSoon(flushAndOpenAltChannel);
+ });
+ });
+}
+
+function flushAndOpenAltChannel() {
+ // We need to do a GC pass to ensure the cache entry has been freed.
+ gc();
+ do_send_remote_message("flush");
+ do_await_remote_message("flushed").then(() => {
+ openAltChannel();
+ });
+}
+
+function openAltChannel() {
+ var chan = make_channel(URL);
+ var cc = chan.QueryInterface(Ci.nsICacheInfoChannel);
+ cc.preferAlternativeDataType(altContentType, "", true);
+
+ chan.asyncOpen(new ChannelListener(readAltContent, null));
+}
+
+function readAltContent(request, buffer) {
+ var cc = request.QueryInterface(Ci.nsICacheInfoChannel);
+
+ Assert.equal(servedNotModified, true);
+ Assert.equal(cc.alternativeDataType, altContentType);
+ Assert.equal(buffer, altContent);
+
+ // FINISH
+ do_send_remote_message("done");
+ do_await_remote_message("finish").then(() => {
+ httpServer.stop(do_test_finished);
+ });
+}
diff --git a/netwerk/test/unit/test_alt-data_overwrite.js b/netwerk/test/unit/test_alt-data_overwrite.js
new file mode 100644
index 0000000000..8521c13107
--- /dev/null
+++ b/netwerk/test/unit/test_alt-data_overwrite.js
@@ -0,0 +1,203 @@
+/**
+ * Test for overwriting the alternative data in a cache entry.
+ *
+ * - run_test loads a new channel
+ * - readServerContent checks the content, and saves alt-data
+ * - cacheFlushObserver creates a new channel with "text/binary" alt-data type
+ * - readAltContent checks that it gets back alt-data and creates a channel with the dummy/null alt-data type
+ * - readServerContent2 checks that it gets regular content, from the cache and tries to overwrite the alt-data with the same representation
+ * - cacheFlushObserver2 creates a new channel with "text/binary" alt-data type
+ * - readAltContent2 checks that it gets back alt-data, and tries to overwrite with a kind of alt-data
+ * - cacheFlushObserver3 creates a new channel with "text/binary2" alt-data type
+ * - readAltContent3 checks that it gets back the newly saved alt-data
+ */
+
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+XPCOMUtils.defineLazyGetter(this, "URL", function() {
+ return "http://localhost:" + httpServer.identity.primaryPort + "/content";
+});
+
+let httpServer = null;
+
+function make_and_open_channel(url, altContentType, callback) {
+ let chan = NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true });
+ if (altContentType) {
+ let cc = chan.QueryInterface(Ci.nsICacheInfoChannel);
+ cc.preferAlternativeDataType(altContentType, "", true);
+ }
+ chan.asyncOpen(new ChannelListener(callback, null));
+}
+
+const responseContent = "response body";
+const altContent = "!@#$%^&*()";
+const altContentType = "text/binary";
+const altContent2 = "abc";
+const altContentType2 = "text/binary2";
+
+let servedNotModified = false;
+
+function contentHandler(metadata, response) {
+ response.setHeader("Content-Type", "text/plain");
+ response.setHeader("Cache-Control", "no-cache");
+ response.setHeader("ETag", "test-etag1");
+ let etag = "";
+ try {
+ etag = metadata.getHeader("If-None-Match");
+ } catch (ex) {
+ etag = "";
+ }
+
+ if (etag == "test-etag1") {
+ response.setStatusLine(metadata.httpVersion, 304, "Not Modified");
+ servedNotModified = true;
+ } else {
+ servedNotModified = false;
+ response.bodyOutputStream.write(responseContent, responseContent.length);
+ }
+}
+
+function run_test() {
+ do_get_profile();
+ httpServer = new HttpServer();
+ httpServer.registerPathHandler("/content", contentHandler);
+ httpServer.start(-1);
+
+ do_test_pending();
+ make_and_open_channel(URL, altContentType, readServerContent);
+}
+
+function readServerContent(request, buffer) {
+ let cc = request.QueryInterface(Ci.nsICacheInfoChannel);
+
+ Assert.equal(buffer, responseContent);
+ Assert.equal(cc.alternativeDataType, "");
+
+ executeSoon(() => {
+ let os = cc.openAlternativeOutputStream(altContentType, altContent.length);
+ os.write(altContent, altContent.length);
+ os.close();
+
+ executeSoon(flushAndOpenAltChannel);
+ });
+}
+
+function flushAndOpenAltChannel() {
+ // We need to do a GC pass to ensure the cache entry has been freed.
+ Cu.forceShrinkingGC();
+ Services.cache2.QueryInterface(Ci.nsICacheTesting).flush(cacheFlushObserver);
+}
+
+// needs to be rooted
+let cacheFlushObserver = {
+ observe() {
+ if (!cacheFlushObserver) {
+ info("ignoring cacheFlushObserver\n");
+ return;
+ }
+ cacheFlushObserver = null;
+ Cu.forceShrinkingGC();
+ make_and_open_channel(URL, altContentType, readAltContent);
+ },
+};
+
+function readAltContent(request, buffer, closure, fromCache) {
+ Cu.forceShrinkingGC();
+ let cc = request.QueryInterface(Ci.nsICacheInfoChannel);
+
+ Assert.equal(fromCache || servedNotModified, true);
+ Assert.equal(cc.alternativeDataType, altContentType);
+ Assert.equal(buffer, altContent);
+
+ make_and_open_channel(URL, "dummy/null", readServerContent2);
+}
+
+function readServerContent2(request, buffer, closure, fromCache) {
+ Cu.forceShrinkingGC();
+ let cc = request.QueryInterface(Ci.nsICacheInfoChannel);
+
+ Assert.equal(fromCache || servedNotModified, true);
+ Assert.equal(buffer, responseContent);
+ Assert.equal(cc.alternativeDataType, "");
+
+ executeSoon(() => {
+ let os = cc.openAlternativeOutputStream(altContentType, altContent.length);
+ os.write(altContent, altContent.length);
+ os.close();
+
+ executeSoon(flushAndOpenAltChannel2);
+ });
+}
+
+function flushAndOpenAltChannel2() {
+ // We need to do a GC pass to ensure the cache entry has been freed.
+ Cu.forceShrinkingGC();
+ Services.cache2.QueryInterface(Ci.nsICacheTesting).flush(cacheFlushObserver2);
+}
+
+// needs to be rooted
+let cacheFlushObserver2 = {
+ observe() {
+ if (!cacheFlushObserver2) {
+ info("ignoring cacheFlushObserver2\n");
+ return;
+ }
+ cacheFlushObserver2 = null;
+ Cu.forceShrinkingGC();
+ make_and_open_channel(URL, altContentType, readAltContent2);
+ },
+};
+
+function readAltContent2(request, buffer, closure, fromCache) {
+ Cu.forceShrinkingGC();
+ let cc = request.QueryInterface(Ci.nsICacheInfoChannel);
+
+ Assert.equal(servedNotModified || fromCache, true);
+ Assert.equal(cc.alternativeDataType, altContentType);
+ Assert.equal(buffer, altContent);
+
+ executeSoon(() => {
+ Cu.forceShrinkingGC();
+ info("writing other content\n");
+ let os = cc.openAlternativeOutputStream(
+ altContentType2,
+ altContent2.length
+ );
+ os.write(altContent2, altContent2.length);
+ os.close();
+
+ executeSoon(flushAndOpenAltChannel3);
+ });
+}
+
+function flushAndOpenAltChannel3() {
+ // We need to do a GC pass to ensure the cache entry has been freed.
+ Cu.forceShrinkingGC();
+ Services.cache2.QueryInterface(Ci.nsICacheTesting).flush(cacheFlushObserver3);
+}
+
+// needs to be rooted
+let cacheFlushObserver3 = {
+ observe() {
+ if (!cacheFlushObserver3) {
+ info("ignoring cacheFlushObserver3\n");
+ return;
+ }
+
+ cacheFlushObserver3 = null;
+ Cu.forceShrinkingGC();
+ make_and_open_channel(URL, altContentType2, readAltContent3);
+ },
+};
+
+function readAltContent3(request, buffer, closure, fromCache) {
+ let cc = request.QueryInterface(Ci.nsICacheInfoChannel);
+
+ Assert.equal(servedNotModified || fromCache, true);
+ Assert.equal(cc.alternativeDataType, altContentType2);
+ Assert.equal(buffer, altContent2);
+
+ httpServer.stop(do_test_finished);
+}
diff --git a/netwerk/test/unit/test_alt-data_simple.js b/netwerk/test/unit/test_alt-data_simple.js
new file mode 100644
index 0000000000..2132a9237f
--- /dev/null
+++ b/netwerk/test/unit/test_alt-data_simple.js
@@ -0,0 +1,196 @@
+/**
+ * Test for the "alternative data stream" stored withing a cache entry.
+ *
+ * - we load a URL with preference for an alt data (check what we get is the raw data,
+ * since there was nothing previously cached)
+ * - we store the alt data along the channel (to the cache entry)
+ * - we flush the HTTP cache
+ * - we reload the same URL using a new channel, again prefering the alt data be loaded
+ * - this time the alt data must arive
+ */
+
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+XPCOMUtils.defineLazyGetter(this, "URL", function() {
+ return "http://localhost:" + httpServer.identity.primaryPort + "/content";
+});
+
+var httpServer = null;
+
+function make_channel(url, callback, ctx) {
+ return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true });
+}
+
+function inChildProcess() {
+ return (
+ Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime)
+ .processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT
+ );
+}
+
+const responseContent = "response body";
+const responseContent2 = "response body 2";
+const altContent = "!@#$%^&*()";
+const altContentType = "text/binary";
+
+var servedNotModified = false;
+var shouldPassRevalidation = true;
+
+var cache_storage = null;
+
+function contentHandler(metadata, response) {
+ response.setHeader("Content-Type", "text/plain");
+ response.setHeader("Cache-Control", "no-cache");
+ response.setHeader("ETag", "test-etag1");
+
+ try {
+ var etag = metadata.getHeader("If-None-Match");
+ } catch (ex) {
+ var etag = "";
+ }
+
+ if (etag == "test-etag1" && shouldPassRevalidation) {
+ response.setStatusLine(metadata.httpVersion, 304, "Not Modified");
+ servedNotModified = true;
+ } else {
+ var content = shouldPassRevalidation ? responseContent : responseContent2;
+ response.bodyOutputStream.write(content, content.length);
+ }
+}
+
+function check_has_alt_data_in_index(aHasAltData, callback) {
+ if (inChildProcess()) {
+ callback();
+ return;
+ }
+
+ syncWithCacheIOThread(() => {
+ var hasAltData = {};
+ cache_storage.getCacheIndexEntryAttrs(createURI(URL), "", hasAltData, {});
+ Assert.equal(hasAltData.value, aHasAltData);
+ callback();
+ }, true);
+}
+
+function run_test() {
+ do_get_profile();
+ httpServer = new HttpServer();
+ httpServer.registerPathHandler("/content", contentHandler);
+ httpServer.start(-1);
+ do_test_pending();
+
+ if (!inChildProcess()) {
+ cache_storage = getCacheStorage("disk");
+ wait_for_cache_index(asyncOpen);
+ } else {
+ asyncOpen();
+ }
+}
+
+function asyncOpen() {
+ var chan = make_channel(URL);
+
+ var cc = chan.QueryInterface(Ci.nsICacheInfoChannel);
+ cc.preferAlternativeDataType(altContentType, "", true);
+
+ chan.asyncOpen(new ChannelListener(readServerContent, null));
+}
+
+function readServerContent(request, buffer) {
+ var cc = request.QueryInterface(Ci.nsICacheInfoChannel);
+
+ Assert.equal(buffer, responseContent);
+ Assert.equal(cc.alternativeDataType, "");
+ check_has_alt_data_in_index(false, () => {
+ executeSoon(() => {
+ var os = cc.openAlternativeOutputStream(
+ altContentType,
+ altContent.length
+ );
+ os.write(altContent, altContent.length);
+ os.close();
+
+ executeSoon(flushAndOpenAltChannel);
+ });
+ });
+}
+
+// needs to be rooted
+var cacheFlushObserver = (cacheFlushObserver = {
+ observe() {
+ cacheFlushObserver = null;
+ openAltChannel();
+ },
+});
+
+function flushAndOpenAltChannel() {
+ // We need to do a GC pass to ensure the cache entry has been freed.
+ gc();
+ if (!inChildProcess()) {
+ Services.cache2
+ .QueryInterface(Ci.nsICacheTesting)
+ .flush(cacheFlushObserver);
+ } else {
+ do_send_remote_message("flush");
+ do_await_remote_message("flushed").then(() => {
+ openAltChannel();
+ });
+ }
+}
+
+function openAltChannel() {
+ var chan = make_channel(URL);
+ var cc = chan.QueryInterface(Ci.nsICacheInfoChannel);
+ cc.preferAlternativeDataType("dummy1", "text/javascript", true);
+ cc.preferAlternativeDataType(altContentType, "text/plain", true);
+ cc.preferAlternativeDataType("dummy2", "", true);
+
+ chan.asyncOpen(new ChannelListener(readAltContent, null));
+}
+
+function readAltContent(request, buffer) {
+ var cc = request.QueryInterface(Ci.nsICacheInfoChannel);
+
+ Assert.equal(servedNotModified, true);
+ Assert.equal(cc.alternativeDataType, altContentType);
+ Assert.equal(buffer, altContent);
+ check_has_alt_data_in_index(true, () => {
+ cc.getOriginalInputStream({
+ onInputStreamReady(aInputStream) {
+ executeSoon(() => readOriginalInputStream(aInputStream));
+ },
+ });
+ });
+}
+
+function readOriginalInputStream(aInputStream) {
+ // We expect the async stream length to match the expected content.
+ // If the test times out, it's probably because of this.
+ try {
+ let originalData = read_stream(aInputStream, responseContent.length);
+ Assert.equal(originalData, responseContent);
+ requestAgain();
+ } catch (e) {
+ equal(e.result, Cr.NS_BASE_STREAM_WOULD_BLOCK);
+ executeSoon(() => readOriginalInputStream(aInputStream));
+ }
+}
+
+function requestAgain() {
+ shouldPassRevalidation = false;
+ var chan = make_channel(URL);
+ var cc = chan.QueryInterface(Ci.nsICacheInfoChannel);
+ cc.preferAlternativeDataType(altContentType, "", true);
+ chan.asyncOpen(new ChannelListener(readEmptyAltContent, null));
+}
+
+function readEmptyAltContent(request, buffer) {
+ var cc = request.QueryInterface(Ci.nsICacheInfoChannel);
+
+ // the cache is overwrite and the alt-data is reset
+ Assert.equal(cc.alternativeDataType, "");
+ Assert.equal(buffer, responseContent2);
+ check_has_alt_data_in_index(false, () => httpServer.stop(do_test_finished));
+}
diff --git a/netwerk/test/unit/test_alt-data_stream.js b/netwerk/test/unit/test_alt-data_stream.js
new file mode 100644
index 0000000000..c6eb665226
--- /dev/null
+++ b/netwerk/test/unit/test_alt-data_stream.js
@@ -0,0 +1,153 @@
+/**
+ * Test for the "alternative data stream" stored withing a cache entry.
+ *
+ * - we load a URL with preference for an alt data (check what we get is the raw data,
+ * since there was nothing previously cached)
+ * - we write a big chunk of alt-data to the output stream
+ * - we load the URL again, expecting to get alt-data
+ * - we check that the alt-data is streamed. We should get the first chunk, then
+ * the rest of the alt-data is written, and we check that it is received in
+ * the proper order.
+ *
+ */
+
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+XPCOMUtils.defineLazyGetter(this, "URL", function() {
+ return "http://localhost:" + httpServer.identity.primaryPort + "/content";
+});
+
+var httpServer = null;
+
+function make_channel(url) {
+ return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true });
+}
+
+const responseContent = "response body";
+// We need a large content in order to make sure that the IPDL stream is cut
+// into several different chunks.
+// We fill each chunk with a different character for easy debugging.
+const altContent =
+ "a".repeat(128 * 1024) +
+ "b".repeat(128 * 1024) +
+ "c".repeat(128 * 1024) +
+ "d".repeat(128 * 1024) +
+ "e".repeat(128 * 1024) +
+ "f".repeat(128 * 1024) +
+ "g".repeat(128 * 1024) +
+ "h".repeat(128 * 1024) +
+ "i".repeat(13); // Just so the chunk size doesn't match exactly.
+
+const firstChunkSize = Math.floor(altContent.length / 4);
+const altContentType = "text/binary";
+
+function contentHandler(metadata, response) {
+ response.setHeader("Content-Type", "text/plain");
+ response.setHeader("Cache-Control", "max-age=86400");
+
+ response.bodyOutputStream.write(responseContent, responseContent.length);
+}
+
+function run_test() {
+ do_get_profile();
+ httpServer = new HttpServer();
+ httpServer.registerPathHandler("/content", contentHandler);
+ httpServer.start(-1);
+
+ var chan = make_channel(URL);
+
+ var cc = chan.QueryInterface(Ci.nsICacheInfoChannel);
+ cc.preferAlternativeDataType(altContentType, "", true);
+
+ chan.asyncOpen(new ChannelListener(readServerContent, null));
+ do_test_pending();
+}
+
+// Output stream used to write alt-data to the cache entry.
+var os;
+
+function readServerContent(request, buffer) {
+ var cc = request.QueryInterface(Ci.nsICacheInfoChannel);
+
+ Assert.equal(buffer, responseContent);
+ Assert.equal(cc.alternativeDataType, "");
+
+ executeSoon(() => {
+ os = cc.openAlternativeOutputStream(altContentType, altContent.length);
+ // Write a quarter of the alt data content
+ os.write(altContent, firstChunkSize);
+
+ executeSoon(openAltChannel);
+ });
+}
+
+function openAltChannel() {
+ var chan = make_channel(URL);
+ var cc = chan.QueryInterface(Ci.nsICacheInfoChannel);
+ cc.preferAlternativeDataType(altContentType, "", true);
+
+ chan.asyncOpen(altDataListener);
+}
+
+var altDataListener = {
+ buffer: "",
+ onStartRequest(request) {},
+ onDataAvailable(request, stream, offset, count) {
+ let string = NetUtil.readInputStreamToString(stream, count);
+ this.buffer += string;
+
+ // XXX: this condition might be a bit volatile. If this test times out,
+ // it probably means that for some reason, the listener didn't get all the
+ // data in the first chunk.
+ if (this.buffer.length == firstChunkSize) {
+ // write the rest of the content
+ os.write(
+ altContent.substring(firstChunkSize, altContent.length),
+ altContent.length - firstChunkSize
+ );
+ os.close();
+ }
+ },
+ onStopRequest(request, status) {
+ var cc = request.QueryInterface(Ci.nsICacheInfoChannel);
+ Assert.equal(cc.alternativeDataType, altContentType);
+ Assert.equal(this.buffer.length, altContent.length);
+ Assert.equal(this.buffer, altContent);
+ openAltChannelWithOriginalContent();
+ },
+};
+
+function openAltChannelWithOriginalContent() {
+ var chan = make_channel(URL);
+ var cc = chan.QueryInterface(Ci.nsICacheInfoChannel);
+ cc.preferAlternativeDataType(altContentType, "", false);
+
+ chan.asyncOpen(originalListener);
+}
+
+var originalListener = {
+ buffer: "",
+ onStartRequest(request) {},
+ onDataAvailable(request, stream, offset, count) {
+ let string = NetUtil.readInputStreamToString(stream, count);
+ this.buffer += string;
+ },
+ onStopRequest(request, status) {
+ var cc = request.QueryInterface(Ci.nsICacheInfoChannel);
+ Assert.equal(cc.alternativeDataType, altContentType);
+ Assert.equal(this.buffer.length, responseContent.length);
+ Assert.equal(this.buffer, responseContent);
+ testAltDataStream(cc);
+ },
+};
+
+function testAltDataStream(cc) {
+ cc.getAltDataInputStream(altContentType, {
+ onInputStreamReady(aInputStream) {
+ Assert.ok(!!aInputStream);
+ httpServer.stop(do_test_finished);
+ },
+ });
+}
diff --git a/netwerk/test/unit/test_alt-data_too_big.js b/netwerk/test/unit/test_alt-data_too_big.js
new file mode 100644
index 0000000000..d928394272
--- /dev/null
+++ b/netwerk/test/unit/test_alt-data_too_big.js
@@ -0,0 +1,113 @@
+/**
+ * Test for handling too big alternative data
+ *
+ * - first we try to open an output stream for too big alt-data which must fail
+ * and leave original data intact
+ *
+ * - then we open the output stream without passing predicted data size which
+ * succeeds but writing must fail later at the size limit and the original
+ * data must be kept
+ */
+
+"use strict";
+
+var data = "data ";
+var altData = "alt-data";
+
+function run_test() {
+ do_get_profile();
+
+ // Expand both data to 1MB
+ for (let i = 0; i < 17; i++) {
+ data += data;
+ altData += altData;
+ }
+
+ // Set the limit so that the data fits but alt-data doesn't.
+ Services.prefs.setIntPref("browser.cache.disk.max_entry_size", 1800);
+
+ write_data();
+
+ do_test_pending();
+}
+
+function write_data() {
+ asyncOpenCacheEntry(
+ "http://data/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ function(status, entry) {
+ Assert.equal(status, Cr.NS_OK);
+
+ var os = entry.openOutputStream(0, -1);
+ var written = os.write(data, data.length);
+ Assert.equal(written, data.length);
+ os.close();
+
+ open_big_altdata_output(entry);
+ }
+ );
+}
+
+function open_big_altdata_output(entry) {
+ try {
+ entry.openAlternativeOutputStream("text/binary", altData.length);
+ } catch (e) {
+ Assert.equal(e.result, Cr.NS_ERROR_FILE_TOO_BIG);
+ }
+ entry.close();
+
+ check_entry(write_big_altdata);
+}
+
+function write_big_altdata() {
+ asyncOpenCacheEntry(
+ "http://data/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ function(status, entry) {
+ Assert.equal(status, Cr.NS_OK);
+
+ var os = entry.openAlternativeOutputStream("text/binary", -1);
+ try {
+ os.write(altData, altData.length);
+ } catch (e) {
+ Assert.equal(e.result, Cr.NS_ERROR_FILE_TOO_BIG);
+ }
+ os.close();
+ entry.close();
+
+ check_entry(do_test_finished);
+ }
+ );
+}
+
+function check_entry(cb) {
+ asyncOpenCacheEntry(
+ "http://data/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ function(status, entry) {
+ Assert.equal(status, Cr.NS_OK);
+
+ var is = null;
+ try {
+ is = entry.openAlternativeInputStream("text/binary");
+ } catch (e) {
+ Assert.equal(e.result, Cr.NS_ERROR_NOT_AVAILABLE);
+ }
+
+ is = entry.openInputStream(0);
+ pumpReadStream(is, function(read) {
+ Assert.equal(read.length, data.length);
+ is.close();
+ entry.close();
+
+ executeSoon(cb);
+ });
+ }
+ );
+}
diff --git a/netwerk/test/unit/test_altsvc.js b/netwerk/test/unit/test_altsvc.js
new file mode 100644
index 0000000000..e9e0d5fc7b
--- /dev/null
+++ b/netwerk/test/unit/test_altsvc.js
@@ -0,0 +1,519 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+var h2Port;
+var prefs;
+var spdypref;
+var http2pref;
+var altsvcpref1;
+var altsvcpref2;
+
+// https://foo.example.com:(h2Port)
+// https://bar.example.com:(h2Port) <- invalid for bar, but ok for foo
+var h1Foo; // server http://foo.example.com:(h1Foo.identity.primaryPort)
+var h1Bar; // server http://bar.example.com:(h1bar.identity.primaryPort)
+
+var otherServer; // server socket listening for other connection.
+
+var h2FooRoute; // foo.example.com:H2PORT
+var h2BarRoute; // bar.example.com:H2PORT
+var h2Route; // :H2PORT
+var httpFooOrigin; // http://foo.exmaple.com:PORT/
+var httpsFooOrigin; // https://foo.exmaple.com:PORT/
+var httpBarOrigin; // http://bar.example.com:PORT/
+var httpsBarOrigin; // https://bar.example.com:PORT/
+
+function run_test() {
+ var env = Cc["@mozilla.org/process/environment;1"].getService(
+ Ci.nsIEnvironment
+ );
+ h2Port = env.get("MOZHTTP2_PORT");
+ Assert.notEqual(h2Port, null);
+ Assert.notEqual(h2Port, "");
+
+ // Set to allow the cert presented by our H2 server
+ do_get_profile();
+ prefs = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch);
+
+ spdypref = prefs.getBoolPref("network.http.spdy.enabled");
+ http2pref = prefs.getBoolPref("network.http.spdy.enabled.http2");
+ altsvcpref1 = prefs.getBoolPref("network.http.altsvc.enabled");
+ altsvcpref2 = prefs.getBoolPref("network.http.altsvc.oe", true);
+
+ prefs.setBoolPref("network.http.spdy.enabled", true);
+ prefs.setBoolPref("network.http.spdy.enabled.http2", true);
+ prefs.setBoolPref("network.http.altsvc.enabled", true);
+ prefs.setBoolPref("network.http.altsvc.oe", true);
+ prefs.setCharPref(
+ "network.dns.localDomains",
+ "foo.example.com, bar.example.com"
+ );
+
+ // The moz-http2 cert is for foo.example.com and is signed by http2-ca.pem
+ // so add that cert to the trust list as a signing cert. The same cert is used
+ // for both h2FooRoute and h2BarRoute though it is only valid for
+ // the foo.example.com domain name.
+ let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(
+ Ci.nsIX509CertDB
+ );
+ addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u");
+
+ h1Foo = new HttpServer();
+ h1Foo.registerPathHandler("/altsvc-test", h1Server);
+ h1Foo.registerPathHandler("/.well-known/http-opportunistic", h1ServerWK);
+ h1Foo.start(-1);
+ h1Foo.identity.setPrimary(
+ "http",
+ "foo.example.com",
+ h1Foo.identity.primaryPort
+ );
+
+ h1Bar = new HttpServer();
+ h1Bar.registerPathHandler("/altsvc-test", h1Server);
+ h1Bar.start(-1);
+ h1Bar.identity.setPrimary(
+ "http",
+ "bar.example.com",
+ h1Bar.identity.primaryPort
+ );
+
+ h2FooRoute = "foo.example.com:" + h2Port;
+ h2BarRoute = "bar.example.com:" + h2Port;
+ h2Route = ":" + h2Port;
+
+ httpFooOrigin = "http://foo.example.com:" + h1Foo.identity.primaryPort + "/";
+ httpsFooOrigin = "https://" + h2FooRoute + "/";
+ httpBarOrigin = "http://bar.example.com:" + h1Bar.identity.primaryPort + "/";
+ httpsBarOrigin = "https://" + h2BarRoute + "/";
+ dump(
+ "http foo - " +
+ httpFooOrigin +
+ "\n" +
+ "https foo - " +
+ httpsFooOrigin +
+ "\n" +
+ "http bar - " +
+ httpBarOrigin +
+ "\n" +
+ "https bar - " +
+ httpsBarOrigin +
+ "\n"
+ );
+
+ doTest1();
+}
+
+function h1Server(metadata, response) {
+ response.setStatusLine(metadata.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "text/plain", false);
+ response.setHeader("Connection", "close", false);
+ response.setHeader("Cache-Control", "no-cache", false);
+ response.setHeader("Access-Control-Allow-Origin", "*", false);
+ response.setHeader("Access-Control-Allow-Method", "GET", false);
+ response.setHeader("Access-Control-Allow-Headers", "x-altsvc", false);
+
+ try {
+ var hval = "h2=" + metadata.getHeader("x-altsvc");
+ response.setHeader("Alt-Svc", hval, false);
+ } catch (e) {}
+
+ var body = "Q: What did 0 say to 8? A: Nice Belt!\n";
+ response.bodyOutputStream.write(body, body.length);
+}
+
+function h1ServerWK(metadata, response) {
+ response.setStatusLine(metadata.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "application/json", false);
+ response.setHeader("Connection", "close", false);
+ response.setHeader("Cache-Control", "no-cache", false);
+ response.setHeader("Access-Control-Allow-Origin", "*", false);
+ response.setHeader("Access-Control-Allow-Method", "GET", false);
+ response.setHeader("Access-Control-Allow-Headers", "x-altsvc", false);
+
+ var body = '["http://foo.example.com:' + h1Foo.identity.primaryPort + '"]';
+ response.bodyOutputStream.write(body, body.length);
+}
+
+function resetPrefs() {
+ prefs.setBoolPref("network.http.spdy.enabled", spdypref);
+ prefs.setBoolPref("network.http.spdy.enabled.http2", http2pref);
+ prefs.setBoolPref("network.http.altsvc.enabled", altsvcpref1);
+ prefs.setBoolPref("network.http.altsvc.oe", altsvcpref2);
+ prefs.clearUserPref("network.dns.localDomains");
+ prefs.clearUserPref("network.security.ports.banned");
+}
+
+function makeChan(origin) {
+ return NetUtil.newChannel({
+ uri: origin + "altsvc-test",
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+}
+
+var origin;
+var xaltsvc;
+var retryCounter = 0;
+var loadWithoutClearingMappings = false;
+var disallowH3 = false;
+var disallowH2 = false;
+var nextTest;
+var expectPass = true;
+var waitFor = 0;
+var originAttributes = {};
+
+var Listener = function() {};
+Listener.prototype = {
+ onStartRequest: function testOnStartRequest(request) {
+ Assert.ok(request instanceof Ci.nsIHttpChannel);
+
+ if (expectPass) {
+ if (!Components.isSuccessCode(request.status)) {
+ do_throw(
+ "Channel should have a success code! (" + request.status + ")"
+ );
+ }
+ Assert.equal(request.responseStatus, 200);
+ } else {
+ Assert.equal(Components.isSuccessCode(request.status), false);
+ }
+ },
+
+ onDataAvailable: function testOnDataAvailable(request, stream, off, cnt) {
+ read_stream(stream, cnt);
+ },
+
+ onStopRequest: function testOnStopRequest(request, status) {
+ var routed = "";
+ try {
+ routed = request.getRequestHeader("Alt-Used");
+ } catch (e) {}
+ dump("routed is " + routed + "\n");
+ Assert.equal(Components.isSuccessCode(status), expectPass);
+
+ if (waitFor != 0) {
+ Assert.equal(routed, "");
+ do_test_pending();
+ loadWithoutClearingMappings = true;
+ do_timeout(waitFor, doTest);
+ waitFor = 0;
+ xaltsvc = "NA";
+ } else if (xaltsvc == "NA") {
+ Assert.equal(routed, "");
+ nextTest();
+ } else if (routed == xaltsvc) {
+ Assert.equal(routed, xaltsvc); // always true, but a useful log
+ nextTest();
+ } else {
+ dump("poll later for alt svc mapping\n");
+ do_test_pending();
+ loadWithoutClearingMappings = true;
+ do_timeout(500, doTest);
+ }
+
+ do_test_finished();
+ },
+};
+
+function testsDone() {
+ dump("testDone\n");
+ resetPrefs();
+ do_test_pending();
+ otherServer.close();
+ do_test_pending();
+ h1Foo.stop(do_test_finished);
+ do_test_pending();
+ h1Bar.stop(do_test_finished);
+}
+
+function doTest() {
+ dump("execute doTest " + origin + "\n");
+ var chan = makeChan(origin);
+ var listener = new Listener();
+ if (xaltsvc != "NA") {
+ chan.setRequestHeader("x-altsvc", xaltsvc, false);
+ }
+ if (loadWithoutClearingMappings) {
+ chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI;
+ } else {
+ chan.loadFlags =
+ Ci.nsIRequest.LOAD_FRESH_CONNECTION |
+ Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI;
+ }
+ if (disallowH3) {
+ let internalChannel = chan.QueryInterface(Ci.nsIHttpChannelInternal);
+ internalChannel.allowHttp3 = false;
+ disallowH3 = false;
+ }
+ if (disallowH2) {
+ let internalChannel = chan.QueryInterface(Ci.nsIHttpChannelInternal);
+ internalChannel.allowSpdy = false;
+ disallowH2 = false;
+ }
+ loadWithoutClearingMappings = false;
+ chan.loadInfo.originAttributes = originAttributes;
+ chan.asyncOpen(listener);
+}
+
+// xaltsvc is overloaded to do two things..
+// 1] it is sent in the x-altsvc request header, and the response uses the value in the Alt-Svc response header
+// 2] the test polls until necko sets Alt-Used to that value (i.e. it uses that route)
+//
+// When xaltsvc is set to h2Route (i.e. :port with the implied hostname) it doesn't match the alt-used,
+// which is always explicit, so it needs to be changed after the channel is created but before the
+// listener is invoked
+
+// http://foo served from h2=:port
+function doTest1() {
+ dump("doTest1()\n");
+ origin = httpFooOrigin;
+ xaltsvc = h2Route;
+ nextTest = doTest2;
+ do_test_pending();
+ doTest();
+ xaltsvc = h2FooRoute;
+}
+
+// http://foo served from h2=foo:port
+function doTest2() {
+ dump("doTest2()\n");
+ origin = httpFooOrigin;
+ xaltsvc = h2FooRoute;
+ nextTest = doTest3;
+ do_test_pending();
+ doTest();
+}
+
+// http://foo served from h2=bar:port
+// requires cert for foo
+function doTest3() {
+ dump("doTest3()\n");
+ origin = httpFooOrigin;
+ xaltsvc = h2BarRoute;
+ nextTest = doTest4;
+ do_test_pending();
+ doTest();
+}
+
+// https://bar should fail because host bar has cert for foo
+function doTest4() {
+ dump("doTest4()\n");
+ origin = httpsBarOrigin;
+ xaltsvc = "";
+ expectPass = false;
+ nextTest = doTest5;
+ do_test_pending();
+ doTest();
+}
+
+// https://foo no alt-svc (just check cert setup)
+function doTest5() {
+ dump("doTest5()\n");
+ origin = httpsFooOrigin;
+ xaltsvc = "NA";
+ expectPass = true;
+ nextTest = doTest6;
+ do_test_pending();
+ doTest();
+}
+
+// https://foo via bar (bar has cert for foo)
+function doTest6() {
+ dump("doTest6()\n");
+ origin = httpsFooOrigin;
+ xaltsvc = h2BarRoute;
+ nextTest = doTest7;
+ do_test_pending();
+ doTest();
+}
+
+// check again https://bar should fail because host bar has cert for foo
+function doTest7() {
+ dump("doTest7()\n");
+ origin = httpsBarOrigin;
+ xaltsvc = "";
+ expectPass = false;
+ nextTest = doTest8;
+ do_test_pending();
+ doTest();
+}
+
+// http://bar via h2 on bar
+// should not use TLS/h2 because h2BarRoute is not auth'd for bar
+// however the test ought to PASS (i.e. get a 200) because fallback
+// to plaintext happens.. thus the timeout
+function doTest8() {
+ dump("doTest8()\n");
+ origin = httpBarOrigin;
+ xaltsvc = h2BarRoute;
+ expectPass = true;
+ waitFor = 500;
+ nextTest = doTest9;
+ do_test_pending();
+ doTest();
+}
+
+// http://bar served from h2=:port, which is like the bar route in 8
+function doTest9() {
+ dump("doTest9()\n");
+ origin = httpBarOrigin;
+ xaltsvc = h2Route;
+ expectPass = true;
+ waitFor = 500;
+ nextTest = doTest10;
+ do_test_pending();
+ doTest();
+ xaltsvc = h2BarRoute;
+}
+
+// check again https://bar should fail because host bar has cert for foo
+function doTest10() {
+ dump("doTest10()\n");
+ origin = httpsBarOrigin;
+ xaltsvc = "";
+ expectPass = false;
+ nextTest = doTest11;
+ do_test_pending();
+ doTest();
+}
+
+// http://bar served from h2=foo, should fail because host foo only has
+// cert for foo. Fail in this case means alt-svc is not used, but content
+// is served
+function doTest11() {
+ dump("doTest11()\n");
+ origin = httpBarOrigin;
+ xaltsvc = h2FooRoute;
+ expectPass = true;
+ waitFor = 500;
+ nextTest = doTest12;
+ do_test_pending();
+ doTest();
+}
+
+// Test 12-15:
+// Insert a cache of http://foo served from h2=:port with origin attributes.
+function doTest12() {
+ dump("doTest12()\n");
+ origin = httpFooOrigin;
+ xaltsvc = h2Route;
+ originAttributes = {
+ userContextId: 1,
+ firstPartyDomain: "a.com",
+ };
+ nextTest = doTest13;
+ do_test_pending();
+ doTest();
+ xaltsvc = h2FooRoute;
+}
+
+// Make sure we get a cache miss with a different userContextId.
+function doTest13() {
+ dump("doTest13()\n");
+ origin = httpFooOrigin;
+ xaltsvc = "NA";
+ originAttributes = {
+ userContextId: 2,
+ firstPartyDomain: "a.com",
+ };
+ loadWithoutClearingMappings = true;
+ nextTest = doTest14;
+ do_test_pending();
+ doTest();
+}
+
+// Make sure we get a cache miss with a different firstPartyDomain.
+function doTest14() {
+ dump("doTest14()\n");
+ origin = httpFooOrigin;
+ xaltsvc = "NA";
+ originAttributes = {
+ userContextId: 1,
+ firstPartyDomain: "b.com",
+ };
+ loadWithoutClearingMappings = true;
+ nextTest = doTest15;
+ do_test_pending();
+ doTest();
+}
+//
+// Make sure we get a cache hit with the same origin attributes.
+function doTest15() {
+ dump("doTest15()\n");
+ origin = httpFooOrigin;
+ xaltsvc = "NA";
+ originAttributes = {
+ userContextId: 1,
+ firstPartyDomain: "a.com",
+ };
+ loadWithoutClearingMappings = true;
+ nextTest = doTest16;
+ do_test_pending();
+ doTest();
+ // This ensures a cache hit.
+ xaltsvc = h2FooRoute;
+}
+
+// Make sure we do not use H2 if it is disabled on a channel.
+function doTest16() {
+ dump("doTest16()\n");
+ origin = httpFooOrigin;
+ xaltsvc = "NA";
+ disallowH2 = true;
+ originAttributes = {
+ userContextId: 1,
+ firstPartyDomain: "a.com",
+ };
+ loadWithoutClearingMappings = true;
+ nextTest = doTest17;
+ do_test_pending();
+ doTest();
+}
+
+// Make sure we use H2 if only Http3 is disabled on a channel.
+function doTest17() {
+ dump("doTest17()\n");
+ origin = httpFooOrigin;
+ xaltsvc = h2Route;
+ disallowH3 = true;
+ originAttributes = {
+ userContextId: 1,
+ firstPartyDomain: "a.com",
+ };
+ loadWithoutClearingMappings = true;
+ nextTest = doTest18;
+ do_test_pending();
+ doTest();
+ // This should ensures a cache hit.
+ xaltsvc = h2FooRoute;
+}
+
+// Check we don't connect to blocked ports
+function doTest18() {
+ dump("doTest18()\n");
+ origin = httpFooOrigin;
+ nextTest = testsDone;
+ otherServer = Cc["@mozilla.org/network/server-socket;1"].createInstance(
+ Ci.nsIServerSocket
+ );
+ otherServer.init(-1, true, -1);
+ xaltsvc = "localhost:" + otherServer.port;
+ Services.prefs.setCharPref(
+ "network.security.ports.banned",
+ "" + otherServer.port
+ );
+ dump("Blocked port: " + otherServer.port);
+ waitFor = 500;
+ otherServer.asyncListen({
+ onSocketAccepted() {
+ Assert.ok(false, "Got connection to socket when we didn't expect it!");
+ },
+ onStopListening() {
+ // We get closed when the entire file is done, which guarantees we get the socket accept
+ // if we do connect to the alt-svc header
+ do_test_finished();
+ },
+ });
+ do_test_pending();
+ doTest();
+}
diff --git a/netwerk/test/unit/test_altsvc_http3.js b/netwerk/test/unit/test_altsvc_http3.js
new file mode 100644
index 0000000000..a443f4732f
--- /dev/null
+++ b/netwerk/test/unit/test_altsvc_http3.js
@@ -0,0 +1,496 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+var h3Port;
+
+// https://foo.example.com:(h3Port)
+// https://bar.example.com:(h3Port) <- invalid for bar, but ok for foo
+var h1Foo; // server http://foo.example.com:(h1Foo.identity.primaryPort)
+var h1Bar; // server http://bar.example.com:(h1bar.identity.primaryPort)
+
+var otherServer; // server socket listening for other connection.
+
+var h3FooRoute; // foo.example.com:H3PORT
+var h3BarRoute; // bar.example.com:H3PORT
+var h3Route; // :H3PORT
+var httpFooOrigin; // http://foo.exmaple.com:PORT/
+var httpsFooOrigin; // https://foo.exmaple.com:PORT/
+var httpBarOrigin; // http://bar.example.com:PORT/
+var httpsBarOrigin; // https://bar.example.com:PORT/
+
+function run_test() {
+ var env = Cc["@mozilla.org/process/environment;1"].getService(
+ Ci.nsIEnvironment
+ );
+ h3Port = env.get("MOZHTTP3_PORT");
+ Assert.notEqual(h3Port, null);
+ Assert.notEqual(h3Port, "");
+
+ // Set to allow the cert presented by our H3 server
+ do_get_profile();
+
+ Services.prefs.setBoolPref("network.http.http3.enabled", true);
+ Services.prefs.setBoolPref("network.http.altsvc.enabled", true);
+ Services.prefs.setBoolPref("network.http.altsvc.oe", true);
+ Services.prefs.setCharPref(
+ "network.dns.localDomains",
+ "foo.example.com, bar.example.com"
+ );
+
+ // The moz-http2 cert is for foo.example.com and is signed by http2-ca.pem
+ // so add that cert to the trust list as a signing cert. The same cert is used
+ // for both h3FooRoute and h3BarRoute though it is only valid for
+ // the foo.example.com domain name.
+ let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(
+ Ci.nsIX509CertDB
+ );
+ addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u");
+
+ h1Foo = new HttpServer();
+ h1Foo.registerPathHandler("/altsvc-test", h1Server);
+ h1Foo.registerPathHandler("/.well-known/http-opportunistic", h1ServerWK);
+ h1Foo.start(-1);
+ h1Foo.identity.setPrimary(
+ "http",
+ "foo.example.com",
+ h1Foo.identity.primaryPort
+ );
+
+ h1Bar = new HttpServer();
+ h1Bar.registerPathHandler("/altsvc-test", h1Server);
+ h1Bar.start(-1);
+ h1Bar.identity.setPrimary(
+ "http",
+ "bar.example.com",
+ h1Bar.identity.primaryPort
+ );
+
+ h3FooRoute = "foo.example.com:" + h3Port;
+ h3BarRoute = "bar.example.com:" + h3Port;
+ h3Route = ":" + h3Port;
+
+ httpFooOrigin = "http://foo.example.com:" + h1Foo.identity.primaryPort + "/";
+ httpsFooOrigin = "https://" + h3FooRoute + "/";
+ httpBarOrigin = "http://bar.example.com:" + h1Bar.identity.primaryPort + "/";
+ httpsBarOrigin = "https://" + h3BarRoute + "/";
+ dump(
+ "http foo - " +
+ httpFooOrigin +
+ "\n" +
+ "https foo - " +
+ httpsFooOrigin +
+ "\n" +
+ "http bar - " +
+ httpBarOrigin +
+ "\n" +
+ "https bar - " +
+ httpsBarOrigin +
+ "\n"
+ );
+
+ doTest1();
+}
+
+function h1Server(metadata, response) {
+ response.setStatusLine(metadata.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "text/plain", false);
+ response.setHeader("Connection", "close", false);
+ response.setHeader("Cache-Control", "no-cache", false);
+ response.setHeader("Access-Control-Allow-Origin", "*", false);
+ response.setHeader("Access-Control-Allow-Method", "GET", false);
+ response.setHeader("Access-Control-Allow-Headers", "x-altsvc", false);
+
+ try {
+ var hval = "h3-27=" + metadata.getHeader("x-altsvc");
+ response.setHeader("Alt-Svc", hval, false);
+ } catch (e) {}
+
+ var body = "Q: What did 0 say to 8? A: Nice Belt!\n";
+ response.bodyOutputStream.write(body, body.length);
+}
+
+function h1ServerWK(metadata, response) {
+ response.setStatusLine(metadata.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "application/json", false);
+ response.setHeader("Connection", "close", false);
+ response.setHeader("Cache-Control", "no-cache", false);
+ response.setHeader("Access-Control-Allow-Origin", "*", false);
+ response.setHeader("Access-Control-Allow-Method", "GET", false);
+ response.setHeader("Access-Control-Allow-Headers", "x-altsvc", false);
+
+ var body = '["http://foo.example.com:' + h1Foo.identity.primaryPort + '"]';
+ response.bodyOutputStream.write(body, body.length);
+}
+
+function resetPrefs() {
+ Services.prefs.clearUserPref("network.http.http3.enabled");
+ Services.prefs.clearUserPref("network.dns.localDomains");
+ Services.prefs.clearUserPref("network.http.altsvc.enabled");
+ Services.prefs.clearUserPref("network.http.altsvc.oe");
+ Services.prefs.clearUserPref("network.dns.localDomains");
+ Services.prefs.clearUserPref("network.security.ports.banned");
+}
+
+function makeChan(origin) {
+ return NetUtil.newChannel({
+ uri: origin + "altsvc-test",
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+}
+
+var origin;
+var xaltsvc;
+var retryCounter = 0;
+var loadWithoutClearingMappings = false;
+var disallowH3 = false;
+var disallowH2 = false;
+var testKeepAliveNotSet = false;
+var nextTest;
+var expectPass = true;
+var waitFor = 0;
+var originAttributes = {};
+
+var Listener = function() {};
+Listener.prototype = {
+ onStartRequest: function testOnStartRequest(request) {
+ Assert.ok(request instanceof Ci.nsIHttpChannel);
+
+ if (expectPass) {
+ if (!Components.isSuccessCode(request.status)) {
+ do_throw(
+ "Channel should have a success code! (" + request.status + ")"
+ );
+ }
+ Assert.equal(request.responseStatus, 200);
+ } else {
+ Assert.equal(Components.isSuccessCode(request.status), false);
+ }
+ },
+
+ onDataAvailable: function testOnDataAvailable(request, stream, off, cnt) {
+ read_stream(stream, cnt);
+ },
+
+ onStopRequest: function testOnStopRequest(request, status) {
+ var routed = "";
+ try {
+ routed = request.getRequestHeader("Alt-Used");
+ } catch (e) {}
+ dump("routed is " + routed + "\n");
+ Assert.equal(Components.isSuccessCode(status), expectPass);
+
+ if (waitFor != 0) {
+ Assert.equal(routed, "");
+ do_test_pending();
+ loadWithoutClearingMappings = true;
+ do_timeout(waitFor, doTest);
+ waitFor = 0;
+ xaltsvc = "NA";
+ } else if (xaltsvc == "NA") {
+ Assert.equal(routed, "");
+ nextTest();
+ } else if (routed == xaltsvc) {
+ Assert.equal(routed, xaltsvc); // always true, but a useful log
+ nextTest();
+ } else {
+ dump("poll later for alt svc mapping\n");
+ do_test_pending();
+ loadWithoutClearingMappings = true;
+ do_timeout(500, doTest);
+ }
+
+ do_test_finished();
+ },
+};
+
+function testsDone() {
+ dump("testDone\n");
+ resetPrefs();
+ do_test_pending();
+ otherServer.close();
+ do_test_pending();
+ h1Foo.stop(do_test_finished);
+ do_test_pending();
+ h1Bar.stop(do_test_finished);
+}
+
+function doTest() {
+ dump("execute doTest " + origin + "\n");
+ var chan = makeChan(origin);
+ var listener = new Listener();
+ if (xaltsvc != "NA") {
+ chan.setRequestHeader("x-altsvc", xaltsvc, false);
+ }
+ if (testKeepAliveNotSet) {
+ chan.setRequestHeader("Connection", "close", false);
+ testKeepAliveNotSet = false;
+ }
+ if (loadWithoutClearingMappings) {
+ chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI;
+ } else {
+ chan.loadFlags =
+ Ci.nsIRequest.LOAD_FRESH_CONNECTION |
+ Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI;
+ }
+ if (disallowH3) {
+ let internalChannel = chan.QueryInterface(Ci.nsIHttpChannelInternal);
+ internalChannel.allowHttp3 = false;
+ disallowH3 = false;
+ }
+ if (disallowH2) {
+ let internalChannel = chan.QueryInterface(Ci.nsIHttpChannelInternal);
+ internalChannel.allowSpdy = false;
+ disallowH2 = false;
+ }
+ loadWithoutClearingMappings = false;
+ chan.loadInfo.originAttributes = originAttributes;
+ chan.asyncOpen(listener);
+}
+
+// xaltsvc is overloaded to do two things..
+// 1] it is sent in the x-altsvc request header, and the response uses the value in the Alt-Svc response header
+// 2] the test polls until necko sets Alt-Used to that value (i.e. it uses that route)
+//
+// When xaltsvc is set to h3Route (i.e. :port with the implied hostname) it doesn't match the alt-used,
+// which is always explicit, so it needs to be changed after the channel is created but before the
+// listener is invoked
+
+// http://foo served from h3-27=:port
+function doTest1() {
+ dump("doTest1()\n");
+ origin = httpFooOrigin;
+ xaltsvc = h3Route;
+ nextTest = doTest2;
+ do_test_pending();
+ doTest();
+ xaltsvc = h3FooRoute;
+}
+
+// http://foo served from h3-27=foo:port
+function doTest2() {
+ dump("doTest2()\n");
+ origin = httpFooOrigin;
+ xaltsvc = h3FooRoute;
+ nextTest = doTest3;
+ do_test_pending();
+ doTest();
+}
+
+// http://foo served from h3-27=bar:port
+// requires cert for foo
+function doTest3() {
+ dump("doTest3()\n");
+ origin = httpFooOrigin;
+ xaltsvc = h3BarRoute;
+ nextTest = doTest4;
+ do_test_pending();
+ doTest();
+}
+
+// https://bar should fail because host bar has cert for foo
+function doTest4() {
+ dump("doTest4()\n");
+ origin = httpsBarOrigin;
+ xaltsvc = "";
+ expectPass = false;
+ nextTest = doTest5;
+ do_test_pending();
+ doTest();
+}
+
+// http://bar via h3 on bar
+// should not use TLS/h3 because h3BarRoute is not auth'd for bar
+// however the test ought to PASS (i.e. get a 200) because fallback
+// to plaintext happens.. thus the timeout
+function doTest5() {
+ dump("doTest5()\n");
+ origin = httpBarOrigin;
+ xaltsvc = h3BarRoute;
+ expectPass = true;
+ waitFor = 500;
+ nextTest = doTest6;
+ do_test_pending();
+ doTest();
+}
+
+// http://bar served from h3-27=:port, which is like the bar route in 8
+function doTest6() {
+ dump("doTest6()\n");
+ origin = httpBarOrigin;
+ xaltsvc = h3Route;
+ expectPass = true;
+ waitFor = 500;
+ nextTest = doTest7;
+ do_test_pending();
+ doTest();
+ xaltsvc = h3BarRoute;
+}
+
+// check again https://bar should fail because host bar has cert for foo
+function doTest7() {
+ dump("doTest7()\n");
+ origin = httpsBarOrigin;
+ xaltsvc = "";
+ expectPass = false;
+ nextTest = doTest8;
+ do_test_pending();
+ doTest();
+}
+
+// http://bar served from h3-27=foo, should fail because host foo only has
+// cert for foo. Fail in this case means alt-svc is not used, but content
+// is served
+function doTest8() {
+ dump("doTest8()\n");
+ origin = httpBarOrigin;
+ xaltsvc = h3FooRoute;
+ expectPass = true;
+ waitFor = 500;
+ nextTest = doTest9;
+ do_test_pending();
+ doTest();
+}
+
+// Test 9-12:
+// Insert a cache of http://foo served from h3-27=:port with origin attributes.
+function doTest9() {
+ dump("doTest9()\n");
+ origin = httpFooOrigin;
+ xaltsvc = h3Route;
+ originAttributes = {
+ userContextId: 1,
+ firstPartyDomain: "a.com",
+ };
+ nextTest = doTest10;
+ do_test_pending();
+ doTest();
+ xaltsvc = h3FooRoute;
+}
+
+// Make sure we get a cache miss with a different userContextId.
+function doTest10() {
+ dump("doTest10()\n");
+ origin = httpFooOrigin;
+ xaltsvc = "NA";
+ originAttributes = {
+ userContextId: 2,
+ firstPartyDomain: "a.com",
+ };
+ loadWithoutClearingMappings = true;
+ nextTest = doTest11;
+ do_test_pending();
+ doTest();
+}
+
+// Make sure we get a cache miss with a different firstPartyDomain.
+function doTest11() {
+ dump("doTest11()\n");
+ origin = httpFooOrigin;
+ xaltsvc = "NA";
+ originAttributes = {
+ userContextId: 1,
+ firstPartyDomain: "b.com",
+ };
+ loadWithoutClearingMappings = true;
+ nextTest = doTest12;
+ do_test_pending();
+ doTest();
+}
+//
+// Make sure we get a cache hit with the same origin attributes.
+function doTest12() {
+ dump("doTest12()\n");
+ origin = httpFooOrigin;
+ xaltsvc = "NA";
+ originAttributes = {
+ userContextId: 1,
+ firstPartyDomain: "a.com",
+ };
+ loadWithoutClearingMappings = true;
+ nextTest = doTest13;
+ do_test_pending();
+ doTest();
+ // This ensures a cache hit.
+ xaltsvc = h3FooRoute;
+}
+
+// Make sure we do not use H3 if it is disabled on a channel.
+function doTest13() {
+ dump("doTest13()\n");
+ origin = httpFooOrigin;
+ xaltsvc = "NA";
+ disallowH3 = true;
+ originAttributes = {
+ userContextId: 1,
+ firstPartyDomain: "a.com",
+ };
+ loadWithoutClearingMappings = true;
+ nextTest = doTest14;
+ do_test_pending();
+ doTest();
+}
+
+// Make sure we use H3 if only Http2 is disabled on a channel.
+function doTest14() {
+ dump("doTest14()\n");
+ origin = httpFooOrigin;
+ xaltsvc = "NA";
+ disallowH2 = true;
+ originAttributes = {
+ userContextId: 1,
+ firstPartyDomain: "a.com",
+ };
+ loadWithoutClearingMappings = true;
+ nextTest = doTest15;
+ do_test_pending();
+ doTest();
+ // This should ensures a cache hit.
+ xaltsvc = h3FooRoute;
+}
+
+// Make sure we do not use H3 if NS_HTTP_ALLOW_KEEPALIVE is not set.
+function doTest15() {
+ dump("doTest15()\n");
+ origin = httpFooOrigin;
+ xaltsvc = "NA";
+ testKeepAliveNotSet = true;
+ originAttributes = {
+ userContextId: 1,
+ firstPartyDomain: "a.com",
+ };
+ loadWithoutClearingMappings = true;
+ nextTest = doTest16;
+ do_test_pending();
+ doTest();
+}
+
+// Check we don't connect to blocked ports
+function doTest16() {
+ dump("doTest16()\n");
+ origin = httpFooOrigin;
+ nextTest = testsDone;
+ otherServer = Cc["@mozilla.org/network/server-socket;1"].createInstance(
+ Ci.nsIServerSocket
+ );
+ otherServer.init(-1, true, -1);
+ xaltsvc = "localhost:" + otherServer.port;
+ Services.prefs.setCharPref(
+ "network.security.ports.banned",
+ "" + otherServer.port
+ );
+ dump("Blocked port: " + otherServer.port);
+ waitFor = 500;
+ otherServer.asyncListen({
+ onSocketAccepted() {
+ Assert.ok(false, "Got connection to socket when we didn't expect it!");
+ },
+ onStopListening() {
+ // We get closed when the entire file is done, which guarantees we get the socket accept
+ // if we do connect to the alt-svc header
+ do_test_finished();
+ },
+ });
+ do_test_pending();
+ doTest();
+}
diff --git a/netwerk/test/unit/test_altsvc_pref.js b/netwerk/test/unit/test_altsvc_pref.js
new file mode 100644
index 0000000000..a93d67dac5
--- /dev/null
+++ b/netwerk/test/unit/test_altsvc_pref.js
@@ -0,0 +1,139 @@
+"use strict";
+
+let h3Port;
+let h3Route;
+let h3AltSvc;
+let prefs;
+let httpsOrigin;
+
+let tests = [
+ // The altSvc storage may not be up imediately, therefore run test_no_altsvc_pref
+ // for a couple times to wait for the storage.
+ test_no_altsvc_pref,
+ test_no_altsvc_pref,
+ test_no_altsvc_pref,
+ test_altsvc_pref,
+ testsDone,
+];
+
+let current_test = 0;
+
+function run_next_test() {
+ if (current_test < tests.length) {
+ dump("starting test number " + current_test + "\n");
+ tests[current_test]();
+ current_test++;
+ }
+}
+
+function run_test() {
+ let env = Cc["@mozilla.org/process/environment;1"].getService(
+ Ci.nsIEnvironment
+ );
+ h3Port = env.get("MOZHTTP3_PORT");
+ Assert.notEqual(h3Port, null);
+ Assert.notEqual(h3Port, "");
+ h3AltSvc = ":" + h3Port;
+
+ h3Route = "foo.example.com:" + h3Port;
+ do_get_profile();
+ prefs = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch);
+
+ prefs.setBoolPref("network.http.http3.enabled", true);
+ prefs.setCharPref("network.dns.localDomains", "foo.example.com");
+ prefs.setBoolPref("network.dns.disableIPv6", true);
+
+ // The certificate for the http3server server is for foo.example.com and
+ // is signed by http2-ca.pem so add that cert to the trust list as a
+ // signing cert.
+ let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(
+ Ci.nsIX509CertDB
+ );
+ addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u");
+ httpsOrigin = "https://foo.example.com/";
+
+ run_next_test();
+}
+
+let Http3CheckListener = function() {};
+
+Http3CheckListener.prototype = {
+ expectedRoute: "",
+ expectedStatus: Cr.NS_OK,
+
+ onStartRequest: function testOnStartRequest(request) {
+ Assert.ok(request instanceof Ci.nsIHttpChannel);
+ Assert.equal(request.status, this.expectedStatus);
+ if (Components.isSuccessCode(this.expectedStatus)) {
+ Assert.equal(request.responseStatus, 200);
+ }
+ },
+
+ onDataAvailable: function testOnDataAvailable(request, stream, off, cnt) {
+ read_stream(stream, cnt);
+ },
+
+ onStopRequest: function testOnStopRequest(request, status) {
+ Assert.equal(status, this.expectedStatus);
+ if (Components.isSuccessCode(this.expectedStatus)) {
+ Assert.equal(request.responseStatus, 200);
+ let routed = "NA";
+ try {
+ routed = request.getRequestHeader("Alt-Used");
+ } catch (e) {}
+ dump("routed is " + routed + "\n");
+
+ Assert.equal(routed, this.expectedRoute);
+
+ let httpVersion = "";
+ try {
+ httpVersion = request.protocolVersion;
+ } catch (e) {}
+ Assert.equal(httpVersion, "h3");
+ }
+
+ do_test_finished();
+ },
+};
+
+function makeChan(uri) {
+ let chan = NetUtil.newChannel({
+ uri,
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+ chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI;
+ return chan;
+}
+
+function test_no_altsvc_pref() {
+ dump("test_no_altsvc_pref");
+ do_test_pending();
+
+ let chan = makeChan(httpsOrigin + "http3-test");
+ let listener = new Http3CheckListener();
+ listener.expectedStatus = Cr.NS_ERROR_CONNECTION_REFUSED;
+ chan.asyncOpen(listener);
+}
+
+function test_altsvc_pref() {
+ dump("test_altsvc_pref");
+ do_test_pending();
+
+ prefs.setCharPref(
+ "network.http.http3.alt-svc-mapping-for-testing",
+ "foo.example.com;h3-27=" + h3AltSvc
+ );
+
+ let chan = makeChan(httpsOrigin + "http3-test");
+ let listener = new Http3CheckListener();
+ listener.expectedRoute = h3Route;
+ chan.asyncOpen(listener);
+}
+
+function testsDone() {
+ prefs.clearUserPref("network.http.http3.enabled");
+ prefs.clearUserPref("network.dns.localDomains");
+ prefs.clearUserPref("network.dns.disableIPv6");
+ prefs.clearUserPref("network.http.http3.alt-svc-mapping-for-testing");
+ dump("testDone\n");
+}
diff --git a/netwerk/test/unit/test_anonymous-coalescing.js b/netwerk/test/unit/test_anonymous-coalescing.js
new file mode 100644
index 0000000000..e118f6f96c
--- /dev/null
+++ b/netwerk/test/unit/test_anonymous-coalescing.js
@@ -0,0 +1,186 @@
+/*
+- test to check we use only a single connection for both onymous and anonymous requests over an existing h2 session
+- request from a domain w/o LOAD_ANONYMOUS flag
+- request again from the same domain, but different URI, with LOAD_ANONYMOUS flag, check the client is using the same conn
+- close all and do it in the opposite way (do an anonymous req first)
+*/
+
+"use strict";
+
+var h2Port;
+var prefs;
+var spdypref;
+var http2pref;
+var extpref;
+
+function run_test() {
+ var env = Cc["@mozilla.org/process/environment;1"].getService(
+ Ci.nsIEnvironment
+ );
+ h2Port = env.get("MOZHTTP2_PORT");
+ Assert.notEqual(h2Port, null);
+ Assert.notEqual(h2Port, "");
+
+ // Set to allow the cert presented by our H2 server
+ do_get_profile();
+ prefs = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch);
+
+ spdypref = prefs.getBoolPref("network.http.spdy.enabled");
+ http2pref = prefs.getBoolPref("network.http.spdy.enabled.http2");
+ extpref = prefs.getBoolPref("network.http.originextension");
+
+ prefs.setBoolPref("network.http.spdy.enabled", true);
+ prefs.setBoolPref("network.http.spdy.enabled.http2", true);
+ prefs.setBoolPref("network.http.originextension", true);
+ prefs.setCharPref(
+ "network.dns.localDomains",
+ "foo.example.com, alt1.example.com"
+ );
+
+ // The moz-http2 cert is for {foo, alt1, alt2}.example.com and is signed by http2-ca.pem
+ // so add that cert to the trust list as a signing cert.
+ let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(
+ Ci.nsIX509CertDB
+ );
+ addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u");
+
+ doTest1();
+}
+
+function resetPrefs() {
+ prefs.setBoolPref("network.http.spdy.enabled", spdypref);
+ prefs.setBoolPref("network.http.spdy.enabled.http2", http2pref);
+ prefs.setBoolPref("network.http.originextension", extpref);
+ prefs.clearUserPref("network.dns.localDomains");
+}
+
+function makeChan(origin) {
+ return NetUtil.newChannel({
+ uri: origin,
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+}
+
+var nextTest;
+var origin;
+var nextPortExpectedToBeSame = false;
+var currentPort = 0;
+var forceReload = false;
+var anonymous = false;
+
+var Listener = function() {};
+Listener.prototype.clientPort = 0;
+Listener.prototype = {
+ onStartRequest: function testOnStartRequest(request) {
+ Assert.ok(request instanceof Ci.nsIHttpChannel);
+
+ if (!Components.isSuccessCode(request.status)) {
+ do_throw("Channel should have a success code! (" + request.status + ")");
+ }
+ Assert.equal(request.responseStatus, 200);
+ this.clientPort = parseInt(request.getResponseHeader("x-client-port"));
+ },
+
+ onDataAvailable: function testOnDataAvailable(request, stream, off, cnt) {
+ read_stream(stream, cnt);
+ },
+
+ onStopRequest: function testOnStopRequest(request, status) {
+ Assert.ok(Components.isSuccessCode(status));
+ if (nextPortExpectedToBeSame) {
+ Assert.equal(currentPort, this.clientPort);
+ } else {
+ Assert.notEqual(currentPort, this.clientPort);
+ }
+ currentPort = this.clientPort;
+ nextTest();
+ do_test_finished();
+ },
+};
+
+function testsDone() {
+ dump("testsDone\n");
+ resetPrefs();
+}
+
+function doTest() {
+ dump("execute doTest " + origin + "\n");
+
+ var loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI;
+ if (anonymous) {
+ loadFlags |= Ci.nsIRequest.LOAD_ANONYMOUS;
+ }
+ anonymous = false;
+ if (forceReload) {
+ loadFlags |= Ci.nsIRequest.LOAD_FRESH_CONNECTION;
+ }
+ forceReload = false;
+
+ var chan = makeChan(origin);
+ chan.loadFlags = loadFlags;
+
+ var listener = new Listener();
+ chan.asyncOpen(listener);
+}
+
+function doTest1() {
+ dump("doTest1()\n");
+ origin = "https://foo.example.com:" + h2Port + "/origin-1";
+ nextTest = doTest2;
+ nextPortExpectedToBeSame = false;
+ do_test_pending();
+ doTest();
+}
+
+function doTest2() {
+ // Run the same test as above to make sure connection is marked experienced.
+ dump("doTest2()\n");
+ origin = "https://foo.example.com:" + h2Port + "/origin-1";
+ nextTest = doTest3;
+ nextPortExpectedToBeSame = true;
+ do_test_pending();
+ doTest();
+}
+
+function doTest3() {
+ // connection expected to be reused for an anonymous request
+ dump("doTest3()\n");
+ origin = "https://foo.example.com:" + h2Port + "/origin-2";
+ nextTest = doTest4;
+ nextPortExpectedToBeSame = true;
+ anonymous = true;
+ do_test_pending();
+ doTest();
+}
+
+function doTest4() {
+ dump("doTest4()\n");
+ origin = "https://foo.example.com:" + h2Port + "/origin-3";
+ nextTest = doTest5;
+ nextPortExpectedToBeSame = false;
+ forceReload = true;
+ anonymous = true;
+ do_test_pending();
+ doTest();
+}
+
+function doTest5() {
+ // Run the same test as above just without forceReload to make sure connection
+ // is marked experienced.
+ dump("doTest5()\n");
+ origin = "https://foo.example.com:" + h2Port + "/origin-3";
+ nextTest = doTest6;
+ nextPortExpectedToBeSame = true;
+ anonymous = true;
+ do_test_pending();
+ doTest();
+}
+
+function doTest6() {
+ dump("doTest6()\n");
+ origin = "https://foo.example.com:" + h2Port + "/origin-4";
+ nextTest = testsDone;
+ nextPortExpectedToBeSame = true;
+ do_test_pending();
+ doTest();
+}
diff --git a/netwerk/test/unit/test_auth_dialog_permission.js b/netwerk/test/unit/test_auth_dialog_permission.js
new file mode 100644
index 0000000000..18a9c8286b
--- /dev/null
+++ b/netwerk/test/unit/test_auth_dialog_permission.js
@@ -0,0 +1,283 @@
+// This file tests authentication prompt depending on pref
+// network.auth.subresource-http-auth-allow:
+// 0 - don't allow sub-resources to open HTTP authentication credentials
+// dialogs
+// 1 - allow sub-resources to open HTTP authentication credentials dialogs,
+// but don't allow it for cross-origin sub-resources
+// 2 - allow the cross-origin authentication as well.
+
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+var prefs = Cc["@mozilla.org/preferences-service;1"].getService(
+ Ci.nsIPrefBranch
+);
+
+// Since this test creates a TYPE_DOCUMENT channel via javascript, it will
+// end up using the wrong LoadInfo constructor. Setting this pref will disable
+// the ContentPolicyType assertion in the constructor.
+prefs.setBoolPref("network.loadinfo.skip_type_assertion", true);
+
+function authHandler(metadata, response) {
+ // btoa("guest:guest"), but that function is not available here
+ var expectedHeader = "Basic Z3Vlc3Q6Z3Vlc3Q=";
+
+ var body;
+ if (
+ metadata.hasHeader("Authorization") &&
+ metadata.getHeader("Authorization") == expectedHeader
+ ) {
+ response.setStatusLine(metadata.httpVersion, 200, "OK, authorized");
+ response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false);
+
+ body = "success";
+ } else {
+ // didn't know guest:guest, failure
+ response.setStatusLine(metadata.httpVersion, 401, "Unauthorized");
+ response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false);
+
+ body = "failed";
+ }
+
+ response.bodyOutputStream.write(body, body.length);
+}
+
+var httpserv = new HttpServer();
+httpserv.registerPathHandler("/auth", authHandler);
+httpserv.start(-1);
+
+XPCOMUtils.defineLazyGetter(this, "URL", function() {
+ return "http://localhost:" + httpserv.identity.primaryPort;
+});
+
+XPCOMUtils.defineLazyGetter(this, "PORT", function() {
+ return httpserv.identity.primaryPort;
+});
+
+function AuthPrompt(promptExpected) {
+ this.promptExpected = promptExpected;
+}
+
+AuthPrompt.prototype = {
+ user: "guest",
+ pass: "guest",
+
+ QueryInterface: ChromeUtils.generateQI(["nsIAuthPrompt"]),
+
+ prompt(title, text, realm, save, defaultText, result) {
+ do_throw("unexpected prompt call");
+ },
+
+ promptUsernameAndPassword(title, text, realm, savePW, user, pw) {
+ Assert.ok(this.promptExpected, "Not expected the authentication prompt.");
+
+ user.value = this.user;
+ pw.value = this.pass;
+ return true;
+ },
+
+ promptPassword(title, text, realm, save, pwd) {
+ do_throw("unexpected promptPassword call");
+ },
+};
+
+function Requestor(promptExpected) {
+ this.promptExpected = promptExpected;
+}
+
+Requestor.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["nsIInterfaceRequestor"]),
+
+ getInterface(iid) {
+ if (iid.equals(Ci.nsIAuthPrompt)) {
+ this.prompter = new AuthPrompt(this.promptExpected);
+ return this.prompter;
+ }
+
+ throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE);
+ },
+
+ prompter: null,
+};
+
+function make_uri(url) {
+ var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
+ return ios.newURI(url);
+}
+
+function makeChan(loadingUrl, url, contentPolicy) {
+ var ssm = Cc["@mozilla.org/scriptsecuritymanager;1"].getService(
+ Ci.nsIScriptSecurityManager
+ );
+ var uri = make_uri(loadingUrl);
+ var principal = ssm.createContentPrincipal(uri, {});
+
+ return NetUtil.newChannel({
+ uri: url,
+ loadingPrincipal: principal,
+ securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_INHERITS_SEC_CONTEXT,
+ contentPolicyType: contentPolicy,
+ }).QueryInterface(Ci.nsIHttpChannel);
+}
+
+function Test(
+ subresource_http_auth_allow_pref,
+ loadingUri,
+ uri,
+ contentPolicy,
+ expectedCode
+) {
+ this._subresource_http_auth_allow_pref = subresource_http_auth_allow_pref;
+ this._loadingUri = loadingUri;
+ this._uri = uri;
+ this._contentPolicy = contentPolicy;
+ this._expectedCode = expectedCode;
+}
+
+Test.prototype = {
+ _subresource_http_auth_allow_pref: 1,
+ _loadingUri: null,
+ _uri: null,
+ _contentPolicy: Ci.nsIContentPolicy.TYPE_OTHER,
+ _expectedCode: 200,
+
+ onStartRequest(request) {
+ try {
+ if (!Components.isSuccessCode(request.status)) {
+ do_throw("Channel should have a success code!");
+ }
+
+ if (!(request instanceof Ci.nsIHttpChannel)) {
+ do_throw("Expecting an HTTP channel");
+ }
+
+ Assert.equal(request.responseStatus, this._expectedCode);
+ // The request should be succeeded iff we expect 200
+ Assert.equal(request.requestSucceeded, this._expectedCode == 200);
+ } catch (e) {
+ do_throw("Unexpected exception: " + e);
+ }
+
+ throw Components.Exception("", Cr.NS_ERROR_ABORT);
+ },
+
+ onDataAvailable(request, stream, offset, count) {
+ do_throw("Should not get any data!");
+ },
+
+ onStopRequest(request, status) {
+ Assert.equal(status, Cr.NS_ERROR_ABORT);
+
+ // Clear the auth cache.
+ Cc["@mozilla.org/network/http-auth-manager;1"]
+ .getService(Ci.nsIHttpAuthManager)
+ .clearAll();
+
+ do_timeout(0, run_next_test);
+ },
+
+ run() {
+ dump(
+ "Run test: " +
+ this._subresource_http_auth_allow_pref +
+ this._loadingUri +
+ this._uri +
+ this._contentPolicy +
+ this._expectedCode +
+ " \n"
+ );
+
+ prefs.setIntPref(
+ "network.auth.subresource-http-auth-allow",
+ this._subresource_http_auth_allow_pref
+ );
+ let chan = makeChan(this._loadingUri, this._uri, this._contentPolicy);
+ chan.notificationCallbacks = new Requestor(this._expectedCode == 200);
+ chan.asyncOpen(this);
+ },
+};
+
+var tests = [
+ // For the next 3 tests the preference is set to 2 - allow the cross-origin
+ // authentication as well.
+
+ // A cross-origin request.
+ new Test(
+ 2,
+ "http://example.com",
+ URL + "/auth",
+ Ci.nsIContentPolicy.TYPE_OTHER,
+ 200
+ ),
+ // A non cross-origin sub-resource request.
+ new Test(2, URL + "/", URL + "/auth", Ci.nsIContentPolicy.TYPE_OTHER, 200),
+ // A top level document.
+ new Test(
+ 2,
+ URL + "/auth",
+ URL + "/auth",
+ Ci.nsIContentPolicy.TYPE_DOCUMENT,
+ 200
+ ),
+
+ // For the next 3 tests the preference is set to 1 - allow sub-resources to
+ // open HTTP authentication credentials dialogs, but don't allow it for
+ // cross-origin sub-resources
+
+ // A cross-origin request.
+ new Test(
+ 1,
+ "http://example.com",
+ URL + "/auth",
+ Ci.nsIContentPolicy.TYPE_OTHER,
+ 401
+ ),
+ // A non cross-origin sub-resource request.
+ new Test(1, URL + "/", URL + "/auth", Ci.nsIContentPolicy.TYPE_OTHER, 200),
+ // A top level document.
+ new Test(
+ 1,
+ URL + "/auth",
+ URL + "/auth",
+ Ci.nsIContentPolicy.TYPE_DOCUMENT,
+ 200
+ ),
+
+ // For the next 3 tests the preference is set to 0 - don't allow sub-resources
+ // to open HTTP authentication credentials dialogs.
+
+ // A cross-origin request.
+ new Test(
+ 0,
+ "http://example.com",
+ URL + "/auth",
+ Ci.nsIContentPolicy.TYPE_OTHER,
+ 401
+ ),
+ // A sub-resource request.
+ new Test(0, URL + "/", URL + "/auth", Ci.nsIContentPolicy.TYPE_OTHER, 401),
+ // A top level request.
+ new Test(
+ 0,
+ URL + "/auth",
+ URL + "/auth",
+ Ci.nsIContentPolicy.TYPE_DOCUMENT,
+ 200
+ ),
+];
+
+function run_next_test() {
+ var nextTest = tests.shift();
+ if (!nextTest) {
+ httpserv.stop(do_test_finished);
+ return;
+ }
+
+ nextTest.run();
+}
+
+function run_test() {
+ do_test_pending();
+ run_next_test();
+}
diff --git a/netwerk/test/unit/test_auth_jar.js b/netwerk/test/unit/test_auth_jar.js
new file mode 100644
index 0000000000..75c0dd2c82
--- /dev/null
+++ b/netwerk/test/unit/test_auth_jar.js
@@ -0,0 +1,97 @@
+"use strict";
+
+function createURI(s) {
+ let service = Cc["@mozilla.org/network/io-service;1"].getService(
+ Ci.nsIIOService
+ );
+ return service.newURI(s);
+}
+
+function run_test() {
+ // Set up a profile.
+ do_get_profile();
+
+ var secMan = Cc["@mozilla.org/scriptsecuritymanager;1"].getService(
+ Ci.nsIScriptSecurityManager
+ );
+ const kURI1 = "http://example.com";
+ var app = secMan.createContentPrincipal(createURI(kURI1), {});
+ var appbrowser = secMan.createContentPrincipal(createURI(kURI1), {
+ inIsolatedMozBrowser: true,
+ });
+
+ var am = Cc["@mozilla.org/network/http-auth-manager;1"].getService(
+ Ci.nsIHttpAuthManager
+ );
+ am.setAuthIdentity(
+ "http",
+ "a.example.com",
+ -1,
+ "basic",
+ "realm",
+ "",
+ "example.com",
+ "user",
+ "pass",
+ false,
+ app
+ );
+ am.setAuthIdentity(
+ "http",
+ "a.example.com",
+ -1,
+ "basic",
+ "realm",
+ "",
+ "example.com",
+ "user3",
+ "pass3",
+ false,
+ appbrowser
+ );
+
+ Services.clearData.deleteDataFromOriginAttributesPattern({
+ inIsolatedMozBrowser: true,
+ });
+
+ var domain = { value: "" },
+ user = { value: "" },
+ pass = { value: "" };
+ try {
+ am.getAuthIdentity(
+ "http",
+ "a.example.com",
+ -1,
+ "basic",
+ "realm",
+ "",
+ domain,
+ user,
+ pass,
+ false,
+ appbrowser
+ );
+ Assert.equal(false, true); // no identity should be present
+ } catch (x) {
+ Assert.equal(domain.value, "");
+ Assert.equal(user.value, "");
+ Assert.equal(pass.value, "");
+ }
+
+ am.getAuthIdentity(
+ "http",
+ "a.example.com",
+ -1,
+ "basic",
+ "realm",
+ "",
+ domain,
+ user,
+ pass,
+ false,
+ app
+ );
+ Assert.equal(domain.value, "example.com");
+ Assert.equal(user.value, "user");
+ Assert.equal(pass.value, "pass");
+}
diff --git a/netwerk/test/unit/test_auth_proxy.js b/netwerk/test/unit/test_auth_proxy.js
new file mode 100644
index 0000000000..ab0dee0bdb
--- /dev/null
+++ b/netwerk/test/unit/test_auth_proxy.js
@@ -0,0 +1,465 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * This tests the automatic login to the proxy with password,
+ * if the password is stored and the browser is restarted.
+ *
+ * <copied from="test_authentication.js"/>
+ */
+
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+const FLAG_RETURN_FALSE = 1 << 0;
+const FLAG_WRONG_PASSWORD = 1 << 1;
+const FLAG_PREVIOUS_FAILED = 1 << 2;
+
+function AuthPrompt2(proxyFlags, hostFlags) {
+ this.proxyCred.flags = proxyFlags;
+ this.hostCred.flags = hostFlags;
+}
+AuthPrompt2.prototype = {
+ proxyCred: {
+ user: "proxy",
+ pass: "guest",
+ realmExpected: "intern",
+ flags: 0,
+ },
+ hostCred: { user: "host", pass: "guest", realmExpected: "extern", flags: 0 },
+
+ QueryInterface: ChromeUtils.generateQI(["nsIAuthPrompt2"]),
+
+ promptAuth: function ap2_promptAuth(channel, encryptionLevel, authInfo) {
+ try {
+ // never HOST and PROXY set at the same time in prompt
+ Assert.equal(
+ (authInfo.flags & Ci.nsIAuthInformation.AUTH_HOST) != 0,
+ (authInfo.flags & Ci.nsIAuthInformation.AUTH_PROXY) == 0
+ );
+
+ var isProxy = (authInfo.flags & Ci.nsIAuthInformation.AUTH_PROXY) != 0;
+ var cred = isProxy ? this.proxyCred : this.hostCred;
+
+ dump(
+ "with flags: " +
+ ((cred.flags & FLAG_WRONG_PASSWORD) != 0 ? "wrong password" : "") +
+ " " +
+ ((cred.flags & FLAG_PREVIOUS_FAILED) != 0 ? "previous failed" : "") +
+ " " +
+ ((cred.flags & FLAG_RETURN_FALSE) != 0 ? "return false" : "") +
+ "\n"
+ );
+
+ // PROXY properly set by necko (checked using realm)
+ Assert.equal(cred.realmExpected, authInfo.realm);
+
+ // PREVIOUS_FAILED properly set by necko
+ Assert.equal(
+ (cred.flags & FLAG_PREVIOUS_FAILED) != 0,
+ (authInfo.flags & Ci.nsIAuthInformation.PREVIOUS_FAILED) != 0
+ );
+
+ if (cred.flags & FLAG_RETURN_FALSE) {
+ cred.flags |= FLAG_PREVIOUS_FAILED;
+ cred.flags &= ~FLAG_RETURN_FALSE;
+ return false;
+ }
+
+ authInfo.username = cred.user;
+ if (cred.flags & FLAG_WRONG_PASSWORD) {
+ authInfo.password = cred.pass + ".wrong";
+ cred.flags |= FLAG_PREVIOUS_FAILED;
+ // Now clear the flag to avoid an infinite loop
+ cred.flags &= ~FLAG_WRONG_PASSWORD;
+ } else {
+ authInfo.password = cred.pass;
+ cred.flags &= ~FLAG_PREVIOUS_FAILED;
+ }
+ return true;
+ } catch (e) {
+ do_throw(e);
+ }
+ },
+
+ asyncPromptAuth: function ap2_async(
+ channel,
+ callback,
+ context,
+ encryptionLevel,
+ authInfo
+ ) {
+ try {
+ var me = this;
+ var allOverAndDead = false;
+ executeSoon(function() {
+ try {
+ if (allOverAndDead) {
+ throw "already canceled";
+ }
+ var ret = me.promptAuth(channel, encryptionLevel, authInfo);
+ if (!ret) {
+ callback.onAuthCancelled(context, true);
+ } else {
+ callback.onAuthAvailable(context, authInfo);
+ }
+ allOverAndDead = true;
+ } catch (e) {
+ do_throw(e);
+ }
+ });
+ return new Cancelable(function() {
+ if (allOverAndDead) {
+ throw "can't cancel, already ran";
+ }
+ callback.onAuthAvailable(context, authInfo);
+ allOverAndDead = true;
+ });
+ } catch (e) {
+ do_throw(e);
+ }
+ },
+};
+
+function Cancelable(onCancelFunc) {
+ this.onCancelFunc = onCancelFunc;
+}
+Cancelable.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["nsICancelable"]),
+ cancel: function cancel() {
+ try {
+ this.onCancelFunc();
+ } catch (e) {
+ do_throw(e);
+ }
+ },
+};
+
+function Requestor(proxyFlags, hostFlags) {
+ this.proxyFlags = proxyFlags;
+ this.hostFlags = hostFlags;
+}
+Requestor.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["nsIInterfaceRequestor"]),
+
+ getInterface: function requestor_gi(iid) {
+ if (iid.equals(Ci.nsIAuthPrompt)) {
+ dump("authprompt1 not implemented\n");
+ throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE);
+ }
+ if (iid.equals(Ci.nsIAuthPrompt2)) {
+ try {
+ // Allow the prompt to store state by caching it here
+ if (!this.prompt2) {
+ this.prompt2 = new AuthPrompt2(this.proxyFlags, this.hostFlags);
+ }
+ return this.prompt2;
+ } catch (e) {
+ do_throw(e);
+ }
+ }
+ throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE);
+ },
+
+ prompt2: null,
+};
+
+var listener = {
+ expectedCode: -1, // uninitialized
+
+ onStartRequest: function test_onStartR(request) {
+ try {
+ // Proxy auth cancellation return failures to avoid spoofing
+ if (
+ !Components.isSuccessCode(request.status) &&
+ this.expectedCode != 407
+ ) {
+ do_throw("Channel should have a success code!");
+ }
+
+ if (!(request instanceof Ci.nsIHttpChannel)) {
+ do_throw("Expecting an HTTP channel");
+ }
+
+ Assert.equal(this.expectedCode, request.responseStatus);
+ // If we expect 200, the request should have succeeded
+ Assert.equal(this.expectedCode == 200, request.requestSucceeded);
+
+ var cookie = "";
+ try {
+ cookie = request.getRequestHeader("Cookie");
+ } catch (e) {}
+ Assert.equal(cookie, "");
+ } catch (e) {
+ do_throw("Unexpected exception: " + e);
+ }
+
+ throw Components.Exception("", Cr.NS_ERROR_ABORT);
+ },
+
+ onDataAvailable: function test_ODA() {
+ do_throw("Should not get any data!");
+ },
+
+ onStopRequest: function test_onStopR(request, status) {
+ Assert.equal(status, Cr.NS_ERROR_ABORT);
+
+ if (current_test < tests.length - 1) {
+ // First, need to clear the auth cache
+ Cc["@mozilla.org/network/http-auth-manager;1"]
+ .getService(Ci.nsIHttpAuthManager)
+ .clearAll();
+
+ current_test++;
+ tests[current_test]();
+ } else {
+ do_test_pending();
+ httpserv.stop(do_test_finished);
+ }
+
+ do_test_finished();
+ },
+};
+
+function makeChan(url) {
+ if (!url) {
+ url = "http://somesite/";
+ }
+
+ return NetUtil.newChannel({
+ uri: url,
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+}
+
+var current_test = 0;
+var httpserv = null;
+
+function run_test() {
+ httpserv = new HttpServer();
+ httpserv.registerPathHandler("/", proxyAuthHandler);
+ httpserv.identity.add("http", "somesite", 80);
+ httpserv.start(-1);
+
+ Services.prefs.setCharPref("network.proxy.http", "localhost");
+ Services.prefs.setIntPref(
+ "network.proxy.http_port",
+ httpserv.identity.primaryPort
+ );
+ Services.prefs.setCharPref("network.proxy.no_proxies_on", "");
+ Services.prefs.setIntPref("network.proxy.type", 1);
+
+ // Turn off the authentication dialog blocking for this test.
+ Services.prefs.setIntPref("network.auth.subresource-http-auth-allow", 2);
+ Services.prefs.setBoolPref(
+ "network.auth.non-web-content-triggered-resources-http-auth-allow",
+ true
+ );
+
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("network.proxy.http");
+ Services.prefs.clearUserPref("network.proxy.http_port");
+ Services.prefs.clearUserPref("network.proxy.no_proxies_on");
+ Services.prefs.clearUserPref("network.proxy.type");
+ Services.prefs.clearUserPref("network.auth.subresource-http-auth-allow");
+ Services.prefs.clearUserPref(
+ "network.auth.non-web-content-triggered-resources-http-auth-allow"
+ );
+ });
+
+ tests[current_test]();
+}
+
+function test_proxy_returnfalse() {
+ dump("\ntest: proxy returnfalse\n");
+ var chan = makeChan();
+ chan.notificationCallbacks = new Requestor(FLAG_RETURN_FALSE, 0);
+ listener.expectedCode = 407; // Proxy Unauthorized
+ chan.asyncOpen(listener);
+
+ do_test_pending();
+}
+
+function test_proxy_wrongpw() {
+ dump("\ntest: proxy wrongpw\n");
+ var chan = makeChan();
+ chan.notificationCallbacks = new Requestor(FLAG_WRONG_PASSWORD, 0);
+ listener.expectedCode = 200; // Eventually OK
+ chan.asyncOpen(listener);
+ do_test_pending();
+}
+
+function test_all_ok() {
+ dump("\ntest: all ok\n");
+ var chan = makeChan();
+ chan.notificationCallbacks = new Requestor(0, 0);
+ listener.expectedCode = 200; // OK
+ chan.asyncOpen(listener);
+ do_test_pending();
+}
+
+function test_proxy_407_cookie() {
+ var chan = makeChan();
+ chan.notificationCallbacks = new Requestor(FLAG_RETURN_FALSE, 0);
+ chan.setRequestHeader("X-Set-407-Cookie", "1", false);
+ listener.expectedCode = 407; // Proxy Unauthorized
+ chan.asyncOpen(listener);
+
+ do_test_pending();
+}
+
+function test_proxy_200_cookie() {
+ var chan = makeChan();
+ chan.notificationCallbacks = new Requestor(0, 0);
+ chan.setRequestHeader("X-Set-407-Cookie", "1", false);
+ listener.expectedCode = 200; // OK
+ chan.asyncOpen(listener);
+ do_test_pending();
+}
+
+function test_host_returnfalse() {
+ dump("\ntest: host returnfalse\n");
+ var chan = makeChan();
+ chan.notificationCallbacks = new Requestor(0, FLAG_RETURN_FALSE);
+ listener.expectedCode = 401; // Host Unauthorized
+ chan.asyncOpen(listener);
+
+ do_test_pending();
+}
+
+function test_host_wrongpw() {
+ dump("\ntest: host wrongpw\n");
+ var chan = makeChan();
+ chan.notificationCallbacks = new Requestor(0, FLAG_WRONG_PASSWORD);
+ listener.expectedCode = 200; // Eventually OK
+ chan.asyncOpen(listener);
+ do_test_pending();
+}
+
+function test_proxy_wrongpw_host_wrongpw() {
+ dump("\ntest: proxy wrongpw, host wrongpw\n");
+ var chan = makeChan();
+ chan.notificationCallbacks = new Requestor(
+ FLAG_WRONG_PASSWORD,
+ FLAG_WRONG_PASSWORD
+ );
+ listener.expectedCode = 200; // OK
+ chan.asyncOpen(listener);
+ do_test_pending();
+}
+
+function test_proxy_wrongpw_host_returnfalse() {
+ dump("\ntest: proxy wrongpw, host return false\n");
+ var chan = makeChan();
+ chan.notificationCallbacks = new Requestor(
+ FLAG_WRONG_PASSWORD,
+ FLAG_RETURN_FALSE
+ );
+ listener.expectedCode = 401; // Host Unauthorized
+ chan.asyncOpen(listener);
+ do_test_pending();
+}
+
+var tests = [
+ test_proxy_returnfalse,
+ test_proxy_wrongpw,
+ test_all_ok,
+ test_proxy_407_cookie,
+ test_proxy_200_cookie,
+ test_host_returnfalse,
+ test_host_wrongpw,
+ test_proxy_wrongpw_host_wrongpw,
+ test_proxy_wrongpw_host_returnfalse,
+];
+
+// PATH HANDLERS
+
+// Proxy
+function proxyAuthHandler(metadata, response) {
+ try {
+ var realm = "intern";
+ // btoa("proxy:guest"), but that function is not available here
+ var expectedHeader = "Basic cHJveHk6Z3Vlc3Q=";
+
+ var body;
+ if (
+ metadata.hasHeader("Proxy-Authorization") &&
+ metadata.getHeader("Proxy-Authorization") == expectedHeader
+ ) {
+ dump("proxy password ok\n");
+ response.setHeader(
+ "Proxy-Authenticate",
+ 'Basic realm="' + realm + '"',
+ false
+ );
+
+ hostAuthHandler(metadata, response);
+ } else {
+ dump("proxy password required\n");
+ response.setStatusLine(
+ metadata.httpVersion,
+ 407,
+ "Unauthorized by HTTP proxy"
+ );
+ response.setHeader(
+ "Proxy-Authenticate",
+ 'Basic realm="' + realm + '"',
+ false
+ );
+ if (metadata.hasHeader("X-Set-407-Cookie")) {
+ response.setHeader("Set-Cookie", "chewy", false);
+ }
+ body = "failed";
+ response.bodyOutputStream.write(body, body.length);
+ }
+ } catch (e) {
+ do_throw(e);
+ }
+}
+
+// Host /auth
+function hostAuthHandler(metadata, response) {
+ try {
+ var realm = "extern";
+ // btoa("host:guest"), but that function is not available here
+ var expectedHeader = "Basic aG9zdDpndWVzdA==";
+
+ var body;
+ if (
+ metadata.hasHeader("Authorization") &&
+ metadata.getHeader("Authorization") == expectedHeader
+ ) {
+ dump("host password ok\n");
+ response.setStatusLine(
+ metadata.httpVersion,
+ 200,
+ "OK, authorized for host"
+ );
+ response.setHeader(
+ "WWW-Authenticate",
+ 'Basic realm="' + realm + '"',
+ false
+ );
+ body = "success";
+ } else {
+ dump("host password required\n");
+ response.setStatusLine(
+ metadata.httpVersion,
+ 401,
+ "Unauthorized by HTTP server host"
+ );
+ response.setHeader(
+ "WWW-Authenticate",
+ 'Basic realm="' + realm + '"',
+ false
+ );
+ body = "failed";
+ }
+ response.bodyOutputStream.write(body, body.length);
+ } catch (e) {
+ do_throw(e);
+ }
+}
diff --git a/netwerk/test/unit/test_authentication.js b/netwerk/test/unit/test_authentication.js
new file mode 100644
index 0000000000..b49b0569b9
--- /dev/null
+++ b/netwerk/test/unit/test_authentication.js
@@ -0,0 +1,754 @@
+// This file tests authentication prompt callbacks
+// TODO NIT use do_check_eq(expected, actual) consistently, not sometimes eq(actual, expected)
+
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+// Turn off the authentication dialog blocking for this test.
+var prefs = Cc["@mozilla.org/preferences-service;1"].getService(
+ Ci.nsIPrefBranch
+);
+prefs.setIntPref("network.auth.subresource-http-auth-allow", 2);
+
+XPCOMUtils.defineLazyGetter(this, "URL", function() {
+ return "http://localhost:" + httpserv.identity.primaryPort;
+});
+
+XPCOMUtils.defineLazyGetter(this, "PORT", function() {
+ return httpserv.identity.primaryPort;
+});
+
+const FLAG_RETURN_FALSE = 1 << 0;
+const FLAG_WRONG_PASSWORD = 1 << 1;
+const FLAG_BOGUS_USER = 1 << 2;
+const FLAG_PREVIOUS_FAILED = 1 << 3;
+const CROSS_ORIGIN = 1 << 4;
+const FLAG_NO_REALM = 1 << 5;
+const FLAG_NON_ASCII_USER_PASSWORD = 1 << 6;
+
+const nsIAuthPrompt2 = Ci.nsIAuthPrompt2;
+const nsIAuthInformation = Ci.nsIAuthInformation;
+
+function AuthPrompt1(flags) {
+ this.flags = flags;
+}
+
+AuthPrompt1.prototype = {
+ user: "guest",
+ pass: "guest",
+
+ expectedRealm: "secret",
+
+ QueryInterface: ChromeUtils.generateQI(["nsIAuthPrompt"]),
+
+ prompt: function ap1_prompt(title, text, realm, save, defaultText, result) {
+ do_throw("unexpected prompt call");
+ },
+
+ promptUsernameAndPassword: function ap1_promptUP(
+ title,
+ text,
+ realm,
+ savePW,
+ user,
+ pw
+ ) {
+ if (this.flags & FLAG_NO_REALM) {
+ // Note that the realm here isn't actually the realm. it's a pw mgr key.
+ Assert.equal(URL + " (" + this.expectedRealm + ")", realm);
+ }
+ if (!(this.flags & CROSS_ORIGIN)) {
+ if (!text.includes(this.expectedRealm)) {
+ do_throw("Text must indicate the realm");
+ }
+ } else if (text.includes(this.expectedRealm)) {
+ do_throw("There should not be realm for cross origin");
+ }
+ if (!text.includes("localhost")) {
+ do_throw("Text must indicate the hostname");
+ }
+ if (!text.includes(String(PORT))) {
+ do_throw("Text must indicate the port");
+ }
+ if (text.includes("-1")) {
+ do_throw("Text must contain negative numbers");
+ }
+
+ if (this.flags & FLAG_RETURN_FALSE) {
+ return false;
+ }
+
+ if (this.flags & FLAG_BOGUS_USER) {
+ this.user = "foo\nbar";
+ } else if (this.flags & FLAG_NON_ASCII_USER_PASSWORD) {
+ this.user = "é";
+ }
+
+ user.value = this.user;
+ if (this.flags & FLAG_WRONG_PASSWORD) {
+ pw.value = this.pass + ".wrong";
+ // Now clear the flag to avoid an infinite loop
+ this.flags &= ~FLAG_WRONG_PASSWORD;
+ } else if (this.flags & FLAG_NON_ASCII_USER_PASSWORD) {
+ pw.value = "é";
+ } else {
+ pw.value = this.pass;
+ }
+ return true;
+ },
+
+ promptPassword: function ap1_promptPW(title, text, realm, save, pwd) {
+ do_throw("unexpected promptPassword call");
+ },
+};
+
+function AuthPrompt2(flags) {
+ this.flags = flags;
+}
+
+AuthPrompt2.prototype = {
+ user: "guest",
+ pass: "guest",
+
+ expectedRealm: "secret",
+
+ QueryInterface: ChromeUtils.generateQI(["nsIAuthPrompt2"]),
+
+ promptAuth: function ap2_promptAuth(channel, level, authInfo) {
+ var isNTLM = channel.URI.pathQueryRef.includes("ntlm");
+ var isDigest = channel.URI.pathQueryRef.includes("digest");
+
+ if (isNTLM || this.flags & FLAG_NO_REALM) {
+ this.expectedRealm = ""; // NTLM knows no realms
+ }
+
+ Assert.equal(this.expectedRealm, authInfo.realm);
+
+ var expectedLevel =
+ isNTLM || isDigest
+ ? nsIAuthPrompt2.LEVEL_PW_ENCRYPTED
+ : nsIAuthPrompt2.LEVEL_NONE;
+ Assert.equal(expectedLevel, level);
+
+ var expectedFlags = nsIAuthInformation.AUTH_HOST;
+
+ if (this.flags & FLAG_PREVIOUS_FAILED) {
+ expectedFlags |= nsIAuthInformation.PREVIOUS_FAILED;
+ }
+
+ if (this.flags & CROSS_ORIGIN) {
+ expectedFlags |= nsIAuthInformation.CROSS_ORIGIN_SUB_RESOURCE;
+ }
+
+ if (isNTLM) {
+ expectedFlags |= nsIAuthInformation.NEED_DOMAIN;
+ }
+
+ const kAllKnownFlags = 127; // Don't fail test for newly added flags
+ Assert.equal(expectedFlags, authInfo.flags & kAllKnownFlags);
+
+ var expectedScheme = isNTLM ? "ntlm" : isDigest ? "digest" : "basic";
+ Assert.equal(expectedScheme, authInfo.authenticationScheme);
+
+ // No passwords in the URL -> nothing should be prefilled
+ Assert.equal(authInfo.username, "");
+ Assert.equal(authInfo.password, "");
+ Assert.equal(authInfo.domain, "");
+
+ if (this.flags & FLAG_RETURN_FALSE) {
+ this.flags |= FLAG_PREVIOUS_FAILED;
+ return false;
+ }
+
+ if (this.flags & FLAG_BOGUS_USER) {
+ this.user = "foo\nbar";
+ } else if (this.flags & FLAG_NON_ASCII_USER_PASSWORD) {
+ this.user = "é";
+ }
+
+ authInfo.username = this.user;
+ if (this.flags & FLAG_WRONG_PASSWORD) {
+ authInfo.password = this.pass + ".wrong";
+ this.flags |= FLAG_PREVIOUS_FAILED;
+ // Now clear the flag to avoid an infinite loop
+ this.flags &= ~FLAG_WRONG_PASSWORD;
+ } else if (this.flags & FLAG_NON_ASCII_USER_PASSWORD) {
+ authInfo.password = "é";
+ } else {
+ authInfo.password = this.pass;
+ this.flags &= ~FLAG_PREVIOUS_FAILED;
+ }
+ return true;
+ },
+
+ asyncPromptAuth: function ap2_async(chan, cb, ctx, lvl, info) {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
+ },
+};
+
+function Requestor(flags, versions) {
+ this.flags = flags;
+ this.versions = versions;
+}
+
+Requestor.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["nsIInterfaceRequestor"]),
+
+ getInterface: function requestor_gi(iid) {
+ if (this.versions & 1 && iid.equals(Ci.nsIAuthPrompt)) {
+ // Allow the prompt to store state by caching it here
+ if (!this.prompt1) {
+ this.prompt1 = new AuthPrompt1(this.flags);
+ }
+ return this.prompt1;
+ }
+ if (this.versions & 2 && iid.equals(Ci.nsIAuthPrompt2)) {
+ // Allow the prompt to store state by caching it here
+ if (!this.prompt2) {
+ this.prompt2 = new AuthPrompt2(this.flags);
+ }
+ return this.prompt2;
+ }
+
+ throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE);
+ },
+
+ prompt1: null,
+ prompt2: null,
+};
+
+function RealmTestRequestor() {}
+
+RealmTestRequestor.prototype = {
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIInterfaceRequestor",
+ "nsIAuthPrompt2",
+ ]),
+
+ getInterface: function realmtest_interface(iid) {
+ if (iid.equals(Ci.nsIAuthPrompt2)) {
+ return this;
+ }
+
+ throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE);
+ },
+
+ promptAuth: function realmtest_checkAuth(channel, level, authInfo) {
+ Assert.equal(authInfo.realm, '"foo_bar');
+
+ return false;
+ },
+
+ asyncPromptAuth: function realmtest_async(chan, cb, ctx, lvl, info) {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
+ },
+};
+
+var listener = {
+ expectedCode: -1, // Uninitialized
+
+ onStartRequest: function test_onStartR(request) {
+ try {
+ if (!Components.isSuccessCode(request.status)) {
+ do_throw("Channel should have a success code!");
+ }
+
+ if (!(request instanceof Ci.nsIHttpChannel)) {
+ do_throw("Expecting an HTTP channel");
+ }
+
+ Assert.equal(request.responseStatus, this.expectedCode);
+ // The request should be succeeded iff we expect 200
+ Assert.equal(request.requestSucceeded, this.expectedCode == 200);
+ } catch (e) {
+ do_throw("Unexpected exception: " + e);
+ }
+
+ throw Components.Exception("", Cr.NS_ERROR_ABORT);
+ },
+
+ onDataAvailable: function test_ODA() {
+ do_throw("Should not get any data!");
+ },
+
+ onStopRequest: function test_onStopR(request, status) {
+ Assert.equal(status, Cr.NS_ERROR_ABORT);
+
+ moveToNextTest();
+ },
+};
+
+function makeChan(url, loadingUrl) {
+ var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
+ var ssm = Cc["@mozilla.org/scriptsecuritymanager;1"].getService(
+ Ci.nsIScriptSecurityManager
+ );
+ var principal = ssm.createContentPrincipal(ios.newURI(loadingUrl), {});
+ return NetUtil.newChannel({
+ uri: url,
+ loadingPrincipal: principal,
+ securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER,
+ });
+}
+
+var tests = [
+ test_noauth,
+ test_returnfalse1,
+ test_wrongpw1,
+ test_prompt1,
+ test_prompt1CrossOrigin,
+ test_prompt2CrossOrigin,
+ test_returnfalse2,
+ test_wrongpw2,
+ test_prompt2,
+ test_ntlm,
+ test_basicrealm,
+ test_nonascii,
+ test_digest_noauth,
+ test_digest,
+ test_digest_bogus_user,
+ test_short_digest,
+ test_large_realm,
+ test_large_domain,
+ test_nonascii_xhr,
+];
+
+var current_test = 0;
+
+var httpserv = null;
+
+function moveToNextTest() {
+ if (current_test < tests.length - 1) {
+ // First, gotta clear the auth cache
+ Cc["@mozilla.org/network/http-auth-manager;1"]
+ .getService(Ci.nsIHttpAuthManager)
+ .clearAll();
+
+ current_test++;
+ tests[current_test]();
+ } else {
+ do_test_pending();
+ httpserv.stop(do_test_finished);
+ }
+
+ do_test_finished();
+}
+
+function run_test() {
+ httpserv = new HttpServer();
+
+ httpserv.registerPathHandler("/auth", authHandler);
+ httpserv.registerPathHandler("/auth/ntlm/simple", authNtlmSimple);
+ httpserv.registerPathHandler("/auth/realm", authRealm);
+ httpserv.registerPathHandler("/auth/non_ascii", authNonascii);
+ httpserv.registerPathHandler("/auth/digest", authDigest);
+ httpserv.registerPathHandler("/auth/short_digest", authShortDigest);
+ httpserv.registerPathHandler("/largeRealm", largeRealm);
+ httpserv.registerPathHandler("/largeDomain", largeDomain);
+
+ httpserv.start(-1);
+
+ tests[0]();
+}
+
+function test_noauth() {
+ var chan = makeChan(URL + "/auth", URL);
+
+ listener.expectedCode = 401; // Unauthorized
+ chan.asyncOpen(listener);
+
+ do_test_pending();
+}
+
+function test_returnfalse1() {
+ var chan = makeChan(URL + "/auth", URL);
+
+ chan.notificationCallbacks = new Requestor(FLAG_RETURN_FALSE, 1);
+ listener.expectedCode = 401; // Unauthorized
+ chan.asyncOpen(listener);
+
+ do_test_pending();
+}
+
+function test_wrongpw1() {
+ var chan = makeChan(URL + "/auth", URL);
+
+ chan.notificationCallbacks = new Requestor(FLAG_WRONG_PASSWORD, 1);
+ listener.expectedCode = 200; // OK
+ chan.asyncOpen(listener);
+
+ do_test_pending();
+}
+
+function test_prompt1() {
+ var chan = makeChan(URL + "/auth", URL);
+
+ chan.notificationCallbacks = new Requestor(0, 1);
+ listener.expectedCode = 200; // OK
+ chan.asyncOpen(listener);
+
+ do_test_pending();
+}
+
+function test_prompt1CrossOrigin() {
+ var chan = makeChan(URL + "/auth", "http://example.org");
+
+ chan.notificationCallbacks = new Requestor(16, 1);
+ listener.expectedCode = 200; // OK
+ chan.asyncOpen(listener);
+
+ do_test_pending();
+}
+
+function test_prompt2CrossOrigin() {
+ var chan = makeChan(URL + "/auth", "http://example.org");
+
+ chan.notificationCallbacks = new Requestor(16, 2);
+ listener.expectedCode = 200; // OK
+ chan.asyncOpen(listener);
+
+ do_test_pending();
+}
+
+function test_returnfalse2() {
+ var chan = makeChan(URL + "/auth", URL);
+
+ chan.notificationCallbacks = new Requestor(FLAG_RETURN_FALSE, 2);
+ listener.expectedCode = 401; // Unauthorized
+ chan.asyncOpen(listener);
+
+ do_test_pending();
+}
+
+function test_wrongpw2() {
+ var chan = makeChan(URL + "/auth", URL);
+
+ chan.notificationCallbacks = new Requestor(FLAG_WRONG_PASSWORD, 2);
+ listener.expectedCode = 200; // OK
+ chan.asyncOpen(listener);
+
+ do_test_pending();
+}
+
+function test_prompt2() {
+ var chan = makeChan(URL + "/auth", URL);
+
+ chan.notificationCallbacks = new Requestor(0, 2);
+ listener.expectedCode = 200; // OK
+ chan.asyncOpen(listener);
+
+ do_test_pending();
+}
+
+function test_ntlm() {
+ var chan = makeChan(URL + "/auth/ntlm/simple", URL);
+
+ chan.notificationCallbacks = new Requestor(FLAG_RETURN_FALSE, 2);
+ listener.expectedCode = 401; // Unauthorized
+ chan.asyncOpen(listener);
+
+ do_test_pending();
+}
+
+function test_basicrealm() {
+ var chan = makeChan(URL + "/auth/realm", URL);
+
+ chan.notificationCallbacks = new RealmTestRequestor();
+ listener.expectedCode = 401; // Unauthorized
+ chan.asyncOpen(listener);
+
+ do_test_pending();
+}
+
+function test_nonascii() {
+ var chan = makeChan(URL + "/auth/non_ascii", URL);
+
+ chan.notificationCallbacks = new Requestor(FLAG_NON_ASCII_USER_PASSWORD, 2);
+ listener.expectedCode = 200; // OK
+ chan.asyncOpen(listener);
+
+ do_test_pending();
+}
+
+function test_nonascii_xhr() {
+ var xhr = new XMLHttpRequest();
+ xhr.open("GET", URL + "/auth/non_ascii", true, "é", "é");
+ xhr.onreadystatechange = function(event) {
+ if (xhr.readyState == 4) {
+ Assert.equal(xhr.status, 200);
+ moveToNextTest();
+ xhr.onreadystatechange = null;
+ }
+ };
+ xhr.send(null);
+
+ do_test_pending();
+}
+
+function test_digest_noauth() {
+ var chan = makeChan(URL + "/auth/digest", URL);
+
+ //chan.notificationCallbacks = new Requestor(FLAG_RETURN_FALSE, 2);
+ listener.expectedCode = 401; // Unauthorized
+ chan.asyncOpen(listener);
+
+ do_test_pending();
+}
+
+function test_digest() {
+ var chan = makeChan(URL + "/auth/digest", URL);
+
+ chan.notificationCallbacks = new Requestor(0, 2);
+ listener.expectedCode = 200; // OK
+ chan.asyncOpen(listener);
+
+ do_test_pending();
+}
+
+function test_digest_bogus_user() {
+ var chan = makeChan(URL + "/auth/digest", URL);
+ chan.notificationCallbacks = new Requestor(FLAG_BOGUS_USER, 2);
+ listener.expectedCode = 401; // unauthorized
+ chan.asyncOpen(listener);
+
+ do_test_pending();
+}
+
+// Test header "WWW-Authenticate: Digest" - bug 1338876.
+function test_short_digest() {
+ var chan = makeChan(URL + "/auth/short_digest", URL);
+ chan.notificationCallbacks = new Requestor(FLAG_NO_REALM, 2);
+ listener.expectedCode = 401; // OK
+ chan.asyncOpen(listener);
+
+ do_test_pending();
+}
+
+// PATH HANDLERS
+
+// /auth
+function authHandler(metadata, response) {
+ // btoa("guest:guest"), but that function is not available here
+ var expectedHeader = "Basic Z3Vlc3Q6Z3Vlc3Q=";
+
+ var body;
+ if (
+ metadata.hasHeader("Authorization") &&
+ metadata.getHeader("Authorization") == expectedHeader
+ ) {
+ response.setStatusLine(metadata.httpVersion, 200, "OK, authorized");
+ response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false);
+
+ body = "success";
+ } else {
+ // didn't know guest:guest, failure
+ response.setStatusLine(metadata.httpVersion, 401, "Unauthorized");
+ response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false);
+
+ body = "failed";
+ }
+
+ response.bodyOutputStream.write(body, body.length);
+}
+
+// /auth/ntlm/simple
+function authNtlmSimple(metadata, response) {
+ response.setStatusLine(metadata.httpVersion, 401, "Unauthorized");
+ response.setHeader(
+ "WWW-Authenticate",
+ "NTLM" /* + ' realm="secret"' */,
+ false
+ );
+
+ var body =
+ "NOTE: This just sends an NTLM challenge, it never\n" +
+ "accepts the authentication. It also closes\n" +
+ "the connection after sending the challenge\n";
+
+ response.bodyOutputStream.write(body, body.length);
+}
+
+// /auth/realm
+function authRealm(metadata, response) {
+ response.setStatusLine(metadata.httpVersion, 401, "Unauthorized");
+ response.setHeader("WWW-Authenticate", 'Basic realm="\\"f\\oo_bar"', false);
+ var body = "success";
+
+ response.bodyOutputStream.write(body, body.length);
+}
+
+// /auth/nonAscii
+function authNonascii(metadata, response) {
+ // btoa("é:é"), but that function is not available here
+ var expectedHeader = "Basic w6k6w6k=";
+
+ var body;
+ if (
+ metadata.hasHeader("Authorization") &&
+ metadata.getHeader("Authorization") == expectedHeader
+ ) {
+ response.setStatusLine(metadata.httpVersion, 200, "OK, authorized");
+ response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false);
+
+ // Use correct XML syntax since this function is also used for testing XHR.
+ body = "<?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);
+}
+
+//
+// Digest functions
+//
+function bytesFromString(str) {
+ var converter = Cc[
+ "@mozilla.org/intl/scriptableunicodeconverter"
+ ].createInstance(Ci.nsIScriptableUnicodeConverter);
+ converter.charset = "UTF-8";
+ var data = converter.convertToByteArray(str);
+ return data;
+}
+
+// return the two-digit hexadecimal code for a byte
+function toHexString(charCode) {
+ return ("0" + charCode.toString(16)).slice(-2);
+}
+
+function H(str) {
+ var data = bytesFromString(str);
+ var ch = Cc["@mozilla.org/security/hash;1"].createInstance(Ci.nsICryptoHash);
+ ch.init(Ci.nsICryptoHash.MD5);
+ ch.update(data, data.length);
+ var hash = ch.finish(false);
+ return Array.from(hash, (c, i) => toHexString(hash.charCodeAt(i))).join("");
+}
+
+//
+// Digest handler
+//
+// /auth/digest
+function authDigest(metadata, response) {
+ var nonce = "6f93719059cf8d568005727f3250e798";
+ var opaque = "1234opaque1234";
+ var cnonceRE = /cnonce="(\w+)"/;
+ var responseRE = /response="(\w+)"/;
+ var usernameRE = /username="(\w+)"/;
+ var authenticate =
+ 'Digest realm="secret", domain="/", qop=auth,' +
+ 'algorithm=MD5, nonce="' +
+ nonce +
+ '" opaque="' +
+ opaque +
+ '"';
+ var body;
+ // check creds if we have them
+ if (metadata.hasHeader("Authorization")) {
+ var auth = metadata.getHeader("Authorization");
+ var cnonce = auth.match(cnonceRE)[1];
+ var clientDigest = auth.match(responseRE)[1];
+ var username = auth.match(usernameRE)[1];
+ var nc = "00000001";
+
+ if (username != "guest") {
+ response.setStatusLine(metadata.httpVersion, 400, "bad request");
+ body = "should never get here";
+ } else {
+ // see RFC2617 for the description of this calculation
+ var A1 = "guest:secret:guest";
+ var A2 = "GET:/auth/digest";
+ var noncebits = [nonce, nc, cnonce, "auth", H(A2)].join(":");
+ var digest = H([H(A1), noncebits].join(":"));
+
+ if (clientDigest == digest) {
+ response.setStatusLine(metadata.httpVersion, 200, "OK, authorized");
+ body = "success";
+ } else {
+ response.setStatusLine(metadata.httpVersion, 401, "Unauthorized");
+ response.setHeader("WWW-Authenticate", authenticate, false);
+ body = "auth failed";
+ }
+ }
+ } else {
+ // no header, send one
+ response.setStatusLine(metadata.httpVersion, 401, "Unauthorized");
+ response.setHeader("WWW-Authenticate", authenticate, false);
+ body = "failed, no header";
+ }
+
+ response.bodyOutputStream.write(body, body.length);
+}
+
+function authShortDigest(metadata, response) {
+ // no header, send one
+ response.setStatusLine(metadata.httpVersion, 401, "Unauthorized");
+ response.setHeader("WWW-Authenticate", "Digest", false);
+}
+
+let buildLargePayload = (function() {
+ let size = 33 * 1024;
+ let ret = "";
+ return function() {
+ // Return cached value.
+ if (ret.length > 0) {
+ return ret;
+ }
+ for (let i = 0; i < size; i++) {
+ ret += "a";
+ }
+ return ret;
+ };
+})();
+
+function largeRealm(metadata, response) {
+ // test > 32KB realm tokens
+ var body;
+
+ response.setStatusLine(metadata.httpVersion, 401, "Unauthorized");
+ response.setHeader(
+ "WWW-Authenticate",
+ 'Digest realm="' + buildLargePayload() + '", domain="foo"'
+ );
+
+ body = "need to authenticate";
+ response.bodyOutputStream.write(body, body.length);
+}
+
+function largeDomain(metadata, response) {
+ // test > 32KB domain tokens
+ var body;
+
+ response.setStatusLine(metadata.httpVersion, 401, "Unauthorized");
+ response.setHeader(
+ "WWW-Authenticate",
+ 'Digest realm="foo", domain="' + buildLargePayload() + '"'
+ );
+
+ body = "need to authenticate";
+ response.bodyOutputStream.write(body, body.length);
+}
+
+function test_large_realm() {
+ var chan = makeChan(URL + "/largeRealm", URL);
+
+ listener.expectedCode = 401; // Unauthorized
+ chan.asyncOpen(listener);
+
+ do_test_pending();
+}
+
+function test_large_domain() {
+ var chan = makeChan(URL + "/largeDomain", URL);
+
+ listener.expectedCode = 401; // Unauthorized
+ chan.asyncOpen(listener);
+
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_authpromptwrapper.js b/netwerk/test/unit/test_authpromptwrapper.js
new file mode 100644
index 0000000000..91068dc692
--- /dev/null
+++ b/netwerk/test/unit/test_authpromptwrapper.js
@@ -0,0 +1,233 @@
+// NOTE: This tests code outside of Necko. The test still lives here because
+// the contract is part of Necko.
+
+// TODO:
+// - HTTPS
+// - Proxies
+
+"use strict";
+
+const nsIAuthInformation = Ci.nsIAuthInformation;
+const nsIAuthPromptAdapterFactory = Ci.nsIAuthPromptAdapterFactory;
+
+function run_test() {
+ const contractID = "@mozilla.org/network/authprompt-adapter-factory;1";
+ if (!(contractID in Cc)) {
+ print("No adapter factory found, skipping testing");
+ return;
+ }
+ var adapter = Cc[contractID].getService();
+ Assert.equal(adapter instanceof nsIAuthPromptAdapterFactory, true);
+
+ // NOTE: xpconnect lets us get away with passing an empty object here
+ // For this part of the test, we only care that this function returns
+ // success
+ Assert.notEqual(adapter.createAdapter({}), null);
+
+ const host = "www.mozilla.org";
+
+ var info = {
+ username: "",
+ password: "",
+ domain: "",
+
+ flags: nsIAuthInformation.AUTH_HOST,
+ authenticationScheme: "basic",
+ realm: "secretrealm",
+ };
+
+ const CALLED_PROMPT = 1 << 0;
+ const CALLED_PROMPTUP = 1 << 1;
+ const CALLED_PROMPTP = 1 << 2;
+ function Prompt1() {}
+ Prompt1.prototype = {
+ called: 0,
+ rv: true,
+
+ user: "foo\\bar",
+ pw: "bar",
+
+ scheme: "http",
+
+ QueryInterface: ChromeUtils.generateQI(["nsIAuthPrompt"]),
+
+ prompt: function ap1_prompt(title, text, realm, save, defaultText, result) {
+ this.called |= CALLED_PROMPT;
+ this.doChecks(text, realm);
+ return this.rv;
+ },
+
+ promptUsernameAndPassword: function ap1_promptUP(
+ title,
+ text,
+ realm,
+ savePW,
+ user,
+ pw
+ ) {
+ this.called |= CALLED_PROMPTUP;
+ this.doChecks(text, realm);
+ user.value = this.user;
+ pw.value = this.pw;
+ return this.rv;
+ },
+
+ promptPassword: function ap1_promptPW(title, text, realm, save, pwd) {
+ this.called |= CALLED_PROMPTP;
+ this.doChecks(text, realm);
+ pwd.value = this.pw;
+ return this.rv;
+ },
+
+ doChecks: function ap1_check(text, realm) {
+ Assert.equal(this.scheme + "://" + host + " (" + info.realm + ")", realm);
+
+ Assert.notEqual(text.indexOf(host), -1);
+ if (info.flags & nsIAuthInformation.ONLY_PASSWORD) {
+ // Should have the username in the text
+ Assert.notEqual(text.indexOf(info.username), -1);
+ } else {
+ // Make sure that we show the realm if we have one and that we don't
+ // show "" otherwise
+ if (info.realm != "") {
+ Assert.notEqual(text.indexOf(info.realm), -1);
+ } else {
+ Assert.equal(text.indexOf('""'), -1);
+ }
+ // No explicit port in the URL; message should not contain -1
+ // for those cases
+ Assert.equal(text.indexOf("-1"), -1);
+ }
+ },
+ };
+
+ // Also have to make up a channel
+ var uri = NetUtil.newURI("http://" + host);
+ var chan = NetUtil.newChannel({
+ uri,
+ loadUsingSystemPrincipal: true,
+ });
+
+ function do_tests(expectedRV) {
+ var prompt1;
+ var wrapper;
+
+ // 1: The simple case
+ prompt1 = new Prompt1();
+ prompt1.rv = expectedRV;
+ wrapper = adapter.createAdapter(prompt1);
+
+ var rv = wrapper.promptAuth(chan, 0, info);
+ Assert.equal(rv, prompt1.rv);
+ Assert.equal(prompt1.called, CALLED_PROMPTUP);
+
+ if (rv) {
+ Assert.equal(info.domain, "");
+ Assert.equal(info.username, prompt1.user);
+ Assert.equal(info.password, prompt1.pw);
+ }
+
+ info.domain = "";
+ info.username = "";
+ info.password = "";
+
+ // 2: Only ask for a PW
+ prompt1 = new Prompt1();
+ prompt1.rv = expectedRV;
+ info.flags |= nsIAuthInformation.ONLY_PASSWORD;
+
+ // Initialize the username so that the prompt can show it
+ info.username = prompt1.user;
+
+ wrapper = adapter.createAdapter(prompt1);
+ rv = wrapper.promptAuth(chan, 0, info);
+ Assert.equal(rv, prompt1.rv);
+ Assert.equal(prompt1.called, CALLED_PROMPTP);
+
+ if (rv) {
+ Assert.equal(info.domain, "");
+ Assert.equal(info.username, prompt1.user); // we initialized this
+ Assert.equal(info.password, prompt1.pw);
+ }
+
+ info.flags &= ~nsIAuthInformation.ONLY_PASSWORD;
+
+ info.domain = "";
+ info.username = "";
+ info.password = "";
+
+ // 3: user, pw and domain
+ prompt1 = new Prompt1();
+ prompt1.rv = expectedRV;
+ info.flags |= nsIAuthInformation.NEED_DOMAIN;
+
+ wrapper = adapter.createAdapter(prompt1);
+ rv = wrapper.promptAuth(chan, 0, info);
+ Assert.equal(rv, prompt1.rv);
+ Assert.equal(prompt1.called, CALLED_PROMPTUP);
+
+ if (rv) {
+ Assert.equal(info.domain, "foo");
+ Assert.equal(info.username, "bar");
+ Assert.equal(info.password, prompt1.pw);
+ }
+
+ info.flags &= ~nsIAuthInformation.NEED_DOMAIN;
+
+ info.domain = "";
+ info.username = "";
+ info.password = "";
+
+ // 4: username that doesn't contain a domain
+ prompt1 = new Prompt1();
+ prompt1.rv = expectedRV;
+ info.flags |= nsIAuthInformation.NEED_DOMAIN;
+
+ prompt1.user = "foo";
+
+ wrapper = adapter.createAdapter(prompt1);
+ rv = wrapper.promptAuth(chan, 0, info);
+ Assert.equal(rv, prompt1.rv);
+ Assert.equal(prompt1.called, CALLED_PROMPTUP);
+
+ if (rv) {
+ Assert.equal(info.domain, "");
+ Assert.equal(info.username, prompt1.user);
+ Assert.equal(info.password, prompt1.pw);
+ }
+
+ info.flags &= ~nsIAuthInformation.NEED_DOMAIN;
+
+ info.domain = "";
+ info.username = "";
+ info.password = "";
+
+ // 5: FTP
+ var uri2 = NetUtil.newURI("ftp://" + host);
+ var ftpchan = NetUtil.newChannel({
+ uri: uri2,
+ loadUsingSystemPrincipal: true,
+ });
+
+ prompt1 = new Prompt1();
+ prompt1.rv = expectedRV;
+ prompt1.scheme = "ftp";
+
+ wrapper = adapter.createAdapter(prompt1);
+ var rv = wrapper.promptAuth(ftpchan, 0, info);
+ Assert.equal(rv, prompt1.rv);
+ Assert.equal(prompt1.called, CALLED_PROMPTUP);
+
+ if (rv) {
+ Assert.equal(info.domain, "");
+ Assert.equal(info.username, prompt1.user);
+ Assert.equal(info.password, prompt1.pw);
+ }
+
+ info.domain = "";
+ info.username = "";
+ info.password = "";
+ }
+ do_tests(true);
+ do_tests(false);
+}
diff --git a/netwerk/test/unit/test_backgroundfilesaver.js b/netwerk/test/unit/test_backgroundfilesaver.js
new file mode 100644
index 0000000000..696884f356
--- /dev/null
+++ b/netwerk/test/unit/test_backgroundfilesaver.js
@@ -0,0 +1,775 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * This file tests components that implement nsIBackgroundFileSaver.
+ */
+
+////////////////////////////////////////////////////////////////////////////////
+//// Globals
+
+"use strict";
+
+ChromeUtils.defineModuleGetter(
+ this,
+ "FileUtils",
+ "resource://gre/modules/FileUtils.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ this,
+ "NetUtil",
+ "resource://gre/modules/NetUtil.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ this,
+ "FileTestUtils",
+ "resource://testing-common/FileTestUtils.jsm"
+);
+
+const BackgroundFileSaverOutputStream = Components.Constructor(
+ "@mozilla.org/network/background-file-saver;1?mode=outputstream",
+ "nsIBackgroundFileSaver"
+);
+
+const BackgroundFileSaverStreamListener = Components.Constructor(
+ "@mozilla.org/network/background-file-saver;1?mode=streamlistener",
+ "nsIBackgroundFileSaver"
+);
+
+const StringInputStream = Components.Constructor(
+ "@mozilla.org/io/string-input-stream;1",
+ "nsIStringInputStream",
+ "setData"
+);
+
+const REQUEST_SUSPEND_AT = 1024 * 1024 * 4;
+const TEST_DATA_SHORT = "This test string is written to the file.";
+const TEST_FILE_NAME_1 = "test-backgroundfilesaver-1.txt";
+const TEST_FILE_NAME_2 = "test-backgroundfilesaver-2.txt";
+const TEST_FILE_NAME_3 = "test-backgroundfilesaver-3.txt";
+
+// A map of test data length to the expected SHA-256 hashes
+const EXPECTED_HASHES = {
+ // No data
+ 0: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+ // TEST_DATA_SHORT
+ 40: "f37176b690e8744ee990a206c086cba54d1502aa2456c3b0c84ef6345d72a192",
+ // TEST_DATA_SHORT + TEST_DATA_SHORT
+ 80: "780c0e91f50bb7ec922cc11e16859e6d5df283c0d9470f61772e3d79f41eeb58",
+ // TEST_DATA_LONG
+ 4718592: "372cb9e5ce7b76d3e2a5042e78aa72dcf973e659a262c61b7ff51df74b36767b",
+ // TEST_DATA_LONG + TEST_DATA_LONG
+ 9437184: "693e4f8c6855a6fed4f5f9370d12cc53105672f3ff69783581e7d925984c41d3",
+};
+
+const gTextDecoder = new TextDecoder();
+
+// Generate a long string of data in a moderately fast way.
+const TEST_256_CHARS = new Array(257).join("-");
+const DESIRED_LENGTH = REQUEST_SUSPEND_AT * 1.125;
+const TEST_DATA_LONG = new Array(1 + DESIRED_LENGTH / 256).join(TEST_256_CHARS);
+Assert.equal(TEST_DATA_LONG.length, DESIRED_LENGTH);
+
+/**
+ * Returns a reference to a temporary file that is guaranteed not to exist and
+ * is cleaned up later. See FileTestUtils.getTempFile for details.
+ */
+function getTempFile(leafName) {
+ return FileTestUtils.getTempFile(leafName);
+}
+
+/**
+ * Helper function for converting a binary blob to its hex equivalent.
+ *
+ * @param str
+ * String possibly containing non-printable chars.
+ * @return A hex-encoded string.
+ */
+function toHex(str) {
+ var hex = "";
+ for (var i = 0; i < str.length; i++) {
+ hex += ("0" + str.charCodeAt(i).toString(16)).slice(-2);
+ }
+ return hex;
+}
+
+/**
+ * Ensures that the given file contents are equal to the given string.
+ *
+ * @param aFile
+ * nsIFile whose contents should be verified.
+ * @param aExpectedContents
+ * String containing the octets that are expected in the file.
+ *
+ * @return {Promise}
+ * @resolves When the operation completes.
+ * @rejects Never.
+ */
+function promiseVerifyContents(aFile, aExpectedContents) {
+ return new Promise(resolve => {
+ NetUtil.asyncFetch(
+ {
+ uri: NetUtil.newURI(aFile),
+ loadUsingSystemPrincipal: true,
+ },
+ function(aInputStream, aStatus) {
+ Assert.ok(Components.isSuccessCode(aStatus));
+ let contents = NetUtil.readInputStreamToString(
+ aInputStream,
+ aInputStream.available()
+ );
+ if (contents.length <= TEST_DATA_SHORT.length * 2) {
+ Assert.equal(contents, aExpectedContents);
+ } else {
+ // Do not print the entire content string to the test log.
+ Assert.equal(contents.length, aExpectedContents.length);
+ Assert.ok(contents == aExpectedContents);
+ }
+ resolve();
+ }
+ );
+ });
+}
+
+/**
+ * Waits for the given saver object to complete.
+ *
+ * @param aSaver
+ * The saver, with the output stream or a stream listener implementation.
+ * @param aOnTargetChangeFn
+ * Optional callback invoked with the target file name when it changes.
+ *
+ * @return {Promise}
+ * @resolves When onSaveComplete is called with a success code.
+ * @rejects With an exception, if onSaveComplete is called with a failure code.
+ */
+function promiseSaverComplete(aSaver, aOnTargetChangeFn) {
+ return new Promise((resolve, reject) => {
+ aSaver.observer = {
+ onTargetChange: function BFSO_onSaveComplete(aSaver, aTarget) {
+ if (aOnTargetChangeFn) {
+ aOnTargetChangeFn(aTarget);
+ }
+ },
+ onSaveComplete: function BFSO_onSaveComplete(aSaver, aStatus) {
+ if (Components.isSuccessCode(aStatus)) {
+ resolve();
+ } else {
+ reject(new Components.Exception("Saver failed.", aStatus));
+ }
+ },
+ };
+ });
+}
+
+/**
+ * Feeds a string to a BackgroundFileSaverOutputStream.
+ *
+ * @param aSourceString
+ * The source data to copy.
+ * @param aSaverOutputStream
+ * The BackgroundFileSaverOutputStream to feed.
+ * @param aCloseWhenDone
+ * If true, the output stream will be closed when the copy finishes.
+ *
+ * @return {Promise}
+ * @resolves When the copy completes with a success code.
+ * @rejects With an exception, if the copy fails.
+ */
+function promiseCopyToSaver(aSourceString, aSaverOutputStream, aCloseWhenDone) {
+ return new Promise((resolve, reject) => {
+ let inputStream = new StringInputStream(
+ aSourceString,
+ aSourceString.length
+ );
+ let copier = Cc[
+ "@mozilla.org/network/async-stream-copier;1"
+ ].createInstance(Ci.nsIAsyncStreamCopier);
+ copier.init(
+ inputStream,
+ aSaverOutputStream,
+ null,
+ false,
+ true,
+ 0x8000,
+ true,
+ aCloseWhenDone
+ );
+ copier.asyncCopy(
+ {
+ onStartRequest() {},
+ onStopRequest(aRequest, aStatusCode) {
+ if (Components.isSuccessCode(aStatusCode)) {
+ resolve();
+ } else {
+ reject(new Components.Exception(aStatusCode));
+ }
+ },
+ },
+ null
+ );
+ });
+}
+
+/**
+ * Feeds a string to a BackgroundFileSaverStreamListener.
+ *
+ * @param aSourceString
+ * The source data to copy.
+ * @param aSaverStreamListener
+ * The BackgroundFileSaverStreamListener to feed.
+ * @param aCloseWhenDone
+ * If true, the output stream will be closed when the copy finishes.
+ *
+ * @return {Promise}
+ * @resolves When the operation completes with a success code.
+ * @rejects With an exception, if the operation fails.
+ */
+function promisePumpToSaver(
+ aSourceString,
+ aSaverStreamListener,
+ aCloseWhenDone
+) {
+ return new Promise((resolve, reject) => {
+ aSaverStreamListener.QueryInterface(Ci.nsIStreamListener);
+ let inputStream = new StringInputStream(
+ aSourceString,
+ aSourceString.length
+ );
+ let pump = Cc["@mozilla.org/network/input-stream-pump;1"].createInstance(
+ Ci.nsIInputStreamPump
+ );
+ pump.init(inputStream, 0, 0, true);
+ pump.asyncRead({
+ onStartRequest: function PPTS_onStartRequest(aRequest) {
+ aSaverStreamListener.onStartRequest(aRequest);
+ },
+ onStopRequest: function PPTS_onStopRequest(aRequest, aStatusCode) {
+ aSaverStreamListener.onStopRequest(aRequest, aStatusCode);
+ if (Components.isSuccessCode(aStatusCode)) {
+ resolve();
+ } else {
+ reject(new Components.Exception(aStatusCode));
+ }
+ },
+ onDataAvailable: function PPTS_onDataAvailable(
+ aRequest,
+ aInputStream,
+ aOffset,
+ aCount
+ ) {
+ aSaverStreamListener.onDataAvailable(
+ aRequest,
+ aInputStream,
+ aOffset,
+ aCount
+ );
+ },
+ });
+ });
+}
+
+var gStillRunning = true;
+
+////////////////////////////////////////////////////////////////////////////////
+//// Tests
+
+add_task(function test_setup() {
+ // Wait 10 minutes, that is half of the external xpcshell timeout.
+ do_timeout(10 * 60 * 1000, function() {
+ if (gStillRunning) {
+ do_throw("Test timed out.");
+ }
+ });
+});
+
+add_task(async function test_normal() {
+ // This test demonstrates the most basic use case.
+ let destFile = getTempFile(TEST_FILE_NAME_1);
+
+ // Create the object implementing the output stream.
+ let saver = new BackgroundFileSaverOutputStream();
+
+ // Set up callbacks for completion and target file name change.
+ let receivedOnTargetChange = false;
+ function onTargetChange(aTarget) {
+ Assert.ok(destFile.equals(aTarget));
+ receivedOnTargetChange = true;
+ }
+ let completionPromise = promiseSaverComplete(saver, onTargetChange);
+
+ // Set the target file.
+ saver.setTarget(destFile, false);
+
+ // Write some data and close the output stream.
+ await promiseCopyToSaver(TEST_DATA_SHORT, saver, true);
+
+ // Indicate that we are ready to finish, and wait for a successful callback.
+ saver.finish(Cr.NS_OK);
+ await completionPromise;
+
+ // Only after we receive the completion notification, we can also be sure that
+ // we've received the target file name change notification before it.
+ Assert.ok(receivedOnTargetChange);
+
+ // Clean up.
+ destFile.remove(false);
+});
+
+add_task(async function test_combinations() {
+ let initialFile = getTempFile(TEST_FILE_NAME_1);
+ let renamedFile = getTempFile(TEST_FILE_NAME_2);
+
+ // Keep track of the current file.
+ let currentFile = null;
+ function onTargetChange(aTarget) {
+ currentFile = null;
+ info("Target file changed to: " + aTarget.leafName);
+ currentFile = aTarget;
+ }
+
+ // Tests various combinations of events and behaviors for both the stream
+ // listener and the output stream implementations.
+ for (let testFlags = 0; testFlags < 32; testFlags++) {
+ let keepPartialOnFailure = !!(testFlags & 1);
+ let renameAtSomePoint = !!(testFlags & 2);
+ let cancelAtSomePoint = !!(testFlags & 4);
+ let useStreamListener = !!(testFlags & 8);
+ let useLongData = !!(testFlags & 16);
+
+ let startTime = Date.now();
+ info(
+ "Starting keepPartialOnFailure = " +
+ keepPartialOnFailure +
+ ", renameAtSomePoint = " +
+ renameAtSomePoint +
+ ", cancelAtSomePoint = " +
+ cancelAtSomePoint +
+ ", useStreamListener = " +
+ useStreamListener +
+ ", useLongData = " +
+ useLongData
+ );
+
+ // Create the object and register the observers.
+ currentFile = null;
+ let saver = useStreamListener
+ ? new BackgroundFileSaverStreamListener()
+ : new BackgroundFileSaverOutputStream();
+ saver.enableSha256();
+ let completionPromise = promiseSaverComplete(saver, onTargetChange);
+
+ // Start feeding the first chunk of data to the saver. In case we are using
+ // the stream listener, we only write one chunk.
+ let testData = useLongData ? TEST_DATA_LONG : TEST_DATA_SHORT;
+ let feedPromise = useStreamListener
+ ? promisePumpToSaver(testData + testData, saver)
+ : promiseCopyToSaver(testData, saver, false);
+
+ // Set a target output file.
+ saver.setTarget(initialFile, keepPartialOnFailure);
+
+ // Wait for the first chunk of data to be copied.
+ await feedPromise;
+
+ if (renameAtSomePoint) {
+ saver.setTarget(renamedFile, keepPartialOnFailure);
+ }
+
+ if (cancelAtSomePoint) {
+ saver.finish(Cr.NS_ERROR_FAILURE);
+ }
+
+ // Feed the second chunk of data to the saver.
+ if (!useStreamListener) {
+ await promiseCopyToSaver(testData, saver, true);
+ }
+
+ // Wait for completion, and ensure we succeeded or failed as expected.
+ if (!cancelAtSomePoint) {
+ saver.finish(Cr.NS_OK);
+ }
+ try {
+ await completionPromise;
+ if (cancelAtSomePoint) {
+ do_throw("Failure expected.");
+ }
+ } catch (ex) {
+ if (!cancelAtSomePoint || ex.result != Cr.NS_ERROR_FAILURE) {
+ throw ex;
+ }
+ }
+
+ if (!cancelAtSomePoint) {
+ // In this case, the file must exist.
+ Assert.ok(currentFile.exists());
+ let expectedContents = testData + testData;
+ await promiseVerifyContents(currentFile, expectedContents);
+ Assert.equal(
+ EXPECTED_HASHES[expectedContents.length],
+ toHex(saver.sha256Hash)
+ );
+ currentFile.remove(false);
+
+ // If the target was really renamed, the old file should not exist.
+ if (renamedFile.equals(currentFile)) {
+ Assert.ok(!initialFile.exists());
+ }
+ } else if (!keepPartialOnFailure) {
+ // In this case, the file must not exist.
+ Assert.ok(!initialFile.exists());
+ Assert.ok(!renamedFile.exists());
+ } else {
+ // In this case, the file may or may not exist, because canceling can
+ // interrupt the asynchronous operation at any point, even before the file
+ // has been created for the first time.
+ if (initialFile.exists()) {
+ initialFile.remove(false);
+ }
+ if (renamedFile.exists()) {
+ renamedFile.remove(false);
+ }
+ }
+
+ info("Test case completed in " + (Date.now() - startTime) + " ms.");
+ }
+});
+
+add_task(async function test_setTarget_after_close_stream() {
+ // This test checks the case where we close the output stream before we call
+ // the setTarget method. All the data should be buffered and written anyway.
+ let destFile = getTempFile(TEST_FILE_NAME_1);
+
+ // Test the case where the file does not already exists first, then the case
+ // where the file already exists.
+ for (let i = 0; i < 2; i++) {
+ let saver = new BackgroundFileSaverOutputStream();
+ saver.enableSha256();
+ let completionPromise = promiseSaverComplete(saver);
+
+ // Copy some data to the output stream of the file saver. This data must
+ // be shorter than the internal component's pipe buffer for the test to
+ // succeed, because otherwise the test would block waiting for the write to
+ // complete.
+ await promiseCopyToSaver(TEST_DATA_SHORT, saver, true);
+
+ // Set the target file and wait for the output to finish.
+ saver.setTarget(destFile, false);
+ saver.finish(Cr.NS_OK);
+ await completionPromise;
+
+ // Verify results.
+ await promiseVerifyContents(destFile, TEST_DATA_SHORT);
+ Assert.equal(
+ EXPECTED_HASHES[TEST_DATA_SHORT.length],
+ toHex(saver.sha256Hash)
+ );
+ }
+
+ // Clean up.
+ destFile.remove(false);
+});
+
+add_task(async function test_setTarget_fast() {
+ // This test checks a fast rename of the target file.
+ let destFile1 = getTempFile(TEST_FILE_NAME_1);
+ let destFile2 = getTempFile(TEST_FILE_NAME_2);
+ let saver = new BackgroundFileSaverOutputStream();
+ let completionPromise = promiseSaverComplete(saver);
+
+ // Set the initial name after the stream is closed, then rename immediately.
+ await promiseCopyToSaver(TEST_DATA_SHORT, saver, true);
+ saver.setTarget(destFile1, false);
+ saver.setTarget(destFile2, false);
+
+ // Wait for all the operations to complete.
+ saver.finish(Cr.NS_OK);
+ await completionPromise;
+
+ // Verify results and clean up.
+ Assert.ok(!destFile1.exists());
+ await promiseVerifyContents(destFile2, TEST_DATA_SHORT);
+ destFile2.remove(false);
+});
+
+add_task(async function test_setTarget_multiple() {
+ // This test checks multiple renames of the target file.
+ let destFile = getTempFile(TEST_FILE_NAME_1);
+ let saver = new BackgroundFileSaverOutputStream();
+ let completionPromise = promiseSaverComplete(saver);
+
+ // Rename both before and after the stream is closed.
+ saver.setTarget(getTempFile(TEST_FILE_NAME_2), false);
+ saver.setTarget(getTempFile(TEST_FILE_NAME_3), false);
+ await promiseCopyToSaver(TEST_DATA_SHORT, saver, true);
+ saver.setTarget(getTempFile(TEST_FILE_NAME_2), false);
+ saver.setTarget(destFile, false);
+
+ // Wait for all the operations to complete.
+ saver.finish(Cr.NS_OK);
+ await completionPromise;
+
+ // Verify results and clean up.
+ Assert.ok(!getTempFile(TEST_FILE_NAME_2).exists());
+ Assert.ok(!getTempFile(TEST_FILE_NAME_3).exists());
+ await promiseVerifyContents(destFile, TEST_DATA_SHORT);
+ destFile.remove(false);
+});
+
+add_task(async function test_enableAppend() {
+ // This test checks append mode with hashing disabled.
+ let destFile = getTempFile(TEST_FILE_NAME_1);
+
+ // Test the case where the file does not already exists first, then the case
+ // where the file already exists.
+ for (let i = 0; i < 2; i++) {
+ let saver = new BackgroundFileSaverOutputStream();
+ saver.enableAppend();
+ let completionPromise = promiseSaverComplete(saver);
+
+ saver.setTarget(destFile, false);
+ await promiseCopyToSaver(TEST_DATA_LONG, saver, true);
+
+ saver.finish(Cr.NS_OK);
+ await completionPromise;
+
+ // Verify results.
+ let expectedContents =
+ i == 0 ? TEST_DATA_LONG : TEST_DATA_LONG + TEST_DATA_LONG;
+ await promiseVerifyContents(destFile, expectedContents);
+ }
+
+ // Clean up.
+ destFile.remove(false);
+});
+
+add_task(async function test_enableAppend_setTarget_fast() {
+ // This test checks a fast rename of the target file in append mode.
+ let destFile1 = getTempFile(TEST_FILE_NAME_1);
+ let destFile2 = getTempFile(TEST_FILE_NAME_2);
+
+ // Test the case where the file does not already exists first, then the case
+ // where the file already exists.
+ for (let i = 0; i < 2; i++) {
+ let saver = new BackgroundFileSaverOutputStream();
+ saver.enableAppend();
+ let completionPromise = promiseSaverComplete(saver);
+
+ await promiseCopyToSaver(TEST_DATA_SHORT, saver, true);
+
+ // The first time, we start appending to the first file and rename to the
+ // second file. The second time, we start appending to the second file,
+ // that was created the first time, and rename back to the first file.
+ let firstFile = i == 0 ? destFile1 : destFile2;
+ let secondFile = i == 0 ? destFile2 : destFile1;
+ saver.setTarget(firstFile, false);
+ saver.setTarget(secondFile, false);
+
+ saver.finish(Cr.NS_OK);
+ await completionPromise;
+
+ // Verify results.
+ Assert.ok(!firstFile.exists());
+ let expectedContents =
+ i == 0 ? TEST_DATA_SHORT : TEST_DATA_SHORT + TEST_DATA_SHORT;
+ await promiseVerifyContents(secondFile, expectedContents);
+ }
+
+ // Clean up.
+ destFile1.remove(false);
+});
+
+add_task(async function test_enableAppend_hash() {
+ // This test checks append mode, also verifying that the computed hash
+ // includes the contents of the existing data.
+ let destFile = getTempFile(TEST_FILE_NAME_1);
+
+ // Test the case where the file does not already exists first, then the case
+ // where the file already exists.
+ for (let i = 0; i < 2; i++) {
+ let saver = new BackgroundFileSaverOutputStream();
+ saver.enableAppend();
+ saver.enableSha256();
+ let completionPromise = promiseSaverComplete(saver);
+
+ saver.setTarget(destFile, false);
+ await promiseCopyToSaver(TEST_DATA_LONG, saver, true);
+
+ saver.finish(Cr.NS_OK);
+ await completionPromise;
+
+ // Verify results.
+ let expectedContents =
+ i == 0 ? TEST_DATA_LONG : TEST_DATA_LONG + TEST_DATA_LONG;
+ await promiseVerifyContents(destFile, expectedContents);
+ Assert.equal(
+ EXPECTED_HASHES[expectedContents.length],
+ toHex(saver.sha256Hash)
+ );
+ }
+
+ // Clean up.
+ destFile.remove(false);
+});
+
+add_task(async function test_finish_only() {
+ // This test checks creating the object and doing nothing.
+ let saver = new BackgroundFileSaverOutputStream();
+ function onTargetChange(aTarget) {
+ do_throw("Should not receive the onTargetChange notification.");
+ }
+ let completionPromise = promiseSaverComplete(saver, onTargetChange);
+ saver.finish(Cr.NS_OK);
+ await completionPromise;
+});
+
+add_task(async function test_empty() {
+ // This test checks we still create an empty file when no data is fed.
+ let destFile = getTempFile(TEST_FILE_NAME_1);
+
+ let saver = new BackgroundFileSaverOutputStream();
+ let completionPromise = promiseSaverComplete(saver);
+
+ saver.setTarget(destFile, false);
+ await promiseCopyToSaver("", saver, true);
+
+ saver.finish(Cr.NS_OK);
+ await completionPromise;
+
+ // Verify results.
+ Assert.ok(destFile.exists());
+ Assert.equal(destFile.fileSize, 0);
+
+ // Clean up.
+ destFile.remove(false);
+});
+
+add_task(async function test_empty_hash() {
+ // This test checks the hash of an empty file, both in normal and append mode.
+ let destFile = getTempFile(TEST_FILE_NAME_1);
+
+ // Test normal mode first, then append mode.
+ for (let i = 0; i < 2; i++) {
+ let saver = new BackgroundFileSaverOutputStream();
+ if (i == 1) {
+ saver.enableAppend();
+ }
+ saver.enableSha256();
+ let completionPromise = promiseSaverComplete(saver);
+
+ saver.setTarget(destFile, false);
+ await promiseCopyToSaver("", saver, true);
+
+ saver.finish(Cr.NS_OK);
+ await completionPromise;
+
+ // Verify results.
+ Assert.equal(destFile.fileSize, 0);
+ Assert.equal(EXPECTED_HASHES[0], toHex(saver.sha256Hash));
+ }
+
+ // Clean up.
+ destFile.remove(false);
+});
+
+add_task(async function test_invalid_hash() {
+ let saver = new BackgroundFileSaverStreamListener();
+ let completionPromise = promiseSaverComplete(saver);
+ // We shouldn't be able to get the hash if hashing hasn't been enabled
+ try {
+ saver.sha256Hash;
+ do_throw("Shouldn't be able to get hash if hashing not enabled");
+ } catch (ex) {
+ if (ex.result != Cr.NS_ERROR_NOT_AVAILABLE) {
+ throw ex;
+ }
+ }
+ // Enable hashing, but don't feed any data to saver
+ saver.enableSha256();
+ let destFile = getTempFile(TEST_FILE_NAME_1);
+ saver.setTarget(destFile, false);
+ // We don't wait on promiseSaverComplete, so the hash getter can run before
+ // or after onSaveComplete is called. However, the expected behavior is the
+ // same in both cases since the hash is only valid when the save completes
+ // successfully.
+ saver.finish(Cr.NS_ERROR_FAILURE);
+ try {
+ saver.sha256Hash;
+ do_throw("Shouldn't be able to get hash if save did not succeed");
+ } catch (ex) {
+ if (ex.result != Cr.NS_ERROR_NOT_AVAILABLE) {
+ throw ex;
+ }
+ }
+ // Wait for completion so that the worker thread finishes dealing with the
+ // target file. We expect it to fail.
+ try {
+ await completionPromise;
+ do_throw("completionPromise should throw");
+ } catch (ex) {
+ if (ex.result != Cr.NS_ERROR_FAILURE) {
+ throw ex;
+ }
+ }
+});
+
+add_task(async function test_signature() {
+ // Check that we get a signature if the saver is finished.
+ let destFile = getTempFile(TEST_FILE_NAME_1);
+
+ let saver = new BackgroundFileSaverOutputStream();
+ let completionPromise = promiseSaverComplete(saver);
+
+ try {
+ saver.signatureInfo;
+ do_throw("Can't get signature if saver is not complete");
+ } catch (ex) {
+ if (ex.result != Cr.NS_ERROR_NOT_AVAILABLE) {
+ throw ex;
+ }
+ }
+
+ saver.enableSignatureInfo();
+ saver.setTarget(destFile, false);
+ await promiseCopyToSaver(TEST_DATA_SHORT, saver, true);
+
+ saver.finish(Cr.NS_OK);
+ await completionPromise;
+ await promiseVerifyContents(destFile, TEST_DATA_SHORT);
+
+ // signatureInfo is an empty nsIArray
+ Assert.equal(0, saver.signatureInfo.length);
+
+ // Clean up.
+ destFile.remove(false);
+});
+
+add_task(async function test_signature_not_enabled() {
+ // Check that we get a signature if the saver is finished on Windows.
+ let destFile = getTempFile(TEST_FILE_NAME_1);
+
+ let saver = new BackgroundFileSaverOutputStream();
+ let completionPromise = promiseSaverComplete(saver);
+ saver.setTarget(destFile, false);
+ await promiseCopyToSaver(TEST_DATA_SHORT, saver, true);
+
+ saver.finish(Cr.NS_OK);
+ await completionPromise;
+ try {
+ saver.signatureInfo;
+ do_throw("Can't get signature if not enabled");
+ } catch (ex) {
+ if (ex.result != Cr.NS_ERROR_NOT_AVAILABLE) {
+ throw ex;
+ }
+ }
+
+ // Clean up.
+ destFile.remove(false);
+});
+
+add_task(function test_teardown() {
+ gStillRunning = false;
+});
diff --git a/netwerk/test/unit/test_be_conservative.js b/netwerk/test/unit/test_be_conservative.js
new file mode 100644
index 0000000000..f5ef6b7bb7
--- /dev/null
+++ b/netwerk/test/unit/test_be_conservative.js
@@ -0,0 +1,247 @@
+// -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+// Any copyright is dedicated to the Public Domain.
+// http://creativecommons.org/publicdomain/zero/1.0/
+
+"use strict";
+
+// Tests that nsIHttpChannelInternal.beConservative correctly limits the use of
+// advanced TLS features that may cause compatibility issues. Does so by
+// starting a TLS server that requires the advanced features and then ensuring
+// that a client that is set to be conservative will fail when connecting.
+
+// Get a profile directory and ensure PSM initializes NSS.
+do_get_profile();
+Cc["@mozilla.org/psm;1"].getService(Ci.nsISupports);
+
+function getCert() {
+ return new Promise((resolve, reject) => {
+ let certService = Cc[
+ "@mozilla.org/security/local-cert-service;1"
+ ].getService(Ci.nsILocalCertService);
+ certService.getOrCreateCert("beConservative-test", {
+ handleCert(c, rv) {
+ if (rv) {
+ reject(rv);
+ return;
+ }
+ resolve(c);
+ },
+ });
+ });
+}
+
+class InputStreamCallback {
+ constructor(output) {
+ this.output = output;
+ this.stopped = false;
+ }
+
+ onInputStreamReady(stream) {
+ info("input stream ready");
+ if (this.stopped) {
+ info("input stream callback stopped - bailing");
+ return;
+ }
+ let available = 0;
+ try {
+ available = stream.available();
+ } catch (e) {
+ // onInputStreamReady may fire when the stream has been closed.
+ equal(
+ e.result,
+ Cr.NS_BASE_STREAM_CLOSED,
+ "error should be NS_BASE_STREAM_CLOSED"
+ );
+ }
+ if (available > 0) {
+ let request = NetUtil.readInputStreamToString(stream, available, {
+ charset: "utf8",
+ });
+ ok(
+ request.startsWith("GET / HTTP/1.1\r\n"),
+ "Should get a simple GET / HTTP/1.1 request"
+ );
+ let response =
+ "HTTP/1.1 200 OK\r\n" +
+ "Content-Length: 2\r\n" +
+ "Content-Type: text/plain\r\n" +
+ "\r\nOK";
+ let written = this.output.write(response, response.length);
+ equal(
+ written,
+ response.length,
+ "should have been able to write entire response"
+ );
+ }
+ this.output.close();
+ info("done with input stream ready");
+ }
+
+ stop() {
+ this.stopped = true;
+ this.output.close();
+ }
+}
+
+class TLSServerSecurityObserver {
+ constructor(input, output) {
+ this.input = input;
+ this.output = output;
+ this.callbacks = [];
+ this.stopped = false;
+ }
+
+ onHandshakeDone(socket, status) {
+ info("TLS handshake done");
+ info(`TLS version used: ${status.tlsVersionUsed}`);
+
+ if (this.stopped) {
+ info("handshake done callback stopped - bailing");
+ return;
+ }
+
+ let callback = new InputStreamCallback(this.output);
+ this.callbacks.push(callback);
+ this.input.asyncWait(callback, 0, 0, Services.tm.currentThread);
+ }
+
+ stop() {
+ this.stopped = true;
+ this.input.close();
+ this.output.close();
+ this.callbacks.forEach(callback => {
+ callback.stop();
+ });
+ }
+}
+
+class ServerSocketListener {
+ constructor() {
+ this.securityObservers = [];
+ }
+
+ onSocketAccepted(socket, transport) {
+ info("accepted TLS client connection");
+ let connectionInfo = transport.securityInfo.QueryInterface(
+ Ci.nsITLSServerConnectionInfo
+ );
+ let input = transport.openInputStream(0, 0, 0);
+ let output = transport.openOutputStream(0, 0, 0);
+ let securityObserver = new TLSServerSecurityObserver(input, output);
+ this.securityObservers.push(securityObserver);
+ connectionInfo.setSecurityObserver(securityObserver);
+ }
+
+ // For some reason we get input stream callback events after we've stopped
+ // listening, so this ensures we just drop those events.
+ onStopListening() {
+ info("onStopListening");
+ this.securityObservers.forEach(observer => {
+ observer.stop();
+ });
+ }
+}
+
+function startServer(cert, minServerVersion, maxServerVersion) {
+ let tlsServer = Cc["@mozilla.org/network/tls-server-socket;1"].createInstance(
+ Ci.nsITLSServerSocket
+ );
+ tlsServer.init(-1, true, -1);
+ tlsServer.serverCert = cert;
+ tlsServer.setVersionRange(minServerVersion, maxServerVersion);
+ tlsServer.setSessionTickets(false);
+ tlsServer.asyncListen(new ServerSocketListener());
+ return tlsServer;
+}
+
+const hostname = "example.com";
+
+function storeCertOverride(port, cert) {
+ let certOverrideService = Cc[
+ "@mozilla.org/security/certoverride;1"
+ ].getService(Ci.nsICertOverrideService);
+ let overrideBits =
+ Ci.nsICertOverrideService.ERROR_UNTRUSTED |
+ Ci.nsICertOverrideService.ERROR_MISMATCH;
+ certOverrideService.rememberValidityOverride(
+ hostname,
+ port,
+ cert,
+ overrideBits,
+ true
+ );
+}
+
+function startClient(port, beConservative, expectSuccess) {
+ let req = new XMLHttpRequest();
+ req.open("GET", `https://${hostname}:${port}`);
+ let internalChannel = req.channel.QueryInterface(Ci.nsIHttpChannelInternal);
+ internalChannel.beConservative = beConservative;
+ return new Promise((resolve, reject) => {
+ req.onload = () => {
+ ok(
+ expectSuccess,
+ `should ${expectSuccess ? "" : "not "}have gotten load event`
+ );
+ equal(req.responseText, "OK", "response text should be 'OK'");
+ resolve();
+ };
+ req.onerror = () => {
+ ok(
+ !expectSuccess,
+ `should ${!expectSuccess ? "" : "not "}have gotten an error`
+ );
+ resolve();
+ };
+
+ req.send();
+ });
+}
+
+add_task(async function() {
+ Services.prefs.setIntPref("security.tls.version.max", 4);
+ Services.prefs.setCharPref("network.dns.localDomains", hostname);
+ let cert = await getCert();
+
+ // First run a server that accepts TLS 1.2 and 1.3. A conservative client
+ // should succeed in connecting.
+ let server = startServer(
+ cert,
+ Ci.nsITLSClientStatus.TLS_VERSION_1_2,
+ Ci.nsITLSClientStatus.TLS_VERSION_1_3
+ );
+ storeCertOverride(server.port, cert);
+ await startClient(
+ server.port,
+ true /*be conservative*/,
+ true /*should succeed*/
+ );
+ server.close();
+
+ // Now run a server that only accepts TLS 1.3. A conservative client will not
+ // succeed in this case.
+ server = startServer(
+ cert,
+ Ci.nsITLSClientStatus.TLS_VERSION_1_3,
+ Ci.nsITLSClientStatus.TLS_VERSION_1_3
+ );
+ storeCertOverride(server.port, cert);
+ await startClient(
+ server.port,
+ true /*be conservative*/,
+ false /*should fail*/
+ );
+
+ // However, a non-conservative client should succeed.
+ await startClient(
+ server.port,
+ false /*don't be conservative*/,
+ true /*should succeed*/
+ );
+ server.close();
+});
+
+registerCleanupFunction(function() {
+ Services.prefs.clearUserPref("security.tls.version.max");
+ Services.prefs.clearUserPref("network.dns.localDomains");
+});
diff --git a/netwerk/test/unit/test_be_conservative_error_handling.js b/netwerk/test/unit/test_be_conservative_error_handling.js
new file mode 100644
index 0000000000..190fbba157
--- /dev/null
+++ b/netwerk/test/unit/test_be_conservative_error_handling.js
@@ -0,0 +1,242 @@
+// -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+// Any copyright is dedicated to the Public Domain.
+// http://creativecommons.org/publicdomain/zero/1.0/
+
+"use strict";
+
+// Tests that nsIHttpChannelInternal.beConservative correctly limits the use of
+// advanced TLS features that may cause compatibility issues. Does so by
+// starting a TLS server that requires the advanced features and then ensuring
+// that a client that is set to be conservative will fail when connecting.
+
+// Get a profile directory and ensure PSM initializes NSS.
+do_get_profile();
+Cc["@mozilla.org/psm;1"].getService(Ci.nsISupports);
+
+function getCert() {
+ return new Promise((resolve, reject) => {
+ let certService = Cc[
+ "@mozilla.org/security/local-cert-service;1"
+ ].getService(Ci.nsILocalCertService);
+ certService.getOrCreateCert("beConservative-test", {
+ handleCert(c, rv) {
+ if (rv) {
+ reject(rv);
+ return;
+ }
+ resolve(c);
+ },
+ });
+ });
+}
+
+class InputStreamCallback {
+ constructor(output) {
+ this.output = output;
+ this.stopped = false;
+ }
+
+ onInputStreamReady(stream) {
+ info("input stream ready");
+ if (this.stopped) {
+ info("input stream callback stopped - bailing");
+ return;
+ }
+ let available = 0;
+ try {
+ available = stream.available();
+ } catch (e) {
+ // onInputStreamReady may fire when the stream has been closed.
+ equal(
+ e.result,
+ Cr.NS_BASE_STREAM_CLOSED,
+ "error should be NS_BASE_STREAM_CLOSED"
+ );
+ }
+ if (available > 0) {
+ let request = NetUtil.readInputStreamToString(stream, available, {
+ charset: "utf8",
+ });
+ ok(
+ request.startsWith("GET / HTTP/1.1\r\n"),
+ "Should get a simple GET / HTTP/1.1 request"
+ );
+ let response =
+ "HTTP/1.1 200 OK\r\n" +
+ "Content-Length: 2\r\n" +
+ "Content-Type: text/plain\r\n" +
+ "\r\nOK";
+ let written = this.output.write(response, response.length);
+ equal(
+ written,
+ response.length,
+ "should have been able to write entire response"
+ );
+ }
+ this.output.close();
+ info("done with input stream ready");
+ }
+
+ stop() {
+ this.stopped = true;
+ this.output.close();
+ }
+}
+
+class TLSServerSecurityObserver {
+ constructor(input, output) {
+ this.input = input;
+ this.output = output;
+ this.callbacks = [];
+ this.stopped = false;
+ }
+
+ onHandshakeDone(socket, status) {
+ info("TLS handshake done");
+ info(`TLS version used: ${status.tlsVersionUsed}`);
+
+ if (this.stopped) {
+ info("handshake done callback stopped - bailing");
+ return;
+ }
+
+ let callback = new InputStreamCallback(this.output);
+ this.callbacks.push(callback);
+ this.input.asyncWait(callback, 0, 0, Services.tm.currentThread);
+ }
+
+ stop() {
+ this.stopped = true;
+ this.input.close();
+ this.output.close();
+ this.callbacks.forEach(callback => {
+ callback.stop();
+ });
+ }
+}
+
+class ServerSocketListener {
+ constructor() {
+ this.securityObservers = [];
+ }
+
+ onSocketAccepted(socket, transport) {
+ info("accepted TLS client connection");
+ let connectionInfo = transport.securityInfo.QueryInterface(
+ Ci.nsITLSServerConnectionInfo
+ );
+ let input = transport.openInputStream(0, 0, 0);
+ let output = transport.openOutputStream(0, 0, 0);
+ let securityObserver = new TLSServerSecurityObserver(input, output);
+ this.securityObservers.push(securityObserver);
+ connectionInfo.setSecurityObserver(securityObserver);
+ }
+
+ // For some reason we get input stream callback events after we've stopped
+ // listening, so this ensures we just drop those events.
+ onStopListening() {
+ info("onStopListening");
+ this.securityObservers.forEach(observer => {
+ observer.stop();
+ });
+ }
+}
+
+function startServer(cert, minServerVersion, maxServerVersion) {
+ let tlsServer = Cc["@mozilla.org/network/tls-server-socket;1"].createInstance(
+ Ci.nsITLSServerSocket
+ );
+ tlsServer.init(-1, true, -1);
+ tlsServer.serverCert = cert;
+ tlsServer.setVersionRange(minServerVersion, maxServerVersion);
+ tlsServer.setSessionTickets(false);
+ tlsServer.asyncListen(new ServerSocketListener());
+ return tlsServer;
+}
+
+const hostname = "example.com";
+
+function storeCertOverride(port, cert) {
+ let certOverrideService = Cc[
+ "@mozilla.org/security/certoverride;1"
+ ].getService(Ci.nsICertOverrideService);
+ let overrideBits =
+ Ci.nsICertOverrideService.ERROR_UNTRUSTED |
+ Ci.nsICertOverrideService.ERROR_MISMATCH;
+ certOverrideService.rememberValidityOverride(
+ hostname,
+ port,
+ cert,
+ overrideBits,
+ true
+ );
+}
+
+function startClient(port, beConservative, expectSuccess) {
+ let req = new XMLHttpRequest();
+ req.open("GET", `https://${hostname}:${port}`);
+ let internalChannel = req.channel.QueryInterface(Ci.nsIHttpChannelInternal);
+ internalChannel.beConservative = beConservative;
+ return new Promise((resolve, reject) => {
+ req.onload = () => {
+ ok(
+ expectSuccess,
+ `should ${expectSuccess ? "" : "not "}have gotten load event`
+ );
+ equal(req.responseText, "OK", "response text should be 'OK'");
+ resolve();
+ };
+ req.onerror = () => {
+ ok(
+ !expectSuccess,
+ `should ${!expectSuccess ? "" : "not "}have gotten an error`
+ );
+ resolve();
+ };
+
+ req.send();
+ });
+}
+
+add_task(async function() {
+ // Restrict to only TLS 1.3.
+ Services.prefs.setIntPref("security.tls.version.min", 4);
+ Services.prefs.setIntPref("security.tls.version.max", 4);
+ Services.prefs.setCharPref("network.dns.localDomains", hostname);
+ let cert = await getCert();
+
+ // Run a server that accepts TLS 1.2 and 1.3. The connection should succeed.
+ let server = startServer(
+ cert,
+ Ci.nsITLSClientStatus.TLS_VERSION_1_2,
+ Ci.nsITLSClientStatus.TLS_VERSION_1_3
+ );
+ storeCertOverride(server.port, cert);
+ await startClient(
+ server.port,
+ true /*be conservative*/,
+ true /*should succeed*/
+ );
+ server.close();
+
+ // Now run a server that only accepts TLS 1.3. A conservative client will not
+ // succeed in this case.
+ server = startServer(
+ cert,
+ Ci.nsITLSClientStatus.TLS_VERSION_1_3,
+ Ci.nsITLSClientStatus.TLS_VERSION_1_3
+ );
+ storeCertOverride(server.port, cert);
+ await startClient(
+ server.port,
+ true /*be conservative*/,
+ false /*should fail*/
+ );
+ server.close();
+});
+
+registerCleanupFunction(function() {
+ Services.prefs.clearUserPref("security.tls.version.min");
+ Services.prefs.clearUserPref("security.tls.version.max");
+ Services.prefs.clearUserPref("network.dns.localDomains");
+});
diff --git a/netwerk/test/unit/test_blob_channelname.js b/netwerk/test/unit/test_blob_channelname.js
new file mode 100644
index 0000000000..c1a09272da
--- /dev/null
+++ b/netwerk/test/unit/test_blob_channelname.js
@@ -0,0 +1,42 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(async function channelname() {
+ var file = new File(
+ [new Blob(["test"], { type: "text/plain" })],
+ "test-name"
+ );
+ var url = URL.createObjectURL(file);
+ var channel = NetUtil.newChannel({
+ uri: url,
+ loadUsingSystemPrincipal: true,
+ });
+
+ let inputStream = channel.open();
+ ok(inputStream, "Should be able to open channel");
+ ok(
+ inputStream.QueryInterface(Ci.nsIAsyncInputStream),
+ "Stream should support async operations"
+ );
+
+ await new Promise(resolve => {
+ inputStream.asyncWait(
+ () => {
+ let available = inputStream.available();
+ ok(available, "There should be data to read");
+ Assert.equal(
+ channel.contentDispositionFilename,
+ "test-name",
+ "filename matches"
+ );
+ resolve();
+ },
+ 0,
+ 0,
+ Services.tm.mainThread
+ );
+ });
+
+ inputStream.close();
+ channel.cancel(Cr.NS_ERROR_FAILURE);
+});
diff --git a/netwerk/test/unit/test_brotli_http.js b/netwerk/test/unit/test_brotli_http.js
new file mode 100644
index 0000000000..5c3d2452f0
--- /dev/null
+++ b/netwerk/test/unit/test_brotli_http.js
@@ -0,0 +1,44 @@
+// This test exists mostly as documentation that
+// Firefox can load brotli files over HTTP if we set the proper pref.
+
+"use strict";
+
+function contentHandler(metadata, response) {
+ response.setHeader("Content-Type", "text/plain", false);
+ response.setHeader("Content-Encoding", "br", false);
+ response.write("\x0b\x02\x80hello\x03");
+}
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+XPCOMUtils.defineLazyGetter(this, "URL", function() {
+ return "http://localhost:" + httpServer.identity.primaryPort + "/content";
+});
+
+var httpServer = null;
+
+add_task(async function test() {
+ httpServer = new HttpServer();
+ httpServer.registerPathHandler("/content", contentHandler);
+ httpServer.start(-1);
+ Services.prefs.setCharPref(
+ "network.http.accept-encoding",
+ "gzip, deflate, br"
+ );
+
+ let chan = NetUtil.newChannel({ uri: URL, loadUsingSystemPrincipal: true });
+ let [, buff] = await new Promise(resolve => {
+ chan.asyncOpen(
+ new ChannelListener(
+ (req, buff) => {
+ resolve([req, buff]);
+ },
+ null,
+ CL_IGNORE_CL
+ )
+ );
+ });
+ equal(buff, "hello");
+ Services.prefs.clearUserPref("network.http.accept-encoding");
+ await httpServer.stop();
+});
diff --git a/netwerk/test/unit/test_bug1064258.js b/netwerk/test/unit/test_bug1064258.js
new file mode 100644
index 0000000000..7e9ce74409
--- /dev/null
+++ b/netwerk/test/unit/test_bug1064258.js
@@ -0,0 +1,142 @@
+/**
+ * Check how nsICachingChannel.cacheOnlyMetadata works.
+ * - all channels involved in this test are set cacheOnlyMetadata = true
+ * - do a previously uncached request for a long living content
+ * - check we have downloaded the content from the server (channel provides it)
+ * - check the entry has metadata, but zero-length content
+ * - load the same URL again, now cached
+ * - check the channel is giving no content (no call to OnDataAvailable) but succeeds
+ * - repeat again, but for a different URL that is not cached (immediately expires)
+ * - only difference is that we get a newer version of the content from the server during the second request
+ */
+
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+XPCOMUtils.defineLazyGetter(this, "URL", function() {
+ return "http://localhost:" + httpServer.identity.primaryPort;
+});
+
+var httpServer = null;
+
+function make_channel(url, callback, ctx) {
+ return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true });
+}
+
+const responseBody1 = "response body 1";
+const responseBody2a = "response body 2a";
+const responseBody2b = "response body 2b";
+
+function contentHandler1(metadata, response) {
+ response.setHeader("Content-Type", "text/plain");
+ response.setHeader("Cache-control", "max-age=999999");
+ response.bodyOutputStream.write(responseBody1, responseBody1.length);
+}
+
+var content2passCount = 0;
+
+function contentHandler2(metadata, response) {
+ response.setHeader("Content-Type", "text/plain");
+ response.setHeader("Cache-control", "no-cache");
+ switch (content2passCount++) {
+ case 0:
+ response.setHeader("ETag", "testetag");
+ response.bodyOutputStream.write(responseBody2a, responseBody2a.length);
+ break;
+ case 1:
+ Assert.ok(metadata.hasHeader("If-None-Match"));
+ Assert.equal(metadata.getHeader("If-None-Match"), "testetag");
+ response.bodyOutputStream.write(responseBody2b, responseBody2b.length);
+ break;
+ default:
+ throw "Unexpected request in the test";
+ }
+}
+
+function run_test() {
+ httpServer = new HttpServer();
+ httpServer.registerPathHandler("/content1", contentHandler1);
+ httpServer.registerPathHandler("/content2", contentHandler2);
+ httpServer.start(-1);
+
+ run_test_content1a();
+ do_test_pending();
+}
+
+function run_test_content1a() {
+ var chan = make_channel(URL + "/content1");
+ let caching = chan.QueryInterface(Ci.nsICachingChannel);
+ caching.cacheOnlyMetadata = true;
+ chan.asyncOpen(new ChannelListener(contentListener1a, null));
+}
+
+function contentListener1a(request, buffer) {
+ Assert.equal(buffer, responseBody1);
+
+ asyncOpenCacheEntry(URL + "/content1", "disk", 0, null, cacheCheck1);
+}
+
+function cacheCheck1(status, entry) {
+ Assert.equal(status, 0);
+ Assert.equal(entry.dataSize, 0);
+ try {
+ Assert.notEqual(entry.getMetaDataElement("response-head"), null);
+ } catch (ex) {
+ do_throw("Missing response head");
+ }
+
+ var chan = make_channel(URL + "/content1");
+ let caching = chan.QueryInterface(Ci.nsICachingChannel);
+ caching.cacheOnlyMetadata = true;
+ chan.asyncOpen(new ChannelListener(contentListener1b, null, CL_IGNORE_CL));
+}
+
+function contentListener1b(request, buffer) {
+ request.QueryInterface(Ci.nsIHttpChannel);
+ Assert.equal(request.requestMethod, "GET");
+ Assert.equal(request.responseStatus, 200);
+ Assert.equal(request.getResponseHeader("Cache-control"), "max-age=999999");
+
+ Assert.equal(buffer, "");
+ run_test_content2a();
+}
+
+// Now same set of steps but this time for an immediately expiring content.
+
+function run_test_content2a() {
+ var chan = make_channel(URL + "/content2");
+ let caching = chan.QueryInterface(Ci.nsICachingChannel);
+ caching.cacheOnlyMetadata = true;
+ chan.asyncOpen(new ChannelListener(contentListener2a, null));
+}
+
+function contentListener2a(request, buffer) {
+ Assert.equal(buffer, responseBody2a);
+
+ asyncOpenCacheEntry(URL + "/content2", "disk", 0, null, cacheCheck2);
+}
+
+function cacheCheck2(status, entry) {
+ Assert.equal(status, 0);
+ Assert.equal(entry.dataSize, 0);
+ try {
+ Assert.notEqual(entry.getMetaDataElement("response-head"), null);
+ Assert.ok(
+ entry.getMetaDataElement("response-head").match("etag: testetag")
+ );
+ } catch (ex) {
+ do_throw("Missing response head");
+ }
+
+ var chan = make_channel(URL + "/content2");
+ let caching = chan.QueryInterface(Ci.nsICachingChannel);
+ caching.cacheOnlyMetadata = true;
+ chan.asyncOpen(new ChannelListener(contentListener2b, null));
+}
+
+function contentListener2b(request, buffer) {
+ Assert.equal(buffer, responseBody2b);
+
+ httpServer.stop(do_test_finished);
+}
diff --git a/netwerk/test/unit/test_bug1177909.js b/netwerk/test/unit/test_bug1177909.js
new file mode 100644
index 0000000000..a4e6117cbc
--- /dev/null
+++ b/netwerk/test/unit/test_bug1177909.js
@@ -0,0 +1,194 @@
+"use strict";
+
+const { MockRegistrar } = ChromeUtils.import(
+ "resource://testing-common/MockRegistrar.jsm"
+);
+
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "gProxyService",
+ "@mozilla.org/network/protocol-proxy-service;1",
+ "nsIProtocolProxyService"
+);
+
+XPCOMUtils.defineLazyGetter(this, "systemSettings", function() {
+ return {
+ QueryInterface: ChromeUtils.generateQI(["nsISystemProxySettings"]),
+
+ mainThreadOnly: true,
+ PACURI: null,
+
+ getProxyForURI(aSpec, aScheme, aHost, aPort) {
+ if (aPort != -1) {
+ return "SOCKS5 http://localhost:9050";
+ }
+ if (aScheme == "http" || aScheme == "ftp") {
+ return "PROXY http://localhost:8080";
+ }
+ if (aScheme == "https") {
+ return "HTTPS https://localhost:8080";
+ }
+ return "DIRECT";
+ },
+ };
+});
+
+let gMockProxy = MockRegistrar.register(
+ "@mozilla.org/system-proxy-settings;1",
+ systemSettings
+);
+
+registerCleanupFunction(() => {
+ MockRegistrar.unregister(gMockProxy);
+});
+
+function makeChannel(uri) {
+ return NetUtil.newChannel({
+ uri,
+ loadUsingSystemPrincipal: true,
+ });
+}
+
+async function TestProxyType(chan, flags) {
+ const prefs = Cc["@mozilla.org/preferences-service;1"].getService(
+ Ci.nsIPrefBranch
+ );
+ prefs.setIntPref(
+ "network.proxy.type",
+ Ci.nsIProtocolProxyService.PROXYCONFIG_SYSTEM
+ );
+
+ return new Promise((resolve, reject) => {
+ gProxyService.asyncResolve(chan, flags, {
+ onProxyAvailable(req, uri, pi, status) {
+ resolve(pi);
+ },
+ });
+ });
+}
+
+async function TestProxyTypeByURI(uri) {
+ return TestProxyType(makeChannel(uri), 0);
+}
+
+add_task(async function testHttpProxy() {
+ let pi = await TestProxyTypeByURI("http://www.mozilla.org/");
+ equal(pi.host, "localhost", "Expected proxy host to be localhost");
+ equal(pi.port, 8080, "Expected proxy port to be 8080");
+ equal(pi.type, "http", "Expected proxy type to be http");
+});
+
+add_task(async function testHttpsProxy() {
+ let pi = await TestProxyTypeByURI("https://www.mozilla.org/");
+ equal(pi.host, "localhost", "Expected proxy host to be localhost");
+ equal(pi.port, 8080, "Expected proxy port to be 8080");
+ equal(pi.type, "https", "Expected proxy type to be https");
+});
+
+if (Services.prefs.getBoolPref("network.ftp.enabled")) {
+ add_task(async function testFtpProxy() {
+ let pi = await TestProxyTypeByURI("ftp://ftp.mozilla.org/");
+ equal(pi.host, "localhost", "Expected proxy host to be localhost");
+ equal(pi.port, 8080, "Expected proxy port to be 8080");
+ equal(pi.type, "http", "Expected proxy type to be http");
+ });
+}
+
+add_task(async function testSocksProxy() {
+ let pi = await TestProxyTypeByURI("http://www.mozilla.org:1234/");
+ equal(pi.host, "localhost", "Expected proxy host to be localhost");
+ equal(pi.port, 9050, "Expected proxy port to be 8080");
+ equal(pi.type, "socks", "Expected proxy type to be http");
+});
+
+add_task(async function testDirectProxy() {
+ // Do what |WebSocketChannel::AsyncOpen| do, but do not prefer https proxy.
+ let proxyURI = Cc["@mozilla.org/network/standard-url-mutator;1"]
+ .createInstance(Ci.nsIURIMutator)
+ .setSpec("wss://ws.mozilla.org/")
+ .finalize();
+ let uri = proxyURI
+ .mutate()
+ .setScheme("https")
+ .finalize();
+
+ let ioService = Cc["@mozilla.org/network/io-service;1"].getService(
+ Ci.nsIIOService
+ );
+ let chan = ioService.newChannelFromURIWithProxyFlags(
+ uri,
+ proxyURI,
+ 0,
+ null,
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ null,
+ Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ Ci.nsIContentPolicy.TYPE_OTHER
+ );
+
+ let pi = await TestProxyType(chan, 0);
+ equal(pi, null, "Expected proxy host to be null");
+});
+
+add_task(async function testWebSocketProxy() {
+ // Do what |WebSocketChannel::AsyncOpen| do
+ let proxyURI = Cc["@mozilla.org/network/standard-url-mutator;1"]
+ .createInstance(Ci.nsIURIMutator)
+ .setSpec("wss://ws.mozilla.org/")
+ .finalize();
+ let uri = proxyURI
+ .mutate()
+ .setScheme("https")
+ .finalize();
+
+ let proxyFlags =
+ Ci.nsIProtocolProxyService.RESOLVE_PREFER_SOCKS_PROXY |
+ Ci.nsIProtocolProxyService.RESOLVE_PREFER_HTTPS_PROXY |
+ Ci.nsIProtocolProxyService.RESOLVE_ALWAYS_TUNNEL;
+
+ let ioService = Cc["@mozilla.org/network/io-service;1"].getService(
+ Ci.nsIIOService
+ );
+ let chan = ioService.newChannelFromURIWithProxyFlags(
+ uri,
+ proxyURI,
+ proxyFlags,
+ null,
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ null,
+ Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ Ci.nsIContentPolicy.TYPE_OTHER
+ );
+
+ let pi = await TestProxyType(chan, proxyFlags);
+ equal(pi.host, "localhost", "Expected proxy host to be localhost");
+ equal(pi.port, 8080, "Expected proxy port to be 8080");
+ equal(pi.type, "https", "Expected proxy type to be https");
+});
+
+add_task(async function testPreferHttpsProxy() {
+ let uri = Cc["@mozilla.org/network/standard-url-mutator;1"]
+ .createInstance(Ci.nsIURIMutator)
+ .setSpec("http://mozilla.org/")
+ .finalize();
+ let proxyFlags = Ci.nsIProtocolProxyService.RESOLVE_PREFER_HTTPS_PROXY;
+
+ let ioService = Cc["@mozilla.org/network/io-service;1"].getService(
+ Ci.nsIIOService
+ );
+ let chan = ioService.newChannelFromURIWithProxyFlags(
+ uri,
+ null,
+ proxyFlags,
+ null,
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ null,
+ Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ Ci.nsIContentPolicy.TYPE_OTHER
+ );
+
+ let pi = await TestProxyType(chan, proxyFlags);
+ equal(pi.host, "localhost", "Expected proxy host to be localhost");
+ equal(pi.port, 8080, "Expected proxy port to be 8080");
+ equal(pi.type, "https", "Expected proxy type to be https");
+});
diff --git a/netwerk/test/unit/test_bug1195415.js b/netwerk/test/unit/test_bug1195415.js
new file mode 100644
index 0000000000..491fd2d753
--- /dev/null
+++ b/netwerk/test/unit/test_bug1195415.js
@@ -0,0 +1,163 @@
+// Test for bug 1195415
+
+"use strict";
+
+function run_test() {
+ var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
+ var ssm = Cc["@mozilla.org/scriptsecuritymanager;1"].getService(
+ Ci.nsIScriptSecurityManager
+ );
+
+ // NON-UNICODE
+ var uri = ios.newURI("http://foo.com/file.txt");
+ Assert.equal(uri.asciiHostPort, "foo.com");
+ uri = uri
+ .mutate()
+ .setPort(90)
+ .finalize();
+ var prin = ssm.createContentPrincipal(uri, {});
+ Assert.equal(uri.asciiHostPort, "foo.com:90");
+ Assert.equal(prin.origin, "http://foo.com:90");
+
+ uri = ios.newURI("http://foo.com:10/file.txt");
+ Assert.equal(uri.asciiHostPort, "foo.com:10");
+ uri = uri
+ .mutate()
+ .setPort(500)
+ .finalize();
+ prin = ssm.createContentPrincipal(uri, {});
+ Assert.equal(uri.asciiHostPort, "foo.com:500");
+ Assert.equal(prin.origin, "http://foo.com:500");
+
+ uri = ios.newURI("http://foo.com:5000/file.txt");
+ Assert.equal(uri.asciiHostPort, "foo.com:5000");
+ uri = uri
+ .mutate()
+ .setPort(20)
+ .finalize();
+ prin = ssm.createContentPrincipal(uri, {});
+ Assert.equal(uri.asciiHostPort, "foo.com:20");
+ Assert.equal(prin.origin, "http://foo.com:20");
+
+ uri = ios.newURI("http://foo.com:5000/file.txt");
+ Assert.equal(uri.asciiHostPort, "foo.com:5000");
+ uri = uri
+ .mutate()
+ .setPort(-1)
+ .finalize();
+ prin = ssm.createContentPrincipal(uri, {});
+ Assert.equal(uri.asciiHostPort, "foo.com");
+ Assert.equal(prin.origin, "http://foo.com");
+
+ uri = ios.newURI("http://foo.com:5000/file.txt");
+ Assert.equal(uri.asciiHostPort, "foo.com:5000");
+ uri = uri
+ .mutate()
+ .setPort(80)
+ .finalize();
+ prin = ssm.createContentPrincipal(uri, {});
+ Assert.equal(uri.asciiHostPort, "foo.com");
+ Assert.equal(prin.origin, "http://foo.com");
+
+ // UNICODE
+ uri = ios.newURI("http://jos\u00e9.example.net.ch/file.txt");
+ Assert.equal(uri.asciiHostPort, "xn--jos-dma.example.net.ch");
+ uri = uri
+ .mutate()
+ .setPort(90)
+ .finalize();
+ prin = ssm.createContentPrincipal(uri, {});
+ Assert.equal(uri.asciiHostPort, "xn--jos-dma.example.net.ch:90");
+ Assert.equal(prin.origin, "http://xn--jos-dma.example.net.ch:90");
+
+ uri = ios.newURI("http://jos\u00e9.example.net.ch:10/file.txt");
+ Assert.equal(uri.asciiHostPort, "xn--jos-dma.example.net.ch:10");
+ uri = uri
+ .mutate()
+ .setPort(500)
+ .finalize();
+ prin = ssm.createContentPrincipal(uri, {});
+ Assert.equal(uri.asciiHostPort, "xn--jos-dma.example.net.ch:500");
+ Assert.equal(prin.origin, "http://xn--jos-dma.example.net.ch:500");
+
+ uri = ios.newURI("http://jos\u00e9.example.net.ch:5000/file.txt");
+ Assert.equal(uri.asciiHostPort, "xn--jos-dma.example.net.ch:5000");
+ uri = uri
+ .mutate()
+ .setPort(20)
+ .finalize();
+ prin = ssm.createContentPrincipal(uri, {});
+ Assert.equal(uri.asciiHostPort, "xn--jos-dma.example.net.ch:20");
+ Assert.equal(prin.origin, "http://xn--jos-dma.example.net.ch:20");
+
+ uri = ios.newURI("http://jos\u00e9.example.net.ch:5000/file.txt");
+ Assert.equal(uri.asciiHostPort, "xn--jos-dma.example.net.ch:5000");
+ uri = uri
+ .mutate()
+ .setPort(-1)
+ .finalize();
+ prin = ssm.createContentPrincipal(uri, {});
+ Assert.equal(uri.asciiHostPort, "xn--jos-dma.example.net.ch");
+ Assert.equal(prin.origin, "http://xn--jos-dma.example.net.ch");
+
+ uri = ios.newURI("http://jos\u00e9.example.net.ch:5000/file.txt");
+ Assert.equal(uri.asciiHostPort, "xn--jos-dma.example.net.ch:5000");
+ uri = uri
+ .mutate()
+ .setPort(80)
+ .finalize();
+ prin = ssm.createContentPrincipal(uri, {});
+ Assert.equal(uri.asciiHostPort, "xn--jos-dma.example.net.ch");
+ Assert.equal(prin.origin, "http://xn--jos-dma.example.net.ch");
+
+ // ipv6
+ uri = ios.newURI("http://[123:45::678]/file.txt");
+ Assert.equal(uri.asciiHostPort, "[123:45::678]");
+ uri = uri
+ .mutate()
+ .setPort(90)
+ .finalize();
+ prin = ssm.createContentPrincipal(uri, {});
+ Assert.equal(uri.asciiHostPort, "[123:45::678]:90");
+ Assert.equal(prin.origin, "http://[123:45::678]:90");
+
+ uri = ios.newURI("http://[123:45::678]:10/file.txt");
+ Assert.equal(uri.asciiHostPort, "[123:45::678]:10");
+ uri = uri
+ .mutate()
+ .setPort(500)
+ .finalize();
+ prin = ssm.createContentPrincipal(uri, {});
+ Assert.equal(uri.asciiHostPort, "[123:45::678]:500");
+ Assert.equal(prin.origin, "http://[123:45::678]:500");
+
+ uri = ios.newURI("http://[123:45::678]:5000/file.txt");
+ Assert.equal(uri.asciiHostPort, "[123:45::678]:5000");
+ uri = uri
+ .mutate()
+ .setPort(20)
+ .finalize();
+ prin = ssm.createContentPrincipal(uri, {});
+ Assert.equal(uri.asciiHostPort, "[123:45::678]:20");
+ Assert.equal(prin.origin, "http://[123:45::678]:20");
+
+ uri = ios.newURI("http://[123:45::678]:5000/file.txt");
+ Assert.equal(uri.asciiHostPort, "[123:45::678]:5000");
+ uri = uri
+ .mutate()
+ .setPort(-1)
+ .finalize();
+ prin = ssm.createContentPrincipal(uri, {});
+ Assert.equal(uri.asciiHostPort, "[123:45::678]");
+ Assert.equal(prin.origin, "http://[123:45::678]");
+
+ uri = ios.newURI("http://[123:45::678]:5000/file.txt");
+ Assert.equal(uri.asciiHostPort, "[123:45::678]:5000");
+ uri = uri
+ .mutate()
+ .setPort(80)
+ .finalize();
+ prin = ssm.createContentPrincipal(uri, {});
+ Assert.equal(uri.asciiHostPort, "[123:45::678]");
+ Assert.equal(prin.origin, "http://[123:45::678]");
+}
diff --git a/netwerk/test/unit/test_bug1218029.js b/netwerk/test/unit/test_bug1218029.js
new file mode 100644
index 0000000000..de49952623
--- /dev/null
+++ b/netwerk/test/unit/test_bug1218029.js
@@ -0,0 +1,116 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var tests = [
+ { data: "", chunks: [], status: Cr.NS_OK, consume: [], dataChunks: [""] },
+ {
+ data: "TWO-PARTS",
+ chunks: [4, 5],
+ status: Cr.NS_OK,
+ consume: [4, 5],
+ dataChunks: ["TWO-", "PARTS", ""],
+ },
+ {
+ data: "TWO-PARTS",
+ chunks: [4, 5],
+ status: Cr.NS_OK,
+ consume: [0, 0],
+ dataChunks: ["TWO-", "TWO-PARTS", "TWO-PARTS"],
+ },
+ {
+ data: "3-PARTS",
+ chunks: [1, 1, 5],
+ status: Cr.NS_OK,
+ consume: [0, 2, 5],
+ dataChunks: ["3", "3-", "PARTS", ""],
+ },
+ {
+ data: "ALL-AT-ONCE",
+ chunks: [11],
+ status: Cr.NS_OK,
+ consume: [0],
+ dataChunks: ["ALL-AT-ONCE", "ALL-AT-ONCE"],
+ },
+ {
+ data: "ALL-AT-ONCE",
+ chunks: [11],
+ status: Cr.NS_OK,
+ consume: [11],
+ dataChunks: ["ALL-AT-ONCE", ""],
+ },
+ {
+ data: "ERROR",
+ chunks: [1],
+ status: Cr.NS_ERROR_OUT_OF_MEMORY,
+ consume: [0],
+ dataChunks: ["E", "E"],
+ },
+];
+
+/**
+ * @typedef TestData
+ * @property {string} data - data for the test.
+ * @property {Array} chunks - lengths of the chunks that are incrementally sent
+ * to the loader.
+ * @property {number} status - final status sent on onStopRequest.
+ * @property {Array} consume - lengths of consumed data that is reported at
+ * the onIncrementalData callback.
+ * @property {Array} dataChunks - data chunks that are reported at the
+ * onIncrementalData and onStreamComplete callbacks.
+ */
+
+function execute_test(test) {
+ let stream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(
+ Ci.nsIStringInputStream
+ );
+ stream.data = test.data;
+
+ let channel = {
+ contentLength: -1,
+ QueryInterface: ChromeUtils.generateQI(["nsIChannel"]),
+ };
+
+ let chunkIndex = 0;
+
+ let observer = {
+ onStreamComplete(loader, context, status, length, data) {
+ equal(chunkIndex, test.dataChunks.length - 1);
+ var expectedChunk = test.dataChunks[chunkIndex];
+ equal(length, expectedChunk.length);
+ equal(String.fromCharCode.apply(null, data), expectedChunk);
+
+ equal(status, test.status);
+ },
+ onIncrementalData(loader, context, length, data, consumed) {
+ ok(chunkIndex < test.dataChunks.length - 1);
+ var expectedChunk = test.dataChunks[chunkIndex];
+ equal(length, expectedChunk.length);
+ equal(String.fromCharCode.apply(null, data), expectedChunk);
+
+ consumed.value = test.consume[chunkIndex];
+ chunkIndex++;
+ },
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIIncrementalStreamLoaderObserver",
+ ]),
+ };
+
+ let listener = Cc[
+ "@mozilla.org/network/incremental-stream-loader;1"
+ ].createInstance(Ci.nsIIncrementalStreamLoader);
+ listener.init(observer);
+
+ listener.onStartRequest(channel);
+ var offset = 0;
+ test.chunks.forEach(function(chunkLength) {
+ listener.onDataAvailable(channel, stream, offset, chunkLength);
+ offset += chunkLength;
+ });
+ listener.onStopRequest(channel, test.status);
+}
+
+function run_test() {
+ tests.forEach(execute_test);
+}
diff --git a/netwerk/test/unit/test_bug1279246.js b/netwerk/test/unit/test_bug1279246.js
new file mode 100644
index 0000000000..a0a8639cf3
--- /dev/null
+++ b/netwerk/test/unit/test_bug1279246.js
@@ -0,0 +1,98 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+var httpserver = new HttpServer();
+var pass = 0;
+var responseBody = [0x0b, 0x02, 0x80, 0x74, 0x65, 0x73, 0x74, 0x0a, 0x03];
+var responseLen = 5;
+var testUrl = "/test/brotli";
+
+function setupChannel() {
+ return NetUtil.newChannel({
+ uri: "http://localhost:" + httpserver.identity.primaryPort + testUrl,
+ loadUsingSystemPrincipal: true,
+ });
+}
+
+function Listener() {}
+
+Listener.prototype = {
+ _buffer: null,
+
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIStreamListener",
+ "nsIRequestObserver",
+ ]),
+
+ onStartRequest(request) {
+ Assert.equal(request.status, Cr.NS_OK);
+ this._buffer = "";
+ },
+
+ onDataAvailable(request, stream, offset, cnt) {
+ if (pass == 0) {
+ this._buffer = this._buffer.concat(read_stream(stream, cnt));
+ } else {
+ request.QueryInterface(Ci.nsICachingChannel);
+ if (!request.isFromCache()) {
+ do_throw("Response is not from the cache");
+ }
+
+ request.cancel(Cr.NS_ERROR_ABORT);
+ }
+ },
+
+ onStopRequest(request, status) {
+ if (pass == 0) {
+ Assert.equal(this._buffer.length, responseLen);
+ pass++;
+
+ var channel = setupChannel();
+ channel.loadFlags = Ci.nsIRequest.VALIDATE_NEVER;
+ channel.asyncOpen(new Listener());
+ } else {
+ httpserver.stop(do_test_finished);
+ prefs.setCharPref("network.http.accept-encoding", cePref);
+ }
+ },
+};
+
+var prefs;
+var cePref;
+function run_test() {
+ do_get_profile();
+
+ prefs = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch);
+ cePref = prefs.getCharPref("network.http.accept-encoding");
+ prefs.setCharPref("network.http.accept-encoding", "gzip, deflate, br");
+
+ // Disable rcwn to make cache behavior deterministic.
+ prefs.setBoolPref("network.http.rcwn.enabled", false);
+
+ httpserver.registerPathHandler(testUrl, handler);
+ httpserver.start(-1);
+
+ var channel = setupChannel();
+ channel.asyncOpen(new Listener());
+
+ do_test_pending();
+}
+
+function handler(metadata, response) {
+ Assert.equal(pass, 0); // the second response must be server from the cache
+
+ response.setStatusLine(metadata.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "text/plain", false);
+ response.setHeader("Content-Encoding", "br", false);
+ response.setHeader("Content-Length", "" + responseBody.length, false);
+
+ var bos = Cc["@mozilla.org/binaryoutputstream;1"].createInstance(
+ Ci.nsIBinaryOutputStream
+ );
+ bos.setOutputStream(response.bodyOutputStream);
+
+ response.processAsync();
+ bos.writeByteArray(responseBody);
+ response.finish();
+}
diff --git a/netwerk/test/unit/test_bug1312774_http1.js b/netwerk/test/unit/test_bug1312774_http1.js
new file mode 100644
index 0000000000..97a7f4d1eb
--- /dev/null
+++ b/netwerk/test/unit/test_bug1312774_http1.js
@@ -0,0 +1,153 @@
+// test bug 1312774.
+// Create 6 (=network.http.max-persistent-connections-per-server)
+// common Http requests and 2 urgent-start Http requests to a single
+// host and path, in parallel.
+// Let all the requests unanswered by the server handler. (process them
+// async and don't finish)
+// The first 6 pending common requests will fill the limit for per-server
+// parallelism.
+// But the two urgent requests must reach the server despite those 6 common
+// pending requests.
+// The server handler doesn't let the test finish until all 8 expected requests
+// arrive.
+// Note: if the urgent request handling is broken (the urgent-marked requests
+// get blocked by queuing) this test will time out
+
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+var server = new HttpServer();
+server.start(-1);
+var baseURL = "http://localhost:" + server.identity.primaryPort + "/";
+var maxConnections = 0;
+var urgentRequests = 0;
+var totalRequests = 0;
+var debug = false;
+
+function log(msg) {
+ if (!debug) {
+ return;
+ }
+
+ if (msg) {
+ dump("TEST INFO | " + msg + "\n");
+ }
+}
+
+function make_channel(url) {
+ var request = NetUtil.newChannel({
+ uri: url,
+ loadUsingSystemPrincipal: true,
+ });
+ request.QueryInterface(Ci.nsIHttpChannel);
+ return request;
+}
+
+function serverStopListener() {
+ server.stop();
+}
+
+function commonHttpRequest(id) {
+ let uri = baseURL;
+ var chan = make_channel(uri);
+ var listner = new HttpResponseListener(id);
+ chan.setRequestHeader("X-ID", id, false);
+ chan.setRequestHeader("Cache-control", "no-store", false);
+ chan.asyncOpen(listner);
+ log("Create common http request id=" + id);
+}
+
+function urgentStartHttpRequest(id) {
+ let uri = baseURL;
+ var chan = make_channel(uri);
+ var listner = new HttpResponseListener(id);
+ var cos = chan.QueryInterface(Ci.nsIClassOfService);
+ cos.addClassFlags(Ci.nsIClassOfService.UrgentStart);
+ chan.setRequestHeader("X-ID", id, false);
+ chan.setRequestHeader("Cache-control", "no-store", false);
+ chan.asyncOpen(listner);
+ log("Create urgent-start http request id=" + id);
+}
+
+function setup_httpRequests() {
+ log("setup_httpRequests");
+ for (var i = 0; i < maxConnections; i++) {
+ commonHttpRequest(i);
+ do_test_pending();
+ }
+}
+
+function setup_urgentStartRequests() {
+ for (var i = 0; i < urgentRequests; i++) {
+ urgentStartHttpRequest(1000 + i);
+ do_test_pending();
+ }
+}
+
+function HttpResponseListener(id) {
+ this.id = id;
+}
+
+var testOrder = 0;
+HttpResponseListener.prototype = {
+ onStartRequest(request) {},
+
+ onDataAvailable(request, stream, off, cnt) {},
+
+ onStopRequest(request, status) {
+ log("STOP id=" + this.id);
+ do_test_finished();
+ },
+};
+
+var responseQueue = [];
+function setup_http_server() {
+ log("setup_http_server");
+ var prefs = Cc["@mozilla.org/preferences-service;1"].getService(
+ Ci.nsIPrefBranch
+ );
+ maxConnections = prefs.getIntPref(
+ "network.http.max-persistent-connections-per-server"
+ );
+ urgentRequests = 2;
+ totalRequests = maxConnections + urgentRequests;
+ var allCommonHttpRequestReceived = false;
+ // Start server; will be stopped at test cleanup time.
+ server.registerPathHandler("/", function(metadata, response) {
+ var id = metadata.getHeader("X-ID");
+ log("Server recived the response id=" + id);
+ response.processAsync();
+ responseQueue.push(response);
+
+ if (
+ responseQueue.length == maxConnections &&
+ !allCommonHttpRequestReceived
+ ) {
+ allCommonHttpRequestReceived = true;
+ setup_urgentStartRequests();
+ }
+ // Wait for all expected requests to come but don't process then.
+ // Collect them in a queue for later processing. We don't want to
+ // respond to the client until all the expected requests are made
+ // to the server.
+ if (responseQueue.length == maxConnections + urgentRequests) {
+ processResponse();
+ }
+ });
+
+ registerCleanupFunction(function() {
+ server.stop(serverStopListener);
+ });
+}
+
+function processResponse() {
+ while (responseQueue.length) {
+ var resposne = responseQueue.pop();
+ resposne.finish();
+ }
+}
+
+function run_test() {
+ setup_http_server();
+ setup_httpRequests();
+}
diff --git a/netwerk/test/unit/test_bug1312782_http1.js b/netwerk/test/unit/test_bug1312782_http1.js
new file mode 100644
index 0000000000..4c13c2b4ab
--- /dev/null
+++ b/netwerk/test/unit/test_bug1312782_http1.js
@@ -0,0 +1,206 @@
+// test bug 1312782.
+//
+// Summary:
+// Assume we have 6 http requests in queue, 4 are from the focused window and
+// the other 2 are from the non-focused window. We want to test that the server
+// should receive 4 requests from the focused window first and then receive the
+// rest 2 requests.
+//
+// Test step:
+// 1. Create 6 dummy http requests. Server would not process responses until get
+// all 6 requests.
+// 2. Once server receive 6 dummy requests, create 4 http requests with the focused
+// window id and 2 requests with non-focused window id. Note that the requets's
+// id is a serial number starting from the focused window id.
+// 3. Server starts to process the 6 dummy http requests, so the client can start to
+// process the pending queue. Server will queue those http requests again and wait
+// until get all 6 requests.
+// 4. When the server receive all 6 requests, starts to check that the request ids of
+// the first 4 requests in the queue should be all less than focused window id
+// plus 4. Also, the request ids of the rest requests should be less than non-focused
+// window id + 2.
+
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+var server = new HttpServer();
+server.start(-1);
+var baseURL = "http://localhost:" + server.identity.primaryPort + "/";
+var maxConnections = 0;
+var debug = false;
+const FOCUSED_WINDOW_ID = 123;
+var NON_FOCUSED_WINDOW_ID;
+var FOCUSED_WINDOW_REQUEST_COUNT;
+var NON_FOCUSED_WINDOW_REQUEST_COUNT;
+
+function log(msg) {
+ if (!debug) {
+ return;
+ }
+
+ if (msg) {
+ dump("TEST INFO | " + msg + "\n");
+ }
+}
+
+function make_channel(url) {
+ var request = NetUtil.newChannel({
+ uri: url,
+ loadUsingSystemPrincipal: true,
+ });
+ request.QueryInterface(Ci.nsIHttpChannel);
+ return request;
+}
+
+function serverStopListener() {
+ server.stop();
+}
+
+function createHttpRequest(windowId, requestId) {
+ let uri = baseURL;
+ var chan = make_channel(uri);
+ chan.topLevelOuterContentWindowId = windowId;
+ var listner = new HttpResponseListener(requestId);
+ chan.setRequestHeader("X-ID", requestId, false);
+ chan.setRequestHeader("Cache-control", "no-store", false);
+ chan.asyncOpen(listner);
+ log("Create http request id=" + requestId);
+}
+
+function setup_dummyHttpRequests() {
+ log("setup_dummyHttpRequests");
+ for (var i = 0; i < maxConnections; i++) {
+ createHttpRequest(0, i);
+ do_test_pending();
+ }
+}
+
+function setup_focusedWindowHttpRequests() {
+ log("setup_focusedWindowHttpRequests");
+ for (var i = 0; i < FOCUSED_WINDOW_REQUEST_COUNT; i++) {
+ createHttpRequest(FOCUSED_WINDOW_ID, FOCUSED_WINDOW_ID + i);
+ do_test_pending();
+ }
+}
+
+function setup_nonFocusedWindowHttpRequests() {
+ log("setup_nonFocusedWindowHttpRequests");
+ for (var i = 0; i < NON_FOCUSED_WINDOW_REQUEST_COUNT; i++) {
+ createHttpRequest(NON_FOCUSED_WINDOW_ID, NON_FOCUSED_WINDOW_ID + i);
+ do_test_pending();
+ }
+}
+
+function HttpResponseListener(id) {
+ this.id = id;
+}
+
+HttpResponseListener.prototype = {
+ onStartRequest(request) {},
+
+ onDataAvailable(request, stream, off, cnt) {},
+
+ onStopRequest(request, status) {
+ log("STOP id=" + this.id);
+ do_test_finished();
+ },
+};
+
+function check_response_id(responses, maxWindowId) {
+ for (var i = 0; i < responses.length; i++) {
+ var id = responses[i].getHeader("X-ID");
+ log("response id=" + id + " maxWindowId=" + maxWindowId);
+ Assert.ok(id < maxWindowId);
+ }
+}
+
+var responseQueue = [];
+function setup_http_server() {
+ log("setup_http_server");
+ var prefs = Cc["@mozilla.org/preferences-service;1"].getService(
+ Ci.nsIPrefBranch
+ );
+ maxConnections = prefs.getIntPref(
+ "network.http.max-persistent-connections-per-server"
+ );
+ FOCUSED_WINDOW_REQUEST_COUNT = Math.floor(maxConnections * 0.8);
+ NON_FOCUSED_WINDOW_REQUEST_COUNT =
+ maxConnections - FOCUSED_WINDOW_REQUEST_COUNT;
+ NON_FOCUSED_WINDOW_ID = FOCUSED_WINDOW_ID + FOCUSED_WINDOW_REQUEST_COUNT;
+
+ var allDummyHttpRequestReceived = false;
+ // Start server; will be stopped at test cleanup time.
+ server.registerPathHandler("/", function(metadata, response) {
+ var id = metadata.getHeader("X-ID");
+ log("Server recived the response id=" + id);
+
+ response.processAsync();
+ response.setHeader("X-ID", id);
+ responseQueue.push(response);
+
+ if (
+ responseQueue.length == maxConnections &&
+ !allDummyHttpRequestReceived
+ ) {
+ log("received all dummy http requets");
+ allDummyHttpRequestReceived = true;
+ setup_nonFocusedWindowHttpRequests();
+ setup_focusedWindowHttpRequests();
+ processResponses();
+ } else if (responseQueue.length == maxConnections) {
+ var focusedWindowResponses = responseQueue.slice(
+ 0,
+ FOCUSED_WINDOW_REQUEST_COUNT
+ );
+ var nonFocusedWindowResponses = responseQueue.slice(
+ FOCUSED_WINDOW_REQUEST_COUNT,
+ responseQueue.length
+ );
+ check_response_id(
+ focusedWindowResponses,
+ FOCUSED_WINDOW_ID + FOCUSED_WINDOW_REQUEST_COUNT
+ );
+ check_response_id(
+ nonFocusedWindowResponses,
+ NON_FOCUSED_WINDOW_ID + NON_FOCUSED_WINDOW_REQUEST_COUNT
+ );
+ processResponses();
+ }
+ });
+
+ registerCleanupFunction(function() {
+ server.stop(serverStopListener);
+ });
+}
+
+function processResponses() {
+ while (responseQueue.length) {
+ var resposne = responseQueue.pop();
+ resposne.finish();
+ }
+}
+
+function run_test() {
+ // Make sure "network.http.active_tab_priority" is true, so we can expect to
+ // receive http requests with focused window id before others.
+ var prefs = Cc["@mozilla.org/preferences-service;1"].getService(
+ Ci.nsIPrefBranch
+ );
+ prefs.setBoolPref("network.http.active_tab_priority", true);
+
+ setup_http_server();
+ setup_dummyHttpRequests();
+
+ var windowIdWrapper = Cc["@mozilla.org/supports-PRUint64;1"].createInstance(
+ Ci.nsISupportsPRUint64
+ );
+ windowIdWrapper.data = FOCUSED_WINDOW_ID;
+ var obsvc = Cc["@mozilla.org/observer-service;1"].getService(
+ Ci.nsIObserverService
+ );
+ obsvc.notifyObservers(
+ windowIdWrapper,
+ "net:current-toplevel-outer-content-windowid"
+ );
+}
diff --git a/netwerk/test/unit/test_bug1355539_http1.js b/netwerk/test/unit/test_bug1355539_http1.js
new file mode 100644
index 0000000000..55bc890d66
--- /dev/null
+++ b/netwerk/test/unit/test_bug1355539_http1.js
@@ -0,0 +1,207 @@
+// test bug 1355539.
+//
+// Summary:
+// Transactions in one pending queue are splited into two groups:
+// [(Blocking Group)|(Non Blocking Group)]
+// In each group, the transactions are ordered by its priority.
+// This test will check if the transaction's order in pending queue is correct.
+//
+// Test step:
+// 1. Create 6 dummy http requests. Server would not process responses until get
+// all 6 requests.
+// 2. Once server receive 6 dummy requests, create another 6 http requests with the
+// defined priority and class flag in |transactionQueue|.
+// 3. Server starts to process the 6 dummy http requests, so the client can start to
+// process the pending queue. Server will queue those http requests and put them in
+// |responseQueue|.
+// 4. When the server receive all 6 requests, check if the order in |responseQueue| is
+// equal to |transactionQueue| by comparing the value of X-ID.
+
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+var server = new HttpServer();
+server.start(-1);
+var baseURL = "http://localhost:" + server.identity.primaryPort + "/";
+var maxConnections = 0;
+var debug = false;
+var dummyResponseQueue = [];
+var responseQueue = [];
+
+function log(msg) {
+ if (!debug) {
+ return;
+ }
+
+ if (msg) {
+ dump("TEST INFO | " + msg + "\n");
+ }
+}
+
+function make_channel(url) {
+ var request = NetUtil.newChannel({
+ uri: url,
+ loadUsingSystemPrincipal: true,
+ });
+ request.QueryInterface(Ci.nsIHttpChannel);
+ return request;
+}
+
+function serverStopListener() {
+ server.stop();
+}
+
+function createHttpRequest(requestId, priority, isBlocking, callback) {
+ let uri = baseURL;
+ var chan = make_channel(uri);
+ var listner = new HttpResponseListener(requestId, callback);
+ chan.setRequestHeader("X-ID", requestId, false);
+ chan.setRequestHeader("Cache-control", "no-store", false);
+ chan.QueryInterface(Ci.nsISupportsPriority).priority = priority;
+ if (isBlocking) {
+ var cos = chan.QueryInterface(Ci.nsIClassOfService);
+ cos.addClassFlags(Ci.nsIClassOfService.Leader);
+ }
+ chan.asyncOpen(listner);
+ log("Create http request id=" + requestId);
+}
+
+function setup_dummyHttpRequests(callback) {
+ log("setup_dummyHttpRequests");
+ for (var i = 0; i < maxConnections; i++) {
+ createHttpRequest(i, i, false, callback);
+ do_test_pending();
+ }
+}
+
+var transactionQueue = [
+ {
+ requestId: 101,
+ priority: Ci.nsISupportsPriority.PRIORITY_HIGH,
+ isBlocking: true,
+ },
+ {
+ requestId: 102,
+ priority: Ci.nsISupportsPriority.PRIORITY_NORMAL,
+ isBlocking: true,
+ },
+ {
+ requestId: 103,
+ priority: Ci.nsISupportsPriority.PRIORITY_LOW,
+ isBlocking: true,
+ },
+ {
+ requestId: 104,
+ priority: Ci.nsISupportsPriority.PRIORITY_HIGH,
+ isBlocking: false,
+ },
+ {
+ requestId: 105,
+ priority: Ci.nsISupportsPriority.PRIORITY_NORMAL,
+ isBlocking: false,
+ },
+ {
+ requestId: 106,
+ priority: Ci.nsISupportsPriority.PRIORITY_LOW,
+ isBlocking: false,
+ },
+];
+
+function setup_HttpRequests() {
+ log("setup_HttpRequests");
+ // Create channels in reverse order
+ for (var i = transactionQueue.length - 1; i > -1; ) {
+ var e = transactionQueue[i];
+ createHttpRequest(e.requestId, e.priority, e.isBlocking);
+ do_test_pending();
+ --i;
+ }
+}
+
+function check_response_id(responses) {
+ for (var i = 0; i < responses.length; i++) {
+ var id = responses[i].getHeader("X-ID");
+ Assert.equal(id, transactionQueue[i].requestId);
+ }
+}
+
+function HttpResponseListener(id, onStopCallback) {
+ this.id = id;
+ this.stopCallback = onStopCallback;
+}
+
+HttpResponseListener.prototype = {
+ onStartRequest(request) {},
+
+ onDataAvailable(request, stream, off, cnt) {},
+
+ onStopRequest(request, status) {
+ log("STOP id=" + this.id);
+ do_test_finished();
+ if (this.stopCallback) {
+ this.stopCallback();
+ }
+ },
+};
+
+function setup_http_server() {
+ log("setup_http_server");
+ var prefs = Cc["@mozilla.org/preferences-service;1"].getService(
+ Ci.nsIPrefBranch
+ );
+ maxConnections = prefs.getIntPref(
+ "network.http.max-persistent-connections-per-server"
+ );
+
+ var allDummyHttpRequestReceived = false;
+ // Start server; will be stopped at test cleanup time.
+ server.registerPathHandler("/", function(metadata, response) {
+ var id = metadata.getHeader("X-ID");
+ log("Server recived the response id=" + id);
+
+ response.processAsync();
+ response.setHeader("X-ID", id);
+
+ if (!allDummyHttpRequestReceived) {
+ dummyResponseQueue.push(response);
+ } else {
+ responseQueue.push(response);
+ }
+
+ if (dummyResponseQueue.length == maxConnections) {
+ log("received all dummy http requets");
+ allDummyHttpRequestReceived = true;
+ setup_HttpRequests();
+ processDummyResponse();
+ } else if (responseQueue.length == maxConnections) {
+ log("received all http requets");
+ check_response_id(responseQueue);
+ processResponses();
+ }
+ });
+
+ registerCleanupFunction(function() {
+ server.stop(serverStopListener);
+ });
+}
+
+function processDummyResponse() {
+ if (!dummyResponseQueue.length) {
+ return;
+ }
+ var resposne = dummyResponseQueue.pop();
+ resposne.finish();
+}
+
+function processResponses() {
+ while (responseQueue.length) {
+ var resposne = responseQueue.pop();
+ resposne.finish();
+ }
+}
+
+function run_test() {
+ setup_http_server();
+ setup_dummyHttpRequests(processDummyResponse);
+}
diff --git a/netwerk/test/unit/test_bug1378385_http1.js b/netwerk/test/unit/test_bug1378385_http1.js
new file mode 100644
index 0000000000..e14a9e6a99
--- /dev/null
+++ b/netwerk/test/unit/test_bug1378385_http1.js
@@ -0,0 +1,202 @@
+// test bug 1378385.
+//
+// Summary:
+// Assume we have 6 http requests in queue, 3 are from the focused window with
+// normal priority and the other 3 are from the non-focused window with the
+// highest priority.
+// We want to test that when "network.http.active_tab_priority" is false,
+// the server should receive 3 requests with the highest priority first
+// and then receive the rest 3 requests.
+//
+// Test step:
+// 1. Create 6 dummy http requests. Server would not process responses until told
+// all 6 requests.
+// 2. Once server receive 6 dummy requests, create 3 http requests with the focused
+// window id and normal priority and 3 requests with non-focused window id and
+// the highrst priority.
+// Note that the requets's id is set to its window id.
+// 3. Server starts to process the 6 dummy http requests, so the client can start to
+// process the pending queue. Server will queue those http requests again and wait
+// until get all 6 requests.
+// 4. When the server receive all 6 requests, we want to check if 3 requests with higher
+// priority are sent before others.
+// First, we check that if the request id of the first 3 requests in the queue is
+// equal to non focused window id.
+// Second, we check if the request id of the rest requests is equal to focused
+// window id.
+
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+var server = new HttpServer();
+server.start(-1);
+var baseURL = "http://localhost:" + server.identity.primaryPort + "/";
+var maxConnections = 0;
+var debug = false;
+const FOCUSED_WINDOW_ID = 123;
+var NON_FOCUSED_WINDOW_ID;
+var FOCUSED_WINDOW_REQUEST_COUNT;
+var NON_FOCUSED_WINDOW_REQUEST_COUNT;
+
+function log(msg) {
+ if (!debug) {
+ return;
+ }
+
+ if (msg) {
+ dump("TEST INFO | " + msg + "\n");
+ }
+}
+
+function make_channel(url) {
+ var request = NetUtil.newChannel({
+ uri: url,
+ loadUsingSystemPrincipal: true,
+ });
+ request.QueryInterface(Ci.nsIHttpChannel);
+ return request;
+}
+
+function serverStopListener() {
+ server.stop();
+}
+
+function createHttpRequest(windowId, requestId, priority) {
+ let uri = baseURL;
+ var chan = make_channel(uri);
+ chan.topLevelOuterContentWindowId = windowId;
+ chan.QueryInterface(Ci.nsISupportsPriority).priority = priority;
+ var listner = new HttpResponseListener(requestId);
+ chan.setRequestHeader("X-ID", requestId, false);
+ chan.setRequestHeader("Cache-control", "no-store", false);
+ chan.asyncOpen(listner);
+ log("Create http request id=" + requestId);
+}
+
+function setup_dummyHttpRequests() {
+ log("setup_dummyHttpRequests");
+ for (var i = 0; i < maxConnections; i++) {
+ createHttpRequest(0, i, Ci.nsISupportsPriority.PRIORITY_NORMAL);
+ do_test_pending();
+ }
+}
+
+function setup_focusedWindowHttpRequests() {
+ log("setup_focusedWindowHttpRequests");
+ for (var i = 0; i < FOCUSED_WINDOW_REQUEST_COUNT; i++) {
+ createHttpRequest(
+ FOCUSED_WINDOW_ID,
+ FOCUSED_WINDOW_ID,
+ Ci.nsISupportsPriority.PRIORITY_NORMAL
+ );
+ do_test_pending();
+ }
+}
+
+function setup_nonFocusedWindowHttpRequests() {
+ log("setup_nonFocusedWindowHttpRequests");
+ for (var i = 0; i < NON_FOCUSED_WINDOW_REQUEST_COUNT; i++) {
+ createHttpRequest(
+ NON_FOCUSED_WINDOW_ID,
+ NON_FOCUSED_WINDOW_ID,
+ Ci.nsISupportsPriority.PRIORITY_HIGHEST
+ );
+ do_test_pending();
+ }
+}
+
+function HttpResponseListener(id) {
+ this.id = id;
+}
+
+HttpResponseListener.prototype = {
+ onStartRequest(request) {},
+
+ onDataAvailable(request, stream, off, cnt) {},
+
+ onStopRequest(request, status) {
+ log("STOP id=" + this.id);
+ do_test_finished();
+ },
+};
+
+function check_response_id(responses, windowId) {
+ for (var i = 0; i < responses.length; i++) {
+ var id = responses[i].getHeader("X-ID");
+ log("response id=" + id + " windowId=" + windowId);
+ Assert.equal(id, windowId);
+ }
+}
+
+var responseQueue = [];
+function setup_http_server() {
+ log("setup_http_server");
+ var prefs = Cc["@mozilla.org/preferences-service;1"].getService(
+ Ci.nsIPrefBranch
+ );
+ maxConnections = prefs.getIntPref(
+ "network.http.max-persistent-connections-per-server"
+ );
+ FOCUSED_WINDOW_REQUEST_COUNT = Math.floor(maxConnections * 0.5);
+ NON_FOCUSED_WINDOW_REQUEST_COUNT =
+ maxConnections - FOCUSED_WINDOW_REQUEST_COUNT;
+ NON_FOCUSED_WINDOW_ID = FOCUSED_WINDOW_ID + FOCUSED_WINDOW_REQUEST_COUNT;
+
+ var allDummyHttpRequestReceived = false;
+ // Start server; will be stopped at test cleanup time.
+ server.registerPathHandler("/", function(metadata, response) {
+ var id = metadata.getHeader("X-ID");
+ log("Server recived the response id=" + id);
+
+ response.processAsync();
+ response.setHeader("X-ID", id);
+ responseQueue.push(response);
+
+ if (
+ responseQueue.length == maxConnections &&
+ !allDummyHttpRequestReceived
+ ) {
+ log("received all dummy http requets");
+ allDummyHttpRequestReceived = true;
+ setup_nonFocusedWindowHttpRequests();
+ setup_focusedWindowHttpRequests();
+ processResponses();
+ } else if (responseQueue.length == maxConnections) {
+ var nonFocusedWindowResponses = responseQueue.slice(
+ 0,
+ NON_FOCUSED_WINDOW_REQUEST_COUNT
+ );
+ var focusedWindowResponses = responseQueue.slice(
+ NON_FOCUSED_WINDOW_REQUEST_COUNT,
+ responseQueue.length
+ );
+ check_response_id(nonFocusedWindowResponses, NON_FOCUSED_WINDOW_ID);
+ check_response_id(focusedWindowResponses, FOCUSED_WINDOW_ID);
+ processResponses();
+ }
+ });
+
+ registerCleanupFunction(function() {
+ server.stop(serverStopListener);
+ });
+}
+
+function processResponses() {
+ while (responseQueue.length) {
+ var resposne = responseQueue.pop();
+ resposne.finish();
+ }
+}
+
+function run_test() {
+ // Set "network.http.active_tab_priority" to false, so we can expect to
+ // receive http requests with higher priority first.
+ var prefs = Cc["@mozilla.org/preferences-service;1"].getService(
+ Ci.nsIPrefBranch
+ );
+ prefs.setBoolPref("network.http.active_tab_priority", false);
+
+ setup_http_server();
+ setup_dummyHttpRequests();
+}
diff --git a/netwerk/test/unit/test_bug1411316_http1.js b/netwerk/test/unit/test_bug1411316_http1.js
new file mode 100644
index 0000000000..e41615688e
--- /dev/null
+++ b/netwerk/test/unit/test_bug1411316_http1.js
@@ -0,0 +1,117 @@
+// Test bug 1411316.
+//
+// Summary:
+// The purpose of this test is to test whether the HttpConnectionMgr really
+// cancel and close all connecitons when get "net:cancel-all-connections".
+//
+// Test step:
+// 1. Create 6 http requests. Server would not process responses and just put
+// all requests in its queue.
+// 2. Once server receive all 6 requests, call notifyObservers with the
+// topic "net:cancel-all-connections".
+// 3. We expect that all 6 active connections should be closed with the status
+// NS_ERROR_ABORT.
+
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+var server = new HttpServer();
+server.start(-1);
+var baseURL = "http://localhost:" + server.identity.primaryPort + "/";
+var maxConnections = 0;
+var debug = false;
+var requestId = 0;
+
+function log(msg) {
+ if (!debug) {
+ return;
+ }
+
+ if (msg) {
+ dump("TEST INFO | " + msg + "\n");
+ }
+}
+
+function make_channel(url) {
+ var request = NetUtil.newChannel({
+ uri: url,
+ loadUsingSystemPrincipal: true,
+ });
+ request.QueryInterface(Ci.nsIHttpChannel);
+ return request;
+}
+
+function serverStopListener() {
+ server.stop();
+}
+
+function createHttpRequest(status) {
+ let uri = baseURL;
+ var chan = make_channel(uri);
+ var listner = new HttpResponseListener(++requestId, status);
+ chan.setRequestHeader("X-ID", requestId, false);
+ chan.setRequestHeader("Cache-control", "no-store", false);
+ chan.asyncOpen(listner);
+ log("Create http request id=" + requestId);
+}
+
+function setupHttpRequests(status) {
+ log("setupHttpRequests");
+ for (var i = 0; i < maxConnections; i++) {
+ createHttpRequest(status);
+ do_test_pending();
+ }
+}
+
+function HttpResponseListener(id, onStopRequestStatus) {
+ this.id = id;
+ this.onStopRequestStatus = onStopRequestStatus;
+}
+
+HttpResponseListener.prototype = {
+ onStartRequest(request) {},
+
+ onDataAvailable(request, stream, off, cnt) {},
+
+ onStopRequest(request, status) {
+ log("STOP id=" + this.id + " status=" + status);
+ Assert.ok(this.onStopRequestStatus == status);
+ do_test_finished();
+ },
+};
+
+var responseQueue = [];
+function setup_http_server() {
+ log("setup_http_server");
+ var prefs = Cc["@mozilla.org/preferences-service;1"].getService(
+ Ci.nsIPrefBranch
+ );
+ maxConnections = prefs.getIntPref(
+ "network.http.max-persistent-connections-per-server"
+ );
+
+ // Start server; will be stopped at test cleanup time.
+ server.registerPathHandler("/", function(metadata, response) {
+ var id = metadata.getHeader("X-ID");
+ log("Server recived the response id=" + id);
+
+ response.processAsync();
+ response.setHeader("X-ID", id);
+ responseQueue.push(response);
+
+ if (responseQueue.length == maxConnections) {
+ log("received all http requets");
+ Services.obs.notifyObservers(null, "net:cancel-all-connections");
+ }
+ });
+
+ registerCleanupFunction(function() {
+ server.stop(serverStopListener);
+ });
+}
+
+function run_test() {
+ setup_http_server();
+ setupHttpRequests(Cr.NS_ERROR_ABORT);
+}
diff --git a/netwerk/test/unit/test_bug1527293.js b/netwerk/test/unit/test_bug1527293.js
new file mode 100644
index 0000000000..9f4ab24008
--- /dev/null
+++ b/netwerk/test/unit/test_bug1527293.js
@@ -0,0 +1,92 @@
+// Test bug 1527293
+//
+// Summary:
+// The purpose of this test is to check that a cache entry is doomed and not
+// reused when we don't write the content due to max entry size limit.
+//
+// Test step:
+// 1. Create http request for an entry whose size is bigger than we allow to
+// cache. The response must contain Content-Range header so the content size
+// is known in advance, but it must not contain Content-Length header because
+// the bug isn't reproducible with it.
+// 2. After receiving and checking the content do the same request again.
+// 3. Check that the request isn't conditional, i.e. the entry from previous
+// load was doomed.
+
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+XPCOMUtils.defineLazyGetter(this, "URL", function() {
+ return "http://localhost:" + httpServer.identity.primaryPort;
+});
+
+var httpServer = null;
+
+function make_channel(url, callback, ctx) {
+ return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true });
+}
+
+// need something bigger than 1024 bytes
+const responseBody =
+ "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" +
+ "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" +
+ "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" +
+ "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" +
+ "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" +
+ "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" +
+ "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" +
+ "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" +
+ "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" +
+ "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" +
+ "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
+
+function contentHandler(metadata, response) {
+ response.setHeader("Content-Type", "text/plain");
+ response.setHeader("ETag", "Just testing");
+ response.setHeader("Cache-Control", "max-age=99999");
+
+ Assert.throws(
+ () => {
+ metadata.getHeader("If-None-Match");
+ },
+ /NS_ERROR_NOT_AVAILABLE/,
+ "conditional request not expected"
+ );
+
+ response.setHeader("Accept-Ranges", "bytes");
+ let len = responseBody.length;
+ response.setHeader("Content-Range", "0-" + (len - 1) + "/" + len);
+ response.bodyOutputStream.write(responseBody, responseBody.length);
+}
+
+function run_test() {
+ // Static check
+ Assert.ok(responseBody.length > 1024);
+
+ do_get_profile();
+
+ Services.prefs.setIntPref("browser.cache.disk.max_entry_size", 1);
+ Services.prefs.setBoolPref("network.http.rcwn.enabled", false);
+
+ httpServer = new HttpServer();
+ httpServer.registerPathHandler("/content", contentHandler);
+ httpServer.start(-1);
+
+ var chan = make_channel(URL + "/content");
+ chan.asyncOpen(new ChannelListener(firstTimeThrough, null));
+
+ do_test_pending();
+}
+
+function firstTimeThrough(request, buffer) {
+ Assert.equal(buffer, responseBody);
+
+ var chan = make_channel(URL + "/content");
+ chan.asyncOpen(new ChannelListener(secondTimeThrough, null));
+}
+
+function secondTimeThrough(request, buffer) {
+ Assert.equal(buffer, responseBody);
+ httpServer.stop(do_test_finished);
+}
diff --git a/netwerk/test/unit/test_bug1683176.js b/netwerk/test/unit/test_bug1683176.js
new file mode 100644
index 0000000000..16b1ff4a3f
--- /dev/null
+++ b/netwerk/test/unit/test_bug1683176.js
@@ -0,0 +1,104 @@
+// Test bug 1683176
+//
+// Summary:
+// Test the case when a channel is cancelled when "negotiate" authentication
+// is processing.
+//
+
+"use strict";
+
+ChromeUtils.import("resource://gre/modules/NetUtil.jsm");
+
+let prefs;
+let httpserv;
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+XPCOMUtils.defineLazyGetter(this, "URL", function() {
+ return "http://localhost:" + httpserv.identity.primaryPort;
+});
+
+XPCOMUtils.defineLazyGetter(this, "PORT", function() {
+ return httpserv.identity.primaryPort;
+});
+
+function makeChan(url, loadingUrl) {
+ var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
+ var ssm = Cc["@mozilla.org/scriptsecuritymanager;1"].getService(
+ Ci.nsIScriptSecurityManager
+ );
+ var principal = ssm.createContentPrincipal(ios.newURI(loadingUrl), {});
+ return NetUtil.newChannel({
+ uri: url,
+ loadingPrincipal: principal,
+ securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER,
+ });
+}
+
+function authHandler(metadata, response) {
+ var body = "blablabla";
+
+ response.seizePower();
+ response.write("HTTP/1.1 401 Unauthorized\r\n");
+ response.write("WWW-Authenticate: Negotiate\r\n");
+ response.write("WWW-Authenticate: Basic realm=test\r\n");
+ response.write("\r\n");
+ response.write(body);
+ response.finish();
+}
+
+function setup() {
+ prefs = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch);
+
+ prefs.setIntPref("network.auth.subresource-http-auth-allow", 2);
+ prefs.setStringPref("network.negotiate-auth.trusted-uris", "localhost");
+
+ httpserv = new HttpServer();
+ httpserv.registerPathHandler("/auth", authHandler);
+ httpserv.start(-1);
+}
+
+setup();
+registerCleanupFunction(async () => {
+ prefs.clearUserPref("network.auth.subresource-http-auth-allow");
+ prefs.clearUserPref("network.negotiate-auth.trusted-uris");
+ await httpserv.stop();
+});
+
+function makeChan(url) {
+ let chan = NetUtil.newChannel({
+ uri: url,
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+ return chan;
+}
+
+function channelOpenPromise(chan) {
+ return new Promise(resolve => {
+ let topic = "http-on-transaction-suspended-authentication";
+ let observer = {
+ QueryInterface: ChromeUtils.generateQI(["nsIObserver"]),
+ observe(aSubject, aTopic, aData) {
+ if (aTopic == topic) {
+ Services.obs.removeObserver(observer, topic);
+ let channel = aSubject.QueryInterface(Ci.nsIChannel);
+ channel.cancel(Cr.NS_BINDING_ABORTED);
+ resolve();
+ }
+ },
+ };
+ Services.obs.addObserver(observer, topic);
+
+ chan.asyncOpen(new ChannelListener(finish, null, CL_EXPECT_FAILURE));
+ function finish() {
+ resolve();
+ }
+ });
+}
+
+add_task(async function testCancelAuthentication() {
+ let chan = makeChan(URL + "/auth", URL);
+ await channelOpenPromise(chan);
+ Assert.equal(chan.status, Cr.NS_BINDING_ABORTED);
+});
diff --git a/netwerk/test/unit/test_bug203271.js b/netwerk/test/unit/test_bug203271.js
new file mode 100644
index 0000000000..9801d5c5d4
--- /dev/null
+++ b/netwerk/test/unit/test_bug203271.js
@@ -0,0 +1,248 @@
+//
+// Tests if a response with an Expires-header in the past
+// and Cache-Control: max-age in the future works as
+// specified in RFC 2616 section 14.9.3 by letting max-age
+// take precedence
+
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+const BUGID = "203271";
+
+var httpserver = new HttpServer();
+var index = 0;
+var tests = [
+ // original problem described in bug#203271
+ {
+ url: "/precedence",
+ server: "0",
+ expected: "0",
+ responseheader: [
+ "Expires: " + getDateString(-1),
+ "Cache-Control: max-age=3600",
+ ],
+ },
+
+ {
+ url: "/precedence?0",
+ server: "0",
+ expected: "0",
+ responseheader: [
+ "Cache-Control: max-age=3600",
+ "Expires: " + getDateString(-1),
+ ],
+ },
+
+ // max-age=1s, expires=1 year from now
+ {
+ url: "/precedence?1",
+ server: "0",
+ expected: "0",
+ responseheader: [
+ "Expires: " + getDateString(1),
+ "Cache-Control: max-age=1",
+ ],
+ },
+
+ // expires=now
+ {
+ url: "/precedence?2",
+ server: "0",
+ expected: "0",
+ responseheader: ["Expires: " + getDateString(0)],
+ },
+
+ // max-age=1s
+ {
+ url: "/precedence?3",
+ server: "0",
+ expected: "0",
+ responseheader: ["Cache-Control: max-age=1"],
+ },
+
+ // The test below is the example from
+ //
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=203271#c27
+ //
+ // max-age=2592000s (1 month), expires=1 year from now, date=1 year ago
+ {
+ url: "/precedence?4",
+ server: "0",
+ expected: "0",
+ responseheader: [
+ "Cache-Control: private, max-age=2592000",
+ "Expires: " + getDateString(+1),
+ ],
+ explicitDate: getDateString(-1),
+ },
+
+ // The two tests below are also examples of clocks really out of synch
+ // max-age=1s, date=1 year from now
+ {
+ url: "/precedence?5",
+ server: "0",
+ expected: "0",
+ responseheader: ["Cache-Control: max-age=1"],
+ explicitDate: getDateString(1),
+ },
+
+ // max-age=60s, date=1 year from now
+ {
+ url: "/precedence?6",
+ server: "0",
+ expected: "0",
+ responseheader: ["Cache-Control: max-age=60"],
+ explicitDate: getDateString(1),
+ },
+
+ // this is just to get a pause of 3s to allow cache-entries to expire
+ { url: "/precedence?999", server: "0", expected: "0", delay: "3000" },
+
+ // Below are the cases which actually matters
+ { url: "/precedence", server: "1", expected: "0" }, // should be cached
+
+ { url: "/precedence?0", server: "1", expected: "0" }, // should be cached
+
+ { url: "/precedence?1", server: "1", expected: "1" }, // should have expired
+
+ { url: "/precedence?2", server: "1", expected: "1" }, // should have expired
+
+ { url: "/precedence?3", server: "1", expected: "1" }, // should have expired
+
+ { url: "/precedence?4", server: "1", expected: "1" }, // should have expired
+
+ { url: "/precedence?5", server: "1", expected: "1" }, // should have expired
+
+ { url: "/precedence?6", server: "1", expected: "0" }, // should be cached
+];
+
+function logit(i, data, ctx) {
+ dump(
+ "requested [" +
+ tests[i].server +
+ "] " +
+ "got [" +
+ data +
+ "] " +
+ "expected [" +
+ tests[i].expected +
+ "]"
+ );
+
+ if (tests[i].responseheader) {
+ dump("\t[" + tests[i].responseheader + "]");
+ }
+ dump("\n");
+ // Dump all response-headers
+ dump("\n===================================\n");
+ ctx.visitResponseHeaders({
+ visitHeader(key, val) {
+ dump("\t" + key + ":" + val + "\n");
+ },
+ });
+ dump("===================================\n");
+}
+
+function setupChannel(suffix, value) {
+ var chan = NetUtil.newChannel({
+ uri: "http://localhost:" + httpserver.identity.primaryPort + suffix,
+ loadUsingSystemPrincipal: true,
+ });
+ var httpChan = chan.QueryInterface(Ci.nsIHttpChannel);
+ httpChan.requestMethod = "GET"; // default value, just being paranoid...
+ httpChan.setRequestHeader("x-request", value, false);
+ return httpChan;
+}
+
+function triggerNextTest() {
+ var channel = setupChannel(tests[index].url, tests[index].server);
+ channel.asyncOpen(new ChannelListener(checkValueAndTrigger, channel));
+}
+
+function checkValueAndTrigger(request, data, ctx) {
+ logit(index, data, ctx);
+ Assert.equal(tests[index].expected, data);
+
+ if (index < tests.length - 1) {
+ var delay = tests[index++].delay;
+ if (delay) {
+ do_timeout(delay, triggerNextTest);
+ } else {
+ triggerNextTest();
+ }
+ } else {
+ httpserver.stop(do_test_finished);
+ }
+}
+
+function run_test() {
+ httpserver.registerPathHandler("/precedence", handler);
+ httpserver.start(-1);
+
+ // clear cache
+ evict_cache_entries();
+
+ triggerNextTest();
+ do_test_pending();
+}
+
+function handler(metadata, response) {
+ var body = metadata.getHeader("x-request");
+ response.setHeader("Content-Type", "text/plain", false);
+
+ var date = tests[index].explicitDate;
+ if (date == undefined) {
+ response.setHeader("Date", getDateString(0), false);
+ } else {
+ response.setHeader("Date", date, false);
+ }
+
+ var header = tests[index].responseheader;
+ if (header == undefined) {
+ response.setHeader("Last-Modified", getDateString(-1), false);
+ } else {
+ for (var i = 0; i < header.length; i++) {
+ var splitHdr = header[i].split(": ");
+ response.setHeader(splitHdr[0], splitHdr[1], false);
+ }
+ }
+
+ response.setStatusLine(metadata.httpVersion, 200, "OK");
+ response.bodyOutputStream.write(body, body.length);
+}
+
+function getDateString(yearDelta) {
+ var months = [
+ "Jan",
+ "Feb",
+ "Mar",
+ "Apr",
+ "May",
+ "Jun",
+ "Jul",
+ "Aug",
+ "Sep",
+ "Oct",
+ "Nov",
+ "Dec",
+ ];
+ var days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
+
+ var d = new Date();
+ return (
+ days[d.getUTCDay()] +
+ ", " +
+ d.getUTCDate() +
+ " " +
+ months[d.getUTCMonth()] +
+ " " +
+ (d.getUTCFullYear() + yearDelta) +
+ " " +
+ d.getUTCHours() +
+ ":" +
+ d.getUTCMinutes() +
+ ":" +
+ d.getUTCSeconds() +
+ " UTC"
+ );
+}
diff --git a/netwerk/test/unit/test_bug248970_cache.js b/netwerk/test/unit/test_bug248970_cache.js
new file mode 100644
index 0000000000..2b7521565d
--- /dev/null
+++ b/netwerk/test/unit/test_bug248970_cache.js
@@ -0,0 +1,168 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+// names for cache devices
+const kDiskDevice = "disk";
+const kMemoryDevice = "memory";
+const kOfflineDevice = "appcache";
+
+const kCacheA = "http://cache/A";
+const kCacheA2 = "http://cache/A2";
+const kCacheB = "http://cache/B";
+const kCacheC = "http://cache/C";
+const kTestContent = "test content";
+
+function make_input_stream_scriptable(input) {
+ var wrapper = Cc["@mozilla.org/scriptableinputstream;1"].createInstance(
+ Ci.nsIScriptableInputStream
+ );
+ wrapper.init(input);
+ return wrapper;
+}
+
+const entries = [
+ // key content device should exist after leaving PB
+ [kCacheA, kTestContent, kMemoryDevice, true],
+ [kCacheA2, kTestContent, kDiskDevice, false],
+ [kCacheB, kTestContent, kDiskDevice, true],
+ [kCacheC, kTestContent, kOfflineDevice, true],
+];
+
+var store_idx;
+var store_cb = null;
+var appCache = null;
+
+function store_entries(cb) {
+ if (cb) {
+ store_cb = cb;
+ store_idx = 0;
+ }
+
+ if (store_idx == entries.length) {
+ executeSoon(store_cb);
+ return;
+ }
+
+ asyncOpenCacheEntry(
+ entries[store_idx][0],
+ entries[store_idx][2],
+ Ci.nsICacheStorage.OPEN_TRUNCATE,
+ Services.loadContextInfo.custom(false, {
+ privateBrowsingId: entries[store_idx][3] ? 0 : 1,
+ }),
+ store_data,
+ appCache
+ );
+}
+
+var store_data = function(status, entry) {
+ Assert.equal(status, Cr.NS_OK);
+ var os = entry.openOutputStream(0, entries[store_idx][1].length);
+
+ var written = os.write(entries[store_idx][1], entries[store_idx][1].length);
+ if (written != entries[store_idx][1].length) {
+ do_throw(
+ "os.write has not written all data!\n" +
+ " Expected: " +
+ entries[store_idx][1].length +
+ "\n" +
+ " Actual: " +
+ written +
+ "\n"
+ );
+ }
+ os.close();
+ entry.close();
+ store_idx++;
+ executeSoon(store_entries);
+};
+
+var check_idx;
+var check_cb = null;
+var check_pb_exited;
+function check_entries(cb, pbExited) {
+ if (cb) {
+ check_cb = cb;
+ check_idx = 0;
+ check_pb_exited = pbExited;
+ }
+
+ if (check_idx == entries.length) {
+ executeSoon(check_cb);
+ return;
+ }
+
+ asyncOpenCacheEntry(
+ entries[check_idx][0],
+ entries[check_idx][2],
+ Ci.nsICacheStorage.OPEN_READONLY,
+ Services.loadContextInfo.custom(false, {
+ privateBrowsingId: entries[check_idx][3] ? 0 : 1,
+ }),
+ check_data,
+ appCache
+ );
+}
+
+var check_data = function(status, entry) {
+ var cont = function() {
+ check_idx++;
+ executeSoon(check_entries);
+ };
+
+ if (!check_pb_exited || entries[check_idx][3]) {
+ Assert.equal(status, Cr.NS_OK);
+ var is = entry.openInputStream(0);
+ pumpReadStream(is, function(read) {
+ entry.close();
+ Assert.equal(read, entries[check_idx][1]);
+ cont();
+ });
+ } else {
+ Assert.equal(status, Cr.NS_ERROR_CACHE_KEY_NOT_FOUND);
+ cont();
+ }
+};
+
+function run_test() {
+ // Simulate a profile dir for xpcshell
+ do_get_profile();
+
+ Services.prefs.setBoolPref("browser.cache.offline.enable", true);
+ Services.prefs.setBoolPref("browser.cache.offline.storage.enable", true);
+
+ appCache = Cc["@mozilla.org/network/application-cache-service;1"]
+ .getService(Ci.nsIApplicationCacheService)
+ .getApplicationCache("fake-client-id|fake-group-id");
+
+ // Start off with an empty cache
+ evict_cache_entries();
+
+ // Store cache-A, cache-A2, cache-B and cache-C
+ store_entries(run_test2);
+
+ do_test_pending();
+}
+
+function run_test2() {
+ // Check if cache-A, cache-A2, cache-B and cache-C are available
+ check_entries(run_test3, false);
+}
+
+function run_test3() {
+ // Simulate all private browsing instances being closed
+ var obsvc = Cc["@mozilla.org/observer-service;1"].getService(
+ Ci.nsIObserverService
+ );
+ obsvc.notifyObservers(null, "last-pb-context-exited");
+
+ // Make sure the memory device is not empty
+ get_device_entry_count(kMemoryDevice, null, function(count) {
+ Assert.equal(count, 1);
+ // Check if cache-A is gone, and cache-B and cache-C are still available
+ check_entries(do_test_finished, true);
+ });
+}
diff --git a/netwerk/test/unit/test_bug248970_cookie.js b/netwerk/test/unit/test_bug248970_cookie.js
new file mode 100644
index 0000000000..d7f3b381a5
--- /dev/null
+++ b/netwerk/test/unit/test_bug248970_cookie.js
@@ -0,0 +1,150 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+var httpserver;
+
+function inChildProcess() {
+ return (
+ Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime)
+ .processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT
+ );
+}
+function makeChan(path) {
+ return NetUtil.newChannel({
+ uri: "http://localhost:" + httpserver.identity.primaryPort + "/" + path,
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+}
+
+function setup_chan(path, isPrivate, callback) {
+ var chan = makeChan(path);
+ chan.QueryInterface(Ci.nsIPrivateBrowsingChannel).setPrivate(isPrivate);
+ chan.asyncOpen(new ChannelListener(callback));
+}
+
+function set_cookie(value, callback) {
+ return setup_chan("set?cookie=" + value, false, callback);
+}
+
+function set_private_cookie(value, callback) {
+ return setup_chan("set?cookie=" + value, true, callback);
+}
+
+function check_cookie_presence(value, isPrivate, expected, callback) {
+ setup_chan("present?cookie=" + value.replace("=", "|"), isPrivate, function(
+ req
+ ) {
+ req.QueryInterface(Ci.nsIHttpChannel);
+ Assert.equal(req.responseStatus, expected ? 200 : 404);
+ callback(req);
+ });
+}
+
+function presentHandler(metadata, response) {
+ var present = false;
+ var match = /cookie=([^&]*)/.exec(metadata.queryString);
+ if (match) {
+ try {
+ present = metadata
+ .getHeader("Cookie")
+ .includes(match[1].replace("|", "="));
+ } catch (x) {}
+ }
+ response.setStatusLine("1.0", present ? 200 : 404, "");
+}
+
+function setHandler(metadata, response) {
+ response.setStatusLine("1.0", 200, "Cookie set");
+ var match = /cookie=([^&]*)/.exec(metadata.queryString);
+ if (match) {
+ response.setHeader("Set-Cookie", match[1]);
+ }
+}
+
+function run_test() {
+ // Allow all cookies if the pref service is available in this process.
+ if (!inChildProcess()) {
+ Services.prefs.setIntPref("network.cookie.cookieBehavior", 0);
+ Services.prefs.setBoolPref(
+ "network.cookieJarSettings.unblocked_for_testing",
+ true
+ );
+ }
+
+ httpserver = new HttpServer();
+ httpserver.registerPathHandler("/set", setHandler);
+ httpserver.registerPathHandler("/present", presentHandler);
+ httpserver.start(-1);
+
+ do_test_pending();
+
+ function check_cookie(req) {
+ req.QueryInterface(Ci.nsIHttpChannel);
+ Assert.equal(req.responseStatus, 200);
+ try {
+ Assert.ok(
+ req.getResponseHeader("Set-Cookie") != "",
+ "expected a Set-Cookie header"
+ );
+ } catch (x) {
+ do_throw("missing Set-Cookie header");
+ }
+
+ runNextTest();
+ }
+
+ let tests = [];
+
+ function runNextTest() {
+ executeSoon(tests.shift());
+ }
+
+ tests.push(function() {
+ set_cookie("C1=V1", check_cookie);
+ });
+ tests.push(function() {
+ set_private_cookie("C2=V2", check_cookie);
+ });
+ tests.push(function() {
+ // Check that the first cookie is present in a non-private request
+ check_cookie_presence("C1=V1", false, true, runNextTest);
+ });
+ tests.push(function() {
+ // Check that the second cookie is present in a private request
+ check_cookie_presence("C2=V2", true, true, runNextTest);
+ });
+ tests.push(function() {
+ // Check that the first cookie is not present in a private request
+ check_cookie_presence("C1=V1", true, false, runNextTest);
+ });
+ tests.push(function() {
+ // Check that the second cookie is not present in a non-private request
+ check_cookie_presence("C2=V2", false, false, runNextTest);
+ });
+
+ // The following test only works in a non-e10s situation at the moment,
+ // since the notification needs to run in the parent process but there is
+ // no existing mechanism to make that happen.
+ if (!inChildProcess()) {
+ tests.push(function() {
+ // Simulate all private browsing instances being closed
+ var obsvc = Cc["@mozilla.org/observer-service;1"].getService(
+ Ci.nsIObserverService
+ );
+ obsvc.notifyObservers(null, "last-pb-context-exited");
+ // Check that all private cookies are now unavailable in new private requests
+ check_cookie_presence("C2=V2", true, false, runNextTest);
+ });
+ }
+
+ tests.push(function() {
+ httpserver.stop(do_test_finished);
+ });
+
+ runNextTest();
+}
diff --git a/netwerk/test/unit/test_bug261425.js b/netwerk/test/unit/test_bug261425.js
new file mode 100644
index 0000000000..3da3ba610f
--- /dev/null
+++ b/netwerk/test/unit/test_bug261425.js
@@ -0,0 +1,37 @@
+"use strict";
+
+function run_test() {
+ var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
+
+ var newURI = ios.newURI("http://foo.com");
+
+ var success = false;
+ try {
+ newURI = newURI
+ .mutate()
+ .setSpec("http: //foo.com")
+ .finalize();
+ } catch (e) {
+ success = e.result == Cr.NS_ERROR_MALFORMED_URI;
+ }
+ if (!success) {
+ do_throw(
+ "We didn't throw NS_ERROR_MALFORMED_URI when a space was passed in the hostname!"
+ );
+ }
+
+ success = false;
+ try {
+ newURI
+ .mutate()
+ .setHost(" foo.com")
+ .finalize();
+ } catch (e) {
+ success = e.result == Cr.NS_ERROR_MALFORMED_URI;
+ }
+ if (!success) {
+ do_throw(
+ "We didn't throw NS_ERROR_MALFORMED_URI when a space was passed in the hostname!"
+ );
+ }
+}
diff --git a/netwerk/test/unit/test_bug263127.js b/netwerk/test/unit/test_bug263127.js
new file mode 100644
index 0000000000..d829bbd800
--- /dev/null
+++ b/netwerk/test/unit/test_bug263127.js
@@ -0,0 +1,58 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+var server;
+const BUGID = "263127";
+
+var listener = {
+ QueryInterface: ChromeUtils.generateQI(["nsIDownloadObserver"]),
+
+ onDownloadComplete(downloader, request, ctxt, status, file) {
+ do_test_pending();
+ server.stop(do_test_finished);
+
+ if (!file) {
+ do_throw("Download failed");
+ }
+
+ try {
+ file.remove(false);
+ } catch (e) {
+ do_throw(e);
+ }
+
+ Assert.ok(!file.exists());
+
+ do_test_finished();
+ },
+};
+
+function run_test() {
+ // start server
+ server = new HttpServer();
+ server.start(-1);
+
+ // Initialize downloader
+ var channel = NetUtil.newChannel({
+ uri: "http://localhost:" + server.identity.primaryPort + "/",
+ loadUsingSystemPrincipal: true,
+ });
+ var targetFile = Cc["@mozilla.org/file/directory_service;1"]
+ .getService(Ci.nsIProperties)
+ .get("TmpD", Ci.nsIFile);
+ targetFile.append("bug" + BUGID + ".test");
+ if (targetFile.exists()) {
+ targetFile.remove(false);
+ }
+
+ var downloader = Cc["@mozilla.org/network/downloader;1"].createInstance(
+ Ci.nsIDownloader
+ );
+ downloader.init(listener, targetFile);
+
+ // Start download
+ channel.asyncOpen(downloader);
+
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_bug282432.js b/netwerk/test/unit/test_bug282432.js
new file mode 100644
index 0000000000..7794408523
--- /dev/null
+++ b/netwerk/test/unit/test_bug282432.js
@@ -0,0 +1,38 @@
+"use strict";
+
+function run_test() {
+ do_test_pending();
+
+ function StreamListener() {}
+
+ StreamListener.prototype = {
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIStreamListener",
+ "nsIRequestObserver",
+ ]),
+
+ onStartRequest(aRequest) {},
+
+ onStopRequest(aRequest, aStatusCode) {
+ // Make sure we can catch the error NS_ERROR_FILE_NOT_FOUND here.
+ Assert.equal(aStatusCode, Cr.NS_ERROR_FILE_NOT_FOUND);
+ do_test_finished();
+ },
+
+ onDataAvailable(aRequest, aStream, aOffset, aCount) {
+ do_throw("The channel must not call onDataAvailable().");
+ },
+ };
+
+ let listener = new StreamListener();
+ let ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
+
+ // This file does not exist.
+ let file = do_get_file("_NOT_EXIST_.txt", true);
+ Assert.ok(!file.exists());
+ let channel = NetUtil.newChannel({
+ uri: ios.newFileURI(file),
+ loadUsingSystemPrincipal: true,
+ });
+ channel.asyncOpen(listener);
+}
diff --git a/netwerk/test/unit/test_bug321706.js b/netwerk/test/unit/test_bug321706.js
new file mode 100644
index 0000000000..86d8bdc373
--- /dev/null
+++ b/netwerk/test/unit/test_bug321706.js
@@ -0,0 +1,12 @@
+"use strict";
+
+const url = "http://foo.com/folder/file?/.";
+
+function run_test() {
+ var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
+
+ var newURI = ios.newURI(url);
+ Assert.equal(newURI.spec, url);
+ Assert.equal(newURI.pathQueryRef, "/folder/file?/.");
+ Assert.equal(newURI.resolve("./file?/."), url);
+}
diff --git a/netwerk/test/unit/test_bug331825.js b/netwerk/test/unit/test_bug331825.js
new file mode 100644
index 0000000000..acb6d7690a
--- /dev/null
+++ b/netwerk/test/unit/test_bug331825.js
@@ -0,0 +1,41 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+var server;
+const BUGID = "331825";
+
+function TestListener() {}
+TestListener.prototype.onStartRequest = function(request) {};
+TestListener.prototype.onStopRequest = function(request, status) {
+ var channel = request.QueryInterface(Ci.nsIHttpChannel);
+ Assert.equal(channel.responseStatus, 304);
+
+ server.stop(do_test_finished);
+};
+
+function run_test() {
+ // start server
+ server = new HttpServer();
+
+ server.registerPathHandler("/bug" + BUGID, bug331825);
+
+ server.start(-1);
+
+ // make request
+ var channel = NetUtil.newChannel({
+ uri: "http://localhost:" + server.identity.primaryPort + "/bug" + BUGID,
+ loadUsingSystemPrincipal: true,
+ });
+
+ channel.QueryInterface(Ci.nsIHttpChannel);
+ channel.setRequestHeader("If-None-Match", "foobar", false);
+ channel.asyncOpen(new TestListener());
+
+ do_test_pending();
+}
+
+// PATH HANDLER FOR /bug331825
+function bug331825(metadata, response) {
+ response.setStatusLine(metadata.httpVersion, 304, "Not Modified");
+}
diff --git a/netwerk/test/unit/test_bug336501.js b/netwerk/test/unit/test_bug336501.js
new file mode 100644
index 0000000000..3e15fd42bb
--- /dev/null
+++ b/netwerk/test/unit/test_bug336501.js
@@ -0,0 +1,26 @@
+"use strict";
+
+function run_test() {
+ var f = do_get_file("test_bug336501.js");
+
+ var fis = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(
+ Ci.nsIFileInputStream
+ );
+ fis.init(f, -1, -1, 0);
+
+ var bis = Cc["@mozilla.org/network/buffered-input-stream;1"].createInstance(
+ Ci.nsIBufferedInputStream
+ );
+ bis.init(fis, 32);
+
+ var sis = Cc["@mozilla.org/scriptableinputstream;1"].createInstance(
+ Ci.nsIScriptableInputStream
+ );
+ sis.init(bis);
+
+ sis.read(45);
+ sis.close();
+
+ var data = sis.read(45);
+ Assert.equal(data.length, 0);
+}
diff --git a/netwerk/test/unit/test_bug337744.js b/netwerk/test/unit/test_bug337744.js
new file mode 100644
index 0000000000..583627f270
--- /dev/null
+++ b/netwerk/test/unit/test_bug337744.js
@@ -0,0 +1,126 @@
+/* verify that certain invalid URIs are not parsed by the resource
+ protocol handler */
+
+"use strict";
+
+const specs = [
+ "resource://res-test//",
+ "resource://res-test/?foo=http:",
+ "resource://res-test/?foo=" + encodeURIComponent("http://example.com/"),
+ "resource://res-test/?foo=" + encodeURIComponent("x\\y"),
+ "resource://res-test/..%2F",
+ "resource://res-test/..%2f",
+ "resource://res-test/..%2F..",
+ "resource://res-test/..%2f..",
+ "resource://res-test/../../",
+ "resource://res-test/http://www.mozilla.org/",
+ "resource://res-test/file:///",
+];
+
+const error_specs = [
+ "resource://res-test/..\\",
+ "resource://res-test/..\\..\\",
+ "resource://res-test/..%5C",
+ "resource://res-test/..%5c",
+];
+
+// Create some fake principal that has not enough
+// privileges to access any resource: uri.
+var uri = NetUtil.newURI("http://www.example.com");
+var principal = Services.scriptSecurityManager.createContentPrincipal(uri, {});
+
+function get_channel(spec) {
+ var channel = NetUtil.newChannel({
+ uri: NetUtil.newURI(spec),
+ loadingPrincipal: principal,
+ securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER,
+ });
+
+ Assert.throws(
+ () => {
+ channel.asyncOpen(null);
+ },
+ /NS_ERROR_DOM_BAD_URI/,
+ `asyncOpen() of uri: ${spec} should throw`
+ );
+ Assert.throws(
+ () => {
+ channel.open();
+ },
+ /NS_ERROR_DOM_BAD_URI/,
+ `Open() of uri: ${spec} should throw`
+ );
+
+ return channel;
+}
+
+function check_safe_resolution(spec, rootURI) {
+ info(`Testing URL "${spec}"`);
+
+ let channel = get_channel(spec);
+
+ ok(
+ channel.name.startsWith(rootURI),
+ `URL resolved safely to ${channel.name}`
+ );
+ let startOfQuery = channel.name.indexOf("?");
+ if (startOfQuery == -1) {
+ ok(!/%2f/i.test(channel.name), `URL contains no escaped / characters`);
+ } else {
+ // Escaped slashes are allowed in the query or hash part of the URL
+ ok(
+ !channel.name.replace(/\?.*/, "").includes("%2f"),
+ `URL contains no escaped slashes before the query ${channel.name}`
+ );
+ }
+}
+
+function check_resolution_error(spec) {
+ Assert.throws(
+ () => {
+ get_channel(spec);
+ },
+ /NS_ERROR_MALFORMED_URI/,
+ "Expected a malformed URI error"
+ );
+}
+
+function run_test() {
+ // resource:/// and resource://gre/ are resolved specially, so we need
+ // to create a temporary resource package to test the standard logic
+ // with.
+
+ let resProto = Cc["@mozilla.org/network/protocol;1?name=resource"].getService(
+ Ci.nsIResProtocolHandler
+ );
+ let rootFile = Services.dirsvc.get("GreD", Ci.nsIFile);
+ let rootURI = Services.io.newFileURI(rootFile);
+
+ rootFile.append("directory-that-does-not-exist");
+ let inexistentURI = Services.io.newFileURI(rootFile);
+
+ resProto.setSubstitution("res-test", rootURI);
+ resProto.setSubstitution("res-inexistent", inexistentURI);
+ registerCleanupFunction(() => {
+ resProto.setSubstitution("res-test", null);
+ resProto.setSubstitution("res-inexistent", null);
+ });
+
+ let baseRoot = resProto.resolveURI(Services.io.newURI("resource:///"));
+ let greRoot = resProto.resolveURI(Services.io.newURI("resource://gre/"));
+
+ for (var spec of specs) {
+ check_safe_resolution(spec, rootURI.spec);
+ check_safe_resolution(
+ spec.replace("res-test", "res-inexistent"),
+ inexistentURI.spec
+ );
+ check_safe_resolution(spec.replace("res-test", ""), baseRoot);
+ check_safe_resolution(spec.replace("res-test", "gre"), greRoot);
+ }
+
+ for (var spec of error_specs) {
+ check_resolution_error(spec);
+ }
+}
diff --git a/netwerk/test/unit/test_bug365133.js b/netwerk/test/unit/test_bug365133.js
new file mode 100644
index 0000000000..1f5408d301
--- /dev/null
+++ b/netwerk/test/unit/test_bug365133.js
@@ -0,0 +1,131 @@
+"use strict";
+
+const URL = "ftp://localhost/bug365133/";
+
+const tests = [
+ [
+ /* Unix style listing, space at the end of filename */
+ "drwxrwxr-x 2 500 500 4096 Jan 01 2000 a \r\n",
+ "300: " +
+ URL +
+ "\n" +
+ "200: filename content-length last-modified file-type\n" +
+ '201: "a%20" 0 Sat%2C%2001%20Jan%202000%2000%3A00%3A00%20GMT DIRECTORY \n',
+ ],
+ [
+ /* Unix style listing, space at the end of link name */
+ "lrwxrwxrwx 1 500 500 2 Jan 01 2000 l -> a \r\n",
+ "300: " +
+ URL +
+ "\n" +
+ "200: filename content-length last-modified file-type\n" +
+ '201: "l%20" 2 Sat%2C%2001%20Jan%202000%2000%3A00%3A00%20GMT SYMBOLIC-LINK \n',
+ ],
+ [
+ /* Unix style listing, regular file with " -> " in name */
+ "-rw-rw-r-- 1 500 500 0 Jan 01 2000 arrow -> in name \r\n",
+ "300: " +
+ URL +
+ "\n" +
+ "200: filename content-length last-modified file-type\n" +
+ '201: "arrow%20-%3E%20in%20name%20" 0 Sat%2C%2001%20Jan%202000%2000%3A00%3A00%20GMT FILE \n',
+ ],
+ [
+ /* Unix style listing, tab at the end of filename */
+ "drwxrwxrwx 2 500 500 4096 Jan 01 2000 t \r\n",
+ "300: " +
+ URL +
+ "\n" +
+ "200: filename content-length last-modified file-type\n" +
+ '201: "t%09" 0 Sat%2C%2001%20Jan%202000%2000%3A00%3A00%20GMT DIRECTORY \n',
+ ],
+ [
+ /* Unix style listing, multiple " -> " in filename */
+ "lrwxrwxrwx 1 500 500 26 Jan 01 2000 symlink with arrow -> in name -> file with arrow -> in name\r\n",
+ "300: " +
+ URL +
+ "\n" +
+ "200: filename content-length last-modified file-type\n" +
+ '201: "symlink%20with%20arrow%20-%3E%20in%20name" 26 Sat%2C%2001%20Jan%202000%2000%3A00%3A00%20GMT SYMBOLIC-LINK \n',
+ ],
+ [
+ /* Unix style listing, multiple " -> " in filename, incorrect filesize */
+ "lrwxrwxrwx 1 500 500 0 Jan 01 2000 symlink with arrow -> in name -> file with arrow -> in name\r\n",
+ "300: " +
+ URL +
+ "\n" +
+ "200: filename content-length last-modified file-type\n" +
+ '201: "symlink%20with%20arrow%20-%3E%20in%20name%20-%3E%20file%20with%20arrow" 0 Sat%2C%2001%20Jan%202000%2000%3A00%3A00%20GMT SYMBOLIC-LINK \n',
+ ],
+ [
+ /* DOS style listing, space at the end of filename, year 1999 */
+ "01-01-99 01:00AM 1024 file \r\n",
+ "300: " +
+ URL +
+ "\n" +
+ "200: filename content-length last-modified file-type\n" +
+ '201: "file%20" 1024 Fri%2C%2001%20Jan%201999%2001%3A00%3A00%20GMT FILE \n',
+ ],
+ [
+ /* DOS style listing, tab at the end of filename, year 2000 */
+ "01-01-00 01:00AM 1024 file \r\n",
+ "300: " +
+ URL +
+ "\n" +
+ "200: filename content-length last-modified file-type\n" +
+ '201: "file%09" 1024 Sat%2C%2001%20Jan%202000%2001%3A00%3A00%20GMT FILE \n',
+ ],
+];
+
+function checkData(request, data, ctx) {
+ Assert.equal(tests[0][1], data);
+ tests.shift();
+ executeSoon(next_test);
+}
+
+function storeData() {
+ var scs = Cc["@mozilla.org/streamConverters;1"].getService(
+ Ci.nsIStreamConverterService
+ );
+ var converter = scs.asyncConvertData(
+ "text/ftp-dir",
+ "application/http-index-format",
+ new ChannelListener(checkData, null, CL_ALLOW_UNKNOWN_CL),
+ null
+ );
+
+ var stream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(
+ Ci.nsIStringInputStream
+ );
+ stream.data = tests[0][0];
+
+ var url = NetUtil.newURI(URL);
+
+ var channel = {
+ URI: url,
+ contentLength: -1,
+ pending: true,
+ isPending() {
+ return this.pending;
+ },
+ QueryInterface: ChromeUtils.generateQI(["nsIChannel"]),
+ };
+
+ converter.onStartRequest(channel, null);
+ converter.onDataAvailable(channel, stream, 0, 0);
+ channel.pending = false;
+ converter.onStopRequest(channel, null, Cr.NS_OK);
+}
+
+function next_test() {
+ if (tests.length == 0) {
+ do_test_finished();
+ } else {
+ storeData();
+ }
+}
+
+function run_test() {
+ executeSoon(next_test);
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_bug368702.js b/netwerk/test/unit/test_bug368702.js
new file mode 100644
index 0000000000..89a5cc4b20
--- /dev/null
+++ b/netwerk/test/unit/test_bug368702.js
@@ -0,0 +1,153 @@
+"use strict";
+
+function run_test() {
+ var tld = Cc["@mozilla.org/network/effective-tld-service;1"].getService(
+ Ci.nsIEffectiveTLDService
+ );
+ Assert.equal(tld.getPublicSuffixFromHost("localhost"), "localhost");
+ Assert.equal(tld.getPublicSuffixFromHost("localhost."), "localhost.");
+ Assert.equal(tld.getPublicSuffixFromHost("domain.com"), "com");
+ Assert.equal(tld.getPublicSuffixFromHost("domain.com."), "com.");
+ Assert.equal(tld.getPublicSuffixFromHost("domain.co.uk"), "co.uk");
+ Assert.equal(tld.getPublicSuffixFromHost("domain.co.uk."), "co.uk.");
+ Assert.equal(tld.getPublicSuffixFromHost("co.uk"), "co.uk");
+ Assert.equal(tld.getBaseDomainFromHost("domain.co.uk"), "domain.co.uk");
+ Assert.equal(tld.getBaseDomainFromHost("domain.co.uk."), "domain.co.uk.");
+
+ try {
+ tld.getPublicSuffixFromHost("");
+ do_throw("this should fail");
+ } catch (e) {
+ Assert.equal(e.result, Cr.NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS);
+ }
+
+ try {
+ tld.getBaseDomainFromHost("domain.co.uk", 1);
+ do_throw("this should fail");
+ } catch (e) {
+ Assert.equal(e.result, Cr.NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS);
+ }
+
+ try {
+ tld.getBaseDomainFromHost("co.uk");
+ do_throw("this should fail");
+ } catch (e) {
+ Assert.equal(e.result, Cr.NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS);
+ }
+
+ try {
+ tld.getBaseDomainFromHost("");
+ do_throw("this should fail");
+ } catch (e) {
+ Assert.equal(e.result, Cr.NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS);
+ }
+
+ try {
+ tld.getPublicSuffixFromHost("1.2.3.4");
+ do_throw("this should fail");
+ } catch (e) {
+ Assert.equal(e.result, Cr.NS_ERROR_HOST_IS_IP_ADDRESS);
+ }
+
+ try {
+ tld.getPublicSuffixFromHost("2010:836B:4179::836B:4179");
+ do_throw("this should fail");
+ } catch (e) {
+ Assert.equal(e.result, Cr.NS_ERROR_HOST_IS_IP_ADDRESS);
+ }
+
+ try {
+ tld.getPublicSuffixFromHost("3232235878");
+ do_throw("this should fail");
+ } catch (e) {
+ Assert.equal(e.result, Cr.NS_ERROR_HOST_IS_IP_ADDRESS);
+ }
+
+ try {
+ tld.getPublicSuffixFromHost("::ffff:192.9.5.5");
+ do_throw("this should fail");
+ } catch (e) {
+ Assert.equal(e.result, Cr.NS_ERROR_HOST_IS_IP_ADDRESS);
+ }
+
+ try {
+ tld.getPublicSuffixFromHost("::1");
+ do_throw("this should fail");
+ } catch (e) {
+ Assert.equal(e.result, Cr.NS_ERROR_HOST_IS_IP_ADDRESS);
+ }
+
+ // Check IP addresses with trailing dot as well, Necko sometimes accepts
+ // those (depending on operating system, see bug 380543)
+ try {
+ tld.getPublicSuffixFromHost("127.0.0.1.");
+ do_throw("this should fail");
+ } catch (e) {
+ Assert.equal(e.result, Cr.NS_ERROR_HOST_IS_IP_ADDRESS);
+ }
+
+ try {
+ tld.getPublicSuffixFromHost("::ffff:127.0.0.1.");
+ do_throw("this should fail");
+ } catch (e) {
+ Assert.equal(e.result, Cr.NS_ERROR_HOST_IS_IP_ADDRESS);
+ }
+
+ // check normalization: output should be consistent with
+ // nsIURI::GetAsciiHost(), i.e. lowercased and ASCII/ACE encoded
+ var ioService = Cc["@mozilla.org/network/io-service;1"].getService(
+ Ci.nsIIOService
+ );
+
+ var uri = ioService.newURI("http://b\u00FCcher.co.uk");
+ Assert.equal(tld.getBaseDomain(uri), "xn--bcher-kva.co.uk");
+ Assert.equal(
+ tld.getBaseDomainFromHost("b\u00FCcher.co.uk"),
+ "xn--bcher-kva.co.uk"
+ );
+ Assert.equal(tld.getPublicSuffix(uri), "co.uk");
+ Assert.equal(tld.getPublicSuffixFromHost("b\u00FCcher.co.uk"), "co.uk");
+
+ // check that malformed hosts are rejected as invalid args
+ try {
+ tld.getBaseDomainFromHost("domain.co.uk..");
+ do_throw("this should fail");
+ } catch (e) {
+ Assert.equal(e.result, Cr.NS_ERROR_ILLEGAL_VALUE);
+ }
+
+ try {
+ tld.getBaseDomainFromHost("domain.co..uk");
+ do_throw("this should fail");
+ } catch (e) {
+ Assert.equal(e.result, Cr.NS_ERROR_ILLEGAL_VALUE);
+ }
+
+ try {
+ tld.getBaseDomainFromHost(".domain.co.uk");
+ do_throw("this should fail");
+ } catch (e) {
+ Assert.equal(e.result, Cr.NS_ERROR_ILLEGAL_VALUE);
+ }
+
+ try {
+ tld.getBaseDomainFromHost(".domain.co.uk");
+ do_throw("this should fail");
+ } catch (e) {
+ Assert.equal(e.result, Cr.NS_ERROR_ILLEGAL_VALUE);
+ }
+
+ try {
+ tld.getBaseDomainFromHost(".");
+ do_throw("this should fail");
+ } catch (e) {
+ Assert.equal(e.result, Cr.NS_ERROR_ILLEGAL_VALUE);
+ }
+
+ try {
+ tld.getBaseDomainFromHost("..");
+ do_throw("this should fail");
+ } catch (e) {
+ Assert.equal(e.result, Cr.NS_ERROR_ILLEGAL_VALUE);
+ }
+}
diff --git a/netwerk/test/unit/test_bug369787.js b/netwerk/test/unit/test_bug369787.js
new file mode 100644
index 0000000000..9a961b0024
--- /dev/null
+++ b/netwerk/test/unit/test_bug369787.js
@@ -0,0 +1,71 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+const BUGID = "369787";
+var server = null;
+var channel = null;
+
+function change_content_type() {
+ var origType = channel.contentType;
+ const newType = "x-foo/x-bar";
+ channel.contentType = newType;
+ Assert.equal(channel.contentType, newType);
+ channel.contentType = origType;
+ Assert.equal(channel.contentType, origType);
+}
+
+function TestListener() {}
+TestListener.prototype.onStartRequest = function(request) {
+ try {
+ // request might be different from channel
+ channel = request.QueryInterface(Ci.nsIChannel);
+
+ change_content_type();
+ } catch (ex) {
+ print(ex);
+ throw ex;
+ }
+};
+TestListener.prototype.onStopRequest = function(request, status) {
+ try {
+ change_content_type();
+ } catch (ex) {
+ print(ex);
+ // don't re-throw ex to avoid hanging the test
+ }
+
+ do_timeout(0, after_channel_closed);
+};
+
+function after_channel_closed() {
+ try {
+ change_content_type();
+ } finally {
+ server.stop(do_test_finished);
+ }
+}
+
+function run_test() {
+ // start server
+ server = new HttpServer();
+
+ server.registerPathHandler("/bug" + BUGID, bug369787);
+
+ server.start(-1);
+
+ // make request
+ channel = NetUtil.newChannel({
+ uri: "http://localhost:" + server.identity.primaryPort + "/bug" + BUGID,
+ loadUsingSystemPrincipal: true,
+ });
+ channel.QueryInterface(Ci.nsIHttpChannel);
+ channel.asyncOpen(new TestListener());
+
+ do_test_pending();
+}
+
+// PATH HANDLER FOR /bug369787
+function bug369787(metadata, response) {
+ /* do nothing */
+}
diff --git a/netwerk/test/unit/test_bug371473.js b/netwerk/test/unit/test_bug371473.js
new file mode 100644
index 0000000000..05ea15aeac
--- /dev/null
+++ b/netwerk/test/unit/test_bug371473.js
@@ -0,0 +1,40 @@
+"use strict";
+
+function test_not_too_long() {
+ var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
+
+ var spec = "jar:http://example.com/bar.jar!/";
+ try {
+ ios.newURI(spec);
+ } catch (e) {
+ do_throw("newURI threw even though it wasn't passed a large nested URI?");
+ }
+}
+
+function test_too_long() {
+ var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
+
+ var i;
+ var prefix = "jar:";
+ for (i = 0; i < 16; i++) {
+ prefix = prefix + prefix;
+ }
+ var suffix = "!/";
+ for (i = 0; i < 16; i++) {
+ suffix = suffix + suffix;
+ }
+
+ var spec = prefix + "http://example.com/bar.jar" + suffix;
+ try {
+ // The following will produce a recursive call that if
+ // unchecked would lead to a stack overflow. If we
+ // do not crash here and thus an exception is caught
+ // we have passed the test.
+ ios.newURI(spec);
+ } catch (e) {}
+}
+
+function run_test() {
+ test_not_too_long();
+ test_too_long();
+}
diff --git a/netwerk/test/unit/test_bug376844.js b/netwerk/test/unit/test_bug376844.js
new file mode 100644
index 0000000000..3d97d568c5
--- /dev/null
+++ b/netwerk/test/unit/test_bug376844.js
@@ -0,0 +1,23 @@
+"use strict";
+
+const testURLs = [
+ ["http://example.com/<", "http://example.com/%3C"],
+ ["http://example.com/>", "http://example.com/%3E"],
+ ["http://example.com/'", "http://example.com/'"],
+ ['http://example.com/"', "http://example.com/%22"],
+ ["http://example.com/?<", "http://example.com/?%3C"],
+ ["http://example.com/?>", "http://example.com/?%3E"],
+ ["http://example.com/?'", "http://example.com/?%27"],
+ ['http://example.com/?"', "http://example.com/?%22"],
+];
+
+function run_test() {
+ var ioServ = Cc["@mozilla.org/network/io-service;1"].getService(
+ Ci.nsIIOService
+ );
+
+ for (var i = 0; i < testURLs.length; i++) {
+ var uri = ioServ.newURI(testURLs[i][0]);
+ Assert.equal(uri.spec, testURLs[i][1]);
+ }
+}
diff --git a/netwerk/test/unit/test_bug376865.js b/netwerk/test/unit/test_bug376865.js
new file mode 100644
index 0000000000..260dcbf7e6
--- /dev/null
+++ b/netwerk/test/unit/test_bug376865.js
@@ -0,0 +1,23 @@
+"use strict";
+
+function run_test() {
+ var stream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(
+ Ci.nsISupportsCString
+ );
+ stream.data = "foo bar baz";
+
+ var pump = Cc["@mozilla.org/network/input-stream-pump;1"].createInstance(
+ Ci.nsIInputStreamPump
+ );
+ pump.init(stream, 0, 0, false);
+
+ // When we pass a null listener argument too asyncRead we expect it to throw
+ // instead of crashing.
+ try {
+ pump.asyncRead(null);
+ } catch (e) {
+ return;
+ }
+
+ do_throw("asyncRead didn't throw when passed a null listener argument.");
+}
diff --git a/netwerk/test/unit/test_bug379034.js b/netwerk/test/unit/test_bug379034.js
new file mode 100644
index 0000000000..af2f78dd26
--- /dev/null
+++ b/netwerk/test/unit/test_bug379034.js
@@ -0,0 +1,21 @@
+"use strict";
+
+function run_test() {
+ const ios = Cc["@mozilla.org/network/io-service;1"].getService(
+ Ci.nsIIOService
+ );
+
+ var base = ios.newURI("http://localhost/bug379034/index.html");
+
+ var uri = ios.newURI("http:a.html", null, base);
+ Assert.equal(uri.spec, "http://localhost/bug379034/a.html");
+
+ uri = ios.newURI("HtTp:b.html", null, base);
+ Assert.equal(uri.spec, "http://localhost/bug379034/b.html");
+
+ uri = ios.newURI("https:c.html", null, base);
+ Assert.equal(uri.spec, "https://c.html/");
+
+ uri = ios.newURI("./https:d.html", null, base);
+ Assert.equal(uri.spec, "http://localhost/bug379034/https:d.html");
+}
diff --git a/netwerk/test/unit/test_bug380994.js b/netwerk/test/unit/test_bug380994.js
new file mode 100644
index 0000000000..01f896a90f
--- /dev/null
+++ b/netwerk/test/unit/test_bug380994.js
@@ -0,0 +1,26 @@
+/* check resource: protocol for traversal problems */
+
+"use strict";
+
+const specs = [
+ "resource:///chrome/../plugins",
+ "resource:///chrome%2f../plugins",
+ "resource:///chrome/..%2fplugins",
+ "resource:///chrome%2f%2e%2e%2fplugins",
+ "resource:///../../../..",
+ "resource:///..%2f..%2f..%2f..",
+ "resource:///%2e%2e",
+];
+
+function run_test() {
+ var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
+
+ for (var spec of specs) {
+ var uri = ios.newURI(spec);
+ if (uri.spec.includes("..")) {
+ do_throw(
+ "resource: traversal remains: '" + spec + "' ==> '" + uri.spec + "'"
+ );
+ }
+ }
+}
diff --git a/netwerk/test/unit/test_bug388281.js b/netwerk/test/unit/test_bug388281.js
new file mode 100644
index 0000000000..8fd43c7c90
--- /dev/null
+++ b/netwerk/test/unit/test_bug388281.js
@@ -0,0 +1,42 @@
+"use strict";
+
+function run_test() {
+ const ios = Cc["@mozilla.org/network/io-service;1"].getService(
+ Ci.nsIIOService
+ );
+
+ var uri = ios.newURI("http://foo.com/file.txt");
+ uri = uri
+ .mutate()
+ .setPort(90)
+ .finalize();
+ Assert.equal(uri.hostPort, "foo.com:90");
+
+ uri = ios.newURI("http://foo.com:10/file.txt");
+ uri = uri
+ .mutate()
+ .setPort(500)
+ .finalize();
+ Assert.equal(uri.hostPort, "foo.com:500");
+
+ uri = ios.newURI("http://foo.com:5000/file.txt");
+ uri = uri
+ .mutate()
+ .setPort(20)
+ .finalize();
+ Assert.equal(uri.hostPort, "foo.com:20");
+
+ uri = ios.newURI("http://foo.com:5000/file.txt");
+ uri = uri
+ .mutate()
+ .setPort(-1)
+ .finalize();
+ Assert.equal(uri.hostPort, "foo.com");
+
+ uri = ios.newURI("http://foo.com:5000/file.txt");
+ uri = uri
+ .mutate()
+ .setPort(80)
+ .finalize();
+ Assert.equal(uri.hostPort, "foo.com");
+}
diff --git a/netwerk/test/unit/test_bug396389.js b/netwerk/test/unit/test_bug396389.js
new file mode 100644
index 0000000000..5095a77bd4
--- /dev/null
+++ b/netwerk/test/unit/test_bug396389.js
@@ -0,0 +1,71 @@
+"use strict";
+
+function round_trip(uri) {
+ var objectOutStream = Cc["@mozilla.org/binaryoutputstream;1"].createInstance(
+ Ci.nsIObjectOutputStream
+ );
+ var pipe = Cc["@mozilla.org/pipe;1"].createInstance(Ci.nsIPipe);
+ pipe.init(false, false, 0, 0xffffffff, null);
+ objectOutStream.setOutputStream(pipe.outputStream);
+ objectOutStream.writeCompoundObject(uri, Ci.nsISupports, true);
+ objectOutStream.close();
+
+ var objectInStream = Cc["@mozilla.org/binaryinputstream;1"].createInstance(
+ Ci.nsIObjectInputStream
+ );
+ objectInStream.setInputStream(pipe.inputStream);
+ return objectInStream.readObject(true).QueryInterface(Ci.nsIURI);
+}
+
+var prefData = [
+ {
+ name: "network.IDN_show_punycode",
+ newVal: false,
+ },
+ {
+ name: "network.IDN.whitelist.ch",
+ newVal: true,
+ },
+];
+
+function run_test() {
+ var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
+
+ var uri1 = ios.newURI("file:///");
+ Assert.ok(uri1 instanceof Ci.nsIFileURL);
+
+ var uri2 = uri1.mutate().finalize();
+ Assert.ok(uri2 instanceof Ci.nsIFileURL);
+ Assert.ok(uri1.equals(uri2));
+
+ var uri3 = round_trip(uri1);
+ Assert.ok(uri3 instanceof Ci.nsIFileURL);
+ Assert.ok(uri1.equals(uri3));
+
+ // Make sure our prefs are set such that this test actually means something
+ var prefs = Cc["@mozilla.org/preferences-service;1"].getService(
+ Ci.nsIPrefBranch
+ );
+ for (var pref of prefData) {
+ prefs.setBoolPref(pref.name, pref.newVal);
+ }
+
+ try {
+ // URI stolen from
+ // http://lists.w3.org/Archives/Public/public-iri/2004Mar/0012.html
+ var uri4 = ios.newURI("http://xn--jos-dma.example.net.ch/");
+ Assert.equal(uri4.asciiHost, "xn--jos-dma.example.net.ch");
+ Assert.equal(uri4.displayHost, "jos\u00e9.example.net.ch");
+
+ var uri5 = round_trip(uri4);
+ Assert.ok(uri4.equals(uri5));
+ Assert.equal(uri4.displayHost, uri5.displayHost);
+ Assert.equal(uri4.asciiHost, uri5.asciiHost);
+ } finally {
+ for (var pref of prefData) {
+ if (prefs.prefHasUserValue(pref.name)) {
+ prefs.clearUserPref(pref.name);
+ }
+ }
+ }
+}
diff --git a/netwerk/test/unit/test_bug401564.js b/netwerk/test/unit/test_bug401564.js
new file mode 100644
index 0000000000..659fda4ab5
--- /dev/null
+++ b/netwerk/test/unit/test_bug401564.js
@@ -0,0 +1,44 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+var httpserver = null;
+const noRedirectURI = "/content";
+const pageValue = "Final page";
+const acceptType = "application/json";
+
+function redirectHandler(metadata, response) {
+ response.setStatusLine(metadata.httpVersion, 302, "Moved Temporarily");
+ response.setHeader("Location", noRedirectURI, false);
+}
+
+function contentHandler(metadata, response) {
+ Assert.equal(metadata.getHeader("Accept"), acceptType);
+ httpserver.stop(do_test_finished);
+}
+
+function dummyHandler(request, buffer) {}
+
+function run_test() {
+ httpserver = new HttpServer();
+ httpserver.registerPathHandler("/redirect", redirectHandler);
+ httpserver.registerPathHandler("/content", contentHandler);
+ httpserver.start(-1);
+
+ var prefs = Cc["@mozilla.org/preferences-service;1"].getService(
+ Ci.nsIPrefBranch
+ );
+ prefs.setBoolPref("network.http.prompt-temp-redirect", false);
+
+ var chan = NetUtil.newChannel({
+ uri: "http://localhost:" + httpserver.identity.primaryPort + "/redirect",
+ loadUsingSystemPrincipal: true,
+ });
+ chan.QueryInterface(Ci.nsIHttpChannel);
+ chan.setRequestHeader("Accept", acceptType, false);
+
+ chan.asyncOpen(new ChannelListener(dummyHandler, null));
+
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_bug411952.js b/netwerk/test/unit/test_bug411952.js
new file mode 100644
index 0000000000..a7bdfb178f
--- /dev/null
+++ b/netwerk/test/unit/test_bug411952.js
@@ -0,0 +1,53 @@
+"use strict";
+
+function run_test() {
+ try {
+ var cm = Cc["@mozilla.org/cookiemanager;1"].getService(Ci.nsICookieManager);
+ Assert.notEqual(cm, null, "Retrieving the cookie manager failed");
+
+ const time = new Date("Jan 1, 2030").getTime() / 1000;
+ cm.add(
+ "example.com",
+ "/",
+ "C",
+ "V",
+ false,
+ true,
+ false,
+ time,
+ {},
+ Ci.nsICookie.SAMESITE_NONE,
+ Ci.nsICookie.SCHEME_HTTPS
+ );
+ const now = Math.floor(new Date().getTime() / 1000);
+
+ var found = false;
+ for (let cookie of cm.cookies) {
+ if (
+ cookie.host == "example.com" &&
+ cookie.path == "/" &&
+ cookie.name == "C"
+ ) {
+ Assert.ok(
+ "creationTime" in cookie,
+ "creationTime attribute is not accessible on the cookie"
+ );
+ var creationTime = Math.floor(cookie.creationTime / 1000000);
+ // allow the times to slip by one second at most,
+ // which should be fine under normal circumstances.
+ Assert.ok(
+ Math.abs(creationTime - now) <= 1,
+ "Cookie's creationTime is set incorrectly"
+ );
+ found = true;
+ break;
+ }
+ }
+
+ Assert.ok(found, "Didn't find the cookie we were after");
+ } catch (e) {
+ do_throw("Unexpected exception: " + e.toString());
+ }
+
+ do_test_finished();
+}
diff --git a/netwerk/test/unit/test_bug412457.js b/netwerk/test/unit/test_bug412457.js
new file mode 100644
index 0000000000..b5549352a8
--- /dev/null
+++ b/netwerk/test/unit/test_bug412457.js
@@ -0,0 +1,117 @@
+"use strict";
+
+function run_test() {
+ var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
+
+ // check if hostname is unescaped before applying IDNA
+ var newURI = ios.newURI("http://\u5341%2ecom/");
+ Assert.equal(newURI.asciiHost, "xn--kkr.com");
+
+ // escaped UTF8
+ newURI = newURI
+ .mutate()
+ .setSpec("http://%e5%8d%81.com")
+ .finalize();
+ Assert.equal(newURI.asciiHost, "xn--kkr.com");
+
+ // There should be only allowed characters in hostname after
+ // unescaping and attempting to apply IDNA. "\x80" is illegal in
+ // UTF-8, so IDNA fails, and 0x80 is illegal in DNS too.
+ Assert.throws(
+ () => {
+ newURI = newURI
+ .mutate()
+ .setSpec("http://%80.com")
+ .finalize();
+ },
+ /NS_ERROR_UNEXPECTED/,
+ "illegal UTF character"
+ );
+
+ // test parsing URL with all possible host terminators
+ newURI = newURI
+ .mutate()
+ .setSpec("http://example.com?foo")
+ .finalize();
+ Assert.equal(newURI.asciiHost, "example.com");
+
+ newURI = newURI
+ .mutate()
+ .setSpec("http://example.com#foo")
+ .finalize();
+ Assert.equal(newURI.asciiHost, "example.com");
+
+ newURI = newURI
+ .mutate()
+ .setSpec("http://example.com:80")
+ .finalize();
+ Assert.equal(newURI.asciiHost, "example.com");
+
+ newURI = newURI
+ .mutate()
+ .setSpec("http://example.com/foo")
+ .finalize();
+ Assert.equal(newURI.asciiHost, "example.com");
+
+ // Characters that are invalid in the host
+ Assert.throws(
+ () => {
+ newURI = newURI
+ .mutate()
+ .setSpec("http://example.com%3ffoo")
+ .finalize();
+ },
+ /NS_ERROR_MALFORMED_URI/,
+ "bad escaped character"
+ );
+ Assert.throws(
+ () => {
+ newURI = newURI
+ .mutate()
+ .setSpec("http://example.com%23foo")
+ .finalize();
+ },
+ /NS_ERROR_MALFORMED_URI/,
+ "bad escaped character"
+ );
+ Assert.throws(
+ () => {
+ newURI = newURI
+ .mutate()
+ .setSpec("http://example.com%3bfoo")
+ .finalize();
+ },
+ /NS_ERROR_MALFORMED_URI/,
+ "bad escaped character"
+ );
+ Assert.throws(
+ () => {
+ newURI = newURI
+ .mutate()
+ .setSpec("http://example.com%3a80")
+ .finalize();
+ },
+ /NS_ERROR_MALFORMED_URI/,
+ "bad escaped character"
+ );
+ Assert.throws(
+ () => {
+ newURI = newURI
+ .mutate()
+ .setSpec("http://example.com%2ffoo")
+ .finalize();
+ },
+ /NS_ERROR_MALFORMED_URI/,
+ "bad escaped character"
+ );
+ Assert.throws(
+ () => {
+ newURI = newURI
+ .mutate()
+ .setSpec("http://example.com%00")
+ .finalize();
+ },
+ /NS_ERROR_MALFORMED_URI/,
+ "bad escaped character"
+ );
+}
diff --git a/netwerk/test/unit/test_bug412945.js b/netwerk/test/unit/test_bug412945.js
new file mode 100644
index 0000000000..33bfda4de6
--- /dev/null
+++ b/netwerk/test/unit/test_bug412945.js
@@ -0,0 +1,42 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+var httpserv;
+
+function TestListener() {}
+
+TestListener.prototype.onStartRequest = function(request) {};
+
+TestListener.prototype.onStopRequest = function(request, status) {
+ httpserv.stop(do_test_finished);
+};
+
+function run_test() {
+ httpserv = new HttpServer();
+
+ httpserv.registerPathHandler("/bug412945", bug412945);
+
+ httpserv.start(-1);
+
+ // make request
+ var channel = NetUtil.newChannel({
+ uri: "http://localhost:" + httpserv.identity.primaryPort + "/bug412945",
+ loadUsingSystemPrincipal: true,
+ });
+
+ channel.QueryInterface(Ci.nsIHttpChannel);
+ channel.requestMethod = "POST";
+ channel.asyncOpen(new TestListener(), null);
+
+ do_test_pending();
+}
+
+function bug412945(metadata, response) {
+ if (
+ !metadata.hasHeader("Content-Length") ||
+ metadata.getHeader("Content-Length") != "0"
+ ) {
+ do_throw("Content-Length header not found!");
+ }
+}
diff --git a/netwerk/test/unit/test_bug414122.js b/netwerk/test/unit/test_bug414122.js
new file mode 100644
index 0000000000..49251da7fe
--- /dev/null
+++ b/netwerk/test/unit/test_bug414122.js
@@ -0,0 +1,62 @@
+"use strict";
+
+const PR_RDONLY = 0x1;
+
+var etld = Cc["@mozilla.org/network/effective-tld-service;1"].getService(
+ Ci.nsIEffectiveTLDService
+);
+var idn = Cc["@mozilla.org/network/idn-service;1"].getService(Ci.nsIIDNService);
+
+function run_test() {
+ var fis = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(
+ Ci.nsIFileInputStream
+ );
+ fis.init(
+ do_get_file("effective_tld_names.dat"),
+ PR_RDONLY,
+ 0o444,
+ Ci.nsIFileInputStream.CLOSE_ON_EOF
+ );
+
+ var lis = Cc["@mozilla.org/intl/converter-input-stream;1"].createInstance(
+ Ci.nsIConverterInputStream
+ );
+ lis.init(fis, "UTF-8", 1024, 0);
+ lis.QueryInterface(Ci.nsIUnicharLineInputStream);
+
+ var out = { value: "" };
+ do {
+ var more = lis.readLine(out);
+ var line = out.value;
+
+ line = line.replace(/^\s+/, "");
+ var firstTwo = line.substring(0, 2); // a misnomer, but whatever
+ if (firstTwo == "" || firstTwo == "//") {
+ continue;
+ }
+
+ var space = line.search(/[ \t]/);
+ line = line.substring(0, space == -1 ? line.length : space);
+
+ if ("*." == firstTwo) {
+ let rest = line.substring(2);
+ checkPublicSuffix(
+ "foo.SUPER-SPECIAL-AWESOME-PREFIX." + rest,
+ "SUPER-SPECIAL-AWESOME-PREFIX." + rest
+ );
+ } else if ("!" == line.charAt(0)) {
+ checkPublicSuffix(
+ line.substring(1),
+ line.substring(line.indexOf(".") + 1)
+ );
+ } else {
+ checkPublicSuffix("SUPER-SPECIAL-AWESOME-PREFIX." + line, line);
+ }
+ } while (more);
+}
+
+function checkPublicSuffix(host, expectedSuffix) {
+ expectedSuffix = idn.convertUTF8toACE(expectedSuffix).toLowerCase();
+ var actualSuffix = etld.getPublicSuffixFromHost(host);
+ Assert.equal(actualSuffix, expectedSuffix);
+}
diff --git a/netwerk/test/unit/test_bug427957.js b/netwerk/test/unit/test_bug427957.js
new file mode 100644
index 0000000000..f202b92300
--- /dev/null
+++ b/netwerk/test/unit/test_bug427957.js
@@ -0,0 +1,106 @@
+/**
+ * Test for Bidi restrictions on IDNs from RFC 3454
+ */
+
+"use strict";
+
+var idnService;
+
+function expected_pass(inputIDN) {
+ var isASCII = {};
+ var displayIDN = idnService.convertToDisplayIDN(inputIDN, isASCII);
+ Assert.equal(displayIDN, inputIDN);
+}
+
+function expected_fail(inputIDN) {
+ var isASCII = {};
+ var displayIDN = "";
+
+ try {
+ displayIDN = idnService.convertToDisplayIDN(inputIDN, isASCII);
+ } catch (e) {}
+
+ Assert.notEqual(displayIDN, inputIDN);
+}
+
+function run_test() {
+ // add an IDN whitelist pref
+ var pbi = Cc["@mozilla.org/preferences-service;1"].getService(
+ Ci.nsIPrefBranch
+ );
+ pbi.setBoolPref("network.IDN.whitelist.com", true);
+
+ idnService = Cc["@mozilla.org/network/idn-service;1"].getService(
+ Ci.nsIIDNService
+ );
+ /*
+ * In any profile that specifies bidirectional character handling, all
+ * three of the following requirements MUST be met:
+ *
+ * 1) The characters in section 5.8 MUST be prohibited.
+ */
+
+ // 0340; COMBINING GRAVE TONE MARK
+ expected_fail("foo\u0340bar.com");
+ // 0341; COMBINING ACUTE TONE MARK
+ expected_fail("foo\u0341bar.com");
+ // 200E; LEFT-TO-RIGHT MARK
+ expected_fail("foo\u200ebar.com");
+ // 200F; RIGHT-TO-LEFT MARK
+ // Note: this is an RTL IDN so that it doesn't fail test 2) below
+ expected_fail(
+ "\u200f\u0645\u062B\u0627\u0644.\u0622\u0632\u0645\u0627\u06CC\u0634\u06CC"
+ );
+ // 202A; LEFT-TO-RIGHT EMBEDDING
+ expected_fail("foo\u202abar.com");
+ // 202B; RIGHT-TO-LEFT EMBEDDING
+ expected_fail("foo\u202bbar.com");
+ // 202C; POP DIRECTIONAL FORMATTING
+ expected_fail("foo\u202cbar.com");
+ // 202D; LEFT-TO-RIGHT OVERRIDE
+ expected_fail("foo\u202dbar.com");
+ // 202E; RIGHT-TO-LEFT OVERRIDE
+ expected_fail("foo\u202ebar.com");
+ // 206A; INHIBIT SYMMETRIC SWAPPING
+ expected_fail("foo\u206abar.com");
+ // 206B; ACTIVATE SYMMETRIC SWAPPING
+ expected_fail("foo\u206bbar.com");
+ // 206C; INHIBIT ARABIC FORM SHAPING
+ expected_fail("foo\u206cbar.com");
+ // 206D; ACTIVATE ARABIC FORM SHAPING
+ expected_fail("foo\u206dbar.com");
+ // 206E; NATIONAL DIGIT SHAPES
+ expected_fail("foo\u206ebar.com");
+ // 206F; NOMINAL DIGIT SHAPES
+ expected_fail("foo\u206fbar.com");
+
+ /*
+ * 2) If a string contains any RandALCat character, the string MUST NOT
+ * contain any LCat character.
+ */
+
+ // www.מיץpetel.com is invalid
+ expected_fail("www.\u05DE\u05D9\u05E5petel.com");
+ // But www.מיץפטל.com is fine because the ltr and rtl characters are in
+ // different labels
+ expected_pass("www.\u05DE\u05D9\u05E5\u05E4\u05D8\u05DC.com");
+
+ /*
+ * 3) If a string contains any RandALCat character, a RandALCat
+ * character MUST be the first character of the string, and a
+ * RandALCat character MUST be the last character of the string.
+ */
+
+ // www.1מיץ.com is invalid
+ expected_fail("www.1\u05DE\u05D9\u05E5.com");
+ // www.!מיץ.com is invalid
+ expected_fail("www.!\u05DE\u05D9\u05E5.com");
+ // www.מיץ!.com is invalid
+ expected_fail("www.\u05DE\u05D9\u05E5!.com");
+
+ // XXX TODO: add a test for an RTL label ending with a digit. This was
+ // invalid in IDNA2003 but became valid in IDNA2008
+
+ // But www.מיץ1פטל.com is fine
+ expected_pass("www.\u05DE\u05D9\u05E51\u05E4\u05D8\u05DC.com");
+}
diff --git a/netwerk/test/unit/test_bug429347.js b/netwerk/test/unit/test_bug429347.js
new file mode 100644
index 0000000000..7fcc898e7a
--- /dev/null
+++ b/netwerk/test/unit/test_bug429347.js
@@ -0,0 +1,75 @@
+"use strict";
+
+function run_test() {
+ var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
+
+ var uri1 = ios.newURI("http://example.com#bar");
+ var uri2 = ios.newURI("http://example.com/#bar");
+ Assert.ok(uri1.equals(uri2));
+
+ uri1 = uri1
+ .mutate()
+ .setSpec("http://example.com?bar")
+ .finalize();
+ uri2 = uri2
+ .mutate()
+ .setSpec("http://example.com/?bar")
+ .finalize();
+ Assert.ok(uri1.equals(uri2));
+
+ // see https://bugzilla.mozilla.org/show_bug.cgi?id=665706
+ // ";" is not parsed as special anymore and thus ends up
+ // in the authority component (see RFC 3986)
+ uri1 = uri1
+ .mutate()
+ .setSpec("http://example.com;bar")
+ .finalize();
+ uri2 = uri2
+ .mutate()
+ .setSpec("http://example.com/;bar")
+ .finalize();
+ Assert.ok(!uri1.equals(uri2));
+
+ uri1 = uri1
+ .mutate()
+ .setSpec("http://example.com#")
+ .finalize();
+ uri2 = uri2
+ .mutate()
+ .setSpec("http://example.com/#")
+ .finalize();
+ Assert.ok(uri1.equals(uri2));
+
+ uri1 = uri1
+ .mutate()
+ .setSpec("http://example.com?")
+ .finalize();
+ uri2 = uri2
+ .mutate()
+ .setSpec("http://example.com/?")
+ .finalize();
+ Assert.ok(uri1.equals(uri2));
+
+ // see https://bugzilla.mozilla.org/show_bug.cgi?id=665706
+ // ";" is not parsed as special anymore and thus ends up
+ // in the authority component (see RFC 3986)
+ uri1 = uri1
+ .mutate()
+ .setSpec("http://example.com;")
+ .finalize();
+ uri2 = uri2
+ .mutate()
+ .setSpec("http://example.com/;")
+ .finalize();
+ Assert.ok(!uri1.equals(uri2));
+
+ uri1 = uri1
+ .mutate()
+ .setSpec("http://example.com")
+ .finalize();
+ uri2 = uri2
+ .mutate()
+ .setSpec("http://example.com/")
+ .finalize();
+ Assert.ok(uri1.equals(uri2));
+}
diff --git a/netwerk/test/unit/test_bug455311.js b/netwerk/test/unit/test_bug455311.js
new file mode 100644
index 0000000000..faa5685e34
--- /dev/null
+++ b/netwerk/test/unit/test_bug455311.js
@@ -0,0 +1,128 @@
+"use strict";
+
+function getUrlLinkFile() {
+ if (mozinfo.os == "win") {
+ return do_get_file("test_link.url");
+ }
+ if (mozinfo.os == "linux") {
+ return do_get_file("test_link.desktop");
+ }
+ do_throw("Unexpected platform");
+ return null;
+}
+
+const ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
+
+function NotificationCallbacks(origURI, newURI) {
+ this._origURI = origURI;
+ this._newURI = newURI;
+}
+NotificationCallbacks.prototype = {
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIInterfaceRequestor",
+ "nsIChannelEventSink",
+ ]),
+ getInterface(iid) {
+ return this.QueryInterface(iid);
+ },
+ asyncOnChannelRedirect(oldChan, newChan, flags, callback) {
+ Assert.equal(oldChan.URI.spec, this._origURI.spec);
+ Assert.equal(oldChan.URI, this._origURI);
+ Assert.equal(oldChan.originalURI.spec, this._origURI.spec);
+ Assert.equal(oldChan.originalURI, this._origURI);
+ Assert.equal(newChan.originalURI.spec, this._newURI.spec);
+ Assert.equal(newChan.originalURI, newChan.URI);
+ Assert.equal(newChan.URI.spec, this._newURI.spec);
+ throw Components.Exception("", Cr.NS_ERROR_ABORT);
+ },
+};
+
+function RequestObserver(origURI, newURI, nextTest) {
+ this._origURI = origURI;
+ this._newURI = newURI;
+ this._nextTest = nextTest;
+}
+RequestObserver.prototype = {
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIRequestObserver",
+ "nsIStreamListener",
+ ]),
+ onStartRequest(req) {
+ var chan = req.QueryInterface(Ci.nsIChannel);
+ Assert.equal(chan.URI.spec, this._origURI.spec);
+ Assert.equal(chan.URI, this._origURI);
+ Assert.equal(chan.originalURI.spec, this._origURI.spec);
+ Assert.equal(chan.originalURI, this._origURI);
+ },
+ onDataAvailable(req, stream, offset, count) {
+ do_throw("Unexpected call to onDataAvailable");
+ },
+ onStopRequest(req, status) {
+ var chan = req.QueryInterface(Ci.nsIChannel);
+ try {
+ Assert.equal(chan.URI.spec, this._origURI.spec);
+ Assert.equal(chan.URI, this._origURI);
+ Assert.equal(chan.originalURI.spec, this._origURI.spec);
+ Assert.equal(chan.originalURI, this._origURI);
+ Assert.equal(status, Cr.NS_ERROR_ABORT);
+ Assert.ok(!chan.isPending());
+ } catch (e) {}
+ this._nextTest();
+ },
+};
+
+function test_cancel(linkURI, newURI) {
+ var chan = NetUtil.newChannel({
+ uri: linkURI,
+ loadUsingSystemPrincipal: true,
+ });
+ Assert.equal(chan.URI, linkURI);
+ Assert.equal(chan.originalURI, linkURI);
+ chan.asyncOpen(new RequestObserver(linkURI, newURI, do_test_finished));
+ Assert.ok(chan.isPending());
+ chan.cancel(Cr.NS_ERROR_ABORT);
+ Assert.ok(chan.isPending());
+}
+
+function test_channel(linkURI, newURI) {
+ const chan = NetUtil.newChannel({
+ uri: linkURI,
+ loadUsingSystemPrincipal: true,
+ });
+ Assert.equal(chan.URI, linkURI);
+ Assert.equal(chan.originalURI, linkURI);
+ chan.notificationCallbacks = new NotificationCallbacks(linkURI, newURI);
+ chan.asyncOpen(
+ new RequestObserver(linkURI, newURI, () => test_cancel(linkURI, newURI))
+ );
+ Assert.ok(chan.isPending());
+}
+
+function run_test() {
+ if (mozinfo.os != "win" && mozinfo.os != "linux") {
+ return;
+ }
+
+ let link = getUrlLinkFile();
+ let linkURI;
+ if (link.isSymlink()) {
+ let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ file.initWithPath(link.target);
+ linkURI = ios.newFileURI(file);
+ } else {
+ linkURI = ios.newFileURI(link);
+ }
+
+ do_test_pending();
+ test_channel(linkURI, ios.newURI("http://www.mozilla.org/"));
+
+ if (mozinfo.os != "win") {
+ return;
+ }
+
+ link = do_get_file("test_link.lnk");
+ test_channel(
+ ios.newFileURI(link),
+ ios.newURI("file:///Z:/moz-nonexistent/index.html")
+ );
+}
diff --git a/netwerk/test/unit/test_bug464591.js b/netwerk/test/unit/test_bug464591.js
new file mode 100644
index 0000000000..d2200c1eb6
--- /dev/null
+++ b/netwerk/test/unit/test_bug464591.js
@@ -0,0 +1,100 @@
+// 1.percent-encoded IDN that contains blacklisted character should be converted
+// to punycode, not UTF-8 string
+// 2.only hostname-valid percent encoded ASCII characters should be decoded
+// 3.IDN convertion must not bypassed by %00
+
+"use strict";
+
+let reference = [
+ [
+ "www.example.com%e2%88%95www.mozill%d0%b0.com%e2%81%84www.mozilla.org",
+ "www.example.xn--comwww-re3c.xn--mozill-8nf.xn--comwww-rq0c.mozilla.org",
+ ],
+];
+
+let badURIs = [
+ ["www.mozill%61%2f.org"], // a slash is not valid in the hostname
+ ["www.e%00xample.com%e2%88%95www.mozill%d0%b0.com%e2%81%84www.mozill%61.org"],
+];
+
+let prefData = [
+ {
+ name: "network.enableIDN",
+ newVal: true,
+ },
+ {
+ name: "network.IDN_show_punycode",
+ newVal: false,
+ },
+ {
+ name: "network.IDN.whitelist.org",
+ newVal: true,
+ },
+];
+
+let prefIdnBlackList = {
+ name: "network.IDN.extra_blocked_chars",
+ minimumList: "\u2215\u0430\u2044",
+};
+
+function stringToURL(str) {
+ return Cc["@mozilla.org/network/standard-url-mutator;1"]
+ .createInstance(Ci.nsIStandardURLMutator)
+ .init(Ci.nsIStandardURL.URLTYPE_AUTHORITY, 80, str, "UTF-8", null)
+ .finalize()
+ .QueryInterface(Ci.nsIURL);
+}
+
+function run_test() {
+ // Make sure our prefs are set such that this test actually means something
+ let prefs = Cc["@mozilla.org/preferences-service;1"].getService(
+ Ci.nsIPrefBranch
+ );
+ for (let pref of prefData) {
+ prefs.setBoolPref(pref.name, pref.newVal);
+ }
+
+ prefIdnBlackList.set = false;
+ try {
+ prefIdnBlackList.oldVal = prefs.getComplexValue(
+ prefIdnBlackList.name,
+ Ci.nsIPrefLocalizedString
+ ).data;
+ prefs.getComplexValue(
+ prefIdnBlackList.name,
+ Ci.nsIPrefLocalizedString
+ ).data = prefIdnBlackList.minimumList;
+ prefIdnBlackList.set = true;
+ } catch (e) {}
+
+ registerCleanupFunction(function() {
+ for (let pref of prefData) {
+ prefs.clearUserPref(pref.name);
+ }
+ if (prefIdnBlackList.set) {
+ prefs.getComplexValue(
+ prefIdnBlackList.name,
+ Ci.nsIPrefLocalizedString
+ ).data = prefIdnBlackList.oldVal;
+ }
+ });
+
+ for (let i = 0; i < reference.length; ++i) {
+ try {
+ let result = stringToURL("http://" + reference[i][0]).host;
+ equal(result, reference[i][1]);
+ } catch (e) {
+ ok(false, "Error testing " + reference[i][0]);
+ }
+ }
+
+ for (let i = 0; i < badURIs.length; ++i) {
+ Assert.throws(
+ () => {
+ stringToURL("http://" + badURIs[i][0]).host;
+ },
+ /NS_ERROR_MALFORMED_URI/,
+ "bad escaped character"
+ );
+ }
+}
diff --git a/netwerk/test/unit/test_bug468426.js b/netwerk/test/unit/test_bug468426.js
new file mode 100644
index 0000000000..f8f6b8ed62
--- /dev/null
+++ b/netwerk/test/unit/test_bug468426.js
@@ -0,0 +1,129 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+var httpserver = new HttpServer();
+var index = 0;
+var tests = [
+ // Initial request. Cached variant will have no cookie
+ { url: "/bug468426", server: "0", expected: "0", cookie: null },
+
+ // Cache now contains a variant with no value for cookie. If we don't
+ // set cookie we expect to receive the cached variant
+ { url: "/bug468426", server: "1", expected: "0", cookie: null },
+
+ // Cache still contains a variant with no value for cookie. If we
+ // set a value for cookie we expect a fresh value
+ { url: "/bug468426", server: "2", expected: "2", cookie: "c=2" },
+
+ // Cache now contains a variant with cookie "c=2". If the request
+ // also set cookie "c=2", we expect to receive the cached variant.
+ { url: "/bug468426", server: "3", expected: "2", cookie: "c=2" },
+
+ // Cache still contains a variant with cookie "c=2". When setting
+ // cookie "c=4" in the request we expect a fresh value
+ { url: "/bug468426", server: "4", expected: "4", cookie: "c=4" },
+
+ // Cache now contains a variant with cookie "c=4". When setting
+ // cookie "c=4" in the request we expect the cached variant
+ { url: "/bug468426", server: "5", expected: "4", cookie: "c=4" },
+
+ // Cache still contains a variant with cookie "c=4". When setting
+ // no cookie in the request we expect a fresh value
+ { url: "/bug468426", server: "6", expected: "6", cookie: null },
+];
+
+function setupChannel(suffix, value, cookie) {
+ var chan = NetUtil.newChannel({
+ uri: "http://localhost:" + httpserver.identity.primaryPort + suffix,
+ loadUsingSystemPrincipal: true,
+ });
+ var httpChan = chan.QueryInterface(Ci.nsIHttpChannel);
+ httpChan.requestMethod = "GET";
+ httpChan.setRequestHeader("x-request", value, false);
+ if (cookie != null) {
+ httpChan.setRequestHeader("Cookie", cookie, false);
+ }
+ return httpChan;
+}
+
+function triggerNextTest() {
+ var channel = setupChannel(
+ tests[index].url,
+ tests[index].server,
+ tests[index].cookie
+ );
+ channel.asyncOpen(new ChannelListener(checkValueAndTrigger, null));
+}
+
+function checkValueAndTrigger(request, data, ctx) {
+ Assert.equal(tests[index].expected, data);
+
+ if (index < tests.length - 1) {
+ index++;
+ // This call happens in onStopRequest from the channel. Opening a new
+ // channel to the same url here is no good idea! Post it instead...
+ do_timeout(1, triggerNextTest);
+ } else {
+ httpserver.stop(do_test_finished);
+ }
+}
+
+function run_test() {
+ httpserver.registerPathHandler("/bug468426", handler);
+ httpserver.start(-1);
+
+ // Clear cache and trigger the first test
+ evict_cache_entries();
+ triggerNextTest();
+
+ do_test_pending();
+}
+
+function handler(metadata, response) {
+ var body = "unset";
+ try {
+ body = metadata.getHeader("x-request");
+ } catch (e) {}
+ response.setStatusLine(metadata.httpVersion, 200, "Ok");
+ response.setHeader("Content-Type", "text/plain", false);
+ response.setHeader("Last-Modified", getDateString(-1), false);
+ response.setHeader("Vary", "Cookie", false);
+ response.bodyOutputStream.write(body, body.length);
+}
+
+function getDateString(yearDelta) {
+ var months = [
+ "Jan",
+ "Feb",
+ "Mar",
+ "Apr",
+ "May",
+ "Jun",
+ "Jul",
+ "Aug",
+ "Sep",
+ "Oct",
+ "Nov",
+ "Dec",
+ ];
+ var days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
+
+ var d = new Date();
+ return (
+ days[d.getUTCDay()] +
+ ", " +
+ d.getUTCDate() +
+ " " +
+ months[d.getUTCMonth()] +
+ " " +
+ (d.getUTCFullYear() + yearDelta) +
+ " " +
+ d.getUTCHours() +
+ ":" +
+ d.getUTCMinutes() +
+ ":" +
+ d.getUTCSeconds() +
+ " UTC"
+ );
+}
diff --git a/netwerk/test/unit/test_bug468594.js b/netwerk/test/unit/test_bug468594.js
new file mode 100644
index 0000000000..545ae75e5f
--- /dev/null
+++ b/netwerk/test/unit/test_bug468594.js
@@ -0,0 +1,176 @@
+//
+// This script emulates the test called "Freshness"
+// by Mark Nottingham, located at
+//
+// http://mnot.net/javascript/xmlhttprequest/cache.html
+//
+// The issue with Mr. Nottinghams page is that the server
+// always seems to send an Expires-header in the response,
+// breaking the finer details of the test. This script has
+// full control of response-headers, however, and can perform
+// the intended testing plus some extra stuff.
+//
+// Please see RFC 2616 section 13.2.1 6th paragraph for the
+// definition of "explicit expiration time" being used here.
+
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+var httpserver = new HttpServer();
+var index = 0;
+var tests = [
+ { url: "/freshness", server: "0", expected: "0" },
+ { url: "/freshness", server: "1", expected: "0" }, // cached
+
+ // RFC 2616 section 13.9 2nd paragraph says not to heuristically cache
+ // querystring, but we allow it to maintain web compat
+ { url: "/freshness?a", server: "2", expected: "2" },
+ { url: "/freshness?a", server: "3", expected: "2" },
+
+ // explicit expiration dates in the future should be cached
+ {
+ url: "/freshness?b",
+ server: "4",
+ expected: "4",
+ responseheader: "Expires: " + getDateString(1),
+ },
+ { url: "/freshness?b", server: "5", expected: "4" }, // cached due to Expires
+
+ {
+ url: "/freshness?c",
+ server: "6",
+ expected: "6",
+ responseheader: "Cache-Control: max-age=3600",
+ },
+ { url: "/freshness?c", server: "7", expected: "6" }, // cached due to max-age
+
+ // explicit expiration dates in the past should NOT be cached
+ {
+ url: "/freshness?d",
+ server: "8",
+ expected: "8",
+ responseheader: "Expires: " + getDateString(-1),
+ },
+ { url: "/freshness?d", server: "9", expected: "9" },
+
+ {
+ url: "/freshness?e",
+ server: "10",
+ expected: "10",
+ responseheader: "Cache-Control: max-age=0",
+ },
+ { url: "/freshness?e", server: "11", expected: "11" },
+
+ { url: "/freshness", server: "99", expected: "0" }, // cached
+];
+
+function logit(i, data) {
+ dump(
+ tests[i].url +
+ "\t requested [" +
+ tests[i].server +
+ "]" +
+ " got [" +
+ data +
+ "] expected [" +
+ tests[i].expected +
+ "]"
+ );
+ if (tests[i].responseheader) {
+ dump("\t[" + tests[i].responseheader + "]");
+ }
+ dump("\n");
+}
+
+function setupChannel(suffix, value) {
+ var chan = NetUtil.newChannel({
+ uri: "http://localhost:" + httpserver.identity.primaryPort + suffix,
+ loadUsingSystemPrincipal: true,
+ });
+ var httpChan = chan.QueryInterface(Ci.nsIHttpChannel);
+ httpChan.requestMethod = "GET";
+ httpChan.setRequestHeader("x-request", value, false);
+ return httpChan;
+}
+
+function triggerNextTest() {
+ var channel = setupChannel(tests[index].url, tests[index].server);
+ channel.asyncOpen(new ChannelListener(checkValueAndTrigger, null));
+}
+
+function checkValueAndTrigger(request, data, ctx) {
+ logit(index, data);
+ Assert.equal(tests[index].expected, data);
+
+ if (index < tests.length - 1) {
+ index++;
+ triggerNextTest();
+ } else {
+ httpserver.stop(do_test_finished);
+ }
+}
+
+function run_test() {
+ httpserver.registerPathHandler("/freshness", handler);
+ httpserver.start(-1);
+
+ // clear cache
+ evict_cache_entries();
+ triggerNextTest();
+
+ do_test_pending();
+}
+
+function handler(metadata, response) {
+ var body = metadata.getHeader("x-request");
+ response.setHeader("Content-Type", "text/plain", false);
+ response.setHeader("Date", getDateString(0), false);
+
+ var header = tests[index].responseheader;
+ if (header == null) {
+ response.setHeader("Last-Modified", getDateString(-1), false);
+ } else {
+ var splitHdr = header.split(": ");
+ response.setHeader(splitHdr[0], splitHdr[1], false);
+ }
+
+ response.setStatusLine(metadata.httpVersion, 200, "OK");
+ response.bodyOutputStream.write(body, body.length);
+}
+
+function getDateString(yearDelta) {
+ var months = [
+ "Jan",
+ "Feb",
+ "Mar",
+ "Apr",
+ "May",
+ "Jun",
+ "Jul",
+ "Aug",
+ "Sep",
+ "Oct",
+ "Nov",
+ "Dec",
+ ];
+ var days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
+
+ var d = new Date();
+ return (
+ days[d.getUTCDay()] +
+ ", " +
+ d.getUTCDate() +
+ " " +
+ months[d.getUTCMonth()] +
+ " " +
+ (d.getUTCFullYear() + yearDelta) +
+ " " +
+ d.getUTCHours() +
+ ":" +
+ d.getUTCMinutes() +
+ ":" +
+ d.getUTCSeconds() +
+ " UTC"
+ );
+}
diff --git a/netwerk/test/unit/test_bug470716.js b/netwerk/test/unit/test_bug470716.js
new file mode 100644
index 0000000000..a661dc902d
--- /dev/null
+++ b/netwerk/test/unit/test_bug470716.js
@@ -0,0 +1,171 @@
+"use strict";
+
+var CC = Components.Constructor;
+
+const StreamCopier = CC(
+ "@mozilla.org/network/async-stream-copier;1",
+ "nsIAsyncStreamCopier",
+ "init"
+);
+
+const ScriptableInputStream = CC(
+ "@mozilla.org/scriptableinputstream;1",
+ "nsIScriptableInputStream",
+ "init"
+);
+
+const Pipe = CC("@mozilla.org/pipe;1", "nsIPipe", "init");
+
+var pipe1;
+var pipe2;
+var copier;
+var test_result;
+var test_content;
+var test_source_closed;
+var test_sink_closed;
+var test_nr;
+
+var copyObserver = {
+ onStartRequest(request) {},
+
+ onStopRequest(request, statusCode) {
+ // check status code
+ Assert.equal(statusCode, test_result);
+
+ // check number of copied bytes
+ Assert.equal(pipe2.inputStream.available(), test_content.length);
+
+ // check content
+ var scinp = new ScriptableInputStream(pipe2.inputStream);
+ var content = scinp.read(scinp.available());
+ Assert.equal(content, test_content);
+
+ // check closed sink
+ try {
+ pipe2.outputStream.write("closedSinkTest", 14);
+ Assert.ok(!test_sink_closed);
+ } catch (ex) {
+ Assert.ok(test_sink_closed);
+ }
+
+ // check closed source
+ try {
+ pipe1.outputStream.write("closedSourceTest", 16);
+ Assert.ok(!test_source_closed);
+ } catch (ex) {
+ Assert.ok(test_source_closed);
+ }
+
+ do_timeout(0, do_test);
+ },
+
+ QueryInterface: ChromeUtils.generateQI(["nsIRequestObserver"]),
+};
+
+function startCopier(closeSource, closeSink) {
+ pipe1 = new Pipe(
+ true /* nonBlockingInput */,
+ true /* nonBlockingOutput */,
+ 0 /* segmentSize */,
+ 0xffffffff /* segmentCount */,
+ null /* segmentAllocator */
+ );
+
+ pipe2 = new Pipe(
+ true /* nonBlockingInput */,
+ true /* nonBlockingOutput */,
+ 0 /* segmentSize */,
+ 0xffffffff /* segmentCount */,
+ null /* segmentAllocator */
+ );
+
+ copier = new StreamCopier(
+ pipe1.inputStream /* aSource */,
+ pipe2.outputStream /* aSink */,
+ null /* aTarget */,
+ true /* aSourceBuffered */,
+ true /* aSinkBuffered */,
+ 8192 /* aChunkSize */,
+ closeSource /* aCloseSource */,
+ closeSink /* aCloseSink */
+ );
+
+ copier.asyncCopy(copyObserver, null);
+}
+
+function do_test() {
+ test_nr++;
+ test_content = "test" + test_nr;
+
+ switch (test_nr) {
+ case 1:
+ case 2: // close sink
+ case 3: // close source
+ case 4: // close both
+ // test canceling transfer
+ // use some undefined error code to check if it is successfully passed
+ // to the request observer
+ test_result = 0x87654321;
+
+ test_source_closed = (test_nr - 1) >> 1 != 0;
+ test_sink_closed = (test_nr - 1) % 2 != 0;
+
+ startCopier(test_source_closed, test_sink_closed);
+ pipe1.outputStream.write(test_content, test_content.length);
+ pipe1.outputStream.flush();
+ do_timeout(20, function() {
+ copier.cancel(test_result);
+ pipe1.outputStream.write("a", 1);
+ });
+ break;
+ case 5:
+ case 6: // close sink
+ case 7: // close source
+ case 8: // close both
+ // test copying with EOF on source
+ test_result = 0;
+
+ test_source_closed = (test_nr - 5) >> 1 != 0;
+ test_sink_closed = (test_nr - 5) % 2 != 0;
+
+ startCopier(test_source_closed, test_sink_closed);
+ pipe1.outputStream.write(test_content, test_content.length);
+ // we will close the source
+ test_source_closed = true;
+ pipe1.outputStream.close();
+ break;
+ case 9:
+ case 10: // close sink
+ case 11: // close source
+ case 12: // close both
+ // test copying with error on sink
+ // use some undefined error code to check if it is successfully passed
+ // to the request observer
+ test_result = 0x87654321;
+
+ test_source_closed = (test_nr - 9) >> 1 != 0;
+ test_sink_closed = (test_nr - 9) % 2 != 0;
+
+ startCopier(test_source_closed, test_sink_closed);
+ pipe1.outputStream.write(test_content, test_content.length);
+ pipe1.outputStream.flush();
+ // we will close the sink
+ test_sink_closed = true;
+ do_timeout(20, function() {
+ pipe2.outputStream
+ .QueryInterface(Ci.nsIAsyncOutputStream)
+ .closeWithStatus(test_result);
+ pipe1.outputStream.write("a", 1);
+ });
+ break;
+ case 13:
+ do_test_finished();
+ break;
+ }
+}
+
+function run_test() {
+ test_nr = 0;
+ do_timeout(0, do_test);
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_bug477578.js b/netwerk/test/unit/test_bug477578.js
new file mode 100644
index 0000000000..c21b1281da
--- /dev/null
+++ b/netwerk/test/unit/test_bug477578.js
@@ -0,0 +1,51 @@
+// test that methods are not normalized
+
+"use strict";
+
+const testMethods = [
+ ["GET"],
+ ["get"],
+ ["Get"],
+ ["gET"],
+ ["gEt"],
+ ["post"],
+ ["POST"],
+ ["head"],
+ ["HEAD"],
+ ["put"],
+ ["PUT"],
+ ["delete"],
+ ["DELETE"],
+ ["connect"],
+ ["CONNECT"],
+ ["options"],
+ ["trace"],
+ ["track"],
+ ["copy"],
+ ["index"],
+ ["lock"],
+ ["m-post"],
+ ["mkcol"],
+ ["move"],
+ ["propfind"],
+ ["proppatch"],
+ ["unlock"],
+ ["link"],
+ ["LINK"],
+ ["foo"],
+ ["foO"],
+ ["fOo"],
+ ["Foo"],
+];
+
+function run_test() {
+ var chan = NetUtil.newChannel({
+ uri: "http://localhost/",
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+
+ for (var i = 0; i < testMethods.length; i++) {
+ chan.requestMethod = testMethods[i];
+ Assert.equal(chan.requestMethod, testMethods[i]);
+ }
+}
diff --git a/netwerk/test/unit/test_bug479413.js b/netwerk/test/unit/test_bug479413.js
new file mode 100644
index 0000000000..8a0ce0752e
--- /dev/null
+++ b/netwerk/test/unit/test_bug479413.js
@@ -0,0 +1,61 @@
+/**
+ * Test for unassigned code points in IDNs (RFC 3454 section 7)
+ */
+
+"use strict";
+
+var idnService;
+
+function expected_pass(inputIDN) {
+ var isASCII = {};
+ var displayIDN = idnService.convertToDisplayIDN(inputIDN, isASCII);
+ Assert.equal(displayIDN, inputIDN);
+}
+
+function expected_fail(inputIDN) {
+ var isASCII = {};
+ var displayIDN = "";
+
+ try {
+ displayIDN = idnService.convertToDisplayIDN(inputIDN, isASCII);
+ } catch (e) {}
+
+ Assert.notEqual(displayIDN, inputIDN);
+}
+
+function run_test() {
+ // add an IDN whitelist pref
+ var pbi = Cc["@mozilla.org/preferences-service;1"].getService(
+ Ci.nsIPrefBranch
+ );
+ var whitelistPref = "network.IDN.whitelist.com";
+
+ pbi.setBoolPref(whitelistPref, true);
+
+ idnService = Cc["@mozilla.org/network/idn-service;1"].getService(
+ Ci.nsIIDNService
+ );
+
+ // assigned code point
+ expected_pass("foo\u0101bar.com");
+
+ // assigned code point in punycode. Should *fail* because the URL will be
+ // converted to Unicode for display
+ expected_fail("xn--foobar-5za.com");
+
+ // unassigned code point
+ expected_fail("foo\u3040bar.com");
+
+ // unassigned code point in punycode. Should *pass* because the URL will not
+ // be converted to Unicode
+ expected_pass("xn--foobar-533e.com");
+
+ // code point assigned since Unicode 3.0
+ // XXX This test will unexpectedly pass when we update to IDNAbis
+ expected_fail("foo\u0370bar.com");
+
+ // reset the pref
+ if (pbi.prefHasUserValue(whitelistPref)) {
+ pbi.clearUserPref(whitelistPref);
+ }
+}
diff --git a/netwerk/test/unit/test_bug479485.js b/netwerk/test/unit/test_bug479485.js
new file mode 100644
index 0000000000..bde98c6b70
--- /dev/null
+++ b/netwerk/test/unit/test_bug479485.js
@@ -0,0 +1,65 @@
+"use strict";
+
+function run_test() {
+ var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
+
+ var test_port = function(port, exception_expected) {
+ dump((port || "no port provided") + "\n");
+ var exception_threw = false;
+ try {
+ var newURI = ios.newURI("http://foo.com" + port);
+ } catch (e) {
+ exception_threw = e.result == Cr.NS_ERROR_MALFORMED_URI;
+ }
+ if (exception_threw != exception_expected) {
+ do_throw(
+ "We did" +
+ (exception_expected ? "n't" : "") +
+ " throw NS_ERROR_MALFORMED_URI when creating a new URI with " +
+ port +
+ " as a port"
+ );
+ }
+ Assert.equal(exception_threw, exception_expected);
+
+ exception_threw = false;
+ newURI = ios.newURI("http://foo.com");
+ try {
+ newURI
+ .mutate()
+ .setSpec("http://foo.com" + port)
+ .finalize();
+ } catch (e) {
+ exception_threw = e.result == Cr.NS_ERROR_MALFORMED_URI;
+ }
+ if (exception_threw != exception_expected) {
+ do_throw(
+ "We did" +
+ (exception_expected ? "n't" : "") +
+ " throw NS_ERROR_MALFORMED_URI when setting a spec of a URI with " +
+ port +
+ " as a port"
+ );
+ }
+ Assert.equal(exception_threw, exception_expected);
+ };
+
+ test_port(":invalid", true);
+ test_port(":-2", true);
+ test_port(":-1", true);
+ test_port(":0", false);
+ test_port(
+ ":185891548721348172817857824356013651809236172635716571865023757816234081723451516780356",
+ true
+ );
+
+ // Following 3 tests are all failing, we do not throw, although we parse the whole string and use only 5870 as a portnumber
+ test_port(":5870:80", true);
+ test_port(":5870-80", true);
+ test_port(":5870+80", true);
+
+ // Just a regression check
+ test_port(":5870", false);
+ test_port(":80", false);
+ test_port("", false);
+}
diff --git a/netwerk/test/unit/test_bug482601.js b/netwerk/test/unit/test_bug482601.js
new file mode 100644
index 0000000000..a583a21663
--- /dev/null
+++ b/netwerk/test/unit/test_bug482601.js
@@ -0,0 +1,262 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+var httpserv = null;
+var test_nr = 0;
+var observers_called = "";
+var handlers_called = "";
+var buffer = "";
+
+var observer = {
+ QueryInterface: ChromeUtils.generateQI(["nsIObserver"]),
+
+ observe(subject, topic, data) {
+ if (observers_called.length) {
+ observers_called += ",";
+ }
+
+ observers_called += topic;
+ },
+};
+
+var listener = {
+ onStartRequest(request) {
+ buffer = "";
+ },
+
+ onDataAvailable(request, stream, offset, count) {
+ buffer = buffer.concat(read_stream(stream, count));
+ },
+
+ onStopRequest(request, status) {
+ Assert.equal(status, Cr.NS_OK);
+ Assert.equal(buffer, "0123456789");
+ Assert.equal(observers_called, results[test_nr]);
+ test_nr++;
+ do_timeout(0, do_test);
+ },
+};
+
+function run_test() {
+ httpserv = new HttpServer();
+ httpserv.registerPathHandler("/bug482601/nocache", bug482601_nocache);
+ httpserv.registerPathHandler("/bug482601/partial", bug482601_partial);
+ httpserv.registerPathHandler("/bug482601/cached", bug482601_cached);
+ httpserv.registerPathHandler(
+ "/bug482601/only_from_cache",
+ bug482601_only_from_cache
+ );
+ httpserv.start(-1);
+
+ var obs = Cc["@mozilla.org/observer-service;1"].getService();
+ obs = obs.QueryInterface(Ci.nsIObserverService);
+ obs.addObserver(observer, "http-on-examine-response");
+ obs.addObserver(observer, "http-on-examine-merged-response");
+ obs.addObserver(observer, "http-on-examine-cached-response");
+
+ do_timeout(0, do_test);
+ do_test_pending();
+}
+
+function do_test() {
+ if (test_nr < tests.length) {
+ tests[test_nr]();
+ } else {
+ Assert.equal(handlers_called, "nocache,partial,cached");
+ httpserv.stop(do_test_finished);
+ }
+}
+
+var tests = [test_nocache, test_partial, test_cached, test_only_from_cache];
+
+var results = [
+ "http-on-examine-response",
+ "http-on-examine-response,http-on-examine-merged-response",
+ "http-on-examine-response,http-on-examine-merged-response",
+ "http-on-examine-cached-response",
+];
+
+function makeChan(url) {
+ return NetUtil.newChannel({
+ uri: url,
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+}
+
+function storeCache(aCacheEntry, aResponseHeads, aContent) {
+ aCacheEntry.setMetaDataElement("request-method", "GET");
+ aCacheEntry.setMetaDataElement("response-head", aResponseHeads);
+ aCacheEntry.setMetaDataElement("charset", "ISO-8859-1");
+
+ var oStream = aCacheEntry.openOutputStream(0, aContent.length);
+ var written = oStream.write(aContent, aContent.length);
+ if (written != aContent.length) {
+ do_throw(
+ "oStream.write has not written all data!\n" +
+ " Expected: " +
+ written +
+ "\n" +
+ " Actual: " +
+ aContent.length +
+ "\n"
+ );
+ }
+ oStream.close();
+ aCacheEntry.close();
+}
+
+function test_nocache() {
+ observers_called = "";
+
+ var chan = makeChan(
+ "http://localhost:" + httpserv.identity.primaryPort + "/bug482601/nocache"
+ );
+ chan.asyncOpen(listener);
+}
+
+function test_partial() {
+ asyncOpenCacheEntry(
+ "http://localhost:" + httpserv.identity.primaryPort + "/bug482601/partial",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ test_partial2
+ );
+}
+
+function test_partial2(status, entry) {
+ Assert.equal(status, Cr.NS_OK);
+ storeCache(
+ entry,
+ "HTTP/1.1 200 OK\r\n" +
+ "Date: Thu, 1 Jan 2009 00:00:00 GMT\r\n" +
+ "Server: httpd.js\r\n" +
+ "Last-Modified: Thu, 1 Jan 2009 00:00:00 GMT\r\n" +
+ "Accept-Ranges: bytes\r\n" +
+ "Content-Length: 10\r\n" +
+ "Content-Type: text/plain\r\n",
+ "0123"
+ );
+
+ observers_called = "";
+
+ var chan = makeChan(
+ "http://localhost:" + httpserv.identity.primaryPort + "/bug482601/partial"
+ );
+ chan.asyncOpen(listener);
+}
+
+function test_cached() {
+ asyncOpenCacheEntry(
+ "http://localhost:" + httpserv.identity.primaryPort + "/bug482601/cached",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ test_cached2
+ );
+}
+
+function test_cached2(status, entry) {
+ Assert.equal(status, Cr.NS_OK);
+ storeCache(
+ entry,
+ "HTTP/1.1 200 OK\r\n" +
+ "Date: Thu, 1 Jan 2009 00:00:00 GMT\r\n" +
+ "Server: httpd.js\r\n" +
+ "Last-Modified: Thu, 1 Jan 2009 00:00:00 GMT\r\n" +
+ "Accept-Ranges: bytes\r\n" +
+ "Content-Length: 10\r\n" +
+ "Content-Type: text/plain\r\n",
+ "0123456789"
+ );
+
+ observers_called = "";
+
+ var chan = makeChan(
+ "http://localhost:" + httpserv.identity.primaryPort + "/bug482601/cached"
+ );
+ chan.loadFlags = Ci.nsIRequest.VALIDATE_ALWAYS;
+ chan.asyncOpen(listener);
+}
+
+function test_only_from_cache() {
+ asyncOpenCacheEntry(
+ "http://localhost:" +
+ httpserv.identity.primaryPort +
+ "/bug482601/only_from_cache",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ test_only_from_cache2
+ );
+}
+
+function test_only_from_cache2(status, entry) {
+ Assert.equal(status, Cr.NS_OK);
+ storeCache(
+ entry,
+ "HTTP/1.1 200 OK\r\n" +
+ "Date: Thu, 1 Jan 2009 00:00:00 GMT\r\n" +
+ "Server: httpd.js\r\n" +
+ "Last-Modified: Thu, 1 Jan 2009 00:00:00 GMT\r\n" +
+ "Accept-Ranges: bytes\r\n" +
+ "Content-Length: 10\r\n" +
+ "Content-Type: text/plain\r\n",
+ "0123456789"
+ );
+
+ observers_called = "";
+
+ var chan = makeChan(
+ "http://localhost:" +
+ httpserv.identity.primaryPort +
+ "/bug482601/only_from_cache"
+ );
+ chan.loadFlags = Ci.nsICachingChannel.LOAD_ONLY_FROM_CACHE;
+ chan.asyncOpen(listener);
+}
+
+// PATHS
+
+// /bug482601/nocache
+function bug482601_nocache(metadata, response) {
+ response.setHeader("Content-Type", "text/plain", false);
+ var body = "0123456789";
+ response.bodyOutputStream.write(body, body.length);
+ handlers_called += "nocache";
+}
+
+// /bug482601/partial
+function bug482601_partial(metadata, response) {
+ Assert.ok(metadata.hasHeader("If-Range"));
+ Assert.equal(metadata.getHeader("If-Range"), "Thu, 1 Jan 2009 00:00:00 GMT");
+ Assert.ok(metadata.hasHeader("Range"));
+ Assert.equal(metadata.getHeader("Range"), "bytes=4-");
+
+ response.setStatusLine(metadata.httpVersion, 206, "Partial Content");
+ response.setHeader("Content-Range", "bytes 4-9/10", false);
+ response.setHeader("Content-Type", "text/plain", false);
+ response.setHeader("Last-Modified", "Thu, 1 Jan 2009 00:00:00 GMT");
+
+ var body = "456789";
+ response.bodyOutputStream.write(body, body.length);
+ handlers_called += ",partial";
+}
+
+// /bug482601/cached
+function bug482601_cached(metadata, response) {
+ Assert.ok(metadata.hasHeader("If-Modified-Since"));
+ Assert.equal(
+ metadata.getHeader("If-Modified-Since"),
+ "Thu, 1 Jan 2009 00:00:00 GMT"
+ );
+
+ response.setStatusLine(metadata.httpVersion, 304, "Not Modified");
+ handlers_called += ",cached";
+}
+
+// /bug482601/only_from_cache
+function bug482601_only_from_cache(metadata, response) {
+ do_throw("This should not be reached");
+}
diff --git a/netwerk/test/unit/test_bug482934.js b/netwerk/test/unit/test_bug482934.js
new file mode 100644
index 0000000000..2827e4c875
--- /dev/null
+++ b/netwerk/test/unit/test_bug482934.js
@@ -0,0 +1,189 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+var response_code;
+var response_body;
+
+var request_time;
+var response_time;
+
+var cache_storage;
+
+var httpserver = new HttpServer();
+httpserver.start(-1);
+
+var base_url = "http://localhost:" + httpserver.identity.primaryPort;
+var resource = "/resource";
+var resource_url = base_url + resource;
+
+// Test flags
+var hit_server = false;
+
+function make_channel(aUrl) {
+ // Reset test global status
+ hit_server = false;
+
+ var req = NetUtil.newChannel({ uri: aUrl, loadUsingSystemPrincipal: true });
+ req.QueryInterface(Ci.nsIHttpChannel);
+ req.setRequestHeader("If-Modified-Since", request_time, false);
+ return req;
+}
+
+function make_uri(aUrl) {
+ var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
+ return ios.newURI(aUrl);
+}
+
+function resource_handler(aMetadata, aResponse) {
+ hit_server = true;
+ Assert.ok(aMetadata.hasHeader("If-Modified-Since"));
+ Assert.equal(aMetadata.getHeader("If-Modified-Since"), request_time);
+
+ if (response_code == "200") {
+ aResponse.setStatusLine(aMetadata.httpVersion, 200, "OK");
+ aResponse.setHeader("Content-Type", "text/plain", false);
+ aResponse.setHeader("Last-Modified", response_time, false);
+
+ aResponse.bodyOutputStream.write(response_body, response_body.length);
+ } else if (response_code == "304") {
+ aResponse.setStatusLine(aMetadata.httpVersion, 304, "Not Modified");
+ aResponse.setHeader("Returned-From-Handler", "1");
+ }
+}
+
+function check_cached_data(aCachedData, aCallback) {
+ asyncOpenCacheEntry(
+ resource_url,
+ "disk",
+ Ci.nsICacheStorage.OPEN_READONLY,
+ null,
+ function(aStatus, aEntry) {
+ Assert.equal(aStatus, Cr.NS_OK);
+ pumpReadStream(aEntry.openInputStream(0), function(aData) {
+ Assert.equal(aData, aCachedData);
+ aCallback();
+ });
+ }
+ );
+}
+
+function run_test() {
+ do_get_profile();
+ evict_cache_entries();
+
+ do_test_pending();
+
+ cache_storage = getCacheStorage("disk");
+ httpserver.registerPathHandler(resource, resource_handler);
+
+ wait_for_cache_index(run_next_test);
+}
+
+// 1. send custom conditional request when we don't have an entry
+// server returns 304 -> client receives 304
+add_test(() => {
+ response_code = "304";
+ response_body = "";
+ request_time = "Thu, 1 Jan 2009 00:00:00 GMT";
+ response_time = "Thu, 1 Jan 2009 00:00:00 GMT";
+
+ var ch = make_channel(resource_url);
+ ch.asyncOpen(
+ new ChannelListener(function(aRequest, aData) {
+ syncWithCacheIOThread(() => {
+ Assert.ok(hit_server);
+ Assert.equal(
+ aRequest.QueryInterface(Ci.nsIHttpChannel).responseStatus,
+ 304
+ );
+ Assert.ok(!cache_storage.exists(make_uri(resource_url), ""));
+ Assert.equal(aRequest.getResponseHeader("Returned-From-Handler"), "1");
+
+ run_next_test();
+ }, true);
+ }, null)
+ );
+});
+
+// 2. send custom conditional request when we don't have an entry
+// server returns 200 -> result is cached
+add_test(() => {
+ response_code = "200";
+ response_body = "content_body";
+ request_time = "Thu, 1 Jan 2009 00:00:00 GMT";
+ response_time = "Fri, 2 Jan 2009 00:00:00 GMT";
+
+ var ch = make_channel(resource_url);
+ ch.asyncOpen(
+ new ChannelListener(function(aRequest, aData) {
+ syncWithCacheIOThread(() => {
+ Assert.ok(hit_server);
+ Assert.equal(
+ aRequest.QueryInterface(Ci.nsIHttpChannel).responseStatus,
+ 200
+ );
+ Assert.ok(cache_storage.exists(make_uri(resource_url), ""));
+
+ check_cached_data(response_body, run_next_test);
+ }, true);
+ }, null)
+ );
+});
+
+// 3. send custom conditional request when we have an entry
+// server returns 304 -> client receives 304 and cached entry is unchanged
+add_test(() => {
+ response_code = "304";
+ var cached_body = response_body;
+ response_body = "";
+ request_time = "Fri, 2 Jan 2009 00:00:00 GMT";
+ response_time = "Fri, 2 Jan 2009 00:00:00 GMT";
+
+ var ch = make_channel(resource_url);
+ ch.asyncOpen(
+ new ChannelListener(function(aRequest, aData) {
+ syncWithCacheIOThread(() => {
+ Assert.ok(hit_server);
+ Assert.equal(
+ aRequest.QueryInterface(Ci.nsIHttpChannel).responseStatus,
+ 304
+ );
+ Assert.ok(cache_storage.exists(make_uri(resource_url), ""));
+ Assert.equal(aRequest.getResponseHeader("Returned-From-Handler"), "1");
+ Assert.equal(aData, "");
+
+ // Check the cache data is not changed
+ check_cached_data(cached_body, run_next_test);
+ }, true);
+ }, null)
+ );
+});
+
+// 4. send custom conditional request when we have an entry
+// server returns 200 -> result is cached
+add_test(() => {
+ response_code = "200";
+ response_body = "updated_content_body";
+ request_time = "Fri, 2 Jan 2009 00:00:00 GMT";
+ response_time = "Sat, 3 Jan 2009 00:00:00 GMT";
+ var ch = make_channel(resource_url);
+ ch.asyncOpen(
+ new ChannelListener(function(aRequest, aData) {
+ syncWithCacheIOThread(() => {
+ Assert.ok(hit_server);
+ Assert.equal(
+ aRequest.QueryInterface(Ci.nsIHttpChannel).responseStatus,
+ 200
+ );
+ Assert.ok(cache_storage.exists(make_uri(resource_url), ""));
+
+ // Check the cache data is updated
+ check_cached_data(response_body, () => {
+ run_next_test();
+ httpserver.stop(do_test_finished);
+ });
+ }, true);
+ }, null)
+ );
+});
diff --git a/netwerk/test/unit/test_bug484684.js b/netwerk/test/unit/test_bug484684.js
new file mode 100644
index 0000000000..49a7f79205
--- /dev/null
+++ b/netwerk/test/unit/test_bug484684.js
@@ -0,0 +1,139 @@
+"use strict";
+
+const URL = "ftp://localhost/bug464884/";
+
+const tests = [
+ // standard ls unix format
+ [
+ "-rw-rw-r-- 1 500 500 0 Jan 01 2000 file1\r\n" +
+ "-rw-rw-r-- 1 500 500 0 Jan 01 2000 file2\r\n",
+
+ "300: " +
+ URL +
+ "\n" +
+ "200: filename content-length last-modified file-type\n" +
+ '201: "file1" 0 Sat%2C%2001%20Jan%202000%2000%3A00%3A00%20GMT FILE \n' +
+ '201: "%20file2" 0 Sat%2C%2001%20Jan%202000%2000%3A00%3A00%20GMT FILE \n',
+ ],
+ // old Hellsoft unix format
+ [
+ "-[RWCEMFA] supervisor 214059 Jan 01 2000 file1\r\n" +
+ "-[RWCEMFA] supervisor 214059 Jan 01 2000 file2\r\n",
+
+ "300: " +
+ URL +
+ "\n" +
+ "200: filename content-length last-modified file-type\n" +
+ '201: "file1" 214059 Sat%2C%2001%20Jan%202000%2000%3A00%3A00%20GMT FILE \n' +
+ '201: "file2" 214059 Sat%2C%2001%20Jan%202000%2000%3A00%3A00%20GMT FILE \n',
+ ],
+ // new Hellsoft unix format
+ [
+ "- [RWCEAFMS] jrd 192 Jan 01 2000 file1\r\n" +
+ "- [RWCEAFMS] jrd 192 Jan 01 2000 file2\r\n",
+
+ "300: " +
+ URL +
+ "\n" +
+ "200: filename content-length last-modified file-type\n" +
+ '201: "file1" 192 Sat%2C%2001%20Jan%202000%2000%3A00%3A00%20GMT FILE \n' +
+ '201: "%20file2" 192 Sat%2C%2001%20Jan%202000%2000%3A00%3A00%20GMT FILE \n',
+ ],
+ // DOS format with correct offsets
+ [
+ "01-01-00 01:00AM <DIR> dir1\r\n" +
+ "01-01-00 01:00AM <JUNCTION> junction1 -> foo1\r\n" +
+ "01-01-00 01:00AM 95077 file1\r\n" +
+ "01-01-00 01:00AM <DIR> dir2\r\n" +
+ "01-01-00 01:00AM <JUNCTION> junction2 -> foo2\r\n" +
+ "01-01-00 01:00AM 95077 file2\r\n",
+
+ "300: " +
+ URL +
+ "\n" +
+ "200: filename content-length last-modified file-type\n" +
+ '201: "dir1" 0 Sat%2C%2001%20Jan%202000%2001%3A00%3A00%20GMT DIRECTORY \n' +
+ '201: "junction1" Sat%2C%2001%20Jan%202000%2001%3A00%3A00%20GMT SYMBOLIC-LINK \n' +
+ '201: "file1" 95077 Sat%2C%2001%20Jan%202000%2001%3A00%3A00%20GMT FILE \n' +
+ '201: "%20dir2" 0 Sat%2C%2001%20Jan%202000%2001%3A00%3A00%20GMT DIRECTORY \n' +
+ '201: "%20junction2" Sat%2C%2001%20Jan%202000%2001%3A00%3A00%20GMT SYMBOLIC-LINK \n' +
+ '201: "%20file2" 95077 Sat%2C%2001%20Jan%202000%2001%3A00%3A00%20GMT FILE \n',
+ ],
+ // DOS format with wrong offsets
+ [
+ "01-01-00 01:00AM <DIR> dir1\r\n" +
+ "01-01-00 01:00AM <DIR> dir2\r\n" +
+ "01-01-00 01:00AM <DIR> dir3\r\n" +
+ "01-01-00 01:00AM <JUNCTION> junction1 -> foo1\r\n" +
+ "01-01-00 01:00AM <JUNCTION> junction2 -> foo2\r\n" +
+ "01-01-00 01:00AM <JUNCTION> junction3 -> foo3\r\n" +
+ "01-01-00 01:00AM 95077 file1\r\n" +
+ "01-01-00 01:00AM 95077 file2\r\n",
+
+ "300: " +
+ URL +
+ "\n" +
+ "200: filename content-length last-modified file-type\n" +
+ '201: "dir1" 0 Sat%2C%2001%20Jan%202000%2001%3A00%3A00%20GMT DIRECTORY \n' +
+ '201: "dir2" 0 Sat%2C%2001%20Jan%202000%2001%3A00%3A00%20GMT DIRECTORY \n' +
+ '201: "dir3" 0 Sat%2C%2001%20Jan%202000%2001%3A00%3A00%20GMT DIRECTORY \n' +
+ '201: "junction1" Sat%2C%2001%20Jan%202000%2001%3A00%3A00%20GMT SYMBOLIC-LINK \n' +
+ '201: "junction2" Sat%2C%2001%20Jan%202000%2001%3A00%3A00%20GMT SYMBOLIC-LINK \n' +
+ '201: "junction3" Sat%2C%2001%20Jan%202000%2001%3A00%3A00%20GMT SYMBOLIC-LINK \n' +
+ '201: "file1" 95077 Sat%2C%2001%20Jan%202000%2001%3A00%3A00%20GMT FILE \n' +
+ '201: "file2" 95077 Sat%2C%2001%20Jan%202000%2001%3A00%3A00%20GMT FILE \n',
+ ],
+];
+
+function checkData(request, data, ctx) {
+ Assert.equal(tests[0][1], data);
+ tests.shift();
+ executeSoon(next_test);
+}
+
+function storeData(status, entry) {
+ var scs = Cc["@mozilla.org/streamConverters;1"].getService(
+ Ci.nsIStreamConverterService
+ );
+ var converter = scs.asyncConvertData(
+ "text/ftp-dir",
+ "application/http-index-format",
+ new ChannelListener(checkData, null, CL_ALLOW_UNKNOWN_CL),
+ null
+ );
+
+ var stream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(
+ Ci.nsIStringInputStream
+ );
+ stream.data = tests[0][0];
+
+ var url = NetUtil.newURI(URL);
+
+ var channel = {
+ URI: url,
+ contentLength: -1,
+ pending: true,
+ isPending() {
+ return this.pending;
+ },
+ QueryInterface: ChromeUtils.generateQI(["nsIChannel"]),
+ };
+
+ converter.onStartRequest(channel, null);
+ converter.onDataAvailable(channel, stream, 0, 0);
+ channel.pending = false;
+ converter.onStopRequest(channel, null, Cr.NS_OK);
+}
+
+function next_test() {
+ if (tests.length == 0) {
+ do_test_finished();
+ } else {
+ storeData();
+ }
+}
+
+function run_test() {
+ executeSoon(next_test);
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_bug490095.js b/netwerk/test/unit/test_bug490095.js
new file mode 100644
index 0000000000..7e8de69af1
--- /dev/null
+++ b/netwerk/test/unit/test_bug490095.js
@@ -0,0 +1,158 @@
+//
+// Verify that the VALIDATE_NEVER and LOAD_FROM_CACHE flags override
+// heuristic query freshness as defined in RFC 2616 section 13.9
+//
+
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+var httpserver = new HttpServer();
+var index = 0;
+var tests = [
+ { url: "/freshness?a", server: "0", expected: "0" },
+ { url: "/freshness?a", server: "1", expected: "1" },
+
+ // Setting the VALIDATE_NEVER flag should grab entry from cache
+ {
+ url: "/freshness?a",
+ server: "2",
+ expected: "1",
+ flags: Ci.nsIRequest.VALIDATE_NEVER,
+ },
+
+ // Finally, check that request is validated with no flags set
+ { url: "/freshness?a", server: "99", expected: "99" },
+
+ { url: "/freshness?b", server: "0", expected: "0" },
+ { url: "/freshness?b", server: "1", expected: "1" },
+
+ // Setting the LOAD_FROM_CACHE flag also grab the entry from cache
+ {
+ url: "/freshness?b",
+ server: "2",
+ expected: "1",
+ flags: Ci.nsIRequest.LOAD_FROM_CACHE,
+ },
+
+ // Finally, check that request is validated with no flags set
+ { url: "/freshness?b", server: "99", expected: "99" },
+];
+
+function logit(i, data) {
+ dump(
+ tests[i].url +
+ "\t requested [" +
+ tests[i].server +
+ "]" +
+ " got [" +
+ data +
+ "] expected [" +
+ tests[i].expected +
+ "]"
+ );
+ if (tests[i].responseheader) {
+ dump("\t[" + tests[i].responseheader + "]");
+ }
+ dump("\n");
+}
+
+function setupChannel(suffix, value) {
+ var chan = NetUtil.newChannel({
+ uri: "http://localhost:" + httpserver.identity.primaryPort + suffix,
+ loadUsingSystemPrincipal: true,
+ });
+ var httpChan = chan.QueryInterface(Ci.nsIHttpChannel);
+ httpChan.requestMethod = "GET";
+ httpChan.setRequestHeader("x-request", value, false);
+ return httpChan;
+}
+
+function triggerNextTest() {
+ var test = tests[index];
+ var channel = setupChannel(test.url, test.server);
+ if (test.flags) {
+ channel.loadFlags = test.flags;
+ }
+ channel.asyncOpen(new ChannelListener(checkValueAndTrigger, null));
+}
+
+function checkValueAndTrigger(request, data, ctx) {
+ logit(index, data);
+ Assert.equal(tests[index].expected, data);
+
+ if (index < tests.length - 1) {
+ index++;
+ // this call happens in onStopRequest from the channel, and opening a
+ // new channel to the same url here is no good idea... post it instead
+ do_timeout(1, triggerNextTest);
+ } else {
+ httpserver.stop(do_test_finished);
+ }
+}
+
+function run_test() {
+ httpserver.registerPathHandler("/freshness", handler);
+ httpserver.start(-1);
+
+ // clear cache
+ evict_cache_entries();
+
+ triggerNextTest();
+
+ do_test_pending();
+}
+
+function handler(metadata, response) {
+ var body = metadata.getHeader("x-request");
+ response.setHeader("Content-Type", "text/plain", false);
+ response.setHeader("Date", getDateString(0), false);
+ response.setHeader("Cache-Control", "max-age=0", false);
+
+ var header = tests[index].responseheader;
+ if (header == null) {
+ response.setHeader("Last-Modified", getDateString(-1), false);
+ } else {
+ var splitHdr = header.split(": ");
+ response.setHeader(splitHdr[0], splitHdr[1], false);
+ }
+
+ response.setStatusLine(metadata.httpVersion, 200, "OK");
+ response.bodyOutputStream.write(body, body.length);
+}
+
+function getDateString(yearDelta) {
+ var months = [
+ "Jan",
+ "Feb",
+ "Mar",
+ "Apr",
+ "May",
+ "Jun",
+ "Jul",
+ "Aug",
+ "Sep",
+ "Oct",
+ "Nov",
+ "Dec",
+ ];
+ var days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
+
+ var d = new Date();
+ return (
+ days[d.getUTCDay()] +
+ ", " +
+ d.getUTCDate() +
+ " " +
+ months[d.getUTCMonth()] +
+ " " +
+ (d.getUTCFullYear() + yearDelta) +
+ " " +
+ d.getUTCHours() +
+ ":" +
+ d.getUTCMinutes() +
+ ":" +
+ d.getUTCSeconds() +
+ " UTC"
+ );
+}
diff --git a/netwerk/test/unit/test_bug504014.js b/netwerk/test/unit/test_bug504014.js
new file mode 100644
index 0000000000..e4fe449648
--- /dev/null
+++ b/netwerk/test/unit/test_bug504014.js
@@ -0,0 +1,74 @@
+"use strict";
+
+var valid_URIs = [
+ "http://[::]/",
+ "http://[::1]/",
+ "http://[1::]/",
+ "http://[::]/",
+ "http://[::1]/",
+ "http://[1::]/",
+ "http://[1:2:3:4:5:6:7::]/",
+ "http://[::1:2:3:4:5:6:7]/",
+ "http://[1:2:a:B:c:D:e:F]/",
+ "http://[1::8]/",
+ "http://[1:2::8]/",
+ "http://[0000:0123:4567:89AB:CDEF:abcd:ef00:0000]/",
+ "http://[::192.168.1.1]/",
+ "http://[1::0.0.0.0]/",
+ "http://[1:2::255.255.255.255]/",
+ "http://[1:2:3::255.255.255.255]/",
+ "http://[1:2:3:4::255.255.255.255]/",
+ "http://[1:2:3:4:5::255.255.255.255]/",
+ "http://[1:2:3:4:5:6:255.255.255.255]/",
+];
+
+var invalid_URIs = [
+ "http://[1]/",
+ "http://[192.168.1.1]/",
+ "http://[:::]/",
+ "http://[:::1]/",
+ "http://[1:::]/",
+ "http://[::1::]/",
+ "http://[1:2:3:4:5:6:7:]/",
+ "http://[:2:3:4:5:6:7:8]/",
+ "http://[1:2:3:4:5:6:7:8:]/",
+ "http://[:1:2:3:4:5:6:7:8]/",
+ "http://[1:2:3:4:5:6:7:8::]/",
+ "http://[::1:2:3:4:5:6:7:8]/",
+ "http://[1:2:3:4:5:6:7]/",
+ "http://[1:2:3:4:5:6:7:8:9]/",
+ "http://[00001:2:3:4:5:6:7:8]/",
+ "http://[0001:2:3:4:5:6:7:89abc]/",
+ "http://[A:b:C:d:E:f:G:h]/",
+ "http://[::192.168.1]/",
+ "http://[::192.168.1.]/",
+ "http://[::.168.1.1]/",
+ "http://[::192..1.1]/",
+ "http://[::0192.168.1.1]/",
+ "http://[::256.255.255.255]/",
+ "http://[::1x.255.255.255]/",
+ "http://[::192.4294967464.1.1]/",
+ "http://[1:2:3:4:5:6::255.255.255.255]/",
+ "http://[1:2:3:4:5:6:7:255.255.255.255]/",
+];
+
+function run_test() {
+ var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
+
+ for (var i = 0; i < valid_URIs.length; i++) {
+ try {
+ ios.newURI(valid_URIs[i]);
+ } catch (e) {
+ do_throw("cannot create URI:" + valid_URIs[i]);
+ }
+ }
+
+ for (var i = 0; i < invalid_URIs.length; i++) {
+ try {
+ ios.newURI(invalid_URIs[i]);
+ do_throw("should throw: " + invalid_URIs[i]);
+ } catch (e) {
+ Assert.equal(e.result, Cr.NS_ERROR_MALFORMED_URI);
+ }
+ }
+}
diff --git a/netwerk/test/unit/test_bug510359.js b/netwerk/test/unit/test_bug510359.js
new file mode 100644
index 0000000000..321da39912
--- /dev/null
+++ b/netwerk/test/unit/test_bug510359.js
@@ -0,0 +1,102 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+var httpserver = new HttpServer();
+var index = 0;
+var tests = [
+ { url: "/bug510359", server: "0", expected: "0" },
+ { url: "/bug510359", server: "1", expected: "1" },
+];
+
+function setupChannel(suffix, value) {
+ var chan = NetUtil.newChannel({
+ uri: "http://localhost:" + httpserver.identity.primaryPort + suffix,
+ loadUsingSystemPrincipal: true,
+ });
+ var httpChan = chan.QueryInterface(Ci.nsIHttpChannel);
+ httpChan.requestMethod = "GET";
+ httpChan.setRequestHeader("x-request", value, false);
+ httpChan.setRequestHeader("Cookie", "c=" + value, false);
+ return httpChan;
+}
+
+function triggerNextTest() {
+ var channel = setupChannel(tests[index].url, tests[index].server);
+ channel.asyncOpen(new ChannelListener(checkValueAndTrigger, null));
+}
+
+function checkValueAndTrigger(request, data, ctx) {
+ Assert.equal(tests[index].expected, data);
+
+ if (index < tests.length - 1) {
+ index++;
+ triggerNextTest();
+ } else {
+ httpserver.stop(do_test_finished);
+ }
+}
+
+function run_test() {
+ httpserver.registerPathHandler("/bug510359", handler);
+ httpserver.start(-1);
+
+ // clear cache
+ evict_cache_entries();
+
+ triggerNextTest();
+
+ do_test_pending();
+}
+
+function handler(metadata, response) {
+ try {
+ metadata.getHeader("If-Modified-Since");
+ response.setStatusLine(metadata.httpVersion, 500, "Failed");
+ var msg = "Client should not set If-Modified-Since header";
+ response.bodyOutputStream.write(msg, msg.length);
+ } catch (ex) {
+ response.setStatusLine(metadata.httpVersion, 200, "Ok");
+ response.setHeader("Content-Type", "text/plain", false);
+ response.setHeader("Last-Modified", getDateString(-1), false);
+ response.setHeader("Vary", "Cookie", false);
+ var body = metadata.getHeader("x-request");
+ response.bodyOutputStream.write(body, body.length);
+ }
+}
+
+function getDateString(yearDelta) {
+ var months = [
+ "Jan",
+ "Feb",
+ "Mar",
+ "Apr",
+ "May",
+ "Jun",
+ "Jul",
+ "Aug",
+ "Sep",
+ "Oct",
+ "Nov",
+ "Dec",
+ ];
+ var days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
+
+ var d = new Date();
+ return (
+ days[d.getUTCDay()] +
+ ", " +
+ d.getUTCDate() +
+ " " +
+ months[d.getUTCMonth()] +
+ " " +
+ (d.getUTCFullYear() + yearDelta) +
+ " " +
+ d.getUTCHours() +
+ ":" +
+ d.getUTCMinutes() +
+ ":" +
+ d.getUTCSeconds() +
+ " UTC"
+ );
+}
diff --git a/netwerk/test/unit/test_bug515583.js b/netwerk/test/unit/test_bug515583.js
new file mode 100644
index 0000000000..fe2bad5399
--- /dev/null
+++ b/netwerk/test/unit/test_bug515583.js
@@ -0,0 +1,82 @@
+"use strict";
+
+const URL = "ftp://localhost/bug515583/";
+
+const tests = [
+ [
+ "[RWCEM1 4 1-MAR-1993 18:09:01.12\r\n" +
+ "[RWCEM1] 4 2-MAR-1993 18:09:01.12\r\n" +
+ "[RWCEM1]A 4 3-MAR-1993 18:09:01.12\r\n" +
+ "[RWCEM1]B; 4 4-MAR-1993 18:09:01.12\r\n" +
+ "[RWCEM1];1 4 5-MAR-1993 18:09:01.12\r\n" +
+ "[RWCEM1]; 4 6-MAR-1993 18:09:01.12\r\n" +
+ "[RWCEM1]C;1D 4 7-MAR-1993 18:09:01.12\r\n" +
+ "[RWCEM1]E;1 4 8-MAR-1993 18:09:01.12\r\n",
+ "300: " +
+ URL +
+ "\n" +
+ "200: filename content-length last-modified file-type\n" +
+ '201: "A" 2048 Wed%2C%2003%20Mar%201993%2018%3A09%3A01%20GMT FILE \n' +
+ '201: "E" 2048 Mon%2C%2008%20Mar%201993%2018%3A09%3A01%20GMT FILE \n',
+ ],
+ [
+ "\r\r\r\n",
+ "300: " +
+ URL +
+ "\n" +
+ "200: filename content-length last-modified file-type\n",
+ ],
+];
+
+function checkData(request, data, ctx) {
+ Assert.equal(tests[0][1], data);
+ tests.shift();
+ executeSoon(next_test);
+}
+
+function storeData(status, entry) {
+ var scs = Cc["@mozilla.org/streamConverters;1"].getService(
+ Ci.nsIStreamConverterService
+ );
+ var converter = scs.asyncConvertData(
+ "text/ftp-dir",
+ "application/http-index-format",
+ new ChannelListener(checkData, null, CL_ALLOW_UNKNOWN_CL),
+ null
+ );
+
+ var stream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(
+ Ci.nsIStringInputStream
+ );
+ stream.data = tests[0][0];
+
+ var url = NetUtil.newURI(URL);
+
+ var channel = {
+ URI: url,
+ contentLength: -1,
+ pending: true,
+ isPending() {
+ return this.pending;
+ },
+ QueryInterface: ChromeUtils.generateQI(["nsIChannel"]),
+ };
+
+ converter.onStartRequest(channel, null);
+ converter.onDataAvailable(channel, stream, 0, 0);
+ channel.pending = false;
+ converter.onStopRequest(channel, null, Cr.NS_OK);
+}
+
+function next_test() {
+ if (tests.length == 0) {
+ do_test_finished();
+ } else {
+ storeData();
+ }
+}
+
+function run_test() {
+ executeSoon(next_test);
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_bug526789.js b/netwerk/test/unit/test_bug526789.js
new file mode 100644
index 0000000000..07ce2d18a3
--- /dev/null
+++ b/netwerk/test/unit/test_bug526789.js
@@ -0,0 +1,287 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async () => {
+ var cm = Cc["@mozilla.org/cookiemanager;1"].getService(Ci.nsICookieManager);
+ var expiry = (Date.now() + 1000) * 1000;
+
+ cm.removeAll();
+
+ // Allow all cookies.
+ Services.prefs.setIntPref("network.cookie.cookieBehavior", 0);
+
+ // test that variants of 'baz.com' get normalized appropriately, but that
+ // malformed hosts are rejected
+ cm.add(
+ "baz.com",
+ "/",
+ "foo",
+ "bar",
+ false,
+ false,
+ true,
+ expiry,
+ {},
+ Ci.nsICookie.SAMESITE_NONE,
+ Ci.nsICookie.SCHEME_HTTPS
+ );
+ Assert.equal(cm.countCookiesFromHost("baz.com"), 1);
+ Assert.equal(cm.countCookiesFromHost("BAZ.com"), 1);
+ Assert.equal(cm.countCookiesFromHost(".baz.com"), 1);
+ Assert.equal(cm.countCookiesFromHost("baz.com."), 0);
+ Assert.equal(cm.countCookiesFromHost(".baz.com."), 0);
+ do_check_throws(function() {
+ cm.countCookiesFromHost("baz.com..");
+ }, Cr.NS_ERROR_ILLEGAL_VALUE);
+ do_check_throws(function() {
+ cm.countCookiesFromHost("baz..com");
+ }, Cr.NS_ERROR_ILLEGAL_VALUE);
+ do_check_throws(function() {
+ cm.countCookiesFromHost("..baz.com");
+ }, Cr.NS_ERROR_ILLEGAL_VALUE);
+ cm.remove("BAZ.com.", "foo", "/", {});
+ Assert.equal(cm.countCookiesFromHost("baz.com"), 1);
+ cm.remove("baz.com", "foo", "/", {});
+ Assert.equal(cm.countCookiesFromHost("baz.com"), 0);
+
+ // Test that 'baz.com' and 'baz.com.' are treated differently
+ cm.add(
+ "baz.com.",
+ "/",
+ "foo",
+ "bar",
+ false,
+ false,
+ true,
+ expiry,
+ {},
+ Ci.nsICookie.SAMESITE_NONE,
+ Ci.nsICookie.SCHEME_HTTPS
+ );
+ Assert.equal(cm.countCookiesFromHost("baz.com"), 0);
+ Assert.equal(cm.countCookiesFromHost("BAZ.com"), 0);
+ Assert.equal(cm.countCookiesFromHost(".baz.com"), 0);
+ Assert.equal(cm.countCookiesFromHost("baz.com."), 1);
+ Assert.equal(cm.countCookiesFromHost(".baz.com."), 1);
+ cm.remove("baz.com", "foo", "/", {});
+ Assert.equal(cm.countCookiesFromHost("baz.com."), 1);
+ cm.remove("baz.com.", "foo", "/", {});
+ Assert.equal(cm.countCookiesFromHost("baz.com."), 0);
+
+ // test that domain cookies are illegal for IP addresses, aliases such as
+ // 'localhost', and eTLD's such as 'co.uk'
+ cm.add(
+ "192.168.0.1",
+ "/",
+ "foo",
+ "bar",
+ false,
+ false,
+ true,
+ expiry,
+ {},
+ Ci.nsICookie.SAMESITE_NONE,
+ Ci.nsICookie.SCHEME_HTTPS
+ );
+ Assert.equal(cm.countCookiesFromHost("192.168.0.1"), 1);
+ Assert.equal(cm.countCookiesFromHost("192.168.0.1."), 0);
+ do_check_throws(function() {
+ cm.countCookiesFromHost(".192.168.0.1");
+ }, Cr.NS_ERROR_ILLEGAL_VALUE);
+ do_check_throws(function() {
+ cm.countCookiesFromHost(".192.168.0.1.");
+ }, Cr.NS_ERROR_ILLEGAL_VALUE);
+
+ cm.add(
+ "localhost",
+ "/",
+ "foo",
+ "bar",
+ false,
+ false,
+ true,
+ expiry,
+ {},
+ Ci.nsICookie.SAMESITE_NONE,
+ Ci.nsICookie.SCHEME_HTTPS
+ );
+ Assert.equal(cm.countCookiesFromHost("localhost"), 1);
+ Assert.equal(cm.countCookiesFromHost("localhost."), 0);
+ do_check_throws(function() {
+ cm.countCookiesFromHost(".localhost");
+ }, Cr.NS_ERROR_ILLEGAL_VALUE);
+ do_check_throws(function() {
+ cm.countCookiesFromHost(".localhost.");
+ }, Cr.NS_ERROR_ILLEGAL_VALUE);
+
+ cm.add(
+ "co.uk",
+ "/",
+ "foo",
+ "bar",
+ false,
+ false,
+ true,
+ expiry,
+ {},
+ Ci.nsICookie.SAMESITE_NONE,
+ Ci.nsICookie.SCHEME_HTTPS
+ );
+ Assert.equal(cm.countCookiesFromHost("co.uk"), 1);
+ Assert.equal(cm.countCookiesFromHost("co.uk."), 0);
+ do_check_throws(function() {
+ cm.countCookiesFromHost(".co.uk");
+ }, Cr.NS_ERROR_ILLEGAL_VALUE);
+ do_check_throws(function() {
+ cm.countCookiesFromHost(".co.uk.");
+ }, Cr.NS_ERROR_ILLEGAL_VALUE);
+
+ cm.removeAll();
+
+ CookieXPCShellUtils.createServer({
+ hosts: ["baz.com", "192.168.0.1", "localhost", "co.uk", "foo.com"],
+ });
+
+ var uri = NetUtil.newURI("http://baz.com/");
+ Services.scriptSecurityManager.createContentPrincipal(uri, {});
+
+ Assert.equal(uri.asciiHost, "baz.com");
+
+ await CookieXPCShellUtils.setCookieToDocument(uri.spec, "foo=bar");
+ const docCookies = await CookieXPCShellUtils.getCookieStringFromDocument(
+ uri.spec
+ );
+ Assert.equal(docCookies, "foo=bar");
+
+ Assert.equal(cm.countCookiesFromHost(""), 0);
+ do_check_throws(function() {
+ cm.countCookiesFromHost(".");
+ }, Cr.NS_ERROR_ILLEGAL_VALUE);
+ do_check_throws(function() {
+ cm.countCookiesFromHost("..");
+ }, Cr.NS_ERROR_ILLEGAL_VALUE);
+
+ var cookies = cm.getCookiesFromHost("", {});
+ Assert.ok(!cookies.length);
+ do_check_throws(function() {
+ cm.getCookiesFromHost(".", {});
+ }, Cr.NS_ERROR_ILLEGAL_VALUE);
+ do_check_throws(function() {
+ cm.getCookiesFromHost("..", {});
+ }, Cr.NS_ERROR_ILLEGAL_VALUE);
+
+ cookies = cm.getCookiesFromHost("baz.com", {});
+ Assert.equal(cookies.length, 1);
+ Assert.equal(cookies[0].name, "foo");
+ cookies = cm.getCookiesFromHost("", {});
+ Assert.ok(!cookies.length);
+ do_check_throws(function() {
+ cm.getCookiesFromHost(".", {});
+ }, Cr.NS_ERROR_ILLEGAL_VALUE);
+ do_check_throws(function() {
+ cm.getCookiesFromHost("..", {});
+ }, Cr.NS_ERROR_ILLEGAL_VALUE);
+
+ cm.removeAll();
+
+ // test that an empty host to add() or remove() works,
+ // but a host of '.' doesn't
+ cm.add(
+ "",
+ "/",
+ "foo2",
+ "bar",
+ false,
+ false,
+ true,
+ expiry,
+ {},
+ Ci.nsICookie.SAMESITE_NONE,
+ Ci.nsICookie.SCHEME_HTTPS
+ );
+ Assert.equal(getCookieCount(), 1);
+ do_check_throws(function() {
+ cm.add(
+ ".",
+ "/",
+ "foo3",
+ "bar",
+ false,
+ false,
+ true,
+ expiry,
+ {},
+ Ci.nsICookie.SAMESITE_NONE,
+ Ci.nsICookie.SCHEME_HTTPS
+ );
+ }, Cr.NS_ERROR_ILLEGAL_VALUE);
+ Assert.equal(getCookieCount(), 1);
+
+ cm.remove("", "foo2", "/", {});
+ Assert.equal(getCookieCount(), 0);
+ do_check_throws(function() {
+ cm.remove(".", "foo3", "/", {});
+ }, Cr.NS_ERROR_ILLEGAL_VALUE);
+
+ // test that the 'domain' attribute accepts a leading dot for IP addresses,
+ // aliases such as 'localhost', and eTLD's such as 'co.uk'; but that the
+ // resulting cookie is for the exact host only.
+ await testDomainCookie("http://192.168.0.1/", "192.168.0.1");
+ await testDomainCookie("http://localhost/", "localhost");
+ await testDomainCookie("http://co.uk/", "co.uk");
+
+ // Test that trailing dots are treated differently for purposes of the
+ // 'domain' attribute when using setCookieStringFromDocument.
+ await testTrailingDotCookie("http://localhost/", "localhost");
+ await testTrailingDotCookie("http://foo.com/", "foo.com");
+
+ cm.removeAll();
+});
+
+function getCookieCount() {
+ var cm = Cc["@mozilla.org/cookiemanager;1"].getService(Ci.nsICookieManager);
+ return cm.cookies.length;
+}
+
+async function testDomainCookie(uriString, domain) {
+ var cm = Cc["@mozilla.org/cookiemanager;1"].getService(Ci.nsICookieManager);
+
+ cm.removeAll();
+
+ await CookieXPCShellUtils.setCookieToDocument(
+ uriString,
+ "foo=bar; domain=" + domain
+ );
+
+ var cookies = cm.getCookiesFromHost(domain, {});
+ Assert.ok(cookies.length);
+ Assert.equal(cookies[0].host, domain);
+ cm.removeAll();
+
+ await CookieXPCShellUtils.setCookieToDocument(
+ uriString,
+ "foo=bar; domain=." + domain
+ );
+
+ cookies = cm.getCookiesFromHost(domain, {});
+ Assert.ok(cookies.length);
+ Assert.equal(cookies[0].host, domain);
+ cm.removeAll();
+}
+
+async function testTrailingDotCookie(uriString, domain) {
+ var cm = Cc["@mozilla.org/cookiemanager;1"].getService(Ci.nsICookieManager);
+
+ cm.removeAll();
+
+ await CookieXPCShellUtils.setCookieToDocument(
+ uriString,
+ "foo=bar; domain=" + domain + "/"
+ );
+
+ Assert.equal(cm.countCookiesFromHost(domain), 0);
+ Assert.equal(cm.countCookiesFromHost(domain + "."), 0);
+ cm.removeAll();
+}
diff --git a/netwerk/test/unit/test_bug528292.js b/netwerk/test/unit/test_bug528292.js
new file mode 100644
index 0000000000..dada3ef6cf
--- /dev/null
+++ b/netwerk/test/unit/test_bug528292.js
@@ -0,0 +1,93 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+const sentCookieVal = "foo=bar";
+const responseBody = "response body";
+
+XPCOMUtils.defineLazyGetter(this, "baseURL", function() {
+ return "http://localhost:" + httpServer.identity.primaryPort;
+});
+
+const preRedirectPath = "/528292/pre-redirect";
+
+XPCOMUtils.defineLazyGetter(this, "preRedirectURL", function() {
+ return baseURL + preRedirectPath;
+});
+
+const postRedirectPath = "/528292/post-redirect";
+
+XPCOMUtils.defineLazyGetter(this, "postRedirectURL", function() {
+ return baseURL + postRedirectPath;
+});
+
+var httpServer = null;
+var receivedCookieVal = null;
+
+function preRedirectHandler(metadata, response) {
+ response.setStatusLine(metadata.httpVersion, 302, "Found");
+ response.setHeader("Location", postRedirectURL, false);
+}
+
+function postRedirectHandler(metadata, response) {
+ receivedCookieVal = metadata.getHeader("Cookie");
+ response.setHeader("Content-Type", "text/plain");
+ response.bodyOutputStream.write(responseBody, responseBody.length);
+}
+
+function inChildProcess() {
+ return (
+ Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime)
+ .processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT
+ );
+}
+
+add_task(async () => {
+ // Start the HTTP server.
+ httpServer = new HttpServer();
+ httpServer.registerPathHandler(preRedirectPath, preRedirectHandler);
+ httpServer.registerPathHandler(postRedirectPath, postRedirectHandler);
+ httpServer.start(-1);
+
+ if (!inChildProcess()) {
+ // Disable third-party cookies in general.
+ Cc["@mozilla.org/preferences-service;1"]
+ .getService(Ci.nsIPrefBranch)
+ .setIntPref("network.cookie.cookieBehavior", 1);
+ Cc["@mozilla.org/preferences-service;1"]
+ .getService(Ci.nsIPrefBranch)
+ .setBoolPref("network.cookieJarSettings.unblocked_for_testing", true);
+ }
+
+ var ioService = Cc["@mozilla.org/network/io-service;1"].getService(
+ Ci.nsIIOService
+ );
+
+ // Set up a channel with forceAllowThirdPartyCookie set to true. We'll use
+ // the channel both to set a cookie and then to load the pre-redirect URI.
+ var chan = NetUtil.newChannel({
+ uri: preRedirectURL,
+ loadUsingSystemPrincipal: true,
+ })
+ .QueryInterface(Ci.nsIHttpChannel)
+ .QueryInterface(Ci.nsIHttpChannelInternal);
+ chan.forceAllowThirdPartyCookie = true;
+
+ // Set a cookie on one of the URIs. It doesn't matter which one, since
+ // they're both from the same host, which is enough for the cookie service
+ // to send the cookie with both requests.
+ var postRedirectURI = ioService.newURI(postRedirectURL);
+
+ await CookieXPCShellUtils.setCookieToDocument(
+ postRedirectURI.spec,
+ sentCookieVal
+ );
+
+ // Load the pre-redirect URI.
+ await new Promise(resolve => {
+ chan.asyncOpen(new ChannelListener(resolve, null));
+ });
+
+ Assert.equal(receivedCookieVal, sentCookieVal);
+ httpServer.stop(do_test_finished);
+});
diff --git a/netwerk/test/unit/test_bug536324_64bit_content_length.js b/netwerk/test/unit/test_bug536324_64bit_content_length.js
new file mode 100644
index 0000000000..9f60d7ac00
--- /dev/null
+++ b/netwerk/test/unit/test_bug536324_64bit_content_length.js
@@ -0,0 +1,64 @@
+/* Test to ensure our 64-bit content length implementation works, at least for
+ a simple HTTP case */
+
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+// This C-L is significantly larger than (U)INT32_MAX, to make sure we do
+// 64-bit properly.
+const CONTENT_LENGTH = "1152921504606846975";
+
+var httpServer = null;
+
+var listener = {
+ onStartRequest(req) {},
+
+ onDataAvailable(req, stream, off, count) {
+ Assert.equal(req.getResponseHeader("Content-Length"), CONTENT_LENGTH);
+
+ // We're done here, cancel the channel
+ req.cancel(Cr.NS_BINDING_ABORTED);
+ },
+
+ onStopRequest(req, stat) {
+ httpServer.stop(do_test_finished);
+ },
+};
+
+function hugeContentLength(metadata, response) {
+ var text = "abcdefghijklmnopqrstuvwxyz";
+ var bytes_written = 0;
+
+ response.seizePower();
+
+ response.write("HTTP/1.1 200 OK\r\n");
+ response.write("Content-Length: " + CONTENT_LENGTH + "\r\n");
+ response.write("Connection: close\r\n");
+ response.write("\r\n");
+
+ // Write enough data to ensure onDataAvailable gets called
+ while (bytes_written < 4096) {
+ response.write(text);
+ bytes_written += text.length;
+ }
+
+ response.finish();
+}
+
+function test_hugeContentLength() {
+ var chan = NetUtil.newChannel({
+ uri: "http://localhost:" + httpServer.identity.primaryPort + "/",
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+ chan.asyncOpen(listener);
+}
+
+add_test(test_hugeContentLength);
+
+function run_test() {
+ httpServer = new HttpServer();
+ httpServer.registerPathHandler("/", hugeContentLength);
+ httpServer.start(-1);
+ run_next_test();
+}
diff --git a/netwerk/test/unit/test_bug540566.js b/netwerk/test/unit/test_bug540566.js
new file mode 100644
index 0000000000..24260421ad
--- /dev/null
+++ b/netwerk/test/unit/test_bug540566.js
@@ -0,0 +1,24 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+"use strict";
+
+function continue_test(status, entry) {
+ Assert.equal(status, Cr.NS_OK);
+ // TODO - mayhemer: remove this tests completely
+ // entry.deviceID;
+ // if the above line does not crash, the test was successful
+ do_test_finished();
+}
+
+function run_test() {
+ asyncOpenCacheEntry(
+ "http://some.key/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ continue_test
+ );
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_bug543805.js b/netwerk/test/unit/test_bug543805.js
new file mode 100644
index 0000000000..4546b81489
--- /dev/null
+++ b/netwerk/test/unit/test_bug543805.js
@@ -0,0 +1,136 @@
+"use strict";
+
+const URL = "ftp://localhost/bug543805/";
+
+var dayNames = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
+var year = new Date().getFullYear().toString();
+var day = dayNames[new Date(year, 0, 1).getDay()];
+
+const tests = [
+ // AIX ls format
+ [
+ "-rw-r--r-- 1 0 11 Jan 1 20:19 nodup.file\r\n" +
+ "-rw-r--r-- 1 0 22 Jan 1 20:19 test.blankfile\r\n" +
+ "-rw-r--r-- 1 0 33 Apr 1 2008 test2.blankfile\r\n" +
+ "-rw-r--r-- 1 0 44 Jan 1 20:19 nodup.file\r\n" +
+ "-rw-r--r-- 1 0 55 Jan 1 20:19 test.file\r\n" +
+ "-rw-r--r-- 1 0 66 Apr 1 2008 test2.file\r\n",
+
+ "300: " +
+ URL +
+ "\n" +
+ "200: filename content-length last-modified file-type\n" +
+ '201: "%20nodup.file" 11 ' +
+ day +
+ "%2C%2001%20Jan%20" +
+ year +
+ "%2020%3A19%3A00%20GMT FILE \n" +
+ '201: "%20test.blankfile" 22 ' +
+ day +
+ "%2C%2001%20Jan%20" +
+ year +
+ "%2020%3A19%3A00%20GMT FILE \n" +
+ '201: "%20test2.blankfile" 33 Tue%2C%2001%20Apr%202008%2000%3A00%3A00%20GMT FILE \n' +
+ '201: "nodup.file" 44 ' +
+ day +
+ "%2C%2001%20Jan%20" +
+ year +
+ "%2020%3A19%3A00%20GMT FILE \n" +
+ '201: "test.file" 55 ' +
+ day +
+ "%2C%2001%20Jan%20" +
+ year +
+ "%2020%3A19%3A00%20GMT FILE \n" +
+ '201: "test2.file" 66 Tue%2C%2001%20Apr%202008%2000%3A00%3A00%20GMT FILE \n',
+ ],
+
+ // standard ls format
+ [
+ "-rw-r--r-- 1 500 500 11 Jan 1 20:19 nodup.file\r\n" +
+ "-rw-r--r-- 1 500 500 22 Jan 1 20:19 test.blankfile\r\n" +
+ "-rw-r--r-- 1 500 500 33 Apr 1 2008 test2.blankfile\r\n" +
+ "-rw-r--r-- 1 500 500 44 Jan 1 20:19 nodup.file\r\n" +
+ "-rw-r--r-- 1 500 500 55 Jan 1 20:19 test.file\r\n" +
+ "-rw-r--r-- 1 500 500 66 Apr 1 2008 test2.file\r\n",
+
+ "300: " +
+ URL +
+ "\n" +
+ "200: filename content-length last-modified file-type\n" +
+ '201: "%20nodup.file" 11 ' +
+ day +
+ "%2C%2001%20Jan%20" +
+ year +
+ "%2020%3A19%3A00%20GMT FILE \n" +
+ '201: "%20test.blankfile" 22 ' +
+ day +
+ "%2C%2001%20Jan%20" +
+ year +
+ "%2020%3A19%3A00%20GMT FILE \n" +
+ '201: "%20test2.blankfile" 33 Tue%2C%2001%20Apr%202008%2000%3A00%3A00%20GMT FILE \n' +
+ '201: "nodup.file" 44 ' +
+ day +
+ "%2C%2001%20Jan%20" +
+ year +
+ "%2020%3A19%3A00%20GMT FILE \n" +
+ '201: "test.file" 55 ' +
+ day +
+ "%2C%2001%20Jan%20" +
+ year +
+ "%2020%3A19%3A00%20GMT FILE \n" +
+ '201: "test2.file" 66 Tue%2C%2001%20Apr%202008%2000%3A00%3A00%20GMT FILE \n',
+ ],
+];
+
+function checkData(request, data, ctx) {
+ Assert.equal(tests[0][1], data);
+ tests.shift();
+ executeSoon(next_test);
+}
+
+function storeData(status, entry) {
+ var scs = Cc["@mozilla.org/streamConverters;1"].getService(
+ Ci.nsIStreamConverterService
+ );
+ var converter = scs.asyncConvertData(
+ "text/ftp-dir",
+ "application/http-index-format",
+ new ChannelListener(checkData, null, CL_ALLOW_UNKNOWN_CL),
+ null
+ );
+
+ var stream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(
+ Ci.nsIStringInputStream
+ );
+ stream.data = tests[0][0];
+
+ var url = NetUtil.newURI(URL);
+
+ var channel = {
+ URI: url,
+ contentLength: -1,
+ pending: true,
+ isPending() {
+ return this.pending;
+ },
+ QueryInterface: ChromeUtils.generateQI(["nsIChannel"]),
+ };
+
+ converter.onStartRequest(channel, null);
+ converter.onDataAvailable(channel, stream, 0, 0);
+ channel.pending = false;
+ converter.onStopRequest(channel, null, Cr.NS_OK);
+}
+
+function next_test() {
+ if (tests.length == 0) {
+ do_test_finished();
+ } else {
+ storeData();
+ }
+}
+
+function run_test() {
+ executeSoon(next_test);
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_bug553970.js b/netwerk/test/unit/test_bug553970.js
new file mode 100644
index 0000000000..95fcf6e9bc
--- /dev/null
+++ b/netwerk/test/unit/test_bug553970.js
@@ -0,0 +1,53 @@
+"use strict";
+
+function makeURL(spec) {
+ return Cc["@mozilla.org/network/io-service;1"]
+ .getService(Ci.nsIIOService)
+ .newURI(spec)
+ .QueryInterface(Ci.nsIURL);
+}
+
+// Checks that nsIURL::GetRelativeSpec does what it claims to do.
+function run_test() {
+ // Elements of tests have the form [this.spec, aURIToCompare.spec, expectedResult].
+ let tests = [
+ [
+ "http://mozilla.org/",
+ "http://www.mozilla.org/",
+ "http://www.mozilla.org/",
+ ],
+ [
+ "http://mozilla.org/",
+ "http://www.mozilla.org",
+ "http://www.mozilla.org/",
+ ],
+ ["http://foo.com/bar/", "http://foo.com:80/bar/", ""],
+ ["http://foo.com/", "http://foo.com/a.htm#b", "a.htm#b"],
+ ["http://foo.com/a/b/", "http://foo.com/c", "../../c"],
+ ["http://foo.com/a?b/c/", "http://foo.com/c", "c"],
+ ["http://foo.com/a#b/c/", "http://foo.com/c", "c"],
+ ["http://foo.com/a;p?b/c/", "http://foo.com/c", "c"],
+ ["http://foo.com/a/b?c/d/", "http://foo.com/c", "../c"],
+ ["http://foo.com/a/b#c/d/", "http://foo.com/c", "../c"],
+ ["http://foo.com/a/b;p?c/d/", "http://foo.com/c", "../c"],
+ ["http://foo.com/a/b/c?d/e/", "http://foo.com/f", "../../f"],
+ ["http://foo.com/a/b/c#d/e/", "http://foo.com/f", "../../f"],
+ ["http://foo.com/a/b/c;p?d/e/", "http://foo.com/f", "../../f"],
+ ["http://foo.com/a?b/c/", "http://foo.com/c/d", "c/d"],
+ ["http://foo.com/a#b/c/", "http://foo.com/c/d", "c/d"],
+ ["http://foo.com/a;p?b/c/", "http://foo.com/c/d", "c/d"],
+ ["http://foo.com/a/b?c/d/", "http://foo.com/c/d", "../c/d"],
+ ["http://foo.com/a/b#c/d/", "http://foo.com/c/d", "../c/d"],
+ ["http://foo.com/a/b;p?c/d/", "http://foo.com/c/d", "../c/d"],
+ ["http://foo.com/a/b/c?d/e/", "http://foo.com/f/g/", "../../f/g/"],
+ ["http://foo.com/a/b/c#d/e/", "http://foo.com/f/g/", "../../f/g/"],
+ ["http://foo.com/a/b/c;p?d/e/", "http://foo.com/f/g/", "../../f/g/"],
+ ];
+
+ for (var i = 0; i < tests.length; i++) {
+ let url1 = makeURL(tests[i][0]);
+ let url2 = makeURL(tests[i][1]);
+ let expected = tests[i][2];
+ Assert.equal(expected, url1.getRelativeSpec(url2));
+ }
+}
diff --git a/netwerk/test/unit/test_bug561042.js b/netwerk/test/unit/test_bug561042.js
new file mode 100644
index 0000000000..cc78a40993
--- /dev/null
+++ b/netwerk/test/unit/test_bug561042.js
@@ -0,0 +1,42 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+const SERVER_PORT = 8080;
+const baseURL = "http://localhost:" + SERVER_PORT + "/";
+
+var cookie = "";
+for (let i = 0; i < 10000; i++) {
+ cookie += " big cookie";
+}
+
+var listener = {
+ onStartRequest(request) {},
+
+ onDataAvailable(request, stream) {},
+
+ onStopRequest(request, status) {
+ Assert.equal(status, Cr.NS_OK);
+ server.stop(do_test_finished);
+ },
+};
+
+var server = new HttpServer();
+function run_test() {
+ server.start(SERVER_PORT);
+ server.registerPathHandler("/", function(metadata, response) {
+ response.setStatusLine(metadata.httpVersion, 200, "OK");
+ response.setHeader("Set-Cookie", "BigCookie=" + cookie, false);
+ response.write("Hello world");
+ });
+ var chan = NetUtil.newChannel({
+ uri: baseURL,
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+ chan.asyncOpen(listener);
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_bug561276.js b/netwerk/test/unit/test_bug561276.js
new file mode 100644
index 0000000000..03ff7b3e35
--- /dev/null
+++ b/netwerk/test/unit/test_bug561276.js
@@ -0,0 +1,64 @@
+//
+// Verify that we hit the net if we discover a cycle of redirects
+// coming from cache.
+//
+
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+var httpserver = new HttpServer();
+var iteration = 0;
+
+function setupChannel(suffix) {
+ var chan = NetUtil.newChannel({
+ uri: "http://localhost:" + httpserver.identity.primaryPort + suffix,
+ loadUsingSystemPrincipal: true,
+ });
+ var httpChan = chan.QueryInterface(Ci.nsIHttpChannel);
+ httpChan.requestMethod = "GET";
+ return httpChan;
+}
+
+function checkValueAndTrigger(request, data, ctx) {
+ Assert.equal("Ok", data);
+ httpserver.stop(do_test_finished);
+}
+
+function run_test() {
+ httpserver.registerPathHandler("/redirect1", redirectHandler1);
+ httpserver.registerPathHandler("/redirect2", redirectHandler2);
+ httpserver.start(-1);
+
+ // clear cache
+ evict_cache_entries();
+
+ // load first time
+ var channel = setupChannel("/redirect1");
+ channel.asyncOpen(new ChannelListener(checkValueAndTrigger, null));
+
+ do_test_pending();
+}
+
+function redirectHandler1(metadata, response) {
+ // first time we return a cacheable 302 pointing to next redirect
+ if (iteration < 1) {
+ response.setStatusLine(metadata.httpVersion, 302, "Found");
+ response.setHeader("Cache-Control", "max-age=600", false);
+ response.setHeader("Location", "/redirect2", false);
+
+ // next time called we return 200
+ } else {
+ response.setStatusLine(metadata.httpVersion, 200, "Ok");
+ response.setHeader("Cache-Control", "max-age=600", false);
+ response.setHeader("Content-Type", "text/plain");
+ response.bodyOutputStream.write("Ok", "Ok".length);
+ }
+ iteration += 1;
+}
+
+function redirectHandler2(metadata, response) {
+ response.setStatusLine(metadata.httpVersion, 302, "Found");
+ response.setHeader("Cache-Control", "max-age=600", false);
+ response.setHeader("Location", "/redirect1", false);
+}
diff --git a/netwerk/test/unit/test_bug580508.js b/netwerk/test/unit/test_bug580508.js
new file mode 100644
index 0000000000..8cc8a7b341
--- /dev/null
+++ b/netwerk/test/unit/test_bug580508.js
@@ -0,0 +1,33 @@
+"use strict";
+
+var ioService = Cc["@mozilla.org/network/io-service;1"].getService(
+ Ci.nsIIOService
+);
+var resProt = ioService
+ .getProtocolHandler("resource")
+ .QueryInterface(Ci.nsIResProtocolHandler);
+
+function run_test() {
+ // Define a resource:// alias that points to another resource:// URI.
+ let greModulesURI = ioService.newURI("resource://gre/modules/");
+ resProt.setSubstitution("my-gre-modules", greModulesURI);
+
+ // When we ask for the alias, we should not get the resource://
+ // URI that we registered it for but the original file URI.
+ let greFileSpec = ioService.newURI(
+ "modules/",
+ null,
+ resProt.getSubstitution("gre")
+ ).spec;
+ let aliasURI = resProt.getSubstitution("my-gre-modules");
+ Assert.equal(aliasURI.spec, greFileSpec);
+
+ // Resolving URIs using the original resource path and the alias
+ // should yield the same result.
+ let greNetUtilURI = ioService.newURI("resource://gre/modules/NetUtil.jsm");
+ let myNetUtilURI = ioService.newURI("resource://my-gre-modules/NetUtil.jsm");
+ Assert.equal(
+ resProt.resolveURI(greNetUtilURI),
+ resProt.resolveURI(myNetUtilURI)
+ );
+}
diff --git a/netwerk/test/unit/test_bug586908.js b/netwerk/test/unit/test_bug586908.js
new file mode 100644
index 0000000000..a6b4b8a802
--- /dev/null
+++ b/netwerk/test/unit/test_bug586908.js
@@ -0,0 +1,104 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+const { MockRegistrar } = ChromeUtils.import(
+ "resource://testing-common/MockRegistrar.jsm"
+);
+
+var httpserv = null;
+
+const CID = Components.ID("{5645d2c1-d6d8-4091-b117-fe7ee4027db7}");
+XPCOMUtils.defineLazyGetter(this, "systemSettings", function() {
+ return {
+ QueryInterface: ChromeUtils.generateQI(["nsISystemProxySettings"]),
+
+ mainThreadOnly: true,
+ PACURI: "http://localhost:" + httpserv.identity.primaryPort + "/redirect",
+ getProxyForURI(aURI) {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
+ },
+ };
+});
+
+function checkValue(request, data, ctx) {
+ Assert.ok(called);
+ Assert.equal("ok", data);
+ httpserv.stop(do_test_finished);
+}
+
+function makeChan(url) {
+ return NetUtil.newChannel({
+ uri: url,
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+}
+
+function run_test() {
+ httpserv = new HttpServer();
+ httpserv.registerPathHandler("/redirect", redirect);
+ httpserv.registerPathHandler("/pac", pac);
+ httpserv.registerPathHandler("/target", target);
+ httpserv.start(-1);
+
+ MockRegistrar.register(
+ "@mozilla.org/system-proxy-settings;1",
+ systemSettings
+ );
+
+ // Ensure we're using system-properties
+ const prefs = Cc["@mozilla.org/preferences-service;1"].getService(
+ Ci.nsIPrefBranch
+ );
+ prefs.setIntPref(
+ "network.proxy.type",
+ Ci.nsIProtocolProxyService.PROXYCONFIG_SYSTEM
+ );
+
+ // clear cache
+ evict_cache_entries();
+
+ var chan = makeChan(
+ "http://localhost:" + httpserv.identity.primaryPort + "/target"
+ );
+ chan.asyncOpen(new ChannelListener(checkValue, null));
+
+ do_test_pending();
+}
+
+var called = false,
+ failed = false;
+function redirect(metadata, response) {
+ // If called second time, just return the PAC but set failed-flag
+ if (called) {
+ failed = true;
+ return pac(metadata, response);
+ }
+
+ called = true;
+ response.setStatusLine(metadata.httpVersion, 302, "Found");
+ response.setHeader("Location", "/pac", false);
+ var body = "Moved\n";
+ response.bodyOutputStream.write(body, body.length);
+}
+
+function pac(metadata, response) {
+ var PAC = 'function FindProxyForURL(url, host) { return "DIRECT"; }';
+ response.setStatusLine(metadata.httpVersion, 200, "Ok");
+ response.setHeader(
+ "Content-Type",
+ "application/x-ns-proxy-autoconfig",
+ false
+ );
+ response.bodyOutputStream.write(PAC, PAC.length);
+}
+
+function target(metadata, response) {
+ var retval = "ok";
+ if (failed) {
+ retval = "failed";
+ }
+
+ response.setStatusLine(metadata.httpVersion, 200, "Ok");
+ response.setHeader("Content-Type", "text/plain", false);
+ response.bodyOutputStream.write(retval, retval.length);
+}
diff --git a/netwerk/test/unit/test_bug596443.js b/netwerk/test/unit/test_bug596443.js
new file mode 100644
index 0000000000..07e4bd760b
--- /dev/null
+++ b/netwerk/test/unit/test_bug596443.js
@@ -0,0 +1,115 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+var httpProtocolHandler = Cc[
+ "@mozilla.org/network/protocol;1?name=http"
+].getService(Ci.nsIHttpProtocolHandler);
+
+var httpserver = new HttpServer();
+
+var expectedOnStopRequests = 3;
+
+function setupChannel(suffix, xRequest, flags) {
+ var chan = NetUtil.newChannel({
+ uri: "http://localhost:" + httpserver.identity.primaryPort + suffix,
+ loadUsingSystemPrincipal: true,
+ });
+ if (flags) {
+ chan.loadFlags |= flags;
+ }
+
+ var httpChan = chan.QueryInterface(Ci.nsIHttpChannel);
+ httpChan.setRequestHeader("x-request", xRequest, false);
+
+ return httpChan;
+}
+
+function Listener(response) {
+ this._response = response;
+}
+Listener.prototype = {
+ _response: null,
+ _buffer: null,
+
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIStreamListener",
+ "nsIRequestObserver",
+ ]),
+
+ onStartRequest(request) {
+ this._buffer = "";
+ },
+ onDataAvailable(request, stream, offset, count) {
+ this._buffer = this._buffer.concat(read_stream(stream, count));
+ },
+ onStopRequest(request, status) {
+ Assert.equal(this._buffer, this._response);
+ if (--expectedOnStopRequests == 0) {
+ do_timeout(10, function() {
+ httpserver.stop(do_test_finished);
+ });
+ }
+ },
+};
+
+function run_test() {
+ httpserver.registerPathHandler("/bug596443", handler);
+ httpserver.start(-1);
+
+ Services.prefs.setBoolPref("network.http.rcwn.enabled", false);
+
+ // make sure we have a profile so we can use the disk-cache
+ do_get_profile();
+
+ // clear cache
+ evict_cache_entries();
+
+ httpProtocolHandler.EnsureHSTSDataReady().then(function() {
+ var ch0 = setupChannel(
+ "/bug596443",
+ "Response0",
+ Ci.nsIRequest.LOAD_BYPASS_CACHE
+ );
+ ch0.asyncOpen(new Listener("Response0"));
+
+ var ch1 = setupChannel(
+ "/bug596443",
+ "Response1",
+ Ci.nsIRequest.LOAD_BYPASS_CACHE
+ );
+ ch1.asyncOpen(new Listener("Response1"));
+
+ var ch2 = setupChannel("/bug596443", "Should not be used");
+ ch2.asyncOpen(new Listener("Response1")); // Note param: we expect this to come from cache
+ });
+
+ do_test_pending();
+}
+
+function triggerHandlers() {
+ do_timeout(100, handlers[1]);
+ do_timeout(100, handlers[0]);
+}
+
+var handlers = [];
+function handler(metadata, response) {
+ var func = function(body) {
+ return function() {
+ response.setStatusLine(metadata.httpVersion, 200, "Ok");
+ response.setHeader("Content-Type", "text/plain", false);
+ response.setHeader("Content-Length", "" + body.length, false);
+ response.setHeader("Cache-Control", "max-age=600", false);
+ response.bodyOutputStream.write(body, body.length);
+ response.finish();
+ };
+ };
+
+ response.processAsync();
+ var request = metadata.getHeader("x-request");
+ handlers.push(func(request));
+
+ if (handlers.length > 1) {
+ triggerHandlers();
+ }
+}
diff --git a/netwerk/test/unit/test_bug618835.js b/netwerk/test/unit/test_bug618835.js
new file mode 100644
index 0000000000..bc9de4e519
--- /dev/null
+++ b/netwerk/test/unit/test_bug618835.js
@@ -0,0 +1,122 @@
+//
+// If a response to a non-safe HTTP request-method contains the Location- or
+// Content-Location header, we must make sure to invalidate any cached entry
+// representing the URIs pointed to by either header. RFC 2616 section 13.10
+//
+// This test uses 3 URIs: "/post" is the target of a POST-request and always
+// redirects (301) to "/redirect". The URIs "/redirect" and "/cl" both counts
+// the number of loads from the server (handler). The response from "/post"
+// always contains the headers "Location: /redirect" and "Content-Location:
+// /cl", whose cached entries are to be invalidated. The tests verifies that
+// "/redirect" and "/cl" are loaded from server the expected number of times.
+//
+
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+var httpserv;
+
+function setupChannel(path) {
+ return NetUtil.newChannel({
+ uri: path,
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+}
+
+// Verify that Content-Location-URI has been loaded once, load post_target
+function InitialListener() {}
+InitialListener.prototype = {
+ onStartRequest(request) {},
+ onStopRequest(request, status) {
+ Assert.equal(1, numberOfCLHandlerCalls);
+ executeSoon(function() {
+ var channel = setupChannel(
+ "http://localhost:" + httpserv.identity.primaryPort + "/post"
+ );
+ channel.requestMethod = "POST";
+ channel.asyncOpen(new RedirectingListener());
+ });
+ },
+};
+
+// Verify that Location-URI has been loaded once, reload post_target
+function RedirectingListener() {}
+RedirectingListener.prototype = {
+ onStartRequest(request) {},
+ onStopRequest(request, status) {
+ Assert.equal(1, numberOfHandlerCalls);
+ executeSoon(function() {
+ var channel = setupChannel(
+ "http://localhost:" + httpserv.identity.primaryPort + "/post"
+ );
+ channel.requestMethod = "POST";
+ channel.asyncOpen(new VerifyingListener());
+ });
+ },
+};
+
+// Verify that Location-URI has been loaded twice (cached entry invalidated),
+// reload Content-Location-URI
+function VerifyingListener() {}
+VerifyingListener.prototype = {
+ onStartRequest(request) {},
+ onStopRequest(request, status) {
+ Assert.equal(2, numberOfHandlerCalls);
+ var channel = setupChannel(
+ "http://localhost:" + httpserv.identity.primaryPort + "/cl"
+ );
+ channel.asyncOpen(new FinalListener());
+ },
+};
+
+// Verify that Location-URI has been loaded twice (cached entry invalidated),
+// stop test
+function FinalListener() {}
+FinalListener.prototype = {
+ onStartRequest(request) {},
+ onStopRequest(request, status) {
+ Assert.equal(2, numberOfCLHandlerCalls);
+ httpserv.stop(do_test_finished);
+ },
+};
+
+function run_test() {
+ httpserv = new HttpServer();
+ httpserv.registerPathHandler("/cl", content_location);
+ httpserv.registerPathHandler("/post", post_target);
+ httpserv.registerPathHandler("/redirect", redirect_target);
+ httpserv.start(-1);
+
+ // Clear cache
+ evict_cache_entries();
+
+ // Load Content-Location URI into cache and start the chain of loads
+ var channel = setupChannel(
+ "http://localhost:" + httpserv.identity.primaryPort + "/cl"
+ );
+ channel.asyncOpen(new InitialListener());
+
+ do_test_pending();
+}
+
+var numberOfCLHandlerCalls = 0;
+function content_location(metadata, response) {
+ numberOfCLHandlerCalls++;
+ response.setStatusLine(metadata.httpVersion, 200, "Ok");
+ response.setHeader("Cache-Control", "max-age=360000", false);
+}
+
+function post_target(metadata, response) {
+ response.setStatusLine(metadata.httpVersion, 301, "Moved Permanently");
+ response.setHeader("Location", "/redirect", false);
+ response.setHeader("Content-Location", "/cl", false);
+ response.setHeader("Cache-Control", "max-age=360000", false);
+}
+
+var numberOfHandlerCalls = 0;
+function redirect_target(metadata, response) {
+ numberOfHandlerCalls++;
+ response.setStatusLine(metadata.httpVersion, 200, "Ok");
+ response.setHeader("Cache-Control", "max-age=360000", false);
+}
diff --git a/netwerk/test/unit/test_bug633743.js b/netwerk/test/unit/test_bug633743.js
new file mode 100644
index 0000000000..1df40c3a48
--- /dev/null
+++ b/netwerk/test/unit/test_bug633743.js
@@ -0,0 +1,193 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+const VALUE_HDR_NAME = "X-HTTP-VALUE-HEADER";
+const VARY_HDR_NAME = "X-HTTP-VARY-HEADER";
+const CACHECTRL_HDR_NAME = "X-CACHE-CONTROL-HEADER";
+
+var httpserver = null;
+
+function make_channel(flags, vary, value) {
+ var chan = NetUtil.newChannel({
+ uri: "http://localhost:" + httpserver.identity.primaryPort + "/bug633743",
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+ return chan.QueryInterface(Ci.nsIHttpChannel);
+}
+
+function Test(flags, varyHdr, sendValue, expectValue, cacheHdr) {
+ this._flags = flags;
+ this._varyHdr = varyHdr;
+ this._sendVal = sendValue;
+ this._expectVal = expectValue;
+ this._cacheHdr = cacheHdr;
+}
+
+Test.prototype = {
+ _buffer: "",
+ _flags: null,
+ _varyHdr: null,
+ _sendVal: null,
+ _expectVal: null,
+ _cacheHdr: null,
+
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIStreamListener",
+ "nsIRequestObserver",
+ ]),
+
+ onStartRequest(request) {},
+
+ onDataAvailable(request, stream, offset, count) {
+ this._buffer = this._buffer.concat(read_stream(stream, count));
+ },
+
+ onStopRequest(request, status) {
+ Assert.equal(this._buffer, this._expectVal);
+ do_timeout(0, run_next_test);
+ },
+
+ run() {
+ var channel = make_channel();
+ channel.loadFlags = this._flags;
+ channel.setRequestHeader(VALUE_HDR_NAME, this._sendVal, false);
+ channel.setRequestHeader(VARY_HDR_NAME, this._varyHdr, false);
+ if (this._cacheHdr) {
+ channel.setRequestHeader(CACHECTRL_HDR_NAME, this._cacheHdr, false);
+ }
+
+ channel.asyncOpen(this);
+ },
+};
+
+var gTests = [
+ // Test LOAD_FROM_CACHE: Load cache-entry
+ new Test(
+ Ci.nsIRequest.LOAD_NORMAL,
+ "entity-initial", // hdr-value used to vary
+ "request1", // echoed by handler
+ "request1" // value expected to receive in channel
+ ),
+ // Verify that it was cached
+ new Test(
+ Ci.nsIRequest.LOAD_NORMAL,
+ "entity-initial", // hdr-value used to vary
+ "fresh value with LOAD_NORMAL", // echoed by handler
+ "request1" // value expected to receive in channel
+ ),
+ // Load same entity with LOAD_FROM_CACHE-flag
+ new Test(
+ Ci.nsIRequest.LOAD_FROM_CACHE,
+ "entity-initial", // hdr-value used to vary
+ "fresh value with LOAD_FROM_CACHE", // echoed by handler
+ "request1" // value expected to receive in channel
+ ),
+ // Load different entity with LOAD_FROM_CACHE-flag
+ new Test(
+ Ci.nsIRequest.LOAD_FROM_CACHE,
+ "entity-l-f-c", // hdr-value used to vary
+ "request2", // echoed by handler
+ "request2" // value expected to receive in channel
+ ),
+ // Verify that new value was cached
+ new Test(
+ Ci.nsIRequest.LOAD_NORMAL,
+ "entity-l-f-c", // hdr-value used to vary
+ "fresh value with LOAD_NORMAL", // echoed by handler
+ "request2" // value expected to receive in channel
+ ),
+
+ // Test VALIDATE_NEVER: Note previous cache-entry
+ new Test(
+ Ci.nsIRequest.VALIDATE_NEVER,
+ "entity-v-n", // hdr-value used to vary
+ "request3", // echoed by handler
+ "request3" // value expected to receive in channel
+ ),
+ // Verify that cache-entry was replaced
+ new Test(
+ Ci.nsIRequest.LOAD_NORMAL,
+ "entity-v-n", // hdr-value used to vary
+ "fresh value with LOAD_NORMAL", // echoed by handler
+ "request3" // value expected to receive in channel
+ ),
+
+ // Test combination VALIDATE_NEVER && no-store: Load new cache-entry
+ new Test(
+ Ci.nsIRequest.LOAD_NORMAL,
+ "entity-2", // hdr-value used to vary
+ "request4", // echoed by handler
+ "request4", // value expected to receive in channel
+ "no-store" // set no-store on response
+ ),
+ // Ensure we validate without IMS header in this case (verified in handler)
+ new Test(
+ Ci.nsIRequest.VALIDATE_NEVER,
+ "entity-2-v-n", // hdr-value used to vary
+ "request5", // echoed by handler
+ "request5" // value expected to receive in channel
+ ),
+
+ // Test VALIDATE-ALWAYS: Load new entity
+ new Test(
+ Ci.nsIRequest.LOAD_NORMAL,
+ "entity-3", // hdr-value used to vary
+ "request6", // echoed by handler
+ "request6", // value expected to receive in channel
+ "no-cache" // set no-cache on response
+ ),
+ // Ensure we don't send IMS header also in this case (verified in handler)
+ new Test(
+ Ci.nsIRequest.VALIDATE_ALWAYS,
+ "entity-3-v-a", // hdr-value used to vary
+ "request7", // echoed by handler
+ "request7" // value expected to receive in channel
+ ),
+];
+
+function run_next_test() {
+ if (gTests.length == 0) {
+ httpserver.stop(do_test_finished);
+ return;
+ }
+
+ var test = gTests.shift();
+ test.run();
+}
+
+function handler(metadata, response) {
+ // None of the tests above should send an IMS
+ Assert.ok(!metadata.hasHeader("If-Modified-Since"));
+
+ // Pick up requested value to echo
+ var hdr = "default value";
+ try {
+ hdr = metadata.getHeader(VALUE_HDR_NAME);
+ } catch (ex) {}
+
+ // Pick up requested cache-control header-value
+ var cctrlVal = "max-age=10000";
+ try {
+ cctrlVal = metadata.getHeader(CACHECTRL_HDR_NAME);
+ } catch (ex) {}
+
+ response.setStatusLine(metadata.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "text/plain", false);
+ response.setHeader("Cache-Control", cctrlVal, false);
+ response.setHeader("Vary", VARY_HDR_NAME, false);
+ response.setHeader("Last-Modified", "Tue, 15 Nov 1994 12:45:26 GMT", false);
+ response.bodyOutputStream.write(hdr, hdr.length);
+}
+
+function run_test() {
+ // clear the cache
+ evict_cache_entries();
+
+ httpserver = new HttpServer();
+ httpserver.registerPathHandler("/bug633743", handler);
+ httpserver.start(-1);
+
+ run_next_test();
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_bug650522.js b/netwerk/test/unit/test_bug650522.js
new file mode 100644
index 0000000000..b7ad89b1c1
--- /dev/null
+++ b/netwerk/test/unit/test_bug650522.js
@@ -0,0 +1,34 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async () => {
+ Services.prefs.setBoolPref("network.cookie.sameSite.schemeful", false);
+
+ var cm = Cc["@mozilla.org/cookiemanager;1"].getService(Ci.nsICookieManager);
+ var expiry = (Date.now() + 1000) * 1000;
+
+ // Test our handling of host names with a single character at the beginning
+ // followed by a dot.
+ cm.add(
+ "e.com",
+ "/",
+ "foo",
+ "bar",
+ false,
+ false,
+ true,
+ expiry,
+ {},
+ Ci.nsICookie.SAMESITE_NONE,
+ Ci.nsICookie.SCHEME_HTTP
+ );
+ Assert.equal(cm.countCookiesFromHost("e.com"), 1);
+
+ CookieXPCShellUtils.createServer({ hosts: ["e.com"] });
+ const cookies = await CookieXPCShellUtils.getCookieStringFromDocument(
+ "http://e.com/"
+ );
+ Assert.equal(cookies, "foo=bar");
+});
diff --git a/netwerk/test/unit/test_bug650995.js b/netwerk/test/unit/test_bug650995.js
new file mode 100644
index 0000000000..73a004390a
--- /dev/null
+++ b/netwerk/test/unit/test_bug650995.js
@@ -0,0 +1,189 @@
+//
+// Test that "max_entry_size" prefs for disk- and memory-cache prevents
+// caching resources with size out of bounds
+//
+
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+do_get_profile();
+
+const prefService = Cc["@mozilla.org/preferences-service;1"].getService(
+ Ci.nsIPrefBranch
+);
+
+const httpserver = new HttpServer();
+
+// Repeats the given data until the total size is larger than 1K
+function repeatToLargerThan1K(data) {
+ while (data.length <= 1024) {
+ data += data;
+ }
+ return data;
+}
+
+function setupChannel(suffix, value) {
+ var chan = NetUtil.newChannel({
+ uri: "http://localhost:" + httpserver.identity.primaryPort + suffix,
+ loadUsingSystemPrincipal: true,
+ });
+ var httpChan = chan.QueryInterface(Ci.nsIHttpChannel);
+ httpChan.setRequestHeader("x-request", value, false);
+
+ return httpChan;
+}
+
+var tests = [
+ new InitializeCacheDevices(true, false), // enable and create mem-device
+ new TestCacheEntrySize(
+ function() {
+ prefService.setIntPref("browser.cache.memory.max_entry_size", 1);
+ },
+ "012345",
+ "9876543210",
+ "012345"
+ ), // expect cached value
+ new TestCacheEntrySize(
+ function() {
+ prefService.setIntPref("browser.cache.memory.max_entry_size", 1);
+ },
+ "0123456789a",
+ "9876543210",
+ "9876543210"
+ ), // expect fresh value
+ new TestCacheEntrySize(
+ function() {
+ prefService.setIntPref("browser.cache.memory.max_entry_size", -1);
+ },
+ "0123456789a",
+ "9876543210",
+ "0123456789a"
+ ), // expect cached value
+
+ new InitializeCacheDevices(false, true), // enable and create disk-device
+ new TestCacheEntrySize(
+ function() {
+ prefService.setIntPref("browser.cache.disk.max_entry_size", 1);
+ },
+ "012345",
+ "9876543210",
+ "012345"
+ ), // expect cached value
+ new TestCacheEntrySize(
+ function() {
+ prefService.setIntPref("browser.cache.disk.max_entry_size", 1);
+ },
+ "0123456789a",
+ "9876543210",
+ "9876543210"
+ ), // expect fresh value
+ new TestCacheEntrySize(
+ function() {
+ prefService.setIntPref("browser.cache.disk.max_entry_size", -1);
+ },
+ "0123456789a",
+ "9876543210",
+ "0123456789a"
+ ), // expect cached value
+];
+
+function nextTest() {
+ // We really want each test to be self-contained. Make sure cache is
+ // cleared and also let all operations finish before starting a new test
+ syncWithCacheIOThread(function() {
+ get_cache_service().clear();
+ syncWithCacheIOThread(runNextTest);
+ });
+}
+
+function runNextTest() {
+ var aTest = tests.shift();
+ if (!aTest) {
+ httpserver.stop(do_test_finished);
+ return;
+ }
+ executeSoon(function() {
+ aTest.start();
+ });
+}
+
+// Just make sure devices are created
+function InitializeCacheDevices(memDevice, diskDevice) {
+ this.start = function() {
+ prefService.setBoolPref("browser.cache.memory.enable", memDevice);
+ if (memDevice) {
+ let cap = prefService.getIntPref("browser.cache.memory.capacity", 0);
+ if (cap == 0) {
+ prefService.setIntPref("browser.cache.memory.capacity", 1024);
+ }
+ }
+ prefService.setBoolPref("browser.cache.disk.enable", diskDevice);
+ if (diskDevice) {
+ let cap = prefService.getIntPref("browser.cache.disk.capacity", 0);
+ if (cap == 0) {
+ prefService.setIntPref("browser.cache.disk.capacity", 1024);
+ }
+ }
+ var channel = setupChannel("/bug650995", "Initial value");
+ channel.asyncOpen(new ChannelListener(nextTest, null));
+ };
+}
+
+function TestCacheEntrySize(
+ setSizeFunc,
+ firstRequest,
+ secondRequest,
+ secondExpectedReply
+) {
+ // Initially, this test used 10 bytes as the limit for caching entries.
+ // Since we now use 1K granularity we have to extend lengths to be larger
+ // than 1K if it is larger than 10
+ if (firstRequest.length > 10) {
+ firstRequest = repeatToLargerThan1K(firstRequest);
+ }
+ if (secondExpectedReply.length > 10) {
+ secondExpectedReply = repeatToLargerThan1K(secondExpectedReply);
+ }
+
+ this.start = function() {
+ setSizeFunc();
+ var channel = setupChannel("/bug650995", firstRequest);
+ channel.asyncOpen(new ChannelListener(this.initialLoad, this));
+ };
+ this.initialLoad = function(request, data, ctx) {
+ Assert.equal(firstRequest, data);
+ var channel = setupChannel("/bug650995", secondRequest);
+ executeSoon(function() {
+ channel.asyncOpen(new ChannelListener(ctx.testAndTriggerNext, ctx));
+ });
+ };
+ this.testAndTriggerNext = function(request, data, ctx) {
+ Assert.equal(secondExpectedReply, data);
+ executeSoon(nextTest);
+ };
+}
+
+function run_test() {
+ httpserver.registerPathHandler("/bug650995", handler);
+ httpserver.start(-1);
+
+ prefService.setBoolPref("browser.cache.offline.enable", false);
+ prefService.setBoolPref("browser.cache.offline.storage.enable", false);
+ prefService.setBoolPref("network.http.rcwn.enabled", false);
+
+ nextTest();
+ do_test_pending();
+}
+
+function handler(metadata, response) {
+ var body = "BOOM!";
+ try {
+ body = metadata.getHeader("x-request");
+ } catch (e) {}
+
+ response.setStatusLine(metadata.httpVersion, 200, "Ok");
+ response.setHeader("Content-Type", "text/plain", false);
+ response.setHeader("Cache-Control", "max-age=3600", false);
+ response.bodyOutputStream.write(body, body.length);
+}
diff --git a/netwerk/test/unit/test_bug652761.js b/netwerk/test/unit/test_bug652761.js
new file mode 100644
index 0000000000..030f18a1fd
--- /dev/null
+++ b/netwerk/test/unit/test_bug652761.js
@@ -0,0 +1,19 @@
+// This is just a crashtest for a url that is rejected at parse time (port 80,000)
+
+"use strict";
+
+function run_test() {
+ // Bug 1301621 makes invalid ports throw
+ Assert.throws(
+ () => {
+ NetUtil.newChannel({
+ uri: "http://localhost:80000/",
+ loadUsingSystemPrincipal: true,
+ });
+ },
+ /NS_ERROR_MALFORMED_URI/,
+ "invalid port"
+ );
+
+ do_test_finished();
+}
diff --git a/netwerk/test/unit/test_bug654926.js b/netwerk/test/unit/test_bug654926.js
new file mode 100644
index 0000000000..3a5660dfdb
--- /dev/null
+++ b/netwerk/test/unit/test_bug654926.js
@@ -0,0 +1,102 @@
+"use strict";
+
+var _PSvc;
+function get_pref_service() {
+ if (_PSvc) {
+ return _PSvc;
+ }
+
+ return (_PSvc = Cc["@mozilla.org/preferences-service;1"].getService(
+ Ci.nsIPrefBranch
+ ));
+}
+
+function gen_1MiB() {
+ var i;
+ var data = "x";
+ for (i = 0; i < 20; i++) {
+ data += data;
+ }
+ return data;
+}
+
+function write_and_check(str, data, len) {
+ var written = str.write(data, len);
+ if (written != len) {
+ do_throw(
+ "str.write has not written all data!\n" +
+ " Expected: " +
+ len +
+ "\n" +
+ " Actual: " +
+ written +
+ "\n"
+ );
+ }
+}
+
+function write_datafile(status, entry) {
+ Assert.equal(status, Cr.NS_OK);
+ var data = gen_1MiB();
+ var os = entry.openOutputStream(0, data.length);
+
+ // write 2MiB
+ var i;
+ for (i = 0; i < 2; i++) {
+ write_and_check(os, data, data.length);
+ }
+
+ os.close();
+ entry.close();
+
+ // now change max_entry_size so that the existing entry is too big
+ get_pref_service().setIntPref("browser.cache.disk.max_entry_size", 1024);
+
+ // append to entry
+ asyncOpenCacheEntry(
+ "http://data/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ append_datafile
+ );
+}
+
+function append_datafile(status, entry) {
+ Assert.equal(status, Cr.NS_OK);
+ var os = entry.openOutputStream(entry.dataSize, -1);
+ var data = gen_1MiB();
+
+ // append 1MiB
+ try {
+ write_and_check(os, data, data.length);
+ do_throw();
+ } catch (ex) {}
+
+ // closing the ostream should fail in this case
+ try {
+ os.close();
+ do_throw();
+ } catch (ex) {}
+
+ entry.close();
+
+ do_test_finished();
+}
+
+function run_test() {
+ do_get_profile();
+
+ // clear the cache
+ evict_cache_entries();
+
+ asyncOpenCacheEntry(
+ "http://data/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ write_datafile
+ );
+
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_bug654926_doom_and_read.js b/netwerk/test/unit/test_bug654926_doom_and_read.js
new file mode 100644
index 0000000000..641b1d233e
--- /dev/null
+++ b/netwerk/test/unit/test_bug654926_doom_and_read.js
@@ -0,0 +1,90 @@
+"use strict";
+
+function gen_1MiB() {
+ var i;
+ var data = "x";
+ for (i = 0; i < 20; i++) {
+ data += data;
+ }
+ return data;
+}
+
+function write_and_check(str, data, len) {
+ var written = str.write(data, len);
+ if (written != len) {
+ do_throw(
+ "str.write has not written all data!\n" +
+ " Expected: " +
+ len +
+ "\n" +
+ " Actual: " +
+ written +
+ "\n"
+ );
+ }
+}
+
+function make_input_stream_scriptable(input) {
+ var wrapper = Cc["@mozilla.org/scriptableinputstream;1"].createInstance(
+ Ci.nsIScriptableInputStream
+ );
+ wrapper.init(input);
+ return wrapper;
+}
+
+function write_datafile(status, entry) {
+ Assert.equal(status, Cr.NS_OK);
+ var data = gen_1MiB();
+ var os = entry.openOutputStream(0, data.length);
+
+ write_and_check(os, data, data.length);
+
+ os.close();
+ entry.close();
+
+ // open, doom, append, read
+ asyncOpenCacheEntry(
+ "http://data/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ test_read_after_doom
+ );
+}
+
+function test_read_after_doom(status, entry) {
+ Assert.equal(status, Cr.NS_OK);
+ var data = gen_1MiB();
+ var os = entry.openOutputStream(entry.dataSize, data.length);
+
+ entry.asyncDoom(null);
+ write_and_check(os, data, data.length);
+
+ os.close();
+
+ var is = entry.openInputStream(0);
+ pumpReadStream(is, function(read) {
+ Assert.equal(read.length, 2 * 1024 * 1024);
+ is.close();
+
+ entry.close();
+ do_test_finished();
+ });
+}
+
+function run_test() {
+ do_get_profile();
+
+ // clear the cache
+ evict_cache_entries();
+
+ asyncOpenCacheEntry(
+ "http://data/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ write_datafile
+ );
+
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_bug654926_test_seek.js b/netwerk/test/unit/test_bug654926_test_seek.js
new file mode 100644
index 0000000000..148e9f9043
--- /dev/null
+++ b/netwerk/test/unit/test_bug654926_test_seek.js
@@ -0,0 +1,76 @@
+"use strict";
+
+function gen_1MiB() {
+ var i;
+ var data = "x";
+ for (i = 0; i < 20; i++) {
+ data += data;
+ }
+ return data;
+}
+
+function write_and_check(str, data, len) {
+ var written = str.write(data, len);
+ if (written != len) {
+ do_throw(
+ "str.write has not written all data!\n" +
+ " Expected: " +
+ len +
+ "\n" +
+ " Actual: " +
+ written +
+ "\n"
+ );
+ }
+}
+
+function write_datafile(status, entry) {
+ Assert.equal(status, Cr.NS_OK);
+ var data = gen_1MiB();
+ var os = entry.openOutputStream(0, data.length);
+
+ write_and_check(os, data, data.length);
+
+ os.close();
+ entry.close();
+
+ // try to open the entry for appending
+ asyncOpenCacheEntry(
+ "http://data/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ open_for_readwrite
+ );
+}
+
+function open_for_readwrite(status, entry) {
+ Assert.equal(status, Cr.NS_OK);
+ var os = entry.openOutputStream(entry.dataSize, -1);
+
+ // Opening the entry for appending data calls nsDiskCacheStreamIO::Seek()
+ // which initializes mFD. If no data is written then mBufDirty is false and
+ // mFD won't be closed in nsDiskCacheStreamIO::Flush().
+
+ os.close();
+ entry.close();
+
+ do_test_finished();
+}
+
+function run_test() {
+ do_get_profile();
+
+ // clear the cache
+ evict_cache_entries();
+
+ asyncOpenCacheEntry(
+ "http://data/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ write_datafile
+ );
+
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_bug659569.js b/netwerk/test/unit/test_bug659569.js
new file mode 100644
index 0000000000..60a14765b2
--- /dev/null
+++ b/netwerk/test/unit/test_bug659569.js
@@ -0,0 +1,58 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+var httpserver = new HttpServer();
+
+function setupChannel(suffix) {
+ return NetUtil.newChannel({
+ uri: "http://localhost:" + httpserver.identity.primaryPort + suffix,
+ loadUsingSystemPrincipal: true,
+ });
+}
+
+function checkValueAndTrigger(request, data, ctx) {
+ Assert.equal("Ok", data);
+ httpserver.stop(do_test_finished);
+}
+
+function run_test() {
+ // We don't want to have CookieJarSettings blocking this test.
+ Services.prefs.setBoolPref(
+ "network.cookieJarSettings.unblocked_for_testing",
+ true
+ );
+
+ // Allow all cookies.
+ Services.prefs.setIntPref("network.cookie.cookieBehavior", 0);
+
+ httpserver.registerPathHandler("/redirect1", redirectHandler1);
+ httpserver.registerPathHandler("/redirect2", redirectHandler2);
+ httpserver.start(-1);
+
+ // clear cache
+ evict_cache_entries();
+
+ // load first time
+ var channel = setupChannel("/redirect1");
+ channel.asyncOpen(new ChannelListener(checkValueAndTrigger, null));
+ do_test_pending();
+}
+
+function redirectHandler1(metadata, response) {
+ if (!metadata.hasHeader("Cookie")) {
+ response.setStatusLine(metadata.httpVersion, 302, "Found");
+ response.setHeader("Cache-Control", "max-age=600", false);
+ response.setHeader("Location", "/redirect2?query", false);
+ response.setHeader("Set-Cookie", "MyCookie=1", false);
+ } else {
+ response.setStatusLine(metadata.httpVersion, 200, "Ok");
+ response.setHeader("Content-Type", "text/plain");
+ response.bodyOutputStream.write("Ok", "Ok".length);
+ }
+}
+
+function redirectHandler2(metadata, response) {
+ response.setStatusLine(metadata.httpVersion, 302, "Found");
+ response.setHeader("Location", "/redirect1", false);
+}
diff --git a/netwerk/test/unit/test_bug660066.js b/netwerk/test/unit/test_bug660066.js
new file mode 100644
index 0000000000..00abe8de84
--- /dev/null
+++ b/netwerk/test/unit/test_bug660066.js
@@ -0,0 +1,56 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+"use strict";
+
+const SIMPLEURI_SPEC = "data:text/plain,hello world";
+const BLOBURI_SPEC = "blob:123456";
+
+function do_info(text, stack) {
+ if (!stack) {
+ stack = Components.stack.caller;
+ }
+
+ dump(
+ "\n" +
+ "TEST-INFO | " +
+ stack.filename +
+ " | [" +
+ stack.name +
+ " : " +
+ stack.lineNumber +
+ "] " +
+ text +
+ "\n"
+ );
+}
+
+function do_check_uri_neq(uri1, uri2) {
+ do_info("Checking equality in forward direction...");
+ Assert.ok(!uri1.equals(uri2));
+ Assert.ok(!uri1.equalsExceptRef(uri2));
+
+ do_info("Checking equality in reverse direction...");
+ Assert.ok(!uri2.equals(uri1));
+ Assert.ok(!uri2.equalsExceptRef(uri1));
+}
+
+function run_test() {
+ var simpleURI = NetUtil.newURI(SIMPLEURI_SPEC);
+ var fileDataURI = NetUtil.newURI(BLOBURI_SPEC);
+
+ do_info("Checking that " + SIMPLEURI_SPEC + " != " + BLOBURI_SPEC);
+ do_check_uri_neq(simpleURI, fileDataURI);
+
+ do_info("Changing the nsSimpleURI spec to match the nsFileDataURI");
+ simpleURI = simpleURI
+ .mutate()
+ .setSpec(BLOBURI_SPEC)
+ .finalize();
+
+ do_info("Verifying that .spec matches");
+ Assert.equal(simpleURI.spec, fileDataURI.spec);
+
+ do_info(
+ "Checking that nsSimpleURI != nsFileDataURI despite their .spec matching"
+ );
+ do_check_uri_neq(simpleURI, fileDataURI);
+}
diff --git a/netwerk/test/unit/test_bug667087.js b/netwerk/test/unit/test_bug667087.js
new file mode 100644
index 0000000000..b3d87402e1
--- /dev/null
+++ b/netwerk/test/unit/test_bug667087.js
@@ -0,0 +1,32 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async () => {
+ var cm = Cc["@mozilla.org/cookiemanager;1"].getService(Ci.nsICookieManager);
+ var expiry = (Date.now() + 1000) * 1000;
+
+ // Test our handling of host names with a single character consisting only
+ // of a single character
+ cm.add(
+ "a",
+ "/",
+ "foo",
+ "bar",
+ false,
+ false,
+ true,
+ expiry,
+ {},
+ Ci.nsICookie.SAMESITE_NONE,
+ Ci.nsICookie.SCHEME_HTTP
+ );
+ Assert.equal(cm.countCookiesFromHost("a"), 1);
+
+ CookieXPCShellUtils.createServer({ hosts: ["a"] });
+ const cookies = await CookieXPCShellUtils.getCookieStringFromDocument(
+ "http://a/"
+ );
+ Assert.equal(cookies, "foo=bar");
+});
diff --git a/netwerk/test/unit/test_bug667818.js b/netwerk/test/unit/test_bug667818.js
new file mode 100644
index 0000000000..1ec185c832
--- /dev/null
+++ b/netwerk/test/unit/test_bug667818.js
@@ -0,0 +1,50 @@
+"use strict";
+
+function makeURI(str) {
+ return Cc["@mozilla.org/network/io-service;1"]
+ .getService(Ci.nsIIOService)
+ .newURI(str);
+}
+
+add_task(async () => {
+ // Allow all cookies.
+ Services.prefs.setIntPref("network.cookie.cookieBehavior", 0);
+ Services.prefs.setBoolPref(
+ "network.cookieJarSettings.unblocked_for_testing",
+ true
+ );
+
+ var serv = Cc["@mozilla.org/cookieService;1"].getService(Ci.nsICookieService);
+ var uri = makeURI("http://example.com/");
+ var channel = NetUtil.newChannel({
+ uri,
+ loadUsingSystemPrincipal: true,
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT,
+ });
+ Services.scriptSecurityManager.createContentPrincipal(uri, {});
+
+ CookieXPCShellUtils.createServer({ hosts: ["example.com"] });
+
+ // Try an expiration time before the epoch
+
+ await CookieXPCShellUtils.setCookieToDocument(
+ uri.spec,
+ "test=test; path=/; domain=example.com; expires=Sun, 31-Dec-1899 16:00:00 GMT;"
+ );
+ Assert.equal(
+ await CookieXPCShellUtils.getCookieStringFromDocument(uri.spec),
+ ""
+ );
+
+ // Now sanity check
+ serv.setCookieStringFromHttp(
+ uri,
+ "test2=test2; path=/; domain=example.com;",
+ channel
+ );
+
+ Assert.equal(
+ await CookieXPCShellUtils.getCookieStringFromDocument(uri.spec),
+ "test2=test2"
+ );
+});
diff --git a/netwerk/test/unit/test_bug667907.js b/netwerk/test/unit/test_bug667907.js
new file mode 100644
index 0000000000..689dcd0886
--- /dev/null
+++ b/netwerk/test/unit/test_bug667907.js
@@ -0,0 +1,86 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+var httpserver = null;
+var simplePath = "/simple";
+var normalPath = "/normal";
+var httpbody = "<html></html>";
+
+XPCOMUtils.defineLazyGetter(this, "uri1", function() {
+ return "http://localhost:" + httpserver.identity.primaryPort + simplePath;
+});
+
+XPCOMUtils.defineLazyGetter(this, "uri2", function() {
+ return "http://localhost:" + httpserver.identity.primaryPort + normalPath;
+});
+
+function make_channel(url) {
+ return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true });
+}
+
+var listener_proto = {
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIStreamListener",
+ "nsIRequestObserver",
+ ]),
+
+ onStartRequest(request) {
+ Assert.equal(
+ request.QueryInterface(Ci.nsIChannel).contentType,
+ this.contentType
+ );
+ request.cancel(Cr.NS_BINDING_ABORTED);
+ },
+
+ onDataAvailable(request, stream, offset, count) {
+ do_throw("Unexpected onDataAvailable");
+ },
+
+ onStopRequest(request, status) {
+ Assert.equal(status, Cr.NS_BINDING_ABORTED);
+ this.termination_func();
+ },
+};
+
+function listener(contentType, termination_func) {
+ this.contentType = contentType;
+ this.termination_func = termination_func;
+}
+listener.prototype = listener_proto;
+
+function run_test() {
+ httpserver = new HttpServer();
+ httpserver.registerPathHandler(simplePath, simpleHandler);
+ httpserver.registerPathHandler(normalPath, normalHandler);
+ httpserver.start(-1);
+
+ var channel = make_channel(uri1);
+ channel.asyncOpen(
+ new listener("text/plain", function() {
+ run_test2();
+ })
+ );
+
+ do_test_pending();
+}
+
+function run_test2() {
+ var channel = make_channel(uri2);
+ channel.asyncOpen(
+ new listener("text/html", function() {
+ httpserver.stop(do_test_finished);
+ })
+ );
+}
+
+function simpleHandler(metadata, response) {
+ response.seizePower();
+ response.bodyOutputStream.write(httpbody, httpbody.length);
+ response.finish();
+}
+
+function normalHandler(metadata, response) {
+ response.bodyOutputStream.write(httpbody, httpbody.length);
+ response.finish();
+}
diff --git a/netwerk/test/unit/test_bug669001.js b/netwerk/test/unit/test_bug669001.js
new file mode 100644
index 0000000000..bb16f0a5d7
--- /dev/null
+++ b/netwerk/test/unit/test_bug669001.js
@@ -0,0 +1,179 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+var httpServer = null;
+var path = "/bug699001";
+
+XPCOMUtils.defineLazyGetter(this, "URI", function() {
+ return "http://localhost:" + httpServer.identity.primaryPort + path;
+});
+
+function make_channel(url) {
+ return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true });
+}
+
+var fetched;
+
+// The test loads a resource that expires in one year, has an etag and varies only by User-Agent
+// First we load it, then check we load it only from the cache w/o even checking with the server
+// Then we modify our User-Agent and try it again
+// We have to get a new content (even though with the same etag) and again on next load only from
+// cache w/o accessing the server
+// Goal is to check we've updated User-Agent request header in cache after we've got 304 response
+// from the server
+
+var tests = [
+ {
+ prepare() {},
+ test(response) {
+ Assert.ok(fetched);
+ },
+ },
+ {
+ prepare() {},
+ test(response) {
+ Assert.ok(!fetched);
+ },
+ },
+ {
+ prepare() {
+ setUA("A different User Agent");
+ },
+ test(response) {
+ Assert.ok(fetched);
+ },
+ },
+ {
+ prepare() {},
+ test(response) {
+ Assert.ok(!fetched);
+ },
+ },
+ {
+ prepare() {
+ setUA("And another User Agent");
+ },
+ test(response) {
+ Assert.ok(fetched);
+ },
+ },
+ {
+ prepare() {},
+ test(response) {
+ Assert.ok(!fetched);
+ },
+ },
+];
+
+function handler(metadata, response) {
+ if (metadata.hasHeader("If-None-Match")) {
+ response.setStatusLine(metadata.httpVersion, 304, "Not modified");
+ } else {
+ response.setStatusLine(metadata.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "text/plain");
+
+ var body = "body";
+ response.bodyOutputStream.write(body, body.length);
+ }
+
+ fetched = true;
+
+ response.setHeader("Expires", getDateString(+1));
+ response.setHeader("Cache-Control", "private");
+ response.setHeader("Vary", "User-Agent");
+ response.setHeader("ETag", "1234");
+}
+
+function run_test() {
+ httpServer = new HttpServer();
+ httpServer.registerPathHandler(path, handler);
+ httpServer.start(-1);
+
+ do_test_pending();
+
+ nextTest();
+}
+
+function nextTest() {
+ fetched = false;
+ tests[0].prepare();
+
+ dump("Testing with User-Agent: " + getUA() + "\n");
+ var chan = make_channel(URI);
+
+ // Give the old channel a chance to close the cache entry first.
+ // XXX This is actually a race condition that might be considered a bug...
+ executeSoon(function() {
+ chan.asyncOpen(new ChannelListener(checkAndShiftTest, null));
+ });
+}
+
+function checkAndShiftTest(request, response) {
+ tests[0].test(response);
+
+ tests.shift();
+ if (tests.length == 0) {
+ httpServer.stop(tearDown);
+ return;
+ }
+
+ nextTest();
+}
+
+function tearDown() {
+ setUA("");
+ do_test_finished();
+}
+
+// Helpers
+
+function getUA() {
+ var httphandler = Cc["@mozilla.org/network/protocol;1?name=http"].getService(
+ Ci.nsIHttpProtocolHandler
+ );
+ return httphandler.userAgent;
+}
+
+function setUA(value) {
+ var prefs = Cc["@mozilla.org/preferences-service;1"].getService(
+ Ci.nsIPrefBranch
+ );
+ prefs.setCharPref("general.useragent.override", value);
+}
+
+function getDateString(yearDelta) {
+ var months = [
+ "Jan",
+ "Feb",
+ "Mar",
+ "Apr",
+ "May",
+ "Jun",
+ "Jul",
+ "Aug",
+ "Sep",
+ "Oct",
+ "Nov",
+ "Dec",
+ ];
+ var days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
+
+ var d = new Date();
+ return (
+ days[d.getUTCDay()] +
+ ", " +
+ d.getUTCDate() +
+ " " +
+ months[d.getUTCMonth()] +
+ " " +
+ (d.getUTCFullYear() + yearDelta) +
+ " " +
+ d.getUTCHours() +
+ ":" +
+ d.getUTCMinutes() +
+ ":" +
+ d.getUTCSeconds() +
+ " UTC"
+ );
+}
diff --git a/netwerk/test/unit/test_bug767025.js b/netwerk/test/unit/test_bug767025.js
new file mode 100644
index 0000000000..cc10d85ae2
--- /dev/null
+++ b/netwerk/test/unit/test_bug767025.js
@@ -0,0 +1,315 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+const { PermissionTestUtils } = ChromeUtils.import(
+ "resource://testing-common/PermissionTestUtils.jsm"
+);
+
+/**
+ * This testcase does the following steps to make sure that bug767025 removes
+ * files as expected.
+ *
+ * STEPS:
+ * - Schedule a offline cache update for app.manifest.
+ * - pages/foo1, pages/foo2, pages/foo3, and pages/foo4 are cached.
+ * - Activate pages/foo1
+ * - Doom pages/foo1, and pages/foo2.
+ * - pages/foo1 should keep alive while pages/foo2 was gone.
+ * - Activate pages/foo3
+ * - Evict all documents.
+ * - all documents except pages/foo1 are gone since pages/foo1 & pages/foo3
+ * are activated.
+ */
+
+const kNS_OFFLINECACHEUPDATESERVICE_CONTRACTID =
+ "@mozilla.org/offlinecacheupdate-service;1";
+const kNS_CACHESTORAGESERVICE_CONTRACTID =
+ "@mozilla.org/netwerk/cache-storage-service;1";
+const kNS_APPLICATIONCACHESERVICE_CONTRACTID =
+ "@mozilla.org/network/application-cache-service;1";
+
+const kManifest =
+ "CACHE MANIFEST\n" +
+ "/pages/foo1\n" +
+ "/pages/foo2\n" +
+ "/pages/foo3\n" +
+ "/pages/foo4\n";
+
+const kDataFileSize = 1024; // file size for each content page
+const kHttpLocation = "http://localhost:4444/";
+
+function manifest_handler(metadata, response) {
+ info("manifest\n");
+ response.setHeader("content-type", "text/cache-manifest");
+
+ response.write(kManifest);
+}
+
+function datafile_handler(metadata, response) {
+ info("datafile_handler\n");
+ let data = "";
+
+ while (data.length < kDataFileSize) {
+ data =
+ data +
+ Math.random()
+ .toString(36)
+ .substring(2, 15);
+ }
+
+ response.setHeader("content-type", "text/plain");
+ response.write(data.substring(0, kDataFileSize));
+}
+
+function app_handler(metadata, response) {
+ info("app_handler\n");
+ response.setHeader("content-type", "text/html");
+
+ response.write("<html></html>");
+}
+
+var httpServer;
+
+function init_profile() {
+ var ps = Cc["@mozilla.org/preferences-service;1"].getService(
+ Ci.nsIPrefBranch
+ );
+ dump(ps.getBoolPref("browser.cache.offline.enable"));
+ ps.setBoolPref("browser.cache.offline.enable", true);
+ ps.setBoolPref("browser.cache.offline.storage.enable", true);
+ ps.setComplexValue(
+ "browser.cache.offline.parent_directory",
+ Ci.nsIFile,
+ do_get_profile()
+ );
+ info("profile " + do_get_profile());
+}
+
+function init_http_server() {
+ httpServer = new HttpServer();
+ httpServer.registerPathHandler("/app.appcache", manifest_handler);
+ httpServer.registerPathHandler("/app", app_handler);
+ for (let i = 1; i <= 4; i++) {
+ httpServer.registerPathHandler("/pages/foo" + i, datafile_handler);
+ }
+ httpServer.start(4444);
+}
+
+function clean_app_cache() {
+ let cache_service = Cc[kNS_CACHESTORAGESERVICE_CONTRACTID].getService(
+ Ci.nsICacheStorageService
+ );
+ let storage = cache_service.appCacheStorage(
+ Services.loadContextInfo.default,
+ null
+ );
+ storage.asyncEvictStorage(null);
+}
+
+function do_app_cache(manifestURL, pageURL) {
+ let update_service = Cc[kNS_OFFLINECACHEUPDATESERVICE_CONTRACTID].getService(
+ Ci.nsIOfflineCacheUpdateService
+ );
+
+ PermissionTestUtils.add(
+ manifestURL,
+ "offline-app",
+ Ci.nsIPermissionManager.ALLOW_ACTION
+ );
+
+ let update = update_service.scheduleUpdate(
+ manifestURL,
+ pageURL,
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ null
+ ); /* no window */
+
+ return update;
+}
+
+function watch_update(update, stateChangeHandler, cacheAvailHandler) {
+ let observer = {
+ QueryInterface: ChromeUtils.generateQI([]),
+
+ updateStateChanged: stateChangeHandler,
+ applicationCacheAvailable: cacheAvailHandler,
+ };
+ ~update.addObserver(observer);
+
+ return update;
+}
+
+function start_and_watch_app_cache(
+ manifestURL,
+ pageURL,
+ stateChangeHandler,
+ cacheAvailHandler
+) {
+ let ioService = Cc["@mozilla.org/network/io-service;1"].getService(
+ Ci.nsIIOService
+ );
+ let update = do_app_cache(
+ ioService.newURI(manifestURL),
+ ioService.newURI(pageURL)
+ );
+ watch_update(update, stateChangeHandler, cacheAvailHandler);
+ return update;
+}
+
+const {
+ STATE_FINISHED: STATE_FINISHED,
+ STATE_CHECKING: STATE_CHECKING,
+ STATE_ERROR: STATE_ERROR,
+} = Ci.nsIOfflineCacheUpdateObserver;
+
+/*
+ * Start caching app1 as a non-pinned app.
+ */
+function start_cache_nonpinned_app() {
+ info("Start non-pinned App1");
+ start_and_watch_app_cache(
+ kHttpLocation + "app.appcache",
+ kHttpLocation + "app",
+ function(update, state) {
+ switch (state) {
+ case STATE_FINISHED:
+ check_bug();
+ break;
+
+ case STATE_ERROR:
+ do_throw("App cache state = " + state);
+ break;
+ }
+ },
+ function(appcache) {
+ info("app avail " + appcache + "\n");
+ }
+ );
+}
+
+var hold_entry_foo1 = null;
+
+function check_bug() {
+ // activate foo1
+ asyncOpenCacheEntry(
+ kHttpLocation + "pages/foo1",
+ "appcache",
+ Ci.nsICacheStorage.OPEN_READONLY,
+ null,
+ function(status, entry, appcache) {
+ let storage = get_cache_service().appCacheStorage(
+ Services.loadContextInfo.default,
+ appcache
+ );
+
+ // Doom foo1 & foo2
+ storage.asyncDoomURI(createURI(kHttpLocation + "pages/foo1"), "", {
+ onCacheEntryDoomed() {
+ storage.asyncDoomURI(createURI(kHttpLocation + "pages/foo2"), "", {
+ onCacheEntryDoomed() {
+ check_evict_cache(appcache);
+ },
+ });
+ },
+ });
+
+ hold_entry_foo1 = entry;
+ }
+ );
+}
+
+function check_evict_cache(appcache) {
+ // Only foo2 should be removed.
+ let file = do_get_profile().clone();
+ file.append("OfflineCache");
+ file.append("5");
+ file.append("9");
+ file.append("8379C6596B8CA4-0");
+ Assert.equal(file.exists(), true);
+
+ file = do_get_profile().clone();
+ file.append("OfflineCache");
+ file.append("C");
+ file.append("2");
+ file.append("5F356A168B5E3B-0");
+ Assert.equal(file.exists(), false);
+
+ // activate foo3
+ asyncOpenCacheEntry(
+ kHttpLocation + "pages/foo3",
+ "appcache",
+ Ci.nsICacheStorage.OPEN_READONLY,
+ null,
+ function(status, entry, appcache) {
+ // evict all documents.
+ let storage = get_cache_service().appCacheStorage(
+ Services.loadContextInfo.default,
+ appcache
+ );
+ storage.asyncEvictStorage(null);
+
+ // All documents are removed except foo1 & foo3.
+ syncWithCacheIOThread(function() {
+ // foo1
+ let file = do_get_profile().clone();
+ file.append("OfflineCache");
+ file.append("5");
+ file.append("9");
+ file.append("8379C6596B8CA4-0");
+ Assert.equal(file.exists(), true);
+
+ file = do_get_profile().clone();
+ file.append("OfflineCache");
+ file.append("0");
+ file.append("0");
+ file.append("61FEE819921D39-0");
+ Assert.equal(file.exists(), false);
+
+ file = do_get_profile().clone();
+ file.append("OfflineCache");
+ file.append("3");
+ file.append("9");
+ file.append("0D8759F1DE5452-0");
+ Assert.equal(file.exists(), false);
+
+ file = do_get_profile().clone();
+ file.append("OfflineCache");
+ file.append("C");
+ file.append("2");
+ file.append("5F356A168B5E3B-0");
+ Assert.equal(file.exists(), false);
+
+ // foo3
+ file = do_get_profile().clone();
+ file.append("OfflineCache");
+ file.append("D");
+ file.append("C");
+ file.append("1ADCCC843B5C00-0");
+ Assert.equal(file.exists(), true);
+
+ file = do_get_profile().clone();
+ file.append("OfflineCache");
+ file.append("F");
+ file.append("0");
+ file.append("FC3E6D6C1164E9-0");
+ Assert.equal(file.exists(), false);
+
+ httpServer.stop(do_test_finished);
+ }, true /* force even with the new cache back end */);
+ },
+ appcache
+ );
+}
+
+function run_test() {
+ if (typeof _XPCSHELL_PROCESS == "undefined" || _XPCSHELL_PROCESS != "child") {
+ init_profile();
+ clean_app_cache();
+ }
+
+ init_http_server();
+ start_cache_nonpinned_app();
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_bug770243.js b/netwerk/test/unit/test_bug770243.js
new file mode 100644
index 0000000000..5c79d6b1c1
--- /dev/null
+++ b/netwerk/test/unit/test_bug770243.js
@@ -0,0 +1,246 @@
+/* this test does the following:
+ Always requests the same resource, while for each request getting:
+ 1. 200 + ETag: "one"
+ 2. 401 followed by 200 + ETag: "two"
+ 3. 401 followed by 304
+ 4. 407 followed by 200 + ETag: "three"
+ 5. 407 followed by 304
+*/
+
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+var httpserv;
+
+function addCreds(scheme, host) {
+ var authMgr = Cc["@mozilla.org/network/http-auth-manager;1"].getService(
+ Ci.nsIHttpAuthManager
+ );
+ authMgr.setAuthIdentity(
+ scheme,
+ host,
+ httpserv.identity.primaryPort,
+ "basic",
+ "secret",
+ "/",
+ "",
+ "user",
+ "pass"
+ );
+}
+
+function clearCreds() {
+ var authMgr = Cc["@mozilla.org/network/http-auth-manager;1"].getService(
+ Ci.nsIHttpAuthManager
+ );
+ authMgr.clearAll();
+}
+
+function makeChan() {
+ return NetUtil.newChannel({
+ uri: "http://localhost:" + httpserv.identity.primaryPort + "/",
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+}
+
+// Array of handlers that are called one by one in response to expected requests
+
+var handlers = [
+ // Test 1
+ function(metadata, response) {
+ Assert.equal(metadata.hasHeader("Authorization"), false);
+ response.setStatusLine(metadata.httpVersion, 200, "OK");
+ response.setHeader("ETag", '"one"', false);
+ response.setHeader("Cache-control", "no-cache", false);
+ response.setHeader("Content-type", "text/plain", false);
+ var body = "Response body 1";
+ response.bodyOutputStream.write(body, body.length);
+ },
+
+ // Test 2
+ function(metadata, response) {
+ Assert.equal(metadata.hasHeader("Authorization"), false);
+ Assert.equal(metadata.getHeader("If-None-Match"), '"one"');
+ response.setStatusLine(metadata.httpVersion, 401, "Authenticate");
+ response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false);
+ addCreds("http", "localhost");
+ },
+ function(metadata, response) {
+ Assert.equal(metadata.hasHeader("Authorization"), true);
+ response.setStatusLine(metadata.httpVersion, 200, "OK");
+ response.setHeader("ETag", '"two"', false);
+ response.setHeader("Cache-control", "no-cache", false);
+ response.setHeader("Content-type", "text/plain", false);
+ var body = "Response body 2";
+ response.bodyOutputStream.write(body, body.length);
+ clearCreds();
+ },
+
+ // Test 3
+ function(metadata, response) {
+ Assert.equal(metadata.hasHeader("Authorization"), false);
+ Assert.equal(metadata.getHeader("If-None-Match"), '"two"');
+ response.setStatusLine(metadata.httpVersion, 401, "Authenticate");
+ response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false);
+ addCreds("http", "localhost");
+ },
+ function(metadata, response) {
+ Assert.equal(metadata.hasHeader("Authorization"), true);
+ Assert.equal(metadata.getHeader("If-None-Match"), '"two"');
+ response.setStatusLine(metadata.httpVersion, 304, "OK");
+ response.setHeader("ETag", '"two"', false);
+ clearCreds();
+ },
+
+ // Test 4
+ function(metadata, response) {
+ Assert.equal(metadata.hasHeader("Authorization"), false);
+ Assert.equal(metadata.getHeader("If-None-Match"), '"two"');
+ response.setStatusLine(metadata.httpVersion, 407, "Proxy Authenticate");
+ response.setHeader("Proxy-Authenticate", 'Basic realm="secret"', false);
+ addCreds("http", "localhost");
+ },
+ function(metadata, response) {
+ Assert.equal(metadata.hasHeader("Proxy-Authorization"), true);
+ Assert.equal(metadata.getHeader("If-None-Match"), '"two"');
+ response.setStatusLine(metadata.httpVersion, 200, "OK");
+ response.setHeader("ETag", '"three"', false);
+ response.setHeader("Cache-control", "no-cache", false);
+ response.setHeader("Content-type", "text/plain", false);
+ var body = "Response body 3";
+ response.bodyOutputStream.write(body, body.length);
+ clearCreds();
+ },
+
+ // Test 5
+ function(metadata, response) {
+ Assert.equal(metadata.hasHeader("Proxy-Authorization"), false);
+ Assert.equal(metadata.getHeader("If-None-Match"), '"three"');
+ response.setStatusLine(metadata.httpVersion, 407, "Proxy Authenticate");
+ response.setHeader("Proxy-Authenticate", 'Basic realm="secret"', false);
+ addCreds("http", "localhost");
+ },
+ function(metadata, response) {
+ Assert.equal(metadata.hasHeader("Proxy-Authorization"), true);
+ Assert.equal(metadata.getHeader("If-None-Match"), '"three"');
+ response.setStatusLine(metadata.httpVersion, 304, "OK");
+ response.setHeader("ETag", '"three"', false);
+ response.setHeader("Cache-control", "no-cache", false);
+ clearCreds();
+ },
+];
+
+function handler(metadata, response) {
+ handlers.shift()(metadata, response);
+}
+
+// Array of tests to run, self-driven
+
+function sync_and_run_next_test() {
+ syncWithCacheIOThread(function() {
+ tests.shift()();
+ });
+}
+
+var tests = [
+ // Test 1: 200 (cacheable)
+ function() {
+ var ch = makeChan();
+ ch.asyncOpen(
+ new ChannelListener(
+ function(req, body) {
+ Assert.equal(body, "Response body 1");
+ sync_and_run_next_test();
+ },
+ null,
+ CL_NOT_FROM_CACHE
+ )
+ );
+ },
+
+ // Test 2: 401 and 200 + new content
+ function() {
+ var ch = makeChan();
+ ch.asyncOpen(
+ new ChannelListener(
+ function(req, body) {
+ Assert.equal(body, "Response body 2");
+ sync_and_run_next_test();
+ },
+ null,
+ CL_NOT_FROM_CACHE
+ )
+ );
+ },
+
+ // Test 3: 401 and 304
+ function() {
+ var ch = makeChan();
+ ch.asyncOpen(
+ new ChannelListener(
+ function(req, body) {
+ Assert.equal(body, "Response body 2");
+ sync_and_run_next_test();
+ },
+ null,
+ CL_FROM_CACHE
+ )
+ );
+ },
+
+ // Test 4: 407 and 200 + new content
+ function() {
+ var ch = makeChan();
+ ch.asyncOpen(
+ new ChannelListener(
+ function(req, body) {
+ Assert.equal(body, "Response body 3");
+ sync_and_run_next_test();
+ },
+ null,
+ CL_NOT_FROM_CACHE
+ )
+ );
+ },
+
+ // Test 5: 407 and 304
+ function() {
+ var ch = makeChan();
+ ch.asyncOpen(
+ new ChannelListener(
+ function(req, body) {
+ Assert.equal(body, "Response body 3");
+ sync_and_run_next_test();
+ },
+ null,
+ CL_FROM_CACHE
+ )
+ );
+ },
+
+ // End of test run
+ function() {
+ httpserv.stop(do_test_finished);
+ },
+];
+
+function run_test() {
+ do_get_profile();
+
+ httpserv = new HttpServer();
+ httpserv.registerPathHandler("/", handler);
+ httpserv.start(-1);
+
+ const prefs = Cc["@mozilla.org/preferences-service;1"].getService(
+ Ci.nsIPrefBranch
+ );
+ prefs.setCharPref("network.proxy.http", "localhost");
+ prefs.setIntPref("network.proxy.http_port", httpserv.identity.primaryPort);
+ prefs.setBoolPref("network.proxy.allow_hijacking_localhost", true);
+ prefs.setIntPref("network.proxy.type", 1);
+ prefs.setBoolPref("network.http.rcwn.enabled", false);
+
+ tests.shift()();
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_bug812167.js b/netwerk/test/unit/test_bug812167.js
new file mode 100644
index 0000000000..29bf894698
--- /dev/null
+++ b/netwerk/test/unit/test_bug812167.js
@@ -0,0 +1,141 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+/*
+- get 302 with Cache-control: no-store
+- check cache entry for the 302 response is cached only in memory device
+- get 302 with Expires: -1
+- check cache entry for the 302 response is not cached at all
+*/
+
+var httpserver = null;
+// Need to randomize, because apparently no one clears our cache
+var randomPath1 = "/redirect-no-store/" + Math.random();
+
+XPCOMUtils.defineLazyGetter(this, "randomURI1", function() {
+ return "http://localhost:" + httpserver.identity.primaryPort + randomPath1;
+});
+
+var randomPath2 = "/redirect-expires-past/" + Math.random();
+
+XPCOMUtils.defineLazyGetter(this, "randomURI2", function() {
+ return "http://localhost:" + httpserver.identity.primaryPort + randomPath2;
+});
+
+function make_channel(url, callback, ctx) {
+ return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true });
+}
+
+const responseBody = "response body";
+
+var redirectHandler_NoStore_calls = 0;
+function redirectHandler_NoStore(metadata, response) {
+ response.setStatusLine(metadata.httpVersion, 302, "Found");
+ response.setHeader(
+ "Location",
+ "http://localhost:" + httpserver.identity.primaryPort + "/content",
+ false
+ );
+ response.setHeader("Cache-control", "no-store");
+ ++redirectHandler_NoStore_calls;
+}
+
+var redirectHandler_ExpiresInPast_calls = 0;
+function redirectHandler_ExpiresInPast(metadata, response) {
+ response.setStatusLine(metadata.httpVersion, 302, "Found");
+ response.setHeader(
+ "Location",
+ "http://localhost:" + httpserver.identity.primaryPort + "/content",
+ false
+ );
+ response.setHeader("Expires", "-1");
+ ++redirectHandler_ExpiresInPast_calls;
+}
+
+function contentHandler(metadata, response) {
+ response.setHeader("Content-Type", "text/plain");
+ response.bodyOutputStream.write(responseBody, responseBody.length);
+}
+
+function check_response(
+ path,
+ request,
+ buffer,
+ expectedExpiration,
+ continuation
+) {
+ Assert.equal(buffer, responseBody);
+
+ // Entry is always there, old cache wrapping code does session->SetDoomEntriesIfExpired(false),
+ // just check it's not persisted or is expired (dep on the test).
+ asyncOpenCacheEntry(
+ path,
+ "disk",
+ Ci.nsICacheStorage.OPEN_READONLY,
+ null,
+ function(status, entry) {
+ Assert.equal(status, 0);
+
+ // Expired entry is on disk, no-store entry is in memory
+ Assert.equal(entry.persistent, expectedExpiration);
+
+ // Do the request again and check the server handler is called appropriately
+ var chan = make_channel(path);
+ chan.asyncOpen(
+ new ChannelListener(function(request, buffer) {
+ Assert.equal(buffer, responseBody);
+
+ if (expectedExpiration) {
+ // Handler had to be called second time
+ Assert.equal(redirectHandler_ExpiresInPast_calls, 2);
+ } else {
+ // Handler had to be called second time (no-store forces validate),
+ // and we are just in memory
+ Assert.equal(redirectHandler_NoStore_calls, 2);
+ Assert.ok(!entry.persistent);
+ }
+
+ continuation();
+ }, null)
+ );
+ }
+ );
+}
+
+function run_test_no_store() {
+ var chan = make_channel(randomURI1);
+ chan.asyncOpen(
+ new ChannelListener(function(request, buffer) {
+ // Cache-control: no-store response should only be found in the memory cache.
+ check_response(randomURI1, request, buffer, false, run_test_expires_past);
+ }, null)
+ );
+}
+
+function run_test_expires_past() {
+ var chan = make_channel(randomURI2);
+ chan.asyncOpen(
+ new ChannelListener(function(request, buffer) {
+ // Expires: -1 response should not be found in any cache.
+ check_response(randomURI2, request, buffer, true, finish_test);
+ }, null)
+ );
+}
+
+function finish_test() {
+ httpserver.stop(do_test_finished);
+}
+
+function run_test() {
+ do_get_profile();
+
+ httpserver = new HttpServer();
+ httpserver.registerPathHandler(randomPath1, redirectHandler_NoStore);
+ httpserver.registerPathHandler(randomPath2, redirectHandler_ExpiresInPast);
+ httpserver.registerPathHandler("/content", contentHandler);
+ httpserver.start(-1);
+
+ run_test_no_store();
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_bug826063.js b/netwerk/test/unit/test_bug826063.js
new file mode 100644
index 0000000000..54e5d79a96
--- /dev/null
+++ b/netwerk/test/unit/test_bug826063.js
@@ -0,0 +1,91 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test that nsIPrivateBrowsingChannel.isChannelPrivate yields the correct
+ * result for various combinations of .setPrivate() and nsILoadContexts
+ */
+
+"use strict";
+
+var URIs = ["http://example.org", "https://example.org"];
+if (Services.prefs.getBoolPref("network.ftp.enabled")) {
+ URIs.push("ftp://example.org");
+}
+
+function* getChannels() {
+ for (let u of URIs) {
+ yield NetUtil.newChannel({
+ uri: u,
+ loadUsingSystemPrincipal: true,
+ });
+ }
+}
+
+function checkPrivate(channel, shouldBePrivate) {
+ Assert.equal(
+ channel.QueryInterface(Ci.nsIPrivateBrowsingChannel).isChannelPrivate,
+ shouldBePrivate
+ );
+}
+
+/**
+ * Default configuration
+ * Default is non-private
+ */
+add_test(function test_plain() {
+ for (let c of getChannels()) {
+ checkPrivate(c, false);
+ }
+ run_next_test();
+});
+
+/**
+ * Explicitly setPrivate(true), no load context
+ */
+add_test(function test_setPrivate_private() {
+ for (let c of getChannels()) {
+ c.QueryInterface(Ci.nsIPrivateBrowsingChannel).setPrivate(true);
+ checkPrivate(c, true);
+ }
+ run_next_test();
+});
+
+/**
+ * Explicitly setPrivate(false), no load context
+ */
+add_test(function test_setPrivate_regular() {
+ for (let c of getChannels()) {
+ c.QueryInterface(Ci.nsIPrivateBrowsingChannel).setPrivate(false);
+ checkPrivate(c, false);
+ }
+ run_next_test();
+});
+
+/**
+ * Load context mandates private mode
+ */
+add_test(function test_LoadContextPrivate() {
+ let ctx = Cu.createPrivateLoadContext();
+ for (let c of getChannels()) {
+ c.notificationCallbacks = ctx;
+ checkPrivate(c, true);
+ }
+ run_next_test();
+});
+
+/**
+ * Load context mandates regular mode
+ */
+add_test(function test_LoadContextRegular() {
+ let ctx = Cu.createLoadContext();
+ for (let c of getChannels()) {
+ c.notificationCallbacks = ctx;
+ checkPrivate(c, false);
+ }
+ run_next_test();
+});
+
+// Do not test simultanous uses of .setPrivate and load context.
+// There is little merit in doing so, and combining both will assert in
+// Debug builds anyway.
diff --git a/netwerk/test/unit/test_bug856978.js b/netwerk/test/unit/test_bug856978.js
new file mode 100644
index 0000000000..071366e66f
--- /dev/null
+++ b/netwerk/test/unit/test_bug856978.js
@@ -0,0 +1,135 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// This test makes sure that the authorization header can get deleted e.g. by
+// extensions if they are observing "http-on-modify-request". In a first step
+// the auth cache is filled with credentials which then get added to the
+// following request. On "http-on-modify-request" it is tested whether the
+// authorization header got added at all and if so it gets removed. This test
+// passes iff both succeeds.
+
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+var notification = "http-on-modify-request";
+
+var httpServer = null;
+
+var authCredentials = "guest:guest";
+var authPath = "/authTest";
+var authCredsURL = "http://" + authCredentials + "@localhost:8888" + authPath;
+var authURL = "http://localhost:8888" + authPath;
+
+function authHandler(metadata, response) {
+ if (metadata.hasHeader("Test")) {
+ // Lets see if the auth header got deleted.
+ var noAuthHeader = false;
+ if (!metadata.hasHeader("Authorization")) {
+ noAuthHeader = true;
+ }
+ Assert.ok(noAuthHeader);
+ }
+ // Not our test request yet.
+ else if (!metadata.hasHeader("Authorization")) {
+ response.setStatusLine(metadata.httpVersion, 401, "Unauthorized");
+ response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false);
+ }
+}
+
+function RequestObserver() {
+ this.register();
+}
+
+RequestObserver.prototype = {
+ register() {
+ info("Registering " + notification);
+ Cc["@mozilla.org/observer-service;1"]
+ .getService(Ci.nsIObserverService)
+ .addObserver(this, notification, true);
+ },
+
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIObserver",
+ "nsISupportsWeakReference",
+ ]),
+
+ observe(subject, topic, data) {
+ if (topic == notification) {
+ if (!(subject instanceof Ci.nsIHttpChannel)) {
+ do_throw(notification + " observed a non-HTTP channel.");
+ }
+ try {
+ subject.getRequestHeader("Authorization");
+ } catch (e) {
+ // Throw if there is no header to delete. We should get one iff caching
+ // the auth credentials is working and the header gets added _before_
+ // "http-on-modify-request" gets called.
+ httpServer.stop(do_test_finished);
+ do_throw("No authorization header found, aborting!");
+ }
+ // We are still here. Let's remove the authorization header now.
+ subject.setRequestHeader("Authorization", null, false);
+ }
+ },
+};
+
+var listener = {
+ onStartRequest: function test_onStartR(request) {},
+
+ onDataAvailable: function test_ODA() {
+ do_throw("Should not get any data!");
+ },
+
+ onStopRequest: function test_onStopR(request, status) {
+ if (current_test < tests.length - 1) {
+ current_test++;
+ tests[current_test]();
+ } else {
+ do_test_pending();
+ httpServer.stop(do_test_finished);
+ }
+ do_test_finished();
+ },
+};
+
+function makeChan(url) {
+ return NetUtil.newChannel({
+ uri: url,
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+}
+
+var tests = [startAuthHeaderTest, removeAuthHeaderTest];
+
+var current_test = 0;
+
+var requestObserver = null;
+
+function run_test() {
+ httpServer = new HttpServer();
+ httpServer.registerPathHandler(authPath, authHandler);
+ httpServer.start(8888);
+
+ tests[0]();
+}
+
+function startAuthHeaderTest() {
+ var chan = makeChan(authCredsURL);
+ chan.asyncOpen(listener);
+
+ do_test_pending();
+}
+
+function removeAuthHeaderTest() {
+ // After caching the auth credentials in the first test, lets try to remove
+ // the authorization header now...
+ requestObserver = new RequestObserver();
+ var chan = makeChan(authURL);
+ // Indicating that the request is coming from the second test.
+ chan.setRequestHeader("Test", "1", false);
+ chan.asyncOpen(listener);
+
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_bug894586.js b/netwerk/test/unit/test_bug894586.js
new file mode 100644
index 0000000000..a3d1b78c29
--- /dev/null
+++ b/netwerk/test/unit/test_bug894586.js
@@ -0,0 +1,157 @@
+/*
+ * Tests for bug 894586: nsSyncLoadService::PushSyncStreamToListener
+ * should not fail for channels of unknown size
+ */
+
+"use strict";
+
+var contentSecManager = Cc["@mozilla.org/contentsecuritymanager;1"].getService(
+ Ci.nsIContentSecurityManager
+);
+
+function ProtocolHandler() {
+ this.uri = Cc["@mozilla.org/network/simple-uri-mutator;1"]
+ .createInstance(Ci.nsIURIMutator)
+ .setSpec(this.scheme + ":dummy")
+ .finalize();
+}
+
+ProtocolHandler.prototype = {
+ /** nsIProtocolHandler */
+ get scheme() {
+ return "x-bug894586";
+ },
+ get defaultPort() {
+ return -1;
+ },
+ get protocolFlags() {
+ return (
+ Ci.nsIProtocolHandler.URI_NORELATIVE |
+ Ci.nsIProtocolHandler.URI_NOAUTH |
+ Ci.nsIProtocolHandler.URI_IS_UI_RESOURCE |
+ Ci.nsIProtocolHandler.URI_IS_LOCAL_RESOURCE |
+ Ci.nsIProtocolHandler.URI_NON_PERSISTABLE |
+ Ci.nsIProtocolHandler.URI_SYNC_LOAD_IS_OK
+ );
+ },
+ newChannel(aURI, aLoadInfo) {
+ this.loadInfo = aLoadInfo;
+ return this;
+ },
+ allowPort(port, scheme) {
+ return port != -1;
+ },
+
+ /** nsIChannel */
+ get originalURI() {
+ return this.uri;
+ },
+ get URI() {
+ return this.uri;
+ },
+ owner: null,
+ notificationCallbacks: null,
+ get securityInfo() {
+ return null;
+ },
+ get contentType() {
+ return "text/css";
+ },
+ set contentType(val) {},
+ contentCharset: "UTF-8",
+ get contentLength() {
+ return -1;
+ },
+ set contentLength(val) {
+ throw Components.Exception(
+ "Setting content length",
+ Cr.NS_ERROR_NOT_IMPLEMENTED
+ );
+ },
+ open() {
+ // throws an error if security checks fail
+ contentSecManager.performSecurityCheck(this, null);
+
+ var file = do_get_file("test_bug894586.js", false);
+ Assert.ok(file.exists());
+ var url = Services.io.newFileURI(file);
+ return NetUtil.newChannel({
+ uri: url,
+ loadUsingSystemPrincipal: true,
+ }).open();
+ },
+ asyncOpen(aListener, aContext) {
+ throw Components.Exception("Not implemented", Cr.NS_ERROR_NOT_IMPLEMENTED);
+ },
+ contentDisposition: Ci.nsIChannel.DISPOSITION_INLINE,
+ get contentDispositionFilename() {
+ throw Components.Exception("No file name", Cr.NS_ERROR_NOT_AVAILABLE);
+ },
+ get contentDispositionHeader() {
+ throw Components.Exception("No header", Cr.NS_ERROR_NOT_AVAILABLE);
+ },
+
+ /** nsIRequest */
+ get name() {
+ return this.uri.spec;
+ },
+ isPending: () => false,
+ get status() {
+ return Cr.NS_OK;
+ },
+ cancel(status) {},
+ loadGroup: null,
+ loadFlags:
+ Ci.nsIRequest.LOAD_NORMAL |
+ Ci.nsIRequest.INHIBIT_CACHING |
+ Ci.nsIRequest.LOAD_BYPASS_CACHE,
+
+ /** nsIFactory */
+ createInstance(aOuter, aIID) {
+ if (aOuter) {
+ throw Components.Exception(
+ "createInstance no aggregation",
+ Cr.NS_ERROR_NO_AGGREGATION
+ );
+ }
+ return this.QueryInterface(aIID);
+ },
+ lockFactory() {},
+
+ /** nsISupports */
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIProtocolHandler",
+ "nsIRequest",
+ "nsIChannel",
+ "nsIFactory",
+ ]),
+ classID: Components.ID("{16d594bc-d9d8-47ae-a139-ea714dc0c35c}"),
+};
+
+/**
+ * Attempt a sync load; we use the stylesheet service to do this for us,
+ * based on the knowledge that it forces a sync load under the hood.
+ */
+function run_test() {
+ var handler = new ProtocolHandler();
+ var registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
+ registrar.registerFactory(
+ handler.classID,
+ "",
+ "@mozilla.org/network/protocol;1?name=" + handler.scheme,
+ handler
+ );
+ try {
+ var ss = Cc["@mozilla.org/content/style-sheet-service;1"].getService(
+ Ci.nsIStyleSheetService
+ );
+ ss.loadAndRegisterSheet(handler.uri, Ci.nsIStyleSheetService.AGENT_SHEET);
+ Assert.ok(
+ ss.sheetRegistered(handler.uri, Ci.nsIStyleSheetService.AGENT_SHEET)
+ );
+ } finally {
+ registrar.unregisterFactory(handler.classID, handler);
+ }
+}
+
+// vim: set et ts=2 :
diff --git a/netwerk/test/unit/test_bug935499.js b/netwerk/test/unit/test_bug935499.js
new file mode 100644
index 0000000000..2da8168d2d
--- /dev/null
+++ b/netwerk/test/unit/test_bug935499.js
@@ -0,0 +1,10 @@
+"use strict";
+
+function run_test() {
+ var idnService = Cc["@mozilla.org/network/idn-service;1"].getService(
+ Ci.nsIIDNService
+ );
+
+ var isASCII = {};
+ Assert.equal(idnService.convertToDisplayIDN("xn--", isASCII), "xn--");
+}
diff --git a/netwerk/test/unit/test_cache-control_request.js b/netwerk/test/unit/test_cache-control_request.js
new file mode 100644
index 0000000000..501fa7ae87
--- /dev/null
+++ b/netwerk/test/unit/test_cache-control_request.js
@@ -0,0 +1,447 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+var httpserver = new HttpServer();
+httpserver.start(-1);
+var cache = null;
+
+var base_url = "http://localhost:" + httpserver.identity.primaryPort;
+var resource_age_100 = "/resource_age_100";
+var resource_age_100_url = base_url + resource_age_100;
+var resource_stale_100 = "/resource_stale_100";
+var resource_stale_100_url = base_url + resource_stale_100;
+var resource_fresh_100 = "/resource_fresh_100";
+var resource_fresh_100_url = base_url + resource_fresh_100;
+
+// Test flags
+var hit_server = false;
+
+function make_channel(url, cache_control) {
+ // Reset test global status
+ hit_server = false;
+
+ var req = NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true });
+ req.QueryInterface(Ci.nsIHttpChannel);
+ if (cache_control) {
+ req.setRequestHeader("Cache-control", cache_control, false);
+ }
+
+ return req;
+}
+
+function make_uri(url) {
+ var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
+ return ios.newURI(url);
+}
+
+function resource_age_100_handler(metadata, response) {
+ hit_server = true;
+
+ response.setStatusLine(metadata.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "text/plain", false);
+ response.setHeader("Age", "100", false);
+ response.setHeader("Last-Modified", date_string_from_now(-100), false);
+ response.setHeader("Expires", date_string_from_now(+9999), false);
+
+ const body = "data1";
+ response.bodyOutputStream.write(body, body.length);
+}
+
+function resource_stale_100_handler(metadata, response) {
+ hit_server = true;
+
+ response.setStatusLine(metadata.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "text/plain", false);
+ response.setHeader("Date", date_string_from_now(-200), false);
+ response.setHeader("Last-Modified", date_string_from_now(-200), false);
+ response.setHeader("Cache-Control", "max-age=100", false);
+ response.setHeader("Expires", date_string_from_now(-100), false);
+
+ const body = "data2";
+ response.bodyOutputStream.write(body, body.length);
+}
+
+function resource_fresh_100_handler(metadata, response) {
+ hit_server = true;
+
+ response.setStatusLine(metadata.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "text/plain", false);
+ response.setHeader("Last-Modified", date_string_from_now(0), false);
+ response.setHeader("Cache-Control", "max-age=100", false);
+ response.setHeader("Expires", date_string_from_now(+100), false);
+
+ const body = "data3";
+ response.bodyOutputStream.write(body, body.length);
+}
+
+function run_test() {
+ do_get_profile();
+
+ do_test_pending();
+
+ Services.prefs.setBoolPref("network.http.rcwn.enabled", false);
+
+ httpserver.registerPathHandler(resource_age_100, resource_age_100_handler);
+ httpserver.registerPathHandler(
+ resource_stale_100,
+ resource_stale_100_handler
+ );
+ httpserver.registerPathHandler(
+ resource_fresh_100,
+ resource_fresh_100_handler
+ );
+ cache = getCacheStorage("disk");
+
+ wait_for_cache_index(run_next_test);
+}
+
+// Here starts the list of tests
+
+// ============================================================================
+// Cache-Control: no-store
+
+add_test(() => {
+ // Must not create a cache entry
+ var ch = make_channel(resource_age_100_url, "no-store");
+ ch.asyncOpen(
+ new ChannelListener(function(request, data) {
+ Assert.ok(hit_server);
+ Assert.ok(!cache.exists(make_uri(resource_age_100_url), ""));
+
+ run_next_test();
+ }, null)
+ );
+});
+
+add_test(() => {
+ // Prepare state only, cache the entry
+ var ch = make_channel(resource_age_100_url);
+ ch.asyncOpen(
+ new ChannelListener(function(request, data) {
+ Assert.ok(hit_server);
+ Assert.ok(cache.exists(make_uri(resource_age_100_url), ""));
+
+ run_next_test();
+ }, null)
+ );
+});
+
+add_test(() => {
+ // Check the prepared cache entry is used when no special directives are added
+ var ch = make_channel(resource_age_100_url);
+ ch.asyncOpen(
+ new ChannelListener(function(request, data) {
+ Assert.ok(!hit_server);
+ Assert.ok(cache.exists(make_uri(resource_age_100_url), ""));
+
+ run_next_test();
+ }, null)
+ );
+});
+
+add_test(() => {
+ // Try again, while we already keep a cache entry,
+ // the channel must not use it, entry should stay in the cache
+ var ch = make_channel(resource_age_100_url, "no-store");
+ ch.asyncOpen(
+ new ChannelListener(function(request, data) {
+ Assert.ok(hit_server);
+ Assert.ok(cache.exists(make_uri(resource_age_100_url), ""));
+
+ run_next_test();
+ }, null)
+ );
+});
+
+// ============================================================================
+// Cache-Control: no-cache
+
+add_test(() => {
+ // Check the prepared cache entry is used when no special directives are added
+ var ch = make_channel(resource_age_100_url);
+ ch.asyncOpen(
+ new ChannelListener(function(request, data) {
+ Assert.ok(!hit_server);
+ Assert.ok(cache.exists(make_uri(resource_age_100_url), ""));
+
+ run_next_test();
+ }, null)
+ );
+});
+
+add_test(() => {
+ // The existing entry should be revalidated (we expect a server hit)
+ var ch = make_channel(resource_age_100_url, "no-cache");
+ ch.asyncOpen(
+ new ChannelListener(function(request, data) {
+ Assert.ok(hit_server);
+ Assert.ok(cache.exists(make_uri(resource_age_100_url), ""));
+
+ run_next_test();
+ }, null)
+ );
+});
+
+// ============================================================================
+// Cache-Control: max-age
+
+add_test(() => {
+ // Check the prepared cache entry is used when no special directives are added
+ var ch = make_channel(resource_age_100_url);
+ ch.asyncOpen(
+ new ChannelListener(function(request, data) {
+ Assert.ok(!hit_server);
+ Assert.ok(cache.exists(make_uri(resource_age_100_url), ""));
+
+ run_next_test();
+ }, null)
+ );
+});
+
+add_test(() => {
+ // The existing entry's age is greater than the maximum requested,
+ // should hit server
+ var ch = make_channel(resource_age_100_url, "max-age=10");
+ ch.asyncOpen(
+ new ChannelListener(function(request, data) {
+ Assert.ok(hit_server);
+ Assert.ok(cache.exists(make_uri(resource_age_100_url), ""));
+
+ run_next_test();
+ }, null)
+ );
+});
+
+add_test(() => {
+ // The existing entry's age is greater than the maximum requested,
+ // but the max-stale directive says to use it when it's fresh enough
+ var ch = make_channel(resource_age_100_url, "max-age=10, max-stale=99999");
+ ch.asyncOpen(
+ new ChannelListener(function(request, data) {
+ Assert.ok(!hit_server);
+ Assert.ok(cache.exists(make_uri(resource_age_100_url), ""));
+
+ run_next_test();
+ }, null)
+ );
+});
+
+add_test(() => {
+ // The existing entry's age is lesser than the maximum requested,
+ // should go from cache
+ var ch = make_channel(resource_age_100_url, "max-age=1000");
+ ch.asyncOpen(
+ new ChannelListener(function(request, data) {
+ Assert.ok(!hit_server);
+ Assert.ok(cache.exists(make_uri(resource_age_100_url), ""));
+
+ run_next_test();
+ }, null)
+ );
+});
+
+// ============================================================================
+// Cache-Control: max-stale
+
+add_test(() => {
+ // Preprate the entry first
+ var ch = make_channel(resource_stale_100_url);
+ ch.asyncOpen(
+ new ChannelListener(function(request, data) {
+ Assert.ok(hit_server);
+ Assert.ok(cache.exists(make_uri(resource_stale_100_url), ""));
+
+ // Must shift the expiration time set on the entry to |now| be in the past
+ do_timeout(1500, run_next_test);
+ }, null)
+ );
+});
+
+add_test(() => {
+ // Check it's not reused (as it's stale) when no special directives
+ // are provided
+ var ch = make_channel(resource_stale_100_url);
+ ch.asyncOpen(
+ new ChannelListener(function(request, data) {
+ Assert.ok(hit_server);
+ Assert.ok(cache.exists(make_uri(resource_stale_100_url), ""));
+
+ do_timeout(1500, run_next_test);
+ }, null)
+ );
+});
+
+add_test(() => {
+ // Accept cached responses of any stale time
+ var ch = make_channel(resource_stale_100_url, "max-stale");
+ ch.asyncOpen(
+ new ChannelListener(function(request, data) {
+ Assert.ok(!hit_server);
+ Assert.ok(cache.exists(make_uri(resource_stale_100_url), ""));
+
+ do_timeout(1500, run_next_test);
+ }, null)
+ );
+});
+
+add_test(() => {
+ // The entry is stale only by 100 seconds, accept it
+ var ch = make_channel(resource_stale_100_url, "max-stale=1000");
+ ch.asyncOpen(
+ new ChannelListener(function(request, data) {
+ Assert.ok(!hit_server);
+ Assert.ok(cache.exists(make_uri(resource_stale_100_url), ""));
+
+ do_timeout(1500, run_next_test);
+ }, null)
+ );
+});
+
+add_test(() => {
+ // The entry is stale by 100 seconds but we only accept a 10 seconds stale
+ // entry, go from server
+ var ch = make_channel(resource_stale_100_url, "max-stale=10");
+ ch.asyncOpen(
+ new ChannelListener(function(request, data) {
+ Assert.ok(hit_server);
+ Assert.ok(cache.exists(make_uri(resource_stale_100_url), ""));
+
+ run_next_test();
+ }, null)
+ );
+});
+
+// ============================================================================
+// Cache-Control: min-fresh
+
+add_test(() => {
+ // Preprate the entry first
+ var ch = make_channel(resource_fresh_100_url);
+ ch.asyncOpen(
+ new ChannelListener(function(request, data) {
+ Assert.ok(hit_server);
+ Assert.ok(cache.exists(make_uri(resource_fresh_100_url), ""));
+
+ run_next_test();
+ }, null)
+ );
+});
+
+add_test(() => {
+ // Check it's reused when no special directives are provided
+ var ch = make_channel(resource_fresh_100_url);
+ ch.asyncOpen(
+ new ChannelListener(function(request, data) {
+ Assert.ok(!hit_server);
+ Assert.ok(cache.exists(make_uri(resource_fresh_100_url), ""));
+
+ run_next_test();
+ }, null)
+ );
+});
+
+add_test(() => {
+ // Entry fresh enough to be served from the cache
+ var ch = make_channel(resource_fresh_100_url, "min-fresh=10");
+ ch.asyncOpen(
+ new ChannelListener(function(request, data) {
+ Assert.ok(!hit_server);
+ Assert.ok(cache.exists(make_uri(resource_fresh_100_url), ""));
+
+ run_next_test();
+ }, null)
+ );
+});
+
+add_test(() => {
+ // The entry is not fresh enough
+ var ch = make_channel(resource_fresh_100_url, "min-fresh=1000");
+ ch.asyncOpen(
+ new ChannelListener(function(request, data) {
+ Assert.ok(hit_server);
+ Assert.ok(cache.exists(make_uri(resource_fresh_100_url), ""));
+
+ run_next_test();
+ }, null)
+ );
+});
+
+// ============================================================================
+// Parser test, if the Cache-Control header would not parse correctly, the entry
+// doesn't load from the server.
+
+add_test(() => {
+ var ch = make_channel(
+ resource_fresh_100_url,
+ 'unknown1,unknown2 = "a,b", min-fresh = 1000 '
+ );
+ ch.asyncOpen(
+ new ChannelListener(function(request, data) {
+ Assert.ok(hit_server);
+ Assert.ok(cache.exists(make_uri(resource_fresh_100_url), ""));
+
+ run_next_test();
+ }, null)
+ );
+});
+
+add_test(() => {
+ var ch = make_channel(resource_fresh_100_url, "no-cache = , min-fresh = 10");
+ ch.asyncOpen(
+ new ChannelListener(function(request, data) {
+ Assert.ok(hit_server);
+ Assert.ok(cache.exists(make_uri(resource_fresh_100_url), ""));
+
+ run_next_test();
+ }, null)
+ );
+});
+
+// ============================================================================
+// Done
+
+add_test(() => {
+ run_next_test();
+ httpserver.stop(do_test_finished);
+});
+
+// ============================================================================
+// Helpers
+
+function date_string_from_now(delta_secs) {
+ var months = [
+ "Jan",
+ "Feb",
+ "Mar",
+ "Apr",
+ "May",
+ "Jun",
+ "Jul",
+ "Aug",
+ "Sep",
+ "Oct",
+ "Nov",
+ "Dec",
+ ];
+ var days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
+
+ var d = new Date();
+ d.setTime(d.getTime() + delta_secs * 1000);
+ return (
+ days[d.getUTCDay()] +
+ ", " +
+ d.getUTCDate() +
+ " " +
+ months[d.getUTCMonth()] +
+ " " +
+ d.getUTCFullYear() +
+ " " +
+ d.getUTCHours() +
+ ":" +
+ d.getUTCMinutes() +
+ ":" +
+ d.getUTCSeconds() +
+ " UTC"
+ );
+}
diff --git a/netwerk/test/unit/test_cache-entry-id.js b/netwerk/test/unit/test_cache-entry-id.js
new file mode 100644
index 0000000000..121c30ced4
--- /dev/null
+++ b/netwerk/test/unit/test_cache-entry-id.js
@@ -0,0 +1,215 @@
+/**
+ * Test for the "CacheEntryId" under several cases.
+ */
+
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+XPCOMUtils.defineLazyGetter(this, "URL", function() {
+ return "http://localhost:" + httpServer.identity.primaryPort + "/content";
+});
+
+var httpServer = null;
+
+const responseContent = "response body";
+const responseContent2 = "response body 2";
+const altContent = "!@#$%^&*()";
+const altContentType = "text/binary";
+
+function isParentProcess() {
+ let appInfo = Cc["@mozilla.org/xre/app-info;1"];
+ return (
+ !appInfo ||
+ appInfo.getService(Ci.nsIXULRuntime).processType ==
+ Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT
+ );
+}
+
+var handlers = [
+ (m, r) => {
+ r.bodyOutputStream.write(responseContent, responseContent.length);
+ },
+ (m, r) => {
+ r.setStatusLine(m.httpVersion, 304, "Not Modified");
+ },
+ (m, r) => {
+ r.setStatusLine(m.httpVersion, 304, "Not Modified");
+ },
+ (m, r) => {
+ r.setStatusLine(m.httpVersion, 304, "Not Modified");
+ },
+ (m, r) => {
+ r.setStatusLine(m.httpVersion, 304, "Not Modified");
+ },
+ (m, r) => {
+ r.bodyOutputStream.write(responseContent2, responseContent2.length);
+ },
+ (m, r) => {
+ r.setStatusLine(m.httpVersion, 304, "Not Modified");
+ },
+];
+
+function contentHandler(metadata, response) {
+ response.setHeader("Content-Type", "text/plain");
+ response.setHeader("Cache-Control", "no-cache");
+
+ var handler = handlers.shift();
+ if (handler) {
+ handler(metadata, response);
+ return;
+ }
+
+ Assert.ok(false, "Should not reach here.");
+}
+
+function fetch(preferredDataType = null) {
+ return new Promise(resolve => {
+ var chan = NetUtil.newChannel({ uri: URL, loadUsingSystemPrincipal: true });
+
+ if (preferredDataType) {
+ var cc = chan.QueryInterface(Ci.nsICacheInfoChannel);
+ cc.preferAlternativeDataType(altContentType, "", true);
+ }
+
+ chan.asyncOpen(
+ new ChannelListener((request, buffer, ctx, isFromCache, cacheEntryId) => {
+ resolve({ request, buffer, isFromCache, cacheEntryId });
+ }, null)
+ );
+ });
+}
+
+function check(
+ response,
+ content,
+ preferredDataType,
+ isFromCache,
+ cacheEntryIdChecker
+) {
+ var cc = response.request.QueryInterface(Ci.nsICacheInfoChannel);
+
+ Assert.equal(response.buffer, content);
+ Assert.equal(cc.alternativeDataType, preferredDataType);
+ Assert.equal(response.isFromCache, isFromCache);
+ Assert.ok(!cacheEntryIdChecker || cacheEntryIdChecker(response.cacheEntryId));
+
+ return response;
+}
+
+function writeAltData(request) {
+ var cc = request.QueryInterface(Ci.nsICacheInfoChannel);
+ var os = cc.openAlternativeOutputStream(altContentType, altContent.length);
+ os.write(altContent, altContent.length);
+ os.close();
+ gc(); // We need to do a GC pass to ensure the cache entry has been freed.
+
+ return new Promise(resolve => {
+ if (isParentProcess()) {
+ Services.cache2.QueryInterface(Ci.nsICacheTesting).flush(resolve);
+ } else {
+ do_send_remote_message("flush");
+ do_await_remote_message("flushed").then(resolve);
+ }
+ });
+}
+
+function run_test() {
+ do_get_profile();
+ httpServer = new HttpServer();
+ httpServer.registerPathHandler("/content", contentHandler);
+ httpServer.start(-1);
+ do_test_pending();
+
+ var targetCacheEntryId = null;
+
+ return (
+ Promise.resolve()
+ // Setup testing environment: Placing alternative data into HTTP cache.
+ .then(_ => fetch(altContentType))
+ .then(r =>
+ check(
+ r,
+ responseContent,
+ "",
+ false,
+ cacheEntryId => cacheEntryId === undefined
+ )
+ )
+ .then(r => writeAltData(r.request))
+
+ // Start testing.
+ .then(_ => fetch(altContentType))
+ .then(r =>
+ check(
+ r,
+ altContent,
+ altContentType,
+ true,
+ cacheEntryId => cacheEntryId !== undefined
+ )
+ )
+ .then(r => (targetCacheEntryId = r.cacheEntryId))
+
+ .then(_ => fetch())
+ .then(r =>
+ check(
+ r,
+ responseContent,
+ "",
+ true,
+ cacheEntryId => cacheEntryId === targetCacheEntryId
+ )
+ )
+
+ .then(_ => fetch(altContentType))
+ .then(r =>
+ check(
+ r,
+ altContent,
+ altContentType,
+ true,
+ cacheEntryId => cacheEntryId === targetCacheEntryId
+ )
+ )
+
+ .then(_ => fetch())
+ .then(r =>
+ check(
+ r,
+ responseContent,
+ "",
+ true,
+ cacheEntryId => cacheEntryId === targetCacheEntryId
+ )
+ )
+
+ .then(_ => fetch()) // The response is changed here.
+ .then(r =>
+ check(
+ r,
+ responseContent2,
+ "",
+ false,
+ cacheEntryId => cacheEntryId === undefined
+ )
+ )
+
+ .then(_ => fetch())
+ .then(r =>
+ check(
+ r,
+ responseContent2,
+ "",
+ true,
+ cacheEntryId =>
+ cacheEntryId !== undefined && cacheEntryId !== targetCacheEntryId
+ )
+ )
+
+ // Tear down.
+ .catch(e => Assert.ok(false, "Unexpected exception: " + e))
+ .then(_ => Assert.equal(handlers.length, 0))
+ .then(_ => httpServer.stop(do_test_finished))
+ );
+}
diff --git a/netwerk/test/unit/test_cache2-00-service-get.js b/netwerk/test/unit/test_cache2-00-service-get.js
new file mode 100644
index 0000000000..7286041111
--- /dev/null
+++ b/netwerk/test/unit/test_cache2-00-service-get.js
@@ -0,0 +1,19 @@
+"use strict";
+
+function run_test() {
+ // Just check the contract ID alias works well.
+ try {
+ var serviceA = Cc[
+ "@mozilla.org/netwerk/cache-storage-service;1"
+ ].getService(Ci.nsICacheStorageService);
+ Assert.ok(serviceA);
+ var serviceB = Cc[
+ "@mozilla.org/network/cache-storage-service;1"
+ ].getService(Ci.nsICacheStorageService);
+ Assert.ok(serviceB);
+
+ Assert.equal(serviceA, serviceB);
+ } catch (ex) {
+ do_throw("Cannot instantiate cache storage service: " + ex);
+ }
+}
diff --git a/netwerk/test/unit/test_cache2-01-basic.js b/netwerk/test/unit/test_cache2-01-basic.js
new file mode 100644
index 0000000000..ee7da95c17
--- /dev/null
+++ b/netwerk/test/unit/test_cache2-01-basic.js
@@ -0,0 +1,45 @@
+"use strict";
+
+function run_test() {
+ do_get_profile();
+
+ // Open for write, write
+ asyncOpenCacheEntry(
+ "http://a/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NEW, "a1m", "a1d", function(entry) {
+ // Open for read and check
+ asyncOpenCacheEntry(
+ "http://a/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NORMAL, "a1m", "a1d", function(entry) {
+ // Open for rewrite (truncate), write different meta and data
+ asyncOpenCacheEntry(
+ "http://a/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_TRUNCATE,
+ null,
+ new OpenCallback(NEW, "a2m", "a2d", function(entry) {
+ // Open for read and check
+ asyncOpenCacheEntry(
+ "http://a/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NORMAL, "a2m", "a2d", function(entry) {
+ finish_cache2_test();
+ })
+ );
+ })
+ );
+ })
+ );
+ })
+ );
+
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_cache2-01a-basic-readonly.js b/netwerk/test/unit/test_cache2-01a-basic-readonly.js
new file mode 100644
index 0000000000..32bed8a69e
--- /dev/null
+++ b/netwerk/test/unit/test_cache2-01a-basic-readonly.js
@@ -0,0 +1,45 @@
+"use strict";
+
+function run_test() {
+ do_get_profile();
+
+ // Open for write, write
+ asyncOpenCacheEntry(
+ "http://ro/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NEW, "a1m", "a1d", function(entry) {
+ // Open for read and check
+ asyncOpenCacheEntry(
+ "http://ro/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_READONLY,
+ null,
+ new OpenCallback(NORMAL, "a1m", "a1d", function(entry) {
+ // Open for rewrite (truncate), write different meta and data
+ asyncOpenCacheEntry(
+ "http://ro/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_TRUNCATE,
+ null,
+ new OpenCallback(NEW, "a2m", "a2d", function(entry) {
+ // Open for read and check
+ asyncOpenCacheEntry(
+ "http://ro/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_READONLY,
+ null,
+ new OpenCallback(NORMAL, "a2m", "a2d", function(entry) {
+ finish_cache2_test();
+ })
+ );
+ })
+ );
+ })
+ );
+ })
+ );
+
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_cache2-01b-basic-datasize.js b/netwerk/test/unit/test_cache2-01b-basic-datasize.js
new file mode 100644
index 0000000000..9c41e114c3
--- /dev/null
+++ b/netwerk/test/unit/test_cache2-01b-basic-datasize.js
@@ -0,0 +1,49 @@
+"use strict";
+
+function run_test() {
+ do_get_profile();
+
+ // Open for write, write
+ asyncOpenCacheEntry(
+ "http://a/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NEW | WAITFORWRITE, "a1m", "a1d", function(entry) {
+ // Open for read and check
+ Assert.equal(entry.dataSize, 3);
+ asyncOpenCacheEntry(
+ "http://a/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NORMAL, "a1m", "a1d", function(entry) {
+ // Open for rewrite (truncate), write different meta and data
+ Assert.equal(entry.dataSize, 3);
+ asyncOpenCacheEntry(
+ "http://a/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_TRUNCATE,
+ null,
+ new OpenCallback(NEW | WAITFORWRITE, "a2m", "a2d", function(entry) {
+ // Open for read and check
+ Assert.equal(entry.dataSize, 3);
+ asyncOpenCacheEntry(
+ "http://a/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NORMAL, "a2m", "a2d", function(entry) {
+ Assert.equal(entry.dataSize, 3);
+ finish_cache2_test();
+ })
+ );
+ })
+ );
+ })
+ );
+ })
+ );
+
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_cache2-01c-basic-hasmeta-only.js b/netwerk/test/unit/test_cache2-01c-basic-hasmeta-only.js
new file mode 100644
index 0000000000..7c97c5ced5
--- /dev/null
+++ b/netwerk/test/unit/test_cache2-01c-basic-hasmeta-only.js
@@ -0,0 +1,45 @@
+"use strict";
+
+function run_test() {
+ do_get_profile();
+
+ // Open for write, write
+ asyncOpenCacheEntry(
+ "http://mt/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NEW | METAONLY, "a1m", "a1d", function(entry) {
+ // Open for read and check
+ asyncOpenCacheEntry(
+ "http://mt/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NORMAL, "a1m", "", function(entry) {
+ // Open for rewrite (truncate), write different meta and data
+ asyncOpenCacheEntry(
+ "http://mt/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_TRUNCATE,
+ null,
+ new OpenCallback(NEW, "a2m", "a2d", function(entry) {
+ // Open for read and check
+ asyncOpenCacheEntry(
+ "http://mt/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NORMAL, "a2m", "a2d", function(entry) {
+ finish_cache2_test();
+ })
+ );
+ })
+ );
+ })
+ );
+ })
+ );
+
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_cache2-01d-basic-not-wanted.js b/netwerk/test/unit/test_cache2-01d-basic-not-wanted.js
new file mode 100644
index 0000000000..faed1c9d69
--- /dev/null
+++ b/netwerk/test/unit/test_cache2-01d-basic-not-wanted.js
@@ -0,0 +1,45 @@
+"use strict";
+
+function run_test() {
+ do_get_profile();
+
+ // Open for write, write
+ asyncOpenCacheEntry(
+ "http://a/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NEW, "a1m", "a1d", function(entry) {
+ // Open for read and check
+ asyncOpenCacheEntry(
+ "http://a/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NORMAL, "a1m", "a1d", function(entry) {
+ // Open but don't want the entry
+ asyncOpenCacheEntry(
+ "http://a/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NOTWANTED, "a1m", "a1d", function(entry) {
+ // Open for read again and check the entry is OK
+ asyncOpenCacheEntry(
+ "http://a/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NORMAL, "a1m", "a1d", function(entry) {
+ finish_cache2_test();
+ })
+ );
+ })
+ );
+ })
+ );
+ })
+ );
+
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_cache2-01e-basic-bypass-if-busy.js b/netwerk/test/unit/test_cache2-01e-basic-bypass-if-busy.js
new file mode 100644
index 0000000000..a151f2e752
--- /dev/null
+++ b/netwerk/test/unit/test_cache2-01e-basic-bypass-if-busy.js
@@ -0,0 +1,39 @@
+"use strict";
+
+function run_test() {
+ do_get_profile();
+
+ // Open for write, delay the actual write
+ asyncOpenCacheEntry(
+ "http://a/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NEW | DONTFILL, "a1m", "a1d", function(entry) {
+ var bypassed = false;
+
+ // Open and bypass
+ asyncOpenCacheEntry(
+ "http://a/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_BYPASS_IF_BUSY,
+ null,
+ new OpenCallback(NOTFOUND, "", "", function(entry) {
+ Assert.ok(!bypassed);
+ bypassed = true;
+ })
+ );
+
+ // do_execute_soon for two reasons:
+ // 1. we want finish_cache2_test call for sure after do_test_pending, but all the callbacks here
+ // may invoke synchronously
+ // 2. precaution when the OPEN_BYPASS_IF_BUSY invocation become a post one day
+ executeSoon(function() {
+ Assert.ok(bypassed);
+ finish_cache2_test();
+ });
+ })
+ );
+
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_cache2-01f-basic-openTruncate.js b/netwerk/test/unit/test_cache2-01f-basic-openTruncate.js
new file mode 100644
index 0000000000..2ee2bf85b8
--- /dev/null
+++ b/netwerk/test/unit/test_cache2-01f-basic-openTruncate.js
@@ -0,0 +1,24 @@
+"use strict";
+
+function run_test() {
+ do_get_profile();
+
+ var storage = getCacheStorage("disk");
+ var entry = storage.openTruncate(createURI("http://new1/"), "");
+ Assert.ok(!!entry);
+
+ // Fill the entry, and when done, check it's content
+ new OpenCallback(NEW, "meta", "data", function() {
+ asyncOpenCacheEntry(
+ "http://new1/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NORMAL, "meta", "data", function() {
+ finish_cache2_test();
+ })
+ );
+ }).onCacheEntryAvailable(entry, true, null, 0);
+
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_cache2-02-open-non-existing.js b/netwerk/test/unit/test_cache2-02-open-non-existing.js
new file mode 100644
index 0000000000..8c00e2f632
--- /dev/null
+++ b/netwerk/test/unit/test_cache2-02-open-non-existing.js
@@ -0,0 +1,45 @@
+"use strict";
+
+function run_test() {
+ do_get_profile();
+
+ // Open non-existing for read, should fail
+ asyncOpenCacheEntry(
+ "http://b/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_READONLY,
+ null,
+ new OpenCallback(NOTFOUND, null, null, function(entry) {
+ // Open the same non-existing for read again, should fail second time
+ asyncOpenCacheEntry(
+ "http://b/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_READONLY,
+ null,
+ new OpenCallback(NOTFOUND, null, null, function(entry) {
+ // Try it again normally, should go
+ asyncOpenCacheEntry(
+ "http://b/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NEW, "b1m", "b1d", function(entry) {
+ // ...and check
+ asyncOpenCacheEntry(
+ "http://b/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NORMAL, "b1m", "b1d", function(entry) {
+ finish_cache2_test();
+ })
+ );
+ })
+ );
+ })
+ );
+ })
+ );
+
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_cache2-03-oncacheentryavail-throws.js b/netwerk/test/unit/test_cache2-03-oncacheentryavail-throws.js
new file mode 100644
index 0000000000..f506b1165e
--- /dev/null
+++ b/netwerk/test/unit/test_cache2-03-oncacheentryavail-throws.js
@@ -0,0 +1,36 @@
+"use strict";
+
+function run_test() {
+ do_get_profile();
+
+ // Open but let OCEA throw
+ asyncOpenCacheEntry(
+ "http://c/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NEW | THROWAVAIL, null, null, function(entry) {
+ // Try it again, should go
+ asyncOpenCacheEntry(
+ "http://c/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NEW, "c1m", "c1d", function(entry) {
+ // ...and check
+ asyncOpenCacheEntry(
+ "http://c/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(false, "c1m", "c1d", function(entry) {
+ finish_cache2_test();
+ })
+ );
+ })
+ );
+ })
+ );
+
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_cache2-04-oncacheentryavail-throws2x.js b/netwerk/test/unit/test_cache2-04-oncacheentryavail-throws2x.js
new file mode 100644
index 0000000000..164ec4c1ed
--- /dev/null
+++ b/netwerk/test/unit/test_cache2-04-oncacheentryavail-throws2x.js
@@ -0,0 +1,45 @@
+"use strict";
+
+function run_test() {
+ do_get_profile();
+
+ // Open but let OCEA throw
+ asyncOpenCacheEntry(
+ "http://d/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NEW | THROWAVAIL, null, null, function(entry) {
+ // Open but let OCEA throw ones again
+ asyncOpenCacheEntry(
+ "http://d/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NEW | THROWAVAIL, null, null, function(entry) {
+ // Try it again, should go
+ asyncOpenCacheEntry(
+ "http://d/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NEW, "d1m", "d1d", function(entry) {
+ // ...and check
+ asyncOpenCacheEntry(
+ "http://d/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NORMAL, "d1m", "d1d", function(entry) {
+ finish_cache2_test();
+ })
+ );
+ })
+ );
+ })
+ );
+ })
+ );
+
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_cache2-05-visit.js b/netwerk/test/unit/test_cache2-05-visit.js
new file mode 100644
index 0000000000..ecf3f8061e
--- /dev/null
+++ b/netwerk/test/unit/test_cache2-05-visit.js
@@ -0,0 +1,113 @@
+"use strict";
+
+function run_test() {
+ do_get_profile();
+
+ var storage = getCacheStorage("disk");
+ var mc = new MultipleCallbacks(4, function() {
+ // Method asyncVisitStorage() gets the data from index on Cache I/O thread
+ // with INDEX priority, so it is ensured that index contains information
+ // about all pending writes. However, OpenCallback emulates network latency
+ // by postponing the writes using do_execute_soon. We must do the same here
+ // to make sure that all writes are posted to Cache I/O thread before we
+ // visit the storage.
+ executeSoon(function() {
+ syncWithCacheIOThread(function() {
+ var expectedConsumption = 4096;
+
+ storage.asyncVisitStorage(
+ // Test should store 4 entries
+ new VisitCallback(
+ 4,
+ expectedConsumption,
+ ["http://a/", "http://b/", "http://c/", "http://d/"],
+ function() {
+ storage.asyncVisitStorage(
+ // Still 4 entries expected, now don't walk them
+ new VisitCallback(4, expectedConsumption, null, function() {
+ finish_cache2_test();
+ }),
+ false
+ );
+ }
+ ),
+ true
+ );
+ });
+ });
+ });
+
+ asyncOpenCacheEntry(
+ "http://a/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NEW, "a1m", "a1d", function(entry) {
+ asyncOpenCacheEntry(
+ "http://a/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NORMAL, "a1m", "a1d", function(entry) {
+ mc.fired();
+ })
+ );
+ })
+ );
+
+ asyncOpenCacheEntry(
+ "http://b/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NEW, "b1m", "b1d", function(entry) {
+ asyncOpenCacheEntry(
+ "http://b/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NORMAL, "b1m", "b1d", function(entry) {
+ mc.fired();
+ })
+ );
+ })
+ );
+
+ asyncOpenCacheEntry(
+ "http://c/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NEW, "c1m", "c1d", function(entry) {
+ asyncOpenCacheEntry(
+ "http://c/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NORMAL, "c1m", "c1d", function(entry) {
+ mc.fired();
+ })
+ );
+ })
+ );
+
+ asyncOpenCacheEntry(
+ "http://d/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NEW, "d1m", "d1d", function(entry) {
+ asyncOpenCacheEntry(
+ "http://d/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NORMAL, "d1m", "d1d", function(entry) {
+ mc.fired();
+ })
+ );
+ })
+ );
+
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_cache2-06-pb-mode.js b/netwerk/test/unit/test_cache2-06-pb-mode.js
new file mode 100644
index 0000000000..1eebb84543
--- /dev/null
+++ b/netwerk/test/unit/test_cache2-06-pb-mode.js
@@ -0,0 +1,53 @@
+"use strict";
+
+function exitPB() {
+ var obsvc = Cc["@mozilla.org/observer-service;1"].getService(
+ Ci.nsIObserverService
+ );
+ obsvc.notifyObservers(null, "last-pb-context-exited");
+}
+
+function run_test() {
+ do_get_profile();
+
+ // Store PB entry
+ asyncOpenCacheEntry(
+ "http://p1/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ Services.loadContextInfo.private,
+ new OpenCallback(NEW, "p1m", "p1d", function(entry) {
+ asyncOpenCacheEntry(
+ "http://p1/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ Services.loadContextInfo.private,
+ new OpenCallback(NORMAL, "p1m", "p1d", function(entry) {
+ // Check it's there
+ syncWithCacheIOThread(function() {
+ var storage = getCacheStorage(
+ "disk",
+ Services.loadContextInfo.private
+ );
+ storage.asyncVisitStorage(
+ new VisitCallback(1, 12, ["http://p1/"], function() {
+ // Simulate PB exit
+ exitPB();
+ // Check the entry is gone
+ storage.asyncVisitStorage(
+ new VisitCallback(0, 0, [], function() {
+ finish_cache2_test();
+ }),
+ true
+ );
+ }),
+ true
+ );
+ });
+ })
+ );
+ })
+ );
+
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_cache2-07-visit-memory.js b/netwerk/test/unit/test_cache2-07-visit-memory.js
new file mode 100644
index 0000000000..79065cab19
--- /dev/null
+++ b/netwerk/test/unit/test_cache2-07-visit-memory.js
@@ -0,0 +1,123 @@
+"use strict";
+
+function run_test() {
+ do_get_profile();
+
+ // Add entry to the memory storage
+ var mc = new MultipleCallbacks(5, function() {
+ // Check it's there by visiting the storage
+ syncWithCacheIOThread(function() {
+ var storage = getCacheStorage("memory");
+ storage.asyncVisitStorage(
+ new VisitCallback(1, 12, ["http://mem1/"], function() {
+ storage = getCacheStorage("disk");
+ storage.asyncVisitStorage(
+ // Previous tests should store 4 disk entries
+ new VisitCallback(
+ 4,
+ 4096,
+ ["http://a/", "http://b/", "http://c/", "http://d/"],
+ function() {
+ finish_cache2_test();
+ }
+ ),
+ true
+ );
+ }),
+ true
+ );
+ });
+ });
+
+ asyncOpenCacheEntry(
+ "http://mem1/",
+ "memory",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NEW, "m1m", "m1d", function(entry) {
+ asyncOpenCacheEntry(
+ "http://mem1/",
+ "memory",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NORMAL, "m1m", "m1d", function(entry) {
+ mc.fired();
+ })
+ );
+ })
+ );
+
+ asyncOpenCacheEntry(
+ "http://a/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NEW, "a1m", "a1d", function(entry) {
+ asyncOpenCacheEntry(
+ "http://a/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NORMAL, "a1m", "a1d", function(entry) {
+ mc.fired();
+ })
+ );
+ })
+ );
+
+ asyncOpenCacheEntry(
+ "http://b/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NEW, "a1m", "a1d", function(entry) {
+ asyncOpenCacheEntry(
+ "http://b/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NORMAL, "a1m", "a1d", function(entry) {
+ mc.fired();
+ })
+ );
+ })
+ );
+
+ asyncOpenCacheEntry(
+ "http://c/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NEW, "a1m", "a1d", function(entry) {
+ asyncOpenCacheEntry(
+ "http://c/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NORMAL, "a1m", "a1d", function(entry) {
+ mc.fired();
+ })
+ );
+ })
+ );
+
+ asyncOpenCacheEntry(
+ "http://d/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NEW, "a1m", "a1d", function(entry) {
+ asyncOpenCacheEntry(
+ "http://d/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NORMAL, "a1m", "a1d", function(entry) {
+ mc.fired();
+ })
+ );
+ })
+ );
+
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_cache2-07a-open-memory.js b/netwerk/test/unit/test_cache2-07a-open-memory.js
new file mode 100644
index 0000000000..6944064697
--- /dev/null
+++ b/netwerk/test/unit/test_cache2-07a-open-memory.js
@@ -0,0 +1,81 @@
+"use strict";
+
+function run_test() {
+ do_get_profile();
+
+ // First check how behaves the memory storage.
+
+ asyncOpenCacheEntry(
+ "http://mem-first/",
+ "memory",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NEW, "mem1-meta", "mem1-data", function(entryM1) {
+ Assert.ok(!entryM1.persistent);
+ asyncOpenCacheEntry(
+ "http://mem-first/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NORMAL, "mem1-meta", "mem1-data", function(entryM2) {
+ Assert.ok(!entryM1.persistent);
+ Assert.ok(!entryM2.persistent);
+
+ // Now check the disk storage behavior.
+
+ asyncOpenCacheEntry(
+ "http://disk-first/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ // Must wait for write, since opening the entry as memory-only before the disk one
+ // is written would cause NS_ERROR_NOT_AVAILABLE from openOutputStream when writing
+ // this disk entry since it's doomed during opening of the memory-only entry for the same URL.
+ new OpenCallback(
+ NEW | WAITFORWRITE,
+ "disk1-meta",
+ "disk1-data",
+ function(entryD1) {
+ Assert.ok(entryD1.persistent);
+ // Now open the same URL as a memory-only entry, the disk entry must be doomed.
+ asyncOpenCacheEntry(
+ "http://disk-first/",
+ "memory",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ // This must be recreated
+ new OpenCallback(NEW, "mem2-meta", "mem2-data", function(
+ entryD2
+ ) {
+ Assert.ok(entryD1.persistent);
+ Assert.ok(!entryD2.persistent);
+ // Check we get it back, even when opening via the disk storage
+ asyncOpenCacheEntry(
+ "http://disk-first/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(
+ NORMAL,
+ "mem2-meta",
+ "mem2-data",
+ function(entryD3) {
+ Assert.ok(entryD1.persistent);
+ Assert.ok(!entryD2.persistent);
+ Assert.ok(!entryD3.persistent);
+ finish_cache2_test();
+ }
+ )
+ );
+ })
+ );
+ }
+ )
+ );
+ })
+ );
+ })
+ );
+
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_cache2-08-evict-disk-by-memory-storage.js b/netwerk/test/unit/test_cache2-08-evict-disk-by-memory-storage.js
new file mode 100644
index 0000000000..7e62c45030
--- /dev/null
+++ b/netwerk/test/unit/test_cache2-08-evict-disk-by-memory-storage.js
@@ -0,0 +1,25 @@
+"use strict";
+
+function run_test() {
+ do_get_profile();
+
+ asyncOpenCacheEntry(
+ "http://a/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NEW, "a1m", "a1d", function(entry) {
+ var storage = getCacheStorage("memory");
+ // Have to fail
+ storage.asyncDoomURI(
+ createURI("http://a/"),
+ "",
+ new EvictionCallback(false, function() {
+ finish_cache2_test();
+ })
+ );
+ })
+ );
+
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_cache2-09-evict-disk-by-uri.js b/netwerk/test/unit/test_cache2-09-evict-disk-by-uri.js
new file mode 100644
index 0000000000..dd444f82fb
--- /dev/null
+++ b/netwerk/test/unit/test_cache2-09-evict-disk-by-uri.js
@@ -0,0 +1,32 @@
+"use strict";
+
+function run_test() {
+ do_get_profile();
+
+ asyncOpenCacheEntry(
+ "http://a/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NEW, "a1m", "a1d", function(entry) {
+ asyncOpenCacheEntry(
+ "http://a/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NORMAL, "a1m", "a1d", function(entry) {
+ var storage = getCacheStorage("disk");
+ storage.asyncDoomURI(
+ createURI("http://a/"),
+ "",
+ new EvictionCallback(true, function() {
+ finish_cache2_test();
+ })
+ );
+ })
+ );
+ })
+ );
+
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_cache2-10-evict-direct.js b/netwerk/test/unit/test_cache2-10-evict-direct.js
new file mode 100644
index 0000000000..f9f32a9ab2
--- /dev/null
+++ b/netwerk/test/unit/test_cache2-10-evict-direct.js
@@ -0,0 +1,29 @@
+"use strict";
+
+function run_test() {
+ do_get_profile();
+
+ asyncOpenCacheEntry(
+ "http://b/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NEW, "b1m", "b1d", function(entry) {
+ asyncOpenCacheEntry(
+ "http://b/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NORMAL, "b1m", "b1d", function(entry) {
+ entry.asyncDoom(
+ new EvictionCallback(true, function() {
+ finish_cache2_test();
+ })
+ );
+ })
+ );
+ })
+ );
+
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_cache2-10b-evict-direct-immediate.js b/netwerk/test/unit/test_cache2-10b-evict-direct-immediate.js
new file mode 100644
index 0000000000..688b1f1e00
--- /dev/null
+++ b/netwerk/test/unit/test_cache2-10b-evict-direct-immediate.js
@@ -0,0 +1,21 @@
+"use strict";
+
+function run_test() {
+ do_get_profile();
+
+ asyncOpenCacheEntry(
+ "http://b/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NEW | DOOMED, "b1m", "b1d", function(entry) {
+ entry.asyncDoom(
+ new EvictionCallback(true, function() {
+ finish_cache2_test();
+ })
+ );
+ })
+ );
+
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_cache2-11-evict-memory.js b/netwerk/test/unit/test_cache2-11-evict-memory.js
new file mode 100644
index 0000000000..5868a29552
--- /dev/null
+++ b/netwerk/test/unit/test_cache2-11-evict-memory.js
@@ -0,0 +1,89 @@
+"use strict";
+
+function run_test() {
+ do_get_profile();
+
+ var storage = getCacheStorage("memory");
+ var mc = new MultipleCallbacks(3, function() {
+ storage.asyncEvictStorage(
+ new EvictionCallback(true, function() {
+ storage.asyncVisitStorage(
+ new VisitCallback(0, 0, [], function() {
+ var storage = getCacheStorage("disk");
+
+ var expectedConsumption = 2048;
+
+ storage.asyncVisitStorage(
+ new VisitCallback(
+ 2,
+ expectedConsumption,
+ ["http://a/", "http://b/"],
+ function() {
+ finish_cache2_test();
+ }
+ ),
+ true
+ );
+ }),
+ true
+ );
+ })
+ );
+ });
+
+ asyncOpenCacheEntry(
+ "http://mem1/",
+ "memory",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NEW, "m2m", "m2d", function(entry) {
+ asyncOpenCacheEntry(
+ "http://mem1/",
+ "memory",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NORMAL, "m2m", "m2d", function(entry) {
+ mc.fired();
+ })
+ );
+ })
+ );
+
+ asyncOpenCacheEntry(
+ "http://a/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NEW, "a1m", "a1d", function(entry) {
+ asyncOpenCacheEntry(
+ "http://a/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NORMAL, "a1m", "a1d", function(entry) {
+ mc.fired();
+ })
+ );
+ })
+ );
+
+ asyncOpenCacheEntry(
+ "http://b/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NEW, "a1m", "a1d", function(entry) {
+ asyncOpenCacheEntry(
+ "http://b/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NORMAL, "a1m", "a1d", function(entry) {
+ mc.fired();
+ })
+ );
+ })
+ );
+
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_cache2-12-evict-disk.js b/netwerk/test/unit/test_cache2-12-evict-disk.js
new file mode 100644
index 0000000000..5c40ba94bf
--- /dev/null
+++ b/netwerk/test/unit/test_cache2-12-evict-disk.js
@@ -0,0 +1,81 @@
+"use strict";
+
+function run_test() {
+ do_get_profile();
+
+ var mc = new MultipleCallbacks(3, function() {
+ var storage = getCacheStorage("disk");
+ storage.asyncEvictStorage(
+ new EvictionCallback(true, function() {
+ storage.asyncVisitStorage(
+ new VisitCallback(0, 0, [], function() {
+ var storage = getCacheStorage("memory");
+ storage.asyncVisitStorage(
+ new VisitCallback(0, 0, [], function() {
+ finish_cache2_test();
+ }),
+ true
+ );
+ }),
+ true
+ );
+ })
+ );
+ });
+
+ asyncOpenCacheEntry(
+ "http://mem1/",
+ "memory",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NEW, "m2m", "m2d", function(entry) {
+ asyncOpenCacheEntry(
+ "http://mem1/",
+ "memory",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NORMAL, "m2m", "m2d", function(entry) {
+ mc.fired();
+ })
+ );
+ })
+ );
+
+ asyncOpenCacheEntry(
+ "http://a/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NEW, "a1m", "a1d", function(entry) {
+ asyncOpenCacheEntry(
+ "http://a/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NORMAL, "a1m", "a1d", function(entry) {
+ mc.fired();
+ })
+ );
+ })
+ );
+
+ asyncOpenCacheEntry(
+ "http://b/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NEW, "b1m", "b1d", function(entry) {
+ asyncOpenCacheEntry(
+ "http://b/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NORMAL, "b1m", "b1d", function(entry) {
+ mc.fired();
+ })
+ );
+ })
+ );
+
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_cache2-13-evict-non-existing.js b/netwerk/test/unit/test_cache2-13-evict-non-existing.js
new file mode 100644
index 0000000000..9c25a48d7b
--- /dev/null
+++ b/netwerk/test/unit/test_cache2-13-evict-non-existing.js
@@ -0,0 +1,16 @@
+"use strict";
+
+function run_test() {
+ do_get_profile();
+
+ var storage = getCacheStorage("disk");
+ storage.asyncDoomURI(
+ createURI("http://non-existing/"),
+ "",
+ new EvictionCallback(false, function() {
+ finish_cache2_test();
+ })
+ );
+
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_cache2-14-concurent-readers.js b/netwerk/test/unit/test_cache2-14-concurent-readers.js
new file mode 100644
index 0000000000..03aded21b0
--- /dev/null
+++ b/netwerk/test/unit/test_cache2-14-concurent-readers.js
@@ -0,0 +1,48 @@
+"use strict";
+
+function run_test() {
+ do_get_profile();
+
+ asyncOpenCacheEntry(
+ "http://x/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NEW, "x1m", "x1d", function(entry) {
+ // nothing to do here, we expect concurent callbacks to get
+ // all notified, then the test finishes
+ })
+ );
+
+ var mc = new MultipleCallbacks(3, finish_cache2_test);
+
+ asyncOpenCacheEntry(
+ "http://x/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NORMAL, "x1m", "x1d", function(entry) {
+ mc.fired();
+ })
+ );
+ asyncOpenCacheEntry(
+ "http://x/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NORMAL, "x1m", "x1d", function(entry) {
+ mc.fired();
+ })
+ );
+ asyncOpenCacheEntry(
+ "http://x/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NORMAL, "x1m", "x1d", function(entry) {
+ mc.fired();
+ })
+ );
+
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_cache2-14b-concurent-readers-complete.js b/netwerk/test/unit/test_cache2-14b-concurent-readers-complete.js
new file mode 100644
index 0000000000..cb00a4a8cd
--- /dev/null
+++ b/netwerk/test/unit/test_cache2-14b-concurent-readers-complete.js
@@ -0,0 +1,76 @@
+"use strict";
+
+function run_test() {
+ do_get_profile();
+
+ asyncOpenCacheEntry(
+ "http://x/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NEW, "x1m", "x1d", function(entry) {
+ // nothing to do here, we expect concurent callbacks to get
+ // all notified, then the test finishes
+ })
+ );
+
+ var mc = new MultipleCallbacks(3, finish_cache2_test);
+
+ var order = 0;
+
+ asyncOpenCacheEntry(
+ "http://x/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(
+ NORMAL | COMPLETE | NOTIFYBEFOREREAD,
+ "x1m",
+ "x1d",
+ function(entry, beforeReading) {
+ if (beforeReading) {
+ ++order;
+ Assert.equal(order, 3);
+ } else {
+ mc.fired();
+ }
+ }
+ )
+ );
+ asyncOpenCacheEntry(
+ "http://x/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NORMAL | NOTIFYBEFOREREAD, "x1m", "x1d", function(
+ entry,
+ beforeReading
+ ) {
+ if (beforeReading) {
+ ++order;
+ Assert.equal(order, 1);
+ } else {
+ mc.fired();
+ }
+ })
+ );
+ asyncOpenCacheEntry(
+ "http://x/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NORMAL | NOTIFYBEFOREREAD, "x1m", "x1d", function(
+ entry,
+ beforeReading
+ ) {
+ if (beforeReading) {
+ ++order;
+ Assert.equal(order, 2);
+ } else {
+ mc.fired();
+ }
+ })
+ );
+
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_cache2-15-conditional-304.js b/netwerk/test/unit/test_cache2-15-conditional-304.js
new file mode 100644
index 0000000000..b7271996b9
--- /dev/null
+++ b/netwerk/test/unit/test_cache2-15-conditional-304.js
@@ -0,0 +1,60 @@
+"use strict";
+
+function run_test() {
+ do_get_profile();
+
+ // Open for write, write
+ asyncOpenCacheEntry(
+ "http://304/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NEW, "31m", "31d", function(entry) {
+ // Open normally but wait for validation from the server
+ asyncOpenCacheEntry(
+ "http://304/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(REVAL, "31m", "31d", function(entry) {
+ // emulate 304 from the server
+ executeSoon(function() {
+ entry.setValid(); // this will trigger OpenCallbacks bellow
+ });
+ })
+ );
+
+ var mc = new MultipleCallbacks(3, finish_cache2_test);
+
+ asyncOpenCacheEntry(
+ "http://304/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NORMAL, "31m", "31d", function(entry) {
+ mc.fired();
+ })
+ );
+ asyncOpenCacheEntry(
+ "http://304/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NORMAL, "31m", "31d", function(entry) {
+ mc.fired();
+ })
+ );
+ asyncOpenCacheEntry(
+ "http://304/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NORMAL, "31m", "31d", function(entry) {
+ mc.fired();
+ })
+ );
+ })
+ );
+
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_cache2-16-conditional-200.js b/netwerk/test/unit/test_cache2-16-conditional-200.js
new file mode 100644
index 0000000000..60b38cd2e1
--- /dev/null
+++ b/netwerk/test/unit/test_cache2-16-conditional-200.js
@@ -0,0 +1,76 @@
+"use strict";
+
+function run_test() {
+ do_get_profile();
+
+ // Open for write, write
+ asyncOpenCacheEntry(
+ "http://200/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NEW, "21m", "21d", function(entry) {
+ asyncOpenCacheEntry(
+ "http://200/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NORMAL, "21m", "21d", function(entry) {
+ // Open normally but wait for validation from the server
+ asyncOpenCacheEntry(
+ "http://200/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(REVAL, "21m", "21d", function(entry) {
+ // emulate 200 from server (new content)
+ executeSoon(function() {
+ var entry2 = entry.recreate();
+
+ // now fill the new entry, use OpenCallback directly for it
+ new OpenCallback(
+ NEW,
+ "22m",
+ "22d",
+ function() {}
+ ).onCacheEntryAvailable(entry2, true, null, Cr.NS_OK);
+ });
+ })
+ );
+
+ var mc = new MultipleCallbacks(3, finish_cache2_test);
+
+ asyncOpenCacheEntry(
+ "http://200/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NORMAL, "22m", "22d", function(entry) {
+ mc.fired();
+ })
+ );
+ asyncOpenCacheEntry(
+ "http://200/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NORMAL, "22m", "22d", function(entry) {
+ mc.fired();
+ })
+ );
+ asyncOpenCacheEntry(
+ "http://200/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NORMAL, "22m", "22d", function(entry) {
+ mc.fired();
+ })
+ );
+ })
+ );
+ })
+ );
+
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_cache2-17-evict-all.js b/netwerk/test/unit/test_cache2-17-evict-all.js
new file mode 100644
index 0000000000..48ca1c15ef
--- /dev/null
+++ b/netwerk/test/unit/test_cache2-17-evict-all.js
@@ -0,0 +1,18 @@
+"use strict";
+
+function run_test() {
+ do_get_profile();
+
+ var svc = get_cache_service();
+ svc.clear();
+
+ var storage = getCacheStorage("disk");
+ storage.asyncVisitStorage(
+ new VisitCallback(0, 0, [], function() {
+ finish_cache2_test();
+ }),
+ true
+ );
+
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_cache2-18-not-valid.js b/netwerk/test/unit/test_cache2-18-not-valid.js
new file mode 100644
index 0000000000..82bafb5ea9
--- /dev/null
+++ b/netwerk/test/unit/test_cache2-18-not-valid.js
@@ -0,0 +1,38 @@
+"use strict";
+
+function run_test() {
+ do_get_profile();
+
+ // Open for write, write but expect it to fail, since other callback will recreate (and doom)
+ // the first entry before it opens output stream (note: in case of problems the DOOMED flag
+ // can be removed, it is not the test failure when opening the output stream on recreated entry.
+ asyncOpenCacheEntry(
+ "http://nv/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NEW | DOOMED, "v1m", "v1d", function(entry) {
+ // Open for rewrite (don't validate), write different meta and data
+ asyncOpenCacheEntry(
+ "http://nv/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NOTVALID | RECREATE, "v2m", "v2d", function(entry) {
+ // And check...
+ asyncOpenCacheEntry(
+ "http://nv/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NORMAL, "v2m", "v2d", function(entry) {
+ finish_cache2_test();
+ })
+ );
+ })
+ );
+ })
+ );
+
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_cache2-19-range-206.js b/netwerk/test/unit/test_cache2-19-range-206.js
new file mode 100644
index 0000000000..1baddebbd6
--- /dev/null
+++ b/netwerk/test/unit/test_cache2-19-range-206.js
@@ -0,0 +1,65 @@
+"use strict";
+
+function run_test() {
+ do_get_profile();
+
+ // Open for write, write
+ asyncOpenCacheEntry(
+ "http://r206/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NEW, "206m", "206part1-", function(entry) {
+ // Open normally but wait for validation from the server
+ asyncOpenCacheEntry(
+ "http://r206/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(PARTIAL, "206m", "206part1-", function(entry) {
+ // emulate 206 from the server, i.e. resume transaction and write content to the output stream
+ new OpenCallback(
+ NEW | WAITFORWRITE | PARTIAL,
+ "206m",
+ "-part2",
+ function(entry) {
+ entry.setValid();
+ }
+ ).onCacheEntryAvailable(entry, true, null, Cr.NS_OK);
+ })
+ );
+
+ var mc = new MultipleCallbacks(3, finish_cache2_test);
+
+ asyncOpenCacheEntry(
+ "http://r206/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NORMAL, "206m", "206part1--part2", function(entry) {
+ mc.fired();
+ })
+ );
+ asyncOpenCacheEntry(
+ "http://r206/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NORMAL, "206m", "206part1--part2", function(entry) {
+ mc.fired();
+ })
+ );
+ asyncOpenCacheEntry(
+ "http://r206/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NORMAL, "206m", "206part1--part2", function(entry) {
+ mc.fired();
+ })
+ );
+ })
+ );
+
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_cache2-20-range-200.js b/netwerk/test/unit/test_cache2-20-range-200.js
new file mode 100644
index 0000000000..01ed41d2e7
--- /dev/null
+++ b/netwerk/test/unit/test_cache2-20-range-200.js
@@ -0,0 +1,66 @@
+"use strict";
+
+function run_test() {
+ do_get_profile();
+
+ // Open for write, write
+ asyncOpenCacheEntry(
+ "http://r200/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NEW, "200m1", "200part1a-", function(entry) {
+ // Open normally but wait for validation from the server
+ asyncOpenCacheEntry(
+ "http://r200/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(PARTIAL, "200m1", "200part1a-", function(entry) {
+ // emulate 200 from the server, i.e. recreate the entry, resume transaction and
+ // write new content to the output stream
+ new OpenCallback(
+ NEW | WAITFORWRITE | RECREATE,
+ "200m2",
+ "200part1b--part2b",
+ function(entry) {
+ entry.setValid();
+ }
+ ).onCacheEntryAvailable(entry, true, null, Cr.NS_OK);
+ })
+ );
+
+ var mc = new MultipleCallbacks(3, finish_cache2_test);
+
+ asyncOpenCacheEntry(
+ "http://r200/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NORMAL, "200m2", "200part1b--part2b", function(entry) {
+ mc.fired();
+ })
+ );
+ asyncOpenCacheEntry(
+ "http://r200/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NORMAL, "200m2", "200part1b--part2b", function(entry) {
+ mc.fired();
+ })
+ );
+ asyncOpenCacheEntry(
+ "http://r200/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NORMAL, "200m2", "200part1b--part2b", function(entry) {
+ mc.fired();
+ })
+ );
+ })
+ );
+
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_cache2-21-anon-storage.js b/netwerk/test/unit/test_cache2-21-anon-storage.js
new file mode 100644
index 0000000000..3e2b195d5f
--- /dev/null
+++ b/netwerk/test/unit/test_cache2-21-anon-storage.js
@@ -0,0 +1,52 @@
+"use strict";
+
+function run_test() {
+ do_get_profile();
+
+ // Create and check an entry anon disk storage
+ asyncOpenCacheEntry(
+ "http://anon1/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ Services.loadContextInfo.anonymous,
+ new OpenCallback(NEW, "an1", "an1", function(entry) {
+ asyncOpenCacheEntry(
+ "http://anon1/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ Services.loadContextInfo.anonymous,
+ new OpenCallback(NORMAL, "an1", "an1", function(entry) {
+ // Create and check an entry non-anon disk storage
+ asyncOpenCacheEntry(
+ "http://anon1/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ Services.loadContextInfo.default,
+ new OpenCallback(NEW, "na1", "na1", function(entry) {
+ asyncOpenCacheEntry(
+ "http://anon1/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ Services.loadContextInfo.default,
+ new OpenCallback(NORMAL, "na1", "na1", function(entry) {
+ // check the anon entry is still there and intact
+ asyncOpenCacheEntry(
+ "http://anon1/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ Services.loadContextInfo.anonymous,
+ new OpenCallback(NORMAL, "an1", "an1", function(entry) {
+ finish_cache2_test();
+ })
+ );
+ })
+ );
+ })
+ );
+ })
+ );
+ })
+ );
+
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_cache2-22-anon-visit.js b/netwerk/test/unit/test_cache2-22-anon-visit.js
new file mode 100644
index 0000000000..b9bc961cfa
--- /dev/null
+++ b/netwerk/test/unit/test_cache2-22-anon-visit.js
@@ -0,0 +1,43 @@
+"use strict";
+
+function run_test() {
+ do_get_profile();
+
+ var mc = new MultipleCallbacks(2, function() {
+ var storage = getCacheStorage("disk", Services.loadContextInfo.default);
+ storage.asyncVisitStorage(
+ new VisitCallback(1, 1024, ["http://an2/"], function() {
+ storage = getCacheStorage("disk", Services.loadContextInfo.anonymous);
+ storage.asyncVisitStorage(
+ new VisitCallback(1, 1024, ["http://an2/"], function() {
+ finish_cache2_test();
+ }),
+ true
+ );
+ }),
+ true
+ );
+ });
+
+ asyncOpenCacheEntry(
+ "http://an2/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ Services.loadContextInfo.default,
+ new OpenCallback(NEW | WAITFORWRITE, "an2", "an2", function(entry) {
+ mc.fired();
+ })
+ );
+
+ asyncOpenCacheEntry(
+ "http://an2/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ Services.loadContextInfo.anonymous,
+ new OpenCallback(NEW | WAITFORWRITE, "an2", "an2", function(entry) {
+ mc.fired();
+ })
+ );
+
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_cache2-23-read-over-chunk.js b/netwerk/test/unit/test_cache2-23-read-over-chunk.js
new file mode 100644
index 0000000000..81871224a7
--- /dev/null
+++ b/netwerk/test/unit/test_cache2-23-read-over-chunk.js
@@ -0,0 +1,34 @@
+"use strict";
+
+function run_test() {
+ do_get_profile();
+
+ const kChunkSize = 256 * 1024;
+
+ var payload = "";
+ for (var i = 0; i < kChunkSize + 10; ++i) {
+ if (i < kChunkSize - 5) {
+ payload += "0";
+ } else {
+ payload += String.fromCharCode(i + 65);
+ }
+ }
+
+ asyncOpenCacheEntry(
+ "http://read/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_TRUNCATE,
+ Services.loadContextInfo.default,
+ new OpenCallback(NEW | WAITFORWRITE, "", payload, function(entry) {
+ var is = entry.openInputStream(0);
+ pumpReadStream(is, function(read) {
+ Assert.equal(read.length, kChunkSize + 10);
+ is.close();
+ Assert.ok(read == payload); // not using do_check_eq since logger will fail for the 1/4MB string
+ finish_cache2_test();
+ });
+ })
+ );
+
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_cache2-24-exists.js b/netwerk/test/unit/test_cache2-24-exists.js
new file mode 100644
index 0000000000..67582594cc
--- /dev/null
+++ b/netwerk/test/unit/test_cache2-24-exists.js
@@ -0,0 +1,43 @@
+"use strict";
+
+function run_test() {
+ do_get_profile();
+
+ var mc = new MultipleCallbacks(2, function() {
+ var mem = getCacheStorage("memory");
+ var disk = getCacheStorage("disk");
+
+ Assert.ok(disk.exists(createURI("http://m1/"), ""));
+ Assert.ok(mem.exists(createURI("http://m1/"), ""));
+ Assert.ok(!mem.exists(createURI("http://m2/"), ""));
+ Assert.ok(disk.exists(createURI("http://d1/"), ""));
+ do_check_throws_nsIException(
+ () => disk.exists(createURI("http://d2/"), ""),
+ "NS_ERROR_NOT_AVAILABLE"
+ );
+
+ finish_cache2_test();
+ });
+
+ asyncOpenCacheEntry(
+ "http://d1/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ Services.loadContextInfo.default,
+ new OpenCallback(NEW | WAITFORWRITE, "meta", "data", function(entry) {
+ mc.fired();
+ })
+ );
+
+ asyncOpenCacheEntry(
+ "http://m1/",
+ "memory",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ Services.loadContextInfo.default,
+ new OpenCallback(NEW | WAITFORWRITE, "meta", "data", function(entry) {
+ mc.fired();
+ })
+ );
+
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_cache2-25-chunk-memory-limit.js b/netwerk/test/unit/test_cache2-25-chunk-memory-limit.js
new file mode 100644
index 0000000000..c1c82c4141
--- /dev/null
+++ b/netwerk/test/unit/test_cache2-25-chunk-memory-limit.js
@@ -0,0 +1,57 @@
+"use strict";
+
+function gen_200k() {
+ var i;
+ var data = "0123456789ABCDEFGHIJLKMNO";
+ for (i = 0; i < 13; i++) {
+ data += data;
+ }
+ return data;
+}
+
+// Keep the output stream of the first entry in a global variable, so the
+// CacheFile and its buffer isn't released before we write the data to the
+// second entry.
+var oStr;
+
+function run_test() {
+ do_get_profile();
+
+ var prefBranch = Cc["@mozilla.org/preferences-service;1"].getService(
+ Ci.nsIPrefBranch
+ );
+
+ // set max chunks memory so that only one full chunk fits within the limit
+ prefBranch.setIntPref("browser.cache.disk.max_chunks_memory_usage", 300);
+
+ asyncOpenCacheEntry(
+ "http://a/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ function(status, entry) {
+ Assert.equal(status, Cr.NS_OK);
+ var data = gen_200k();
+ oStr = entry.openOutputStream(0, data.length);
+ Assert.equal(data.length, oStr.write(data, data.length));
+
+ asyncOpenCacheEntry(
+ "http://b/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ function(status, entry) {
+ Assert.equal(status, Cr.NS_OK);
+ var oStr2 = entry.openOutputStream(0, data.length);
+ do_check_throws_nsIException(
+ () => oStr2.write(data, data.length),
+ "NS_ERROR_OUT_OF_MEMORY"
+ );
+ finish_cache2_test();
+ }
+ );
+ }
+ );
+
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_cache2-26-no-outputstream-open.js b/netwerk/test/unit/test_cache2-26-no-outputstream-open.js
new file mode 100644
index 0000000000..378e41b5db
--- /dev/null
+++ b/netwerk/test/unit/test_cache2-26-no-outputstream-open.js
@@ -0,0 +1,36 @@
+"use strict";
+
+function run_test() {
+ do_get_profile();
+
+ // Open for write, but never write and never mark valid
+ asyncOpenCacheEntry(
+ "http://no-data/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(
+ NEW | METAONLY | DONTSETVALID | WAITFORWRITE,
+ "meta",
+ "",
+ function(entry) {
+ // Open again, we must get the callback and zero-length data
+ executeSoon(() => {
+ Cu.forceGC(); // invokes OnHandleClosed on the entry
+
+ asyncOpenCacheEntry(
+ "http://no-data/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NORMAL, "meta", "", function(entry) {
+ finish_cache2_test();
+ })
+ );
+ });
+ }
+ )
+ );
+
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_cache2-27-force-valid-for.js b/netwerk/test/unit/test_cache2-27-force-valid-for.js
new file mode 100644
index 0000000000..223fbd6057
--- /dev/null
+++ b/netwerk/test/unit/test_cache2-27-force-valid-for.js
@@ -0,0 +1,35 @@
+"use strict";
+
+function run_test() {
+ do_get_profile();
+
+ var mc = new MultipleCallbacks(2, function() {
+ finish_cache2_test();
+ });
+
+ asyncOpenCacheEntry(
+ "http://m1/",
+ "memory",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ Services.loadContextInfo.default,
+ new OpenCallback(NEW, "meta", "data", function(entry) {
+ // Check the default
+ equal(entry.isForcedValid, false);
+
+ // Forced valid and confirm
+ entry.forceValidFor(2);
+ do_timeout(1000, function() {
+ equal(entry.isForcedValid, true);
+ mc.fired();
+ });
+
+ // Confirm the timeout occurs
+ do_timeout(3000, function() {
+ equal(entry.isForcedValid, false);
+ mc.fired();
+ });
+ })
+ );
+
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_cache2-28-last-access-attrs.js b/netwerk/test/unit/test_cache2-28-last-access-attrs.js
new file mode 100644
index 0000000000..50c6b99938
--- /dev/null
+++ b/netwerk/test/unit/test_cache2-28-last-access-attrs.js
@@ -0,0 +1,46 @@
+"use strict";
+
+function run_test() {
+ do_get_profile();
+ function NowSeconds() {
+ return parseInt(new Date().getTime() / 1000);
+ }
+ function do_check_time(t, min, max) {
+ Assert.ok(t >= min);
+ Assert.ok(t <= max);
+ }
+
+ var timeStart = NowSeconds();
+
+ asyncOpenCacheEntry(
+ "http://t/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NEW, "m", "d", function(entry) {
+ var firstOpen = NowSeconds();
+ Assert.equal(entry.fetchCount, 1);
+ do_check_time(entry.lastFetched, timeStart, firstOpen);
+ do_check_time(entry.lastModified, timeStart, firstOpen);
+
+ do_timeout(2000, () => {
+ asyncOpenCacheEntry(
+ "http://t/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NORMAL, "m", "d", function(entry) {
+ var secondOpen = NowSeconds();
+ Assert.equal(entry.fetchCount, 2);
+ do_check_time(entry.lastFetched, firstOpen, secondOpen);
+ do_check_time(entry.lastModified, timeStart, firstOpen);
+
+ finish_cache2_test();
+ })
+ );
+ });
+ })
+ );
+
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_cache2-28a-OPEN_SECRETLY.js b/netwerk/test/unit/test_cache2-28a-OPEN_SECRETLY.js
new file mode 100644
index 0000000000..7ed078786a
--- /dev/null
+++ b/netwerk/test/unit/test_cache2-28a-OPEN_SECRETLY.js
@@ -0,0 +1,42 @@
+"use strict";
+
+function run_test() {
+ do_get_profile();
+ function NowSeconds() {
+ return parseInt(new Date().getTime() / 1000);
+ }
+ function do_check_time(a, b) {
+ Assert.ok(Math.abs(a - b) < 0.5);
+ }
+
+ asyncOpenCacheEntry(
+ "http://t/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NEW, "m", "d", function(entry) {
+ var now1 = NowSeconds();
+ Assert.equal(entry.fetchCount, 1);
+ do_check_time(entry.lastFetched, now1);
+ do_check_time(entry.lastModified, now1);
+
+ do_timeout(2000, () => {
+ asyncOpenCacheEntry(
+ "http://t/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_SECRETLY,
+ null,
+ new OpenCallback(NORMAL, "m", "d", function(entry) {
+ Assert.equal(entry.fetchCount, 1);
+ do_check_time(entry.lastFetched, now1);
+ do_check_time(entry.lastModified, now1);
+
+ finish_cache2_test();
+ })
+ );
+ });
+ })
+ );
+
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_cache2-29a-concurrent_read_resumable_entry_size_zero.js b/netwerk/test/unit/test_cache2-29a-concurrent_read_resumable_entry_size_zero.js
new file mode 100644
index 0000000000..a686aaa046
--- /dev/null
+++ b/netwerk/test/unit/test_cache2-29a-concurrent_read_resumable_entry_size_zero.js
@@ -0,0 +1,74 @@
+/*
+
+Checkes if the concurrent cache read/write works when the write is interrupted because of max-entry-size limits
+This test is using a resumable response.
+- with a profile, set max-entry-size to 0
+- first channel makes a request for a resumable response
+- second channel makes a request for the same resource, concurrent read happens
+- first channel sets predicted data size on the entry, it's doomed
+- second channel now must engage interrupted concurrent write algorithm and read the content again from the network
+- both channels must deliver full content w/o errors
+
+*/
+
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+var httpProtocolHandler = Cc[
+ "@mozilla.org/network/protocol;1?name=http"
+].getService(Ci.nsIHttpProtocolHandler);
+
+XPCOMUtils.defineLazyGetter(this, "URL", function() {
+ return "http://localhost:" + httpServer.identity.primaryPort;
+});
+
+var httpServer = null;
+
+function make_channel(url, callback, ctx) {
+ return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true });
+}
+
+const responseBody = "response body";
+
+function contentHandler(metadata, response) {
+ response.setHeader("Content-Type", "text/plain");
+ response.setHeader("ETag", "Just testing");
+ response.setHeader("Cache-Control", "max-age=99999");
+ response.setHeader("Accept-Ranges", "bytes");
+ response.setHeader("Content-Length", "" + responseBody.length);
+ if (metadata.hasHeader("If-Range")) {
+ response.setStatusLine(metadata.httpVersion, 206, "Partial Content");
+ response.setHeader("Content-Range", "0-12/13");
+ }
+ response.bodyOutputStream.write(responseBody, responseBody.length);
+}
+
+function run_test() {
+ do_get_profile();
+
+ Services.prefs.setIntPref("browser.cache.disk.max_entry_size", 0);
+ Services.prefs.setBoolPref("network.http.rcwn.enabled", false);
+
+ httpServer = new HttpServer();
+ httpServer.registerPathHandler("/content", contentHandler);
+ httpServer.start(-1);
+
+ httpProtocolHandler.EnsureHSTSDataReady().then(function() {
+ var chan1 = make_channel(URL + "/content");
+ chan1.asyncOpen(new ChannelListener(firstTimeThrough, null));
+ var chan2 = make_channel(URL + "/content");
+ chan2.asyncOpen(new ChannelListener(secondTimeThrough, null));
+ });
+
+ do_test_pending();
+}
+
+function firstTimeThrough(request, buffer) {
+ Assert.equal(buffer, responseBody);
+}
+
+function secondTimeThrough(request, buffer) {
+ Assert.equal(buffer, responseBody);
+ httpServer.stop(do_test_finished);
+}
diff --git a/netwerk/test/unit/test_cache2-29b-concurrent_read_non-resumable_entry_size_zero.js b/netwerk/test/unit/test_cache2-29b-concurrent_read_non-resumable_entry_size_zero.js
new file mode 100644
index 0000000000..e0da2202c3
--- /dev/null
+++ b/netwerk/test/unit/test_cache2-29b-concurrent_read_non-resumable_entry_size_zero.js
@@ -0,0 +1,78 @@
+/*
+
+Checkes if the concurrent cache read/write works when the write is interrupted because of max-entry-size limits.
+This test is using a non-resumable response.
+- with a profile, set max-entry-size to 0
+- first channel makes a request for a non-resumable (chunked) response
+- second channel makes a request for the same resource, concurrent read is bypassed (non-resumable response)
+- first channel writes first bytes to the cache output stream, but that fails because of the max-entry-size limit and entry is doomed
+- cache entry output stream is closed
+- second channel gets the entry, opening the input stream must fail
+- second channel must read the content again from the network
+- both channels must deliver full content w/o errors
+
+*/
+
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+var httpProtocolHandler = Cc[
+ "@mozilla.org/network/protocol;1?name=http"
+].getService(Ci.nsIHttpProtocolHandler);
+
+XPCOMUtils.defineLazyGetter(this, "URL", function() {
+ return "http://localhost:" + httpServer.identity.primaryPort;
+});
+
+var httpServer = null;
+
+function make_channel(url, callback, ctx) {
+ return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true });
+}
+
+const responseBody = "c\r\ndata reached\r\n3\r\nhej\r\n0\r\n\r\n";
+const responseBodyDecoded = "data reachedhej";
+
+function contentHandler(metadata, response) {
+ response.seizePower();
+ response.write("HTTP/1.1 200 OK\r\n");
+ response.write("Content-Type: text/plain\r\n");
+ response.write("Transfer-Encoding: chunked\r\n");
+ response.write("\r\n");
+ response.write(responseBody);
+ response.finish();
+}
+
+function run_test() {
+ do_get_profile();
+
+ Services.prefs.setIntPref("browser.cache.disk.max_entry_size", 0);
+ Services.prefs.setBoolPref("network.http.rcwn.enabled", false);
+
+ httpServer = new HttpServer();
+ httpServer.registerPathHandler("/content", contentHandler);
+ httpServer.start(-1);
+
+ httpProtocolHandler.EnsureHSTSDataReady().then(function() {
+ var chan1 = make_channel(URL + "/content");
+ chan1.asyncOpen(
+ new ChannelListener(firstTimeThrough, null, CL_ALLOW_UNKNOWN_CL)
+ );
+ var chan2 = make_channel(URL + "/content");
+ chan2.asyncOpen(
+ new ChannelListener(secondTimeThrough, null, CL_ALLOW_UNKNOWN_CL)
+ );
+ });
+
+ do_test_pending();
+}
+
+function firstTimeThrough(request, buffer) {
+ Assert.equal(buffer, responseBodyDecoded);
+}
+
+function secondTimeThrough(request, buffer) {
+ Assert.equal(buffer, responseBodyDecoded);
+ httpServer.stop(do_test_finished);
+}
diff --git a/netwerk/test/unit/test_cache2-29c-concurrent_read_half-interrupted.js b/netwerk/test/unit/test_cache2-29c-concurrent_read_half-interrupted.js
new file mode 100644
index 0000000000..b76db5c39b
--- /dev/null
+++ b/netwerk/test/unit/test_cache2-29c-concurrent_read_half-interrupted.js
@@ -0,0 +1,93 @@
+/*
+
+Checkes if the concurrent cache read/write works when the write is interrupted because of max-entry-size limits.
+This is enhancement of 29a test, this test checks that cocurrency is resumed when the first channel is interrupted
+in the middle of reading and the second channel already consumed some content from the cache entry.
+This test is using a resumable response.
+- with a profile, set max-entry-size to 1 (=1024 bytes)
+- first channel makes a request for a resumable response
+- second channel makes a request for the same resource, concurrent read happens
+- first channel sets predicted data size on the entry with every chunk, it's doomed on 1024
+- second channel now must engage interrupted concurrent write algorithm and read the rest of the content from the network
+- both channels must deliver full content w/o errors
+
+*/
+
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+var httpProtocolHandler = Cc[
+ "@mozilla.org/network/protocol;1?name=http"
+].getService(Ci.nsIHttpProtocolHandler);
+
+XPCOMUtils.defineLazyGetter(this, "URL", function() {
+ return "http://localhost:" + httpServer.identity.primaryPort;
+});
+
+var httpServer = null;
+
+function make_channel(url, callback, ctx) {
+ return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true });
+}
+
+// need something bigger than 1024 bytes
+const responseBody =
+ "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" +
+ "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" +
+ "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" +
+ "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" +
+ "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" +
+ "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" +
+ "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" +
+ "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" +
+ "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" +
+ "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" +
+ "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
+
+function contentHandler(metadata, response) {
+ response.setHeader("Content-Type", "text/plain");
+ response.setHeader("ETag", "Just testing");
+ response.setHeader("Cache-Control", "max-age=99999");
+ response.setHeader("Accept-Ranges", "bytes");
+ response.setHeader("Content-Length", "" + responseBody.length);
+ if (metadata.hasHeader("If-Range")) {
+ response.setStatusLine(metadata.httpVersion, 206, "Partial Content");
+
+ let len = responseBody.length;
+ response.setHeader("Content-Range", "0-" + (len - 1) + "/" + len);
+ }
+ response.bodyOutputStream.write(responseBody, responseBody.length);
+}
+
+function run_test() {
+ // Static check
+ Assert.ok(responseBody.length > 1024);
+
+ do_get_profile();
+
+ Services.prefs.setIntPref("browser.cache.disk.max_entry_size", 1);
+ Services.prefs.setBoolPref("network.http.rcwn.enabled", false);
+
+ httpServer = new HttpServer();
+ httpServer.registerPathHandler("/content", contentHandler);
+ httpServer.start(-1);
+
+ httpProtocolHandler.EnsureHSTSDataReady().then(function() {
+ var chan1 = make_channel(URL + "/content");
+ chan1.asyncOpen(new ChannelListener(firstTimeThrough, null));
+ var chan2 = make_channel(URL + "/content");
+ chan2.asyncOpen(new ChannelListener(secondTimeThrough, null));
+ });
+
+ do_test_pending();
+}
+
+function firstTimeThrough(request, buffer) {
+ Assert.equal(buffer, responseBody);
+}
+
+function secondTimeThrough(request, buffer) {
+ Assert.equal(buffer, responseBody);
+ httpServer.stop(do_test_finished);
+}
diff --git a/netwerk/test/unit/test_cache2-29d-concurrent_read_half-corrupted-206.js b/netwerk/test/unit/test_cache2-29d-concurrent_read_half-corrupted-206.js
new file mode 100644
index 0000000000..fe64d93007
--- /dev/null
+++ b/netwerk/test/unit/test_cache2-29d-concurrent_read_half-corrupted-206.js
@@ -0,0 +1,93 @@
+/*
+
+Checkes if the concurrent cache read/write works when the write is interrupted because of max-entry-size limits.
+This is enhancement of 29c test, this test checks that a corrupted 206 response is correctly handled (no crashes or asserion failures)
+This test is using a resumable response.
+- with a profile, set max-entry-size to 1 (=1024 bytes)
+- first channel makes a request for a resumable response
+- second channel makes a request for the same resource, concurrent read happens
+- first channel sets predicted data size on the entry with every chunk, it's doomed on 1024
+- second channel now must engage interrupted concurrent write algorithm and read the rest of the content from the network
+- the response to the range request is broken (bad Content-Range header)
+- the first must deliver full content w/o errors
+- the second channel must correctly fail
+
+*/
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+var httpProtocolHandler = Cc[
+ "@mozilla.org/network/protocol;1?name=http"
+].getService(Ci.nsIHttpProtocolHandler);
+
+XPCOMUtils.defineLazyGetter(this, "URL", function() {
+ return "http://localhost:" + httpServer.identity.primaryPort;
+});
+
+var httpServer = null;
+
+function make_channel(url, callback, ctx) {
+ return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true });
+}
+
+// need something bigger than 1024 bytes
+const responseBody =
+ "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" +
+ "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" +
+ "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" +
+ "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" +
+ "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" +
+ "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" +
+ "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" +
+ "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" +
+ "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" +
+ "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" +
+ "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
+
+function contentHandler(metadata, response) {
+ response.setHeader("Content-Type", "text/plain");
+ response.setHeader("ETag", "Just testing");
+ response.setHeader("Cache-Control", "max-age=99999");
+ response.setHeader("Accept-Ranges", "bytes");
+ response.setHeader("Content-Length", "" + responseBody.length);
+ if (metadata.hasHeader("If-Range")) {
+ response.setStatusLine(metadata.httpVersion, 206, "Partial Content");
+ // Deliberately broken response header to trigger corrupted content error on the second channel
+ response.setHeader("Content-Range", "0-1/2");
+ }
+ response.bodyOutputStream.write(responseBody, responseBody.length);
+}
+
+function run_test() {
+ // Static check
+ Assert.ok(responseBody.length > 1024);
+
+ do_get_profile();
+
+ Services.prefs.setIntPref("browser.cache.disk.max_entry_size", 1);
+ Services.prefs.setBoolPref("network.http.rcwn.enabled", false);
+
+ httpServer = new HttpServer();
+ httpServer.registerPathHandler("/content", contentHandler);
+ httpServer.start(-1);
+
+ httpProtocolHandler.EnsureHSTSDataReady().then(function() {
+ var chan1 = make_channel(URL + "/content");
+ chan1.asyncOpen(new ChannelListener(firstTimeThrough, null));
+ var chan2 = make_channel(URL + "/content");
+ chan2.asyncOpen(
+ new ChannelListener(secondTimeThrough, null, CL_EXPECT_FAILURE)
+ );
+ });
+
+ do_test_pending();
+}
+
+function firstTimeThrough(request, buffer) {
+ Assert.equal(buffer, responseBody);
+}
+
+function secondTimeThrough(request, buffer) {
+ httpServer.stop(do_test_finished);
+}
diff --git a/netwerk/test/unit/test_cache2-29e-concurrent_read_half-non-206-response.js b/netwerk/test/unit/test_cache2-29e-concurrent_read_half-non-206-response.js
new file mode 100644
index 0000000000..45e67e63a3
--- /dev/null
+++ b/netwerk/test/unit/test_cache2-29e-concurrent_read_half-non-206-response.js
@@ -0,0 +1,88 @@
+/*
+
+Checkes if the concurrent cache read/write works when the write is interrupted because of max-entry-size limits.
+This is enhancement of 29c test, this test checks that a corrupted 206 response is correctly handled (no crashes or asserion failures)
+This test is using a resumable response.
+- with a profile, set max-entry-size to 1 (=1024 bytes)
+- first channel makes a request for a resumable response
+- second channel makes a request for the same resource, concurrent read happens
+- first channel sets predicted data size on the entry with every chunk, it's doomed on 1024
+- second channel now must engage interrupted concurrent write algorithm and read the rest of the content from the network
+- the response to the range request is plain 200
+- the first must deliver full content w/o errors
+- the second channel must correctly fail
+
+*/
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+var httpProtocolHandler = Cc[
+ "@mozilla.org/network/protocol;1?name=http"
+].getService(Ci.nsIHttpProtocolHandler);
+
+XPCOMUtils.defineLazyGetter(this, "URL", function() {
+ return "http://localhost:" + httpServer.identity.primaryPort;
+});
+
+var httpServer = null;
+
+function make_channel(url, callback, ctx) {
+ return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true });
+}
+
+// need something bigger than 1024 bytes
+const responseBody =
+ "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" +
+ "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" +
+ "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" +
+ "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" +
+ "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" +
+ "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" +
+ "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" +
+ "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" +
+ "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" +
+ "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" +
+ "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
+
+function contentHandler(metadata, response) {
+ response.setHeader("Content-Type", "text/plain");
+ response.setHeader("ETag", "Just testing");
+ response.setHeader("Cache-Control", "max-age=99999");
+ response.setHeader("Accept-Ranges", "bytes");
+ response.setHeader("Content-Length", "" + responseBody.length);
+ response.bodyOutputStream.write(responseBody, responseBody.length);
+}
+
+function run_test() {
+ // Static check
+ Assert.ok(responseBody.length > 1024);
+
+ do_get_profile();
+
+ Services.prefs.setIntPref("browser.cache.disk.max_entry_size", 1);
+ Services.prefs.setBoolPref("network.http.rcwn.enabled", false);
+
+ httpServer = new HttpServer();
+ httpServer.registerPathHandler("/content", contentHandler);
+ httpServer.start(-1);
+
+ httpProtocolHandler.EnsureHSTSDataReady().then(function() {
+ var chan1 = make_channel(URL + "/content");
+ chan1.asyncOpen(new ChannelListener(firstTimeThrough, null));
+ var chan2 = make_channel(URL + "/content");
+ chan2.asyncOpen(
+ new ChannelListener(secondTimeThrough, null, CL_EXPECT_FAILURE)
+ );
+ });
+
+ do_test_pending();
+}
+
+function firstTimeThrough(request, buffer) {
+ Assert.equal(buffer, responseBody);
+}
+
+function secondTimeThrough(request, buffer) {
+ httpServer.stop(do_test_finished);
+}
diff --git a/netwerk/test/unit/test_cache2-30a-entry-pinning.js b/netwerk/test/unit/test_cache2-30a-entry-pinning.js
new file mode 100644
index 0000000000..f5ef4e69f3
--- /dev/null
+++ b/netwerk/test/unit/test_cache2-30a-entry-pinning.js
@@ -0,0 +1,39 @@
+"use strict";
+
+function run_test() {
+ do_get_profile();
+
+ // Open for write, write
+ asyncOpenCacheEntry(
+ "http://a/",
+ "pin",
+ Ci.nsICacheStorage.OPEN_TRUNCATE,
+ Services.loadContextInfo.default,
+ new OpenCallback(NEW | WAITFORWRITE, "a1m", "a1d", function(entry) {
+ // Open for read and check
+ asyncOpenCacheEntry(
+ "http://a/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ Services.loadContextInfo.default,
+ new OpenCallback(NORMAL, "a1m", "a1d", function(entry) {
+ // Now clear the whole cache
+ get_cache_service().clear();
+
+ // The pinned entry should be intact
+ asyncOpenCacheEntry(
+ "http://a/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ Services.loadContextInfo.default,
+ new OpenCallback(NORMAL, "a1m", "a1d", function(entry) {
+ finish_cache2_test();
+ })
+ );
+ })
+ );
+ })
+ );
+
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_cache2-30b-pinning-storage-clear.js b/netwerk/test/unit/test_cache2-30b-pinning-storage-clear.js
new file mode 100644
index 0000000000..68dcd995cc
--- /dev/null
+++ b/netwerk/test/unit/test_cache2-30b-pinning-storage-clear.js
@@ -0,0 +1,45 @@
+"use strict";
+
+function run_test() {
+ do_get_profile();
+
+ var lci = Services.loadContextInfo.default;
+
+ // Open a pinned entry for write, write
+ asyncOpenCacheEntry(
+ "http://a/",
+ "pin",
+ Ci.nsICacheStorage.OPEN_TRUNCATE,
+ lci,
+ new OpenCallback(NEW | WAITFORWRITE, "a1m", "a1d", function(entry) {
+ // Now clear the disk storage, that should leave the pinned entry in the cache
+ var diskStorage = getCacheStorage("disk", lci);
+ diskStorage.asyncEvictStorage(null);
+
+ // Open for read and check, it should still be there
+ asyncOpenCacheEntry(
+ "http://a/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ lci,
+ new OpenCallback(NORMAL, "a1m", "a1d", function(entry) {
+ // Now clear the pinning storage, entry should be gone
+ var pinningStorage = getCacheStorage("pin", lci);
+ pinningStorage.asyncEvictStorage(null);
+
+ asyncOpenCacheEntry(
+ "http://a/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ lci,
+ new OpenCallback(NEW, "", "", function(entry) {
+ finish_cache2_test();
+ })
+ );
+ })
+ );
+ })
+ );
+
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_cache2-30c-pinning-deferred-doom.js b/netwerk/test/unit/test_cache2-30c-pinning-deferred-doom.js
new file mode 100644
index 0000000000..bb5ed79b85
--- /dev/null
+++ b/netwerk/test/unit/test_cache2-30c-pinning-deferred-doom.js
@@ -0,0 +1,188 @@
+/*
+
+This is a complex test checking the internal "deferred doom" functionality in both CacheEntry and CacheFileHandle.
+
+- We create a batch of 10 non-pinned and 10 pinned entries, write something to them.
+- Then we purge them from memory, so they have to reload from disk.
+- After that the IO thread is suspended not to process events on the READ (3) level. This forces opening operation and eviction
+ sync operations happen before we know actual pinning status of already cached entries.
+- We async-open the same batch of the 10+10 entries again, all should open as existing with the expected, previously stored
+ content
+- After all these entries are made to open, we clear the cache. This does some synchronous operations on the entries
+ being open and also on the handles being in an already open state (but before the entry metadata has started to be read.)
+ Expected is to leave the pinned entries only.
+- Now, we resume the IO thread, so it start reading. One could say this is a hack, but this can very well happen in reality
+ on slow disk or when a large number of entries is about to be open at once. Suspending the IO thread is just doing this
+ simulation is a fully deterministic way and actually very easily and elegantly.
+- After the resume we want to open all those 10+10 entries once again (no purgin involved this time.). It is expected
+ to open all the pinning entries intact and loose all the non-pinned entries (get them as new and empty again.)
+
+*/
+
+"use strict";
+
+const kENTRYCOUNT = 10;
+
+function log_(msg) {
+ if (true) {
+ dump(">>>>>>>>>>>>> " + msg + "\n");
+ }
+}
+
+function run_test() {
+ do_get_profile();
+
+ var lci = Services.loadContextInfo.default;
+ var testingInterface = get_cache_service().QueryInterface(Ci.nsICacheTesting);
+ Assert.ok(testingInterface);
+
+ var mc = new MultipleCallbacks(
+ 1,
+ function() {
+ // (2)
+
+ mc = new MultipleCallbacks(1, finish_cache2_test);
+ // Release all references to cache entries so that they can be purged
+ // Calling gc() four times is needed to force it to actually release
+ // entries that are obviously unreferenced. Yeah, I know, this is wacky...
+ gc();
+ gc();
+ executeSoon(() => {
+ gc();
+ gc();
+ log_("purging");
+
+ // Invokes cacheservice:purge-memory-pools when done.
+ get_cache_service().purgeFromMemory(
+ Ci.nsICacheStorageService.PURGE_EVERYTHING
+ ); // goes to (3)
+ });
+ },
+ true
+ );
+
+ // (1), here we start
+
+ var i;
+ for (i = 0; i < kENTRYCOUNT; ++i) {
+ log_("first set of opens");
+
+ // Callbacks 1-20
+ mc.add();
+ asyncOpenCacheEntry(
+ "http://pinned" + i + "/",
+ "pin",
+ Ci.nsICacheStorage.OPEN_TRUNCATE,
+ lci,
+ new OpenCallback(NEW | WAITFORWRITE, "m" + i, "p" + i, function(entry) {
+ mc.fired();
+ })
+ );
+
+ mc.add();
+ asyncOpenCacheEntry(
+ "http://common" + i + "/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_TRUNCATE,
+ lci,
+ new OpenCallback(NEW | WAITFORWRITE, "m" + i, "d" + i, function(entry) {
+ mc.fired();
+ })
+ );
+ }
+
+ mc.fired(); // Goes to (2)
+
+ var os = Cc["@mozilla.org/observer-service;1"].getService(
+ Ci.nsIObserverService
+ );
+ os.addObserver(
+ {
+ observe(subject, topic, data) {
+ // (3)
+
+ log_("after purge, second set of opens");
+ // Prevent the I/O thread from reading the data. We first want to schedule clear of the cache.
+ // This deterministically emulates a slow hard drive.
+ testingInterface.suspendCacheIOThread(3);
+
+ // All entries should load
+ // Callbacks 21-40
+ for (i = 0; i < kENTRYCOUNT; ++i) {
+ mc.add();
+ asyncOpenCacheEntry(
+ "http://pinned" + i + "/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ lci,
+ new OpenCallback(NORMAL, "m" + i, "p" + i, function(entry) {
+ mc.fired();
+ })
+ );
+
+ // Unfortunately we cannot ensure that entries existing in the cache will be delivered to the consumer
+ // when soon after are evicted by some cache API call. It's better to not ensure getting an entry
+ // than allowing to get an entry that was just evicted from the cache. Entries may be delievered
+ // as new, but are already doomed. Output stream cannot be openned, or the file handle is already
+ // writing to a doomed file.
+ //
+ // The API now just ensures that entries removed by any of the cache eviction APIs are never more
+ // available to consumers.
+ mc.add();
+ asyncOpenCacheEntry(
+ "http://common" + i + "/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ lci,
+ new OpenCallback(MAYBE_NEW | DOOMED, "m" + i, "d" + i, function(
+ entry
+ ) {
+ mc.fired();
+ })
+ );
+ }
+
+ log_("clearing");
+ // Now clear everything except pinned, all entries are in state of reading
+ get_cache_service().clear();
+ log_("cleared");
+
+ // Resume reading the cache data, only now the pinning status on entries will be discovered,
+ // the deferred dooming code will trigger.
+ testingInterface.resumeCacheIOThread();
+
+ log_("third set of opens");
+ // Now open again. Pinned entries should be there, disk entries should be the renewed entries.
+ // Callbacks 41-60
+ for (i = 0; i < kENTRYCOUNT; ++i) {
+ mc.add();
+ asyncOpenCacheEntry(
+ "http://pinned" + i + "/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ lci,
+ new OpenCallback(NORMAL, "m" + i, "p" + i, function(entry) {
+ mc.fired();
+ })
+ );
+
+ mc.add();
+ asyncOpenCacheEntry(
+ "http://common" + i + "/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ lci,
+ new OpenCallback(NEW, "m2" + i, "d2" + i, function(entry) {
+ mc.fired();
+ })
+ );
+ }
+
+ mc.fired(); // Finishes this test
+ },
+ },
+ "cacheservice:purge-memory-pools"
+ );
+
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_cache2-30d-pinning-WasEvicted-API.js b/netwerk/test/unit/test_cache2-30d-pinning-WasEvicted-API.js
new file mode 100644
index 0000000000..74272072d2
--- /dev/null
+++ b/netwerk/test/unit/test_cache2-30d-pinning-WasEvicted-API.js
@@ -0,0 +1,151 @@
+/*
+
+This test exercises the CacheFileContextEvictor::WasEvicted API and code using it.
+
+- We store 10+10 (pinned and non-pinned) entries to the cache, wait for them being written.
+- Then we purge the memory pools.
+- Now the IO thread is suspended on the EVICT (7) level to prevent actual deletion of the files.
+- Index is disabled.
+- We do clear() of the cache, this creates the "ce_*" file and posts to the EVICT level
+ the eviction loop mechanics.
+- We open again those 10+10 entries previously stored.
+- IO is resumed
+- We expect to get all the pinned and
+ loose all the non-pinned (common) entries.
+
+*/
+
+"use strict";
+
+const kENTRYCOUNT = 10;
+
+function log_(msg) {
+ if (true) {
+ dump(">>>>>>>>>>>>> " + msg + "\n");
+ }
+}
+
+function run_test() {
+ do_get_profile();
+
+ var lci = Services.loadContextInfo.default;
+ var testingInterface = get_cache_service().QueryInterface(Ci.nsICacheTesting);
+ Assert.ok(testingInterface);
+
+ var mc = new MultipleCallbacks(
+ 1,
+ function() {
+ // (2)
+
+ mc = new MultipleCallbacks(1, finish_cache2_test);
+ // Release all references to cache entries so that they can be purged
+ // Calling gc() four times is needed to force it to actually release
+ // entries that are obviously unreferenced. Yeah, I know, this is wacky...
+ gc();
+ gc();
+ executeSoon(() => {
+ gc();
+ gc();
+ log_("purging");
+
+ // Invokes cacheservice:purge-memory-pools when done.
+ get_cache_service().purgeFromMemory(
+ Ci.nsICacheStorageService.PURGE_EVERYTHING
+ ); // goes to (3)
+ });
+ },
+ true
+ );
+
+ // (1), here we start
+
+ log_("first set of opens");
+ var i;
+ for (i = 0; i < kENTRYCOUNT; ++i) {
+ // Callbacks 1-20
+ mc.add();
+ asyncOpenCacheEntry(
+ "http://pinned" + i + "/",
+ "pin",
+ Ci.nsICacheStorage.OPEN_TRUNCATE,
+ lci,
+ new OpenCallback(NEW | WAITFORWRITE, "m" + i, "p" + i, function(entry) {
+ mc.fired();
+ })
+ );
+
+ mc.add();
+ asyncOpenCacheEntry(
+ "http://common" + i + "/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_TRUNCATE,
+ lci,
+ new OpenCallback(NEW | WAITFORWRITE, "m" + i, "d" + i, function(entry) {
+ mc.fired();
+ })
+ );
+ }
+
+ mc.fired(); // Goes to (2)
+
+ var os = Cc["@mozilla.org/observer-service;1"].getService(
+ Ci.nsIObserverService
+ );
+ os.addObserver(
+ {
+ observe(subject, topic, data) {
+ // (3)
+
+ log_("after purge");
+ // Prevent the I/O thread from evicting physically the data. We first want to re-open the entries.
+ // This deterministically emulates a slow hard drive.
+ testingInterface.suspendCacheIOThread(7);
+
+ log_("clearing");
+ // Now clear everything except pinned. Stores the "ce_*" file and schedules background eviction.
+ get_cache_service().clear();
+ log_("cleared");
+
+ log_("second set of opens");
+ // Now open again. Pinned entries should be there, disk entries should be the renewed entries.
+ // Callbacks 21-40
+ for (i = 0; i < kENTRYCOUNT; ++i) {
+ mc.add();
+ asyncOpenCacheEntry(
+ "http://pinned" + i + "/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ lci,
+ new OpenCallback(NORMAL, "m" + i, "p" + i, function(entry) {
+ mc.fired();
+ })
+ );
+
+ mc.add();
+ asyncOpenCacheEntry(
+ "http://common" + i + "/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ lci,
+ new OpenCallback(NEW, "m2" + i, "d2" + i, function(entry) {
+ mc.fired();
+ })
+ );
+ }
+
+ // Resume IO, this will just pop-off the CacheFileContextEvictor::EvictEntries() because of
+ // an early check on CacheIOThread::YieldAndRerun() in that method.
+ // CacheFileIOManager::OpenFileInternal should now run and CacheFileContextEvictor::WasEvicted
+ // should be checked on.
+ log_("resuming");
+ testingInterface.resumeCacheIOThread();
+ log_("resumed");
+
+ mc.fired(); // Finishes this test
+ },
+ },
+ "cacheservice:purge-memory-pools"
+ );
+
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_cache2-31-visit-all.js b/netwerk/test/unit/test_cache2-31-visit-all.js
new file mode 100644
index 0000000000..b9f1b7eec7
--- /dev/null
+++ b/netwerk/test/unit/test_cache2-31-visit-all.js
@@ -0,0 +1,88 @@
+"use strict";
+
+function run_test() {
+ getCacheStorage("disk");
+ var lcis = [
+ Services.loadContextInfo.default,
+ Services.loadContextInfo.custom(false, { userContextId: 1 }),
+ Services.loadContextInfo.custom(false, { userContextId: 2 }),
+ Services.loadContextInfo.custom(false, { userContextId: 3 }),
+ ];
+
+ do_get_profile();
+
+ var mc = new MultipleCallbacks(
+ 8,
+ function() {
+ executeSoon(function() {
+ var expectedConsumption = 8192;
+ var entries = [
+ { uri: "http://a/", lci: lcis[0] }, // default
+ { uri: "http://b/", lci: lcis[0] }, // default
+ { uri: "http://a/", lci: lcis[1] }, // user Context 1
+ { uri: "http://b/", lci: lcis[1] }, // user Context 1
+ { uri: "http://a/", lci: lcis[2] }, // user Context 2
+ { uri: "http://b/", lci: lcis[2] }, // user Context 2
+ { uri: "http://a/", lci: lcis[3] }, // user Context 3
+ { uri: "http://b/", lci: lcis[3] },
+ ]; // user Context 3
+
+ get_cache_service().asyncVisitAllStorages(
+ // Test should store 8 entries across 4 originAttributes
+ new VisitCallback(8, expectedConsumption, entries, function() {
+ get_cache_service().asyncVisitAllStorages(
+ // Still 8 entries expected, now don't walk them
+ new VisitCallback(8, expectedConsumption, null, function() {
+ finish_cache2_test();
+ }),
+ false
+ );
+ }),
+ true
+ );
+ });
+ },
+ true
+ );
+
+ // Add two cache entries for each originAttributes.
+ for (var i = 0; i < lcis.length; i++) {
+ asyncOpenCacheEntry(
+ "http://a/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ lcis[i],
+ new OpenCallback(NEW, "a1m", "a1d", function(entry) {
+ asyncOpenCacheEntry(
+ "http://a/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ lcis[i],
+ new OpenCallback(NORMAL, "a1m", "a1d", function(entry) {
+ mc.fired();
+ })
+ );
+ })
+ );
+
+ asyncOpenCacheEntry(
+ "http://b/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ lcis[i],
+ new OpenCallback(NEW, "b1m", "b1d", function(entry) {
+ asyncOpenCacheEntry(
+ "http://b/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ lcis[i],
+ new OpenCallback(NORMAL, "b1m", "b1d", function(entry) {
+ mc.fired();
+ })
+ );
+ })
+ );
+ }
+
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_cache2-32-clear-origin.js b/netwerk/test/unit/test_cache2-32-clear-origin.js
new file mode 100644
index 0000000000..52987a576f
--- /dev/null
+++ b/netwerk/test/unit/test_cache2-32-clear-origin.js
@@ -0,0 +1,68 @@
+"use strict";
+
+const URL = "http://example.net";
+const URL2 = "http://foo.bar";
+
+function run_test() {
+ do_get_profile();
+
+ asyncOpenCacheEntry(
+ URL + "/a",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NEW, "e1m", "e1d", function(entry) {
+ asyncOpenCacheEntry(
+ URL + "/a",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NORMAL, "e1m", "e1d", function(entry) {
+ asyncOpenCacheEntry(
+ URL2 + "/a",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NEW, "f1m", "f1d", function(entry) {
+ asyncOpenCacheEntry(
+ URL2 + "/a",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NORMAL, "f1m", "f1d", function(entry) {
+ var url = Services.io.newURI(URL);
+ var principal = Services.scriptSecurityManager.createContentPrincipal(
+ url,
+ {}
+ );
+
+ get_cache_service().clearOrigin(principal);
+
+ asyncOpenCacheEntry(
+ URL + "/a",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NEW, "e1m", "e1d", function(entry) {
+ asyncOpenCacheEntry(
+ URL2 + "/a",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ new OpenCallback(NORMAL, "f1m", "f1d", function(entry) {
+ finish_cache2_test();
+ })
+ );
+ })
+ );
+ })
+ );
+ })
+ );
+ })
+ );
+ })
+ );
+
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_cacheForOfflineUse_no-store.js b/netwerk/test/unit/test_cacheForOfflineUse_no-store.js
new file mode 100644
index 0000000000..9d8c14d657
--- /dev/null
+++ b/netwerk/test/unit/test_cacheForOfflineUse_no-store.js
@@ -0,0 +1,104 @@
+"use strict";
+// https://bugzilla.mozilla.org/show_bug.cgi?id=760955
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+var httpServer = null;
+const testFileName = "test_nsHttpChannel_CacheForOfflineUse-no-store";
+const cacheClientID = testFileName + "|fake-group-id";
+const basePath = "/" + testFileName + "/";
+
+XPCOMUtils.defineLazyGetter(this, "baseURI", function() {
+ return "http://localhost:" + httpServer.identity.primaryPort + basePath;
+});
+
+const normalEntry = "normal";
+const noStoreEntry = "no-store";
+
+var cacheUpdateObserver = null;
+var appCache = null;
+
+function make_channel_for_offline_use(url, callback, ctx) {
+ var chan = NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true });
+
+ var cacheService = Cc[
+ "@mozilla.org/network/application-cache-service;1"
+ ].getService(Ci.nsIApplicationCacheService);
+ appCache = cacheService.getApplicationCache(cacheClientID);
+
+ var appCacheChan = chan.QueryInterface(Ci.nsIApplicationCacheChannel);
+ appCacheChan.applicationCacheForWrite = appCache;
+ return chan;
+}
+
+function make_uri(url) {
+ var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
+ return ios.newURI(url);
+}
+
+const responseBody = "response body";
+
+// A HTTP channel for updating the offline cache should normally succeed.
+function normalHandler(metadata, response) {
+ info("normalHandler");
+ response.setHeader("Content-Type", "text/plain");
+ response.bodyOutputStream.write(responseBody, responseBody.length);
+}
+function checkNormal(request, buffer) {
+ Assert.equal(buffer, responseBody);
+ asyncCheckCacheEntryPresence(
+ baseURI + normalEntry,
+ "appcache",
+ true,
+ run_next_test,
+ appCache
+ );
+}
+add_test(function test_normal() {
+ var chan = make_channel_for_offline_use(baseURI + normalEntry);
+ chan.asyncOpen(new ChannelListener(checkNormal, chan));
+});
+
+// An HTTP channel for updating the offline cache should fail when it gets a
+// response with Cache-Control: no-store.
+function noStoreHandler(metadata, response) {
+ info("noStoreHandler");
+ response.setHeader("Content-Type", "text/plain");
+ response.setHeader("Cache-Control", "no-store");
+ response.bodyOutputStream.write(responseBody, responseBody.length);
+}
+function checkNoStore(request, buffer) {
+ Assert.equal(buffer, "");
+ asyncCheckCacheEntryPresence(
+ baseURI + noStoreEntry,
+ "appcache",
+ false,
+ run_next_test,
+ appCache
+ );
+}
+add_test(function test_noStore() {
+ var chan = make_channel_for_offline_use(baseURI + noStoreEntry);
+ // The no-store should cause the channel to fail to load.
+ chan.asyncOpen(new ChannelListener(checkNoStore, chan, CL_EXPECT_FAILURE));
+});
+
+function run_test() {
+ do_get_profile();
+
+ var ps = Cc["@mozilla.org/preferences-service;1"].getService(
+ Ci.nsIPrefBranch
+ );
+ ps.setBoolPref("browser.cache.offline.enable", true);
+ ps.setBoolPref("browser.cache.offline.storage.enable", true);
+
+ httpServer = new HttpServer();
+ httpServer.registerPathHandler(basePath + normalEntry, normalHandler);
+ httpServer.registerPathHandler(basePath + noStoreEntry, noStoreHandler);
+ httpServer.start(-1);
+ run_next_test();
+}
+
+function finish_test(request, buffer) {
+ httpServer.stop(do_test_finished);
+}
diff --git a/netwerk/test/unit/test_cache_204_response.js b/netwerk/test/unit/test_cache_204_response.js
new file mode 100644
index 0000000000..aeb4f4f1d2
--- /dev/null
+++ b/netwerk/test/unit/test_cache_204_response.js
@@ -0,0 +1,60 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*
+Test if 204 response is cached.
+1. Make first http request and return a 204 response.
+2. Check if the first response is not cached.
+3. Make second http request and check if the response is cached.
+
+*/
+
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+function test_handler(metadata, response) {
+ response.setHeader("Content-Type", "text/html", false);
+ response.setHeader("Cache-control", "max-age=9999", false);
+ response.setStatusLine(metadata.httpVersion, 204, "No Content");
+}
+
+function make_channel(url) {
+ let channel = NetUtil.newChannel({
+ uri: url,
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+ return channel;
+}
+
+async function get_response(channel, fromCache) {
+ return new Promise(resolve => {
+ channel.asyncOpen(
+ new ChannelListener((request, buffer, ctx, isFromCache) => {
+ ok(fromCache == isFromCache, `got response from cache = ${fromCache}`);
+ resolve();
+ })
+ );
+ });
+}
+
+async function stop_server(httpserver) {
+ return new Promise(resolve => {
+ httpserver.stop(resolve);
+ });
+}
+
+add_task(async function() {
+ let httpserver = new HttpServer();
+ httpserver.registerPathHandler("/testdir", test_handler);
+ httpserver.start(-1);
+ const PORT = httpserver.identity.primaryPort;
+ const URI = `http://localhost:${PORT}/testdir`;
+
+ await get_response(make_channel(URI, "GET"), false);
+ await get_response(make_channel(URI, "GET"), true);
+
+ await stop_server(httpserver);
+});
diff --git a/netwerk/test/unit/test_cache_jar.js b/netwerk/test/unit/test_cache_jar.js
new file mode 100644
index 0000000000..554759be21
--- /dev/null
+++ b/netwerk/test/unit/test_cache_jar.js
@@ -0,0 +1,104 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+XPCOMUtils.defineLazyGetter(this, "URL", function() {
+ return "http://localhost:" + httpserv.identity.primaryPort + "/cached";
+});
+
+var httpserv = null;
+var handlers_called = 0;
+
+function cached_handler(metadata, response) {
+ response.setHeader("Content-Type", "text/plain", false);
+ response.setHeader("Cache-Control", "max-age=10000", false);
+ response.setStatusLine(metadata.httpVersion, 200, "OK");
+ var body = "0123456789";
+ response.bodyOutputStream.write(body, body.length);
+ handlers_called++;
+}
+
+function makeChan(url, inIsolatedMozBrowser, userContextId) {
+ var chan = NetUtil.newChannel({
+ uri: url,
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+ chan.loadInfo.originAttributes = { inIsolatedMozBrowser, userContextId };
+ return chan;
+}
+
+// [inIsolatedMozBrowser, userContextId, expected_handlers_called]
+var firstTests = [
+ [false, 0, 1],
+ [true, 0, 1],
+ [false, 1, 1],
+ [true, 1, 1],
+];
+var secondTests = [
+ [false, 0, 0],
+ [true, 0, 0],
+ [false, 1, 1],
+ [true, 1, 0],
+];
+
+async function run_all_tests() {
+ for (let test of firstTests) {
+ handlers_called = 0;
+ await test_channel(...test);
+ }
+
+ // We can't easily cause webapp data to be cleared from the child process, so skip
+ // the rest of these tests.
+ let procType = Cc["@mozilla.org/xre/runtime;1"].getService(Ci.nsIXULRuntime)
+ .processType;
+ if (procType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT) {
+ return;
+ }
+
+ Services.clearData.deleteDataFromOriginAttributesPattern({
+ userContextId: 1,
+ });
+
+ for (let test of secondTests) {
+ handlers_called = 0;
+ await test_channel(...test);
+ }
+}
+
+function run_test() {
+ do_get_profile();
+
+ do_test_pending();
+
+ Services.prefs.setBoolPref("network.http.rcwn.enabled", false);
+
+ httpserv = new HttpServer();
+ httpserv.registerPathHandler("/cached", cached_handler);
+ httpserv.start(-1);
+ run_all_tests().then(() => {
+ do_test_finished();
+ });
+}
+
+function test_channel(inIsolatedMozBrowser, userContextId, expected) {
+ return new Promise(resolve => {
+ var chan = makeChan(URL, inIsolatedMozBrowser, userContextId);
+ chan.asyncOpen(
+ new ChannelListener(doneFirstLoad.bind(null, resolve), expected)
+ );
+ });
+}
+
+function doneFirstLoad(resolve, req, buffer, expected) {
+ // Load it again, make sure it hits the cache
+ var oa = req.loadInfo.originAttributes;
+ var chan = makeChan(URL, oa.isInIsolatedMozBrowserElement, oa.userContextId);
+ chan.asyncOpen(
+ new ChannelListener(doneSecondLoad.bind(null, resolve), expected)
+ );
+}
+
+function doneSecondLoad(resolve, req, buffer, expected) {
+ Assert.equal(handlers_called, expected);
+ resolve();
+}
diff --git a/netwerk/test/unit/test_cacheflags.js b/netwerk/test/unit/test_cacheflags.js
new file mode 100644
index 0000000000..4c0ae215f5
--- /dev/null
+++ b/netwerk/test/unit/test_cacheflags.js
@@ -0,0 +1,436 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+var httpserver = new HttpServer();
+httpserver.start(-1);
+
+// Need to randomize, because apparently no one clears our cache
+var suffix = Math.random();
+var httpBase = "http://localhost:" + httpserver.identity.primaryPort;
+var httpsBase = "http://localhost:4445";
+var shortexpPath = "/shortexp" + suffix;
+var longexpPath = "/longexp/" + suffix;
+var longexp2Path = "/longexp/2/" + suffix;
+var nocachePath = "/nocache" + suffix;
+var nostorePath = "/nostore" + suffix;
+var test410Path = "/test410" + suffix;
+var test404Path = "/test404" + suffix;
+
+var PrivateBrowsingLoadContext = Cu.createPrivateLoadContext();
+
+function make_channel(url, flags, usePrivateBrowsing) {
+ var securityFlags = Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL;
+
+ var uri = Services.io.newURI(url);
+ var principal = Services.scriptSecurityManager.createContentPrincipal(uri, {
+ privateBrowsingId: usePrivateBrowsing ? 1 : 0,
+ });
+
+ var req = NetUtil.newChannel({
+ uri,
+ loadingPrincipal: principal,
+ securityFlags,
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER,
+ });
+
+ req.loadFlags = flags;
+ if (usePrivateBrowsing) {
+ req.notificationCallbacks = PrivateBrowsingLoadContext;
+ }
+ return req;
+}
+
+function Test(
+ path,
+ flags,
+ expectSuccess,
+ readFromCache,
+ hitServer,
+ usePrivateBrowsing /* defaults to false */
+) {
+ this.path = path;
+ this.flags = flags;
+ this.expectSuccess = expectSuccess;
+ this.readFromCache = readFromCache;
+ this.hitServer = hitServer;
+ this.usePrivateBrowsing = usePrivateBrowsing;
+}
+
+Test.prototype = {
+ flags: 0,
+ expectSuccess: true,
+ readFromCache: false,
+ hitServer: true,
+ usePrivateBrowsing: false,
+ _buffer: "",
+ _isFromCache: false,
+
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIStreamListener",
+ "nsIRequestObserver",
+ ]),
+
+ onStartRequest(request) {
+ var cachingChannel = request.QueryInterface(Ci.nsICacheInfoChannel);
+ this._isFromCache = request.isPending() && cachingChannel.isFromCache();
+ },
+
+ onDataAvailable(request, stream, offset, count) {
+ this._buffer = this._buffer.concat(read_stream(stream, count));
+ },
+
+ onStopRequest(request, status) {
+ Assert.equal(Components.isSuccessCode(status), this.expectSuccess);
+ Assert.equal(this._isFromCache, this.readFromCache);
+ Assert.equal(gHitServer, this.hitServer);
+
+ do_timeout(0, run_next_test);
+ },
+
+ run() {
+ dump(
+ "Running:" +
+ "\n " +
+ this.path +
+ "\n " +
+ this.flags +
+ "\n " +
+ this.expectSuccess +
+ "\n " +
+ this.readFromCache +
+ "\n " +
+ this.hitServer +
+ "\n"
+ );
+ gHitServer = false;
+ var channel = make_channel(this.path, this.flags, this.usePrivateBrowsing);
+ channel.asyncOpen(this);
+ },
+};
+
+var gHitServer = false;
+
+var gTests = [
+ new Test(
+ httpBase + shortexpPath,
+ 0,
+ true, // expect success
+ false, // read from cache
+ true, // hit server
+ true
+ ), // USE PRIVATE BROWSING, so not cached for later requests
+ new Test(
+ httpBase + shortexpPath,
+ 0,
+ true, // expect success
+ false, // read from cache
+ true
+ ), // hit server
+ new Test(
+ httpBase + shortexpPath,
+ 0,
+ true, // expect success
+ true, // read from cache
+ true
+ ), // hit server
+ new Test(
+ httpBase + shortexpPath,
+ Ci.nsIRequest.LOAD_BYPASS_CACHE,
+ true, // expect success
+ false, // read from cache
+ true
+ ), // hit server
+ new Test(
+ httpBase + shortexpPath,
+ Ci.nsICachingChannel.LOAD_ONLY_FROM_CACHE,
+ false, // expect success
+ false, // read from cache
+ false
+ ), // hit server
+ new Test(
+ httpBase + shortexpPath,
+ Ci.nsICachingChannel.LOAD_ONLY_FROM_CACHE | Ci.nsIRequest.VALIDATE_NEVER,
+ true, // expect success
+ true, // read from cache
+ false
+ ), // hit server
+ new Test(
+ httpBase + shortexpPath,
+ Ci.nsIRequest.LOAD_FROM_CACHE,
+ true, // expect success
+ true, // read from cache
+ false
+ ), // hit server
+
+ new Test(
+ httpBase + longexpPath,
+ 0,
+ true, // expect success
+ false, // read from cache
+ true
+ ), // hit server
+ new Test(
+ httpBase + longexpPath,
+ 0,
+ true, // expect success
+ true, // read from cache
+ false
+ ), // hit server
+ new Test(
+ httpBase + longexpPath,
+ Ci.nsIRequest.LOAD_BYPASS_CACHE,
+ true, // expect success
+ false, // read from cache
+ true
+ ), // hit server
+ new Test(
+ httpBase + longexpPath,
+ Ci.nsIRequest.VALIDATE_ALWAYS,
+ true, // expect success
+ true, // read from cache
+ true
+ ), // hit server
+ new Test(
+ httpBase + longexpPath,
+ Ci.nsICachingChannel.LOAD_ONLY_FROM_CACHE,
+ true, // expect success
+ true, // read from cache
+ false
+ ), // hit server
+ new Test(
+ httpBase + longexpPath,
+ Ci.nsICachingChannel.LOAD_ONLY_FROM_CACHE | Ci.nsIRequest.VALIDATE_NEVER,
+ true, // expect success
+ true, // read from cache
+ false
+ ), // hit server
+ new Test(
+ httpBase + longexpPath,
+ Ci.nsICachingChannel.LOAD_ONLY_FROM_CACHE | Ci.nsIRequest.VALIDATE_ALWAYS,
+ false, // expect success
+ false, // read from cache
+ false
+ ), // hit server
+ new Test(
+ httpBase + longexpPath,
+ Ci.nsIRequest.LOAD_FROM_CACHE,
+ true, // expect success
+ true, // read from cache
+ false
+ ), // hit server
+
+ new Test(
+ httpBase + longexp2Path,
+ 0,
+ true, // expect success
+ false, // read from cache
+ true
+ ), // hit server
+ new Test(
+ httpBase + longexp2Path,
+ 0,
+ true, // expect success
+ true, // read from cache
+ false
+ ), // hit server
+
+ new Test(
+ httpBase + nocachePath,
+ 0,
+ true, // expect success
+ false, // read from cache
+ true
+ ), // hit server
+ new Test(
+ httpBase + nocachePath,
+ 0,
+ true, // expect success
+ true, // read from cache
+ true
+ ), // hit server
+ new Test(
+ httpBase + nocachePath,
+ Ci.nsICachingChannel.LOAD_ONLY_FROM_CACHE,
+ false, // expect success
+ false, // read from cache
+ false
+ ), // hit server
+
+ // CACHE2: mayhemer - entry is doomed... I think the logic is wrong, we should not doom them
+ // as they are not valid, but take them as they need to reval
+ /*
+ new Test(httpBase + nocachePath, Ci.nsIRequest.LOAD_FROM_CACHE,
+ true, // expect success
+ true, // read from cache
+ false), // hit server
+ */
+
+ // LOAD_ONLY_FROM_CACHE would normally fail (because no-cache forces
+ // a validation), but VALIDATE_NEVER should override that.
+ new Test(
+ httpBase + nocachePath,
+ Ci.nsICachingChannel.LOAD_ONLY_FROM_CACHE | Ci.nsIRequest.VALIDATE_NEVER,
+ true, // expect success
+ true, // read from cache
+ false
+ ), // hit server
+
+ // ... however, no-cache over ssl should act like no-store and force
+ // a validation (and therefore failure) even if VALIDATE_NEVER is
+ // set.
+ /* XXX bug 466524: We can't currently start an ssl server in xpcshell tests,
+ so this test is currently disabled.
+ new Test(httpsBase + nocachePath,
+ Ci.nsICachingChannel.LOAD_ONLY_FROM_CACHE |
+ Ci.nsIRequest.VALIDATE_NEVER,
+ false, // expect success
+ false, // read from cache
+ false) // hit server
+ */
+
+ new Test(
+ httpBase + nostorePath,
+ 0,
+ true, // expect success
+ false, // read from cache
+ true
+ ), // hit server
+ new Test(
+ httpBase + nostorePath,
+ 0,
+ true, // expect success
+ false, // read from cache
+ true
+ ), // hit server
+ new Test(
+ httpBase + nostorePath,
+ Ci.nsICachingChannel.LOAD_ONLY_FROM_CACHE,
+ false, // expect success
+ false, // read from cache
+ false
+ ), // hit server
+ new Test(
+ httpBase + nostorePath,
+ Ci.nsIRequest.LOAD_FROM_CACHE,
+ true, // expect success
+ true, // read from cache
+ false
+ ), // hit server
+ // no-store should force the validation (and therefore failure, with
+ // LOAD_ONLY_FROM_CACHE) even if VALIDATE_NEVER is set.
+ new Test(
+ httpBase + nostorePath,
+ Ci.nsICachingChannel.LOAD_ONLY_FROM_CACHE | Ci.nsIRequest.VALIDATE_NEVER,
+ false, // expect success
+ false, // read from cache
+ false
+ ), // hit server
+
+ new Test(
+ httpBase + test410Path,
+ 0,
+ true, // expect success
+ false, // read from cache
+ true
+ ), // hit server
+ new Test(
+ httpBase + test410Path,
+ 0,
+ true, // expect success
+ true, // read from cache
+ false
+ ), // hit server
+
+ new Test(
+ httpBase + test404Path,
+ 0,
+ true, // expect success
+ false, // read from cache
+ true
+ ), // hit server
+ new Test(
+ httpBase + test404Path,
+ 0,
+ true, // expect success
+ false, // read from cache
+ true
+ ), // hit server
+];
+
+function run_next_test() {
+ if (gTests.length == 0) {
+ httpserver.stop(do_test_finished);
+ return;
+ }
+
+ var test = gTests.shift();
+ test.run();
+}
+
+function handler(httpStatus, metadata, response) {
+ gHitServer = true;
+ try {
+ var etag = metadata.getHeader("If-None-Match");
+ } catch (ex) {
+ var etag = "";
+ }
+ if (etag == "testtag") {
+ // Allow using the cached data
+ response.setStatusLine(metadata.httpVersion, 304, "Not Modified");
+ } else {
+ response.setStatusLine(metadata.httpVersion, httpStatus, "Useless Phrase");
+ response.setHeader("Content-Type", "text/plain", false);
+ response.setHeader("ETag", "testtag", false);
+ const body = "data";
+ response.bodyOutputStream.write(body, body.length);
+ }
+}
+
+function nocache_handler(metadata, response) {
+ response.setHeader("Cache-Control", "no-cache", false);
+ handler(200, metadata, response);
+}
+
+function nostore_handler(metadata, response) {
+ response.setHeader("Cache-Control", "no-store", false);
+ handler(200, metadata, response);
+}
+
+function test410_handler(metadata, response) {
+ handler(410, metadata, response);
+}
+
+function test404_handler(metadata, response) {
+ handler(404, metadata, response);
+}
+
+function shortexp_handler(metadata, response) {
+ response.setHeader("Cache-Control", "max-age=0", false);
+ handler(200, metadata, response);
+}
+
+function longexp_handler(metadata, response) {
+ response.setHeader("Cache-Control", "max-age=10000", false);
+ handler(200, metadata, response);
+}
+
+// test spaces around max-age value token
+function longexp2_handler(metadata, response) {
+ response.setHeader("Cache-Control", "max-age = 10000", false);
+ handler(200, metadata, response);
+}
+
+function run_test() {
+ httpserver.registerPathHandler(shortexpPath, shortexp_handler);
+ httpserver.registerPathHandler(longexpPath, longexp_handler);
+ httpserver.registerPathHandler(longexp2Path, longexp2_handler);
+ httpserver.registerPathHandler(nocachePath, nocache_handler);
+ httpserver.registerPathHandler(nostorePath, nostore_handler);
+ httpserver.registerPathHandler(test410Path, test410_handler);
+ httpserver.registerPathHandler(test404Path, test404_handler);
+
+ run_next_test();
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_captive_portal_service.js b/netwerk/test/unit/test_captive_portal_service.js
new file mode 100644
index 0000000000..55ca0874b9
--- /dev/null
+++ b/netwerk/test/unit/test_captive_portal_service.js
@@ -0,0 +1,207 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+let httpserver = null;
+XPCOMUtils.defineLazyGetter(this, "cpURI", function() {
+ return "http://localhost:" + httpserver.identity.primaryPort + "/captive.txt";
+});
+
+const SUCCESS_STRING = "success\n";
+let cpResponse = SUCCESS_STRING;
+function contentHandler(metadata, response) {
+ response.setHeader("Content-Type", "text/plain");
+ response.bodyOutputStream.write(cpResponse, cpResponse.length);
+}
+
+const PREF_CAPTIVE_ENABLED = "network.captive-portal-service.enabled";
+const PREF_CAPTIVE_TESTMODE = "network.captive-portal-service.testMode";
+const PREF_CAPTIVE_ENDPOINT = "captivedetect.canonicalURL";
+const PREF_CAPTIVE_MINTIME = "network.captive-portal-service.minInterval";
+const PREF_CAPTIVE_MAXTIME = "network.captive-portal-service.maxInterval";
+const PREF_DNS_NATIVE_IS_LOCALHOST = "network.dns.native-is-localhost";
+
+const cps = Cc["@mozilla.org/network/captive-portal-service;1"].getService(
+ Ci.nsICaptivePortalService
+);
+
+registerCleanupFunction(async () => {
+ Services.prefs.clearUserPref(PREF_CAPTIVE_ENABLED);
+ Services.prefs.clearUserPref(PREF_CAPTIVE_TESTMODE);
+ Services.prefs.clearUserPref(PREF_CAPTIVE_ENDPOINT);
+ Services.prefs.clearUserPref(PREF_CAPTIVE_MINTIME);
+ Services.prefs.clearUserPref(PREF_CAPTIVE_MAXTIME);
+ Services.prefs.clearUserPref(PREF_DNS_NATIVE_IS_LOCALHOST);
+
+ await new Promise(resolve => {
+ httpserver.stop(resolve);
+ });
+});
+
+function observerPromise(topic) {
+ return new Promise(resolve => {
+ let observer = {
+ QueryInterface: ChromeUtils.generateQI(["nsIObserver"]),
+ observe(aSubject, aTopic, aData) {
+ if (aTopic == topic) {
+ Services.obs.removeObserver(observer, topic);
+ resolve(aData);
+ }
+ },
+ };
+ Services.obs.addObserver(observer, topic);
+ });
+}
+
+add_task(function setup() {
+ httpserver = new HttpServer();
+ httpserver.registerPathHandler("/captive.txt", contentHandler);
+ httpserver.start(-1);
+
+ Services.prefs.setCharPref(PREF_CAPTIVE_ENDPOINT, cpURI);
+ Services.prefs.setIntPref(PREF_CAPTIVE_MINTIME, 50);
+ Services.prefs.setIntPref(PREF_CAPTIVE_MAXTIME, 100);
+ Services.prefs.setBoolPref(PREF_CAPTIVE_TESTMODE, true);
+ Services.prefs.setBoolPref(PREF_DNS_NATIVE_IS_LOCALHOST, true);
+});
+
+add_task(async function test_simple() {
+ Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, false);
+
+ equal(cps.state, Ci.nsICaptivePortalService.UNKNOWN);
+
+ let notification = observerPromise("network:captive-portal-connectivity");
+ // The service is started by nsIOService when the pref becomes true.
+ // We might want to add a method to do this in the future.
+ Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, true);
+
+ let observerPayload = await notification;
+ equal(observerPayload, "clear");
+ equal(cps.state, Ci.nsICaptivePortalService.NOT_CAPTIVE);
+
+ cpResponse = "other";
+ notification = observerPromise("captive-portal-login");
+ cps.recheckCaptivePortal();
+ await notification;
+ equal(cps.state, Ci.nsICaptivePortalService.LOCKED_PORTAL);
+
+ cpResponse = SUCCESS_STRING;
+ notification = observerPromise("captive-portal-login-success");
+ cps.recheckCaptivePortal();
+ await notification;
+ equal(cps.state, Ci.nsICaptivePortalService.UNLOCKED_PORTAL);
+});
+
+// This test redirects to another URL which returns the same content.
+// It should still be interpreted as a captive portal.
+add_task(async function test_redirect_success() {
+ Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, false);
+ equal(cps.state, Ci.nsICaptivePortalService.UNKNOWN);
+
+ httpserver.registerPathHandler("/succ.txt", (metadata, response) => {
+ response.setHeader("Content-Type", "text/plain");
+ response.bodyOutputStream.write(cpResponse, cpResponse.length);
+ });
+ httpserver.registerPathHandler("/captive.txt", (metadata, response) => {
+ response.setStatusLine(metadata.httpVersion, 307, "Moved Temporarily");
+ response.setHeader(
+ "Location",
+ `http://localhost:${httpserver.identity.primaryPort}/succ.txt`
+ );
+ });
+
+ let notification = observerPromise("captive-portal-login").then(
+ () => "login"
+ );
+ let succNotif = observerPromise("network:captive-portal-connectivity").then(
+ () => "connectivity"
+ );
+ Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, true);
+
+ let winner = await Promise.race([notification, succNotif]);
+ equal(winner, "login", "This should have been a login, not a success");
+ equal(
+ cps.state,
+ Ci.nsICaptivePortalService.LOCKED_PORTAL,
+ "Should be locked after redirect to same text"
+ );
+});
+
+// This redirects to another URI with a different content.
+// We check that it triggers a captive portal login
+add_task(async function test_redirect_bad() {
+ Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, false);
+ equal(cps.state, Ci.nsICaptivePortalService.UNKNOWN);
+
+ httpserver.registerPathHandler("/bad.txt", (metadata, response) => {
+ response.setHeader("Content-Type", "text/plain");
+ response.bodyOutputStream.write("bad", "bad".length);
+ });
+
+ httpserver.registerPathHandler("/captive.txt", (metadata, response) => {
+ response.setStatusLine(metadata.httpVersion, 307, "Moved Temporarily");
+ response.setHeader(
+ "Location",
+ `http://localhost:${httpserver.identity.primaryPort}/bad.txt`
+ );
+ });
+
+ let notification = observerPromise("captive-portal-login");
+ Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, true);
+
+ await notification;
+ equal(
+ cps.state,
+ Ci.nsICaptivePortalService.LOCKED_PORTAL,
+ "Should be locked after redirect to bad text"
+ );
+});
+
+// This redirects to the same URI.
+// We check that it triggers a captive portal login
+add_task(async function test_redirect_loop() {
+ Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, false);
+ equal(cps.state, Ci.nsICaptivePortalService.UNKNOWN);
+
+ // This is actually a redirect loop
+ httpserver.registerPathHandler("/captive.txt", (metadata, response) => {
+ response.setStatusLine(metadata.httpVersion, 307, "Moved Temporarily");
+ response.setHeader("Location", cpURI);
+ });
+
+ let notification = observerPromise("captive-portal-login");
+ Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, true);
+
+ await notification;
+ equal(cps.state, Ci.nsICaptivePortalService.LOCKED_PORTAL);
+});
+
+// This redirects to a https URI.
+// We check that it triggers a captive portal login
+add_task(async function test_redirect_https() {
+ Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, false);
+ equal(cps.state, Ci.nsICaptivePortalService.UNKNOWN);
+
+ let h2Port = Cc["@mozilla.org/process/environment;1"]
+ .getService(Ci.nsIEnvironment)
+ .get("MOZHTTP2_PORT");
+ Assert.notEqual(h2Port, null);
+ Assert.notEqual(h2Port, "");
+
+ // Any kind of redirection should trigger the captive portal login.
+ httpserver.registerPathHandler("/captive.txt", (metadata, response) => {
+ response.setStatusLine(metadata.httpVersion, 307, "Moved Temporarily");
+ response.setHeader("Location", `https://foo.example.com:${h2Port}/exit`);
+ });
+
+ let notification = observerPromise("captive-portal-login");
+ Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, true);
+
+ await notification;
+ equal(
+ cps.state,
+ Ci.nsICaptivePortalService.LOCKED_PORTAL,
+ "Should be locked after redirect to https"
+ );
+ Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, false);
+});
diff --git a/netwerk/test/unit/test_channel_close.js b/netwerk/test/unit/test_channel_close.js
new file mode 100644
index 0000000000..e05f0e9bb2
--- /dev/null
+++ b/netwerk/test/unit/test_channel_close.js
@@ -0,0 +1,68 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+var httpProtocolHandler = Cc[
+ "@mozilla.org/network/protocol;1?name=http"
+].getService(Ci.nsIHttpProtocolHandler);
+
+XPCOMUtils.defineLazyGetter(this, "URL", function() {
+ return "http://localhost:" + httpserver.identity.primaryPort;
+});
+
+var httpserver = new HttpServer();
+var testpath = "/simple";
+var httpbody = "0123456789";
+
+var live_channels = [];
+
+function run_test() {
+ httpserver.registerPathHandler(testpath, serverHandler);
+ httpserver.start(-1);
+
+ httpProtocolHandler.EnsureHSTSDataReady().then(function() {
+ var local_channel;
+
+ // Opened channel that has no remaining references on shutdown
+ local_channel = setupChannel(testpath);
+ local_channel.asyncOpen(new ChannelListener(checkRequest, local_channel));
+
+ // Opened channel that has no remaining references after being opened
+ setupChannel(testpath).asyncOpen(new ChannelListener(function() {}, null));
+
+ // Unopened channel that has remaining references on shutdown
+ live_channels.push(setupChannel(testpath));
+
+ // Opened channel that has remaining references on shutdown
+ live_channels.push(setupChannel(testpath));
+ live_channels[1].asyncOpen(
+ new ChannelListener(checkRequestFinish, live_channels[1])
+ );
+ });
+
+ do_test_pending();
+}
+
+function setupChannel(path) {
+ var chan = NetUtil.newChannel({
+ uri: URL + path,
+ loadUsingSystemPrincipal: true,
+ });
+ chan.QueryInterface(Ci.nsIHttpChannel);
+ chan.requestMethod = "GET";
+ return chan;
+}
+
+function serverHandler(metadata, response) {
+ response.setHeader("Content-Type", "text/plain", false);
+ response.bodyOutputStream.write(httpbody, httpbody.length);
+}
+
+function checkRequest(request, data, context) {
+ Assert.equal(data, httpbody);
+}
+
+function checkRequestFinish(request, data, context) {
+ checkRequest(request, data, context);
+ httpserver.stop(do_test_finished);
+}
diff --git a/netwerk/test/unit/test_channel_priority.js b/netwerk/test/unit/test_channel_priority.js
new file mode 100644
index 0000000000..2a757de90a
--- /dev/null
+++ b/netwerk/test/unit/test_channel_priority.js
@@ -0,0 +1,97 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* globals NetUtil*/
+/* globals HttpServer */
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+let httpserver;
+let port;
+
+function startHttpServer() {
+ httpserver = new HttpServer();
+
+ httpserver.registerPathHandler("/resource", (metadata, response) => {
+ response.setStatusLine(metadata.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "text/plain", false);
+ response.setHeader("Cache-Control", "no-cache", false);
+ response.bodyOutputStream.write("data", 4);
+ });
+
+ httpserver.registerPathHandler("/redirect", (metadata, response) => {
+ response.setStatusLine(metadata.httpVersion, 302, "Redirect");
+ response.setHeader("Location", "/resource", false);
+ response.setHeader("Cache-Control", "no-cache", false);
+ });
+
+ httpserver.start(-1);
+ port = httpserver.identity.primaryPort;
+}
+
+function stopHttpServer() {
+ httpserver.stop(() => {});
+}
+
+function makeRequest(uri) {
+ let requestChannel = NetUtil.newChannel({
+ uri,
+ loadUsingSystemPrincipal: true,
+ });
+ requestChannel.QueryInterface(Ci.nsISupportsPriority);
+ requestChannel.priority = Ci.nsISupportsPriority.PRIORITY_HIGHEST;
+ requestChannel.asyncOpen(new ChannelListener(checkResponse, requestChannel));
+}
+
+function checkResponse(request, buffer, requestChannel) {
+ requestChannel.QueryInterface(Ci.nsISupportsPriority);
+ Assert.equal(
+ requestChannel.priority,
+ Ci.nsISupportsPriority.PRIORITY_HIGHEST
+ );
+
+ // the response channel can be different (if it was redirected)
+ let responseChannel = request.QueryInterface(Ci.nsISupportsPriority);
+ Assert.equal(
+ responseChannel.priority,
+ Ci.nsISupportsPriority.PRIORITY_HIGHEST
+ );
+
+ run_next_test();
+}
+
+add_test(function test_regular_request() {
+ makeRequest(`http://localhost:${port}/resource`);
+});
+
+add_test(function test_redirect() {
+ makeRequest(`http://localhost:${port}/redirect`);
+});
+
+function run_test() {
+ // jshint ignore:line
+ if (!runningInParent) {
+ // add a task to report test finished to parent process at the end of test queue,
+ // since do_register_cleanup is not available in child xpcshell test script.
+ add_test(function() {
+ do_send_remote_message("finished");
+ run_next_test();
+ });
+
+ // waiting for parent process to assign server port via configPort()
+ return;
+ }
+
+ startHttpServer();
+ registerCleanupFunction(stopHttpServer);
+ run_next_test();
+}
+
+// This is used by unit_ipc/test_channel_priority_wrap.js for e10s XPCShell test
+function configPort(serverPort) {
+ // jshint ignore:line
+ port = serverPort;
+ run_next_test();
+}
diff --git a/netwerk/test/unit/test_chunked_responses.js b/netwerk/test/unit/test_chunked_responses.js
new file mode 100644
index 0000000000..ea0149e5c5
--- /dev/null
+++ b/netwerk/test/unit/test_chunked_responses.js
@@ -0,0 +1,172 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*
+ * Test Chunked-Encoded response parsing.
+ */
+
+////////////////////////////////////////////////////////////////////////////////
+// Test infrastructure
+
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true);
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("security.allow_eval_with_system_principal");
+});
+
+XPCOMUtils.defineLazyGetter(this, "URL", function() {
+ return "http://localhost:" + httpserver.identity.primaryPort;
+});
+
+var httpserver = new HttpServer();
+var index = 0;
+var test_flags = [];
+var testPathBase = "/chunked_hdrs";
+
+function run_test() {
+ httpserver.start(-1);
+
+ do_test_pending();
+ run_test_number(1);
+}
+
+function run_test_number(num) {
+ var testPath = testPathBase + num;
+ httpserver.registerPathHandler(testPath, eval("handler" + num));
+
+ var channel = setupChannel(testPath);
+ var flags = test_flags[num]; // OK if flags undefined for test
+ channel.asyncOpen(
+ new ChannelListener(eval("completeTest" + num), channel, flags)
+ );
+}
+
+function setupChannel(url) {
+ var chan = NetUtil.newChannel({
+ uri: URL + url,
+ loadUsingSystemPrincipal: true,
+ });
+ var httpChan = chan.QueryInterface(Ci.nsIHttpChannel);
+ return httpChan;
+}
+
+function endTests() {
+ httpserver.stop(do_test_finished);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Test 1: FAIL because of overflowed chunked size. The parser uses long so
+// the test case uses >64bit to fail on all platforms.
+test_flags[1] = CL_EXPECT_LATE_FAILURE | CL_ALLOW_UNKNOWN_CL;
+
+function handler1(metadata, response) {
+ var body = "12345678123456789\r\ndata never reached";
+
+ response.seizePower();
+ response.write("HTTP/1.1 200 OK\r\n");
+ response.write("Content-Type: text/plain\r\n");
+ response.write("Transfer-Encoding: chunked\r\n");
+ response.write("\r\n");
+ response.write(body);
+ response.finish();
+}
+
+function completeTest1(request, data, ctx) {
+ Assert.equal(request.status, Cr.NS_ERROR_UNEXPECTED);
+
+ run_test_number(2);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Test 2: FAIL because of non-hex in chunked length
+
+test_flags[2] = CL_EXPECT_LATE_FAILURE | CL_ALLOW_UNKNOWN_CL;
+
+function handler2(metadata, response) {
+ var body = "junkintheway 123\r\ndata never reached";
+
+ response.seizePower();
+ response.write("HTTP/1.1 200 OK\r\n");
+ response.write("Content-Type: text/plain\r\n");
+ response.write("Transfer-Encoding: chunked\r\n");
+ response.write("\r\n");
+ response.write(body);
+ response.finish();
+}
+
+function completeTest2(request, data, ctx) {
+ Assert.equal(request.status, Cr.NS_ERROR_UNEXPECTED);
+ run_test_number(3);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Test 3: OK in spite of non-hex digits after size in the length field
+
+test_flags[3] = CL_ALLOW_UNKNOWN_CL;
+
+function handler3(metadata, response) {
+ var body = "c junkafter\r\ndata reached\r\n0\r\n\r\n";
+
+ response.seizePower();
+ response.write("HTTP/1.1 200 OK\r\n");
+ response.write("Content-Type: text/plain\r\n");
+ response.write("Transfer-Encoding: chunked\r\n");
+ response.write("\r\n");
+ response.write(body);
+ response.finish();
+}
+
+function completeTest3(request, data, ctx) {
+ Assert.equal(request.status, 0);
+ run_test_number(4);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Test 4: Verify a fully compliant chunked response.
+
+test_flags[4] = CL_ALLOW_UNKNOWN_CL;
+
+function handler4(metadata, response) {
+ var body = "c\r\ndata reached\r\n3\r\nhej\r\n0\r\n\r\n";
+
+ response.seizePower();
+ response.write("HTTP/1.1 200 OK\r\n");
+ response.write("Content-Type: text/plain\r\n");
+ response.write("Transfer-Encoding: chunked\r\n");
+ response.write("\r\n");
+ response.write(body);
+ response.finish();
+}
+
+function completeTest4(request, data, ctx) {
+ Assert.equal(request.status, 0);
+ run_test_number(5);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Test 5: A chunk size larger than 32 bit but smaller than 64bit also fails
+// This is probabaly subject to get improved at some point.
+
+test_flags[5] = CL_EXPECT_LATE_FAILURE | CL_ALLOW_UNKNOWN_CL;
+
+function handler5(metadata, response) {
+ var body = "123456781\r\ndata never reached";
+
+ response.seizePower();
+ response.write("HTTP/1.1 200 OK\r\n");
+ response.write("Content-Type: text/plain\r\n");
+ response.write("Transfer-Encoding: chunked\r\n");
+ response.write("\r\n");
+ response.write(body);
+ response.finish();
+}
+
+function completeTest5(request, data, ctx) {
+ Assert.equal(request.status, Cr.NS_ERROR_UNEXPECTED);
+ endTests();
+ // run_test_number(6);
+}
diff --git a/netwerk/test/unit/test_compareURIs.js b/netwerk/test/unit/test_compareURIs.js
new file mode 100644
index 0000000000..c2899b33bc
--- /dev/null
+++ b/netwerk/test/unit/test_compareURIs.js
@@ -0,0 +1,60 @@
+"use strict";
+
+function do_info(text, stack) {
+ if (!stack) {
+ stack = Components.stack.caller;
+ }
+
+ dump(
+ "TEST-INFO | " +
+ stack.filename +
+ " | [" +
+ stack.name +
+ " : " +
+ stack.lineNumber +
+ "] " +
+ text +
+ "\n"
+ );
+}
+function run_test() {
+ var tests = [
+ ["http://mozilla.org/", "http://mozilla.org/somewhere/there", true],
+ ["http://mozilla.org/", "http://www.mozilla.org/", false],
+ ["http://mozilla.org/", "http://mozilla.org:80", true],
+ ["http://mozilla.org/", "http://mozilla.org:90", false],
+ ["http://mozilla.org", "https://mozilla.org", false],
+ ["http://mozilla.org", "https://mozilla.org:80", false],
+ ["http://mozilla.org:443", "https://mozilla.org", false],
+ ["https://mozilla.org:443", "https://mozilla.org", true],
+ ["https://mozilla.org:443", "https://mozilla.org/somewhere/", true],
+ ["about:", "about:", false],
+ ["data:text/plain,text", "data:text/plain,text", false],
+ ["about:blank", "about:blank", false],
+ ["about:", "http://mozilla.org/", false],
+ ["about:", "about:config", false],
+ ["about:text/plain,text", "data:text/plain,text", false],
+ ["jar:http://mozilla.org/!/", "http://mozilla.org/", true],
+ ["view-source:http://mozilla.org/", "http://mozilla.org/", true],
+ ];
+
+ var secman = Cc["@mozilla.org/scriptsecuritymanager;1"].getService(
+ Ci.nsIScriptSecurityManager
+ );
+
+ tests.forEach(function(aTest) {
+ do_info("Comparing " + aTest[0] + " to " + aTest[1]);
+
+ var uri1 = NetUtil.newURI(aTest[0]);
+ var uri2 = NetUtil.newURI(aTest[1]);
+
+ var equal;
+ try {
+ secman.checkSameOriginURI(uri1, uri2, false, false);
+ equal = true;
+ } catch (e) {
+ equal = false;
+ }
+ Assert.equal(equal, aTest[2]);
+ });
+}
diff --git a/netwerk/test/unit/test_compressappend.js b/netwerk/test/unit/test_compressappend.js
new file mode 100644
index 0000000000..1374eb01ab
--- /dev/null
+++ b/netwerk/test/unit/test_compressappend.js
@@ -0,0 +1,99 @@
+//
+// Test that data can be appended to a cache entry even when the data is
+// compressed by the cache compression feature - bug 648429.
+//
+
+"use strict";
+
+function write_and_check(str, data, len) {
+ var written = str.write(data, len);
+ if (written != len) {
+ do_throw(
+ "str.write has not written all data!\n" +
+ " Expected: " +
+ len +
+ "\n" +
+ " Actual: " +
+ written +
+ "\n"
+ );
+ }
+}
+
+function TestAppend(compress, callback) {
+ this._compress = compress;
+ this._callback = callback;
+ this.run();
+}
+
+TestAppend.prototype = {
+ _compress: false,
+ _callback: null,
+
+ run() {
+ evict_cache_entries();
+ asyncOpenCacheEntry(
+ "http://data/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ this.writeData.bind(this)
+ );
+ },
+
+ writeData(status, entry) {
+ Assert.equal(status, Cr.NS_OK);
+ if (this._compress) {
+ entry.setMetaDataElement("uncompressed-len", "0");
+ }
+ var os = entry.openOutputStream(0, 5);
+ write_and_check(os, "12345", 5);
+ os.close();
+ entry.close();
+ asyncOpenCacheEntry(
+ "http://data/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ this.appendData.bind(this)
+ );
+ },
+
+ appendData(status, entry) {
+ Assert.equal(status, Cr.NS_OK);
+ var os = entry.openOutputStream(entry.storageDataSize, 5);
+ write_and_check(os, "abcde", 5);
+ os.close();
+ entry.close();
+
+ asyncOpenCacheEntry(
+ "http://data/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_READONLY,
+ null,
+ this.checkData.bind(this)
+ );
+ },
+
+ checkData(status, entry) {
+ Assert.equal(status, Cr.NS_OK);
+ var self = this;
+ pumpReadStream(entry.openInputStream(0), function(str) {
+ Assert.equal(str.length, 10);
+ Assert.equal(str, "12345abcde");
+ entry.close();
+
+ executeSoon(self._callback);
+ });
+ },
+};
+
+function run_test() {
+ do_get_profile();
+ new TestAppend(false, run_test2);
+ do_test_pending();
+}
+
+function run_test2() {
+ new TestAppend(true, do_test_finished);
+}
diff --git a/netwerk/test/unit/test_content_encoding_gzip.js b/netwerk/test/unit/test_content_encoding_gzip.js
new file mode 100644
index 0000000000..a3fb41e76c
--- /dev/null
+++ b/netwerk/test/unit/test_content_encoding_gzip.js
@@ -0,0 +1,210 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+var httpserver = new HttpServer();
+var index = 0;
+var tests = [
+ {
+ url: "/test/cegzip1",
+ flags: CL_EXPECT_GZIP,
+ ce: "gzip",
+ body: [
+ 0x1f,
+ 0x8b,
+ 0x08,
+ 0x08,
+ 0x5a,
+ 0xa0,
+ 0x31,
+ 0x4f,
+ 0x00,
+ 0x03,
+ 0x74,
+ 0x78,
+ 0x74,
+ 0x00,
+ 0x2b,
+ 0xc9,
+ 0xc8,
+ 0x2c,
+ 0x56,
+ 0x00,
+ 0xa2,
+ 0x92,
+ 0xd4,
+ 0xe2,
+ 0x12,
+ 0x43,
+ 0x2e,
+ 0x00,
+ 0xb9,
+ 0x23,
+ 0xd7,
+ 0x3b,
+ 0x0e,
+ 0x00,
+ 0x00,
+ 0x00,
+ ],
+ datalen: 14, // the data length of the uncompressed document
+ },
+
+ {
+ url: "/test/cegzip2",
+ flags: CL_EXPECT_GZIP,
+ ce: "gzip, gzip",
+ body: [
+ 0x1f,
+ 0x8b,
+ 0x08,
+ 0x00,
+ 0x72,
+ 0xa1,
+ 0x31,
+ 0x4f,
+ 0x00,
+ 0x03,
+ 0x93,
+ 0xef,
+ 0xe6,
+ 0xe0,
+ 0x88,
+ 0x5a,
+ 0x60,
+ 0xe8,
+ 0xcf,
+ 0xc0,
+ 0x5c,
+ 0x52,
+ 0x51,
+ 0xc2,
+ 0xa0,
+ 0x7d,
+ 0xf2,
+ 0x84,
+ 0x4e,
+ 0x18,
+ 0xc3,
+ 0xa2,
+ 0x49,
+ 0x57,
+ 0x1e,
+ 0x09,
+ 0x39,
+ 0xeb,
+ 0x31,
+ 0xec,
+ 0x54,
+ 0xbe,
+ 0x6e,
+ 0xcd,
+ 0xc7,
+ 0xc0,
+ 0xc0,
+ 0x00,
+ 0x00,
+ 0x6e,
+ 0x90,
+ 0x7a,
+ 0x85,
+ 0x24,
+ 0x00,
+ 0x00,
+ 0x00,
+ ],
+ datalen: 14, // the data length of the uncompressed document
+ },
+
+ {
+ url: "/test/cebrotli1",
+ flags: CL_EXPECT_GZIP,
+ ce: "br",
+ body: [0x0b, 0x02, 0x80, 0x74, 0x65, 0x73, 0x74, 0x0a, 0x03],
+
+ datalen: 5, // the data length of the uncompressed document
+ },
+
+ // this is not a brotli document
+ {
+ url: "/test/cebrotli2",
+ flags: CL_EXPECT_GZIP | CL_EXPECT_FAILURE,
+ ce: "br",
+ body: [0x0b, 0x0a, 0x09],
+ datalen: 3,
+ },
+
+ // this is brotli but should come through as identity due to prefs
+ {
+ url: "/test/cebrotli3",
+ flags: 0,
+ ce: "br",
+ body: [0x0b, 0x02, 0x80, 0x74, 0x65, 0x73, 0x74, 0x0a, 0x03],
+
+ datalen: 9,
+ },
+];
+
+function setupChannel(url) {
+ return NetUtil.newChannel({
+ uri: "http://localhost:" + httpserver.identity.primaryPort + url,
+ loadUsingSystemPrincipal: true,
+ });
+}
+
+function startIter() {
+ if (tests[index].url === "/test/cebrotli3") {
+ // this test wants to make sure we don't do brotli when not in a-e
+ prefs.setCharPref("network.http.accept-encoding", "gzip, deflate");
+ }
+ var channel = setupChannel(tests[index].url);
+ channel.asyncOpen(
+ new ChannelListener(completeIter, channel, tests[index].flags)
+ );
+}
+
+function completeIter(request, data, ctx) {
+ if (!(tests[index].flags & CL_EXPECT_FAILURE)) {
+ Assert.equal(data.length, tests[index].datalen);
+ }
+ if (++index < tests.length) {
+ startIter();
+ } else {
+ httpserver.stop(do_test_finished);
+ prefs.setCharPref("network.http.accept-encoding", cePref);
+ }
+}
+
+var prefs;
+var cePref;
+function run_test() {
+ prefs = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch);
+ cePref = prefs.getCharPref("network.http.accept-encoding");
+ prefs.setCharPref("network.http.accept-encoding", "gzip, deflate, br");
+
+ httpserver.registerPathHandler("/test/cegzip1", handler);
+ httpserver.registerPathHandler("/test/cegzip2", handler);
+ httpserver.registerPathHandler("/test/cebrotli1", handler);
+ httpserver.registerPathHandler("/test/cebrotli2", handler);
+ httpserver.registerPathHandler("/test/cebrotli3", handler);
+ httpserver.start(-1);
+
+ startIter();
+ do_test_pending();
+}
+
+function handler(metadata, response) {
+ response.setStatusLine(metadata.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "text/plain", false);
+ response.setHeader("Content-Encoding", tests[index].ce, false);
+ response.setHeader("Content-Length", "" + tests[index].body.length, false);
+
+ var bos = Cc["@mozilla.org/binaryoutputstream;1"].createInstance(
+ Ci.nsIBinaryOutputStream
+ );
+ bos.setOutputStream(response.bodyOutputStream);
+
+ response.processAsync();
+ bos.writeByteArray(tests[index].body);
+ response.finish();
+}
diff --git a/netwerk/test/unit/test_content_length_underrun.js b/netwerk/test/unit/test_content_length_underrun.js
new file mode 100644
index 0000000000..b651d36065
--- /dev/null
+++ b/netwerk/test/unit/test_content_length_underrun.js
@@ -0,0 +1,280 @@
+/*
+ * Test Content-Length underrun behavior
+ */
+
+////////////////////////////////////////////////////////////////////////////////
+// Test infrastructure
+
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+XPCOMUtils.defineLazyGetter(this, "URL", function() {
+ return "http://localhost:" + httpserver.identity.primaryPort;
+});
+
+var httpserver = new HttpServer();
+var index = 0;
+var test_flags = [];
+var testPathBase = "/cl_hdrs";
+
+var prefs;
+var enforcePrefStrict;
+var enforcePrefSoft;
+var enforcePrefStrictChunked;
+
+Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true);
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("security.allow_eval_with_system_principal");
+});
+
+function run_test() {
+ prefs = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch);
+ enforcePrefStrict = prefs.getBoolPref("network.http.enforce-framing.http1");
+ enforcePrefSoft = prefs.getBoolPref("network.http.enforce-framing.soft");
+ enforcePrefStrictChunked = prefs.getBoolPref(
+ "network.http.enforce-framing.strict_chunked_encoding"
+ );
+
+ prefs.setBoolPref("network.http.enforce-framing.http1", true);
+
+ httpserver.start(-1);
+
+ do_test_pending();
+ run_test_number(1);
+}
+
+function run_test_number(num) {
+ let testPath = testPathBase + num;
+ httpserver.registerPathHandler(testPath, eval("handler" + num));
+
+ var channel = setupChannel(testPath);
+ let flags = test_flags[num]; // OK if flags undefined for test
+ channel.asyncOpen(
+ new ChannelListener(eval("completeTest" + num), channel, flags)
+ );
+}
+
+function run_gzip_test(num) {
+ let testPath = testPathBase + num;
+ httpserver.registerPathHandler(testPath, eval("handler" + num));
+
+ var channel = setupChannel(testPath);
+
+ function StreamListener() {}
+
+ StreamListener.prototype = {
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIStreamListener",
+ "nsIRequestObserver",
+ ]),
+
+ onStartRequest(aRequest) {},
+
+ onStopRequest(aRequest, aStatusCode) {
+ // Make sure we catch the error NS_ERROR_NET_PARTIAL_TRANSFER here.
+ Assert.equal(aStatusCode, Cr.NS_ERROR_NET_PARTIAL_TRANSFER);
+ // do_test_finished();
+ endTests();
+ },
+
+ onDataAvailable(request, stream, offset, count) {},
+ };
+
+ let listener = new StreamListener();
+
+ channel.asyncOpen(listener);
+}
+
+function setupChannel(url) {
+ var chan = NetUtil.newChannel({
+ uri: URL + url,
+ loadUsingSystemPrincipal: true,
+ });
+ var httpChan = chan.QueryInterface(Ci.nsIHttpChannel);
+ return httpChan;
+}
+
+function endTests() {
+ // restore the prefs to pre-test values
+ prefs.setBoolPref("network.http.enforce-framing.http1", enforcePrefStrict);
+ prefs.setBoolPref("network.http.enforce-framing.soft", enforcePrefSoft);
+ prefs.setBoolPref(
+ "network.http.enforce-framing.strict_chunked_encoding",
+ enforcePrefStrictChunked
+ );
+ httpserver.stop(do_test_finished);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Test 1: FAIL because of Content-Length underrun with HTTP 1.1
+test_flags[1] = CL_EXPECT_LATE_FAILURE;
+
+function handler1(metadata, response) {
+ var body = "blablabla";
+
+ response.seizePower();
+ response.write("HTTP/1.1 200 OK\r\n");
+ response.write("Content-Type: text/plain\r\n");
+ response.write("Content-Length: 556677\r\n");
+ response.write("\r\n");
+ response.write(body);
+ response.finish();
+}
+
+function completeTest1(request, data, ctx) {
+ Assert.equal(request.status, Cr.NS_ERROR_NET_PARTIAL_TRANSFER);
+
+ run_test_number(11);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Test 11: PASS because of Content-Length underrun with HTTP 1.1 but non 2xx
+test_flags[11] = CL_IGNORE_CL;
+
+function handler11(metadata, response) {
+ var body = "blablabla";
+
+ response.seizePower();
+ response.write("HTTP/1.1 404 NotOK\r\n");
+ response.write("Content-Type: text/plain\r\n");
+ response.write("Content-Length: 556677\r\n");
+ response.write("\r\n");
+ response.write(body);
+ response.finish();
+}
+
+function completeTest11(request, data, ctx) {
+ Assert.equal(request.status, Cr.NS_OK);
+ run_test_number(2);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Test 2: Succeed because Content-Length underrun is with HTTP 1.0
+
+test_flags[2] = CL_IGNORE_CL;
+
+function handler2(metadata, response) {
+ var body = "short content";
+
+ response.seizePower();
+ response.write("HTTP/1.0 200 OK\r\n");
+ response.write("Content-Type: text/plain\r\n");
+ response.write("Content-Length: 12345678\r\n");
+ response.write("\r\n");
+ response.write(body);
+ response.finish();
+}
+
+function completeTest2(request, data, ctx) {
+ Assert.equal(request.status, Cr.NS_OK);
+
+ // test 3 requires the enforce-framing prefs to be false
+ prefs.setBoolPref("network.http.enforce-framing.http1", false);
+ prefs.setBoolPref("network.http.enforce-framing.soft", false);
+ prefs.setBoolPref(
+ "network.http.enforce-framing.strict_chunked_encoding",
+ false
+ );
+ run_test_number(3);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Test 3: SUCCEED with bad Content-Length because pref allows it
+test_flags[3] = CL_IGNORE_CL;
+
+function handler3(metadata, response) {
+ var body = "blablabla";
+
+ response.seizePower();
+ response.write("HTTP/1.1 200 OK\r\n");
+ response.write("Content-Type: text/plain\r\n");
+ response.write("Content-Length: 556677\r\n");
+ response.write("\r\n");
+ response.write(body);
+ response.finish();
+}
+
+function completeTest3(request, data, ctx) {
+ Assert.equal(request.status, Cr.NS_OK);
+ prefs.setBoolPref("network.http.enforce-framing.soft", true);
+ run_test_number(4);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Test 4: Succeed because a cut off deflate stream can't be detected
+test_flags[4] = CL_IGNORE_CL;
+
+function handler4(metadata, response) {
+ // this is the beginning of a deflate compressed response body
+
+ var body =
+ "\xcd\x57\xcd\x6e\x1b\x37\x10\xbe\x07\xc8\x3b\x0c\x36\x68\x72\xd1" +
+ "\xbf\x92\x22\xb1\x57\x0a\x64\x4b\x6a\x0c\x28\xb6\x61\xa9\x41\x73" +
+ "\x2a\xb8\xbb\x94\x44\x98\xfb\x03\x92\x92\xec\x06\x7d\x97\x1e\xeb" +
+ "\xbe\x86\x5e\xac\xc3\x25\x97\xa2\x64\xb9\x75\x0b\x14\xe8\x69\x87" +
+ "\x33\x9c\x1f\x7e\x33\x9c\xe1\x86\x9f\x66\x9f\x27\xfd\x97\x2f\x20" +
+ "\xfc\x34\x1a\x0c\x35\x01\xa1\x62\x8a\xd3\xfe\xf5\xcd\xd5\xe5\xd5" +
+ "\x6c\x54\x83\x49\xbe\x60\x31\xa3\x1c\x12\x0a\x0b\x2a\x15\xcb\x33" +
+ "\x4d\xae\x19\x05\x19\xe7\x9c\x30\x41\x1b\x61\xd3\x28\x95\xfa\x29" +
+ "\x55\x04\x32\x92\xd2\x5e\x90\x50\x19\x0b\x56\x68\x9d\x00\xe2\x3c" +
+ "\x53\x34\x53\xbd\xc0\x99\x56\xf9\x4a\x51\xe0\x64\xcf\x18\x24\x24" +
+ "\x93\xb0\xca\x40\xd2\x15\x07\x6e\xbd\x37\x60\x82\x3b\x8f\x86\x22" +
+ "\x21\xcb\x15\x95\x35\x20\x91\xa4\x59\xac\xa9\x62\x95\x31\xed\x14" +
+ "\xc9\x98\x2c\x19\x15\x3a\x62\x45\xef\x70\x1b\x50\x05\xa4\x28\xc4" +
+ "\xf6\x21\x66\xa4\xdc\x83\x32\x09\x85\xc8\xe7\x54\xa2\x4b\x81\x74" +
+ "\xbe\x12\xc0\x91\xb9\x7d\x50\x24\xe2\x0c\xd9\x29\x06\x2e\xdd\x79";
+
+ response.seizePower();
+ response.write("HTTP/1.1 200 OK\r\n");
+ response.write("Content-Type: text/plain\r\n");
+ response.write("Content-Length: 553677\r\n");
+ response.write("Content-Encoding: deflate\r\n");
+ response.write("\r\n");
+ response.write(body);
+ response.finish();
+}
+
+function completeTest4(request, data, ctx) {
+ Assert.equal(request.status, Cr.NS_OK);
+
+ prefs.setBoolPref("network.http.enforce-framing.http1", true);
+ run_gzip_test(99);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Test 99: FAIL because a cut off gzip stream CAN be detected
+
+// Note that test 99 here is run completely different than the other tests in
+// this file so if you add more tests here, consider adding them before this.
+
+function handler99(metadata, response) {
+ // this is the beginning of a gzip compressed response body
+
+ var body =
+ "\x1f\x8b\x08\x00\x80\xb9\x25\x53\x00\x03\xd4\xd9\x79\xb8\x8e\xe5" +
+ "\xba\x00\xf0\x65\x19\x33\x24\x15\x29\xf3\x50\x52\xc6\xac\x85\x10" +
+ "\x8b\x12\x22\x45\xe6\xb6\x21\x9a\x96\x84\x4c\x69\x32\xec\x84\x92" +
+ "\xcc\x99\x6a\xd9\x32\xa5\xd0\x40\xd9\xc6\x14\x15\x95\x28\x62\x9b" +
+ "\x09\xc9\x70\x4a\x25\x53\xec\x8e\x9c\xe5\x1c\x9d\xeb\xfe\x9d\x73" +
+ "\x9d\x3f\xf6\x1f\xe7\xbd\xae\xcf\xf3\xbd\xbf\xef\x7e\x9f\xeb\x79" +
+ "\xef\xf7\x99\xde\xe5\xee\x6e\xdd\x3b\x75\xeb\xd1\xb5\x6c\xb3\xd4" +
+ "\x47\x1f\x48\xf8\x17\x1d\x15\xce\x1d\x55\x92\x93\xcf\x97\xe7\x8e" +
+ "\x8b\xca\xe4\xca\x55\x92\x2a\x54\x4e\x4e\x4e\x4a\xa8\x78\x53\xa5" +
+ "\x8a\x15\x2b\x55\x4a\xfa\xe3\x7b\x85\x8a\x37\x55\x48\xae\x92\x50" +
+ "\xb4\xc2\xbf\xaa\x41\x17\x1f\xbd\x7b\xf6\xba\xaf\x47\xd1\xa2\x09" +
+ "\x3d\xba\x75\xeb\xf5\x3f\xc5\xfd\x6f\xbf\xff\x3f\x3d\xfa\xd7\x6d" +
+ "\x74\x7b\x62\x86\x0c\xff\x79\x9e\x98\x50\x33\xe1\x8f\xb3\x01\xef" +
+ "\xb6\x38\x7f\x9e\x92\xee\xf9\xa7\xee\xcb\x74\x21\x26\x25\xa1\x6a" +
+ "\x42\xf6\x73\xff\x96\x4c\x28\x91\x90\xe5\xdc\x79\xa6\x8b\xe2\x52" +
+ "\xd2\xbf\x5d\x28\x2b\x24\x26\xfc\xa9\xcc\x96\x1e\x97\x31\xfd\xba" +
+ "\xee\xe9\xde\x3d\x31\xe5\x4f\x65\xc1\xf4\xb8\x0b\x65\x86\x8b\xca";
+ response.seizePower();
+ response.write("HTTP/1.1 200 OK\r\n");
+ response.write("Content-Type: text/plain\r\n");
+ response.write("Content-Length: 553677\r\n");
+ response.write("Content-Encoding: gzip\r\n");
+ response.write("\r\n");
+ response.write(body);
+ response.finish();
+}
diff --git a/netwerk/test/unit/test_content_sniffer.js b/netwerk/test/unit/test_content_sniffer.js
new file mode 100644
index 0000000000..770eb1922b
--- /dev/null
+++ b/netwerk/test/unit/test_content_sniffer.js
@@ -0,0 +1,163 @@
+// This file tests nsIContentSniffer, introduced in bug 324985
+
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+const unknownType = "application/x-unknown-content-type";
+const sniffedType = "application/x-sniffed";
+
+const snifferCID = Components.ID("{4c93d2db-8a56-48d7-b261-9cf2a8d998eb}");
+const snifferContract = "@mozilla.org/network/unittest/contentsniffer;1";
+const categoryName = "net-content-sniffers";
+
+var sniffing_enabled = true;
+
+var isNosniff = false;
+
+/**
+ * This object is both a factory and an nsIContentSniffer implementation (so, it
+ * is de-facto a service)
+ */
+var sniffer = {
+ QueryInterface: ChromeUtils.generateQI(["nsIFactory", "nsIContentSniffer"]),
+ createInstance: function sniffer_ci(outer, iid) {
+ if (outer) {
+ throw Components.Exception("", Cr.NS_ERROR_NO_AGGREGATION);
+ }
+ return this.QueryInterface(iid);
+ },
+ lockFactory: function sniffer_lockf(lock) {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
+ },
+
+ getMIMETypeFromContent(request, data, length) {
+ return sniffedType;
+ },
+};
+
+var listener = {
+ onStartRequest: function test_onStartR(request) {
+ try {
+ var chan = request.QueryInterface(Ci.nsIChannel);
+ if (chan.contentType == unknownType) {
+ do_throw("Type should not be unknown!");
+ }
+ if (isNosniff) {
+ if (chan.contentType == sniffedType) {
+ do_throw("Sniffer called for X-Content-Type-Options:nosniff");
+ }
+ } else if (
+ sniffing_enabled &&
+ this._iteration > 2 &&
+ chan.contentType != sniffedType
+ ) {
+ do_throw(
+ "Expecting <" +
+ sniffedType +
+ "> but got <" +
+ chan.contentType +
+ "> for " +
+ chan.URI.spec
+ );
+ } else if (!sniffing_enabled && chan.contentType == sniffedType) {
+ do_throw(
+ "Sniffing not enabled but sniffer called for " + chan.URI.spec
+ );
+ }
+ } catch (e) {
+ do_throw("Unexpected exception: " + e);
+ }
+
+ throw Components.Exception("", Cr.NS_ERROR_ABORT);
+ },
+
+ onDataAvailable: function test_ODA() {
+ throw Components.Exception("", Cr.NS_ERROR_UNEXPECTED);
+ },
+
+ onStopRequest: function test_onStopR(request, status) {
+ run_test_iteration(this._iteration);
+ do_test_finished();
+ },
+
+ _iteration: 1,
+};
+
+function makeChan(url) {
+ var chan = NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true });
+ if (sniffing_enabled) {
+ chan.loadFlags |= Ci.nsIChannel.LOAD_CALL_CONTENT_SNIFFERS;
+ }
+
+ return chan;
+}
+
+var httpserv = null;
+var urls = null;
+
+function run_test() {
+ httpserv = new HttpServer();
+ httpserv.registerPathHandler("/nosniff", nosniffHandler);
+ httpserv.start(-1);
+
+ urls = [
+ // NOTE: First URL here runs without our content sniffer
+ "data:" + unknownType + ", Some text",
+ "data:" + unknownType + ", Text", // Make sure sniffing works even if we
+ // used the unknown content sniffer too
+ "data:text/plain, Some more text",
+ "http://localhost:" + httpserv.identity.primaryPort,
+ "http://localhost:" + httpserv.identity.primaryPort + "/nosniff",
+ ];
+
+ Components.manager.nsIComponentRegistrar.registerFactory(
+ snifferCID,
+ "Unit test content sniffer",
+ snifferContract,
+ sniffer
+ );
+
+ run_test_iteration(1);
+}
+
+function nosniffHandler(request, response) {
+ response.setHeader("X-Content-Type-Options", "nosniff");
+}
+
+function run_test_iteration(index) {
+ if (index > urls.length) {
+ if (sniffing_enabled) {
+ sniffing_enabled = false;
+ index = listener._iteration = 1;
+ } else {
+ do_test_pending();
+ httpserv.stop(do_test_finished);
+ return; // we're done
+ }
+ }
+
+ if (sniffing_enabled && index == 2) {
+ // Register our sniffer only here
+ // This also makes sure that dynamic registration is working
+ var catMan = Cc["@mozilla.org/categorymanager;1"].getService(
+ Ci.nsICategoryManager
+ );
+ catMan.nsICategoryManager.addCategoryEntry(
+ categoryName,
+ "unit test",
+ snifferContract,
+ false,
+ true
+ );
+ } else if (sniffing_enabled && index == 5) {
+ isNosniff = true;
+ }
+
+ var chan = makeChan(urls[index - 1]);
+
+ listener._iteration++;
+ chan.asyncOpen(listener);
+
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_cookie_blacklist.js b/netwerk/test/unit/test_cookie_blacklist.js
new file mode 100644
index 0000000000..3f81ae93e9
--- /dev/null
+++ b/netwerk/test/unit/test_cookie_blacklist.js
@@ -0,0 +1,39 @@
+"use strict";
+
+const GOOD_COOKIE = "GoodCookie=OMNOMNOM";
+const SPACEY_COOKIE = "Spacey Cookie=Major Tom";
+
+add_task(async () => {
+ Services.prefs.setBoolPref(
+ "network.cookieJarSettings.unblocked_for_testing",
+ true
+ );
+
+ var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
+ var cookieURI = ios.newURI("http://mozilla.org/test_cookie_blacklist.js");
+ const channel = NetUtil.newChannel({
+ uri: cookieURI,
+ loadUsingSystemPrincipal: true,
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT,
+ });
+
+ var cookieService = Cc["@mozilla.org/cookieService;1"].getService(
+ Ci.nsICookieService
+ );
+ cookieService.setCookieStringFromHttp(cookieURI, "BadCookie1=\x01", channel);
+ cookieService.setCookieStringFromHttp(cookieURI, "BadCookie2=\v", channel);
+ cookieService.setCookieStringFromHttp(
+ cookieURI,
+ "Bad\x07Name=illegal",
+ channel
+ );
+ cookieService.setCookieStringFromHttp(cookieURI, GOOD_COOKIE, channel);
+ cookieService.setCookieStringFromHttp(cookieURI, SPACEY_COOKIE, channel);
+
+ CookieXPCShellUtils.createServer({ hosts: ["mozilla.org"] });
+
+ const storedCookie = await CookieXPCShellUtils.getCookieStringFromDocument(
+ cookieURI.spec
+ );
+ Assert.equal(storedCookie, GOOD_COOKIE + "; " + SPACEY_COOKIE);
+});
diff --git a/netwerk/test/unit/test_cookie_header.js b/netwerk/test/unit/test_cookie_header.js
new file mode 100644
index 0000000000..82074196fc
--- /dev/null
+++ b/netwerk/test/unit/test_cookie_header.js
@@ -0,0 +1,113 @@
+// This file tests bug 250375
+
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+XPCOMUtils.defineLazyGetter(this, "URL", function() {
+ return "http://localhost:" + httpserv.identity.primaryPort + "/";
+});
+
+function inChildProcess() {
+ return (
+ Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime)
+ .processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT
+ );
+}
+
+function check_request_header(chan, name, value) {
+ var chanValue;
+ try {
+ chanValue = chan.getRequestHeader(name);
+ } catch (e) {
+ do_throw(
+ "Expected to find header '" +
+ name +
+ "' but didn't find it, got exception: " +
+ e
+ );
+ }
+ dump("Value for header '" + name + "' is '" + chanValue + "'\n");
+ Assert.equal(chanValue, value);
+}
+
+var cookieVal = "C1=V1";
+
+var listener = {
+ onStartRequest: function test_onStartR(request) {
+ try {
+ var chan = request.QueryInterface(Ci.nsIHttpChannel);
+ check_request_header(chan, "Cookie", cookieVal);
+ } catch (e) {
+ do_throw("Unexpected exception: " + e);
+ }
+
+ throw Components.Exception("", Cr.NS_ERROR_ABORT);
+ },
+
+ onDataAvailable: function test_ODA() {
+ throw Components.Exception("", Cr.NS_ERROR_UNEXPECTED);
+ },
+
+ onStopRequest: async function test_onStopR(request, status) {
+ if (this._iteration == 1) {
+ await run_test_continued();
+ } else {
+ do_test_pending();
+ httpserv.stop(do_test_finished);
+ }
+ do_test_finished();
+ },
+
+ _iteration: 1,
+};
+
+function makeChan() {
+ return NetUtil.newChannel({
+ uri: URL,
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+}
+
+var httpserv = null;
+
+function run_test() {
+ // Allow all cookies if the pref service is available in this process.
+ if (!inChildProcess()) {
+ Services.prefs.setIntPref("network.cookie.cookieBehavior", 0);
+ Services.prefs.setBoolPref(
+ "network.cookieJarSettings.unblocked_for_testing",
+ true
+ );
+ }
+
+ httpserv = new HttpServer();
+ httpserv.start(-1);
+
+ var chan = makeChan();
+
+ chan.setRequestHeader("Cookie", cookieVal, false);
+
+ chan.asyncOpen(listener);
+
+ do_test_pending();
+}
+
+async function run_test_continued() {
+ var chan = makeChan();
+
+ var cookie2 = "C2=V2";
+
+ await CookieXPCShellUtils.setCookieToDocument(chan.URI.spec, cookie2);
+
+ chan.setRequestHeader("Cookie", cookieVal, false);
+
+ // We expect that the setRequestHeader overrides the
+ // automatically-added one, so insert cookie2 in front
+ cookieVal = cookie2 + "; " + cookieVal;
+
+ listener._iteration++;
+ chan.asyncOpen(listener);
+
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_cookie_ipv6.js b/netwerk/test/unit/test_cookie_ipv6.js
new file mode 100644
index 0000000000..70bb360e0f
--- /dev/null
+++ b/netwerk/test/unit/test_cookie_ipv6.js
@@ -0,0 +1,52 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*
+ * Test that channels with different LoadInfo
+ * are stored in separate namespaces ("cookie jars")
+ */
+
+"use strict";
+
+let ip = "[::1]";
+XPCOMUtils.defineLazyGetter(this, "URL", function() {
+ return `http://${ip}:${httpserver.identity.primaryPort}/`;
+});
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+let httpserver = new HttpServer();
+
+function cookieSetHandler(metadata, response) {
+ response.setStatusLine(metadata.httpVersion, 200, "Ok");
+ response.setHeader(
+ "Set-Cookie",
+ `Set-Cookie: T1=T2; path=/; SameSite=Lax; domain=${ip}; httponly`,
+ false
+ );
+ response.setHeader("Content-Type", "text/html");
+ response.setHeader("Content-Length", "2");
+ response.bodyOutputStream.write("Ok", "Ok".length);
+}
+
+add_task(async function test_cookie_ipv6() {
+ Services.prefs.setIntPref("network.cookie.cookieBehavior", 0);
+ Services.prefs.setBoolPref(
+ "network.cookieJarSettings.unblocked_for_testing",
+ true
+ );
+
+ httpserver.registerPathHandler("/", cookieSetHandler);
+ httpserver._start(-1, ip);
+
+ var chan = NetUtil.newChannel({
+ uri: URL,
+ loadUsingSystemPrincipal: true,
+ });
+ await new Promise(resolve => {
+ chan.asyncOpen(new ChannelListener(resolve));
+ });
+ var cm = Cc["@mozilla.org/cookiemanager;1"].getService(Ci.nsICookieManager);
+ equal(cm.cookies.length, 1);
+});
diff --git a/netwerk/test/unit/test_cookiejars.js b/netwerk/test/unit/test_cookiejars.js
new file mode 100644
index 0000000000..5a421ec3f6
--- /dev/null
+++ b/netwerk/test/unit/test_cookiejars.js
@@ -0,0 +1,172 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*
+ * Test that channels with different LoadInfo
+ * are stored in separate namespaces ("cookie jars")
+ */
+
+"use strict";
+
+XPCOMUtils.defineLazyGetter(this, "URL", function() {
+ return "http://localhost:" + httpserver.identity.primaryPort;
+});
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+var httpserver = new HttpServer();
+
+var cookieSetPath = "/setcookie";
+var cookieCheckPath = "/checkcookie";
+
+function inChildProcess() {
+ return (
+ Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime)
+ .processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT
+ );
+}
+
+// Test array:
+// - element 0: name for cookie, used both to set and later to check
+// - element 1: loadInfo (determines cookie namespace)
+//
+// TODO: bug 722850: make private browsing work per-app, and add tests. For now
+// all values are 'false' for PB.
+
+var tests = [
+ {
+ cookieName: "LCC_App0_BrowF_PrivF",
+ originAttributes: new OriginAttributes(0, false, 0),
+ },
+ {
+ cookieName: "LCC_App0_BrowT_PrivF",
+ originAttributes: new OriginAttributes(0, true, 0),
+ },
+ {
+ cookieName: "LCC_App1_BrowF_PrivF",
+ originAttributes: new OriginAttributes(1, false, 0),
+ },
+ {
+ cookieName: "LCC_App1_BrowT_PrivF",
+ originAttributes: new OriginAttributes(1, true, 0),
+ },
+];
+
+// test number: index into 'tests' array
+var i = 0;
+
+function setupChannel(path) {
+ var chan = NetUtil.newChannel({
+ uri: URL + path,
+ loadUsingSystemPrincipal: true,
+ });
+ chan.loadInfo.originAttributes = tests[i].originAttributes;
+ chan.QueryInterface(Ci.nsIHttpChannel);
+ return chan;
+}
+
+function setCookie() {
+ var channel = setupChannel(cookieSetPath);
+ channel.setRequestHeader("foo-set-cookie", tests[i].cookieName, false);
+ channel.asyncOpen(new ChannelListener(setNextCookie, null));
+}
+
+function setNextCookie(request, data, context) {
+ if (++i == tests.length) {
+ // all cookies set: switch to checking them
+ i = 0;
+ checkCookie();
+ } else {
+ info("setNextCookie:i=" + i);
+ setCookie();
+ }
+}
+
+// Open channel that should send one and only one correct Cookie: header to
+// server, corresponding to it's namespace
+function checkCookie() {
+ var channel = setupChannel(cookieCheckPath);
+ channel.asyncOpen(new ChannelListener(completeCheckCookie, null));
+}
+
+function completeCheckCookie(request, data, context) {
+ // Look for all cookies in what the server saw: fail if we see any besides the
+ // one expected cookie for each namespace;
+ var expectedCookie = tests[i].cookieName;
+ request.QueryInterface(Ci.nsIHttpChannel);
+ var cookiesSeen = request.getResponseHeader("foo-saw-cookies");
+
+ var j;
+ for (j = 0; j < tests.length; j++) {
+ var cookieToCheck = tests[j].cookieName;
+ let found = cookiesSeen.includes(cookieToCheck);
+ if (found && expectedCookie != cookieToCheck) {
+ do_throw(
+ "test index " +
+ i +
+ ": found unexpected cookie '" +
+ cookieToCheck +
+ "': in '" +
+ cookiesSeen +
+ "'"
+ );
+ } else if (!found && expectedCookie == cookieToCheck) {
+ do_throw(
+ "test index " +
+ i +
+ ": missing expected cookie '" +
+ expectedCookie +
+ "': in '" +
+ cookiesSeen +
+ "'"
+ );
+ }
+ }
+ // If we get here we're good.
+ info("Saw only correct cookie '" + expectedCookie + "'");
+ Assert.ok(true);
+
+ if (++i == tests.length) {
+ // end of tests
+ httpserver.stop(do_test_finished);
+ } else {
+ checkCookie();
+ }
+}
+
+function run_test() {
+ // Allow all cookies if the pref service is available in this process.
+ if (!inChildProcess()) {
+ Services.prefs.setIntPref("network.cookie.cookieBehavior", 0);
+ Services.prefs.setBoolPref(
+ "network.cookieJarSettings.unblocked_for_testing",
+ true
+ );
+ }
+
+ httpserver.registerPathHandler(cookieSetPath, cookieSetHandler);
+ httpserver.registerPathHandler(cookieCheckPath, cookieCheckHandler);
+ httpserver.start(-1);
+
+ setCookie();
+ do_test_pending();
+}
+
+function cookieSetHandler(metadata, response) {
+ var cookieName = metadata.getHeader("foo-set-cookie");
+
+ response.setStatusLine(metadata.httpVersion, 200, "Ok");
+ response.setHeader("Set-Cookie", cookieName + "=1; Path=/", false);
+ response.setHeader("Content-Type", "text/plain");
+ response.bodyOutputStream.write("Ok", "Ok".length);
+}
+
+function cookieCheckHandler(metadata, response) {
+ var cookies = metadata.getHeader("Cookie");
+
+ response.setStatusLine(metadata.httpVersion, 200, "Ok");
+ response.setHeader("foo-saw-cookies", cookies, false);
+ response.setHeader("Content-Type", "text/plain");
+ response.bodyOutputStream.write("Ok", "Ok".length);
+}
diff --git a/netwerk/test/unit/test_cookiejars_safebrowsing.js b/netwerk/test/unit/test_cookiejars_safebrowsing.js
new file mode 100644
index 0000000000..cafecdf7fe
--- /dev/null
+++ b/netwerk/test/unit/test_cookiejars_safebrowsing.js
@@ -0,0 +1,238 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*
+ * Description of the test:
+ * We show that we can separate the safebrowsing cookie by creating a custom
+ * OriginAttributes using a unique safebrowsing first-party domain. Setting this
+ * custom OriginAttributes on the loadInfo of the channel allows us to query the
+ * first-party domain and therefore separate the safebrowsing cookie in its own
+ * cookie-jar. For testing safebrowsing update we do >> NOT << emulate a response
+ * in the body, rather we only set the cookies in the header of the response
+ * and confirm that cookies are separated in their own cookie-jar.
+ *
+ * 1) We init safebrowsing and simulate an update (cookies are set for localhost)
+ *
+ * 2) We open a channel that should send regular cookies, but not the
+ * safebrowsing cookie.
+ *
+ * 3) We open a channel with a custom callback, simulating a safebrowsing cookie
+ * that should send this simulated safebrowsing cookie as well as the
+ * real safebrowsing cookies. (Confirming that the safebrowsing cookies
+ * actually get stored in the correct jar).
+ */
+
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+XPCOMUtils.defineLazyGetter(this, "URL", function() {
+ return "http://localhost:" + httpserver.identity.primaryPort;
+});
+
+ChromeUtils.defineModuleGetter(
+ this,
+ "SafeBrowsing",
+ "resource://gre/modules/SafeBrowsing.jsm"
+);
+
+var setCookiePath = "/setcookie";
+var checkCookiePath = "/checkcookie";
+var safebrowsingUpdatePath = "/safebrowsingUpdate";
+var safebrowsingGethashPath = "/safebrowsingGethash";
+var httpserver;
+
+function inChildProcess() {
+ return (
+ Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime)
+ .processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT
+ );
+}
+
+function cookieSetHandler(metadata, response) {
+ var cookieName = metadata.getHeader("set-cookie");
+ response.setStatusLine(metadata.httpVersion, 200, "Ok");
+ response.setHeader("set-Cookie", cookieName + "=1; Path=/", false);
+ response.setHeader("Content-Type", "text/plain");
+ response.bodyOutputStream.write("Ok", "Ok".length);
+}
+
+function cookieCheckHandler(metadata, response) {
+ var cookies = metadata.getHeader("Cookie");
+ response.setStatusLine(metadata.httpVersion, 200, "Ok");
+ response.setHeader("saw-cookies", cookies, false);
+ response.setHeader("Content-Type", "text/plain");
+ response.bodyOutputStream.write("Ok", "Ok".length);
+}
+
+function safebrowsingUpdateHandler(metadata, response) {
+ var cookieName = "sb-update-cookie";
+ response.setStatusLine(metadata.httpVersion, 200, "Ok");
+ response.setHeader("set-Cookie", cookieName + "=1; Path=/", false);
+ response.setHeader("Content-Type", "text/plain");
+ response.bodyOutputStream.write("Ok", "Ok".length);
+}
+
+function safebrowsingGethashHandler(metadata, response) {
+ var cookieName = "sb-gethash-cookie";
+ response.setStatusLine(metadata.httpVersion, 200, "Ok");
+ response.setHeader("set-Cookie", cookieName + "=1; Path=/", false);
+ response.setHeader("Content-Type", "text/plain");
+
+ let msg = "test-phish-simplea:1:32\n" + "a".repeat(32);
+ response.bodyOutputStream.write(msg, msg.length);
+}
+
+function setupChannel(path, originAttributes) {
+ var channel = NetUtil.newChannel({
+ uri: URL + path,
+ loadUsingSystemPrincipal: true,
+ });
+ channel.loadInfo.originAttributes = originAttributes;
+ channel.QueryInterface(Ci.nsIHttpChannel);
+ return channel;
+}
+
+function run_test() {
+ // Set up a profile
+ do_get_profile();
+
+ // Allow all cookies if the pref service is available in this process.
+ if (!inChildProcess()) {
+ Services.prefs.setIntPref("network.cookie.cookieBehavior", 0);
+ Services.prefs.setBoolPref(
+ "network.cookieJarSettings.unblocked_for_testing",
+ true
+ );
+ }
+
+ httpserver = new HttpServer();
+ httpserver.registerPathHandler(setCookiePath, cookieSetHandler);
+ httpserver.registerPathHandler(checkCookiePath, cookieCheckHandler);
+ httpserver.registerPathHandler(
+ safebrowsingUpdatePath,
+ safebrowsingUpdateHandler
+ );
+ httpserver.registerPathHandler(
+ safebrowsingGethashPath,
+ safebrowsingGethashHandler
+ );
+
+ httpserver.start(-1);
+ run_next_test();
+}
+
+// this test does not emulate a response in the body,
+// rather we only set the cookies in the header of response.
+add_test(function test_safebrowsing_update() {
+ var streamUpdater = Cc[
+ "@mozilla.org/url-classifier/streamupdater;1"
+ ].getService(Ci.nsIUrlClassifierStreamUpdater);
+
+ function onSuccess() {
+ run_next_test();
+ }
+ function onUpdateError() {
+ do_throw("ERROR: received onUpdateError!");
+ }
+ function onDownloadError() {
+ do_throw("ERROR: received onDownloadError!");
+ }
+
+ streamUpdater.downloadUpdates(
+ "test-phish-simple,test-malware-simple",
+ "",
+ true,
+ URL + safebrowsingUpdatePath,
+ onSuccess,
+ onUpdateError,
+ onDownloadError
+ );
+});
+
+add_test(function test_safebrowsing_gethash() {
+ var hashCompleter = Cc[
+ "@mozilla.org/url-classifier/hashcompleter;1"
+ ].getService(Ci.nsIUrlClassifierHashCompleter);
+
+ hashCompleter.complete(
+ "aaaa",
+ URL + safebrowsingGethashPath,
+ "test-phish-simple",
+ {
+ completionV2(hash, table, chunkId) {},
+
+ completionFinished(status) {
+ Assert.equal(status, Cr.NS_OK);
+ run_next_test();
+ },
+ }
+ );
+});
+
+add_test(function test_non_safebrowsing_cookie() {
+ var cookieName = "regCookie_id0";
+ var originAttributes = new OriginAttributes(0, false, 0);
+
+ function setNonSafeBrowsingCookie() {
+ var channel = setupChannel(setCookiePath, originAttributes);
+ channel.setRequestHeader("set-cookie", cookieName, false);
+ channel.asyncOpen(new ChannelListener(checkNonSafeBrowsingCookie, null));
+ }
+
+ function checkNonSafeBrowsingCookie() {
+ var channel = setupChannel(checkCookiePath, originAttributes);
+ channel.asyncOpen(
+ new ChannelListener(completeCheckNonSafeBrowsingCookie, null)
+ );
+ }
+
+ function completeCheckNonSafeBrowsingCookie(request, data, context) {
+ // Confirm that only the >> ONE << cookie is sent over the channel.
+ var expectedCookie = cookieName + "=1";
+ request.QueryInterface(Ci.nsIHttpChannel);
+ var cookiesSeen = request.getResponseHeader("saw-cookies");
+ Assert.equal(cookiesSeen, expectedCookie);
+ run_next_test();
+ }
+
+ setNonSafeBrowsingCookie();
+});
+
+add_test(function test_safebrowsing_cookie() {
+ var cookieName = "sbCookie_id4294967294";
+ var originAttributes = new OriginAttributes(0, false, 0);
+ originAttributes.firstPartyDomain =
+ "safebrowsing.86868755-6b82-4842-b301-72671a0db32e.mozilla";
+
+ function setSafeBrowsingCookie() {
+ var channel = setupChannel(setCookiePath, originAttributes);
+ channel.setRequestHeader("set-cookie", cookieName, false);
+ channel.asyncOpen(new ChannelListener(checkSafeBrowsingCookie, null));
+ }
+
+ function checkSafeBrowsingCookie() {
+ var channel = setupChannel(checkCookiePath, originAttributes);
+ channel.asyncOpen(
+ new ChannelListener(completeCheckSafeBrowsingCookie, null)
+ );
+ }
+
+ function completeCheckSafeBrowsingCookie(request, data, context) {
+ // Confirm that all >> THREE << cookies are sent back over the channel:
+ // a) the safebrowsing cookie set when updating
+ // b) the safebrowsing cookie set when sending gethash
+ // c) the regular cookie with custom loadcontext defined in this test.
+ var expectedCookies = "sb-update-cookie=1; ";
+ expectedCookies += "sb-gethash-cookie=1; ";
+ expectedCookies += cookieName + "=1";
+ request.QueryInterface(Ci.nsIHttpChannel);
+ var cookiesSeen = request.getResponseHeader("saw-cookies");
+
+ Assert.equal(cookiesSeen, expectedCookies);
+ httpserver.stop(do_test_finished);
+ }
+
+ setSafeBrowsingCookie();
+});
diff --git a/netwerk/test/unit/test_cookies_async_failure.js b/netwerk/test/unit/test_cookies_async_failure.js
new file mode 100644
index 0000000000..c7a455cb53
--- /dev/null
+++ b/netwerk/test/unit/test_cookies_async_failure.js
@@ -0,0 +1,508 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test the various ways opening a cookie database can fail in an asynchronous
+// (i.e. after synchronous initialization) manner, and that the database is
+// renamed and recreated under each circumstance. These circumstances are, in no
+// particular order:
+//
+// 1) A write operation failing after the database has been read in.
+// 2) Asynchronous read failure due to a corrupt database.
+// 3) Synchronous read failure due to a corrupt database, when reading:
+// a) a single base domain;
+// b) the entire database.
+// 4) Asynchronous read failure, followed by another failure during INSERT but
+// before the database closes for rebuilding. (The additional error should be
+// ignored.)
+// 5) Asynchronous read failure, followed by an INSERT failure during rebuild.
+// This should result in an abort of the database rebuild; the partially-
+// built database should be moved to 'cookies.sqlite.bak-rebuild'.
+
+"use strict";
+
+let profile;
+let cookie;
+
+add_task(async () => {
+ // Set up a profile.
+ profile = do_get_profile();
+
+ // Allow all cookies.
+ Services.prefs.setIntPref("network.cookie.cookieBehavior", 0);
+ Services.prefs.setBoolPref(
+ "network.cookieJarSettings.unblocked_for_testing",
+ true
+ );
+
+ // The server.
+ const hosts = ["foo.com", "hither.com", "haithur.com", "bar.com"];
+ for (let i = 0; i < 3000; ++i) {
+ hosts.push(i + ".com");
+ }
+ CookieXPCShellUtils.createServer({ hosts });
+
+ // Get the cookie file and the backup file.
+ Assert.ok(!do_get_cookie_file(profile).exists());
+ Assert.ok(!do_get_backup_file(profile).exists());
+
+ // Create a cookie object for testing.
+ let now = Date.now() * 1000;
+ let futureExpiry = Math.round(now / 1e6 + 1000);
+ cookie = new Cookie(
+ "oh",
+ "hai",
+ "bar.com",
+ "/",
+ futureExpiry,
+ now,
+ now,
+ false,
+ false,
+ false
+ );
+
+ await run_test_1();
+ await run_test_2();
+ await run_test_3();
+ await run_test_4();
+ await run_test_5();
+});
+
+function do_get_backup_file(profile) {
+ let file = profile.clone();
+ file.append("cookies.sqlite.bak");
+ return file;
+}
+
+function do_get_rebuild_backup_file(profile) {
+ let file = profile.clone();
+ file.append("cookies.sqlite.bak-rebuild");
+ return file;
+}
+
+function do_corrupt_db(file) {
+ // Sanity check: the database size should be larger than 320k, since we've
+ // written about 460k of data. If it's not, let's make it obvious now.
+ let size = file.fileSize;
+ Assert.ok(size > 320e3);
+
+ // Corrupt the database by writing bad data to the end of the file. We
+ // assume that the important metadata -- table structure etc -- is stored
+ // elsewhere, and that doing this will not cause synchronous failure when
+ // initializing the database connection. This is totally empirical --
+ // overwriting between 1k and 100k of live data seems to work. (Note that the
+ // database file will be larger than the actual content requires, since the
+ // cookie service uses a large growth increment. So we calculate the offset
+ // based on the expected size of the content, not just the file size.)
+ let ostream = Cc["@mozilla.org/network/file-output-stream;1"].createInstance(
+ Ci.nsIFileOutputStream
+ );
+ ostream.init(file, 2, -1, 0);
+ let sstream = ostream.QueryInterface(Ci.nsISeekableStream);
+ let n = size - 320e3 + 20e3;
+ sstream.seek(Ci.nsISeekableStream.NS_SEEK_SET, size - n);
+ for (let i = 0; i < n; ++i) {
+ ostream.write("a", 1);
+ }
+ ostream.flush();
+ ostream.close();
+
+ Assert.equal(file.clone().fileSize, size);
+ return size;
+}
+
+async function run_test_1() {
+ // Load the profile and populate it.
+ await CookieXPCShellUtils.setCookieToDocument(
+ "http://foo.com/",
+ "oh=hai; max-age=1000"
+ );
+
+ // Close the profile.
+ await promise_close_profile();
+
+ // Open a database connection now, before we load the profile and begin
+ // asynchronous write operations.
+ let db = new CookieDatabaseConnection(do_get_cookie_file(profile), 12);
+ Assert.equal(do_count_cookies_in_db(db.db), 1);
+
+ // Load the profile, and wait for async read completion...
+ await promise_load_profile();
+
+ // Insert a row.
+ db.insertCookie(cookie);
+ db.close();
+
+ // Attempt to insert a cookie with the same (name, host, path) triplet.
+ Services.cookiemgr.add(
+ cookie.host,
+ cookie.path,
+ cookie.name,
+ "hallo",
+ cookie.isSecure,
+ cookie.isHttpOnly,
+ cookie.isSession,
+ cookie.expiry,
+ {},
+ Ci.nsICookie.SAMESITE_NONE,
+ Ci.nsICookie.SCHEME_HTTPS
+ );
+
+ // Check that the cookie service accepted the new cookie.
+ Assert.equal(Services.cookiemgr.countCookiesFromHost(cookie.host), 1);
+
+ let isRebuildingDone = false;
+ let rebuildingObserve = function(subject, topic, data) {
+ isRebuildingDone = true;
+ Services.obs.removeObserver(rebuildingObserve, "cookie-db-rebuilding");
+ };
+ Services.obs.addObserver(rebuildingObserve, "cookie-db-rebuilding");
+
+ // Crash test: we're going to rebuild the cookie database. Close all the db
+ // connections in the main thread and initialize a new database file in the
+ // cookie thread. Trigger some access of cookies to ensure we won't crash in
+ // the chaos status.
+ for (let i = 0; i < 10; ++i) {
+ Assert.equal(Services.cookiemgr.countCookiesFromHost(cookie.host), 1);
+ await new Promise(resolve => executeSoon(resolve));
+ }
+
+ // Wait for the cookie service to rename the old database and rebuild if not yet.
+ if (!isRebuildingDone) {
+ Services.obs.removeObserver(rebuildingObserve, "cookie-db-rebuilding");
+ await new _promise_observer("cookie-db-rebuilding");
+ }
+
+ await new Promise(resolve => executeSoon(resolve));
+
+ // At this point, the cookies should still be in memory.
+ Assert.equal(Services.cookiemgr.countCookiesFromHost("foo.com"), 1);
+ Assert.equal(Services.cookiemgr.countCookiesFromHost(cookie.host), 1);
+ Assert.equal(do_count_cookies(), 2);
+
+ // Close the profile.
+ await promise_close_profile();
+
+ // Check that the original database was renamed, and that it contains the
+ // original cookie.
+ Assert.ok(do_get_backup_file(profile).exists());
+ let backupdb = Services.storage.openDatabase(do_get_backup_file(profile));
+ Assert.equal(do_count_cookies_in_db(backupdb, "foo.com"), 1);
+ backupdb.close();
+
+ // Load the profile, and check that it contains the new cookie.
+ do_load_profile();
+
+ Assert.equal(Services.cookiemgr.countCookiesFromHost("foo.com"), 1);
+ let cookies = Services.cookiemgr.getCookiesFromHost(cookie.host, {});
+ Assert.equal(cookies.length, 1);
+ let dbcookie = cookies[0];
+ Assert.equal(dbcookie.value, "hallo");
+
+ // Close the profile.
+ await promise_close_profile();
+
+ // Clean up.
+ do_get_cookie_file(profile).remove(false);
+ do_get_backup_file(profile).remove(false);
+ Assert.ok(!do_get_cookie_file(profile).exists());
+ Assert.ok(!do_get_backup_file(profile).exists());
+}
+
+async function run_test_2() {
+ // Load the profile and populate it.
+ do_load_profile();
+
+ Services.cookiesvc.runInTransaction(_ => {
+ let uri = NetUtil.newURI("http://foo.com/");
+ const channel = NetUtil.newChannel({
+ uri,
+ loadUsingSystemPrincipal: true,
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT,
+ });
+
+ for (let i = 0; i < 3000; ++i) {
+ let uri = NetUtil.newURI("http://" + i + ".com/");
+ Services.cookiesvc.setCookieStringFromHttp(
+ uri,
+ "oh=hai; max-age=1000",
+ channel
+ );
+ }
+ });
+
+ // Close the profile.
+ await promise_close_profile();
+
+ // Corrupt the database file.
+ let size = do_corrupt_db(do_get_cookie_file(profile));
+
+ // Load the profile.
+ do_load_profile();
+
+ // At this point, the database connection should be open. Ensure that it
+ // succeeded.
+ Assert.ok(!do_get_backup_file(profile).exists());
+
+ // Recreate a new database since it was corrupted
+ Assert.equal(Services.cookiemgr.countCookiesFromHost("0.com"), 0);
+ Assert.equal(do_count_cookies(), 0);
+
+ // Close the profile.
+ await promise_close_profile();
+
+ // Check that the original database was renamed.
+ Assert.ok(do_get_backup_file(profile).exists());
+ Assert.equal(do_get_backup_file(profile).fileSize, size);
+ let db = Services.storage.openDatabase(do_get_cookie_file(profile));
+ db.close();
+
+ do_load_profile();
+ Assert.equal(Services.cookiemgr.countCookiesFromHost("0.com"), 0);
+ Assert.equal(do_count_cookies(), 0);
+
+ // Close the profile.
+ await promise_close_profile();
+
+ // Clean up.
+ do_get_cookie_file(profile).remove(false);
+ do_get_backup_file(profile).remove(false);
+ Assert.ok(!do_get_cookie_file(profile).exists());
+ Assert.ok(!do_get_backup_file(profile).exists());
+}
+
+async function run_test_3() {
+ // Set the maximum cookies per base domain limit to a large value, so that
+ // corrupting the database is easier.
+ Services.prefs.setIntPref("network.cookie.maxPerHost", 3000);
+
+ // Load the profile and populate it.
+ do_load_profile();
+ Services.cookiesvc.runInTransaction(_ => {
+ let uri = NetUtil.newURI("http://hither.com/");
+ let channel = NetUtil.newChannel({
+ uri,
+ loadUsingSystemPrincipal: true,
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT,
+ });
+ for (let i = 0; i < 10; ++i) {
+ Services.cookiesvc.setCookieStringFromHttp(
+ uri,
+ "oh" + i + "=hai; max-age=1000",
+ channel
+ );
+ }
+ uri = NetUtil.newURI("http://haithur.com/");
+ channel = NetUtil.newChannel({
+ uri,
+ loadUsingSystemPrincipal: true,
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT,
+ });
+ for (let i = 10; i < 3000; ++i) {
+ Services.cookiesvc.setCookieStringFromHttp(
+ uri,
+ "oh" + i + "=hai; max-age=1000",
+ channel
+ );
+ }
+ });
+
+ // Close the profile.
+ await promise_close_profile();
+
+ // Corrupt the database file.
+ let size = do_corrupt_db(do_get_cookie_file(profile));
+
+ // Load the profile.
+ do_load_profile();
+
+ // At this point, the database connection should be open. Ensure that it
+ // succeeded.
+ Assert.ok(!do_get_backup_file(profile).exists());
+
+ // Recreate a new database since it was corrupted
+ Assert.equal(Services.cookiemgr.countCookiesFromHost("hither.com"), 0);
+ Assert.equal(Services.cookiemgr.countCookiesFromHost("haithur.com"), 0);
+
+ // Close the profile.
+ await promise_close_profile();
+
+ let db = Services.storage.openDatabase(do_get_cookie_file(profile));
+ Assert.equal(do_count_cookies_in_db(db, "hither.com"), 0);
+ Assert.equal(do_count_cookies_in_db(db), 0);
+ db.close();
+
+ // Check that the original database was renamed.
+ Assert.ok(do_get_backup_file(profile).exists());
+ Assert.equal(do_get_backup_file(profile).fileSize, size);
+
+ // Rename it back, and try loading the entire database synchronously.
+ do_get_backup_file(profile).moveTo(null, "cookies.sqlite");
+ do_load_profile();
+
+ // At this point, the database connection should be open. Ensure that it
+ // succeeded.
+ Assert.ok(!do_get_backup_file(profile).exists());
+
+ // Synchronously read in everything.
+ Assert.equal(do_count_cookies(), 0);
+
+ // Close the profile.
+ await promise_close_profile();
+
+ db = Services.storage.openDatabase(do_get_cookie_file(profile));
+ Assert.equal(do_count_cookies_in_db(db), 0);
+ db.close();
+
+ // Check that the original database was renamed.
+ Assert.ok(do_get_backup_file(profile).exists());
+ Assert.equal(do_get_backup_file(profile).fileSize, size);
+
+ // Clean up.
+ do_get_cookie_file(profile).remove(false);
+ do_get_backup_file(profile).remove(false);
+ Assert.ok(!do_get_cookie_file(profile).exists());
+ Assert.ok(!do_get_backup_file(profile).exists());
+}
+
+async function run_test_4() {
+ // Load the profile and populate it.
+ do_load_profile();
+ Services.cookiesvc.runInTransaction(_ => {
+ let uri = NetUtil.newURI("http://foo.com/");
+ let channel = NetUtil.newChannel({
+ uri,
+ loadUsingSystemPrincipal: true,
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT,
+ });
+ for (let i = 0; i < 3000; ++i) {
+ let uri = NetUtil.newURI("http://" + i + ".com/");
+ Services.cookiesvc.setCookieStringFromHttp(
+ uri,
+ "oh=hai; max-age=1000",
+ channel
+ );
+ }
+ });
+
+ // Close the profile.
+ await promise_close_profile();
+
+ // Corrupt the database file.
+ let size = do_corrupt_db(do_get_cookie_file(profile));
+
+ // Load the profile.
+ do_load_profile();
+
+ // At this point, the database connection should be open. Ensure that it
+ // succeeded.
+ Assert.ok(!do_get_backup_file(profile).exists());
+
+ // Recreate a new database since it was corrupted
+ Assert.equal(Services.cookiemgr.countCookiesFromHost("0.com"), 0);
+
+ // Queue up an INSERT for the same base domain. This should also go into
+ // memory and be written out during database rebuild.
+ await CookieXPCShellUtils.setCookieToDocument(
+ "http://0.com/",
+ "oh2=hai; max-age=1000"
+ );
+
+ // At this point, the cookies should still be in memory.
+ Assert.equal(Services.cookiemgr.countCookiesFromHost("0.com"), 1);
+ Assert.equal(do_count_cookies(), 1);
+
+ // Close the profile.
+ await promise_close_profile();
+
+ // Check that the original database was renamed.
+ Assert.ok(do_get_backup_file(profile).exists());
+ Assert.equal(do_get_backup_file(profile).fileSize, size);
+
+ // Load the profile, and check that it contains the new cookie.
+ do_load_profile();
+ Assert.equal(Services.cookiemgr.countCookiesFromHost("0.com"), 1);
+ Assert.equal(do_count_cookies(), 1);
+
+ // Close the profile.
+ await promise_close_profile();
+
+ // Clean up.
+ do_get_cookie_file(profile).remove(false);
+ do_get_backup_file(profile).remove(false);
+ Assert.ok(!do_get_cookie_file(profile).exists());
+ Assert.ok(!do_get_backup_file(profile).exists());
+}
+
+async function run_test_5() {
+ // Load the profile and populate it.
+ do_load_profile();
+ Services.cookiesvc.runInTransaction(_ => {
+ let uri = NetUtil.newURI("http://bar.com/");
+ const channel = NetUtil.newChannel({
+ uri,
+ loadUsingSystemPrincipal: true,
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT,
+ });
+ Services.cookiesvc.setCookieStringFromHttp(
+ uri,
+ "oh=hai; path=/; max-age=1000",
+ channel
+ );
+ for (let i = 0; i < 3000; ++i) {
+ let uri = NetUtil.newURI("http://" + i + ".com/");
+ Services.cookiesvc.setCookieStringFromHttp(
+ uri,
+ "oh=hai; max-age=1000",
+ channel
+ );
+ }
+ });
+
+ // Close the profile.
+ await promise_close_profile();
+
+ // Corrupt the database file.
+ let size = do_corrupt_db(do_get_cookie_file(profile));
+
+ // Load the profile.
+ do_load_profile();
+
+ // At this point, the database connection should be open. Ensure that it
+ // succeeded.
+ Assert.ok(!do_get_backup_file(profile).exists());
+
+ // Recreate a new database since it was corrupted
+ Assert.equal(Services.cookiemgr.countCookiesFromHost("bar.com"), 0);
+ Assert.equal(Services.cookiemgr.countCookiesFromHost("0.com"), 0);
+ Assert.equal(do_count_cookies(), 0);
+ Assert.ok(do_get_backup_file(profile).exists());
+ Assert.equal(do_get_backup_file(profile).fileSize, size);
+ Assert.ok(!do_get_rebuild_backup_file(profile).exists());
+
+ // Open a database connection, and write a row that will trigger a constraint
+ // violation.
+ let db = new CookieDatabaseConnection(do_get_cookie_file(profile), 12);
+ db.insertCookie(cookie);
+ Assert.equal(do_count_cookies_in_db(db.db, "bar.com"), 1);
+ Assert.equal(do_count_cookies_in_db(db.db), 1);
+ db.close();
+
+ // Check that the original backup and the database itself are gone.
+ Assert.ok(do_get_backup_file(profile).exists());
+ Assert.equal(do_get_backup_file(profile).fileSize, size);
+
+ Assert.equal(Services.cookiemgr.countCookiesFromHost("bar.com"), 0);
+ Assert.equal(Services.cookiemgr.countCookiesFromHost("0.com"), 0);
+ Assert.equal(do_count_cookies(), 0);
+
+ // Close the profile. We do not need to wait for completion, because the
+ // database has already been closed. Ensure the cookie file is unlocked.
+ await promise_close_profile();
+
+ // Clean up.
+ do_get_cookie_file(profile).remove(false);
+ do_get_backup_file(profile).remove(false);
+ Assert.ok(!do_get_cookie_file(profile).exists());
+ Assert.ok(!do_get_backup_file(profile).exists());
+}
diff --git a/netwerk/test/unit/test_cookies_persistence.js b/netwerk/test/unit/test_cookies_persistence.js
new file mode 100644
index 0000000000..a155bb5305
--- /dev/null
+++ b/netwerk/test/unit/test_cookies_persistence.js
@@ -0,0 +1,76 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// test for cookie persistence across sessions, for the cases:
+// 1) network.cookie.lifetimePolicy = 0 (expire naturally)
+// 2) network.cookie.lifetimePolicy = 2 (expire at end of session)
+
+"use strict";
+
+add_task(async () => {
+ // Set up a profile.
+ do_get_profile();
+
+ CookieXPCShellUtils.createServer({
+ hosts: ["foo.com", "bar.com", "third.com"],
+ });
+
+ // Create URIs and channels pointing to foo.com and bar.com.
+ // We will use these to put foo.com into first and third party contexts.
+ var spec1 = "http://foo.com/foo.html";
+ var spec2 = "http://bar.com/bar.html";
+ var uri1 = NetUtil.newURI(spec1);
+ var uri2 = NetUtil.newURI(spec2);
+ var channel1 = NetUtil.newChannel({
+ uri: uri1,
+ loadUsingSystemPrincipal: true,
+ });
+ var channel2 = NetUtil.newChannel({
+ uri: uri2,
+ loadUsingSystemPrincipal: true,
+ });
+
+ // Force the channel URI to be used when determining the originating URI of
+ // the channel.
+ var httpchannel1 = channel1.QueryInterface(Ci.nsIHttpChannelInternal);
+ var httpchannel2 = channel1.QueryInterface(Ci.nsIHttpChannelInternal);
+ httpchannel1.forceAllowThirdPartyCookie = true;
+ httpchannel2.forceAllowThirdPartyCookie = true;
+
+ // test with cookies enabled, and third party cookies persistent.
+ Services.prefs.setIntPref("network.cookie.cookieBehavior", 0);
+ Services.prefs.setBoolPref("network.cookie.thirdparty.sessionOnly", false);
+ Services.prefs.setBoolPref(
+ "network.cookieJarSettings.unblocked_for_testing",
+ true
+ );
+
+ await do_set_cookies(uri1, channel1, false, [1, 2]);
+ await do_set_cookies(uri2, channel2, true, [1, 2]);
+
+ // fake a profile change
+ await promise_close_profile();
+ do_load_profile();
+ Assert.equal(Services.cookies.countCookiesFromHost(uri1.host), 2);
+ Assert.equal(Services.cookies.countCookiesFromHost(uri2.host), 0);
+
+ // Again, but don't wait for the async close to complete. This should always
+ // work, since we blocked on close above and haven't kicked off any writes
+ // since then.
+ await promise_close_profile();
+ do_load_profile();
+ Assert.equal(Services.cookies.countCookiesFromHost(uri1.host), 2);
+ Assert.equal(Services.cookies.countCookiesFromHost(uri2.host), 0);
+
+ // test with cookies set to session-only
+ Services.prefs.setIntPref("network.cookie.lifetimePolicy", 2);
+ Services.cookies.removeAll();
+ await do_set_cookies(uri1, channel1, false, [1, 2]);
+ await do_set_cookies(uri2, channel2, true, [1, 2]);
+
+ // fake a profile change
+ await promise_close_profile();
+ do_load_profile();
+ Assert.equal(Services.cookies.countCookiesFromHost(uri1.host), 0);
+ Assert.equal(Services.cookies.countCookiesFromHost(uri2.host), 0);
+});
diff --git a/netwerk/test/unit/test_cookies_privatebrowsing.js b/netwerk/test/unit/test_cookies_privatebrowsing.js
new file mode 100644
index 0000000000..e594fdf3f9
--- /dev/null
+++ b/netwerk/test/unit/test_cookies_privatebrowsing.js
@@ -0,0 +1,139 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test private browsing mode.
+
+"use strict";
+
+function make_channel(url) {
+ return NetUtil.newChannel({
+ uri: url,
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+}
+
+function getCookieStringFromPrivateDocument(uriSpec) {
+ return CookieXPCShellUtils.getCookieStringFromDocument(uriSpec, {
+ privateBrowsing: true,
+ });
+}
+
+add_task(async () => {
+ // Set up a profile.
+ do_get_profile();
+
+ // We don't want to have CookieJarSettings blocking this test.
+ Services.prefs.setBoolPref(
+ "network.cookieJarSettings.unblocked_for_testing",
+ true
+ );
+
+ // Test with cookies enabled.
+ Services.prefs.setIntPref("network.cookie.cookieBehavior", 0);
+
+ CookieXPCShellUtils.createServer({ hosts: ["foo.com", "bar.com"] });
+
+ // We need to keep a private-browsing window active, otherwise the
+ // 'last-pb-context-exited' notification will be dispatched.
+ const privateBrowsingHolder = await CookieXPCShellUtils.loadContentPage(
+ "http://bar.com/",
+ { privateBrowsing: true }
+ );
+
+ // Create URIs pointing to foo.com and bar.com.
+ let uri1 = NetUtil.newURI("http://foo.com/foo.html");
+ let uri2 = NetUtil.newURI("http://bar.com/bar.html");
+
+ // Set a cookie for host 1.
+ Services.cookiesvc.setCookieStringFromHttp(
+ uri1,
+ "oh=hai; max-age=1000",
+ make_channel(uri1.spec)
+ );
+ Assert.equal(Services.cookiemgr.countCookiesFromHost(uri1.host), 1);
+
+ // Enter private browsing mode, set a cookie for host 2, and check the counts.
+ var chan1 = make_channel(uri1.spec);
+ chan1.QueryInterface(Ci.nsIPrivateBrowsingChannel);
+ chan1.setPrivate(true);
+
+ var chan2 = make_channel(uri2.spec);
+ chan2.QueryInterface(Ci.nsIPrivateBrowsingChannel);
+ chan2.setPrivate(true);
+
+ Services.cookiesvc.setCookieStringFromHttp(
+ uri2,
+ "oh=hai; max-age=1000",
+ chan2
+ );
+ Assert.equal(await getCookieStringFromPrivateDocument(uri1.spec), "");
+ Assert.equal(await getCookieStringFromPrivateDocument(uri2.spec), "oh=hai");
+
+ // Remove cookies and check counts.
+ Services.obs.notifyObservers(null, "last-pb-context-exited");
+ Assert.equal(await getCookieStringFromPrivateDocument(uri1.spec), "");
+ Assert.equal(await getCookieStringFromPrivateDocument(uri2.spec), "");
+
+ Services.cookiesvc.setCookieStringFromHttp(
+ uri2,
+ "oh=hai; max-age=1000",
+ chan2
+ );
+ Assert.equal(await getCookieStringFromPrivateDocument(uri2.spec), "oh=hai");
+
+ // Leave private browsing mode and check counts.
+ Services.obs.notifyObservers(null, "last-pb-context-exited");
+ Assert.equal(Services.cookiemgr.countCookiesFromHost(uri1.host), 1);
+ Assert.equal(Services.cookiemgr.countCookiesFromHost(uri2.host), 0);
+
+ // Fake a profile change.
+ await promise_close_profile();
+ do_load_profile();
+
+ // Check that the right cookie persisted.
+ Assert.equal(Services.cookiemgr.countCookiesFromHost(uri1.host), 1);
+ Assert.equal(Services.cookiemgr.countCookiesFromHost(uri2.host), 0);
+
+ // Enter private browsing mode, set a cookie for host 2, and check the counts.
+ Assert.equal(await getCookieStringFromPrivateDocument(uri1.spec), "");
+ Assert.equal(await getCookieStringFromPrivateDocument(uri2.spec), "");
+ Services.cookiesvc.setCookieStringFromHttp(
+ uri2,
+ "oh=hai; max-age=1000",
+ chan2
+ );
+ Assert.equal(await getCookieStringFromPrivateDocument(uri2.spec), "oh=hai");
+
+ // Fake a profile change.
+ await promise_close_profile();
+ do_load_profile();
+
+ // We're still in private browsing mode, but should have a new session.
+ // Check counts.
+ Assert.equal(await getCookieStringFromPrivateDocument(uri1.spec), "");
+ Assert.equal(await getCookieStringFromPrivateDocument(uri2.spec), "");
+
+ // Leave private browsing mode and check counts.
+ Services.obs.notifyObservers(null, "last-pb-context-exited");
+ Assert.equal(Services.cookiemgr.countCookiesFromHost(uri1.host), 1);
+ Assert.equal(Services.cookiemgr.countCookiesFromHost(uri2.host), 0);
+
+ // Enter private browsing mode.
+
+ // Fake a profile change, but wait for async read completion.
+ await promise_close_profile();
+ await promise_load_profile();
+
+ // We're still in private browsing mode, but should have a new session.
+ // Check counts.
+ Assert.equal(await getCookieStringFromPrivateDocument(uri1.spec), "");
+ Assert.equal(await getCookieStringFromPrivateDocument(uri2.spec), "");
+
+ // Leave private browsing mode and check counts.
+ Services.obs.notifyObservers(null, "last-pb-context-exited");
+ Assert.equal(Services.cookiemgr.countCookiesFromHost(uri1.host), 1);
+ Assert.equal(Services.cookiemgr.countCookiesFromHost(uri2.host), 0);
+
+ // Let's release the last PB window.
+ privateBrowsingHolder.close();
+});
diff --git a/netwerk/test/unit/test_cookies_profile_close.js b/netwerk/test/unit/test_cookies_profile_close.js
new file mode 100644
index 0000000000..f9cff71f4d
--- /dev/null
+++ b/netwerk/test/unit/test_cookies_profile_close.js
@@ -0,0 +1,112 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that the cookie APIs behave sanely after 'profile-before-change'.
+
+"use strict";
+
+add_task(async () => {
+ // Set up a profile.
+ do_get_profile();
+
+ // Allow all cookies.
+ Services.prefs.setIntPref("network.cookie.cookieBehavior", 0);
+ Services.prefs.setBoolPref(
+ "network.cookieJarSettings.unblocked_for_testing",
+ true
+ );
+
+ // Start the cookieservice.
+ Services.cookies;
+
+ CookieXPCShellUtils.createServer({ hosts: ["foo.com"] });
+
+ // Set a cookie.
+ let uri = NetUtil.newURI("http://foo.com");
+ let channel = NetUtil.newChannel({
+ uri,
+ loadUsingSystemPrincipal: true,
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT,
+ });
+
+ Services.scriptSecurityManager.createContentPrincipal(uri, {});
+
+ await CookieXPCShellUtils.setCookieToDocument(
+ uri.spec,
+ "oh=hai; max-age=1000"
+ );
+
+ let cookies = Services.cookiemgr.cookies;
+ Assert.ok(cookies.length == 1);
+ let cookie = cookies[0];
+
+ // Fire 'profile-before-change'.
+ do_close_profile();
+
+ let promise = new _promise_observer("cookie-db-closed");
+
+ // Check that the APIs behave appropriately.
+ Assert.equal(
+ await CookieXPCShellUtils.getCookieStringFromDocument("http://foo.com/"),
+ ""
+ );
+
+ Assert.equal(Services.cookiesvc.getCookieStringFromHttp(uri, channel), "");
+
+ await CookieXPCShellUtils.setCookieToDocument(uri.spec, "oh2=hai");
+
+ Services.cookiesvc.setCookieStringFromHttp(uri, "oh3=hai", channel);
+ Assert.equal(
+ await CookieXPCShellUtils.getCookieStringFromDocument("http://foo.com/"),
+ ""
+ );
+
+ do_check_throws(function() {
+ Services.cookiemgr.removeAll();
+ }, Cr.NS_ERROR_NOT_AVAILABLE);
+
+ do_check_throws(function() {
+ Services.cookiemgr.cookies;
+ }, Cr.NS_ERROR_NOT_AVAILABLE);
+
+ do_check_throws(function() {
+ Services.cookiemgr.add(
+ "foo.com",
+ "",
+ "oh4",
+ "hai",
+ false,
+ false,
+ false,
+ 0,
+ {},
+ Ci.nsICookie.SAMESITE_NONE,
+ Ci.nsICookie.SCHEME_HTTPS
+ );
+ }, Cr.NS_ERROR_NOT_AVAILABLE);
+
+ do_check_throws(function() {
+ Services.cookiemgr.remove("foo.com", "", "oh4", {});
+ }, Cr.NS_ERROR_NOT_AVAILABLE);
+
+ do_check_throws(function() {
+ Services.cookiemgr.cookieExists(cookie.host, cookie.path, cookie.name, {});
+ }, Cr.NS_ERROR_NOT_AVAILABLE);
+
+ do_check_throws(function() {
+ Services.cookies.countCookiesFromHost("foo.com");
+ }, Cr.NS_ERROR_NOT_AVAILABLE);
+
+ do_check_throws(function() {
+ Services.cookies.getCookiesFromHost("foo.com", {});
+ }, Cr.NS_ERROR_NOT_AVAILABLE);
+
+ // Wait for the database to finish closing.
+ await promise;
+
+ // Load the profile and check that the API is available.
+ do_load_profile();
+ Assert.ok(
+ Services.cookiemgr.cookieExists(cookie.host, cookie.path, cookie.name, {})
+ );
+});
diff --git a/netwerk/test/unit/test_cookies_read.js b/netwerk/test/unit/test_cookies_read.js
new file mode 100644
index 0000000000..614862e3f5
--- /dev/null
+++ b/netwerk/test/unit/test_cookies_read.js
@@ -0,0 +1,114 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// test cookie database asynchronous read operation.
+
+"use strict";
+
+var CMAX = 1000; // # of cookies to create
+
+add_task(async () => {
+ // Set up a profile.
+ let profile = do_get_profile();
+
+ // Allow all cookies.
+ Services.prefs.setIntPref("network.cookie.cookieBehavior", 0);
+ Services.prefs.setBoolPref(
+ "network.cookieJarSettings.unblocked_for_testing",
+ true
+ );
+
+ // Start the cookieservice, to force creation of a database.
+ // Get the sessionCookies to join the initialization in cookie thread
+ Services.cookiemgr.sessionCookies;
+
+ // Open a database connection now, after synchronous initialization has
+ // completed. We may not be able to open one later once asynchronous writing
+ // begins.
+ Assert.ok(do_get_cookie_file(profile).exists());
+ let db = new CookieDatabaseConnection(do_get_cookie_file(profile), 12);
+
+ let uri = NetUtil.newURI("http://foo.com/");
+ let channel = NetUtil.newChannel({
+ uri,
+ loadUsingSystemPrincipal: true,
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT,
+ });
+ for (let i = 0; i < CMAX; ++i) {
+ let uri = NetUtil.newURI("http://" + i + ".com/");
+ Services.cookiesvc.setCookieStringFromHttp(
+ uri,
+ "oh=hai; max-age=1000",
+ channel
+ );
+ }
+
+ Assert.equal(do_count_cookies(), CMAX);
+
+ // Wait until all CMAX cookies have been written out to the database.
+ while (do_count_cookies_in_db(db.db) < CMAX) {
+ await new Promise(resolve => executeSoon(resolve));
+ }
+
+ // Check the WAL file size. We set it to 16 pages of 32k, which means it
+ // should be around 500k.
+ let file = db.db.databaseFile;
+ Assert.ok(file.exists());
+ Assert.ok(file.fileSize < 1e6);
+ db.close();
+
+ // fake a profile change
+ await promise_close_profile();
+ do_load_profile();
+
+ // test a few random cookies
+ Assert.equal(Services.cookiemgr.countCookiesFromHost("999.com"), 1);
+ Assert.equal(Services.cookiemgr.countCookiesFromHost("abc.com"), 0);
+ Assert.equal(Services.cookiemgr.countCookiesFromHost("100.com"), 1);
+ Assert.equal(Services.cookiemgr.countCookiesFromHost("400.com"), 1);
+ Assert.equal(Services.cookiemgr.countCookiesFromHost("xyz.com"), 0);
+
+ // force synchronous load of everything
+ Assert.equal(do_count_cookies(), CMAX);
+
+ // check that everything's precisely correct
+ for (let i = 0; i < CMAX; ++i) {
+ let host = i.toString() + ".com";
+ Assert.equal(Services.cookiemgr.countCookiesFromHost(host), 1);
+ }
+
+ // reload again, to make sure the additions were written correctly
+ await promise_close_profile();
+ do_load_profile();
+
+ // remove some of the cookies, in both reverse and forward order
+ for (let i = 100; i-- > 0; ) {
+ let host = i.toString() + ".com";
+ Services.cookiemgr.remove(host, "oh", "/", {});
+ }
+ for (let i = CMAX - 100; i < CMAX; ++i) {
+ let host = i.toString() + ".com";
+ Services.cookiemgr.remove(host, "oh", "/", {});
+ }
+
+ // check the count
+ Assert.equal(do_count_cookies(), CMAX - 200);
+
+ // reload again, to make sure the removals were written correctly
+ await promise_close_profile();
+ do_load_profile();
+
+ // check the count
+ Assert.equal(do_count_cookies(), CMAX - 200);
+
+ // reload again, but wait for async read completion
+ await promise_close_profile();
+ await promise_load_profile();
+
+ // check that everything's precisely correct
+ Assert.equal(do_count_cookies(), CMAX - 200);
+ for (let i = 100; i < CMAX - 100; ++i) {
+ let host = i.toString() + ".com";
+ Assert.equal(Services.cookiemgr.countCookiesFromHost(host), 1);
+ }
+});
diff --git a/netwerk/test/unit/test_cookies_sync_failure.js b/netwerk/test/unit/test_cookies_sync_failure.js
new file mode 100644
index 0000000000..f63955b483
--- /dev/null
+++ b/netwerk/test/unit/test_cookies_sync_failure.js
@@ -0,0 +1,344 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test the various ways opening a cookie database can fail in a synchronous
+// (i.e. immediate) manner, and that the database is renamed and recreated
+// under each circumstance. These circumstances are, in no particular order:
+//
+// 1) A corrupt database, such that opening the connection fails.
+// 2) The 'moz_cookies' table doesn't exist.
+// 3) Not all of the expected columns exist, and statement creation fails when:
+// a) The schema version is larger than the current version.
+// b) The schema version is less than or equal to the current version.
+// 4) Migration fails. This will have different modes depending on the initial
+// version:
+// a) Schema 1: the 'lastAccessed' column already exists.
+// b) Schema 2: the 'baseDomain' column already exists; or 'baseDomain'
+// cannot be computed for a particular host.
+// c) Schema 3: the 'creationTime' column already exists; or the
+// 'moz_uniqueid' index already exists.
+
+"use strict";
+
+let profile;
+let cookieFile;
+let backupFile;
+let sub_generator;
+let now;
+let futureExpiry;
+let cookie;
+
+var COOKIE_DATABASE_SCHEMA_CURRENT = 12;
+
+var test_generator = do_run_test();
+
+function run_test() {
+ do_test_pending();
+ do_run_generator(test_generator);
+}
+
+function finish_test() {
+ executeSoon(function() {
+ test_generator.return();
+ do_test_finished();
+ });
+}
+
+function* do_run_test() {
+ // Set up a profile.
+ profile = do_get_profile();
+
+ // Allow all cookies.
+ Services.prefs.setIntPref("network.cookie.cookieBehavior", 0);
+ Services.prefs.setBoolPref(
+ "network.cookieJarSettings.unblocked_for_testing",
+ true
+ );
+
+ // Get the cookie file and the backup file.
+ cookieFile = profile.clone();
+ cookieFile.append("cookies.sqlite");
+ backupFile = profile.clone();
+ backupFile.append("cookies.sqlite.bak");
+ Assert.ok(!cookieFile.exists());
+ Assert.ok(!backupFile.exists());
+
+ // Create a cookie object for testing.
+ now = Date.now() * 1000;
+ futureExpiry = Math.round(now / 1e6 + 1000);
+ cookie = new Cookie(
+ "oh",
+ "hai",
+ "bar.com",
+ "/",
+ futureExpiry,
+ now,
+ now,
+ false,
+ false,
+ false
+ );
+
+ sub_generator = run_test_1(test_generator);
+ sub_generator.next();
+ yield;
+
+ sub_generator = run_test_2(test_generator);
+ sub_generator.next();
+ yield;
+
+ sub_generator = run_test_3(test_generator, 99);
+ sub_generator.next();
+ yield;
+
+ sub_generator = run_test_3(test_generator, COOKIE_DATABASE_SCHEMA_CURRENT);
+ sub_generator.next();
+ yield;
+
+ sub_generator = run_test_3(test_generator, 4);
+ sub_generator.next();
+ yield;
+
+ sub_generator = run_test_3(test_generator, 3);
+ sub_generator.next();
+ yield;
+
+ sub_generator = run_test_4_exists(
+ test_generator,
+ 1,
+ "ALTER TABLE moz_cookies ADD lastAccessed INTEGER"
+ );
+ sub_generator.next();
+ yield;
+
+ sub_generator = run_test_4_exists(
+ test_generator,
+ 2,
+ "ALTER TABLE moz_cookies ADD baseDomain TEXT"
+ );
+ sub_generator.next();
+ yield;
+
+ sub_generator = run_test_4_baseDomain(test_generator);
+ sub_generator.next();
+ yield;
+
+ sub_generator = run_test_4_exists(
+ test_generator,
+ 3,
+ "ALTER TABLE moz_cookies ADD creationTime INTEGER"
+ );
+ sub_generator.next();
+ yield;
+
+ sub_generator = run_test_4_exists(
+ test_generator,
+ 3,
+ "CREATE UNIQUE INDEX moz_uniqueid ON moz_cookies (name, host, path)"
+ );
+ sub_generator.next();
+ yield;
+
+ finish_test();
+}
+
+const garbage = "hello thar!";
+
+function create_garbage_file(file) {
+ // Create an empty database file.
+ file.create(Ci.nsIFile.NORMAL_FILE_TYPE, -1);
+ Assert.ok(file.exists());
+ Assert.equal(file.fileSize, 0);
+
+ // Write some garbage to it.
+ let ostream = Cc["@mozilla.org/network/file-output-stream;1"].createInstance(
+ Ci.nsIFileOutputStream
+ );
+ ostream.init(file, -1, -1, 0);
+ ostream.write(garbage, garbage.length);
+ ostream.flush();
+ ostream.close();
+
+ file = file.clone(); // Windows maintains a stat cache. It's lame.
+ Assert.equal(file.fileSize, garbage.length);
+}
+
+function check_garbage_file(file) {
+ Assert.ok(file.exists());
+ Assert.equal(file.fileSize, garbage.length);
+ file.remove(false);
+ Assert.ok(!file.exists());
+}
+
+function* run_test_1(generator) {
+ // Create a garbage database file.
+ create_garbage_file(cookieFile);
+
+ let uri = NetUtil.newURI("http://foo.com/");
+ const channel = NetUtil.newChannel({
+ uri,
+ loadUsingSystemPrincipal: true,
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT,
+ });
+
+ // Load the profile and populate it.
+ Services.cookiesvc.setCookieStringFromHttp(
+ uri,
+ "oh=hai; max-age=1000",
+ channel
+ );
+
+ // Fake a profile change.
+ do_close_profile(sub_generator);
+ yield;
+ do_load_profile();
+
+ // Check that the new database contains the cookie, and the old file was
+ // renamed.
+ Assert.equal(do_count_cookies(), 1);
+ check_garbage_file(backupFile);
+
+ // Close the profile.
+ do_close_profile(sub_generator);
+ yield;
+
+ // Clean up.
+ cookieFile.remove(false);
+ Assert.ok(!cookieFile.exists());
+ do_run_generator(generator);
+}
+
+function* run_test_2(generator) {
+ // Load the profile and populate it.
+ do_load_profile();
+ let uri = NetUtil.newURI("http://foo.com/");
+ const channel = NetUtil.newChannel({
+ uri,
+ loadUsingSystemPrincipal: true,
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT,
+ });
+ Services.cookiesvc.setCookieStringFromHttp(
+ uri,
+ "oh=hai; max-age=1000",
+ channel
+ );
+
+ // Fake a profile change.
+ do_close_profile(sub_generator);
+ yield;
+
+ // Drop the table.
+ let db = Services.storage.openDatabase(cookieFile);
+ db.executeSimpleSQL("DROP TABLE moz_cookies");
+ db.close();
+
+ // Load the profile and check that the table is recreated in-place.
+ do_load_profile();
+ Assert.equal(do_count_cookies(), 0);
+ Assert.ok(!backupFile.exists());
+
+ // Close the profile.
+ do_close_profile(sub_generator);
+ yield;
+
+ // Clean up.
+ cookieFile.remove(false);
+ Assert.ok(!cookieFile.exists());
+ do_run_generator(generator);
+}
+
+function* run_test_3(generator, schema) {
+ // Manually create a schema 2 database, populate it, and set the schema
+ // version to the desired number.
+ let schema2db = new CookieDatabaseConnection(do_get_cookie_file(profile), 2);
+ schema2db.insertCookie(cookie);
+ schema2db.db.schemaVersion = schema;
+ schema2db.close();
+
+ // Load the profile and check that the column existence test fails.
+ do_load_profile();
+ Assert.equal(do_count_cookies(), 0);
+
+ // Close the profile.
+ do_close_profile(sub_generator);
+ yield;
+
+ // Check that the schema version has been reset.
+ let db = Services.storage.openDatabase(cookieFile);
+ Assert.equal(db.schemaVersion, COOKIE_DATABASE_SCHEMA_CURRENT);
+ db.close();
+
+ // Clean up.
+ cookieFile.remove(false);
+ Assert.ok(!cookieFile.exists());
+ do_run_generator(generator);
+}
+
+function* run_test_4_exists(generator, schema, stmt) {
+ // Manually create a database, populate it, and add the desired column.
+ let db = new CookieDatabaseConnection(do_get_cookie_file(profile), schema);
+ db.insertCookie(cookie);
+ db.db.executeSimpleSQL(stmt);
+ db.close();
+
+ // Load the profile and check that migration fails.
+ do_load_profile();
+ Assert.equal(do_count_cookies(), 0);
+
+ // Close the profile.
+ do_close_profile(sub_generator);
+ yield;
+
+ // Check that the schema version has been reset and the backup file exists.
+ db = Services.storage.openDatabase(cookieFile);
+ Assert.equal(db.schemaVersion, COOKIE_DATABASE_SCHEMA_CURRENT);
+ db.close();
+ Assert.ok(backupFile.exists());
+
+ // Clean up.
+ cookieFile.remove(false);
+ backupFile.remove(false);
+ Assert.ok(!cookieFile.exists());
+ Assert.ok(!backupFile.exists());
+ do_run_generator(generator);
+}
+
+function* run_test_4_baseDomain(generator) {
+ // Manually create a database and populate it with a bad host.
+ let db = new CookieDatabaseConnection(do_get_cookie_file(profile), 2);
+ let badCookie = new Cookie(
+ "oh",
+ "hai",
+ ".",
+ "/",
+ futureExpiry,
+ now,
+ now,
+ false,
+ false,
+ false
+ );
+ db.insertCookie(badCookie);
+ db.close();
+
+ // Load the profile and check that migration fails.
+ do_load_profile();
+ Assert.equal(do_count_cookies(), 0);
+
+ // Close the profile.
+ do_close_profile(sub_generator);
+ yield;
+
+ // Check that the schema version has been reset and the backup file exists.
+ db = Services.storage.openDatabase(cookieFile);
+ Assert.equal(db.schemaVersion, COOKIE_DATABASE_SCHEMA_CURRENT);
+ db.close();
+ Assert.ok(backupFile.exists());
+
+ // Clean up.
+ cookieFile.remove(false);
+ backupFile.remove(false);
+ Assert.ok(!cookieFile.exists());
+ Assert.ok(!backupFile.exists());
+ do_run_generator(generator);
+}
diff --git a/netwerk/test/unit/test_cookies_thirdparty.js b/netwerk/test/unit/test_cookies_thirdparty.js
new file mode 100644
index 0000000000..413fd1e2e4
--- /dev/null
+++ b/netwerk/test/unit/test_cookies_thirdparty.js
@@ -0,0 +1,162 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// test third party cookie blocking, for the cases:
+// 1) with null channel
+// 2) with channel, but with no docshell parent
+
+"use strict";
+
+add_task(async () => {
+ Services.prefs.setBoolPref(
+ "network.cookieJarSettings.unblocked_for_testing",
+ true
+ );
+
+ Services.prefs.setBoolPref(
+ "network.cookie.rejectForeignWithExceptions.enabled",
+ false
+ );
+
+ CookieXPCShellUtils.createServer({
+ hosts: ["foo.com", "bar.com", "third.com"],
+ });
+
+ function createChannel(uri, principal = null) {
+ const channel = NetUtil.newChannel({
+ uri,
+ loadingPrincipal:
+ principal ||
+ Services.scriptSecurityManager.createContentPrincipal(uri, {}),
+ securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER,
+ });
+
+ return channel.QueryInterface(Ci.nsIHttpChannelInternal);
+ }
+
+ // Create URIs and channels pointing to foo.com and bar.com.
+ // We will use these to put foo.com into first and third party contexts.
+ let spec1 = "http://foo.com/foo.html";
+ let spec2 = "http://bar.com/bar.html";
+ let uri1 = NetUtil.newURI(spec1);
+ let uri2 = NetUtil.newURI(spec2);
+
+ // test with cookies enabled
+ {
+ Services.prefs.setIntPref(
+ "network.cookie.cookieBehavior",
+ Ci.nsICookieService.BEHAVIOR_ACCEPT
+ );
+
+ let channel1 = createChannel(uri1);
+ let channel2 = createChannel(uri2);
+
+ await do_set_cookies(uri1, channel1, true, [1, 2]);
+ Services.cookies.removeAll();
+ await do_set_cookies(uri1, channel2, true, [1, 2]);
+ Services.cookies.removeAll();
+ }
+
+ // test with third party cookies blocked
+ {
+ Services.prefs.setIntPref(
+ "network.cookie.cookieBehavior",
+ Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN
+ );
+
+ let channel1 = createChannel(uri1);
+ let channel2 = createChannel(uri2);
+
+ await do_set_cookies(uri1, channel1, true, [0, 1]);
+ Services.cookies.removeAll();
+ await do_set_cookies(uri1, channel2, true, [0, 0]);
+ Services.cookies.removeAll();
+ }
+
+ // test with third party cookies blocked using system principal
+ {
+ Services.prefs.setIntPref(
+ "network.cookie.cookieBehavior",
+ Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN
+ );
+
+ let channel1 = createChannel(
+ uri1,
+ Services.scriptSecurityManager.getSystemPrincipal()
+ );
+ let channel2 = createChannel(
+ uri2,
+ Services.scriptSecurityManager.getSystemPrincipal()
+ );
+
+ await do_set_cookies(uri1, channel1, true, [0, 1]);
+ Services.cookies.removeAll();
+ await do_set_cookies(uri1, channel2, true, [0, 0]);
+ Services.cookies.removeAll();
+ }
+
+ // Force the channel URI to be used when determining the originating URI of
+ // the channel.
+ // test with third party cookies blocked
+
+ // test with cookies enabled
+ {
+ Services.prefs.setIntPref(
+ "network.cookie.cookieBehavior",
+ Ci.nsICookieService.BEHAVIOR_ACCEPT
+ );
+
+ let channel1 = createChannel(uri1);
+ channel1.forceAllowThirdPartyCookie = true;
+
+ let channel2 = createChannel(uri2);
+ channel2.forceAllowThirdPartyCookie = true;
+
+ await do_set_cookies(uri1, channel1, true, [1, 2]);
+ Services.cookies.removeAll();
+ await do_set_cookies(uri1, channel2, true, [1, 2]);
+ Services.cookies.removeAll();
+ }
+
+ // test with third party cookies blocked
+ {
+ Services.prefs.setIntPref(
+ "network.cookie.cookieBehavior",
+ Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN
+ );
+
+ let channel1 = createChannel(uri1);
+ channel1.forceAllowThirdPartyCookie = true;
+
+ let channel2 = createChannel(uri2);
+ channel2.forceAllowThirdPartyCookie = true;
+
+ await do_set_cookies(uri1, channel1, true, [0, 1]);
+ Services.cookies.removeAll();
+ await do_set_cookies(uri1, channel2, true, [0, 0]);
+ Services.cookies.removeAll();
+ }
+
+ // test with third party cookies limited
+ {
+ Services.prefs.setIntPref(
+ "network.cookie.cookieBehavior",
+ Ci.nsICookieService.BEHAVIOR_LIMIT_FOREIGN
+ );
+
+ let channel1 = createChannel(uri1);
+ channel1.forceAllowThirdPartyCookie = true;
+
+ let channel2 = createChannel(uri2);
+ channel2.forceAllowThirdPartyCookie = true;
+
+ await do_set_cookies(uri1, channel1, true, [0, 1]);
+ Services.cookies.removeAll();
+ await do_set_cookies(uri1, channel2, true, [0, 0]);
+ Services.cookies.removeAll();
+ do_set_single_http_cookie(uri1, channel1, 1);
+ await do_set_cookies(uri1, channel2, true, [1, 2]);
+ Services.cookies.removeAll();
+ }
+});
diff --git a/netwerk/test/unit/test_cookies_thirdparty_session.js b/netwerk/test/unit/test_cookies_thirdparty_session.js
new file mode 100644
index 0000000000..a846689790
--- /dev/null
+++ b/netwerk/test/unit/test_cookies_thirdparty_session.js
@@ -0,0 +1,71 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// test third party persistence across sessions, for the cases:
+// 1) network.cookie.thirdparty.sessionOnly = false
+// 2) network.cookie.thirdparty.sessionOnly = true
+
+"use strict";
+
+add_task(async () => {
+ // Set up a profile.
+ do_get_profile();
+
+ // We don't want to have CookieJarSettings blocking this test.
+ Services.prefs.setBoolPref(
+ "network.cookieJarSettings.unblocked_for_testing",
+ true
+ );
+
+ CookieXPCShellUtils.createServer({
+ hosts: ["foo.com", "bar.com", "third.com"],
+ });
+
+ // Create URIs and channels pointing to foo.com and bar.com.
+ // We will use these to put foo.com into first and third party contexts.
+ var spec1 = "http://foo.com/foo.html";
+ var spec2 = "http://bar.com/bar.html";
+ var uri1 = NetUtil.newURI(spec1);
+ var uri2 = NetUtil.newURI(spec2);
+ var channel1 = NetUtil.newChannel({
+ uri: uri1,
+ loadUsingSystemPrincipal: true,
+ });
+ var channel2 = NetUtil.newChannel({
+ uri: uri2,
+ loadUsingSystemPrincipal: true,
+ });
+
+ // Force the channel URI to be used when determining the originating URI of
+ // the channel.
+ var httpchannel1 = channel1.QueryInterface(Ci.nsIHttpChannelInternal);
+ var httpchannel2 = channel2.QueryInterface(Ci.nsIHttpChannelInternal);
+ httpchannel1.forceAllowThirdPartyCookie = true;
+ httpchannel2.forceAllowThirdPartyCookie = true;
+
+ // test with cookies enabled, and third party cookies persistent.
+ Services.prefs.setIntPref("network.cookie.cookieBehavior", 0);
+ Services.prefs.setBoolPref("network.cookie.thirdparty.sessionOnly", false);
+ await do_set_cookies(uri1, channel2, false, [1, 2]);
+ await do_set_cookies(uri2, channel1, true, [1, 2]);
+
+ // fake a profile change
+ await promise_close_profile();
+
+ do_load_profile();
+ Assert.equal(Services.cookies.countCookiesFromHost(uri1.host), 2);
+ Assert.equal(Services.cookies.countCookiesFromHost(uri2.host), 0);
+
+ // test with third party cookies for session only.
+ Services.prefs.setBoolPref("network.cookie.thirdparty.sessionOnly", true);
+ Services.cookies.removeAll();
+ await do_set_cookies(uri1, channel2, false, [1, 2]);
+ await do_set_cookies(uri2, channel1, true, [1, 2]);
+
+ // fake a profile change
+ await promise_close_profile();
+
+ do_load_profile();
+ Assert.equal(Services.cookies.countCookiesFromHost(uri1.host), 0);
+ Assert.equal(Services.cookies.countCookiesFromHost(uri2.host), 0);
+});
diff --git a/netwerk/test/unit/test_cookies_upgrade_10.js b/netwerk/test/unit/test_cookies_upgrade_10.js
new file mode 100644
index 0000000000..61dbcda46e
--- /dev/null
+++ b/netwerk/test/unit/test_cookies_upgrade_10.js
@@ -0,0 +1,59 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+function getDBVersion(dbfile) {
+ let dbConnection = Services.storage.openDatabase(dbfile);
+ let version = dbConnection.schemaVersion;
+ dbConnection.close();
+
+ return version;
+}
+
+function indexExists(dbfile, indexname) {
+ let dbConnection = Services.storage.openDatabase(dbfile);
+ let result = dbConnection.indexExists(indexname);
+ dbConnection.close();
+
+ return result;
+}
+
+add_task(async function() {
+ try {
+ let testfile = do_get_file("data/cookies_v10.sqlite");
+ let profileDir = do_get_profile();
+
+ // Cleanup from any previous tests or failures.
+ let destFile = profileDir.clone();
+ destFile.append("cookies.sqlite");
+ if (destFile.exists()) {
+ destFile.remove(false);
+ }
+
+ testfile.copyTo(profileDir, "cookies.sqlite");
+ Assert.equal(10, getDBVersion(destFile));
+
+ Assert.ok(destFile.exists());
+
+ // Check that the index exists
+ Assert.ok(indexExists(destFile, "moz_basedomain"));
+
+ // Do something that will cause the cookie service access and upgrade the
+ // database.
+ Services.cookies.cookies;
+
+ // Pretend that we're about to shut down, to tell the cookie manager
+ // to clean up its connection with its database.
+ Services.obs.notifyObservers(null, "profile-before-change");
+
+ // check for upgraded schema.
+ Assert.equal(12, getDBVersion(destFile));
+
+ // Check that the index was deleted
+ Assert.ok(!indexExists(destFile, "moz_basedomain"));
+ } catch (e) {
+ throw new Error(`FAILED: ${e}`);
+ }
+});
diff --git a/netwerk/test/unit/test_data_protocol.js b/netwerk/test/unit/test_data_protocol.js
new file mode 100644
index 0000000000..3ce36b2242
--- /dev/null
+++ b/netwerk/test/unit/test_data_protocol.js
@@ -0,0 +1,91 @@
+/* run some tests on the data: protocol handler */
+
+// The behaviour wrt spaces is:
+// - Textual content keeps all spaces
+// - Other content strips unescaped spaces
+// - Base64 content strips escaped and unescaped spaces
+
+"use strict";
+
+var urls = [
+ ["data:,", "text/plain", ""],
+ ["data:,foo", "text/plain", "foo"],
+ [
+ "data:application/octet-stream,foo bar",
+ "application/octet-stream",
+ "foobar",
+ ],
+ [
+ "data:application/octet-stream,foo%20bar",
+ "application/octet-stream",
+ "foo bar",
+ ],
+ ["data:application/xhtml+xml,foo bar", "application/xhtml+xml", "foo bar"],
+ ["data:application/xhtml+xml,foo%20bar", "application/xhtml+xml", "foo bar"],
+ ["data:text/plain,foo%00 bar", "text/plain", "foo\x00 bar"],
+ ["data:text/plain;x=y,foo%00 bar", "text/plain", "foo\x00 bar"],
+ ["data:;x=y,foo%00 bar", "text/plain", "foo\x00 bar"],
+ ["data:text/plain;base64,Zm9 vI%20GJ%0Dhc%0Ag==", "text/plain", "foo bar"],
+ ["DATA:TEXT/PLAIN;BASE64,Zm9 vI%20GJ%0Dhc%0Ag==", "text/plain", "foo bar"],
+ ["DaTa:;BaSe64,Zm9 vI%20GJ%0Dhc%0Ag==", "text/plain", "foo bar"],
+ ["data:;x=y;base64,Zm9 vI%20GJ%0Dhc%0Ag==", "text/plain", "foo bar"],
+ // Bug 774240
+ [
+ "data:application/octet-stream;base64=y,foobar",
+ "application/octet-stream",
+ "foobar",
+ ],
+ // Bug 781693
+ ["data:text/plain;base64;x=y,dGVzdA==", "text/plain", "test"],
+ ["data:text/plain;x=y;base64,dGVzdA==", "text/plain", "test"],
+ ["data:text/plain;x=y;base64,", "text/plain", ""],
+ ["data: ;charset=x ; base64,WA", "text/plain", "X", "x"],
+ ["data:base64,WA", "text/plain", "WA", "US-ASCII"],
+];
+
+function run_test() {
+ dump("*** run_test\n");
+
+ function on_read_complete(request, data, idx) {
+ dump("*** run_test.on_read_complete\n");
+
+ if (request.nsIChannel.contentType != urls[idx][1]) {
+ do_throw(
+ "Type mismatch! Is <" +
+ chan.contentType +
+ ">, should be <" +
+ urls[idx][1] +
+ ">"
+ );
+ }
+
+ if (urls[idx][3] && request.nsIChannel.contentCharset !== urls[idx][3]) {
+ do_throw(
+ `Charset mismatch! Test <${urls[idx][0]}> - Is <${request.nsIChannel.contentCharset}>, should be <${urls[idx][3]}>`
+ );
+ }
+
+ /* read completed successfully. now compare the data. */
+ if (data != urls[idx][2]) {
+ do_throw(
+ "Stream contents do not match with direct read! Is <" +
+ data +
+ ">, should be <" +
+ urls[idx][2] +
+ ">"
+ );
+ }
+ do_test_finished();
+ }
+
+ for (var i = 0; i < urls.length; ++i) {
+ dump("*** opening channel " + i + "\n");
+ do_test_pending();
+ var chan = NetUtil.newChannel({
+ uri: urls[i][0],
+ loadUsingSystemPrincipal: true,
+ });
+ chan.contentType = "foo/bar"; // should be ignored
+ chan.asyncOpen(new ChannelListener(on_read_complete, i));
+ }
+}
diff --git a/netwerk/test/unit/test_defaultURI.js b/netwerk/test/unit/test_defaultURI.js
new file mode 100644
index 0000000000..92ac3042b3
--- /dev/null
+++ b/netwerk/test/unit/test_defaultURI.js
@@ -0,0 +1,185 @@
+"use strict";
+
+function stringToDefaultURI(str) {
+ return Cc["@mozilla.org/network/default-uri-mutator;1"]
+ .createInstance(Ci.nsIURIMutator)
+ .setSpec(str)
+ .finalize();
+}
+
+add_task(function test_getters() {
+ let uri = stringToDefaultURI(
+ "proto://user:password@hostname:123/path/to/file?query#hash"
+ );
+ equal(uri.spec, "proto://user:password@hostname:123/path/to/file?query#hash");
+ equal(uri.prePath, "proto://user:password@hostname:123");
+ equal(uri.scheme, "proto");
+ equal(uri.userPass, "user:password");
+ equal(uri.username, "user");
+ equal(uri.password, "password");
+ equal(uri.hostPort, "hostname:123");
+ equal(uri.host, "hostname");
+ equal(uri.port, 123);
+ equal(uri.pathQueryRef, "/path/to/file?query#hash");
+ equal(uri.asciiSpec, uri.spec);
+ equal(uri.asciiHostPort, uri.hostPort);
+ equal(uri.asciiHost, uri.host);
+ equal(uri.ref, "hash");
+ equal(
+ uri.specIgnoringRef,
+ "proto://user:password@hostname:123/path/to/file?query"
+ );
+ equal(uri.hasRef, true);
+ equal(uri.filePath, "/path/to/file");
+ equal(uri.query, "query");
+ equal(uri.displayHost, uri.host);
+ equal(uri.displayHostPort, uri.hostPort);
+ equal(uri.displaySpec, uri.spec);
+ equal(uri.displayPrePath, uri.prePath);
+});
+
+add_task(function test_methods() {
+ let uri = stringToDefaultURI(
+ "proto://user:password@hostname:123/path/to/file?query#hash"
+ );
+ let uri_same = stringToDefaultURI(
+ "proto://user:password@hostname:123/path/to/file?query#hash"
+ );
+ let uri_different = stringToDefaultURI(
+ "proto://user:password@hostname:123/path/to/file?query"
+ );
+ let uri_very_different = stringToDefaultURI(
+ "proto://user:password@hostname:123/path/to/file?query1#hash"
+ );
+ ok(uri.equals(uri_same));
+ ok(!uri.equals(uri_different));
+ ok(uri.schemeIs("proto"));
+ ok(!uri.schemeIs("proto2"));
+ ok(!uri.schemeIs("proto "));
+ ok(!uri.schemeIs("proto\n"));
+ equal(uri.resolve("/hello"), "proto://user:password@hostname:123/hello");
+ equal(
+ uri.resolve("hello"),
+ "proto://user:password@hostname:123/path/to/hello"
+ );
+ equal(uri.resolve("proto2:otherhost"), "proto2:otherhost");
+ ok(uri.equalsExceptRef(uri_same));
+ ok(uri.equalsExceptRef(uri_different));
+ ok(!uri.equalsExceptRef(uri_very_different));
+});
+
+add_task(function test_mutator() {
+ let uri = stringToDefaultURI(
+ "proto://user:pass@host:123/path/to/file?query#hash"
+ );
+
+ let check = (callSetters, verify) => {
+ let m = uri.mutate();
+ callSetters(m);
+ verify(m.finalize());
+ };
+
+ check(
+ m => m.setSpec("test:bla"),
+ u => equal(u.spec, "test:bla")
+ );
+ check(
+ m => m.setSpec("test:bla"),
+ u => equal(u.spec, "test:bla")
+ );
+ check(
+ m => m.setScheme("some"),
+ u => equal(u.spec, "some://user:pass@host:123/path/to/file?query#hash")
+ );
+ check(
+ m => m.setUserPass("u"),
+ u => equal(u.spec, "proto://u@host:123/path/to/file?query#hash")
+ );
+ check(
+ m => m.setUserPass("u:p"),
+ u => equal(u.spec, "proto://u:p@host:123/path/to/file?query#hash")
+ );
+ check(
+ m => m.setUserPass(":p"),
+ u => equal(u.spec, "proto://:p@host:123/path/to/file?query#hash")
+ );
+ check(
+ m => m.setUserPass(""),
+ u => equal(u.spec, "proto://host:123/path/to/file?query#hash")
+ );
+ check(
+ m => m.setUsername("u"),
+ u => equal(u.spec, "proto://u:pass@host:123/path/to/file?query#hash")
+ );
+ check(
+ m => m.setPassword("p"),
+ u => equal(u.spec, "proto://user:p@host:123/path/to/file?query#hash")
+ );
+ check(
+ m => m.setHostPort("h"),
+ u => equal(u.spec, "proto://user:pass@h:123/path/to/file?query#hash")
+ );
+ check(
+ m => m.setHostPort("h:456"),
+ u => equal(u.spec, "proto://user:pass@h:456/path/to/file?query#hash")
+ );
+ check(
+ m => m.setHost("bla"),
+ u => equal(u.spec, "proto://user:pass@bla:123/path/to/file?query#hash")
+ );
+ check(
+ m => m.setPort(987),
+ u => equal(u.spec, "proto://user:pass@host:987/path/to/file?query#hash")
+ );
+ check(
+ m => m.setPathQueryRef("/p?q#r"),
+ u => equal(u.spec, "proto://user:pass@host:123/p?q#r")
+ );
+ check(
+ m => m.setRef("r"),
+ u => equal(u.spec, "proto://user:pass@host:123/path/to/file?query#r")
+ );
+ check(
+ m => m.setFilePath("/my/path"),
+ u => equal(u.spec, "proto://user:pass@host:123/my/path?query#hash")
+ );
+ check(
+ m => m.setQuery("q"),
+ u => equal(u.spec, "proto://user:pass@host:123/path/to/file?q#hash")
+ );
+});
+
+add_task(function test_ipv6() {
+ let uri = stringToDefaultURI("non-special://[2001::1]/");
+ equal(uri.hostPort, "[2001::1]");
+ // Hopefully this will change after bug 1603199.
+ equal(uri.host, "2001::1");
+});
+
+add_task(function test_serialization() {
+ let uri = stringToDefaultURI("http://example.org/path");
+ let str = serialize_to_escaped_string(uri);
+ let other = deserialize_from_escaped_string(str).QueryInterface(Ci.nsIURI);
+ equal(other.spec, uri.spec);
+});
+
+// This test assumes the serialization never changes, which might not be true.
+// It's OK to change the test if we ever make changes to the serialization
+// code and this starts failing.
+add_task(function test_deserialize_from_string() {
+ let payload =
+ "%04DZ%A0%FD%27L%99%BDAk%E61%8A%E9%2C%00%00%00%00%00%00%00" +
+ "%00%C0%00%00%00%00%00%00F%00%00%00%13scheme%3Astuff/to/say";
+ equal(
+ deserialize_from_escaped_string(payload).QueryInterface(Ci.nsIURI).spec,
+ stringToDefaultURI("scheme:stuff/to/say").spec
+ );
+
+ let payload2 =
+ "%04DZ%A0%FD%27L%99%BDAk%E61%8A%E9%2C%00%00%00%00%00%00%00" +
+ "%00%C0%00%00%00%00%00%00F%00%00%00%17http%3A//example.org/path";
+ equal(
+ deserialize_from_escaped_string(payload2).QueryInterface(Ci.nsIURI).spec,
+ stringToDefaultURI("http://example.org/path").spec
+ );
+});
diff --git a/netwerk/test/unit/test_disabled_ftp.js b/netwerk/test/unit/test_disabled_ftp.js
new file mode 100644
index 0000000000..953c9ac45b
--- /dev/null
+++ b/netwerk/test/unit/test_disabled_ftp.js
@@ -0,0 +1,21 @@
+"use strict";
+
+registerCleanupFunction(function() {
+ Services.prefs.clearUserPref("network.ftp.enabled");
+});
+
+add_task(async function ftp_disabled() {
+ Services.prefs.setBoolPref("network.ftp.enabled", false);
+ Services.prefs.setBoolPref("network.protocol-handler.external.ftp", false);
+
+ Assert.throws(
+ () => {
+ NetUtil.newChannel({
+ uri: "ftp://ftp.de.debian.org/",
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+ },
+ /NS_ERROR_UNKNOWN_PROTOCOL/,
+ "creating the FTP channel must throw"
+ );
+});
diff --git a/netwerk/test/unit/test_dns_by_type_resolve.js b/netwerk/test/unit/test_dns_by_type_resolve.js
new file mode 100644
index 0000000000..f31325b8c0
--- /dev/null
+++ b/netwerk/test/unit/test_dns_by_type_resolve.js
@@ -0,0 +1,180 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+ChromeUtils.import("resource://gre/modules/NetUtil.jsm");
+
+let prefs;
+let h2Port;
+let listen;
+
+const dns = Cc["@mozilla.org/network/dns-service;1"].getService(
+ Ci.nsIDNSService
+);
+const threadManager = Cc["@mozilla.org/thread-manager;1"].getService(
+ Ci.nsIThreadManager
+);
+const mainThread = threadManager.currentThread;
+
+const defaultOriginAttributes = {};
+
+function setup() {
+ let env = Cc["@mozilla.org/process/environment;1"].getService(
+ Ci.nsIEnvironment
+ );
+ h2Port = env.get("MOZHTTP2_PORT");
+ Assert.notEqual(h2Port, null);
+ Assert.notEqual(h2Port, "");
+
+ // Set to allow the cert presented by our H2 server
+ do_get_profile();
+ prefs = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch);
+
+ prefs.setBoolPref("network.http.spdy.enabled", true);
+ prefs.setBoolPref("network.http.spdy.enabled.http2", true);
+ // the TRR server is on 127.0.0.1
+ prefs.setCharPref("network.trr.bootstrapAddress", "127.0.0.1");
+
+ // make all native resolve calls "secretly" resolve localhost instead
+ prefs.setBoolPref("network.dns.native-is-localhost", true);
+
+ // 0 - off, 1 - race, 2 TRR first, 3 TRR only, 4 shadow
+ prefs.setIntPref("network.trr.mode", 2); // TRR first
+ prefs.setBoolPref("network.trr.wait-for-portal", false);
+ // don't confirm that TRR is working, just go!
+ prefs.setCharPref("network.trr.confirmationNS", "skip");
+
+ // So we can change the pref without clearing the cache to check a pushed
+ // record with a TRR path that fails.
+ Services.prefs.setBoolPref("network.trr.clear-cache-on-pref-change", false);
+
+ // The moz-http2 cert is for foo.example.com and is signed by http2-ca.pem
+ // so add that cert to the trust list as a signing cert. // the foo.example.com domain name.
+ const certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(
+ Ci.nsIX509CertDB
+ );
+ addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u");
+}
+
+setup();
+registerCleanupFunction(() => {
+ prefs.clearUserPref("network.http.spdy.enabled");
+ prefs.clearUserPref("network.http.spdy.enabled.http2");
+ prefs.clearUserPref("network.dns.localDomains");
+ prefs.clearUserPref("network.dns.native-is-localhost");
+ prefs.clearUserPref("network.trr.mode");
+ prefs.clearUserPref("network.trr.uri");
+ prefs.clearUserPref("network.trr.credentials");
+ prefs.clearUserPref("network.trr.wait-for-portal");
+ prefs.clearUserPref("network.trr.allow-rfc1918");
+ prefs.clearUserPref("network.trr.useGET");
+ prefs.clearUserPref("network.trr.confirmationNS");
+ prefs.clearUserPref("network.trr.bootstrapAddress");
+ prefs.clearUserPref("network.trr.blacklist-duration");
+ prefs.clearUserPref("network.trr.request-timeout");
+ prefs.clearUserPref("network.trr.clear-cache-on-pref-change");
+});
+
+let test_answer = "bXkgdm9pY2UgaXMgbXkgcGFzc3dvcmQ=";
+let test_answer_addr = "127.0.0.1";
+
+class DNSListener {
+ constructor() {
+ this.promise = new Promise(resolve => {
+ this.resolve = resolve;
+ });
+ }
+ onLookupComplete(inRequest, inRecord, inStatus) {
+ this.resolve([inRequest, inRecord, inStatus]);
+ }
+ // So we can await this as a promise.
+ then() {
+ return this.promise.then.apply(this.promise, arguments);
+ }
+}
+
+DNSListener.prototype.QueryInterface = ChromeUtils.generateQI([
+ "nsIDNSListener",
+]);
+
+add_task(async function testTXTResolve() {
+ // use the h2 server as DOH provider
+ prefs.setCharPref(
+ "network.trr.uri",
+ "https://foo.example.com:" + h2Port + "/doh"
+ );
+
+ let listenerEsni = new DNSListener();
+ let request = dns.asyncResolve(
+ "_esni.example.com",
+ dns.RESOLVE_TYPE_TXT,
+ 0,
+ null, // resolverInfo
+ listenerEsni,
+ mainThread,
+ defaultOriginAttributes
+ );
+
+ let [inRequest, inRecord, inStatus] = await listenerEsni;
+ Assert.equal(inRequest, request, "correct request was used");
+ Assert.equal(inStatus, Cr.NS_OK, "status OK");
+ let answer = inRecord
+ .QueryInterface(Ci.nsIDNSTXTRecord)
+ .getRecordsAsOneString();
+ Assert.equal(answer, test_answer, "got correct answer");
+});
+
+// verify TXT record pushed on a A record request
+add_task(async function testTXTRecordPushPart1() {
+ prefs.setCharPref(
+ "network.trr.uri",
+ "https://foo.example.com:" + h2Port + "/txt-dns-push"
+ );
+ let listenerAddr = new DNSListener();
+ let request = dns.asyncResolve(
+ "_esni_push.example.com",
+ dns.RESOLVE_TYPE_DEFAULT,
+ 0,
+ null, // resolverInfo
+ listenerAddr,
+ mainThread,
+ defaultOriginAttributes
+ );
+
+ let [inRequest, inRecord, inStatus] = await listenerAddr;
+ Assert.equal(inRequest, request, "correct request was used");
+ Assert.equal(inStatus, Cr.NS_OK, "status OK");
+ inRecord.QueryInterface(Ci.nsIDNSAddrRecord);
+ let answer = inRecord.getNextAddrAsString();
+ Assert.equal(answer, test_answer_addr, "got correct answer");
+});
+
+// verify the TXT pushed record
+add_task(async function testTXTRecordPushPart2() {
+ // At this point the second host name should've been pushed and we can resolve it using
+ // cache only. Set back the URI to a path that fails.
+ prefs.setCharPref(
+ "network.trr.uri",
+ "https://foo.example.com:" + h2Port + "/404"
+ );
+ let listenerEsni = new DNSListener();
+ let request = dns.asyncResolve(
+ "_esni_push.example.com",
+ dns.RESOLVE_TYPE_TXT,
+ 0,
+ null, // resolverInfo
+ listenerEsni,
+ mainThread,
+ defaultOriginAttributes
+ );
+
+ let [inRequest, inRecord, inStatus] = await listenerEsni;
+ Assert.equal(inRequest, request, "correct request was used");
+ Assert.equal(inStatus, Cr.NS_OK, "status OK");
+ let answer = inRecord
+ .QueryInterface(Ci.nsIDNSTXTRecord)
+ .getRecordsAsOneString();
+ Assert.equal(answer, test_answer, "got correct answer");
+});
diff --git a/netwerk/test/unit/test_dns_cancel.js b/netwerk/test/unit/test_dns_cancel.js
new file mode 100644
index 0000000000..60cff1cc52
--- /dev/null
+++ b/netwerk/test/unit/test_dns_cancel.js
@@ -0,0 +1,125 @@
+"use strict";
+
+var dns = Cc["@mozilla.org/network/dns-service;1"].getService(Ci.nsIDNSService);
+
+var hostname1 = "";
+var hostname2 = "";
+var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
+
+for (var i = 0; i < 20; i++) {
+ hostname1 += possible.charAt(Math.floor(Math.random() * possible.length));
+ hostname2 += possible.charAt(Math.floor(Math.random() * possible.length));
+}
+
+var requestList1Canceled1;
+var requestList1Canceled2;
+var requestList1NotCanceled;
+
+var requestList2Canceled;
+var requestList2NotCanceled;
+
+var listener1 = {
+ onLookupComplete(inRequest, inRecord, inStatus) {
+ // One request should be resolved and two request should be canceled.
+ if (inRequest == requestList1NotCanceled) {
+ // This request should not be canceled.
+ Assert.notEqual(inStatus, Cr.NS_ERROR_ABORT);
+
+ do_test_finished();
+ }
+ },
+ QueryInterface: ChromeUtils.generateQI(["nsIDNSListener"]),
+};
+
+var listener2 = {
+ onLookupComplete(inRequest, inRecord, inStatus) {
+ // One request should be resolved and the other canceled.
+ if (inRequest == requestList2NotCanceled) {
+ // The request should not be canceled.
+ Assert.notEqual(inStatus, Cr.NS_ERROR_ABORT);
+
+ do_test_finished();
+ }
+ },
+ QueryInterface: ChromeUtils.generateQI(["nsIDNSListener"]),
+};
+
+const defaultOriginAttributes = {};
+
+function run_test() {
+ var threadManager = Cc["@mozilla.org/thread-manager;1"].getService(
+ Ci.nsIThreadManager
+ );
+ var mainThread = threadManager.currentThread;
+
+ var flags = Ci.nsIDNSService.RESOLVE_BYPASS_CACHE;
+
+ // This one will be canceled with cancelAsyncResolve.
+ requestList1Canceled1 = dns.asyncResolve(
+ hostname2,
+ Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT,
+ flags,
+ null, // resolverInfo
+ listener1,
+ mainThread,
+ defaultOriginAttributes
+ );
+ dns.cancelAsyncResolve(
+ hostname2,
+ Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT,
+ flags,
+ null, // resolverInfo
+ listener1,
+ Cr.NS_ERROR_ABORT,
+ defaultOriginAttributes
+ );
+
+ // This one will not be canceled.
+ requestList1NotCanceled = dns.asyncResolve(
+ hostname1,
+ Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT,
+ flags,
+ null, // resolverInfo
+ listener1,
+ mainThread,
+ defaultOriginAttributes
+ );
+
+ // This one will be canceled with cancel(Cr.NS_ERROR_ABORT).
+ requestList1Canceled2 = dns.asyncResolve(
+ hostname1,
+ Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT,
+ flags,
+ null, // resolverInfo
+ listener1,
+ mainThread,
+ defaultOriginAttributes
+ );
+ requestList1Canceled2.cancel(Cr.NS_ERROR_ABORT);
+
+ // This one will not be canceled.
+ requestList2NotCanceled = dns.asyncResolve(
+ hostname1,
+ Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT,
+ flags,
+ null, // resolverInfo
+ listener2,
+ mainThread,
+ defaultOriginAttributes
+ );
+
+ // This one will be canceled with cancel(Cr.NS_ERROR_ABORT).
+ requestList2Canceled = dns.asyncResolve(
+ hostname2,
+ Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT,
+ flags,
+ null, // resolverInfo
+ listener2,
+ mainThread,
+ defaultOriginAttributes
+ );
+ requestList2Canceled.cancel(Cr.NS_ERROR_ABORT);
+
+ do_test_pending();
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_dns_disable_ipv4.js b/netwerk/test/unit/test_dns_disable_ipv4.js
new file mode 100644
index 0000000000..686bbf9b4a
--- /dev/null
+++ b/netwerk/test/unit/test_dns_disable_ipv4.js
@@ -0,0 +1,55 @@
+//
+// Tests that calling asyncResolve with the RESOLVE_DISABLE_IPV4 flag doesn't
+// return any IPv4 addresses.
+//
+
+"use strict";
+
+var dns = Cc["@mozilla.org/network/dns-service;1"].getService(Ci.nsIDNSService);
+var ioService = Cc["@mozilla.org/network/io-service;1"].getService(
+ Ci.nsIIOService
+);
+
+var listener = {
+ onLookupComplete(inRequest, inRecord, inStatus) {
+ if (inStatus != Cr.NS_OK) {
+ Assert.equal(inStatus, Cr.NS_ERROR_UNKNOWN_HOST);
+ do_test_finished();
+ return;
+ }
+
+ while (true) {
+ try {
+ inRecord.QueryInterface(Ci.nsIDNSAddrRecord);
+ var answer = inRecord.getNextAddrAsString();
+ // If there is an answer it should be an IPv6 address
+ dump(answer);
+ Assert.ok(answer.includes(":"));
+ } catch (e) {
+ break;
+ }
+ }
+ do_test_finished();
+ },
+};
+
+const defaultOriginAttributes = {};
+
+function run_test() {
+ do_test_pending();
+ try {
+ dns.asyncResolve(
+ "example.org",
+ Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT,
+ Ci.nsIDNSService.RESOLVE_DISABLE_IPV4,
+ null, // resolverInfo
+ listener,
+ null,
+ defaultOriginAttributes
+ );
+ } catch (e) {
+ dump(e);
+ Assert.ok(false);
+ do_test_finished();
+ }
+}
diff --git a/netwerk/test/unit/test_dns_disable_ipv6.js b/netwerk/test/unit/test_dns_disable_ipv6.js
new file mode 100644
index 0000000000..c22cf57144
--- /dev/null
+++ b/netwerk/test/unit/test_dns_disable_ipv6.js
@@ -0,0 +1,56 @@
+//
+// Tests that calling asyncResolve with the RESOLVE_DISABLE_IPV6 flag doesn't
+// return any IPv6 addresses.
+//
+
+"use strict";
+
+var dns = Cc["@mozilla.org/network/dns-service;1"].getService(Ci.nsIDNSService);
+var ioService = Cc["@mozilla.org/network/io-service;1"].getService(
+ Ci.nsIIOService
+);
+
+var listener = {
+ onLookupComplete(inRequest, inRecord, inStatus) {
+ if (inStatus != Cr.NS_OK) {
+ Assert.equal(inStatus, Cr.NS_ERROR_UNKNOWN_HOST);
+ do_test_finished();
+ return;
+ }
+
+ while (true) {
+ try {
+ inRecord.QueryInterface(Ci.nsIDNSAddrRecord);
+ var answer = inRecord.getNextAddrAsString();
+ // If there is an answer it should be an IPv4 address
+ dump(answer);
+ Assert.ok(!answer.includes(":"));
+ Assert.ok(answer.includes("."));
+ } catch (e) {
+ break;
+ }
+ }
+ do_test_finished();
+ },
+};
+
+const defaultOriginAttributes = {};
+
+function run_test() {
+ do_test_pending();
+ try {
+ dns.asyncResolve(
+ "example.com",
+ Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT,
+ Ci.nsIDNSService.RESOLVE_DISABLE_IPV6,
+ null, // resolverInfo
+ listener,
+ null,
+ defaultOriginAttributes
+ );
+ } catch (e) {
+ dump(e);
+ Assert.ok(false);
+ do_test_finished();
+ }
+}
diff --git a/netwerk/test/unit/test_dns_disabled.js b/netwerk/test/unit/test_dns_disabled.js
new file mode 100644
index 0000000000..7b840e2a3b
--- /dev/null
+++ b/netwerk/test/unit/test_dns_disabled.js
@@ -0,0 +1,94 @@
+"use strict";
+
+const dns = Cc["@mozilla.org/network/dns-service;1"].getService(
+ Ci.nsIDNSService
+);
+const threadManager = Cc["@mozilla.org/thread-manager;1"].getService(
+ Ci.nsIThreadManager
+);
+const override = Cc["@mozilla.org/network/native-dns-override;1"].getService(
+ Ci.nsINativeDNSResolverOverride
+);
+const mainThread = threadManager.currentThread;
+
+function makeListenerBlock(next) {
+ return {
+ onLookupComplete(inRequest, inRecord, inStatus) {
+ Assert.ok(!Components.isSuccessCode(inStatus));
+ next();
+ },
+ };
+}
+
+function makeListenerDontBlock(next, expectedAnswer) {
+ return {
+ onLookupComplete(inRequest, inRecord, inStatus) {
+ Assert.equal(inStatus, Cr.NS_OK);
+ inRecord.QueryInterface(Ci.nsIDNSAddrRecord);
+ var answer = inRecord.getNextAddrAsString();
+ if (expectedAnswer) {
+ Assert.equal(answer, expectedAnswer);
+ } else {
+ Assert.ok(answer == "127.0.0.1" || answer == "::1");
+ }
+ next();
+ },
+ };
+}
+
+function do_test({ dnsDisabled, mustBlock, testDomain, expectedAnswer }) {
+ return new Promise(resolve => {
+ Services.prefs.setBoolPref("network.dns.disabled", dnsDisabled);
+ try {
+ dns.asyncResolve(
+ testDomain,
+ Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT,
+ 0,
+ null, // resolverInfo
+ mustBlock
+ ? makeListenerBlock(resolve)
+ : makeListenerDontBlock(resolve, expectedAnswer),
+ mainThread,
+ {} // Default origin attributes
+ );
+ } catch (e) {
+ Assert.ok(mustBlock === true);
+ resolve();
+ }
+ });
+}
+
+function setup() {
+ override.addIPOverride("foo.bar", "127.0.0.1");
+ registerCleanupFunction(function() {
+ override.clearOverrides();
+ Services.prefs.clearUserPref("network.dns.disabled");
+ });
+}
+setup();
+
+// IP literals should be resolved even if dns is disabled
+add_task(async function testIPLiteral() {
+ return do_test({
+ dnsDisabled: true,
+ mustBlock: false,
+ testDomain: "0x01010101",
+ expectedAnswer: "1.1.1.1",
+ });
+});
+
+add_task(async function testBlocked() {
+ return do_test({
+ dnsDisabled: true,
+ mustBlock: true,
+ testDomain: "foo.bar",
+ });
+});
+
+add_task(async function testNotBlocked() {
+ return do_test({
+ dnsDisabled: false,
+ mustBlock: false,
+ testDomain: "foo.bar",
+ });
+});
diff --git a/netwerk/test/unit/test_dns_localredirect.js b/netwerk/test/unit/test_dns_localredirect.js
new file mode 100644
index 0000000000..0af59de813
--- /dev/null
+++ b/netwerk/test/unit/test_dns_localredirect.js
@@ -0,0 +1,68 @@
+"use strict";
+
+var dns = Cc["@mozilla.org/network/dns-service;1"].getService(Ci.nsIDNSService);
+var prefs = Cc["@mozilla.org/preferences-service;1"].getService(
+ Ci.nsIPrefBranch
+);
+
+var nextTest;
+
+var listener = {
+ onLookupComplete(inRequest, inRecord, inStatus) {
+ inRecord.QueryInterface(Ci.nsIDNSAddrRecord);
+ var answer = inRecord.getNextAddrAsString();
+ Assert.ok(answer == "127.0.0.1" || answer == "::1");
+
+ nextTest();
+ do_test_finished();
+ },
+ QueryInterface: ChromeUtils.generateQI(["nsIDNSListener"]),
+};
+
+const defaultOriginAttributes = {};
+
+function run_test() {
+ prefs.setCharPref("network.dns.localDomains", "local.vingtetun.org");
+
+ var threadManager = Cc["@mozilla.org/thread-manager;1"].getService(
+ Ci.nsIThreadManager
+ );
+ var mainThread = threadManager.currentThread;
+ nextTest = do_test_2;
+ dns.asyncResolve(
+ "local.vingtetun.org",
+ Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT,
+ 0,
+ null, // resolverInfo
+ listener,
+ mainThread,
+ defaultOriginAttributes
+ );
+
+ do_test_pending();
+}
+
+function do_test_2() {
+ var threadManager = Cc["@mozilla.org/thread-manager;1"].getService(
+ Ci.nsIThreadManager
+ );
+ var mainThread = threadManager.currentThread;
+ nextTest = testsDone;
+ prefs.setCharPref("network.dns.forceResolve", "localhost");
+ dns.asyncResolve(
+ "www.example.com",
+ Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT,
+ 0,
+ null, // resolverInfo
+ listener,
+ mainThread,
+ defaultOriginAttributes
+ );
+
+ do_test_pending();
+}
+
+function testsDone() {
+ prefs.clearUserPref("network.dns.localDomains");
+ prefs.clearUserPref("network.dns.forceResolve");
+}
diff --git a/netwerk/test/unit/test_dns_offline.js b/netwerk/test/unit/test_dns_offline.js
new file mode 100644
index 0000000000..3b95fe542f
--- /dev/null
+++ b/netwerk/test/unit/test_dns_offline.js
@@ -0,0 +1,113 @@
+"use strict";
+
+var dns = Cc["@mozilla.org/network/dns-service;1"].getService(Ci.nsIDNSService);
+var ioService = Cc["@mozilla.org/network/io-service;1"].getService(
+ Ci.nsIIOService
+);
+var prefs = Cc["@mozilla.org/preferences-service;1"].getService(
+ Ci.nsIPrefBranch
+);
+var threadManager = Cc["@mozilla.org/thread-manager;1"].getService(
+ Ci.nsIThreadManager
+);
+var mainThread = threadManager.currentThread;
+
+var listener1 = {
+ onLookupComplete(inRequest, inRecord, inStatus) {
+ Assert.equal(inStatus, Cr.NS_ERROR_OFFLINE);
+ test2();
+ do_test_finished();
+ },
+};
+
+var listener2 = {
+ onLookupComplete(inRequest, inRecord, inStatus) {
+ Assert.equal(inStatus, Cr.NS_OK);
+ inRecord.QueryInterface(Ci.nsIDNSAddrRecord);
+ var answer = inRecord.getNextAddrAsString();
+ Assert.ok(answer == "127.0.0.1" || answer == "::1");
+ test3();
+ do_test_finished();
+ },
+};
+
+var listener3 = {
+ onLookupComplete(inRequest, inRecord, inStatus) {
+ Assert.equal(inStatus, Cr.NS_OK);
+ inRecord.QueryInterface(Ci.nsIDNSAddrRecord);
+ var answer = inRecord.getNextAddrAsString();
+ Assert.ok(answer == "127.0.0.1" || answer == "::1");
+ cleanup();
+ do_test_finished();
+ },
+};
+
+const defaultOriginAttributes = {};
+
+function run_test() {
+ do_test_pending();
+ prefs.setBoolPref("network.dns.offline-localhost", false);
+ // We always resolve localhost as it's hardcoded without the following pref:
+ prefs.setBoolPref("network.proxy.allow_hijacking_localhost", true);
+ ioService.offline = true;
+ try {
+ dns.asyncResolve(
+ "localhost",
+ Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT,
+ 0,
+ null, // resolverInfo
+ listener1,
+ mainThread,
+ defaultOriginAttributes
+ );
+ } catch (e) {
+ Assert.equal(e.result, Cr.NS_ERROR_OFFLINE);
+ test2();
+ do_test_finished();
+ }
+}
+
+function test2() {
+ do_test_pending();
+ prefs.setBoolPref("network.dns.offline-localhost", true);
+ ioService.offline = false;
+ ioService.offline = true;
+ // we need to let the main thread run and apply the changes
+ do_timeout(0, test2Continued);
+}
+
+function test2Continued() {
+ dns.asyncResolve(
+ "localhost",
+ Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT,
+ 0,
+ null, // resolverInfo
+ listener2,
+ mainThread,
+ defaultOriginAttributes
+ );
+}
+
+function test3() {
+ do_test_pending();
+ ioService.offline = false;
+ // we need to let the main thread run and apply the changes
+ do_timeout(0, test3Continued);
+}
+
+function test3Continued() {
+ dns.asyncResolve(
+ "localhost",
+ Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT,
+ 0,
+ null, // resolverInfo
+ listener3,
+ mainThread,
+ defaultOriginAttributes
+ );
+}
+
+function cleanup() {
+ prefs.clearUserPref("network.dns.offline-localhost");
+ prefs.clearUserPref("network.proxy.allow_hijacking_localhost");
+}
diff --git a/netwerk/test/unit/test_dns_onion.js b/netwerk/test/unit/test_dns_onion.js
new file mode 100644
index 0000000000..b7824fed3f
--- /dev/null
+++ b/netwerk/test/unit/test_dns_onion.js
@@ -0,0 +1,82 @@
+"use strict";
+
+var dns = Cc["@mozilla.org/network/dns-service;1"].getService(Ci.nsIDNSService);
+var threadManager = Cc["@mozilla.org/thread-manager;1"].getService(
+ Ci.nsIThreadManager
+);
+var mainThread = threadManager.currentThread;
+
+var onionPref;
+var localdomainPref;
+var prefs = Cc["@mozilla.org/preferences-service;1"].getService(
+ Ci.nsIPrefBranch
+);
+
+// check that we don't lookup .onion
+var listenerBlock = {
+ onLookupComplete(inRequest, inRecord, inStatus) {
+ Assert.ok(!Components.isSuccessCode(inStatus));
+ do_test_dontBlock();
+ },
+ QueryInterface: ChromeUtils.generateQI(["nsIDNSListener"]),
+};
+
+// check that we do lookup .onion (via pref)
+var listenerDontBlock = {
+ onLookupComplete(inRequest, inRecord, inStatus) {
+ inRecord.QueryInterface(Ci.nsIDNSAddrRecord);
+ var answer = inRecord.getNextAddrAsString();
+ Assert.ok(answer == "127.0.0.1" || answer == "::1");
+ all_done();
+ },
+ QueryInterface: ChromeUtils.generateQI(["nsIDNSListener"]),
+};
+
+const defaultOriginAttributes = {};
+
+function do_test_dontBlock() {
+ prefs.setBoolPref("network.dns.blockDotOnion", false);
+ dns.asyncResolve(
+ "private.onion",
+ Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT,
+ 0,
+ null, // resolverInfo
+ listenerDontBlock,
+ mainThread,
+ defaultOriginAttributes
+ );
+}
+
+function do_test_block() {
+ prefs.setBoolPref("network.dns.blockDotOnion", true);
+ try {
+ dns.asyncResolve(
+ "private.onion",
+ Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT,
+ 0,
+ null, // resolverInfo
+ listenerBlock,
+ mainThread,
+ defaultOriginAttributes
+ );
+ } catch (e) {
+ // it is ok for this negative test to fail fast
+ Assert.ok(true);
+ do_test_dontBlock();
+ }
+}
+
+function all_done() {
+ // reset locally modified prefs
+ prefs.setCharPref("network.dns.localDomains", localdomainPref);
+ prefs.setBoolPref("network.dns.blockDotOnion", onionPref);
+ do_test_finished();
+}
+
+function run_test() {
+ onionPref = prefs.getBoolPref("network.dns.blockDotOnion");
+ localdomainPref = prefs.getCharPref("network.dns.localDomains");
+ prefs.setCharPref("network.dns.localDomains", "private.onion");
+ do_test_block();
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_dns_originAttributes.js b/netwerk/test/unit/test_dns_originAttributes.js
new file mode 100644
index 0000000000..31bc1478a1
--- /dev/null
+++ b/netwerk/test/unit/test_dns_originAttributes.js
@@ -0,0 +1,99 @@
+"use strict";
+
+var dns = Cc["@mozilla.org/network/dns-service;1"].getService(Ci.nsIDNSService);
+var threadManager = Cc["@mozilla.org/thread-manager;1"].getService(
+ Ci.nsIThreadManager
+);
+var prefs = Cc["@mozilla.org/preferences-service;1"].getService(
+ Ci.nsIPrefBranch
+);
+var mainThread = threadManager.currentThread;
+
+var listener1 = {
+ onLookupComplete(inRequest, inRecord, inStatus) {
+ Assert.equal(inStatus, Cr.NS_OK);
+ inRecord.QueryInterface(Ci.nsIDNSAddrRecord);
+ var answer = inRecord.getNextAddrAsString();
+ Assert.ok(answer == "127.0.0.1" || answer == "::1");
+ test2();
+ do_test_finished();
+ },
+};
+
+var listener2 = {
+ onLookupComplete(inRequest, inRecord, inStatus) {
+ Assert.equal(inStatus, Cr.NS_OK);
+ inRecord.QueryInterface(Ci.nsIDNSAddrRecord);
+ var answer = inRecord.getNextAddrAsString();
+ Assert.ok(answer == "127.0.0.1" || answer == "::1");
+ test3();
+ do_test_finished();
+ },
+};
+
+var listener3 = {
+ onLookupComplete(inRequest, inRecord, inStatus) {
+ Assert.equal(inStatus, Cr.NS_ERROR_OFFLINE);
+ cleanup();
+ do_test_finished();
+ },
+};
+
+const firstOriginAttributes = { userContextId: 1 };
+const secondOriginAttributes = { userContextId: 2 };
+
+// First, we resolve the address normally for first originAttributes.
+function run_test() {
+ do_test_pending();
+ prefs.setBoolPref("network.proxy.allow_hijacking_localhost", true);
+ dns.asyncResolve(
+ "localhost",
+ Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT,
+ 0,
+ null, // resolverInfo
+ listener1,
+ mainThread,
+ firstOriginAttributes
+ );
+}
+
+// Second, we resolve the same address offline to see whether its DNS cache works
+// correctly.
+function test2() {
+ do_test_pending();
+ dns.asyncResolve(
+ "localhost",
+ Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT,
+ Ci.nsIDNSService.RESOLVE_OFFLINE,
+ null, // resolverInfo
+ listener2,
+ mainThread,
+ firstOriginAttributes
+ );
+}
+
+// Third, we resolve the same address offline again with different originAttributes.
+// This resolving should fail since the DNS cache of the given address is not exist
+// for this originAttributes.
+function test3() {
+ do_test_pending();
+ try {
+ dns.asyncResolve(
+ "localhost",
+ Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT,
+ Ci.nsIDNSService.RESOLVE_OFFLINE,
+ null, // resolverInfo
+ listener3,
+ mainThread,
+ secondOriginAttributes
+ );
+ } catch (e) {
+ Assert.equal(e.result, Cr.NS_ERROR_OFFLINE);
+ cleanup();
+ do_test_finished();
+ }
+}
+
+function cleanup() {
+ prefs.clearUserPref("network.proxy.allow_hijacking_localhost");
+}
diff --git a/netwerk/test/unit/test_dns_override.js b/netwerk/test/unit/test_dns_override.js
new file mode 100644
index 0000000000..70fa688d8e
--- /dev/null
+++ b/netwerk/test/unit/test_dns_override.js
@@ -0,0 +1,311 @@
+"use strict";
+
+const dns = Cc["@mozilla.org/network/dns-service;1"].getService(
+ Ci.nsIDNSService
+);
+const override = Cc["@mozilla.org/network/native-dns-override;1"].getService(
+ Ci.nsINativeDNSResolverOverride
+);
+const defaultOriginAttributes = {};
+const threadManager = Cc["@mozilla.org/thread-manager;1"].getService(
+ Ci.nsIThreadManager
+);
+const mainThread = threadManager.currentThread;
+
+class Listener {
+ constructor() {
+ this.promise = new Promise(resolve => {
+ this.resolve = resolve;
+ });
+ }
+
+ onLookupComplete(inRequest, inRecord, inStatus) {
+ this.resolve([inRequest, inRecord, inStatus]);
+ }
+
+ async firstAddress() {
+ let all = await this.addresses();
+ if (all.length > 0) {
+ return all[0];
+ }
+
+ return undefined;
+ }
+
+ async addresses() {
+ let [, inRecord] = await this.promise;
+ let addresses = [];
+ if (!inRecord) {
+ return addresses; // returns []
+ }
+ inRecord.QueryInterface(Ci.nsIDNSAddrRecord);
+ while (inRecord.hasMore()) {
+ addresses.push(inRecord.getNextAddrAsString());
+ }
+ return addresses;
+ }
+
+ then() {
+ return this.promise.then.apply(this.promise, arguments);
+ }
+}
+Listener.prototype.QueryInterface = ChromeUtils.generateQI(["nsIDNSListener"]);
+
+const DOMAIN = "example.org";
+const OTHER = "example.com";
+
+add_task(async function test_bad_IPs() {
+ Assert.throws(
+ () => override.addIPOverride(DOMAIN, DOMAIN),
+ /NS_ERROR_UNEXPECTED/,
+ "Should throw if input is not an IP address"
+ );
+ Assert.throws(
+ () => override.addIPOverride(DOMAIN, ""),
+ /NS_ERROR_UNEXPECTED/,
+ "Should throw if input is not an IP address"
+ );
+ Assert.throws(
+ () => override.addIPOverride(DOMAIN, " "),
+ /NS_ERROR_UNEXPECTED/,
+ "Should throw if input is not an IP address"
+ );
+ Assert.throws(
+ () => override.addIPOverride(DOMAIN, "1-2-3-4"),
+ /NS_ERROR_UNEXPECTED/,
+ "Should throw if input is not an IP address"
+ );
+});
+
+add_task(async function test_ipv4() {
+ let listener = new Listener();
+ override.addIPOverride(DOMAIN, "1.2.3.4");
+ dns.asyncResolve(
+ DOMAIN,
+ Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT,
+ 0,
+ null,
+ listener,
+ mainThread,
+ defaultOriginAttributes
+ );
+ Assert.equal(await listener.firstAddress(), "1.2.3.4");
+
+ dns.clearCache(false);
+ override.clearOverrides();
+});
+
+add_task(async function test_ipv6() {
+ let listener = new Listener();
+ override.addIPOverride(DOMAIN, "fe80::6a99:9b2b:6ccc:6e1b");
+ dns.asyncResolve(
+ DOMAIN,
+ Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT,
+ 0,
+ null,
+ listener,
+ mainThread,
+ defaultOriginAttributes
+ );
+ Assert.equal(await listener.firstAddress(), "fe80::6a99:9b2b:6ccc:6e1b");
+
+ dns.clearCache(false);
+ override.clearOverrides();
+});
+
+add_task(async function test_clearOverrides() {
+ let listener = new Listener();
+ override.addIPOverride(DOMAIN, "1.2.3.4");
+ dns.asyncResolve(
+ DOMAIN,
+ Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT,
+ 0,
+ null,
+ listener,
+ mainThread,
+ defaultOriginAttributes
+ );
+ Assert.equal(await listener.firstAddress(), "1.2.3.4");
+
+ dns.clearCache(false);
+ override.clearOverrides();
+
+ listener = new Listener();
+ dns.asyncResolve(
+ DOMAIN,
+ Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT,
+ 0,
+ null,
+ listener,
+ mainThread,
+ defaultOriginAttributes
+ );
+ Assert.notEqual(await listener.firstAddress(), "1.2.3.4");
+
+ await new Promise(resolve => do_timeout(1000, resolve));
+ dns.clearCache(false);
+ override.clearOverrides();
+});
+
+add_task(async function test_clearHostOverride() {
+ override.addIPOverride(DOMAIN, "2.2.2.2");
+ override.addIPOverride(OTHER, "2.2.2.2");
+ override.clearHostOverride(DOMAIN);
+ let listener = new Listener();
+ dns.asyncResolve(
+ DOMAIN,
+ Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT,
+ 0,
+ null,
+ listener,
+ mainThread,
+ defaultOriginAttributes
+ );
+
+ Assert.notEqual(await listener.firstAddress(), "2.2.2.2");
+
+ listener = new Listener();
+ dns.asyncResolve(
+ OTHER,
+ Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT,
+ 0,
+ null,
+ listener,
+ mainThread,
+ defaultOriginAttributes
+ );
+ Assert.equal(await listener.firstAddress(), "2.2.2.2");
+
+ // Note: this test will use the actual system resolver. On windows we do a
+ // second async call to the system libraries to get the TTL values, which
+ // keeps the record alive after the onLookupComplete()
+ // We need to wait for a bit, until the second call is finished before we
+ // can clear the cache to make sure we evict everything.
+ // If the next task ever starts failing, with an IP that is not in this
+ // file, then likely the timeout is too small.
+ await new Promise(resolve => do_timeout(1000, resolve));
+ dns.clearCache(false);
+ override.clearOverrides();
+});
+
+add_task(async function test_multiple_IPs() {
+ override.addIPOverride(DOMAIN, "2.2.2.2");
+ override.addIPOverride(DOMAIN, "1.1.1.1");
+ override.addIPOverride(DOMAIN, "::1");
+ override.addIPOverride(DOMAIN, "fe80::6a99:9b2b:6ccc:6e1b");
+ let listener = new Listener();
+ dns.asyncResolve(
+ DOMAIN,
+ Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT,
+ 0,
+ null,
+ listener,
+ mainThread,
+ defaultOriginAttributes
+ );
+ Assert.deepEqual(await listener.addresses(), [
+ "2.2.2.2",
+ "1.1.1.1",
+ "::1",
+ "fe80::6a99:9b2b:6ccc:6e1b",
+ ]);
+
+ dns.clearCache(false);
+ override.clearOverrides();
+});
+
+add_task(async function test_address_family_flags() {
+ override.addIPOverride(DOMAIN, "2.2.2.2");
+ override.addIPOverride(DOMAIN, "1.1.1.1");
+ override.addIPOverride(DOMAIN, "::1");
+ override.addIPOverride(DOMAIN, "fe80::6a99:9b2b:6ccc:6e1b");
+ let listener = new Listener();
+ dns.asyncResolve(
+ DOMAIN,
+ Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT,
+ Ci.nsIDNSService.RESOLVE_DISABLE_IPV4,
+ null,
+ listener,
+ mainThread,
+ defaultOriginAttributes
+ );
+ Assert.deepEqual(await listener.addresses(), [
+ "::1",
+ "fe80::6a99:9b2b:6ccc:6e1b",
+ ]);
+
+ listener = new Listener();
+ dns.asyncResolve(
+ DOMAIN,
+ Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT,
+ Ci.nsIDNSService.RESOLVE_DISABLE_IPV6,
+ null,
+ listener,
+ mainThread,
+ defaultOriginAttributes
+ );
+ Assert.deepEqual(await listener.addresses(), ["2.2.2.2", "1.1.1.1"]);
+
+ dns.clearCache(false);
+ override.clearOverrides();
+});
+
+add_task(async function test_cname_flag() {
+ override.addIPOverride(DOMAIN, "2.2.2.2");
+ let listener = new Listener();
+ dns.asyncResolve(
+ DOMAIN,
+ Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT,
+ 0,
+ null,
+ listener,
+ mainThread,
+ defaultOriginAttributes
+ );
+ let [, inRecord] = await listener;
+ inRecord.QueryInterface(Ci.nsIDNSAddrRecord);
+ Assert.throws(
+ () => inRecord.canonicalName,
+ /NS_ERROR_NOT_AVAILABLE/,
+ "No canonical name flag"
+ );
+ Assert.equal(inRecord.getNextAddrAsString(), "2.2.2.2");
+
+ listener = new Listener();
+ dns.asyncResolve(
+ DOMAIN,
+ Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT,
+ Ci.nsIDNSService.RESOLVE_CANONICAL_NAME,
+ null,
+ listener,
+ mainThread,
+ defaultOriginAttributes
+ );
+ [, inRecord] = await listener;
+ inRecord.QueryInterface(Ci.nsIDNSAddrRecord);
+ Assert.equal(inRecord.canonicalName, DOMAIN, "No canonical name specified");
+ Assert.equal(inRecord.getNextAddrAsString(), "2.2.2.2");
+
+ dns.clearCache(false);
+ override.clearOverrides();
+
+ override.addIPOverride(DOMAIN, "2.2.2.2");
+ override.setCnameOverride(DOMAIN, OTHER);
+ listener = new Listener();
+ dns.asyncResolve(
+ DOMAIN,
+ Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT,
+ Ci.nsIDNSService.RESOLVE_CANONICAL_NAME,
+ null,
+ listener,
+ mainThread,
+ defaultOriginAttributes
+ );
+ [, inRecord] = await listener;
+ inRecord.QueryInterface(Ci.nsIDNSAddrRecord);
+ Assert.equal(inRecord.canonicalName, OTHER, "Must have correct CNAME");
+ Assert.equal(inRecord.getNextAddrAsString(), "2.2.2.2");
+
+ dns.clearCache(false);
+ override.clearOverrides();
+});
diff --git a/netwerk/test/unit/test_dns_override_for_localhost.js b/netwerk/test/unit/test_dns_override_for_localhost.js
new file mode 100644
index 0000000000..462cb90aac
--- /dev/null
+++ b/netwerk/test/unit/test_dns_override_for_localhost.js
@@ -0,0 +1,92 @@
+"use strict";
+
+const dns = Cc["@mozilla.org/network/dns-service;1"].getService(
+ Ci.nsIDNSService
+);
+const override = Cc["@mozilla.org/network/native-dns-override;1"].getService(
+ Ci.nsINativeDNSResolverOverride
+);
+const defaultOriginAttributes = {};
+const threadManager = Cc["@mozilla.org/thread-manager;1"].getService(
+ Ci.nsIThreadManager
+);
+const mainThread = threadManager.currentThread;
+
+class Listener {
+ constructor() {
+ this.promise = new Promise(resolve => {
+ this.resolve = resolve;
+ });
+ }
+
+ onLookupComplete(inRequest, inRecord, inStatus) {
+ this.resolve([inRequest, inRecord, inStatus]);
+ }
+
+ async addresses() {
+ let [, inRecord] = await this.promise;
+ let addresses = [];
+ if (!inRecord) {
+ return addresses; // returns []
+ }
+ inRecord.QueryInterface(Ci.nsIDNSAddrRecord);
+ while (inRecord.hasMore()) {
+ addresses.push(inRecord.getNextAddrAsString());
+ }
+ return addresses;
+ }
+
+ then() {
+ return this.promise.then.apply(this.promise, arguments);
+ }
+}
+Listener.prototype.QueryInterface = ChromeUtils.generateQI(["nsIDNSListener"]);
+
+["localhost", "vhost.localhost"].forEach(domain => {
+ add_task(async function test_() {
+ let listener1 = new Listener();
+ const overrides = ["1.2.3.4", "5.6.7.8"];
+ overrides.forEach(ip_address => {
+ override.addIPOverride(domain, ip_address);
+ });
+
+ // Verify that loopback host names are not overridden.
+ dns.asyncResolve(
+ domain,
+ Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT,
+ 0,
+ null,
+ listener1,
+ mainThread,
+ defaultOriginAttributes
+ );
+ Assert.deepEqual(
+ await listener1.addresses(),
+ ["127.0.0.1", "::1"],
+ `${domain} is not overridden`
+ );
+
+ // Verify that if localhost hijacking is enabled, the overrides
+ // registered above are taken into account.
+ Services.prefs.setBoolPref("network.proxy.allow_hijacking_localhost", true);
+ let listener2 = new Listener();
+ dns.asyncResolve(
+ domain,
+ Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT,
+ 0,
+ null,
+ listener2,
+ mainThread,
+ defaultOriginAttributes
+ );
+ Assert.deepEqual(
+ await listener2.addresses(),
+ overrides,
+ `${domain} is overridden`
+ );
+ Services.prefs.clearUserPref("network.proxy.allow_hijacking_localhost");
+
+ dns.clearCache(false);
+ override.clearOverrides();
+ });
+});
diff --git a/netwerk/test/unit/test_dns_proxy_bypass.js b/netwerk/test/unit/test_dns_proxy_bypass.js
new file mode 100644
index 0000000000..8fbe91c346
--- /dev/null
+++ b/netwerk/test/unit/test_dns_proxy_bypass.js
@@ -0,0 +1,82 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+var ioService = Cc["@mozilla.org/network/io-service;1"].getService(
+ Ci.nsIIOService
+);
+
+var prefs = Cc["@mozilla.org/preferences-service;1"].getService(
+ Ci.nsIPrefBranch
+);
+
+var url = "ws://dnsleak.example.com";
+
+var dnsRequestObserver = {
+ register() {
+ this.obs = Cc["@mozilla.org/observer-service;1"].getService(
+ Ci.nsIObserverService
+ );
+ this.obs.addObserver(this, "dns-resolution-request");
+ },
+
+ unregister() {
+ if (this.obs) {
+ this.obs.removeObserver(this, "dns-resolution-request");
+ }
+ },
+
+ observe(subject, topic, data) {
+ if (topic == "dns-resolution-request") {
+ info(data);
+ if (data.indexOf("dnsleak.example.com") > -1) {
+ try {
+ Assert.ok(false);
+ } catch (e) {}
+ }
+ }
+ },
+};
+
+var listener = {
+ onAcknowledge(aContext, aSize) {},
+ onBinaryMessageAvailable(aContext, aMsg) {},
+ onMessageAvailable(aContext, aMsg) {},
+ onServerClose(aContext, aCode, aReason) {},
+ onStart(aContext) {},
+ onStop(aContext, aStatusCode) {
+ prefs.clearUserPref("network.proxy.socks");
+ prefs.clearUserPref("network.proxy.socks_port");
+ prefs.clearUserPref("network.proxy.type");
+ prefs.clearUserPref("network.proxy.socks_remote_dns");
+ prefs.clearUserPref("network.dns.notifyResolution");
+ dnsRequestObserver.unregister();
+ do_test_finished();
+ },
+};
+
+function run_test() {
+ dnsRequestObserver.register();
+ prefs.setBoolPref("network.dns.notifyResolution", true);
+ prefs.setCharPref("network.proxy.socks", "127.0.0.1");
+ prefs.setIntPref("network.proxy.socks_port", 9000);
+ prefs.setIntPref("network.proxy.type", 1);
+ prefs.setBoolPref("network.proxy.socks_remote_dns", true);
+ var chan = Cc["@mozilla.org/network/protocol;1?name=ws"].createInstance(
+ Ci.nsIWebSocketChannel
+ );
+
+ chan.initLoadInfo(
+ null, // aLoadingNode
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ null, // aTriggeringPrincipal
+ Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ Ci.nsIContentPolicy.TYPE_WEBSOCKET
+ );
+
+ var uri = ioService.newURI(url);
+ chan.asyncOpen(uri, url, 0, listener, null);
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_dns_service.js b/netwerk/test/unit/test_dns_service.js
new file mode 100644
index 0000000000..5ee3ef2f0f
--- /dev/null
+++ b/netwerk/test/unit/test_dns_service.js
@@ -0,0 +1,68 @@
+"use strict";
+
+const dns = Cc["@mozilla.org/network/dns-service;1"].getService(
+ Ci.nsIDNSService
+);
+const defaultOriginAttributes = {};
+const threadManager = Cc["@mozilla.org/thread-manager;1"].getService(
+ Ci.nsIThreadManager
+);
+const mainThread = threadManager.currentThread;
+
+class Listener {
+ constructor() {
+ this.promise = new Promise(resolve => {
+ this.resolve = resolve;
+ });
+ }
+
+ onLookupComplete(inRequest, inRecord, inStatus) {
+ this.resolve([inRequest, inRecord, inStatus]);
+ }
+
+ then() {
+ return this.promise.then.apply(this.promise, arguments);
+ }
+}
+
+Listener.prototype.QueryInterface = ChromeUtils.generateQI(["nsIDNSListener"]);
+
+const DOMAIN_IDN = "bücher.org";
+const ACE_IDN = "xn--bcher-kva.org";
+
+const DOMAIN = "localhost";
+const ADDR1 = "127.0.0.1";
+const ADDR2 = "::1";
+
+add_task(async function test_dns_localhost() {
+ let listener = new Listener();
+ dns.asyncResolve(
+ "localhost",
+ Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT,
+ 0,
+ null, // resolverInfo
+ listener,
+ mainThread,
+ defaultOriginAttributes
+ );
+ let [, inRecord] = await listener;
+ inRecord.QueryInterface(Ci.nsIDNSAddrRecord);
+ let answer = inRecord.getNextAddrAsString();
+ Assert.ok(answer == ADDR1 || answer == ADDR2);
+});
+
+add_task(async function test_idn_cname() {
+ let listener = new Listener();
+ dns.asyncResolve(
+ DOMAIN_IDN,
+ Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT,
+ Ci.nsIDNSService.RESOLVE_CANONICAL_NAME,
+ null, // resolverInfo
+ listener,
+ mainThread,
+ defaultOriginAttributes
+ );
+ let [, inRecord] = await listener;
+ inRecord.QueryInterface(Ci.nsIDNSAddrRecord);
+ Assert.equal(inRecord.canonicalName, ACE_IDN, "IDN is returned as punycode");
+});
diff --git a/netwerk/test/unit/test_domain_eviction.js b/netwerk/test/unit/test_domain_eviction.js
new file mode 100644
index 0000000000..232fc89d99
--- /dev/null
+++ b/netwerk/test/unit/test_domain_eviction.js
@@ -0,0 +1,185 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that domain eviction occurs when the cookies per base domain limit is
+// reached, and that expired cookies are evicted before live cookies.
+
+"use strict";
+
+var test_generator = do_run_test();
+
+function run_test() {
+ do_test_pending();
+ do_run_generator(test_generator);
+}
+
+function continue_test() {
+ do_run_generator(test_generator);
+}
+
+function* do_run_test() {
+ // Set quotaPerHost to maxPerHost - 1, so there is only one cookie
+ // will be evicted everytime.
+ Services.prefs.setIntPref("network.cookie.quotaPerHost", 49);
+ // Set the base domain limit to 50 so we have a known value.
+ Services.prefs.setIntPref("network.cookie.maxPerHost", 50);
+
+ let futureExpiry = Math.floor(Date.now() / 1000 + 1000);
+
+ // test eviction under the 50 cookies per base domain limit. this means
+ // that cookies for foo.com and bar.foo.com should count toward this limit,
+ // while cookies for baz.com should not. there are several tests we perform
+ // to make sure the base domain logic is working correctly.
+
+ // 1) simplest case: set 100 cookies for "foo.bar" and make sure 50 survive.
+ setCookies("foo.bar", 100, futureExpiry);
+ Assert.equal(countCookies("foo.bar", "foo.bar"), 50);
+
+ // 2) set cookies for different subdomains of "foo.baz", and an unrelated
+ // domain, and make sure all 50 within the "foo.baz" base domain are counted.
+ setCookies("foo.baz", 10, futureExpiry);
+ setCookies(".foo.baz", 10, futureExpiry);
+ setCookies("bar.foo.baz", 10, futureExpiry);
+ setCookies("baz.bar.foo.baz", 10, futureExpiry);
+ setCookies("unrelated.domain", 50, futureExpiry);
+ Assert.equal(countCookies("foo.baz", "baz.bar.foo.baz"), 40);
+ setCookies("foo.baz", 20, futureExpiry);
+ Assert.equal(countCookies("foo.baz", "baz.bar.foo.baz"), 50);
+
+ // 3) ensure cookies are evicted by order of lastAccessed time, if the
+ // limit on cookies per base domain is reached.
+ setCookies("horse.radish", 10, futureExpiry);
+
+ // Wait a while, to make sure the first batch of cookies is older than
+ // the second (timer resolution varies on different platforms).
+ do_timeout(100, continue_test);
+ yield;
+
+ setCookies("tasty.horse.radish", 50, futureExpiry);
+ Assert.equal(countCookies("horse.radish", "horse.radish"), 50);
+
+ for (let cookie of Services.cookiemgr.cookies) {
+ if (cookie.host == "horse.radish") {
+ do_throw("cookies not evicted by lastAccessed order");
+ }
+ }
+
+ // Test that expired cookies for a domain are evicted before live ones.
+ let shortExpiry = Math.floor(Date.now() / 1000 + 2);
+ setCookies("captchart.com", 49, futureExpiry);
+ Services.cookiemgr.add(
+ "captchart.com",
+ "",
+ "test100",
+ "eviction",
+ false,
+ false,
+ false,
+ shortExpiry,
+ {},
+ Ci.nsICookie.SAMESITE_NONE,
+ Ci.nsICookie.SCHEME_HTTPS
+ );
+ do_timeout(2100, continue_test);
+ yield;
+
+ Assert.equal(countCookies("captchart.com", "captchart.com"), 50);
+ Services.cookiemgr.add(
+ "captchart.com",
+ "",
+ "test200",
+ "eviction",
+ false,
+ false,
+ false,
+ futureExpiry,
+ {},
+ Ci.nsICookie.SAMESITE_NONE,
+ Ci.nsICookie.SCHEME_HTTPS
+ );
+ Assert.equal(countCookies("captchart.com", "captchart.com"), 50);
+
+ for (let cookie of Services.cookiemgr.getCookiesFromHost(
+ "captchart.com",
+ {}
+ )) {
+ Assert.ok(cookie.expiry == futureExpiry);
+ }
+
+ do_finish_generator_test(test_generator);
+}
+
+// set 'aNumber' cookies with host 'aHost', with distinct names.
+function setCookies(aHost, aNumber, aExpiry) {
+ for (let i = 0; i < aNumber; ++i) {
+ Services.cookiemgr.add(
+ aHost,
+ "",
+ "test" + i,
+ "eviction",
+ false,
+ false,
+ false,
+ aExpiry,
+ {},
+ Ci.nsICookie.SAMESITE_NONE,
+ Ci.nsICookie.SCHEME_HTTPS
+ );
+ }
+}
+
+// count how many cookies are within domain 'aBaseDomain', using three
+// independent interface methods on nsICookieManager:
+// 1) 'cookies', an array of all cookies;
+// 2) 'countCookiesFromHost', which returns the number of cookies within the
+// base domain of 'aHost',
+// 3) 'getCookiesFromHost', which returns an array of 2).
+function countCookies(aBaseDomain, aHost) {
+ // count how many cookies are within domain 'aBaseDomain' using the cookies
+ // array.
+ let cookies = [];
+ for (let cookie of Services.cookiemgr.cookies) {
+ if (
+ cookie.host.length >= aBaseDomain.length &&
+ cookie.host.slice(cookie.host.length - aBaseDomain.length) == aBaseDomain
+ ) {
+ cookies.push(cookie);
+ }
+ }
+
+ // confirm the count using countCookiesFromHost and getCookiesFromHost.
+ let result = cookies.length;
+ Assert.equal(
+ Services.cookiemgr.countCookiesFromHost(aBaseDomain),
+ cookies.length
+ );
+ Assert.equal(Services.cookiemgr.countCookiesFromHost(aHost), cookies.length);
+
+ for (let cookie of Services.cookiemgr.getCookiesFromHost(aHost, {})) {
+ if (
+ cookie.host.length >= aBaseDomain.length &&
+ cookie.host.slice(cookie.host.length - aBaseDomain.length) == aBaseDomain
+ ) {
+ let found = false;
+ for (let i = 0; i < cookies.length; ++i) {
+ if (cookies[i].host == cookie.host && cookies[i].name == cookie.name) {
+ found = true;
+ cookies.splice(i, 1);
+ break;
+ }
+ }
+
+ if (!found) {
+ do_throw("cookie " + cookie.name + " not found in master cookies");
+ }
+ } else {
+ do_throw(
+ "cookie host " + cookie.host + " not within domain " + aBaseDomain
+ );
+ }
+ }
+
+ Assert.equal(cookies.length, 0);
+
+ return result;
+}
diff --git a/netwerk/test/unit/test_doomentry.js b/netwerk/test/unit/test_doomentry.js
new file mode 100644
index 0000000000..23928d223d
--- /dev/null
+++ b/netwerk/test/unit/test_doomentry.js
@@ -0,0 +1,108 @@
+/**
+ * Test for nsICacheStorage.asyncDoomURI().
+ * It tests dooming
+ * - an existent inactive entry
+ * - a non-existent inactive entry
+ * - an existent active entry
+ */
+
+"use strict";
+
+function doom(url, callback) {
+ get_cache_service()
+ .diskCacheStorage(Services.loadContextInfo.default, false)
+ .asyncDoomURI(createURI(url), "", {
+ onCacheEntryDoomed(result) {
+ callback(result);
+ },
+ });
+}
+
+function write_and_check(str, data, len) {
+ var written = str.write(data, len);
+ if (written != len) {
+ do_throw(
+ "str.write has not written all data!\n" +
+ " Expected: " +
+ len +
+ "\n" +
+ " Actual: " +
+ written +
+ "\n"
+ );
+ }
+}
+
+function write_entry() {
+ asyncOpenCacheEntry(
+ "http://testentry/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_TRUNCATE,
+ null,
+ function(status, entry) {
+ write_entry_cont(entry, entry.openOutputStream(0, -1));
+ }
+ );
+}
+
+function write_entry_cont(entry, ostream) {
+ var data = "testdata";
+ write_and_check(ostream, data, data.length);
+ ostream.close();
+ entry.close();
+ doom("http://testentry/", check_doom1);
+}
+
+function check_doom1(status) {
+ Assert.equal(status, Cr.NS_OK);
+ doom("http://nonexistententry/", check_doom2);
+}
+
+function check_doom2(status) {
+ Assert.equal(status, Cr.NS_ERROR_NOT_AVAILABLE);
+ asyncOpenCacheEntry(
+ "http://testentry/",
+ "disk",
+ Ci.nsICacheStorage.OPEN_TRUNCATE,
+ null,
+ function(status, entry) {
+ write_entry2(entry, entry.openOutputStream(0, -1));
+ }
+ );
+}
+
+var gEntry;
+var gOstream;
+function write_entry2(entry, ostream) {
+ // write some data and doom the entry while it is active
+ var data = "testdata";
+ write_and_check(ostream, data, data.length);
+ gEntry = entry;
+ gOstream = ostream;
+ doom("http://testentry/", check_doom3);
+}
+
+function check_doom3(status) {
+ Assert.equal(status, Cr.NS_OK);
+ // entry was doomed but writing should still succeed
+ var data = "testdata";
+ write_and_check(gOstream, data, data.length);
+ gOstream.close();
+ gEntry.close();
+ // dooming the same entry again should fail
+ doom("http://testentry/", check_doom4);
+}
+
+function check_doom4(status) {
+ Assert.equal(status, Cr.NS_ERROR_NOT_AVAILABLE);
+ do_test_finished();
+}
+
+function run_test() {
+ do_get_profile();
+
+ // clear the cache
+ evict_cache_entries();
+ write_entry();
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_duplicate_headers.js b/netwerk/test/unit/test_duplicate_headers.js
new file mode 100644
index 0000000000..0c747c6066
--- /dev/null
+++ b/netwerk/test/unit/test_duplicate_headers.js
@@ -0,0 +1,558 @@
+/*
+ * Tests bugs 597706, 655389: prevent duplicate headers with differing values
+ * for some headers like Content-Length, Location, etc.
+ */
+
+////////////////////////////////////////////////////////////////////////////////
+// Test infrastructure
+
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+XPCOMUtils.defineLazyGetter(this, "URL", function() {
+ return "http://localhost:" + httpserver.identity.primaryPort;
+});
+
+var httpserver = new HttpServer();
+var index = 0;
+var test_flags = [];
+var testPathBase = "/dupe_hdrs";
+
+function run_test() {
+ httpserver.start(-1);
+
+ do_test_pending();
+ run_test_number(1);
+}
+
+function run_test_number(num) {
+ let testPath = testPathBase + num;
+ httpserver.registerPathHandler(testPath, globalThis["handler" + num]);
+
+ var channel = setupChannel(testPath);
+ let flags = test_flags[num]; // OK if flags undefined for test
+ channel.asyncOpen(
+ new ChannelListener(globalThis["completeTest" + num], channel, flags)
+ );
+}
+
+function setupChannel(url) {
+ var chan = NetUtil.newChannel({
+ uri: URL + url,
+ loadUsingSystemPrincipal: true,
+ });
+ var httpChan = chan.QueryInterface(Ci.nsIHttpChannel);
+ return httpChan;
+}
+
+function endTests() {
+ httpserver.stop(do_test_finished);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Test 1: FAIL because of conflicting Content-Length headers
+test_flags[1] = CL_EXPECT_FAILURE;
+
+function handler1(metadata, response) {
+ var body = "012345678901234567890123456789";
+ // Comrades! We must seize power from the petty-bourgeois running dogs of
+ // httpd.js in order to reply with multiple instances of the same header!
+ response.seizePower();
+ response.write("HTTP/1.0 200 OK\r\n");
+ response.write("Content-Type: text/plain\r\n");
+ response.write("Content-Length: 30\r\n");
+ response.write("Content-Length: 20\r\n");
+ response.write("\r\n");
+ response.write(body);
+ response.finish();
+}
+
+function completeTest1(request, data, ctx) {
+ Assert.equal(request.status, Cr.NS_ERROR_CORRUPTED_CONTENT);
+
+ run_test_number(2);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Test 2: OK to have duplicate same Content-Length headers
+
+function handler2(metadata, response) {
+ var body = "012345678901234567890123456789";
+ response.seizePower();
+ response.write("HTTP/1.0 200 OK\r\n");
+ response.write("Content-Type: text/plain\r\n");
+ response.write("Content-Length: 30\r\n");
+ response.write("Content-Length: 30\r\n");
+ response.write("\r\n");
+ response.write(body);
+ response.finish();
+}
+
+function completeTest2(request, data, ctx) {
+ Assert.equal(request.status, 0);
+ run_test_number(3);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Test 3: FAIL: 2nd Content-length is blank
+test_flags[3] = CL_EXPECT_FAILURE;
+
+function handler3(metadata, response) {
+ var body = "012345678901234567890123456789";
+ response.seizePower();
+ response.write("HTTP/1.0 200 OK\r\n");
+ response.write("Content-Type: text/plain\r\n");
+ response.write("Content-Length: 30\r\n");
+ response.write("Content-Length:\r\n");
+ response.write("\r\n");
+ response.write(body);
+ response.finish();
+}
+
+function completeTest3(request, data, ctx) {
+ Assert.equal(request.status, Cr.NS_ERROR_CORRUPTED_CONTENT);
+
+ run_test_number(4);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Test 4: ensure that blank C-len header doesn't allow attacker to reset Clen,
+// then insert CRLF attack
+test_flags[4] = CL_EXPECT_FAILURE;
+
+function handler4(metadata, response) {
+ var body = "012345678901234567890123456789";
+
+ response.seizePower();
+ response.write("HTTP/1.0 200 OK\r\n");
+ response.write("Content-Type: text/plain\r\n");
+ response.write("Content-Length: 30\r\n");
+
+ // Bad Mr Hacker! Bad!
+ var evilBody = "We are the Evil bytes, Evil bytes, Evil bytes!";
+ response.write("Content-Length:\r\n");
+ response.write("Content-Length: %s\r\n\r\n%s" % (evilBody.length, evilBody));
+ response.write("\r\n");
+ response.write(body);
+ response.finish();
+}
+
+function completeTest4(request, data, ctx) {
+ Assert.equal(request.status, Cr.NS_ERROR_CORRUPTED_CONTENT);
+
+ run_test_number(5);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Test 5: ensure that we take 1st instance of duplicate, nonmerged headers that
+// are permitted : (ex: Referrer)
+
+function handler5(metadata, response) {
+ var body = "012345678901234567890123456789";
+ response.seizePower();
+ response.write("HTTP/1.0 200 OK\r\n");
+ response.write("Content-Type: text/plain\r\n");
+ response.write("Content-Length: 30\r\n");
+ response.write("Referer: naive.org\r\n");
+ response.write("Referer: evil.net\r\n");
+ response.write("\r\n");
+ response.write(body);
+ response.finish();
+}
+
+function completeTest5(request, data, ctx) {
+ try {
+ let referer = request.getResponseHeader("Referer");
+ Assert.equal(referer, "naive.org");
+ } catch (ex) {
+ do_throw("Referer header should be present");
+ }
+
+ run_test_number(6);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Test 5: FAIL if multiple, different Location: headers present
+// - needed to prevent CRLF injection attacks
+test_flags[6] = CL_EXPECT_FAILURE;
+
+function handler6(metadata, response) {
+ var body = "012345678901234567890123456789";
+ response.seizePower();
+ response.write("HTTP/1.0 301 Moved\r\n");
+ response.write("Content-Type: text/plain\r\n");
+ response.write("Content-Length: 30\r\n");
+ response.write("Location: " + URL + "/content\r\n");
+ response.write("Location: http://www.microsoft.com/\r\n");
+ response.write("Connection: close\r\n");
+ response.write("\r\n");
+ response.write(body);
+ response.finish();
+}
+
+function completeTest6(request, data, ctx) {
+ Assert.equal(request.status, Cr.NS_ERROR_CORRUPTED_CONTENT);
+
+ // run_test_number(7); // Test 7 leaking under e10s: unrelated bug?
+ run_test_number(8);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Test 7: OK to have multiple Location: headers with same value
+
+function handler7(metadata, response) {
+ var body = "012345678901234567890123456789";
+ response.seizePower();
+ response.write("HTTP/1.0 301 Moved\r\n");
+ response.write("Content-Type: text/plain\r\n");
+ response.write("Content-Length: 30\r\n");
+ // redirect to previous test handler that completes OK: test 5
+ response.write("Location: " + URL + testPathBase + "5\r\n");
+ response.write("Location: " + URL + testPathBase + "5\r\n");
+ response.write("Connection: close\r\n");
+ response.write("\r\n");
+ response.write(body);
+ response.finish();
+}
+
+function completeTest7(request, data, ctx) {
+ // for some reason need this here
+ request.QueryInterface(Ci.nsIHttpChannel);
+
+ try {
+ let referer = request.getResponseHeader("Referer");
+ Assert.equal(referer, "naive.org");
+ } catch (ex) {
+ do_throw("Referer header should be present");
+ }
+
+ run_test_number(8);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// FAIL if 2nd Location: headers blank
+test_flags[8] = CL_EXPECT_FAILURE;
+
+function handler8(metadata, response) {
+ var body = "012345678901234567890123456789";
+ response.seizePower();
+ response.write("HTTP/1.0 301 Moved\r\n");
+ response.write("Content-Type: text/plain\r\n");
+ response.write("Content-Length: 30\r\n");
+ // redirect to previous test handler that completes OK: test 4
+ response.write("Location: " + URL + testPathBase + "4\r\n");
+ response.write("Location:\r\n");
+ response.write("Connection: close\r\n");
+ response.write("\r\n");
+ response.write(body);
+ response.finish();
+}
+
+function completeTest8(request, data, ctx) {
+ Assert.equal(request.status, Cr.NS_ERROR_CORRUPTED_CONTENT);
+
+ run_test_number(9);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Test 9: ensure that blank Location header doesn't allow attacker to reset,
+// then insert an evil one
+test_flags[9] = CL_EXPECT_FAILURE;
+
+function handler9(metadata, response) {
+ var body = "012345678901234567890123456789";
+ response.seizePower();
+ response.write("HTTP/1.0 301 Moved\r\n");
+ response.write("Content-Type: text/plain\r\n");
+ response.write("Content-Length: 30\r\n");
+ // redirect to previous test handler that completes OK: test 2
+ response.write("Location: " + URL + testPathBase + "2\r\n");
+ response.write("Location:\r\n");
+ // redirect to previous test handler that completes OK: test 4
+ response.write("Location: " + URL + testPathBase + "4\r\n");
+ response.write("Connection: close\r\n");
+ response.write("\r\n");
+ response.write(body);
+ response.finish();
+}
+
+function completeTest9(request, data, ctx) {
+ // All redirection should fail:
+ Assert.equal(request.status, Cr.NS_ERROR_CORRUPTED_CONTENT);
+
+ run_test_number(10);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Test 10: FAIL: if conflicting values for Content-Dispo
+test_flags[10] = CL_EXPECT_FAILURE;
+
+function handler10(metadata, response) {
+ var body = "012345678901234567890123456789";
+ response.seizePower();
+ response.write("HTTP/1.0 200 OK\r\n");
+ response.write("Content-Type: text/plain\r\n");
+ response.write("Content-Length: 30\r\n");
+ response.write("Content-Disposition: attachment; filename=foo\r\n");
+ response.write("Content-Disposition: attachment; filename=bar\r\n");
+ response.write("Content-Disposition: attachment; filename=baz\r\n");
+ response.write("\r\n");
+ response.write(body);
+ response.finish();
+}
+
+function completeTest10(request, data, ctx) {
+ Assert.equal(request.status, Cr.NS_ERROR_CORRUPTED_CONTENT);
+
+ run_test_number(11);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Test 11: OK to have duplicate same Content-Disposition headers
+
+function handler11(metadata, response) {
+ var body = "012345678901234567890123456789";
+ response.seizePower();
+ response.write("HTTP/1.0 200 OK\r\n");
+ response.write("Content-Type: text/plain\r\n");
+ response.write("Content-Length: 30\r\n");
+ response.write("Content-Disposition: attachment; filename=foo\r\n");
+ response.write("Content-Disposition: attachment; filename=foo\r\n");
+ response.write("\r\n");
+ response.write(body);
+ response.finish();
+}
+
+function completeTest11(request, data, ctx) {
+ Assert.equal(request.status, 0);
+
+ try {
+ var chan = request.QueryInterface(Ci.nsIChannel);
+ Assert.equal(chan.contentDisposition, chan.DISPOSITION_ATTACHMENT);
+ Assert.equal(chan.contentDispositionFilename, "foo");
+ Assert.equal(chan.contentDispositionHeader, "attachment; filename=foo");
+ } catch (ex) {
+ do_throw("error parsing Content-Disposition: " + ex);
+ }
+
+ run_test_number(12);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Bug 716801 OK for Location: header to be blank
+
+function handler12(metadata, response) {
+ var body = "012345678901234567890123456789";
+ response.seizePower();
+ response.write("HTTP/1.0 200 OK\r\n");
+ response.write("Content-Type: text/plain\r\n");
+ response.write("Content-Length: 30\r\n");
+ response.write("Location:\r\n");
+ response.write("Connection: close\r\n");
+ response.write("\r\n");
+ response.write(body);
+ response.finish();
+}
+
+function completeTest12(request, data, ctx) {
+ Assert.equal(request.status, Cr.NS_OK);
+ Assert.equal(30, data.length);
+
+ run_test_number(13);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Negative content length is ok
+test_flags[13] = CL_ALLOW_UNKNOWN_CL;
+
+function handler13(metadata, response) {
+ var body = "012345678901234567890123456789";
+ response.seizePower();
+ response.write("HTTP/1.0 200 OK\r\n");
+ response.write("Content-Type: text/plain\r\n");
+ response.write("Content-Length: -1\r\n");
+ response.write("Connection: close\r\n");
+ response.write("\r\n");
+ response.write(body);
+ response.finish();
+}
+
+function completeTest13(request, data, ctx) {
+ Assert.equal(request.status, Cr.NS_OK);
+ Assert.equal(30, data.length);
+
+ run_test_number(14);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// leading negative content length is not ok if paired with positive one
+
+test_flags[14] = CL_EXPECT_FAILURE | CL_ALLOW_UNKNOWN_CL;
+
+function handler14(metadata, response) {
+ var body = "012345678901234567890123456789";
+ response.seizePower();
+ response.write("HTTP/1.0 200 OK\r\n");
+ response.write("Content-Type: text/plain\r\n");
+ response.write("Content-Length: -1\r\n");
+ response.write("Content-Length: 30\r\n");
+ response.write("Connection: close\r\n");
+ response.write("\r\n");
+ response.write(body);
+ response.finish();
+}
+
+function completeTest14(request, data, ctx) {
+ Assert.equal(request.status, Cr.NS_ERROR_CORRUPTED_CONTENT);
+
+ run_test_number(15);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// trailing negative content length is not ok if paired with positive one
+
+test_flags[15] = CL_EXPECT_FAILURE | CL_ALLOW_UNKNOWN_CL;
+
+function handler15(metadata, response) {
+ var body = "012345678901234567890123456789";
+ response.seizePower();
+ response.write("HTTP/1.0 200 OK\r\n");
+ response.write("Content-Type: text/plain\r\n");
+ response.write("Content-Length: 30\r\n");
+ response.write("Content-Length: -1\r\n");
+ response.write("Connection: close\r\n");
+ response.write("\r\n");
+ response.write(body);
+ response.finish();
+}
+
+function completeTest15(request, data, ctx) {
+ Assert.equal(request.status, Cr.NS_ERROR_CORRUPTED_CONTENT);
+
+ run_test_number(16);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// empty content length is ok
+test_flags[16] = CL_ALLOW_UNKNOWN_CL;
+let reran16 = false;
+
+function handler16(metadata, response) {
+ var body = "012345678901234567890123456789";
+ response.seizePower();
+ response.write("HTTP/1.0 200 OK\r\n");
+ response.write("Content-Type: text/plain\r\n");
+ response.write("Content-Length: \r\n");
+ response.write("Cache-Control: max-age=600\r\n");
+ response.write("Connection: close\r\n");
+ response.write("\r\n");
+ response.write(body);
+ response.finish();
+}
+
+function completeTest16(request, data, ctx) {
+ Assert.equal(request.status, Cr.NS_OK);
+ Assert.equal(30, data.length);
+
+ if (!reran16) {
+ reran16 = true;
+ run_test_number(16);
+ } else {
+ run_test_number(17);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// empty content length paired with non empty is not ok
+test_flags[17] = CL_EXPECT_FAILURE | CL_ALLOW_UNKNOWN_CL;
+
+function handler17(metadata, response) {
+ var body = "012345678901234567890123456789";
+ response.seizePower();
+ response.write("HTTP/1.0 200 OK\r\n");
+ response.write("Content-Type: text/plain\r\n");
+ response.write("Content-Length: \r\n");
+ response.write("Content-Length: 30\r\n");
+ response.write("Connection: close\r\n");
+ response.write("\r\n");
+ response.write(body);
+ response.finish();
+}
+
+function completeTest17(request, data, ctx) {
+ Assert.equal(request.status, Cr.NS_ERROR_CORRUPTED_CONTENT);
+
+ run_test_number(18);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// alpha content-length is just like -1
+test_flags[18] = CL_ALLOW_UNKNOWN_CL;
+
+function handler18(metadata, response) {
+ var body = "012345678901234567890123456789";
+ response.seizePower();
+ response.write("HTTP/1.0 200 OK\r\n");
+ response.write("Content-Type: text/plain\r\n");
+ response.write("Content-Length: seventeen\r\n");
+ response.write("Connection: close\r\n");
+ response.write("\r\n");
+ response.write(body);
+ response.finish();
+}
+
+function completeTest18(request, data, ctx) {
+ Assert.equal(request.status, Cr.NS_OK);
+ Assert.equal(30, data.length);
+
+ run_test_number(19);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// semi-colons are ok too in the content-length
+test_flags[19] = CL_ALLOW_UNKNOWN_CL;
+
+function handler19(metadata, response) {
+ var body = "012345678901234567890123456789";
+ response.seizePower();
+ response.write("HTTP/1.0 200 OK\r\n");
+ response.write("Content-Type: text/plain\r\n");
+ response.write("Content-Length: 30;\r\n");
+ response.write("Connection: close\r\n");
+ response.write("\r\n");
+ response.write(body);
+ response.finish();
+}
+
+function completeTest19(request, data, ctx) {
+ Assert.equal(request.status, Cr.NS_OK);
+ Assert.equal(30, data.length);
+
+ run_test_number(20);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// FAIL if 1st Location: header is blank, followed by non-blank
+test_flags[20] = CL_EXPECT_FAILURE;
+
+function handler20(metadata, response) {
+ var body = "012345678901234567890123456789";
+ response.seizePower();
+ response.write("HTTP/1.0 301 Moved\r\n");
+ response.write("Content-Type: text/plain\r\n");
+ response.write("Content-Length: 30\r\n");
+ // redirect to previous test handler that completes OK: test 4
+ response.write("Location:\r\n");
+ response.write("Location: " + URL + testPathBase + "4\r\n");
+ response.write("Connection: close\r\n");
+ response.write("\r\n");
+ response.write(body);
+ response.finish();
+}
+
+function completeTest20(request, data, ctx) {
+ Assert.equal(request.status, Cr.NS_ERROR_CORRUPTED_CONTENT);
+
+ endTests();
+}
diff --git a/netwerk/test/unit/test_event_sink.js b/netwerk/test/unit/test_event_sink.js
new file mode 100644
index 0000000000..ecb16d845d
--- /dev/null
+++ b/netwerk/test/unit/test_event_sink.js
@@ -0,0 +1,195 @@
+// This file tests channel event sinks (bug 315598 et al)
+
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+XPCOMUtils.defineLazyGetter(this, "URL", function() {
+ return "http://localhost:" + httpserv.identity.primaryPort;
+});
+
+const sinkCID = Components.ID("{14aa4b81-e266-45cb-88f8-89595dece114}");
+const sinkContract = "@mozilla.org/network/unittest/channeleventsink;1";
+
+const categoryName = "net-channel-event-sinks";
+
+/**
+ * This object is both a factory and an nsIChannelEventSink implementation (so, it
+ * is de-facto a service). It's also an interface requestor that gives out
+ * itself when asked for nsIChannelEventSink.
+ */
+var eventsink = {
+ QueryInterface: ChromeUtils.generateQI(["nsIFactory", "nsIChannelEventSink"]),
+ createInstance: function eventsink_ci(outer, iid) {
+ if (outer) {
+ throw Components.Exception("", Cr.NS_ERROR_NO_AGGREGATION);
+ }
+ return this.QueryInterface(iid);
+ },
+ lockFactory: function eventsink_lockf(lock) {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
+ },
+
+ asyncOnChannelRedirect: function eventsink_onredir(
+ oldChan,
+ newChan,
+ flags,
+ callback
+ ) {
+ // veto
+ this.called = true;
+ throw Components.Exception("", Cr.NS_BINDING_ABORTED);
+ },
+
+ getInterface: function eventsink_gi(iid) {
+ if (iid.equals(Ci.nsIChannelEventSink)) {
+ return this;
+ }
+ throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE);
+ },
+
+ called: false,
+};
+
+var listener = {
+ expectSinkCall: true,
+
+ onStartRequest: function test_onStartR(request) {
+ try {
+ // Commenting out this check pending resolution of bug 255119
+ //if (Components.isSuccessCode(request.status))
+ // do_throw("Channel should have a failure code!");
+
+ // The current URI must be the original URI, as all redirects have been
+ // cancelled
+ if (
+ !(request instanceof Ci.nsIChannel) ||
+ !request.URI.equals(request.originalURI)
+ ) {
+ do_throw(
+ "Wrong URI: Is <" +
+ request.URI.spec +
+ ">, should be <" +
+ request.originalURI.spec +
+ ">"
+ );
+ }
+
+ if (request instanceof Ci.nsIHttpChannel) {
+ // As we expect a blocked redirect, verify that we have a 3xx status
+ Assert.equal(Math.floor(request.responseStatus / 100), 3);
+ Assert.equal(request.requestSucceeded, false);
+ }
+
+ Assert.equal(eventsink.called, this.expectSinkCall);
+ } catch (e) {
+ do_throw("Unexpected exception: " + e);
+ }
+
+ throw Components.Exception("", Cr.NS_ERROR_ABORT);
+ },
+
+ onDataAvailable: function test_ODA() {
+ do_throw("Should not get any data!");
+ },
+
+ onStopRequest: function test_onStopR(request, status) {
+ if (this._iteration <= 2) {
+ run_test_continued();
+ } else {
+ do_test_pending();
+ httpserv.stop(do_test_finished);
+ }
+ do_test_finished();
+ },
+
+ _iteration: 1,
+};
+
+function makeChan(url) {
+ return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true });
+}
+
+var httpserv = null;
+
+function run_test() {
+ httpserv = new HttpServer();
+ httpserv.registerPathHandler("/redirect", redirect);
+ httpserv.registerPathHandler("/redirectfile", redirectfile);
+ httpserv.start(-1);
+
+ Components.manager.nsIComponentRegistrar.registerFactory(
+ sinkCID,
+ "Unit test Event sink",
+ sinkContract,
+ eventsink
+ );
+
+ // Step 1: Set the callbacks on the listener itself
+ var chan = makeChan(URL + "/redirect");
+ chan.notificationCallbacks = eventsink;
+
+ chan.asyncOpen(listener);
+
+ do_test_pending();
+}
+
+function run_test_continued() {
+ eventsink.called = false;
+
+ var catMan = Cc["@mozilla.org/categorymanager;1"].getService(
+ Ci.nsICategoryManager
+ );
+
+ var chan;
+ if (listener._iteration == 1) {
+ // Step 2: Category entry
+ catMan.nsICategoryManager.addCategoryEntry(
+ categoryName,
+ "unit test",
+ sinkContract,
+ false,
+ true
+ );
+ chan = makeChan(URL + "/redirect");
+ } else {
+ // Step 3: Global contract id
+ catMan.nsICategoryManager.deleteCategoryEntry(
+ categoryName,
+ "unit test",
+ false
+ );
+ listener.expectSinkCall = false;
+ chan = makeChan(URL + "/redirectfile");
+ }
+
+ listener._iteration++;
+ chan.asyncOpen(listener);
+
+ do_test_pending();
+}
+
+// PATHS
+
+// /redirect
+function redirect(metadata, response) {
+ response.setStatusLine(metadata.httpVersion, 301, "Moved Permanently");
+ response.setHeader(
+ "Location",
+ "http://localhost:" + metadata.port + "/",
+ false
+ );
+
+ var body = "Moved\n";
+ response.bodyOutputStream.write(body, body.length);
+}
+
+// /redirectfile
+function redirectfile(metadata, response) {
+ response.setStatusLine(metadata.httpVersion, 301, "Moved Permanently");
+ response.setHeader("Content-Type", "text/plain", false);
+ response.setHeader("Location", "file:///etc/", false);
+
+ var body = "Attempted to move to a file URI, but failed.\n";
+ response.bodyOutputStream.write(body, body.length);
+}
diff --git a/netwerk/test/unit/test_eviction.js b/netwerk/test/unit/test_eviction.js
new file mode 100644
index 0000000000..43be39dba2
--- /dev/null
+++ b/netwerk/test/unit/test_eviction.js
@@ -0,0 +1,252 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var test_generator = do_run_test();
+
+function run_test() {
+ do_test_pending();
+ do_run_generator(test_generator);
+}
+
+function continue_test() {
+ do_run_generator(test_generator);
+}
+
+function repeat_test() {
+ // The test is probably going to fail because setting a batch of cookies took
+ // a significant fraction of 'gPurgeAge'. Compensate by rerunning the
+ // test with a larger purge age.
+ Assert.ok(gPurgeAge < 64);
+ gPurgeAge *= 2;
+ gShortExpiry *= 2;
+
+ executeSoon(function() {
+ test_generator.return();
+ test_generator = do_run_test();
+ do_run_generator(test_generator);
+ });
+}
+
+// Purge threshold, in seconds.
+var gPurgeAge = 1;
+
+// Short expiry age, in seconds.
+var gShortExpiry = 2;
+
+// Required delay to ensure a purge occurs, in milliseconds. This must be at
+// least gPurgeAge + 10%, and includes a little fuzz to account for timer
+// resolution and possible differences between PR_Now() and Date.now().
+function get_purge_delay() {
+ return gPurgeAge * 1100 + 100;
+}
+
+// Required delay to ensure a cookie set with an expiry time 'gShortExpiry' into
+// the future will have expired.
+function get_expiry_delay() {
+ return gShortExpiry * 1000 + 100;
+}
+
+function* do_run_test() {
+ // Set up a profile.
+ do_get_profile();
+
+ // twiddle prefs to convenient values for this test
+ Services.prefs.setIntPref("network.cookie.purgeAge", gPurgeAge);
+ Services.prefs.setIntPref("network.cookie.maxNumber", 100);
+
+ let expiry = Date.now() / 1000 + 1000;
+
+ // eviction is performed based on two limits: when the total number of cookies
+ // exceeds maxNumber + 10% (110), and when cookies are older than purgeAge
+ // (1 second). purging is done when both conditions are satisfied, and only
+ // those cookies are purged.
+
+ // we test the following cases of eviction:
+ // 1) excess and age are satisfied, but only some of the excess are old enough
+ // to be purged.
+ Services.cookiemgr.removeAll();
+ if (!set_cookies(0, 5, expiry)) {
+ repeat_test();
+ return;
+ }
+ // Sleep a while, to make sure the first batch of cookies is older than
+ // the second (timer resolution varies on different platforms).
+ do_timeout(get_purge_delay(), continue_test);
+ yield;
+ if (!set_cookies(5, 111, expiry)) {
+ repeat_test();
+ return;
+ }
+
+ // Fake a profile change, to ensure eviction affects the database correctly.
+ do_close_profile(test_generator);
+ yield;
+ do_load_profile();
+ Assert.ok(check_remaining_cookies(111, 5, 106));
+
+ // 2) excess and age are satisfied, and all of the excess are old enough
+ // to be purged.
+ Services.cookiemgr.removeAll();
+ if (!set_cookies(0, 10, expiry)) {
+ repeat_test();
+ return;
+ }
+ do_timeout(get_purge_delay(), continue_test);
+ yield;
+ if (!set_cookies(10, 111, expiry)) {
+ repeat_test();
+ return;
+ }
+
+ do_close_profile(test_generator);
+ yield;
+ do_load_profile();
+ Assert.ok(check_remaining_cookies(111, 10, 101));
+
+ // 3) excess and age are satisfied, and more than the excess are old enough
+ // to be purged.
+ Services.cookiemgr.removeAll();
+ if (!set_cookies(0, 50, expiry)) {
+ repeat_test();
+ return;
+ }
+ do_timeout(get_purge_delay(), continue_test);
+ yield;
+ if (!set_cookies(50, 111, expiry)) {
+ repeat_test();
+ return;
+ }
+
+ do_close_profile(test_generator);
+ yield;
+ do_load_profile();
+ Assert.ok(check_remaining_cookies(111, 50, 101));
+
+ // 4) excess but not age are satisfied.
+ Services.cookiemgr.removeAll();
+ if (!set_cookies(0, 120, expiry)) {
+ repeat_test();
+ return;
+ }
+
+ do_close_profile(test_generator);
+ yield;
+ do_load_profile();
+ Assert.ok(check_remaining_cookies(120, 0, 120));
+
+ // 5) age but not excess are satisfied.
+ Services.cookiemgr.removeAll();
+ if (!set_cookies(0, 20, expiry)) {
+ repeat_test();
+ return;
+ }
+ do_timeout(get_purge_delay(), continue_test);
+ yield;
+ if (!set_cookies(20, 110, expiry)) {
+ repeat_test();
+ return;
+ }
+
+ do_close_profile(test_generator);
+ yield;
+ do_load_profile();
+ Assert.ok(check_remaining_cookies(110, 20, 110));
+
+ // 6) Excess and age are satisfied, but the cookie limit can be satisfied by
+ // purging expired cookies.
+ Services.cookiemgr.removeAll();
+ let shortExpiry = Math.floor(Date.now() / 1000) + gShortExpiry;
+ if (!set_cookies(0, 20, shortExpiry)) {
+ repeat_test();
+ return;
+ }
+ do_timeout(get_expiry_delay(), continue_test);
+ yield;
+ if (!set_cookies(20, 110, expiry)) {
+ repeat_test();
+ return;
+ }
+ do_timeout(get_purge_delay(), continue_test);
+ yield;
+ if (!set_cookies(110, 111, expiry)) {
+ repeat_test();
+ return;
+ }
+
+ do_close_profile(test_generator);
+ yield;
+ do_load_profile();
+ Assert.ok(check_remaining_cookies(111, 20, 91));
+
+ do_finish_generator_test(test_generator);
+}
+
+// Set 'end - begin' total cookies, with consecutively increasing hosts numbered
+// 'begin' to 'end'.
+function set_cookies(begin, end, expiry) {
+ Assert.ok(begin != end);
+
+ let beginTime;
+ for (let i = begin; i < end; ++i) {
+ let host = "eviction." + i + ".tests";
+ Services.cookiemgr.add(
+ host,
+ "",
+ "test",
+ "eviction",
+ false,
+ false,
+ false,
+ expiry,
+ {},
+ Ci.nsICookie.SAMESITE_NONE,
+ Ci.nsICookie.SCHEME_HTTPS
+ );
+
+ if (i == begin) {
+ beginTime = get_creationTime(i);
+ }
+ }
+
+ let endTime = get_creationTime(end - 1);
+ Assert.ok(begin == end - 1 || endTime > beginTime);
+ if (endTime - beginTime > gPurgeAge * 1000000) {
+ // Setting cookies took an amount of time very close to the purge threshold.
+ // Retry the test with a larger threshold.
+ return false;
+ }
+
+ return true;
+}
+
+function get_creationTime(i) {
+ let host = "eviction." + i + ".tests";
+ let cookies = Services.cookiemgr.getCookiesFromHost(host, {});
+ Assert.ok(cookies.length);
+ let cookie = cookies[0];
+ return cookie.creationTime;
+}
+
+// Test that 'aNumberToExpect' cookies remain after purging is complete, and
+// that the cookies that remain consist of the set expected given the number of
+// of older and newer cookies -- eviction should occur by order of lastAccessed
+// time, if both the limit on total cookies (maxNumber + 10%) and the purge age
+// + 10% are exceeded.
+function check_remaining_cookies(aNumberTotal, aNumberOld, aNumberToExpect) {
+ let i = 0;
+ for (let cookie of Services.cookiemgr.cookies) {
+ ++i;
+
+ if (aNumberTotal != aNumberToExpect) {
+ // make sure the cookie is one of the batch we expect was purged.
+ var hostNumber = Number(cookie.rawHost.split(".")[1]);
+ if (hostNumber < aNumberOld - aNumberToExpect) {
+ break;
+ }
+ }
+ }
+
+ return i == aNumberToExpect;
+}
diff --git a/netwerk/test/unit/test_extract_charset_from_content_type.js b/netwerk/test/unit/test_extract_charset_from_content_type.js
new file mode 100644
index 0000000000..1d12b6bc2b
--- /dev/null
+++ b/netwerk/test/unit/test_extract_charset_from_content_type.js
@@ -0,0 +1,245 @@
+/* -*- tab-width: 2; indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+var charset = {};
+var charsetStart = {};
+var charsetEnd = {};
+var hadCharset;
+
+function reset() {
+ delete charset.value;
+ delete charsetStart.value;
+ delete charsetEnd.value;
+ hadCharset = undefined;
+}
+
+function check(aHadCharset, aCharset, aCharsetStart, aCharsetEnd) {
+ Assert.equal(aHadCharset, hadCharset);
+ Assert.equal(aCharset, charset.value);
+ Assert.equal(aCharsetStart, charsetStart.value);
+ Assert.equal(aCharsetEnd, charsetEnd.value);
+}
+
+function run_test() {
+ var netutil = Cc["@mozilla.org/network/util;1"].getService(Ci.nsINetUtil);
+ hadCharset = netutil.extractCharsetFromContentType(
+ "text/html",
+ charset,
+ charsetStart,
+ charsetEnd
+ );
+ check(false, "", 9, 9);
+
+ hadCharset = netutil.extractCharsetFromContentType(
+ "TEXT/HTML",
+ charset,
+ charsetStart,
+ charsetEnd
+ );
+ check(false, "", 9, 9);
+
+ hadCharset = netutil.extractCharsetFromContentType(
+ "text/html, text/html",
+ charset,
+ charsetStart,
+ charsetEnd
+ );
+ check(false, "", 9, 9);
+
+ hadCharset = netutil.extractCharsetFromContentType(
+ "text/html, text/plain",
+ charset,
+ charsetStart,
+ charsetEnd
+ );
+ check(false, "", 21, 21);
+
+ hadCharset = netutil.extractCharsetFromContentType(
+ "text/html, ",
+ charset,
+ charsetStart,
+ charsetEnd
+ );
+ check(false, "", 9, 9);
+
+ hadCharset = netutil.extractCharsetFromContentType(
+ "text/html, */*",
+ charset,
+ charsetStart,
+ charsetEnd
+ );
+ check(false, "", 9, 9);
+
+ hadCharset = netutil.extractCharsetFromContentType(
+ "text/html, foo",
+ charset,
+ charsetStart,
+ charsetEnd
+ );
+ check(false, "", 9, 9);
+
+ hadCharset = netutil.extractCharsetFromContentType(
+ "text/html; charset=ISO-8859-1",
+ charset,
+ charsetStart,
+ charsetEnd
+ );
+ check(true, "ISO-8859-1", 9, 29);
+
+ hadCharset = netutil.extractCharsetFromContentType(
+ "text/html ; charset=ISO-8859-1",
+ charset,
+ charsetStart,
+ charsetEnd
+ );
+ check(true, "ISO-8859-1", 11, 34);
+
+ hadCharset = netutil.extractCharsetFromContentType(
+ "text/html ; charset=ISO-8859-1 ",
+ charset,
+ charsetStart,
+ charsetEnd
+ );
+ check(true, "ISO-8859-1", 11, 36);
+
+ hadCharset = netutil.extractCharsetFromContentType(
+ "text/html ; charset=ISO-8859-1 ; ",
+ charset,
+ charsetStart,
+ charsetEnd
+ );
+ check(true, "ISO-8859-1", 11, 35);
+
+ hadCharset = netutil.extractCharsetFromContentType(
+ 'text/html; charset="ISO-8859-1"',
+ charset,
+ charsetStart,
+ charsetEnd
+ );
+ check(true, "ISO-8859-1", 9, 31);
+
+ hadCharset = netutil.extractCharsetFromContentType(
+ "text/html; charset='ISO-8859-1'",
+ charset,
+ charsetStart,
+ charsetEnd
+ );
+ check(true, "'ISO-8859-1'", 9, 31);
+
+ hadCharset = netutil.extractCharsetFromContentType(
+ 'text/html; charset="ISO-8859-1", text/html',
+ charset,
+ charsetStart,
+ charsetEnd
+ );
+ check(true, "ISO-8859-1", 9, 31);
+
+ hadCharset = netutil.extractCharsetFromContentType(
+ 'text/html; charset="ISO-8859-1", text/html; charset=UTF8',
+ charset,
+ charsetStart,
+ charsetEnd
+ );
+ check(true, "UTF8", 42, 56);
+
+ hadCharset = netutil.extractCharsetFromContentType(
+ "text/html; charset=ISO-8859-1, TEXT/HTML",
+ charset,
+ charsetStart,
+ charsetEnd
+ );
+ check(true, "ISO-8859-1", 9, 29);
+
+ hadCharset = netutil.extractCharsetFromContentType(
+ "text/html; charset=ISO-8859-1, TEXT/plain",
+ charset,
+ charsetStart,
+ charsetEnd
+ );
+ check(false, "", 41, 41);
+
+ hadCharset = netutil.extractCharsetFromContentType(
+ 'text/plain, TEXT/HTML; charset="ISO-8859-1", text/html, TEXT/HTML',
+ charset,
+ charsetStart,
+ charsetEnd
+ );
+ check(true, "ISO-8859-1", 21, 43);
+
+ hadCharset = netutil.extractCharsetFromContentType(
+ 'text/plain, TEXT/HTML; param="charset=UTF8"; charset="ISO-8859-1"; param2="charset=UTF16", text/html, TEXT/HTML',
+ charset,
+ charsetStart,
+ charsetEnd
+ );
+ check(true, "ISO-8859-1", 43, 65);
+
+ hadCharset = netutil.extractCharsetFromContentType(
+ 'text/plain, TEXT/HTML; param=charset=UTF8; charset="ISO-8859-1"; param2=charset=UTF16, text/html, TEXT/HTML',
+ charset,
+ charsetStart,
+ charsetEnd
+ );
+ check(true, "ISO-8859-1", 41, 63);
+
+ hadCharset = netutil.extractCharsetFromContentType(
+ "text/plain; param= , text/html",
+ charset,
+ charsetStart,
+ charsetEnd
+ );
+ check(false, "", 30, 30);
+
+ hadCharset = netutil.extractCharsetFromContentType(
+ 'text/plain; param=", text/html"',
+ charset,
+ charsetStart,
+ charsetEnd
+ );
+ check(false, "", 10, 10);
+
+ hadCharset = netutil.extractCharsetFromContentType(
+ 'text/plain; param=", \\" , text/html"',
+ charset,
+ charsetStart,
+ charsetEnd
+ );
+ check(false, "", 10, 10);
+
+ hadCharset = netutil.extractCharsetFromContentType(
+ 'text/plain; param=", \\" , text/html , "',
+ charset,
+ charsetStart,
+ charsetEnd
+ );
+ check(false, "", 10, 10);
+
+ hadCharset = netutil.extractCharsetFromContentType(
+ 'text/plain param=", \\" , text/html , "',
+ charset,
+ charsetStart,
+ charsetEnd
+ );
+ check(false, "", 38, 38);
+
+ hadCharset = netutil.extractCharsetFromContentType(
+ "text/plain charset=UTF8",
+ charset,
+ charsetStart,
+ charsetEnd
+ );
+ check(false, "", 23, 23);
+
+ hadCharset = netutil.extractCharsetFromContentType(
+ 'text/plain, TEXT/HTML; param="charset=UTF8"; ; param2="charset=UTF16", text/html, TEXT/HTML',
+ charset,
+ charsetStart,
+ charsetEnd
+ );
+ check(false, "", 21, 21);
+}
diff --git a/netwerk/test/unit/test_fallback_no-cache-entry_canceled.js b/netwerk/test/unit/test_fallback_no-cache-entry_canceled.js
new file mode 100644
index 0000000000..d5ca20badf
--- /dev/null
+++ b/netwerk/test/unit/test_fallback_no-cache-entry_canceled.js
@@ -0,0 +1,131 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+var httpServer = null;
+// Need to randomize, because apparently no one clears our cache
+var randomPath = "/redirect/" + Math.random();
+
+XPCOMUtils.defineLazyGetter(this, "randomURI", function() {
+ return "http://localhost:" + httpServer.identity.primaryPort + randomPath;
+});
+
+var cacheUpdateObserver = null;
+
+function make_channel(url, callback, ctx) {
+ return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true });
+}
+
+function make_uri(url) {
+ var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
+ return ios.newURI(url);
+}
+
+var responseBody = "Content body";
+
+// start the test with loading this master entry referencing the manifest
+function masterEntryHandler(metadata, response) {
+ var masterEntryContent = "<html manifest='/manifest'></html>";
+ response.setHeader("Content-Type", "text/html");
+ response.bodyOutputStream.write(
+ masterEntryContent,
+ masterEntryContent.length
+ );
+}
+
+// manifest defines fallback namespace from any /redirect path to /content
+function manifestHandler(metadata, response) {
+ var manifestContent = "CACHE MANIFEST\nFALLBACK:\nredirect /content\n";
+ response.setHeader("Content-Type", "text/cache-manifest");
+ response.bodyOutputStream.write(manifestContent, manifestContent.length);
+}
+
+// content handler correctly returns some plain text data
+function contentHandler(metadata, response) {
+ response.setHeader("Content-Type", "text/plain");
+ response.bodyOutputStream.write(responseBody, responseBody.length);
+}
+
+// finally check we got fallback content
+function finish_test(request, buffer) {
+ Assert.equal(buffer, "");
+ httpServer.stop(do_test_finished);
+}
+
+function run_test() {
+ httpServer = new HttpServer();
+ httpServer.registerPathHandler("/masterEntry", masterEntryHandler);
+ httpServer.registerPathHandler("/manifest", manifestHandler);
+ httpServer.registerPathHandler("/content", contentHandler);
+ httpServer.start(-1);
+
+ var pm = Cc["@mozilla.org/permissionmanager;1"].getService(
+ Ci.nsIPermissionManager
+ );
+ var ssm = Cc["@mozilla.org/scriptsecuritymanager;1"].getService(
+ Ci.nsIScriptSecurityManager
+ );
+ var uri = make_uri("http://localhost:" + httpServer.identity.primaryPort);
+ var principal = ssm.createContentPrincipal(uri, {});
+
+ if (pm.testPermissionFromPrincipal(principal, "offline-app") != 0) {
+ dump(
+ "Previous test failed to clear offline-app permission! Expect failures.\n"
+ );
+ }
+ pm.addFromPrincipal(
+ principal,
+ "offline-app",
+ Ci.nsIPermissionManager.ALLOW_ACTION
+ );
+
+ var ps = Cc["@mozilla.org/preferences-service;1"].getService(
+ Ci.nsIPrefBranch
+ );
+ dump(ps.getBoolPref("browser.cache.offline.enable"));
+ ps.setBoolPref("browser.cache.offline.enable", true);
+ ps.setBoolPref("browser.cache.offline.storage.enable", true);
+ ps.setComplexValue(
+ "browser.cache.offline.parent_directory",
+ Ci.nsIFile,
+ do_get_profile()
+ );
+
+ cacheUpdateObserver = {
+ observe() {
+ dump("got offline-cache-update-completed\n");
+ // offline cache update completed.
+ // In this test the randomURI doesn't exists
+ var chan = make_channel(randomURI);
+ chan.loadFlags =
+ Ci.nsIRequest.INHIBIT_CACHING |
+ Ci.nsIRequest.LOAD_FROM_CACHE |
+ Ci.nsICachingChannel.LOAD_ONLY_FROM_CACHE;
+ chan.notificationCallbacks = new ChannelEventSink(ES_ABORT_REDIRECT);
+ var chanac = chan.QueryInterface(Ci.nsIApplicationCacheChannel);
+ chanac.chooseApplicationCache = true;
+ chan.asyncOpen(new ChannelListener(finish_test, null, CL_EXPECT_FAILURE));
+ },
+ };
+
+ var os = Cc["@mozilla.org/observer-service;1"].getService(
+ Ci.nsIObserverService
+ );
+ os.addObserver(cacheUpdateObserver, "offline-cache-update-completed");
+
+ var us = Cc["@mozilla.org/offlinecacheupdate-service;1"].getService(
+ Ci.nsIOfflineCacheUpdateService
+ );
+ us.scheduleUpdate(
+ make_uri(
+ "http://localhost:" + httpServer.identity.primaryPort + "/manifest"
+ ),
+ make_uri(
+ "http://localhost:" + httpServer.identity.primaryPort + "/masterEntry"
+ ),
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ null
+ );
+
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_fallback_no-cache-entry_passing.js b/netwerk/test/unit/test_fallback_no-cache-entry_passing.js
new file mode 100644
index 0000000000..9bbadb4631
--- /dev/null
+++ b/netwerk/test/unit/test_fallback_no-cache-entry_passing.js
@@ -0,0 +1,130 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+var httpServer = null;
+// Need to randomize, because apparently no one clears our cache
+var randomPath = "/redirect/" + Math.random();
+
+XPCOMUtils.defineLazyGetter(this, "randomURI", function() {
+ return "http://localhost:" + httpServer.identity.primaryPort + randomPath;
+});
+
+var cacheUpdateObserver = null;
+
+function make_channel(url, callback, ctx) {
+ return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true });
+}
+
+function make_uri(url) {
+ var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
+ return ios.newURI(url);
+}
+
+var responseBody = "Content body";
+
+// start the test with loading this master entry referencing the manifest
+function masterEntryHandler(metadata, response) {
+ var masterEntryContent = "<html manifest='/manifest'></html>";
+ response.setHeader("Content-Type", "text/html");
+ response.bodyOutputStream.write(
+ masterEntryContent,
+ masterEntryContent.length
+ );
+}
+
+// manifest defines fallback namespace from any /redirect path to /content
+function manifestHandler(metadata, response) {
+ var manifestContent = "CACHE MANIFEST\nFALLBACK:\nredirect /content\n";
+ response.setHeader("Content-Type", "text/cache-manifest");
+ response.bodyOutputStream.write(manifestContent, manifestContent.length);
+}
+
+// content handler correctly returns some plain text data
+function contentHandler(metadata, response) {
+ response.setHeader("Content-Type", "text/plain");
+ response.bodyOutputStream.write(responseBody, responseBody.length);
+}
+
+// finally check we got fallback content
+function finish_test(request, buffer) {
+ Assert.equal(buffer, responseBody);
+ httpServer.stop(do_test_finished);
+}
+
+function run_test() {
+ httpServer = new HttpServer();
+ httpServer.registerPathHandler("/masterEntry", masterEntryHandler);
+ httpServer.registerPathHandler("/manifest", manifestHandler);
+ httpServer.registerPathHandler("/content", contentHandler);
+ httpServer.start(-1);
+
+ var pm = Cc["@mozilla.org/permissionmanager;1"].getService(
+ Ci.nsIPermissionManager
+ );
+ var ssm = Cc["@mozilla.org/scriptsecuritymanager;1"].getService(
+ Ci.nsIScriptSecurityManager
+ );
+ var uri = make_uri("http://localhost:" + httpServer.identity.primaryPort);
+ var principal = ssm.createContentPrincipal(uri, {});
+
+ if (pm.testPermissionFromPrincipal(principal, "offline-app") != 0) {
+ dump(
+ "Previous test failed to clear offline-app permission! Expect failures.\n"
+ );
+ }
+ pm.addFromPrincipal(
+ principal,
+ "offline-app",
+ Ci.nsIPermissionManager.ALLOW_ACTION
+ );
+
+ var ps = Cc["@mozilla.org/preferences-service;1"].getService(
+ Ci.nsIPrefBranch
+ );
+ dump(ps.getBoolPref("browser.cache.offline.enable"));
+ ps.setBoolPref("browser.cache.offline.enable", true);
+ ps.setBoolPref("browser.cache.offline.storage.enable", true);
+ ps.setComplexValue(
+ "browser.cache.offline.parent_directory",
+ Ci.nsIFile,
+ do_get_profile()
+ );
+
+ cacheUpdateObserver = {
+ observe() {
+ dump("got offline-cache-update-completed\n");
+ // offline cache update completed.
+ // In this test the randomURI doesn't exists
+ var chan = make_channel(randomURI);
+ chan.loadFlags =
+ Ci.nsIRequest.INHIBIT_CACHING |
+ Ci.nsIRequest.LOAD_FROM_CACHE |
+ Ci.nsICachingChannel.LOAD_ONLY_FROM_CACHE;
+ var chanac = chan.QueryInterface(Ci.nsIApplicationCacheChannel);
+ chanac.chooseApplicationCache = true;
+ chan.asyncOpen(new ChannelListener(finish_test));
+ },
+ };
+
+ var os = Cc["@mozilla.org/observer-service;1"].getService(
+ Ci.nsIObserverService
+ );
+ os.addObserver(cacheUpdateObserver, "offline-cache-update-completed");
+
+ var us = Cc["@mozilla.org/offlinecacheupdate-service;1"].getService(
+ Ci.nsIOfflineCacheUpdateService
+ );
+ us.scheduleUpdate(
+ make_uri(
+ "http://localhost:" + httpServer.identity.primaryPort + "/manifest"
+ ),
+ make_uri(
+ "http://localhost:" + httpServer.identity.primaryPort + "/masterEntry"
+ ),
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ null
+ );
+
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_fallback_redirect-to-different-origin_canceled.js b/netwerk/test/unit/test_fallback_redirect-to-different-origin_canceled.js
new file mode 100644
index 0000000000..d04797cacf
--- /dev/null
+++ b/netwerk/test/unit/test_fallback_redirect-to-different-origin_canceled.js
@@ -0,0 +1,133 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+var httpServer = null;
+// Need to randomize, because apparently no one clears our cache
+var randomPath = "/redirect/" + Math.random();
+
+XPCOMUtils.defineLazyGetter(this, "randomURI", function() {
+ return "http://localhost:" + httpServer.identity.primaryPort + randomPath;
+});
+
+var cacheUpdateObserver = null;
+
+function make_channel(url, callback, ctx) {
+ return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true });
+}
+
+function make_uri(url) {
+ var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
+ return ios.newURI(url);
+}
+
+const responseBody = "Content body";
+
+// start the test with loading this master entry referencing the manifest
+function masterEntryHandler(metadata, response) {
+ var masterEntryContent = "<html manifest='/manifest'></html>";
+ response.setHeader("Content-Type", "text/html");
+ response.bodyOutputStream.write(
+ masterEntryContent,
+ masterEntryContent.length
+ );
+}
+
+// manifest defines fallback namespace from any /redirect path to /content
+function manifestHandler(metadata, response) {
+ var manifestContent = "CACHE MANIFEST\nFALLBACK:\nredirect /content\n";
+ response.setHeader("Content-Type", "text/cache-manifest");
+ response.bodyOutputStream.write(manifestContent, manifestContent.length);
+}
+
+// content handler correctly returns some plain text data
+function contentHandler(metadata, response) {
+ response.setHeader("Content-Type", "text/plain");
+ response.bodyOutputStream.write(responseBody, responseBody.length);
+}
+
+// redirect handler returns redirect
+function redirectHandler(metadata, response) {
+ response.setStatusLine(metadata.httpVersion, 301, "Moved");
+ response.setHeader("Location", "http://example.com/", false);
+}
+
+// finally check we got fallback content
+function finish_test(request, buffer) {
+ Assert.equal(buffer, "");
+ httpServer.stop(do_test_finished);
+}
+
+function run_test() {
+ httpServer = new HttpServer();
+ httpServer.registerPathHandler("/masterEntry", masterEntryHandler);
+ httpServer.registerPathHandler("/manifest", manifestHandler);
+ httpServer.registerPathHandler("/content", contentHandler);
+ httpServer.registerPathHandler(randomPath, redirectHandler);
+ httpServer.start(-1);
+
+ var pm = Cc["@mozilla.org/permissionmanager;1"].getService(
+ Ci.nsIPermissionManager
+ );
+ var ssm = Cc["@mozilla.org/scriptsecuritymanager;1"].getService(
+ Ci.nsIScriptSecurityManager
+ );
+ var uri = make_uri("http://localhost:" + httpServer.identity.primaryPort);
+ var principal = ssm.createContentPrincipal(uri, {});
+
+ if (pm.testPermissionFromPrincipal(principal, "offline-app") != 0) {
+ dump(
+ "Previous test failed to clear offline-app permission! Expect failures.\n"
+ );
+ }
+ pm.addFromPrincipal(
+ principal,
+ "offline-app",
+ Ci.nsIPermissionManager.ALLOW_ACTION
+ );
+
+ var ps = Cc["@mozilla.org/preferences-service;1"].getService(
+ Ci.nsIPrefBranch
+ );
+ dump(ps.getBoolPref("browser.cache.offline.enable"));
+ ps.setBoolPref("browser.cache.offline.enable", true);
+ ps.setBoolPref("browser.cache.offline.storage.enable", true);
+ ps.setComplexValue(
+ "browser.cache.offline.parent_directory",
+ Ci.nsIFile,
+ do_get_profile()
+ );
+
+ cacheUpdateObserver = {
+ observe() {
+ dump("got offline-cache-update-completed\n");
+ // offline cache update completed.
+ var chan = make_channel(randomURI);
+ chan.notificationCallbacks = new ChannelEventSink(ES_ABORT_REDIRECT);
+ var chanac = chan.QueryInterface(Ci.nsIApplicationCacheChannel);
+ chanac.chooseApplicationCache = true;
+ chan.asyncOpen(new ChannelListener(finish_test, null, CL_EXPECT_FAILURE));
+ },
+ };
+
+ var os = Cc["@mozilla.org/observer-service;1"].getService(
+ Ci.nsIObserverService
+ );
+ os.addObserver(cacheUpdateObserver, "offline-cache-update-completed");
+
+ var us = Cc["@mozilla.org/offlinecacheupdate-service;1"].getService(
+ Ci.nsIOfflineCacheUpdateService
+ );
+ us.scheduleUpdate(
+ make_uri(
+ "http://localhost:" + httpServer.identity.primaryPort + "/manifest"
+ ),
+ make_uri(
+ "http://localhost:" + httpServer.identity.primaryPort + "/masterEntry"
+ ),
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ null
+ );
+
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_fallback_redirect-to-different-origin_passing.js b/netwerk/test/unit/test_fallback_redirect-to-different-origin_passing.js
new file mode 100644
index 0000000000..dc1702da3f
--- /dev/null
+++ b/netwerk/test/unit/test_fallback_redirect-to-different-origin_passing.js
@@ -0,0 +1,132 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+var httpServer = null;
+// Need to randomize, because apparently no one clears our cache
+var randomPath = "/redirect/" + Math.random();
+
+XPCOMUtils.defineLazyGetter(this, "randomURI", function() {
+ return "http://localhost:" + httpServer.identity.primaryPort + randomPath;
+});
+
+var cacheUpdateObserver = null;
+
+function make_channel(url, callback, ctx) {
+ return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true });
+}
+
+function make_uri(url) {
+ var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
+ return ios.newURI(url);
+}
+
+var responseBody = "Content body";
+
+// start the test with loading this master entry referencing the manifest
+function masterEntryHandler(metadata, response) {
+ var masterEntryContent = "<html manifest='/manifest'></html>";
+ response.setHeader("Content-Type", "text/html");
+ response.bodyOutputStream.write(
+ masterEntryContent,
+ masterEntryContent.length
+ );
+}
+
+// manifest defines fallback namespace from any /redirect path to /content
+function manifestHandler(metadata, response) {
+ var manifestContent = "CACHE MANIFEST\nFALLBACK:\nredirect /content\n";
+ response.setHeader("Content-Type", "text/cache-manifest");
+ response.bodyOutputStream.write(manifestContent, manifestContent.length);
+}
+
+// content handler correctly returns some plain text data
+function contentHandler(metadata, response) {
+ response.setHeader("Content-Type", "text/plain");
+ response.bodyOutputStream.write(responseBody, responseBody.length);
+}
+
+// redirect handler returns redirect
+function redirectHandler(metadata, response) {
+ response.setStatusLine(metadata.httpVersion, 301, "Moved");
+ response.setHeader("Location", "http://example.com/", false);
+}
+
+// finally check we got fallback content
+function finish_test(request, buffer) {
+ Assert.equal(buffer, responseBody);
+ httpServer.stop(do_test_finished);
+}
+
+function run_test() {
+ httpServer = new HttpServer();
+ httpServer.registerPathHandler("/masterEntry", masterEntryHandler);
+ httpServer.registerPathHandler("/manifest", manifestHandler);
+ httpServer.registerPathHandler("/content", contentHandler);
+ httpServer.registerPathHandler(randomPath, redirectHandler);
+ httpServer.start(-1);
+
+ var pm = Cc["@mozilla.org/permissionmanager;1"].getService(
+ Ci.nsIPermissionManager
+ );
+ var ssm = Cc["@mozilla.org/scriptsecuritymanager;1"].getService(
+ Ci.nsIScriptSecurityManager
+ );
+ var uri = make_uri("http://localhost:" + httpServer.identity.primaryPort);
+ var principal = ssm.createContentPrincipal(uri, {});
+
+ if (pm.testPermissionFromPrincipal(principal, "offline-app") != 0) {
+ dump(
+ "Previous test failed to clear offline-app permission! Expect failures.\n"
+ );
+ }
+ pm.addFromPrincipal(
+ principal,
+ "offline-app",
+ Ci.nsIPermissionManager.ALLOW_ACTION
+ );
+
+ var ps = Cc["@mozilla.org/preferences-service;1"].getService(
+ Ci.nsIPrefBranch
+ );
+ dump(ps.getBoolPref("browser.cache.offline.enable"));
+ ps.setBoolPref("browser.cache.offline.enable", true);
+ ps.setBoolPref("browser.cache.offline.storage.enable", true);
+ ps.setComplexValue(
+ "browser.cache.offline.parent_directory",
+ Ci.nsIFile,
+ do_get_profile()
+ );
+
+ cacheUpdateObserver = {
+ observe() {
+ dump("got offline-cache-update-completed\n");
+ // offline cache update completed.
+ var chan = make_channel(randomURI);
+ var chanac = chan.QueryInterface(Ci.nsIApplicationCacheChannel);
+ chanac.chooseApplicationCache = true;
+ chan.asyncOpen(new ChannelListener(finish_test));
+ },
+ };
+
+ var os = Cc["@mozilla.org/observer-service;1"].getService(
+ Ci.nsIObserverService
+ );
+ os.addObserver(cacheUpdateObserver, "offline-cache-update-completed");
+
+ var us = Cc["@mozilla.org/offlinecacheupdate-service;1"].getService(
+ Ci.nsIOfflineCacheUpdateService
+ );
+ us.scheduleUpdate(
+ make_uri(
+ "http://localhost:" + httpServer.identity.primaryPort + "/manifest"
+ ),
+ make_uri(
+ "http://localhost:" + httpServer.identity.primaryPort + "/masterEntry"
+ ),
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ null
+ );
+
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_fallback_request-error_canceled.js b/netwerk/test/unit/test_fallback_request-error_canceled.js
new file mode 100644
index 0000000000..8f1eb8b597
--- /dev/null
+++ b/netwerk/test/unit/test_fallback_request-error_canceled.js
@@ -0,0 +1,140 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+var httpServer = null;
+// Need to randomize, because apparently no one clears our cache
+var randomPath = "/redirect/" + Math.random();
+
+XPCOMUtils.defineLazyGetter(this, "randomURI", function() {
+ return "http://localhost:" + httpServer.identity.primaryPort + randomPath;
+});
+
+var cacheUpdateObserver = null;
+
+function make_channel(url, callback, ctx) {
+ return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true });
+}
+
+function make_uri(url) {
+ var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
+ return ios.newURI(url);
+}
+
+var responseBody = "Content body";
+
+// start the test with loading this master entry referencing the manifest
+function masterEntryHandler(metadata, response) {
+ var masterEntryContent = "<html manifest='/manifest'></html>";
+ response.setHeader("Content-Type", "text/html");
+ response.bodyOutputStream.write(
+ masterEntryContent,
+ masterEntryContent.length
+ );
+}
+
+// manifest defines fallback namespace from any /redirect path to /content
+function manifestHandler(metadata, response) {
+ var manifestContent = "CACHE MANIFEST\nFALLBACK:\nredirect /content\n";
+ response.setHeader("Content-Type", "text/cache-manifest");
+ response.bodyOutputStream.write(manifestContent, manifestContent.length);
+}
+
+// content handler correctly returns some plain text data
+function contentHandler(metadata, response) {
+ response.setHeader("Content-Type", "text/plain");
+ response.bodyOutputStream.write(responseBody, responseBody.length);
+}
+
+// redirect handler returns redirect
+function redirectHandler(metadata, response) {
+ response.setStatusLine(metadata.httpVersion, 301, "Moved");
+ response.setHeader("Location", "http://example.com/", false);
+}
+
+// finally check we got fallback content
+function finish_test(request, buffer) {
+ Assert.equal(buffer, "");
+ do_test_finished();
+}
+
+function run_test() {
+ httpServer = new HttpServer();
+ httpServer.registerPathHandler("/masterEntry", masterEntryHandler);
+ httpServer.registerPathHandler("/manifest", manifestHandler);
+ httpServer.registerPathHandler("/content", contentHandler);
+ httpServer.start(-1);
+
+ var pm = Cc["@mozilla.org/permissionmanager;1"].getService(
+ Ci.nsIPermissionManager
+ );
+ var ssm = Cc["@mozilla.org/scriptsecuritymanager;1"].getService(
+ Ci.nsIScriptSecurityManager
+ );
+ var uri = make_uri("http://localhost:" + httpServer.identity.primaryPort);
+ var principal = ssm.createContentPrincipal(uri, {});
+
+ if (pm.testPermissionFromPrincipal(principal, "offline-app") != 0) {
+ dump(
+ "Previous test failed to clear offline-app permission! Expect failures.\n"
+ );
+ }
+ pm.addFromPrincipal(
+ principal,
+ "offline-app",
+ Ci.nsIPermissionManager.ALLOW_ACTION
+ );
+
+ var ps = Cc["@mozilla.org/preferences-service;1"].getService(
+ Ci.nsIPrefBranch
+ );
+ dump(ps.getBoolPref("browser.cache.offline.enable"));
+ ps.setBoolPref("browser.cache.offline.enable", true);
+ ps.setBoolPref("browser.cache.offline.storage.enable", true);
+ ps.setComplexValue(
+ "browser.cache.offline.parent_directory",
+ Ci.nsIFile,
+ do_get_profile()
+ );
+
+ cacheUpdateObserver = {
+ observe() {
+ dump("got offline-cache-update-completed\n");
+ // offline cache update completed.
+
+ // doing this to eval the lazy getter, otherwise the make_channel call would hang
+ randomURI;
+ httpServer.stop(function() {
+ // Now shut the server down to have an error in onStartRequest
+ var chan = make_channel(randomURI);
+ chan.notificationCallbacks = new ChannelEventSink(ES_ABORT_REDIRECT);
+ var chanac = chan.QueryInterface(Ci.nsIApplicationCacheChannel);
+ chanac.chooseApplicationCache = true;
+ chan.asyncOpen(
+ new ChannelListener(finish_test, null, CL_EXPECT_FAILURE)
+ );
+ });
+ },
+ };
+
+ var os = Cc["@mozilla.org/observer-service;1"].getService(
+ Ci.nsIObserverService
+ );
+ os.addObserver(cacheUpdateObserver, "offline-cache-update-completed");
+
+ var us = Cc["@mozilla.org/offlinecacheupdate-service;1"].getService(
+ Ci.nsIOfflineCacheUpdateService
+ );
+ us.scheduleUpdate(
+ make_uri(
+ "http://localhost:" + httpServer.identity.primaryPort + "/manifest"
+ ),
+ make_uri(
+ "http://localhost:" + httpServer.identity.primaryPort + "/masterEntry"
+ ),
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ null
+ );
+
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_fallback_request-error_passing.js b/netwerk/test/unit/test_fallback_request-error_passing.js
new file mode 100644
index 0000000000..89a672ee4c
--- /dev/null
+++ b/netwerk/test/unit/test_fallback_request-error_passing.js
@@ -0,0 +1,136 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+var httpServer = null;
+// Need to randomize, because apparently no one clears our cache
+var randomPath = "/redirect/" + Math.random();
+
+XPCOMUtils.defineLazyGetter(this, "randomURI", function() {
+ return "http://localhost:" + httpServer.identity.primaryPort + randomPath;
+});
+
+var cacheUpdateObserver = null;
+var systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal();
+
+function make_channel(url, callback, ctx) {
+ return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true });
+}
+
+function make_uri(url) {
+ var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
+ return ios.newURI(url);
+}
+
+var responseBody = "Content body";
+
+// start the test with loading this master entry referencing the manifest
+function masterEntryHandler(metadata, response) {
+ var masterEntryContent = "<html manifest='/manifest'></html>";
+ response.setHeader("Content-Type", "text/html");
+ response.bodyOutputStream.write(
+ masterEntryContent,
+ masterEntryContent.length
+ );
+}
+
+// manifest defines fallback namespace from any /redirect path to /content
+function manifestHandler(metadata, response) {
+ var manifestContent = "CACHE MANIFEST\nFALLBACK:\nredirect /content\n";
+ response.setHeader("Content-Type", "text/cache-manifest");
+ response.bodyOutputStream.write(manifestContent, manifestContent.length);
+}
+
+// content handler correctly returns some plain text data
+function contentHandler(metadata, response) {
+ response.setHeader("Content-Type", "text/plain");
+ response.bodyOutputStream.write(responseBody, responseBody.length);
+}
+
+// redirect handler returns redirect
+function redirectHandler(metadata, response) {
+ response.setStatusLine(metadata.httpVersion, 301, "Moved");
+ response.setHeader("Location", "http://example.com/", false);
+}
+
+// finally check we got fallback content
+function finish_test(request, buffer) {
+ Assert.equal(buffer, responseBody);
+ do_test_finished();
+}
+
+function run_test() {
+ httpServer = new HttpServer();
+ httpServer.registerPathHandler("/masterEntry", masterEntryHandler);
+ httpServer.registerPathHandler("/manifest", manifestHandler);
+ httpServer.registerPathHandler("/content", contentHandler);
+ httpServer.start(-1);
+
+ var pm = Cc["@mozilla.org/permissionmanager;1"].getService(
+ Ci.nsIPermissionManager
+ );
+ var ssm = Cc["@mozilla.org/scriptsecuritymanager;1"].getService(
+ Ci.nsIScriptSecurityManager
+ );
+ var uri = make_uri("http://localhost:" + httpServer.identity.primaryPort);
+ var principal = ssm.createContentPrincipal(uri, {});
+
+ if (pm.testPermissionFromPrincipal(principal, "offline-app") != 0) {
+ dump(
+ "Previous test failed to clear offline-app permission! Expect failures.\n"
+ );
+ }
+ pm.addFromPrincipal(
+ principal,
+ "offline-app",
+ Ci.nsIPermissionManager.ALLOW_ACTION
+ );
+
+ var ps = Cc["@mozilla.org/preferences-service;1"].getService(
+ Ci.nsIPrefBranch
+ );
+ dump(ps.getBoolPref("browser.cache.offline.enable"));
+ ps.setBoolPref("browser.cache.offline.enable", true);
+ ps.setBoolPref("browser.cache.offline.storage.enable", true);
+ ps.setComplexValue(
+ "browser.cache.offline.parent_directory",
+ Ci.nsIFile,
+ do_get_profile()
+ );
+
+ cacheUpdateObserver = {
+ observe() {
+ dump("got offline-cache-update-completed\n");
+ // offline cache update completed.
+ randomURI; // doing this so the lazy value gets computed
+ httpServer.stop(function() {
+ // Now shut the server down to have an error in onstartrequest
+ var chan = make_channel(randomURI);
+ var chanac = chan.QueryInterface(Ci.nsIApplicationCacheChannel);
+ chanac.chooseApplicationCache = true;
+ chan.asyncOpen(new ChannelListener(finish_test));
+ });
+ },
+ };
+
+ var os = Cc["@mozilla.org/observer-service;1"].getService(
+ Ci.nsIObserverService
+ );
+ os.addObserver(cacheUpdateObserver, "offline-cache-update-completed");
+
+ var us = Cc["@mozilla.org/offlinecacheupdate-service;1"].getService(
+ Ci.nsIOfflineCacheUpdateService
+ );
+ us.scheduleUpdate(
+ make_uri(
+ "http://localhost:" + httpServer.identity.primaryPort + "/manifest"
+ ),
+ make_uri(
+ "http://localhost:" + httpServer.identity.primaryPort + "/masterEntry"
+ ),
+ systemPrincipal,
+ null
+ );
+
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_fallback_response-error_canceled.js b/netwerk/test/unit/test_fallback_response-error_canceled.js
new file mode 100644
index 0000000000..5bd8a2d222
--- /dev/null
+++ b/netwerk/test/unit/test_fallback_response-error_canceled.js
@@ -0,0 +1,133 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+var httpServer = null;
+// Need to randomize, because apparently no one clears our cache
+var randomPath = "/error/" + Math.random();
+
+XPCOMUtils.defineLazyGetter(this, "randomURI", function() {
+ return "http://localhost:" + httpServer.identity.primaryPort + randomPath;
+});
+
+var cacheUpdateObserver = null;
+var systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal();
+
+function make_channel(url, callback, ctx) {
+ return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true });
+}
+
+function make_uri(url) {
+ var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
+ return ios.newURI(url);
+}
+
+var responseBody = "Content body";
+
+// start the test with loading this master entry referencing the manifest
+function masterEntryHandler(metadata, response) {
+ var masterEntryContent = "<html manifest='/manifest'></html>";
+ response.setHeader("Content-Type", "text/html");
+ response.bodyOutputStream.write(
+ masterEntryContent,
+ masterEntryContent.length
+ );
+}
+
+// manifest defines fallback namespace from any /error path to /content
+function manifestHandler(metadata, response) {
+ var manifestContent = "CACHE MANIFEST\nFALLBACK:\nerror /content\n";
+ response.setHeader("Content-Type", "text/cache-manifest");
+ response.bodyOutputStream.write(manifestContent, manifestContent.length);
+}
+
+// content handler correctly returns some plain text data
+function contentHandler(metadata, response) {
+ response.setHeader("Content-Type", "text/plain");
+ response.bodyOutputStream.write(responseBody, responseBody.length);
+}
+
+// error handler returns error
+function errorHandler(metadata, response) {
+ response.setStatusLine(metadata.httpVersion, 404, "Bad request");
+}
+
+// finally check we got fallback content
+function finish_test(request, buffer) {
+ Assert.equal(buffer, "");
+ httpServer.stop(do_test_finished);
+}
+
+function run_test() {
+ httpServer = new HttpServer();
+ httpServer.registerPathHandler("/masterEntry", masterEntryHandler);
+ httpServer.registerPathHandler("/manifest", manifestHandler);
+ httpServer.registerPathHandler("/content", contentHandler);
+ httpServer.registerPathHandler(randomPath, errorHandler);
+ httpServer.start(-1);
+
+ var pm = Cc["@mozilla.org/permissionmanager;1"].getService(
+ Ci.nsIPermissionManager
+ );
+ var ssm = Cc["@mozilla.org/scriptsecuritymanager;1"].getService(
+ Ci.nsIScriptSecurityManager
+ );
+ var uri = make_uri("http://localhost:" + httpServer.identity.primaryPort);
+ var principal = ssm.createContentPrincipal(uri, {});
+
+ if (pm.testPermissionFromPrincipal(principal, "offline-app") != 0) {
+ dump(
+ "Previous test failed to clear offline-app permission! Expect failures.\n"
+ );
+ }
+ pm.addFromPrincipal(
+ principal,
+ "offline-app",
+ Ci.nsIPermissionManager.ALLOW_ACTION
+ );
+
+ var ps = Cc["@mozilla.org/preferences-service;1"].getService(
+ Ci.nsIPrefBranch
+ );
+ dump(ps.getBoolPref("browser.cache.offline.enable"));
+ ps.setBoolPref("browser.cache.offline.enable", true);
+ ps.setBoolPref("browser.cache.offline.storage.enable", true);
+ ps.setComplexValue(
+ "browser.cache.offline.parent_directory",
+ Ci.nsIFile,
+ do_get_profile()
+ );
+
+ cacheUpdateObserver = {
+ observe() {
+ dump("got offline-cache-update-completed\n");
+ // offline cache update completed.
+ var chan = make_channel(randomURI);
+ chan.notificationCallbacks = new ChannelEventSink(ES_ABORT_REDIRECT);
+ var chanac = chan.QueryInterface(Ci.nsIApplicationCacheChannel);
+ chanac.chooseApplicationCache = true;
+ chan.asyncOpen(new ChannelListener(finish_test, null, CL_EXPECT_FAILURE));
+ },
+ };
+
+ var os = Cc["@mozilla.org/observer-service;1"].getService(
+ Ci.nsIObserverService
+ );
+ os.addObserver(cacheUpdateObserver, "offline-cache-update-completed");
+
+ var us = Cc["@mozilla.org/offlinecacheupdate-service;1"].getService(
+ Ci.nsIOfflineCacheUpdateService
+ );
+ us.scheduleUpdate(
+ make_uri(
+ "http://localhost:" + httpServer.identity.primaryPort + "/manifest"
+ ),
+ make_uri(
+ "http://localhost:" + httpServer.identity.primaryPort + "/masterEntry"
+ ),
+ systemPrincipal,
+ null
+ );
+
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_fallback_response-error_passing.js b/netwerk/test/unit/test_fallback_response-error_passing.js
new file mode 100644
index 0000000000..c9715a916a
--- /dev/null
+++ b/netwerk/test/unit/test_fallback_response-error_passing.js
@@ -0,0 +1,132 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+var httpServer = null;
+// Need to randomize, because apparently no one clears our cache
+var randomPath = "/error/" + Math.random();
+
+XPCOMUtils.defineLazyGetter(this, "randomURI", function() {
+ return "http://localhost:" + httpServer.identity.primaryPort + randomPath;
+});
+
+var cacheUpdateObserver = null;
+var systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal();
+
+function make_channel(url, callback, ctx) {
+ return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true });
+}
+
+function make_uri(url) {
+ var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
+ return ios.newURI(url);
+}
+
+var responseBody = "Content body";
+
+// start the test with loading this master entry referencing the manifest
+function masterEntryHandler(metadata, response) {
+ var masterEntryContent = "<html manifest='/manifest'></html>";
+ response.setHeader("Content-Type", "text/html");
+ response.bodyOutputStream.write(
+ masterEntryContent,
+ masterEntryContent.length
+ );
+}
+
+// manifest defines fallback namespace from any /error path to /content
+function manifestHandler(metadata, response) {
+ var manifestContent = "CACHE MANIFEST\nFALLBACK:\nerror /content\n";
+ response.setHeader("Content-Type", "text/cache-manifest");
+ response.bodyOutputStream.write(manifestContent, manifestContent.length);
+}
+
+// content handler correctly returns some plain text data
+function contentHandler(metadata, response) {
+ response.setHeader("Content-Type", "text/plain");
+ response.bodyOutputStream.write(responseBody, responseBody.length);
+}
+
+// error handler returns error
+function errorHandler(metadata, response) {
+ response.setStatusLine(metadata.httpVersion, 404, "Bad request");
+}
+
+// finally check we got fallback content
+function finish_test(request, buffer) {
+ Assert.equal(buffer, responseBody);
+ httpServer.stop(do_test_finished);
+}
+
+function run_test() {
+ httpServer = new HttpServer();
+ httpServer.registerPathHandler("/masterEntry", masterEntryHandler);
+ httpServer.registerPathHandler("/manifest", manifestHandler);
+ httpServer.registerPathHandler("/content", contentHandler);
+ httpServer.registerPathHandler(randomPath, errorHandler);
+ httpServer.start(-1);
+
+ var pm = Cc["@mozilla.org/permissionmanager;1"].getService(
+ Ci.nsIPermissionManager
+ );
+ var ssm = Cc["@mozilla.org/scriptsecuritymanager;1"].getService(
+ Ci.nsIScriptSecurityManager
+ );
+ var uri = make_uri("http://localhost:" + httpServer.identity.primaryPort);
+ var principal = ssm.createContentPrincipal(uri, {});
+
+ if (pm.testPermissionFromPrincipal(principal, "offline-app") != 0) {
+ dump(
+ "Previous test failed to clear offline-app permission! Expect failures.\n"
+ );
+ }
+ pm.addFromPrincipal(
+ principal,
+ "offline-app",
+ Ci.nsIPermissionManager.ALLOW_ACTION
+ );
+
+ var ps = Cc["@mozilla.org/preferences-service;1"].getService(
+ Ci.nsIPrefBranch
+ );
+ dump(ps.getBoolPref("browser.cache.offline.enable"));
+ ps.setBoolPref("browser.cache.offline.enable", true);
+ ps.setBoolPref("browser.cache.offline.storage.enable", true);
+ ps.setComplexValue(
+ "browser.cache.offline.parent_directory",
+ Ci.nsIFile,
+ do_get_profile()
+ );
+
+ cacheUpdateObserver = {
+ observe() {
+ dump("got offline-cache-update-completed\n");
+ // offline cache update completed.
+ var chan = make_channel(randomURI);
+ var chanac = chan.QueryInterface(Ci.nsIApplicationCacheChannel);
+ chanac.chooseApplicationCache = true;
+ chan.asyncOpen(new ChannelListener(finish_test));
+ },
+ };
+
+ var os = Cc["@mozilla.org/observer-service;1"].getService(
+ Ci.nsIObserverService
+ );
+ os.addObserver(cacheUpdateObserver, "offline-cache-update-completed");
+
+ var us = Cc["@mozilla.org/offlinecacheupdate-service;1"].getService(
+ Ci.nsIOfflineCacheUpdateService
+ );
+ us.scheduleUpdate(
+ make_uri(
+ "http://localhost:" + httpServer.identity.primaryPort + "/manifest"
+ ),
+ make_uri(
+ "http://localhost:" + httpServer.identity.primaryPort + "/masterEntry"
+ ),
+ systemPrincipal,
+ null
+ );
+
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_file_protocol.js b/netwerk/test/unit/test_file_protocol.js
new file mode 100644
index 0000000000..9aa4cf4183
--- /dev/null
+++ b/netwerk/test/unit/test_file_protocol.js
@@ -0,0 +1,280 @@
+/* run some tests on the file:// protocol handler */
+
+"use strict";
+
+const PR_RDONLY = 0x1; // see prio.h
+
+const special_type = "application/x-our-special-type";
+
+[
+ test_read_file,
+ test_read_dir_1,
+ test_read_dir_2,
+ test_upload_file,
+ test_load_shelllink,
+ do_test_finished,
+].forEach(f => add_test(f));
+
+function getFile(key) {
+ var dirSvc = Cc["@mozilla.org/file/directory_service;1"].getService(
+ Ci.nsIProperties
+ );
+ return dirSvc.get(key, Ci.nsIFile);
+}
+
+function new_file_input_stream(file, buffered) {
+ var stream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(
+ Ci.nsIFileInputStream
+ );
+ stream.init(file, PR_RDONLY, 0, 0);
+ if (!buffered) {
+ return stream;
+ }
+
+ var buffer = Cc[
+ "@mozilla.org/network/buffered-input-stream;1"
+ ].createInstance(Ci.nsIBufferedInputStream);
+ buffer.init(stream, 4096);
+ return buffer;
+}
+
+function new_file_channel(file) {
+ var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
+ return NetUtil.newChannel({
+ uri: ios.newFileURI(file),
+ loadUsingSystemPrincipal: true,
+ });
+}
+
+/*
+ * stream listener
+ * this listener has some additional file-specific tests, so we can't just use
+ * ChannelListener here.
+ */
+function FileStreamListener(closure) {
+ this._closure = closure;
+}
+FileStreamListener.prototype = {
+ _closure: null,
+ _buffer: "",
+ _got_onstartrequest: false,
+ _got_onstoprequest: false,
+ _contentLen: -1,
+
+ _isDir(request) {
+ request.QueryInterface(Ci.nsIFileChannel);
+ return request.file.isDirectory();
+ },
+
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIStreamListener",
+ "nsIRequestObserver",
+ ]),
+
+ onStartRequest(request) {
+ if (this._got_onstartrequest) {
+ do_throw("Got second onStartRequest event!");
+ }
+ this._got_onstartrequest = true;
+
+ if (!this._isDir(request)) {
+ request.QueryInterface(Ci.nsIChannel);
+ this._contentLen = request.contentLength;
+ if (this._contentLen == -1) {
+ do_throw("Content length is unknown in onStartRequest!");
+ }
+ }
+ },
+
+ onDataAvailable(request, stream, offset, count) {
+ if (!this._got_onstartrequest) {
+ do_throw("onDataAvailable without onStartRequest event!");
+ }
+ if (this._got_onstoprequest) {
+ do_throw("onDataAvailable after onStopRequest event!");
+ }
+ if (!request.isPending()) {
+ do_throw("request reports itself as not pending from onStartRequest!");
+ }
+
+ this._buffer = this._buffer.concat(read_stream(stream, count));
+ },
+
+ onStopRequest(request, status) {
+ if (!this._got_onstartrequest) {
+ do_throw("onStopRequest without onStartRequest event!");
+ }
+ if (this._got_onstoprequest) {
+ do_throw("Got second onStopRequest event!");
+ }
+ this._got_onstoprequest = true;
+ if (!Components.isSuccessCode(status)) {
+ do_throw("Failed to load file: " + status.toString(16));
+ }
+ if (status != request.status) {
+ do_throw("request.status does not match status arg to onStopRequest!");
+ }
+ if (request.isPending()) {
+ do_throw("request reports itself as pending from onStopRequest!");
+ }
+ if (this._contentLen != -1 && this._buffer.length != this._contentLen) {
+ do_throw("did not read nsIChannel.contentLength number of bytes!");
+ }
+
+ this._closure(this._buffer);
+ },
+};
+
+function test_read_file() {
+ dump("*** test_read_file\n");
+
+ var file = do_get_file("../unit/data/test_readline6.txt");
+ var chan = new_file_channel(file);
+
+ function on_read_complete(data) {
+ dump("*** test_read_file.on_read_complete\n");
+
+ // bug 326693
+ if (chan.contentType != special_type) {
+ do_throw(
+ "Type mismatch! Is <" +
+ chan.contentType +
+ ">, should be <" +
+ special_type +
+ ">"
+ );
+ }
+
+ /* read completed successfully. now read data directly from file,
+ and compare the result. */
+ var stream = new_file_input_stream(file, false);
+ var result = read_stream(stream, stream.available());
+ if (result != data) {
+ do_throw("Stream contents do not match with direct read!");
+ }
+ run_next_test();
+ }
+
+ chan.contentType = special_type;
+ chan.asyncOpen(new FileStreamListener(on_read_complete));
+}
+
+function do_test_read_dir(set_type, expected_type) {
+ dump("*** test_read_dir(" + set_type + ", " + expected_type + ")\n");
+
+ var file = do_get_tempdir();
+ var chan = new_file_channel(file);
+
+ function on_read_complete(data) {
+ dump(
+ "*** test_read_dir.on_read_complete(" +
+ set_type +
+ ", " +
+ expected_type +
+ ")\n"
+ );
+
+ // bug 326693
+ if (chan.contentType != expected_type) {
+ do_throw(
+ "Type mismatch! Is <" +
+ chan.contentType +
+ ">, should be <" +
+ expected_type +
+ ">"
+ );
+ }
+
+ run_next_test();
+ }
+
+ if (set_type) {
+ chan.contentType = expected_type;
+ }
+ chan.asyncOpen(new FileStreamListener(on_read_complete));
+}
+
+function test_read_dir_1() {
+ return do_test_read_dir(false, "application/http-index-format");
+}
+
+function test_read_dir_2() {
+ return do_test_read_dir(true, special_type);
+}
+
+function test_upload_file() {
+ dump("*** test_upload_file\n");
+
+ var file = do_get_file("../unit/data/test_readline6.txt"); // file to upload
+ var dest = do_get_tempdir(); // file upload destination
+ dest.append("junk.dat");
+ dest.createUnique(dest.NORMAL_FILE_TYPE, 0o600);
+
+ var uploadstream = new_file_input_stream(file, true);
+
+ var chan = new_file_channel(dest);
+ chan.QueryInterface(Ci.nsIUploadChannel);
+ chan.setUploadStream(uploadstream, "", file.fileSize);
+
+ function on_upload_complete(data) {
+ dump("*** test_upload_file.on_upload_complete\n");
+
+ // bug 326693
+ if (chan.contentType != special_type) {
+ do_throw(
+ "Type mismatch! Is <" +
+ chan.contentType +
+ ">, should be <" +
+ special_type +
+ ">"
+ );
+ }
+
+ /* upload of file completed successfully. */
+ if (data.length != 0) {
+ do_throw("Upload resulted in data!");
+ }
+
+ var oldstream = new_file_input_stream(file, false);
+ var newstream = new_file_input_stream(dest, false);
+ var olddata = read_stream(oldstream, oldstream.available());
+ var newdata = read_stream(newstream, newstream.available());
+ if (olddata != newdata) {
+ do_throw("Stream contents do not match after file copy!");
+ }
+ oldstream.close();
+ newstream.close();
+
+ /* cleanup... also ensures that the destination file is not in
+ use when OnStopRequest is called. */
+ try {
+ dest.remove(false);
+ } catch (e) {
+ dump(e + "\n");
+ do_throw("Unable to remove uploaded file!\n");
+ }
+
+ run_next_test();
+ }
+
+ chan.contentType = special_type;
+ chan.asyncOpen(new FileStreamListener(on_upload_complete));
+}
+
+function test_load_shelllink() {
+ // lnk files should not resolve to their targets
+ dump("*** test_load_shelllink\n");
+ let file = do_get_file("data/system_root.lnk", false);
+ var chan = new_file_channel(file);
+
+ // The original URI path should be the same as the URI path
+ Assert.equal(chan.URI.pathQueryRef, chan.originalURI.pathQueryRef);
+
+ // The original URI path should be the same as the lnk file path
+ var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
+ Assert.equal(
+ chan.originalURI.pathQueryRef,
+ ios.newFileURI(file).pathQueryRef
+ );
+ run_next_test();
+}
diff --git a/netwerk/test/unit/test_filestreams.js b/netwerk/test/unit/test_filestreams.js
new file mode 100644
index 0000000000..d8beec0b8a
--- /dev/null
+++ b/netwerk/test/unit/test_filestreams.js
@@ -0,0 +1,306 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// We need the profile directory so the test harness will clean up our test
+// files.
+do_get_profile();
+
+const OUTPUT_STREAM_CONTRACT_ID = "@mozilla.org/network/file-output-stream;1";
+const SAFE_OUTPUT_STREAM_CONTRACT_ID =
+ "@mozilla.org/network/safe-file-output-stream;1";
+
+////////////////////////////////////////////////////////////////////////////////
+//// Helper Methods
+
+/**
+ * Generates a leafName for a file that does not exist, but does *not*
+ * create the file. Similar to createUnique except for the fact that createUnique
+ * does create the file.
+ *
+ * @param aFile
+ * The file to modify in order for it to have a unique leafname.
+ */
+function ensure_unique(aFile) {
+ ensure_unique.fileIndex = ensure_unique.fileIndex || 0;
+
+ var leafName = aFile.leafName;
+ while (aFile.clone().exists()) {
+ aFile.leafName = leafName + "_" + ensure_unique.fileIndex++;
+ }
+}
+
+/**
+ * Tests for files being accessed at the right time. Streams that use
+ * DEFER_OPEN should only open or create the file when an operation is
+ * done, and not during Init().
+ *
+ * Note that for writing, we check for actual writing in test_NetUtil (async)
+ * and in sync_operations in this file (sync), whereas in this function we
+ * just check that the file is *not* created during init.
+ *
+ * @param aContractId
+ * The contract ID to use for the output stream
+ * @param aDeferOpen
+ * Whether to check with DEFER_OPEN or not
+ * @param aTrickDeferredOpen
+ * Whether we try to 'trick' deferred opens by changing the file object before
+ * the actual open. The stream should have a clone, so changes to the file
+ * object after Init and before Open should not affect it.
+ */
+function check_access(aContractId, aDeferOpen, aTrickDeferredOpen) {
+ const LEAF_NAME = "filestreams-test-file.tmp";
+ const TRICKY_LEAF_NAME = "BetYouDidNotExpectThat.tmp";
+ let file = Cc["@mozilla.org/file/directory_service;1"]
+ .getService(Ci.nsIProperties)
+ .get("ProfD", Ci.nsIFile);
+ file.append(LEAF_NAME);
+
+ // Writing
+
+ ensure_unique(file);
+ let ostream = Cc[aContractId].createInstance(Ci.nsIFileOutputStream);
+ ostream.init(
+ file,
+ -1,
+ -1,
+ aDeferOpen ? Ci.nsIFileOutputStream.DEFER_OPEN : 0
+ );
+ Assert.equal(aDeferOpen, !file.clone().exists()); // If defer, should not exist and vice versa
+ if (aDeferOpen) {
+ // File should appear when we do write to it.
+ if (aTrickDeferredOpen) {
+ // See |@param aDeferOpen| in the JavaDoc comment for this function
+ file.leafName = TRICKY_LEAF_NAME;
+ }
+ ostream.write("data", 4);
+ if (aTrickDeferredOpen) {
+ file.leafName = LEAF_NAME;
+ }
+ // We did a write, so the file should now exist
+ Assert.ok(file.clone().exists());
+ }
+ ostream.close();
+
+ // Reading
+
+ ensure_unique(file);
+ let istream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(
+ Ci.nsIFileInputStream
+ );
+ var initOk, getOk;
+ try {
+ istream.init(
+ file,
+ -1,
+ 0,
+ aDeferOpen ? Ci.nsIFileInputStream.DEFER_OPEN : 0
+ );
+ initOk = true;
+ } catch (e) {
+ initOk = false;
+ }
+ try {
+ let fstream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(
+ Ci.nsIFileInputStream
+ );
+ fstream.init(file, -1, 0, 0);
+ getOk = true;
+ } catch (e) {
+ getOk = false;
+ }
+
+ // If the open is deferred, then Init should succeed even though the file we
+ // intend to read does not exist, and then trying to read from it should
+ // fail. The other case is where the open is not deferred, and there we should
+ // get an error when we Init (and also when we try to read).
+ Assert.ok(
+ (aDeferOpen && initOk && !getOk) || (!aDeferOpen && !initOk && !getOk)
+ );
+ istream.close();
+}
+
+/**
+ * We test async operations in test_NetUtil.js, and here test for simple sync
+ * operations on input streams.
+ *
+ * @param aDeferOpen
+ * Whether to use DEFER_OPEN in the streams.
+ */
+function sync_operations(aDeferOpen) {
+ const TEST_DATA = "this is a test string";
+ const LEAF_NAME = "filestreams-test-file.tmp";
+
+ let file = Cc["@mozilla.org/file/directory_service;1"]
+ .getService(Ci.nsIProperties)
+ .get("ProfD", Ci.nsIFile);
+ file.append(LEAF_NAME);
+ file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o666);
+
+ let ostream = Cc[OUTPUT_STREAM_CONTRACT_ID].createInstance(
+ Ci.nsIFileOutputStream
+ );
+ ostream.init(
+ file,
+ -1,
+ -1,
+ aDeferOpen ? Ci.nsIFileOutputStream.DEFER_OPEN : 0
+ );
+
+ ostream.write(TEST_DATA, TEST_DATA.length);
+ ostream.close();
+
+ let fstream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(
+ Ci.nsIFileInputStream
+ );
+ fstream.init(file, -1, 0, aDeferOpen ? Ci.nsIFileInputStream.DEFER_OPEN : 0);
+
+ let cstream = Cc["@mozilla.org/intl/converter-input-stream;1"].createInstance(
+ Ci.nsIConverterInputStream
+ );
+ cstream.init(fstream, "UTF-8", 0, 0);
+
+ let string = {};
+ cstream.readString(-1, string);
+ cstream.close();
+ fstream.close();
+
+ Assert.equal(string.value, TEST_DATA);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+//// Tests
+
+function test_access() {
+ check_access(OUTPUT_STREAM_CONTRACT_ID, false, false);
+}
+
+function test_access_trick() {
+ check_access(OUTPUT_STREAM_CONTRACT_ID, false, true);
+}
+
+function test_access_defer() {
+ check_access(OUTPUT_STREAM_CONTRACT_ID, true, false);
+}
+
+function test_access_defer_trick() {
+ check_access(OUTPUT_STREAM_CONTRACT_ID, true, true);
+}
+
+function test_access_safe() {
+ check_access(SAFE_OUTPUT_STREAM_CONTRACT_ID, false, false);
+}
+
+function test_access_safe_trick() {
+ check_access(SAFE_OUTPUT_STREAM_CONTRACT_ID, false, true);
+}
+
+function test_access_safe_defer() {
+ check_access(SAFE_OUTPUT_STREAM_CONTRACT_ID, true, false);
+}
+
+function test_access_safe_defer_trick() {
+ check_access(SAFE_OUTPUT_STREAM_CONTRACT_ID, true, true);
+}
+
+function test_sync_operations() {
+ sync_operations();
+}
+
+function test_sync_operations_deferred() {
+ sync_operations(true);
+}
+
+function do_test_zero_size_buffered(disableBuffering) {
+ const LEAF_NAME = "filestreams-test-file.tmp";
+ const BUFFERSIZE = 4096;
+
+ let file = Cc["@mozilla.org/file/directory_service;1"]
+ .getService(Ci.nsIProperties)
+ .get("ProfD", Ci.nsIFile);
+ file.append(LEAF_NAME);
+ file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o666);
+
+ let fstream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(
+ Ci.nsIFileInputStream
+ );
+ fstream.init(
+ file,
+ -1,
+ 0,
+ Ci.nsIFileInputStream.CLOSE_ON_EOF | Ci.nsIFileInputStream.REOPEN_ON_REWIND
+ );
+
+ var buffered = Cc[
+ "@mozilla.org/network/buffered-input-stream;1"
+ ].createInstance(Ci.nsIBufferedInputStream);
+ buffered.init(fstream, BUFFERSIZE);
+
+ if (disableBuffering) {
+ buffered.QueryInterface(Ci.nsIStreamBufferAccess).disableBuffering();
+ }
+
+ // Scriptable input streams clamp read sizes to the return value of
+ // available(), so don't quite do what we want here.
+ let cstream = Cc["@mozilla.org/intl/converter-input-stream;1"].createInstance(
+ Ci.nsIConverterInputStream
+ );
+ cstream.init(buffered, "UTF-8", 0, 0);
+
+ Assert.equal(buffered.available(), 0);
+
+ // Now try reading from this stream
+ let string = {};
+ Assert.equal(cstream.readString(BUFFERSIZE, string), 0);
+ Assert.equal(string.value, "");
+
+ // Now check that available() throws
+ var exceptionThrown = false;
+ try {
+ Assert.equal(buffered.available(), 0);
+ } catch (e) {
+ exceptionThrown = true;
+ }
+ Assert.ok(exceptionThrown);
+
+ // OK, now seek back to start
+ buffered.seek(Ci.nsISeekableStream.NS_SEEK_SET, 0);
+
+ // Now check that available() does not throw
+ exceptionThrown = false;
+ try {
+ Assert.equal(buffered.available(), 0);
+ } catch (e) {
+ exceptionThrown = true;
+ }
+ Assert.ok(!exceptionThrown);
+}
+
+function test_zero_size_buffered() {
+ do_test_zero_size_buffered(false);
+ do_test_zero_size_buffered(true);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+//// Test Runner
+
+var tests = [
+ test_access,
+ test_access_trick,
+ test_access_defer,
+ test_access_defer_trick,
+ test_access_safe,
+ test_access_safe_trick,
+ test_access_safe_defer,
+ test_access_safe_defer_trick,
+ test_sync_operations,
+ test_sync_operations_deferred,
+ test_zero_size_buffered,
+];
+
+function run_test() {
+ tests.forEach(function(test) {
+ test();
+ });
+}
diff --git a/netwerk/test/unit/test_freshconnection.js b/netwerk/test/unit/test_freshconnection.js
new file mode 100644
index 0000000000..5d0f5bc5b7
--- /dev/null
+++ b/netwerk/test/unit/test_freshconnection.js
@@ -0,0 +1,30 @@
+// This is essentially a debug mode crashtest to make sure everything
+// involved in a reload runs on the right thread. It relies on the
+// assertions in necko.
+
+"use strict";
+
+var listener = {
+ onStartRequest: function test_onStartR(request) {},
+
+ onDataAvailable: function test_ODA() {
+ do_throw("Should not get any data!");
+ },
+
+ onStopRequest: function test_onStopR(request, status) {
+ do_test_finished();
+ },
+};
+
+function run_test() {
+ var chan = NetUtil.newChannel({
+ uri: "http://localhost:4444",
+ loadUsingSystemPrincipal: true,
+ });
+ chan.loadFlags =
+ Ci.nsIRequest.LOAD_FRESH_CONNECTION |
+ Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI;
+ chan.QueryInterface(Ci.nsIHttpChannel);
+ chan.asyncOpen(listener);
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_getHost.js b/netwerk/test/unit/test_getHost.js
new file mode 100644
index 0000000000..90db800575
--- /dev/null
+++ b/netwerk/test/unit/test_getHost.js
@@ -0,0 +1,63 @@
+// Test getLocalHost/getLocalPort and getRemoteHost/getRemotePort.
+
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+var httpserver = new HttpServer();
+httpserver.start(-1);
+const PORT = httpserver.identity.primaryPort;
+
+var gotOnStartRequest = false;
+
+function CheckGetHostListener() {}
+
+CheckGetHostListener.prototype = {
+ onStartRequest(request) {
+ dump("*** listener onStartRequest\n");
+
+ gotOnStartRequest = true;
+
+ request.QueryInterface(Ci.nsIHttpChannelInternal);
+ try {
+ Assert.equal(request.localAddress, "127.0.0.1");
+ Assert.equal(request.localPort > 0, true);
+ Assert.notEqual(request.localPort, PORT);
+ Assert.equal(request.remoteAddress, "127.0.0.1");
+ Assert.equal(request.remotePort, PORT);
+ } catch (e) {
+ Assert.ok(0, "Get local/remote host/port throws an error!");
+ }
+ },
+
+ onStopRequest(request, statusCode) {
+ dump("*** listener onStopRequest\n");
+
+ Assert.equal(gotOnStartRequest, true);
+ httpserver.stop(do_test_finished);
+ },
+
+ QueryInterface: ChromeUtils.generateQI(["nsIRequestObserver"]),
+};
+
+function make_channel(url) {
+ return NetUtil.newChannel({
+ uri: url,
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+}
+
+function test_handler(metadata, response) {
+ response.setHeader("Content-Type", "text/html", false);
+ response.setStatusLine(metadata.httpVersion, 200, "OK");
+ var responseBody = "blah blah";
+ response.bodyOutputStream.write(responseBody, responseBody.length);
+}
+
+function run_test() {
+ httpserver.registerPathHandler("/testdir", test_handler);
+
+ var channel = make_channel("http://localhost:" + PORT + "/testdir");
+ channel.asyncOpen(new CheckGetHostListener());
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_gre_resources.js b/netwerk/test/unit/test_gre_resources.js
new file mode 100644
index 0000000000..098dae660d
--- /dev/null
+++ b/netwerk/test/unit/test_gre_resources.js
@@ -0,0 +1,32 @@
+// test that things that are expected to be in gre-resources are still there
+
+"use strict";
+
+var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
+
+function wrapInputStream(input) {
+ var nsIScriptableInputStream = Ci.nsIScriptableInputStream;
+ var factory = Cc["@mozilla.org/scriptableinputstream;1"];
+ var wrapper = factory.createInstance(nsIScriptableInputStream);
+ wrapper.init(input);
+ return wrapper;
+}
+
+function check_file(file) {
+ var channel = NetUtil.newChannel({
+ uri: "resource://gre-resources/" + file,
+ loadUsingSystemPrincipal: true,
+ });
+ try {
+ let instr = wrapInputStream(channel.open());
+ Assert.ok(instr.read(1024).length > 0);
+ } catch (e) {
+ do_throw("Failed to read " + file + " from gre-resources:" + e);
+ }
+}
+
+function run_test() {
+ for (let file of ["ua.css"]) {
+ check_file(file);
+ }
+}
diff --git a/netwerk/test/unit/test_gzipped_206.js b/netwerk/test/unit/test_gzipped_206.js
new file mode 100644
index 0000000000..a60bef3d26
--- /dev/null
+++ b/netwerk/test/unit/test_gzipped_206.js
@@ -0,0 +1,167 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+var httpserver = null;
+
+// testString = "This is a slightly longer test\n";
+const responseBody = [
+ 0x1f,
+ 0x8b,
+ 0x08,
+ 0x08,
+ 0xef,
+ 0x70,
+ 0xe6,
+ 0x4c,
+ 0x00,
+ 0x03,
+ 0x74,
+ 0x65,
+ 0x78,
+ 0x74,
+ 0x66,
+ 0x69,
+ 0x6c,
+ 0x65,
+ 0x2e,
+ 0x74,
+ 0x78,
+ 0x74,
+ 0x00,
+ 0x0b,
+ 0xc9,
+ 0xc8,
+ 0x2c,
+ 0x56,
+ 0x00,
+ 0xa2,
+ 0x44,
+ 0x85,
+ 0xe2,
+ 0x9c,
+ 0xcc,
+ 0xf4,
+ 0x8c,
+ 0x92,
+ 0x9c,
+ 0x4a,
+ 0x85,
+ 0x9c,
+ 0xfc,
+ 0xbc,
+ 0xf4,
+ 0xd4,
+ 0x22,
+ 0x85,
+ 0x92,
+ 0xd4,
+ 0xe2,
+ 0x12,
+ 0x2e,
+ 0x2e,
+ 0x00,
+ 0x00,
+ 0xe5,
+ 0xe6,
+ 0xf0,
+ 0x20,
+ 0x00,
+ 0x00,
+ 0x00,
+];
+
+function make_channel(url, callback, ctx) {
+ return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true });
+}
+
+var doRangeResponse = false;
+
+function cachedHandler(metadata, response) {
+ response.setHeader("Content-Type", "application/x-gzip", false);
+ response.setHeader("Content-Encoding", "gzip", false);
+ response.setHeader("ETag", "Just testing");
+ response.setHeader("Cache-Control", "max-age=3600000"); // avoid validation
+
+ var body = responseBody;
+
+ if (doRangeResponse) {
+ Assert.ok(metadata.hasHeader("Range"));
+ var matches = metadata
+ .getHeader("Range")
+ .match(/^\s*bytes=(\d+)?-(\d+)?\s*$/);
+ var from = matches[1] === undefined ? 0 : matches[1];
+ var to = matches[2] === undefined ? responseBody.length - 1 : matches[2];
+ if (from >= responseBody.length) {
+ response.setStatusLine(metadata.httpVersion, 416, "Start pos too high");
+ response.setHeader("Content-Range", "*/" + responseBody.length, false);
+ return;
+ }
+ body = body.slice(from, to + 1);
+ response.setHeader("Content-Length", "" + (to + 1 - from));
+ // always respond to successful range requests with 206
+ response.setStatusLine(metadata.httpVersion, 206, "Partial Content");
+ response.setHeader(
+ "Content-Range",
+ from + "-" + to + "/" + responseBody.length,
+ false
+ );
+ } else {
+ // This response will get cut off prematurely
+ response.setHeader("Content-Length", "" + responseBody.length);
+ response.setHeader("Accept-Ranges", "bytes");
+ body = body.slice(0, 17); // slice off a piece to send first
+ doRangeResponse = true;
+ }
+
+ var bos = Cc["@mozilla.org/binaryoutputstream;1"].createInstance(
+ Ci.nsIBinaryOutputStream
+ );
+ bos.setOutputStream(response.bodyOutputStream);
+
+ response.processAsync();
+ bos.writeByteArray(body);
+ response.finish();
+}
+
+function continue_test(request, data) {
+ Assert.equal(17, data.length);
+ var chan = make_channel(
+ "http://localhost:" + httpserver.identity.primaryPort + "/cached/test.gz"
+ );
+ chan.asyncOpen(new ChannelListener(finish_test, null, CL_EXPECT_GZIP));
+}
+
+var enforcePref;
+
+function finish_test(request, data, ctx) {
+ Assert.equal(request.status, 0);
+ Assert.equal(data.length, responseBody.length);
+ for (var i = 0; i < data.length; ++i) {
+ Assert.equal(data.charCodeAt(i), responseBody[i]);
+ }
+ Services.prefs.setBoolPref("network.http.enforce-framing.http1", enforcePref);
+ httpserver.stop(do_test_finished);
+}
+
+function run_test() {
+ enforcePref = Services.prefs.getBoolPref(
+ "network.http.enforce-framing.http1"
+ );
+ Services.prefs.setBoolPref("network.http.enforce-framing.http1", false);
+
+ httpserver = new HttpServer();
+ httpserver.registerPathHandler("/cached/test.gz", cachedHandler);
+ httpserver.start(-1);
+
+ // wipe out cached content
+ evict_cache_entries();
+
+ var chan = make_channel(
+ "http://localhost:" + httpserver.identity.primaryPort + "/cached/test.gz"
+ );
+ chan.asyncOpen(
+ new ChannelListener(continue_test, null, CL_EXPECT_GZIP | CL_IGNORE_CL)
+ );
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_head.js b/netwerk/test/unit/test_head.js
new file mode 100644
index 0000000000..c25b3e3dbc
--- /dev/null
+++ b/netwerk/test/unit/test_head.js
@@ -0,0 +1,169 @@
+//
+// HTTP headers test
+//
+
+// Note: sets Cc and Ci variables
+
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+const ReferrerInfo = Components.Constructor(
+ "@mozilla.org/referrer-info;1",
+ "nsIReferrerInfo",
+ "init"
+);
+
+XPCOMUtils.defineLazyGetter(this, "URL", function() {
+ return "http://localhost:" + httpserver.identity.primaryPort;
+});
+
+var httpserver = new HttpServer();
+var testpath = "/simple";
+var httpbody = "0123456789";
+var channel;
+var ios;
+
+var dbg = 0;
+if (dbg) {
+ print("============== START ==========");
+}
+
+function run_test() {
+ setup_test();
+ do_test_pending();
+}
+
+function setup_test() {
+ if (dbg) {
+ print("============== setup_test: in");
+ }
+
+ httpserver.registerPathHandler(testpath, serverHandler);
+ httpserver.start(-1);
+
+ channel = setupChannel(testpath);
+
+ channel.setRequestHeader("ReplaceMe", "initial value", true);
+ var setOK = channel.getRequestHeader("ReplaceMe");
+ Assert.equal(setOK, "initial value");
+ channel.setRequestHeader("ReplaceMe", "replaced", false);
+ setOK = channel.getRequestHeader("ReplaceMe");
+ Assert.equal(setOK, "replaced");
+
+ channel.setRequestHeader("MergeMe", "foo1", true);
+ channel.setRequestHeader("MergeMe", "foo2", true);
+ channel.setRequestHeader("MergeMe", "foo3", true);
+ setOK = channel.getRequestHeader("MergeMe");
+ Assert.equal(setOK, "foo1, foo2, foo3");
+
+ channel.setEmptyRequestHeader("Empty");
+ setOK = channel.getRequestHeader("Empty");
+ Assert.equal(setOK, "");
+
+ channel.setRequestHeader("ReplaceWithEmpty", "initial value", true);
+ setOK = channel.getRequestHeader("ReplaceWithEmpty");
+ Assert.equal(setOK, "initial value");
+ channel.setEmptyRequestHeader("ReplaceWithEmpty");
+ setOK = channel.getRequestHeader("ReplaceWithEmpty");
+ Assert.equal(setOK, "");
+
+ channel.setEmptyRequestHeader("MergeWithEmpty");
+ setOK = channel.getRequestHeader("MergeWithEmpty");
+ Assert.equal(setOK, "");
+ channel.setRequestHeader("MergeWithEmpty", "foo", true);
+ setOK = channel.getRequestHeader("MergeWithEmpty");
+ Assert.equal(setOK, "foo");
+
+ var uri = NetUtil.newURI("http://foo1.invalid:80");
+ channel.referrerInfo = new ReferrerInfo(Ci.nsIReferrerInfo.EMPTY, true, uri);
+ setOK = channel.getRequestHeader("Referer");
+ Assert.equal(setOK, "http://foo1.invalid/");
+
+ uri = NetUtil.newURI("http://foo2.invalid:90/bar");
+ channel.referrerInfo = new ReferrerInfo(Ci.nsIReferrerInfo.EMPTY, true, uri);
+ setOK = channel.getRequestHeader("Referer");
+ Assert.equal(setOK, "http://foo2.invalid:90/bar");
+
+ // ChannelListener defined in head_channels.js
+ channel.asyncOpen(new ChannelListener(checkRequestResponse, channel));
+
+ if (dbg) {
+ print("============== setup_test: out");
+ }
+}
+
+function setupChannel(path) {
+ var chan = NetUtil.newChannel({
+ uri: URL + path,
+ loadUsingSystemPrincipal: true,
+ });
+ chan.QueryInterface(Ci.nsIHttpChannel);
+ chan.requestMethod = "GET";
+ return chan;
+}
+
+function serverHandler(metadata, response) {
+ if (dbg) {
+ print("============== serverHandler: in");
+ }
+
+ var setOK = metadata.getHeader("ReplaceMe");
+ Assert.equal(setOK, "replaced");
+ setOK = metadata.getHeader("MergeMe");
+ Assert.equal(setOK, "foo1, foo2, foo3");
+ setOK = metadata.getHeader("Empty");
+ Assert.equal(setOK, "");
+ setOK = metadata.getHeader("ReplaceWithEmpty");
+ Assert.equal(setOK, "");
+ setOK = metadata.getHeader("MergeWithEmpty");
+ Assert.equal(setOK, "foo");
+ setOK = metadata.getHeader("Referer");
+ Assert.equal(setOK, "http://foo2.invalid:90/bar");
+
+ response.setHeader("Content-Type", "text/plain", false);
+ response.setStatusLine("1.1", 200, "OK");
+
+ // note: httpd.js' "Response" class uses ',' (no space) for merge.
+ response.setHeader("httpdMerge", "bar1", false);
+ response.setHeader("httpdMerge", "bar2", true);
+ response.setHeader("httpdMerge", "bar3", true);
+ // Some special headers like Proxy-Authenticate merge with \n
+ response.setHeader("Proxy-Authenticate", "line 1", true);
+ response.setHeader("Proxy-Authenticate", "line 2", true);
+ response.setHeader("Proxy-Authenticate", "line 3", true);
+
+ response.bodyOutputStream.write(httpbody, httpbody.length);
+
+ if (dbg) {
+ print("============== serverHandler: out");
+ }
+}
+
+function checkRequestResponse(request, data, context) {
+ if (dbg) {
+ print("============== checkRequestResponse: in");
+ }
+
+ Assert.equal(channel.responseStatus, 200);
+ Assert.equal(channel.responseStatusText, "OK");
+ Assert.ok(channel.requestSucceeded);
+
+ var response = channel.getResponseHeader("httpdMerge");
+ Assert.equal(response, "bar1,bar2,bar3");
+ channel.setResponseHeader("httpdMerge", "bar", true);
+ Assert.equal(channel.getResponseHeader("httpdMerge"), "bar1,bar2,bar3, bar");
+
+ response = channel.getResponseHeader("Proxy-Authenticate");
+ Assert.equal(response, "line 1\nline 2\nline 3");
+
+ channel.contentCharset = "UTF-8";
+ Assert.equal(channel.contentCharset, "UTF-8");
+ Assert.equal(channel.contentType, "text/plain");
+ Assert.equal(channel.contentLength, httpbody.length);
+ Assert.equal(data, httpbody);
+
+ httpserver.stop(do_test_finished);
+ if (dbg) {
+ print("============== checkRequestResponse: out");
+ }
+}
diff --git a/netwerk/test/unit/test_head_request_no_response_body.js b/netwerk/test/unit/test_head_request_no_response_body.js
new file mode 100644
index 0000000000..df319cbaee
--- /dev/null
+++ b/netwerk/test/unit/test_head_request_no_response_body.js
@@ -0,0 +1,76 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*
+
+Test that a response to HEAD method should not have a body.
+1. Create a GET request and write the response into cache.
+2. Create the second GET request with the same URI and see if the response is
+ from cache.
+3. Create a HEAD request and test if we got a response with an empty body.
+
+*/
+
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+const responseContent = "response body";
+
+function test_handler(metadata, response) {
+ response.setHeader("Content-Type", "text/html", false);
+ response.setHeader("Cache-control", "max-age=9999", false);
+ response.setStatusLine(metadata.httpVersion, 200, "OK");
+ if (metadata.method != "HEAD") {
+ response.bodyOutputStream.write(responseContent, responseContent.length);
+ }
+}
+
+function make_channel(url, method) {
+ let channel = NetUtil.newChannel({
+ uri: url,
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+ channel.requestMethod = method;
+ return channel;
+}
+
+async function get_response(channel, fromCache) {
+ return new Promise(resolve => {
+ channel.asyncOpen(
+ new ChannelListener((request, buffer, ctx, isFromCache) => {
+ ok(fromCache == isFromCache, `got response from cache = ${fromCache}`);
+ resolve(buffer);
+ })
+ );
+ });
+}
+
+async function stop_server(httpserver) {
+ return new Promise(resolve => {
+ httpserver.stop(resolve);
+ });
+}
+
+add_task(async function() {
+ let httpserver = new HttpServer();
+ httpserver.registerPathHandler("/testdir", test_handler);
+ httpserver.start(-1);
+ const PORT = httpserver.identity.primaryPort;
+ const URI = `http://localhost:${PORT}/testdir`;
+
+ let response;
+
+ response = await get_response(make_channel(URI, "GET"), false);
+ ok(response === responseContent, "got response body");
+
+ response = await get_response(make_channel(URI, "GET"), true);
+ ok(response === responseContent, "got response body from cache");
+
+ response = await get_response(make_channel(URI, "HEAD"), false);
+ ok(response === "", "should have empty body");
+
+ await stop_server(httpserver);
+});
diff --git a/netwerk/test/unit/test_header_Accept-Language.js b/netwerk/test/unit/test_header_Accept-Language.js
new file mode 100644
index 0000000000..f3b3571992
--- /dev/null
+++ b/netwerk/test/unit/test_header_Accept-Language.js
@@ -0,0 +1,101 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+//
+// HTTP Accept-Language header test
+//
+
+"use strict";
+
+var testpath = "/bug672448";
+
+function run_test() {
+ let intlPrefs = Cc["@mozilla.org/preferences-service;1"]
+ .getService(Ci.nsIPrefService)
+ .getBranch("intl.");
+
+ // Save old value of preference for later.
+ let oldPref = intlPrefs.getCharPref("accept_languages");
+
+ // Test different numbers of languages, to test different fractions.
+ let acceptLangTests = [
+ "qaa", // 1
+ "qaa,qab", // 2
+ "qaa,qab,qac,qad", // 4
+ "qaa,qab,qac,qad,qae,qaf,qag,qah", // 8
+ "qaa,qab,qac,qad,qae,qaf,qag,qah,qai,qaj", // 10
+ "qaa,qab,qac,qad,qae,qaf,qag,qah,qai,qaj,qak", // 11
+ "qaa,qab,qac,qad,qae,qaf,qag,qah,qai,qaj,qak,qal,qam,qan,qao,qap,qaq,qar,qas,qat,qau", // 21
+ oldPref, // Restore old value of preference (and test it).
+ ];
+
+ let acceptLangTestsNum = acceptLangTests.length;
+
+ for (let i = 0; i < acceptLangTestsNum; i++) {
+ // Set preference to test value.
+ intlPrefs.setCharPref("accept_languages", acceptLangTests[i]);
+
+ // Test value.
+ test_accepted_languages();
+ }
+}
+
+function test_accepted_languages() {
+ let channel = setupChannel(testpath);
+
+ let AcceptLanguage = channel.getRequestHeader("Accept-Language");
+
+ let acceptedLanguages = AcceptLanguage.split(",");
+
+ let acceptedLanguagesLength = acceptedLanguages.length;
+
+ for (let i = 0; i < acceptedLanguagesLength; i++) {
+ let qualityValue;
+
+ try {
+ // The q-value must conform to the definition in HTTP/1.1 Section 3.9.
+ [, , qualityValue] = acceptedLanguages[i]
+ .trim()
+ .match(/^([a-z0-9_-]*?)(?:;q=(1(?:\.0{0,3})?|0(?:\.[0-9]{0,3})))?$/i);
+ } catch (e) {
+ do_throw("Invalid language tag or quality value: " + e);
+ }
+
+ if (i == 0) {
+ // The first language shouldn't have a quality value.
+ Assert.equal(qualityValue, undefined);
+ } else {
+ let decimalPlaces;
+
+ // When the number of languages is small, we keep the quality value to only one decimal place.
+ // Otherwise, it can be up to two decimal places.
+ if (acceptedLanguagesLength < 10) {
+ Assert.ok(qualityValue.length == 3);
+
+ decimalPlaces = 1;
+ } else {
+ Assert.ok(qualityValue.length >= 3);
+ Assert.ok(qualityValue.length <= 4);
+
+ decimalPlaces = 2;
+ }
+
+ // All the other languages should have an evenly-spaced quality value.
+ Assert.equal(
+ parseFloat(qualityValue).toFixed(decimalPlaces),
+ (1.0 - (1 / acceptedLanguagesLength) * i).toFixed(decimalPlaces)
+ );
+ }
+ }
+}
+
+function setupChannel(path) {
+ let chan = NetUtil.newChannel({
+ uri: "http://localhost:4444" + path,
+ loadUsingSystemPrincipal: true,
+ });
+
+ chan.QueryInterface(Ci.nsIHttpChannel);
+ return chan;
+}
diff --git a/netwerk/test/unit/test_header_Accept-Language_case.js b/netwerk/test/unit/test_header_Accept-Language_case.js
new file mode 100644
index 0000000000..bbad7c9879
--- /dev/null
+++ b/netwerk/test/unit/test_header_Accept-Language_case.js
@@ -0,0 +1,53 @@
+"use strict";
+
+var testpath = "/bug1054739";
+
+function run_test() {
+ let intlPrefs = Cc["@mozilla.org/preferences-service;1"]
+ .getService(Ci.nsIPrefService)
+ .getBranch("intl.");
+
+ let oldAcceptLangPref = intlPrefs.getCharPref("accept_languages");
+
+ let testData = [
+ ["en", "en"],
+ ["ast", "ast"],
+ ["fr-ca", "fr-CA"],
+ ["zh-yue", "zh-yue"],
+ ["az-latn", "az-Latn"],
+ ["sl-nedis", "sl-nedis"],
+ ["zh-hant-hk", "zh-Hant-HK"],
+ ["ZH-HANT-HK", "zh-Hant-HK"],
+ ["en-us-x-priv", "en-US-x-priv"],
+ ["en-us-x-twain", "en-US-x-twain"],
+ ["de, en-US, en", "de,en-US;q=0.7,en;q=0.3"],
+ ["de,en-us,en", "de,en-US;q=0.7,en;q=0.3"],
+ ["en-US, en", "en-US,en;q=0.5"],
+ ["EN-US;q=0.2, EN", "en-US,en;q=0.5"],
+ ["en ;q=0.8, de ", "en,de;q=0.5"],
+ [",en,", "en"],
+ ];
+
+ for (let i = 0; i < testData.length; i++) {
+ let acceptLangPref = testData[i][0];
+ let expectedHeader = testData[i][1];
+
+ intlPrefs.setCharPref("accept_languages", acceptLangPref);
+ let acceptLangHeader = setupChannel(testpath).getRequestHeader(
+ "Accept-Language"
+ );
+ equal(acceptLangHeader, expectedHeader);
+ }
+
+ intlPrefs.setCharPref("accept_languages", oldAcceptLangPref);
+}
+
+function setupChannel(path) {
+ let uri = NetUtil.newURI("http://localhost:4444" + path);
+ let chan = NetUtil.newChannel({
+ uri,
+ loadUsingSystemPrincipal: true,
+ });
+ chan.QueryInterface(Ci.nsIHttpChannel);
+ return chan;
+}
diff --git a/netwerk/test/unit/test_header_Server_Timing.js b/netwerk/test/unit/test_header_Server_Timing.js
new file mode 100644
index 0000000000..c4a3f6e336
--- /dev/null
+++ b/netwerk/test/unit/test_header_Server_Timing.js
@@ -0,0 +1,67 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+//
+// HTTP Server-Timing header test
+//
+
+"use strict";
+
+function make_and_open_channel(url, callback) {
+ let chan = NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true });
+ chan.asyncOpen(new ChannelListener(callback, null, CL_ALLOW_UNKNOWN_CL));
+}
+
+var responseServerTiming = [
+ { metric: "metric", duration: "123.4", description: "description" },
+ { metric: "metric2", duration: "456.78", description: "description1" },
+];
+var trailerServerTiming = [
+ { metric: "metric3", duration: "789.11", description: "description2" },
+ { metric: "metric4", duration: "1112.13", description: "description3" },
+];
+
+function run_test() {
+ do_test_pending();
+
+ // Set up to allow the cert presented by the server
+ do_get_profile();
+ let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(
+ Ci.nsIX509CertDB
+ );
+ addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u");
+
+ Services.prefs.setCharPref("network.dns.localDomains", "foo.example.com");
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("network.dns.localDomains");
+ });
+
+ var env = Cc["@mozilla.org/process/environment;1"].getService(
+ Ci.nsIEnvironment
+ );
+ var serverPort = env.get("MOZHTTP2_PORT");
+ make_and_open_channel(
+ "https://foo.example.com:" + serverPort + "/server-timing",
+ readServerContent
+ );
+}
+
+function checkServerTimingContent(headers) {
+ var expectedResult = responseServerTiming.concat(trailerServerTiming);
+ Assert.equal(headers.length, expectedResult.length);
+
+ for (var i = 0; i < expectedResult.length; i++) {
+ let header = headers.queryElementAt(i, Ci.nsIServerTiming);
+ Assert.equal(header.name, expectedResult[i].metric);
+ Assert.equal(header.description, expectedResult[i].description);
+ Assert.equal(header.duration, parseFloat(expectedResult[i].duration));
+ }
+}
+
+function readServerContent(request, buffer) {
+ let channel = request.QueryInterface(Ci.nsITimedChannel);
+ let headers = channel.serverTiming.QueryInterface(Ci.nsIArray);
+ checkServerTimingContent(headers);
+ do_test_finished();
+}
diff --git a/netwerk/test/unit/test_headers.js b/netwerk/test/unit/test_headers.js
new file mode 100644
index 0000000000..baaacdf17b
--- /dev/null
+++ b/netwerk/test/unit/test_headers.js
@@ -0,0 +1,175 @@
+//
+// cleaner HTTP header test infrastructure
+//
+// tests bugs: 589292, [add more here: see hg log for definitive list]
+//
+// TO ADD NEW TESTS:
+// 1) Increment up 'lastTest' to new number (say, "99")
+// 2) Add new test 'handler99' and 'completeTest99' functions.
+// 3) If your test should fail the necko channel, set
+// test_flags[99] = CL_EXPECT_FAILURE.
+//
+// TO DEBUG JUST ONE TEST: temporarily change firstTest and lastTest to equal
+// the test # you're interested in.
+//
+// For tests that need duplicate copies of headers to be sent, see
+// test_duplicate_headers.js
+
+"use strict";
+
+var firstTest = 1; // set to test of interest when debugging
+var lastTest = 4; // set to test of interest when debugging
+////////////////////////////////////////////////////////////////////////////////
+
+// Note: sets Cc and Ci variables
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+XPCOMUtils.defineLazyGetter(this, "URL", function() {
+ return "http://localhost:" + httpserver.identity.primaryPort;
+});
+
+var httpserver = new HttpServer();
+var index = 0;
+var nextTest = firstTest;
+var test_flags = [];
+var testPathBase = "/test_headers";
+
+function run_test() {
+ httpserver.start(-1);
+
+ do_test_pending();
+ run_test_number(nextTest);
+}
+
+function runNextTest() {
+ if (nextTest == lastTest) {
+ endTests();
+ return;
+ }
+ nextTest++;
+ // Make sure test functions exist
+ if (globalThis["handler" + nextTest] == undefined) {
+ do_throw("handler" + nextTest + " undefined!");
+ }
+ if (globalThis["completeTest" + nextTest] == undefined) {
+ do_throw("completeTest" + nextTest + " undefined!");
+ }
+
+ run_test_number(nextTest);
+}
+
+function run_test_number(num) {
+ let testPath = testPathBase + num;
+ httpserver.registerPathHandler(testPath, globalThis["handler" + num]);
+
+ var channel = setupChannel(testPath);
+ let flags = test_flags[num]; // OK if flags undefined for test
+ channel.asyncOpen(
+ new ChannelListener(globalThis["completeTest" + num], channel, flags)
+ );
+}
+
+function setupChannel(url) {
+ var chan = NetUtil.newChannel({
+ uri: URL + url,
+ loadUsingSystemPrincipal: true,
+ });
+ var httpChan = chan.QueryInterface(Ci.nsIHttpChannel);
+ return httpChan;
+}
+
+function endTests() {
+ httpserver.stop(do_test_finished);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Test 1: test Content-Disposition channel attributes
+function handler1(metadata, response) {
+ response.setStatusLine(metadata.httpVersion, 200, "OK");
+ response.setHeader("Content-Disposition", "attachment; filename=foo");
+ response.setHeader("Content-Type", "text/plain", false);
+}
+
+function completeTest1(request, data, ctx) {
+ try {
+ var chan = request.QueryInterface(Ci.nsIChannel);
+ Assert.equal(chan.contentDisposition, chan.DISPOSITION_ATTACHMENT);
+ Assert.equal(chan.contentDispositionFilename, "foo");
+ Assert.equal(chan.contentDispositionHeader, "attachment; filename=foo");
+ } catch (ex) {
+ do_throw("error parsing Content-Disposition: " + ex);
+ }
+ runNextTest();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Test 2: no filename
+function handler2(metadata, response) {
+ response.setStatusLine(metadata.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "text/plain", false);
+ response.setHeader("Content-Disposition", "attachment");
+ var body = "foo";
+ response.bodyOutputStream.write(body, body.length);
+}
+
+function completeTest2(request, data, ctx) {
+ try {
+ var chan = request.QueryInterface(Ci.nsIChannel);
+ Assert.equal(chan.contentDisposition, chan.DISPOSITION_ATTACHMENT);
+ Assert.equal(chan.contentDispositionHeader, "attachment");
+ chan.contentDispositionFilename; // should barf
+ do_throw("Should have failed getting Content-Disposition filename");
+ } catch (ex) {
+ info("correctly ate exception");
+ }
+ runNextTest();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Test 3: filename missing
+function handler3(metadata, response) {
+ response.setStatusLine(metadata.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "text/plain", false);
+ response.setHeader("Content-Disposition", "attachment; filename=");
+ var body = "foo";
+ response.bodyOutputStream.write(body, body.length);
+}
+
+function completeTest3(request, data, ctx) {
+ try {
+ var chan = request.QueryInterface(Ci.nsIChannel);
+ Assert.equal(chan.contentDisposition, chan.DISPOSITION_ATTACHMENT);
+ Assert.equal(chan.contentDispositionHeader, "attachment; filename=");
+ chan.contentDispositionFilename; // should barf
+
+ do_throw("Should have failed getting Content-Disposition filename");
+ } catch (ex) {
+ info("correctly ate exception");
+ }
+ runNextTest();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Test 4: inline
+function handler4(metadata, response) {
+ response.setStatusLine(metadata.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "text/plain", false);
+ response.setHeader("Content-Disposition", "inline");
+ var body = "foo";
+ response.bodyOutputStream.write(body, body.length);
+}
+
+function completeTest4(request, data, ctx) {
+ try {
+ var chan = request.QueryInterface(Ci.nsIChannel);
+ Assert.equal(chan.contentDisposition, chan.DISPOSITION_INLINE);
+ Assert.equal(chan.contentDispositionHeader, "inline");
+
+ chan.contentDispositionFilename; // should barf
+ do_throw("Should have failed getting Content-Disposition filename");
+ } catch (ex) {
+ info("correctly ate exception");
+ }
+ runNextTest();
+}
diff --git a/netwerk/test/unit/test_hostnameIsLocalIPAddress.js b/netwerk/test/unit/test_hostnameIsLocalIPAddress.js
new file mode 100644
index 0000000000..589766b522
--- /dev/null
+++ b/netwerk/test/unit/test_hostnameIsLocalIPAddress.js
@@ -0,0 +1,41 @@
+"use strict";
+
+var ioService = Cc["@mozilla.org/network/io-service;1"].getService(
+ Ci.nsIIOService
+);
+
+function run_test() {
+ let testURIs = [
+ ["http://example.com", false],
+ ["about:robots", false],
+ // 10/8 prefix (RFC 1918)
+ ["http://9.255.255.255", false],
+ ["http://10.0.0.0", true],
+ ["http://10.0.23.31", true],
+ ["http://10.255.255.255", true],
+ ["http://11.0.0.0", false],
+ // 169.254/16 prefix (Link Local)
+ ["http://169.253.255.255", false],
+ ["http://169.254.0.0", true],
+ ["http://169.254.42.91", true],
+ ["http://169.254.255.255", true],
+ ["http://169.255.0.0", false],
+ // 172.16/12 prefix (RFC 1918)
+ ["http://172.15.255.255", false],
+ ["http://172.16.0.0", true],
+ ["http://172.25.110.0", true],
+ ["http://172.31.255.255", true],
+ ["http://172.32.0.0", false],
+ // 192.168/16 prefix (RFC 1918)
+ ["http://192.167.255.255", false],
+ ["http://192.168.0.0", true],
+ ["http://192.168.127.10", true],
+ ["http://192.168.255.255", true],
+ ["http://192.169.0.0", false],
+ ];
+
+ for (let [uri, isLocal] of testURIs) {
+ let nsuri = ioService.newURI(uri);
+ equal(isLocal, ioService.hostnameIsLocalIPAddress(nsuri));
+ }
+}
diff --git a/netwerk/test/unit/test_hostnameIsSharedIPAddress.js b/netwerk/test/unit/test_hostnameIsSharedIPAddress.js
new file mode 100644
index 0000000000..80a848bfbb
--- /dev/null
+++ b/netwerk/test/unit/test_hostnameIsSharedIPAddress.js
@@ -0,0 +1,21 @@
+"use strict";
+
+var ioService = Cc["@mozilla.org/network/io-service;1"].getService(
+ Ci.nsIIOService
+);
+
+function run_test() {
+ let testURIs = [
+ // 100.64/10 prefix (RFC 6598)
+ ["http://100.63.255.254", false],
+ ["http://100.64.0.0", true],
+ ["http://100.91.63.42", true],
+ ["http://100.127.255.254", true],
+ ["http://100.128.0.0", false],
+ ];
+
+ for (let [uri, isShared] of testURIs) {
+ let nsuri = ioService.newURI(uri);
+ equal(isShared, ioService.hostnameIsSharedIPAddress(nsuri));
+ }
+}
diff --git a/netwerk/test/unit/test_http1-proxy.js b/netwerk/test/unit/test_http1-proxy.js
new file mode 100644
index 0000000000..88faf7d884
--- /dev/null
+++ b/netwerk/test/unit/test_http1-proxy.js
@@ -0,0 +1,229 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * This test checks following expectations when using HTTP/1 proxy:
+ *
+ * - check we are seeing expected nsresult error codes on channels
+ * (nsIChannel.status) corresponding to different proxy status code
+ * responses (502, 504, 407, ...)
+ * - check we don't try to ask for credentials or otherwise authenticate to
+ * the proxy when 407 is returned and there is no Proxy-Authenticate
+ * response header sent
+ */
+
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+const pps = Cc["@mozilla.org/network/protocol-proxy-service;1"].getService();
+
+let server_port;
+let http_server;
+
+class ProxyFilter {
+ constructor(type, host, port, flags) {
+ this._type = type;
+ this._host = host;
+ this._port = port;
+ this._flags = flags;
+ this.QueryInterface = ChromeUtils.generateQI(["nsIProtocolProxyFilter"]);
+ }
+ applyFilter(uri, pi, cb) {
+ if (uri.spec.match(/(\/proxy-session-counter)/)) {
+ cb.onProxyFilterResult(pi);
+ return;
+ }
+ cb.onProxyFilterResult(
+ pps.newProxyInfo(
+ this._type,
+ this._host,
+ this._port,
+ "",
+ "",
+ this._flags,
+ 1000,
+ null
+ )
+ );
+ }
+}
+
+class UnxpectedAuthPrompt2 {
+ constructor(signal) {
+ this.signal = signal;
+ this.QueryInterface = ChromeUtils.generateQI(["nsIAuthPrompt2"]);
+ }
+ asyncPromptAuth() {
+ this.signal.triggered = true;
+ throw Components.Exception("", Cr.ERROR_UNEXPECTED);
+ }
+}
+
+class AuthRequestor {
+ constructor(prompt) {
+ this.prompt = prompt;
+ this.QueryInterface = ChromeUtils.generateQI(["nsIInterfaceRequestor"]);
+ }
+ getInterface(iid) {
+ if (iid.equals(Ci.nsIAuthPrompt2)) {
+ return this.prompt();
+ }
+ throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE);
+ }
+}
+
+function make_channel(url) {
+ return NetUtil.newChannel({
+ uri: url,
+ loadUsingSystemPrincipal: true,
+ // Using TYPE_DOCUMENT for the authentication dialog test, it'd be blocked for other types
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT,
+ });
+}
+
+function get_response(channel, flags = CL_ALLOW_UNKNOWN_CL) {
+ return new Promise(resolve => {
+ channel.asyncOpen(
+ new ChannelListener(
+ (request, data) => {
+ request.QueryInterface(Ci.nsIHttpChannel);
+ const status = request.status;
+ const http_code = status ? undefined : request.responseStatus;
+ request.QueryInterface(Ci.nsIProxiedChannel);
+ const proxy_connect_response_code =
+ request.httpProxyConnectResponseCode;
+ resolve({ status, http_code, data, proxy_connect_response_code });
+ },
+ null,
+ flags
+ )
+ );
+ });
+}
+
+function connect_handler(request, response) {
+ Assert.equal(request.method, "CONNECT");
+
+ switch (request.host) {
+ case "404.example.com":
+ response.setStatusLine(request.httpVersion, 404, "Not found");
+ break;
+ case "407.example.com":
+ response.setStatusLine(request.httpVersion, 407, "Authenticate");
+ // And deliberately no Proxy-Authenticate header
+ break;
+ case "429.example.com":
+ response.setStatusLine(request.httpVersion, 429, "Too Many Requests");
+ break;
+ case "502.example.com":
+ response.setStatusLine(request.httpVersion, 502, "Bad Gateway");
+ break;
+ case "504.example.com":
+ response.setStatusLine(request.httpVersion, 504, "Gateway timeout");
+ break;
+ default:
+ response.setStatusLine(request.httpVersion, 500, "I am dumb");
+ }
+}
+
+add_task(async function setup() {
+ http_server = new HttpServer();
+ http_server.identity.add("https", "404.example.com", 443);
+ http_server.identity.add("https", "407.example.com", 443);
+ http_server.identity.add("https", "429.example.com", 443);
+ http_server.identity.add("https", "502.example.com", 443);
+ http_server.identity.add("https", "504.example.com", 443);
+ http_server.registerPathHandler("CONNECT", connect_handler);
+ http_server.start(-1);
+ server_port = http_server.identity.primaryPort;
+
+ // make all native resolve calls "secretly" resolve localhost instead
+ Services.prefs.setBoolPref("network.dns.native-is-localhost", true);
+
+ pps.registerFilter(new ProxyFilter("http", "localhost", server_port, 0), 10);
+});
+
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("network.dns.native-is-localhost");
+});
+
+/**
+ * Test series beginning.
+ */
+
+// The proxy responses with 407 instead of 200 Connected, make sure we get a proper error
+// code from the channel and not try to ask for any credentials.
+add_task(async function proxy_auth_failure() {
+ const chan = make_channel(`https://407.example.com/`);
+ const auth_prompt = { triggered: false };
+ chan.notificationCallbacks = new AuthRequestor(
+ () => new UnxpectedAuthPrompt2(auth_prompt)
+ );
+ const { status, http_code, proxy_connect_response_code } = await get_response(
+ chan,
+ CL_EXPECT_FAILURE
+ );
+
+ Assert.equal(status, Cr.NS_ERROR_PROXY_AUTHENTICATION_FAILED);
+ Assert.equal(proxy_connect_response_code, 407);
+ Assert.equal(http_code, undefined);
+ Assert.equal(auth_prompt.triggered, false, "Auth prompt didn't trigger");
+});
+
+// 502 Bad gateway code returned by the proxy.
+add_task(async function proxy_bad_gateway_failure() {
+ const { status, http_code, proxy_connect_response_code } = await get_response(
+ make_channel(`https://502.example.com/`),
+ CL_EXPECT_FAILURE
+ );
+
+ Assert.equal(status, Cr.NS_ERROR_PROXY_BAD_GATEWAY);
+ Assert.equal(proxy_connect_response_code, 502);
+ Assert.equal(http_code, undefined);
+});
+
+// 504 Gateway timeout code returned by the proxy.
+add_task(async function proxy_gateway_timeout_failure() {
+ const { status, http_code, proxy_connect_response_code } = await get_response(
+ make_channel(`https://504.example.com/`),
+ CL_EXPECT_FAILURE
+ );
+
+ Assert.equal(status, Cr.NS_ERROR_PROXY_GATEWAY_TIMEOUT);
+ Assert.equal(proxy_connect_response_code, 504);
+ Assert.equal(http_code, undefined);
+});
+
+// 404 Not Found means the proxy could not resolve the host.
+add_task(async function proxy_host_not_found_failure() {
+ const { status, http_code, proxy_connect_response_code } = await get_response(
+ make_channel(`https://404.example.com/`),
+ CL_EXPECT_FAILURE
+ );
+
+ Assert.equal(status, Cr.NS_ERROR_UNKNOWN_HOST);
+ Assert.equal(proxy_connect_response_code, 404);
+ Assert.equal(http_code, undefined);
+});
+
+// 429 Too Many Requests means we sent too many requests.
+add_task(async function proxy_too_many_requests_failure() {
+ const { status, http_code, proxy_connect_response_code } = await get_response(
+ make_channel(`https://429.example.com/`),
+ CL_EXPECT_FAILURE
+ );
+
+ Assert.equal(status, Cr.NS_ERROR_PROXY_TOO_MANY_REQUESTS);
+ Assert.equal(proxy_connect_response_code, 429);
+ Assert.equal(http_code, undefined);
+});
+
+add_task(async function shutdown() {
+ await new Promise(resolve => {
+ http_server.stop(resolve);
+ });
+});
diff --git a/netwerk/test/unit/test_http2-proxy.js b/netwerk/test/unit/test_http2-proxy.js
new file mode 100644
index 0000000000..1c9d6151be
--- /dev/null
+++ b/netwerk/test/unit/test_http2-proxy.js
@@ -0,0 +1,615 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * This test checks following expectations when using HTTP/2 proxy:
+ *
+ * - when we request https access, we don't create different sessions for
+ * different origins, only new tunnels inside a single session
+ * - when the isolation key (`proxy_isolation`) is changed, new single session
+ * is created for new requests to same origins as before
+ * - error code returned from the tunnel (a proxy error - not end-server
+ * error!) doesn't kill the existing session
+ * - check we are seeing expected nsresult error codes on channels
+ * (nsIChannel.status) corresponding to different proxy status code
+ * responses (502, 504, 407, ...)
+ * - check we don't try to ask for credentials or otherwise authenticate to
+ * the proxy when 407 is returned and there is no Proxy-Authenticate
+ * response header sent
+ */
+
+/* eslint-env node */
+/* global serverPort */
+
+"use strict";
+
+const pps = Cc["@mozilla.org/network/protocol-proxy-service;1"].getService();
+
+let proxy_port;
+let filter;
+let proxy;
+
+// See moz-http2
+const proxy_auth = "authorization-token";
+let proxy_isolation;
+
+class ProxyFilter {
+ constructor(type, host, port, flags) {
+ this._type = type;
+ this._host = host;
+ this._port = port;
+ this._flags = flags;
+ this.QueryInterface = ChromeUtils.generateQI(["nsIProtocolProxyFilter"]);
+ }
+ applyFilter(uri, pi, cb) {
+ if (
+ uri.pathQueryRef.startsWith("/execute") ||
+ uri.pathQueryRef.startsWith("/fork") ||
+ uri.pathQueryRef.startsWith("/kill")
+ ) {
+ // So we allow NodeServer.execute to work
+ cb.onProxyFilterResult(pi);
+ return;
+ }
+ cb.onProxyFilterResult(
+ pps.newProxyInfo(
+ this._type,
+ this._host,
+ this._port,
+ proxy_auth,
+ proxy_isolation,
+ this._flags,
+ 1000,
+ null
+ )
+ );
+ }
+}
+
+class UnxpectedAuthPrompt2 {
+ constructor(signal) {
+ this.signal = signal;
+ this.QueryInterface = ChromeUtils.generateQI(["nsIAuthPrompt2"]);
+ }
+ asyncPromptAuth() {
+ this.signal.triggered = true;
+ throw Components.Exception("", Cr.ERROR_UNEXPECTED);
+ }
+}
+
+class SimpleAuthPrompt2 {
+ constructor(signal) {
+ this.signal = signal;
+ this.QueryInterface = ChromeUtils.generateQI(["nsIAuthPrompt2"]);
+ }
+ asyncPromptAuth(channel, callback, context, encryptionLevel, authInfo) {
+ this.signal.triggered = true;
+ executeSoon(function() {
+ authInfo.username = "user";
+ authInfo.password = "pass";
+ callback.onAuthAvailable(context, authInfo);
+ });
+ }
+}
+
+class AuthRequestor {
+ constructor(prompt) {
+ this.prompt = prompt;
+ this.QueryInterface = ChromeUtils.generateQI(["nsIInterfaceRequestor"]);
+ }
+ getInterface(iid) {
+ if (iid.equals(Ci.nsIAuthPrompt2)) {
+ return this.prompt();
+ }
+ throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE);
+ }
+}
+
+function make_channel(url) {
+ return NetUtil.newChannel({
+ uri: url,
+ loadUsingSystemPrincipal: true,
+ // Using TYPE_DOCUMENT for the authentication dialog test, it'd be blocked for other types
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT,
+ });
+}
+
+function get_response(channel, flags = CL_ALLOW_UNKNOWN_CL) {
+ return new Promise(resolve => {
+ channel.asyncOpen(
+ new ChannelListener(
+ (request, data) => {
+ request.QueryInterface(Ci.nsIHttpChannel);
+ const status = request.status;
+ const http_code = status ? undefined : request.responseStatus;
+ request.QueryInterface(Ci.nsIProxiedChannel);
+ const proxy_connect_response_code =
+ request.httpProxyConnectResponseCode;
+ resolve({ status, http_code, data, proxy_connect_response_code });
+ },
+ null,
+ flags
+ )
+ );
+ });
+}
+
+let initial_session_count = 0;
+
+class http2ProxyCode {
+ static listen(server, envport) {
+ if (!server) {
+ return Promise.resolve(0);
+ }
+
+ let portSelection = 0;
+ if (envport !== undefined) {
+ try {
+ portSelection = parseInt(envport, 10);
+ } catch (e) {
+ portSelection = -1;
+ }
+ }
+ return new Promise(resolve => {
+ server.listen(portSelection, "0.0.0.0", 2000, () => {
+ resolve(server.address().port);
+ });
+ });
+ }
+
+ static startNewProxy() {
+ const fs = require("fs");
+ const options = {
+ key: fs.readFileSync(__dirname + "/http2-cert.key"),
+ cert: fs.readFileSync(__dirname + "/http2-cert.pem"),
+ };
+ const http2 = require("http2");
+ global.proxy = http2.createSecureServer(options);
+ this.setupProxy();
+ return http2ProxyCode.listen(proxy).then(port => {
+ return { port, success: true };
+ });
+ }
+
+ static closeProxy() {
+ proxy.closeSockets();
+ return new Promise(resolve => {
+ proxy.close(resolve);
+ });
+ }
+
+ static proxySessionCount() {
+ if (!proxy) {
+ return 0;
+ }
+ return proxy.proxy_session_count;
+ }
+
+ static setupProxy() {
+ if (!proxy) {
+ throw new Error("proxy is null");
+ }
+ proxy.proxy_session_count = 0;
+ proxy.on("session", () => {
+ ++proxy.proxy_session_count;
+ });
+
+ // We need to track active connections so we can forcefully close keep-alive
+ // connections when shutting down the proxy.
+ proxy.socketIndex = 0;
+ proxy.socketMap = {};
+ proxy.on("connection", function(socket) {
+ let index = proxy.socketIndex++;
+ proxy.socketMap[index] = socket;
+ socket.on("close", function() {
+ delete proxy.socketMap[index];
+ });
+ });
+ proxy.closeSockets = function() {
+ for (let i in proxy.socketMap) {
+ proxy.socketMap[i].destroy();
+ }
+ };
+
+ proxy.on("stream", (stream, headers) => {
+ if (headers[":method"] !== "CONNECT") {
+ // Only accept CONNECT requests
+ stream.respond({ ":status": 405 });
+ stream.end();
+ return;
+ }
+
+ const target = headers[":authority"];
+
+ const authorization_token = headers["proxy-authorization"];
+ if (target == "407.example.com:443") {
+ stream.respond({ ":status": 407 });
+ // Deliberately send no Proxy-Authenticate header
+ stream.end();
+ return;
+ }
+ if (target == "407.basic.example.com:443") {
+ // we want to return a different response than 407 to not re-request
+ // credentials (and thus loop) but also not 200 to not let the channel
+ // attempt to waste time connecting a non-existing https server - hence
+ // 418 I'm a teapot :)
+ if ("Basic dXNlcjpwYXNz" == authorization_token) {
+ stream.respond({ ":status": 418 });
+ stream.end();
+ return;
+ }
+ stream.respond({
+ ":status": 407,
+ "proxy-authenticate": "Basic realm='foo'",
+ });
+ stream.end();
+ return;
+ }
+ if (target == "404.example.com:443") {
+ // 404 Not Found, a response code that a proxy should return when the host can't be found
+ stream.respond({ ":status": 404 });
+ stream.end();
+ return;
+ }
+ if (target == "429.example.com:443") {
+ // 429 Too Many Requests, a response code that a proxy should return when receiving too many requests
+ stream.respond({ ":status": 429 });
+ stream.end();
+ return;
+ }
+ if (target == "502.example.com:443") {
+ // 502 Bad Gateway, a response code mostly resembling immediate connection error
+ stream.respond({ ":status": 502 });
+ stream.end();
+ return;
+ }
+ if (target == "504.example.com:443") {
+ // 504 Gateway Timeout, did not receive a timely response from an upstream server
+ stream.respond({ ":status": 504 });
+ stream.end();
+ return;
+ }
+
+ const net = require("net");
+ const socket = net.connect(serverPort, "127.0.0.1", () => {
+ try {
+ stream.respond({ ":status": 200 });
+ socket.pipe(stream);
+ stream.pipe(socket);
+ } catch (exception) {
+ console.log(exception);
+ stream.close();
+ }
+ });
+ socket.on("error", error => {
+ throw `Unxpected error when conneting the HTTP/2 server from the HTTP/2 proxy during CONNECT handling: '${error}'`;
+ });
+ });
+ }
+}
+
+async function proxy_session_counter() {
+ let data = await NodeServer.execute(
+ processId,
+ `http2ProxyCode.proxySessionCount()`
+ );
+ return parseInt(data) - initial_session_count;
+}
+let processId;
+add_task(async function setup() {
+ // Set to allow the cert presented by our H2 server
+ do_get_profile();
+
+ // The moz-http2 cert is for foo.example.com and is signed by http2-ca.pem
+ // so add that cert to the trust list as a signing cert.
+ let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(
+ Ci.nsIX509CertDB
+ );
+ addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u");
+
+ const env = Cc["@mozilla.org/process/environment;1"].getService(
+ Ci.nsIEnvironment
+ );
+ let server_port = env.get("MOZHTTP2_PORT");
+ Assert.notEqual(server_port, null);
+ processId = await NodeServer.fork();
+ await NodeServer.execute(processId, `serverPort = ${server_port}`);
+ await NodeServer.execute(processId, http2ProxyCode);
+ let proxy = await NodeServer.execute(
+ processId,
+ `http2ProxyCode.startNewProxy()`
+ );
+ proxy_port = proxy.port;
+ Assert.notEqual(proxy_port, null);
+
+ Services.prefs.setStringPref(
+ "services.settings.server",
+ `data:text/html,test`
+ );
+
+ Services.prefs.setBoolPref("network.http.spdy.enabled", true);
+ Services.prefs.setBoolPref("network.http.spdy.enabled.http2", true);
+
+ // Even with network state isolation active, we don't end up using the
+ // partitioned principal.
+ Services.prefs.setBoolPref("privacy.partition.network_state", true);
+
+ // make all native resolve calls "secretly" resolve localhost instead
+ Services.prefs.setBoolPref("network.dns.native-is-localhost", true);
+
+ filter = new ProxyFilter("https", "localhost", proxy_port, 0);
+ pps.registerFilter(filter, 10);
+
+ initial_session_count = await proxy_session_counter();
+ info(`Initial proxy session count = ${initial_session_count}`);
+});
+
+registerCleanupFunction(async () => {
+ Services.prefs.clearUserPref("services.settings.server");
+ Services.prefs.clearUserPref("network.http.spdy.enabled");
+ Services.prefs.clearUserPref("network.http.spdy.enabled.http2");
+ Services.prefs.clearUserPref("network.dns.native-is-localhost");
+
+ pps.unregisterFilter(filter);
+
+ await NodeServer.execute(processId, `http2ProxyCode.closeProxy()`);
+ await NodeServer.kill(processId);
+});
+
+/**
+ * Test series beginning.
+ */
+
+// Check we reach the h2 end server and keep only one session with the proxy for two different origin.
+// Here we use the first isolation token.
+add_task(async function proxy_success_one_session() {
+ proxy_isolation = "TOKEN1";
+
+ const foo = await get_response(
+ make_channel(`https://foo.example.com/random-request-1`)
+ );
+ const alt1 = await get_response(
+ make_channel(`https://alt1.example.com/random-request-2`)
+ );
+
+ Assert.equal(foo.status, Cr.NS_OK);
+ Assert.equal(foo.proxy_connect_response_code, 200);
+ Assert.equal(foo.http_code, 200);
+ Assert.ok(foo.data.match("random-request-1"));
+ Assert.ok(foo.data.match("You Win!"));
+ Assert.equal(alt1.status, Cr.NS_OK);
+ Assert.equal(alt1.proxy_connect_response_code, 200);
+ Assert.equal(alt1.http_code, 200);
+ Assert.ok(alt1.data.match("random-request-2"));
+ Assert.ok(alt1.data.match("You Win!"));
+ Assert.equal(
+ await proxy_session_counter(),
+ 1,
+ "Created just one session with the proxy"
+ );
+});
+
+// The proxy responses with 407 instead of 200 Connected, make sure we get a proper error
+// code from the channel and not try to ask for any credentials.
+add_task(async function proxy_auth_failure() {
+ const chan = make_channel(`https://407.example.com/`);
+ const auth_prompt = { triggered: false };
+ chan.notificationCallbacks = new AuthRequestor(
+ () => new UnxpectedAuthPrompt2(auth_prompt)
+ );
+ const { status, http_code, proxy_connect_response_code } = await get_response(
+ chan,
+ CL_EXPECT_FAILURE
+ );
+
+ Assert.equal(status, Cr.NS_ERROR_PROXY_AUTHENTICATION_FAILED);
+ Assert.equal(proxy_connect_response_code, 407);
+ Assert.equal(http_code, undefined);
+ Assert.equal(auth_prompt.triggered, false, "Auth prompt didn't trigger");
+ Assert.equal(
+ await proxy_session_counter(),
+ 1,
+ "No new session created by 407"
+ );
+});
+
+// The proxy responses with 407 with Proxy-Authenticate header presence. Make
+// sure that we prompt the auth prompt to ask for credentials.
+add_task(async function proxy_auth_basic() {
+ const chan = make_channel(`https://407.basic.example.com/`);
+ const auth_prompt = { triggered: false };
+ chan.notificationCallbacks = new AuthRequestor(
+ () => new SimpleAuthPrompt2(auth_prompt)
+ );
+ const { status, http_code, proxy_connect_response_code } = await get_response(
+ chan,
+ CL_EXPECT_FAILURE
+ );
+
+ // 418 indicates we pass the basic authentication.
+ Assert.equal(status, Cr.NS_ERROR_PROXY_CONNECTION_REFUSED);
+ Assert.equal(proxy_connect_response_code, 418);
+ Assert.equal(http_code, undefined);
+ Assert.equal(auth_prompt.triggered, true, "Auth prompt should trigger");
+ Assert.equal(
+ await proxy_session_counter(),
+ 1,
+ "No new session created by 407"
+ );
+});
+
+// 502 Bad gateway code returned by the proxy, still one session only, proper different code
+// from the channel.
+add_task(async function proxy_bad_gateway_failure() {
+ const { status, http_code, proxy_connect_response_code } = await get_response(
+ make_channel(`https://502.example.com/`),
+ CL_EXPECT_FAILURE
+ );
+
+ Assert.equal(status, Cr.NS_ERROR_PROXY_BAD_GATEWAY);
+ Assert.equal(proxy_connect_response_code, 502);
+ Assert.equal(http_code, undefined);
+ Assert.equal(
+ await proxy_session_counter(),
+ 1,
+ "No new session created by 502 after 407"
+ );
+});
+
+// Second 502 Bad gateway code returned by the proxy, still one session only with the proxy.
+add_task(async function proxy_bad_gateway_failure_two() {
+ const { status, http_code, proxy_connect_response_code } = await get_response(
+ make_channel(`https://502.example.com/`),
+ CL_EXPECT_FAILURE
+ );
+
+ Assert.equal(status, Cr.NS_ERROR_PROXY_BAD_GATEWAY);
+ Assert.equal(proxy_connect_response_code, 502);
+ Assert.equal(http_code, undefined);
+ Assert.equal(
+ await proxy_session_counter(),
+ 1,
+ "No new session created by second 502"
+ );
+});
+
+// 504 Gateway timeout code returned by the proxy, still one session only, proper different code
+// from the channel.
+add_task(async function proxy_gateway_timeout_failure() {
+ const { status, http_code, proxy_connect_response_code } = await get_response(
+ make_channel(`https://504.example.com/`),
+ CL_EXPECT_FAILURE
+ );
+
+ Assert.equal(status, Cr.NS_ERROR_PROXY_GATEWAY_TIMEOUT);
+ Assert.equal(proxy_connect_response_code, 504);
+ Assert.equal(http_code, undefined);
+ Assert.equal(
+ await proxy_session_counter(),
+ 1,
+ "No new session created by 504 after 502"
+ );
+});
+
+// 404 Not Found means the proxy could not resolve the host. As for other error responses
+// we still expect this not to close the existing session.
+add_task(async function proxy_host_not_found_failure() {
+ const { status, http_code, proxy_connect_response_code } = await get_response(
+ make_channel(`https://404.example.com/`),
+ CL_EXPECT_FAILURE
+ );
+
+ Assert.equal(status, Cr.NS_ERROR_UNKNOWN_HOST);
+ Assert.equal(proxy_connect_response_code, 404);
+ Assert.equal(http_code, undefined);
+ Assert.equal(
+ await proxy_session_counter(),
+ 1,
+ "No new session created by 404 after 504"
+ );
+});
+
+add_task(async function proxy_too_many_requests_failure() {
+ const { status, http_code, proxy_connect_response_code } = await get_response(
+ make_channel(`https://429.example.com/`),
+ CL_EXPECT_FAILURE
+ );
+
+ Assert.equal(status, Cr.NS_ERROR_PROXY_TOO_MANY_REQUESTS);
+ Assert.equal(proxy_connect_response_code, 429);
+ Assert.equal(http_code, undefined);
+ Assert.equal(
+ await proxy_session_counter(),
+ 1,
+ "No new session created by 429 after 504"
+ );
+});
+
+// Make sure that the above error codes don't kill the session and we still reach the end server
+add_task(async function proxy_success_still_one_session() {
+ const foo = await get_response(
+ make_channel(`https://foo.example.com/random-request-1`)
+ );
+ const alt1 = await get_response(
+ make_channel(`https://alt1.example.com/random-request-2`)
+ );
+
+ Assert.equal(foo.status, Cr.NS_OK);
+ Assert.equal(foo.http_code, 200);
+ Assert.equal(foo.proxy_connect_response_code, 200);
+ Assert.ok(foo.data.match("random-request-1"));
+ Assert.equal(alt1.status, Cr.NS_OK);
+ Assert.equal(alt1.proxy_connect_response_code, 200);
+ Assert.equal(alt1.http_code, 200);
+ Assert.ok(alt1.data.match("random-request-2"));
+ Assert.equal(
+ await proxy_session_counter(),
+ 1,
+ "No new session created after proxy error codes"
+ );
+});
+
+// Have a new isolation key, this means we are expected to create a new, and again one only,
+// session with the proxy to reach the end server.
+add_task(async function proxy_success_isolated_session() {
+ Assert.notEqual(proxy_isolation, "TOKEN2");
+ proxy_isolation = "TOKEN2";
+
+ const foo = await get_response(
+ make_channel(`https://foo.example.com/random-request-1`)
+ );
+ const alt1 = await get_response(
+ make_channel(`https://alt1.example.com/random-request-2`)
+ );
+ const lh = await get_response(
+ make_channel(`https://localhost/random-request-3`)
+ );
+
+ Assert.equal(foo.status, Cr.NS_OK);
+ Assert.equal(foo.proxy_connect_response_code, 200);
+ Assert.equal(foo.http_code, 200);
+ Assert.ok(foo.data.match("random-request-1"));
+ Assert.ok(foo.data.match("You Win!"));
+ Assert.equal(alt1.status, Cr.NS_OK);
+ Assert.equal(alt1.proxy_connect_response_code, 200);
+ Assert.equal(alt1.http_code, 200);
+ Assert.ok(alt1.data.match("random-request-2"));
+ Assert.ok(alt1.data.match("You Win!"));
+ Assert.equal(lh.status, Cr.NS_OK);
+ Assert.equal(lh.proxy_connect_response_code, 200);
+ Assert.equal(lh.http_code, 200);
+ Assert.ok(lh.data.match("random-request-3"));
+ Assert.ok(lh.data.match("You Win!"));
+ Assert.equal(
+ await proxy_session_counter(),
+ 2,
+ "Just one new session seen after changing the isolation key"
+ );
+});
+
+// Check that error codes are still handled the same way with new isolation, just in case.
+add_task(async function proxy_bad_gateway_failure_isolated() {
+ const failure1 = await get_response(
+ make_channel(`https://502.example.com/`),
+ CL_EXPECT_FAILURE
+ );
+ const failure2 = await get_response(
+ make_channel(`https://502.example.com/`),
+ CL_EXPECT_FAILURE
+ );
+
+ Assert.equal(failure1.status, Cr.NS_ERROR_PROXY_BAD_GATEWAY);
+ Assert.equal(failure1.proxy_connect_response_code, 502);
+ Assert.equal(failure1.http_code, undefined);
+ Assert.equal(failure2.status, Cr.NS_ERROR_PROXY_BAD_GATEWAY);
+ Assert.equal(failure2.proxy_connect_response_code, 502);
+ Assert.equal(failure2.http_code, undefined);
+ Assert.equal(
+ await proxy_session_counter(),
+ 2,
+ "No new session created by 502"
+ );
+});
diff --git a/netwerk/test/unit/test_http2.js b/netwerk/test/unit/test_http2.js
new file mode 100644
index 0000000000..afc47ba740
--- /dev/null
+++ b/netwerk/test/unit/test_http2.js
@@ -0,0 +1,1442 @@
+// test HTTP/2
+
+"use strict";
+
+// Generate a small and a large post with known pre-calculated md5 sums
+function generateContent(size) {
+ var content = "";
+ for (var i = 0; i < size; i++) {
+ content += "0";
+ }
+ return content;
+}
+
+var posts = [];
+posts.push(generateContent(10));
+posts.push(generateContent(250000));
+posts.push(generateContent(128000));
+
+// pre-calculated md5sums (in hex) of the above posts
+var md5s = [
+ "f1b708bba17f1ce948dc979f4d7092bc",
+ "2ef8d3b6c8f329318eb1a119b12622b6",
+];
+
+var bigListenerData = generateContent(128 * 1024);
+var bigListenerMD5 = "8f607cfdd2c87d6a7eedb657dafbd836";
+
+function checkIsHttp2(request) {
+ try {
+ if (request.getResponseHeader("X-Firefox-Spdy") == "h2") {
+ if (request.getResponseHeader("X-Connection-Http2") == "yes") {
+ return true;
+ }
+ return false; // Weird case, but the server disagrees with us
+ }
+ } catch (e) {
+ // Nothing to do here
+ }
+ return false;
+}
+
+var Http2CheckListener = function() {};
+
+Http2CheckListener.prototype = {
+ onStartRequestFired: false,
+ onDataAvailableFired: false,
+ isHttp2Connection: false,
+ shouldBeHttp2: true,
+ accum: 0,
+ expected: -1,
+ shouldSucceed: true,
+
+ onStartRequest: function testOnStartRequest(request) {
+ this.onStartRequestFired = true;
+ if (this.shouldSucceed && !Components.isSuccessCode(request.status)) {
+ do_throw("Channel should have a success code! (" + request.status + ")");
+ } else if (
+ !this.shouldSucceed &&
+ Components.isSuccessCode(request.status)
+ ) {
+ do_throw("Channel succeeded unexpectedly!");
+ }
+
+ Assert.ok(request instanceof Ci.nsIHttpChannel);
+ Assert.equal(request.requestSucceeded, this.shouldSucceed);
+ if (this.shouldSucceed) {
+ Assert.equal(request.responseStatus, 200);
+ }
+ },
+
+ onDataAvailable: function testOnDataAvailable(request, stream, off, cnt) {
+ this.onDataAvailableFired = true;
+ this.isHttp2Connection = checkIsHttp2(request);
+ this.accum += cnt;
+ read_stream(stream, cnt);
+ },
+
+ onStopRequest: function testOnStopRequest(request, status) {
+ Assert.ok(this.onStartRequestFired);
+ if (this.expected != -1) {
+ Assert.equal(this.accum, this.expected);
+ }
+ if (this.shouldSucceed) {
+ Assert.ok(Components.isSuccessCode(status));
+ Assert.ok(this.onDataAvailableFired);
+ Assert.ok(this.isHttp2Connection == this.shouldBeHttp2);
+ } else {
+ Assert.ok(!Components.isSuccessCode(status));
+ }
+
+ run_next_test();
+ do_test_finished();
+ },
+};
+
+/*
+ * Support for testing valid multiplexing of streams
+ */
+
+var multiplexContent = generateContent(30 * 1024);
+var completed_channels = [];
+function register_completed_channel(listener) {
+ completed_channels.push(listener);
+ if (completed_channels.length == 2) {
+ Assert.notEqual(
+ completed_channels[0].streamID,
+ completed_channels[1].streamID
+ );
+ run_next_test();
+ do_test_finished();
+ }
+}
+
+/* Listener class to control the testing of multiplexing */
+var Http2MultiplexListener = function() {};
+
+Http2MultiplexListener.prototype = new Http2CheckListener();
+
+Http2MultiplexListener.prototype.streamID = 0;
+Http2MultiplexListener.prototype.buffer = "";
+
+Http2MultiplexListener.prototype.onDataAvailable = function(
+ request,
+ stream,
+ off,
+ cnt
+) {
+ this.onDataAvailableFired = true;
+ this.isHttp2Connection = checkIsHttp2(request);
+ this.streamID = parseInt(request.getResponseHeader("X-Http2-StreamID"));
+ var data = read_stream(stream, cnt);
+ this.buffer = this.buffer.concat(data);
+};
+
+Http2MultiplexListener.prototype.onStopRequest = function(request, status) {
+ Assert.ok(this.onStartRequestFired);
+ Assert.ok(this.onDataAvailableFired);
+ Assert.ok(this.isHttp2Connection);
+ Assert.ok(this.buffer == multiplexContent);
+
+ // This is what does most of the hard work for us
+ register_completed_channel(this);
+};
+
+// Does the appropriate checks for header gatewaying
+var Http2HeaderListener = function(name, callback) {
+ this.name = name;
+ this.callback = callback;
+};
+
+Http2HeaderListener.prototype = new Http2CheckListener();
+Http2HeaderListener.prototype.value = "";
+
+Http2HeaderListener.prototype.onDataAvailable = function(
+ request,
+ stream,
+ off,
+ cnt
+) {
+ this.onDataAvailableFired = true;
+ this.isHttp2Connection = checkIsHttp2(request);
+ var hvalue = request.getResponseHeader(this.name);
+ Assert.notEqual(hvalue, "");
+ this.callback(hvalue);
+ read_stream(stream, cnt);
+};
+
+var Http2PushListener = function(shouldBePushed) {
+ this.shouldBePushed = shouldBePushed;
+};
+
+Http2PushListener.prototype = new Http2CheckListener();
+
+Http2PushListener.prototype.onDataAvailable = function(
+ request,
+ stream,
+ off,
+ cnt
+) {
+ this.onDataAvailableFired = true;
+ this.isHttp2Connection = checkIsHttp2(request);
+ if (
+ request.originalURI.spec ==
+ "https://localhost:" + serverPort + "/push.js" ||
+ request.originalURI.spec ==
+ "https://localhost:" + serverPort + "/push2.js" ||
+ request.originalURI.spec == "https://localhost:" + serverPort + "/push5.js"
+ ) {
+ Assert.equal(
+ request.getResponseHeader("pushed"),
+ this.shouldBePushed ? "yes" : "no"
+ );
+ }
+ read_stream(stream, cnt);
+};
+
+const pushHdrTxt =
+ "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
+const pullHdrTxt = pushHdrTxt
+ .split("")
+ .reverse()
+ .join("");
+
+function checkContinuedHeaders(getHeader, headerPrefix, headerText) {
+ for (var i = 0; i < 265; i++) {
+ Assert.equal(getHeader(headerPrefix + 1), headerText);
+ }
+}
+
+var Http2ContinuedHeaderListener = function() {};
+
+Http2ContinuedHeaderListener.prototype = new Http2CheckListener();
+
+Http2ContinuedHeaderListener.prototype.onStopsLeft = 2;
+
+Http2ContinuedHeaderListener.prototype.QueryInterface = ChromeUtils.generateQI([
+ "nsIHttpPushListener",
+ "nsIStreamListener",
+]);
+
+Http2ContinuedHeaderListener.prototype.getInterface = function(aIID) {
+ return this.QueryInterface(aIID);
+};
+
+Http2ContinuedHeaderListener.prototype.onDataAvailable = function(
+ request,
+ stream,
+ off,
+ cnt
+) {
+ this.onDataAvailableFired = true;
+ this.isHttp2Connection = checkIsHttp2(request);
+ if (
+ request.originalURI.spec ==
+ "https://localhost:" + serverPort + "/continuedheaders"
+ ) {
+ // This is the original request, so the only one where we'll have continued response headers
+ checkContinuedHeaders(
+ request.getResponseHeader,
+ "X-Pull-Test-Header-",
+ pullHdrTxt
+ );
+ }
+ read_stream(stream, cnt);
+};
+
+Http2ContinuedHeaderListener.prototype.onStopRequest = function(
+ request,
+ status
+) {
+ Assert.ok(this.onStartRequestFired);
+ Assert.ok(Components.isSuccessCode(status));
+ Assert.ok(this.onDataAvailableFired);
+ Assert.ok(this.isHttp2Connection);
+
+ --this.onStopsLeft;
+ if (this.onStopsLeft === 0) {
+ run_next_test();
+ do_test_finished();
+ }
+};
+
+Http2ContinuedHeaderListener.prototype.onPush = function(
+ associatedChannel,
+ pushChannel
+) {
+ Assert.equal(
+ associatedChannel.originalURI.spec,
+ "https://localhost:" + serverPort + "/continuedheaders"
+ );
+ Assert.equal(pushChannel.getRequestHeader("x-pushed-request"), "true");
+ checkContinuedHeaders(
+ pushChannel.getRequestHeader,
+ "X-Push-Test-Header-",
+ pushHdrTxt
+ );
+
+ pushChannel.asyncOpen(this);
+};
+
+// Does the appropriate checks for a large GET response
+var Http2BigListener = function() {};
+
+Http2BigListener.prototype = new Http2CheckListener();
+Http2BigListener.prototype.buffer = "";
+
+Http2BigListener.prototype.onDataAvailable = function(
+ request,
+ stream,
+ off,
+ cnt
+) {
+ this.onDataAvailableFired = true;
+ this.isHttp2Connection = checkIsHttp2(request);
+ this.buffer = this.buffer.concat(read_stream(stream, cnt));
+ // We know the server should send us the same data as our big post will be,
+ // so the md5 should be the same
+ Assert.equal(bigListenerMD5, request.getResponseHeader("X-Expected-MD5"));
+};
+
+Http2BigListener.prototype.onStopRequest = function(request, status) {
+ Assert.ok(this.onStartRequestFired);
+ Assert.ok(this.onDataAvailableFired);
+ Assert.ok(this.isHttp2Connection);
+
+ // Don't want to flood output, so don't use do_check_eq
+ Assert.ok(this.buffer == bigListenerData);
+
+ run_next_test();
+ do_test_finished();
+};
+
+var Http2HugeSuspendedListener = function() {};
+
+Http2HugeSuspendedListener.prototype = new Http2CheckListener();
+Http2HugeSuspendedListener.prototype.count = 0;
+
+Http2HugeSuspendedListener.prototype.onDataAvailable = function(
+ request,
+ stream,
+ off,
+ cnt
+) {
+ this.onDataAvailableFired = true;
+ this.isHttp2Connection = checkIsHttp2(request);
+ this.count += cnt;
+ read_stream(stream, cnt);
+};
+
+Http2HugeSuspendedListener.prototype.onStopRequest = function(request, status) {
+ Assert.ok(this.onStartRequestFired);
+ Assert.ok(this.onDataAvailableFired);
+ Assert.ok(this.isHttp2Connection);
+ Assert.equal(this.count, 1024 * 1024 * 1); // 1mb of data expected
+ run_next_test();
+ do_test_finished();
+};
+
+// Does the appropriate checks for POSTs
+var Http2PostListener = function(expected_md5) {
+ this.expected_md5 = expected_md5;
+};
+
+Http2PostListener.prototype = new Http2CheckListener();
+Http2PostListener.prototype.expected_md5 = "";
+
+Http2PostListener.prototype.onDataAvailable = function(
+ request,
+ stream,
+ off,
+ cnt
+) {
+ this.onDataAvailableFired = true;
+ this.isHttp2Connection = checkIsHttp2(request);
+ read_stream(stream, cnt);
+ Assert.equal(
+ this.expected_md5,
+ request.getResponseHeader("X-Calculated-MD5")
+ );
+};
+
+function makeChan(url) {
+ return NetUtil.newChannel({
+ uri: url,
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+}
+
+var ResumeStalledChannelListener = function() {};
+
+ResumeStalledChannelListener.prototype = {
+ onStartRequestFired: false,
+ onDataAvailableFired: false,
+ isHttp2Connection: false,
+ shouldBeHttp2: true,
+ resumable: null,
+
+ onStartRequest: function testOnStartRequest(request) {
+ this.onStartRequestFired = true;
+ if (!Components.isSuccessCode(request.status)) {
+ do_throw("Channel should have a success code! (" + request.status + ")");
+ }
+
+ Assert.ok(request instanceof Ci.nsIHttpChannel);
+ Assert.equal(request.responseStatus, 200);
+ Assert.equal(request.requestSucceeded, true);
+ },
+
+ onDataAvailable: function testOnDataAvailable(request, stream, off, cnt) {
+ this.onDataAvailableFired = true;
+ this.isHttp2Connection = checkIsHttp2(request);
+ read_stream(stream, cnt);
+ },
+
+ onStopRequest: function testOnStopRequest(request, status) {
+ Assert.ok(this.onStartRequestFired);
+ Assert.ok(Components.isSuccessCode(status));
+ Assert.ok(this.onDataAvailableFired);
+ Assert.ok(this.isHttp2Connection == this.shouldBeHttp2);
+ this.resumable.resume();
+ },
+};
+
+// test a large download that creates stream flow control and
+// confirm we can do another independent stream while the download
+// stream is stuck
+function test_http2_blocking_download() {
+ var chan = makeChan("https://localhost:" + serverPort + "/bigdownload");
+ var internalChannel = chan.QueryInterface(Ci.nsIHttpChannelInternal);
+ internalChannel.initialRwin = 500000; // make the stream.suspend push back in h2
+ var listener = new Http2CheckListener();
+ listener.expected = 3 * 1024 * 1024;
+ chan.asyncOpen(listener);
+ chan.suspend();
+ // wait 5 seconds so that stream flow control kicks in and then see if we
+ // can do a basic transaction (i.e. session not blocked). afterwards resume
+ // channel
+ do_timeout(5000, function() {
+ var simpleChannel = makeChan("https://localhost:" + serverPort + "/");
+ var sl = new ResumeStalledChannelListener();
+ sl.resumable = chan;
+ simpleChannel.asyncOpen(sl);
+ });
+}
+
+// Make sure we make a HTTP2 connection and both us and the server mark it as such
+function test_http2_basic() {
+ var chan = makeChan("https://localhost:" + serverPort + "/");
+ var listener = new Http2CheckListener();
+ chan.asyncOpen(listener);
+}
+
+function test_http2_basic_unblocked_dep() {
+ var chan = makeChan(
+ "https://localhost:" + serverPort + "/basic_unblocked_dep"
+ );
+ var cos = chan.QueryInterface(Ci.nsIClassOfService);
+ cos.addClassFlags(Ci.nsIClassOfService.Unblocked);
+ var listener = new Http2CheckListener();
+ chan.asyncOpen(listener);
+}
+
+// make sure we don't use h2 when disallowed
+function test_http2_nospdy() {
+ var chan = makeChan("https://localhost:" + serverPort + "/");
+ var listener = new Http2CheckListener();
+ var internalChannel = chan.QueryInterface(Ci.nsIHttpChannelInternal);
+ internalChannel.allowSpdy = false;
+ listener.shouldBeHttp2 = false;
+ chan.asyncOpen(listener);
+}
+
+// Support for making sure XHR works over SPDY
+function checkXhr(xhr) {
+ if (xhr.readyState != 4) {
+ return;
+ }
+
+ Assert.equal(xhr.status, 200);
+ Assert.equal(checkIsHttp2(xhr), true);
+ run_next_test();
+ do_test_finished();
+}
+
+// Fires off an XHR request over h2
+function test_http2_xhr() {
+ var req = new XMLHttpRequest();
+ req.open("GET", "https://localhost:" + serverPort + "/", true);
+ req.addEventListener("readystatechange", function(evt) {
+ checkXhr(req);
+ });
+ req.send(null);
+}
+
+var concurrent_channels = [];
+
+var Http2ConcurrentListener = function() {};
+
+Http2ConcurrentListener.prototype = new Http2CheckListener();
+Http2ConcurrentListener.prototype.count = 0;
+Http2ConcurrentListener.prototype.target = 0;
+Http2ConcurrentListener.prototype.reset = 0;
+Http2ConcurrentListener.prototype.recvdHdr = 0;
+
+Http2ConcurrentListener.prototype.onStopRequest = function(request, status) {
+ this.count++;
+ Assert.ok(this.isHttp2Connection);
+ if (this.recvdHdr > 0) {
+ Assert.equal(request.getResponseHeader("X-Recvd"), this.recvdHdr);
+ }
+
+ if (this.count == this.target) {
+ if (this.reset > 0) {
+ prefs.setIntPref("network.http.spdy.default-concurrent", this.reset);
+ }
+ run_next_test();
+ do_test_finished();
+ }
+};
+
+function test_http2_concurrent() {
+ var concurrent_listener = new Http2ConcurrentListener();
+ concurrent_listener.target = 201;
+ concurrent_listener.reset = prefs.getIntPref(
+ "network.http.spdy.default-concurrent"
+ );
+ prefs.setIntPref("network.http.spdy.default-concurrent", 100);
+
+ for (var i = 0; i < concurrent_listener.target; i++) {
+ concurrent_channels[i] = makeChan(
+ "https://localhost:" + serverPort + "/750ms"
+ );
+ concurrent_channels[i].loadFlags = Ci.nsIRequest.LOAD_BYPASS_CACHE;
+ concurrent_channels[i].asyncOpen(concurrent_listener);
+ }
+}
+
+function test_http2_concurrent_post() {
+ var concurrent_listener = new Http2ConcurrentListener();
+ concurrent_listener.target = 8;
+ concurrent_listener.recvdHdr = posts[2].length;
+ concurrent_listener.reset = prefs.getIntPref(
+ "network.http.spdy.default-concurrent"
+ );
+ prefs.setIntPref("network.http.spdy.default-concurrent", 3);
+
+ for (var i = 0; i < concurrent_listener.target; i++) {
+ concurrent_channels[i] = makeChan(
+ "https://localhost:" + serverPort + "/750msPost"
+ );
+ concurrent_channels[i].loadFlags = Ci.nsIRequest.LOAD_BYPASS_CACHE;
+ var stream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(
+ Ci.nsIStringInputStream
+ );
+ stream.data = posts[2];
+ var uchan = concurrent_channels[i].QueryInterface(Ci.nsIUploadChannel);
+ uchan.setUploadStream(stream, "text/plain", stream.available());
+ concurrent_channels[i].requestMethod = "POST";
+ concurrent_channels[i].asyncOpen(concurrent_listener);
+ }
+}
+
+// Test to make sure we get multiplexing right
+function test_http2_multiplex() {
+ var chan1 = makeChan("https://localhost:" + serverPort + "/multiplex1");
+ var chan2 = makeChan("https://localhost:" + serverPort + "/multiplex2");
+ var listener1 = new Http2MultiplexListener();
+ var listener2 = new Http2MultiplexListener();
+ chan1.asyncOpen(listener1);
+ chan2.asyncOpen(listener2);
+}
+
+// Test to make sure we gateway non-standard headers properly
+function test_http2_header() {
+ var chan = makeChan("https://localhost:" + serverPort + "/header");
+ var hvalue = "Headers are fun";
+ chan.setRequestHeader("X-Test-Header", hvalue, false);
+ var listener = new Http2HeaderListener("X-Received-Test-Header", function(
+ received_hvalue
+ ) {
+ Assert.equal(received_hvalue, hvalue);
+ });
+ chan.asyncOpen(listener);
+}
+
+// Test to make sure cookies are split into separate fields before compression
+function test_http2_cookie_crumbling() {
+ var chan = makeChan("https://localhost:" + serverPort + "/cookie_crumbling");
+ var cookiesSent = ["a=b", "c=d01234567890123456789", "e=f"].sort();
+ chan.setRequestHeader("Cookie", cookiesSent.join("; "), false);
+ var listener = new Http2HeaderListener("X-Received-Header-Pairs", function(
+ pairsReceived
+ ) {
+ var cookiesReceived = JSON.parse(pairsReceived)
+ .filter(function(pair) {
+ return pair[0] == "cookie";
+ })
+ .map(function(pair) {
+ return pair[1];
+ })
+ .sort();
+ Assert.equal(cookiesReceived.length, cookiesSent.length);
+ cookiesReceived.forEach(function(cookieReceived, index) {
+ Assert.equal(cookiesSent[index], cookieReceived);
+ });
+ });
+ chan.asyncOpen(listener);
+}
+
+function test_http2_push1() {
+ var chan = makeChan("https://localhost:" + serverPort + "/push");
+ chan.loadGroup = loadGroup;
+ var listener = new Http2PushListener(true);
+ chan.asyncOpen(listener);
+}
+
+function test_http2_push2() {
+ var chan = makeChan("https://localhost:" + serverPort + "/push.js");
+ chan.loadGroup = loadGroup;
+ var listener = new Http2PushListener(true);
+ chan.asyncOpen(listener);
+}
+
+function test_http2_push3() {
+ var chan = makeChan("https://localhost:" + serverPort + "/push2");
+ chan.loadGroup = loadGroup;
+ var listener = new Http2PushListener(true);
+ chan.asyncOpen(listener);
+}
+
+function test_http2_push4() {
+ var chan = makeChan("https://localhost:" + serverPort + "/push2.js");
+ chan.loadGroup = loadGroup;
+ var listener = new Http2PushListener(true);
+ chan.asyncOpen(listener);
+}
+
+function test_http2_push5() {
+ var chan = makeChan("https://localhost:" + serverPort + "/push5");
+ chan.loadGroup = loadGroup;
+ var listener = new Http2PushListener(true);
+ chan.asyncOpen(listener);
+}
+
+function test_http2_push6() {
+ var chan = makeChan("https://localhost:" + serverPort + "/push5.js");
+ chan.loadGroup = loadGroup;
+ var listener = new Http2PushListener(true);
+ chan.asyncOpen(listener);
+}
+
+// this is a basic test where the server sends a simple document with 2 header
+// blocks. bug 1027364
+function test_http2_doubleheader() {
+ var chan = makeChan("https://localhost:" + serverPort + "/doubleheader");
+ var listener = new Http2CheckListener();
+ chan.asyncOpen(listener);
+}
+
+// Make sure we handle GETs that cover more than 2 frames properly
+function test_http2_big() {
+ var chan = makeChan("https://localhost:" + serverPort + "/big");
+ var listener = new Http2BigListener();
+ chan.asyncOpen(listener);
+}
+
+function test_http2_huge_suspended() {
+ var chan = makeChan("https://localhost:" + serverPort + "/huge");
+ var listener = new Http2HugeSuspendedListener();
+ chan.asyncOpen(listener);
+ chan.suspend();
+ do_timeout(500, chan.resume);
+}
+
+// Support for doing a POST
+function do_post(content, chan, listener, method) {
+ var stream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(
+ Ci.nsIStringInputStream
+ );
+ stream.data = content;
+
+ var uchan = chan.QueryInterface(Ci.nsIUploadChannel);
+ uchan.setUploadStream(stream, "text/plain", stream.available());
+
+ chan.requestMethod = method;
+
+ chan.asyncOpen(listener);
+}
+
+// Make sure we can do a simple POST
+function test_http2_post() {
+ var chan = makeChan("https://localhost:" + serverPort + "/post");
+ var listener = new Http2PostListener(md5s[0]);
+ do_post(posts[0], chan, listener, "POST");
+}
+
+// Make sure we can do a simple PATCH
+function test_http2_patch() {
+ var chan = makeChan("https://localhost:" + serverPort + "/patch");
+ var listener = new Http2PostListener(md5s[0]);
+ do_post(posts[0], chan, listener, "PATCH");
+}
+
+// Make sure we can do a POST that covers more than 2 frames
+function test_http2_post_big() {
+ var chan = makeChan("https://localhost:" + serverPort + "/post");
+ var listener = new Http2PostListener(md5s[1]);
+ do_post(posts[1], chan, listener, "POST");
+}
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+var httpserv = null;
+var httpserv2 = null;
+var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
+
+var altsvcClientListener = {
+ onStartRequest: function test_onStartR(request) {
+ Assert.equal(request.status, Cr.NS_OK);
+ },
+
+ onDataAvailable: function test_ODA(request, stream, offset, cnt) {
+ read_stream(stream, cnt);
+ },
+
+ onStopRequest: function test_onStopR(request, status) {
+ var isHttp2Connection = checkIsHttp2(
+ request.QueryInterface(Ci.nsIHttpChannel)
+ );
+ if (!isHttp2Connection) {
+ dump("/altsvc1 not over h2 yet - retry\n");
+ var chan = makeChan(
+ "http://foo.example.com:" + httpserv.identity.primaryPort + "/altsvc1"
+ ).QueryInterface(Ci.nsIHttpChannel);
+ // we use this header to tell the server to issue a altsvc frame for the
+ // speficied origin we will use in the next part of the test
+ chan.setRequestHeader(
+ "x-redirect-origin",
+ "http://foo.example.com:" + httpserv2.identity.primaryPort,
+ false
+ );
+ chan.loadFlags = Ci.nsIRequest.LOAD_BYPASS_CACHE;
+ chan.asyncOpen(altsvcClientListener);
+ } else {
+ Assert.ok(isHttp2Connection);
+ var chan = makeChan(
+ "http://foo.example.com:" + httpserv2.identity.primaryPort + "/altsvc2"
+ ).QueryInterface(Ci.nsIHttpChannel);
+ chan.loadFlags = Ci.nsIRequest.LOAD_BYPASS_CACHE;
+ chan.asyncOpen(altsvcClientListener2);
+ }
+ },
+};
+
+var altsvcClientListener2 = {
+ onStartRequest: function test_onStartR(request) {
+ Assert.equal(request.status, Cr.NS_OK);
+ },
+
+ onDataAvailable: function test_ODA(request, stream, offset, cnt) {
+ read_stream(stream, cnt);
+ },
+
+ onStopRequest: function test_onStopR(request, status) {
+ var isHttp2Connection = checkIsHttp2(
+ request.QueryInterface(Ci.nsIHttpChannel)
+ );
+ if (!isHttp2Connection) {
+ dump("/altsvc2 not over h2 yet - retry\n");
+ var chan = makeChan(
+ "http://foo.example.com:" + httpserv2.identity.primaryPort + "/altsvc2"
+ ).QueryInterface(Ci.nsIHttpChannel);
+ chan.loadFlags = Ci.nsIRequest.LOAD_BYPASS_CACHE;
+ chan.asyncOpen(altsvcClientListener2);
+ } else {
+ Assert.ok(isHttp2Connection);
+ run_next_test();
+ do_test_finished();
+ }
+ },
+};
+
+function altsvcHttp1Server(metadata, response) {
+ response.setStatusLine(metadata.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "text/plain", false);
+ response.setHeader("Connection", "close", false);
+ response.setHeader("Alt-Svc", 'h2=":' + serverPort + '"', false);
+ var body = "this is where a cool kid would write something neat.\n";
+ response.bodyOutputStream.write(body, body.length);
+}
+
+function h1ServerWK(metadata, response) {
+ response.setStatusLine(metadata.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "application/json", false);
+ response.setHeader("Connection", "close", false);
+ response.setHeader("Cache-Control", "no-cache", false);
+ response.setHeader("Access-Control-Allow-Origin", "*", false);
+ response.setHeader("Access-Control-Allow-Method", "GET", false);
+
+ var body = '["http://foo.example.com:' + httpserv.identity.primaryPort + '"]';
+ response.bodyOutputStream.write(body, body.length);
+}
+
+function altsvcHttp1Server2(metadata, response) {
+ // this server should never be used thanks to an alt svc frame from the
+ // h2 server.. but in case of some async lag in setting the alt svc route
+ // up we have it.
+ response.setStatusLine(metadata.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "text/plain", false);
+ response.setHeader("Connection", "close", false);
+ var body = "hanging.\n";
+ response.bodyOutputStream.write(body, body.length);
+}
+
+function h1ServerWK2(metadata, response) {
+ response.setStatusLine(metadata.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "application/json", false);
+ response.setHeader("Connection", "close", false);
+ response.setHeader("Cache-Control", "no-cache", false);
+ response.setHeader("Access-Control-Allow-Origin", "*", false);
+ response.setHeader("Access-Control-Allow-Method", "GET", false);
+
+ var body =
+ '["http://foo.example.com:' + httpserv2.identity.primaryPort + '"]';
+ response.bodyOutputStream.write(body, body.length);
+}
+function test_http2_altsvc() {
+ var chan = makeChan(
+ "http://foo.example.com:" + httpserv.identity.primaryPort + "/altsvc1"
+ ).QueryInterface(Ci.nsIHttpChannel);
+ chan.asyncOpen(altsvcClientListener);
+}
+
+var Http2PushApiListener = function() {};
+
+Http2PushApiListener.prototype = {
+ checksPending: 9, // 4 onDataAvailable and 5 onStop
+
+ getInterface(aIID) {
+ return this.QueryInterface(aIID);
+ },
+
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIHttpPushListener",
+ "nsIStreamListener",
+ ]),
+
+ // nsIHttpPushListener
+ onPush: function onPush(associatedChannel, pushChannel) {
+ Assert.equal(
+ associatedChannel.originalURI.spec,
+ "https://localhost:" + serverPort + "/pushapi1"
+ );
+ Assert.equal(pushChannel.getRequestHeader("x-pushed-request"), "true");
+
+ pushChannel.asyncOpen(this);
+ if (
+ pushChannel.originalURI.spec ==
+ "https://localhost:" + serverPort + "/pushapi1/2"
+ ) {
+ pushChannel.cancel(Cr.NS_ERROR_ABORT);
+ } else if (
+ pushChannel.originalURI.spec ==
+ "https://localhost:" + serverPort + "/pushapi1/3"
+ ) {
+ Assert.ok(pushChannel.getRequestHeader("Accept-Encoding").includes("br"));
+ }
+ },
+
+ // normal Channel listeners
+ onStartRequest: function pushAPIOnStart(request) {},
+
+ onDataAvailable: function pushAPIOnDataAvailable(
+ request,
+ stream,
+ offset,
+ cnt
+ ) {
+ Assert.notEqual(
+ request.originalURI.spec,
+ "https://localhost:" + serverPort + "/pushapi1/2"
+ );
+
+ var data = read_stream(stream, cnt);
+
+ if (
+ request.originalURI.spec ==
+ "https://localhost:" + serverPort + "/pushapi1"
+ ) {
+ Assert.equal(data[0], "0");
+ --this.checksPending;
+ } else if (
+ request.originalURI.spec ==
+ "https://localhost:" + serverPort + "/pushapi1/1"
+ ) {
+ Assert.equal(data[0], "1");
+ --this.checksPending; // twice
+ } else if (
+ request.originalURI.spec ==
+ "https://localhost:" + serverPort + "/pushapi1/3"
+ ) {
+ Assert.equal(data[0], "3");
+ --this.checksPending;
+ } else {
+ Assert.equal(true, false);
+ }
+ },
+
+ onStopRequest: function test_onStopR(request, status) {
+ if (
+ request.originalURI.spec ==
+ "https://localhost:" + serverPort + "/pushapi1/2"
+ ) {
+ Assert.equal(request.status, Cr.NS_ERROR_ABORT);
+ } else {
+ Assert.equal(request.status, Cr.NS_OK);
+ }
+
+ --this.checksPending; // 5 times - one for each push plus the pull
+ if (!this.checksPending) {
+ run_next_test();
+ do_test_finished();
+ }
+ },
+};
+
+// pushAPI testcase 1 expects
+// 1 to pull /pushapi1 with 0
+// 2 to see /pushapi1/1 with 1
+// 3 to see /pushapi1/1 with 1 (again)
+// 4 to see /pushapi1/2 that it will cancel
+// 5 to see /pushapi1/3 with 3 with brotli
+
+function test_http2_pushapi_1() {
+ var chan = makeChan("https://localhost:" + serverPort + "/pushapi1");
+ chan.loadGroup = loadGroup;
+ var listener = new Http2PushApiListener();
+ chan.notificationCallbacks = listener;
+ chan.asyncOpen(listener);
+}
+
+var WrongSuiteListener = function() {};
+WrongSuiteListener.prototype = new Http2CheckListener();
+WrongSuiteListener.prototype.shouldBeHttp2 = false;
+WrongSuiteListener.prototype.onStopRequest = function(request, status) {
+ prefs.setBoolPref("security.ssl3.ecdhe_rsa_aes_128_gcm_sha256", true);
+ prefs.clearUserPref("security.tls.version.max");
+ Http2CheckListener.prototype.onStopRequest.call(this);
+};
+
+// test that we use h1 without the mandatory cipher suite available when
+// offering at most tls1.2
+function test_http2_wrongsuite_tls12() {
+ prefs.setBoolPref("security.ssl3.ecdhe_rsa_aes_128_gcm_sha256", false);
+ prefs.setIntPref("security.tls.version.max", 3);
+ var chan = makeChan("https://localhost:" + serverPort + "/wrongsuite");
+ chan.loadFlags =
+ Ci.nsIRequest.LOAD_FRESH_CONNECTION |
+ Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI;
+ var listener = new WrongSuiteListener();
+ chan.asyncOpen(listener);
+}
+
+// test that we use h2 when offering tls1.3 or higher regardless of if the
+// mandatory cipher suite is available
+function test_http2_wrongsuite_tls13() {
+ prefs.setBoolPref("security.ssl3.ecdhe_rsa_aes_128_gcm_sha256", false);
+ var chan = makeChan("https://localhost:" + serverPort + "/wrongsuite");
+ chan.loadFlags =
+ Ci.nsIRequest.LOAD_FRESH_CONNECTION |
+ Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI;
+ var listener = new WrongSuiteListener();
+ listener.shouldBeHttp2 = true;
+ chan.asyncOpen(listener);
+}
+
+function test_http2_h11required_stream() {
+ var chan = makeChan(
+ "https://localhost:" + serverPort + "/h11required_stream"
+ );
+ var listener = new Http2CheckListener();
+ listener.shouldBeHttp2 = false;
+ chan.asyncOpen(listener);
+}
+
+function H11RequiredSessionListener() {}
+H11RequiredSessionListener.prototype = new Http2CheckListener();
+
+H11RequiredSessionListener.prototype.onStopRequest = function(request, status) {
+ var streamReused = request.getResponseHeader("X-H11Required-Stream-Ok");
+ Assert.equal(streamReused, "yes");
+
+ Assert.ok(this.onStartRequestFired);
+ Assert.ok(this.onDataAvailableFired);
+ Assert.ok(this.isHttp2Connection == this.shouldBeHttp2);
+
+ run_next_test();
+ do_test_finished();
+};
+
+function test_http2_h11required_session() {
+ var chan = makeChan(
+ "https://localhost:" + serverPort + "/h11required_session"
+ );
+ var listener = new H11RequiredSessionListener();
+ listener.shouldBeHttp2 = false;
+ chan.asyncOpen(listener);
+}
+
+function test_http2_retry_rst() {
+ var chan = makeChan("https://localhost:" + serverPort + "/rstonce");
+ var listener = new Http2CheckListener();
+ chan.asyncOpen(listener);
+}
+
+function test_http2_continuations() {
+ var chan = makeChan("https://localhost:" + serverPort + "/continuedheaders");
+ chan.loadGroup = loadGroup;
+ var listener = new Http2ContinuedHeaderListener();
+ chan.notificationCallbacks = listener;
+ chan.asyncOpen(listener);
+}
+
+function Http2IllegalHpackValidationListener() {}
+Http2IllegalHpackValidationListener.prototype = new Http2CheckListener();
+Http2IllegalHpackValidationListener.prototype.shouldGoAway = false;
+
+Http2IllegalHpackValidationListener.prototype.onStopRequest = function(
+ request,
+ status
+) {
+ var wentAway = request.getResponseHeader("X-Did-Goaway") === "yes";
+ Assert.equal(wentAway, this.shouldGoAway);
+
+ Assert.ok(this.onStartRequestFired);
+ Assert.ok(this.onDataAvailableFired);
+ Assert.ok(this.isHttp2Connection == this.shouldBeHttp2);
+
+ run_next_test();
+ do_test_finished();
+};
+
+function Http2IllegalHpackListener() {}
+Http2IllegalHpackListener.prototype = new Http2CheckListener();
+Http2IllegalHpackListener.prototype.shouldGoAway = false;
+
+Http2IllegalHpackListener.prototype.onStopRequest = function(request, status) {
+ var chan = makeChan(
+ "https://localhost:" + serverPort + "/illegalhpack_validate"
+ );
+ var listener = new Http2IllegalHpackValidationListener();
+ listener.shouldGoAway = this.shouldGoAway;
+ chan.asyncOpen(listener);
+};
+
+function test_http2_illegalhpacksoft() {
+ var chan = makeChan("https://localhost:" + serverPort + "/illegalhpacksoft");
+ var listener = new Http2IllegalHpackListener();
+ listener.shouldGoAway = false;
+ listener.shouldSucceed = false;
+ chan.asyncOpen(listener);
+}
+
+function test_http2_illegalhpackhard() {
+ var chan = makeChan("https://localhost:" + serverPort + "/illegalhpackhard");
+ var listener = new Http2IllegalHpackListener();
+ listener.shouldGoAway = true;
+ listener.shouldSucceed = false;
+ chan.asyncOpen(listener);
+}
+
+function test_http2_folded_header() {
+ var chan = makeChan("https://localhost:" + serverPort + "/foldedheader");
+ chan.loadGroup = loadGroup;
+ var listener = new Http2CheckListener();
+ listener.shouldSucceed = false;
+ chan.asyncOpen(listener);
+}
+
+function test_http2_empty_data() {
+ var chan = makeChan("https://localhost:" + serverPort + "/emptydata");
+ var listener = new Http2CheckListener();
+ chan.asyncOpen(listener);
+}
+
+function test_http2_push_firstparty1() {
+ var chan = makeChan("https://localhost:" + serverPort + "/push");
+ chan.loadGroup = loadGroup;
+ chan.loadInfo.originAttributes = { firstPartyDomain: "foo.com" };
+ var listener = new Http2PushListener(true);
+ chan.asyncOpen(listener);
+}
+
+function test_http2_push_firstparty2() {
+ var chan = makeChan("https://localhost:" + serverPort + "/push.js");
+ chan.loadGroup = loadGroup;
+ chan.loadInfo.originAttributes = { firstPartyDomain: "bar.com" };
+ var listener = new Http2PushListener(false);
+ chan.asyncOpen(listener);
+}
+
+function test_http2_push_firstparty3() {
+ var chan = makeChan("https://localhost:" + serverPort + "/push.js");
+ chan.loadGroup = loadGroup;
+ chan.loadInfo.originAttributes = { firstPartyDomain: "foo.com" };
+ var listener = new Http2PushListener(true);
+ chan.asyncOpen(listener);
+}
+
+function test_http2_push_userContext1() {
+ var chan = makeChan("https://localhost:" + serverPort + "/push");
+ chan.loadGroup = loadGroup;
+ chan.loadInfo.originAttributes = { userContextId: 1 };
+ var listener = new Http2PushListener(true);
+ chan.asyncOpen(listener);
+}
+
+function test_http2_push_userContext2() {
+ var chan = makeChan("https://localhost:" + serverPort + "/push.js");
+ chan.loadGroup = loadGroup;
+ chan.loadInfo.originAttributes = { userContextId: 2 };
+ var listener = new Http2PushListener(false);
+ chan.asyncOpen(listener);
+}
+
+function test_http2_push_userContext3() {
+ var chan = makeChan("https://localhost:" + serverPort + "/push.js");
+ chan.loadGroup = loadGroup;
+ chan.loadInfo.originAttributes = { userContextId: 1 };
+ var listener = new Http2PushListener(true);
+ chan.asyncOpen(listener);
+}
+
+function test_http2_status_phrase() {
+ var chan = makeChan("https://localhost:" + serverPort + "/statusphrase");
+ var listener = new Http2CheckListener();
+ listener.shouldSucceed = false;
+ chan.asyncOpen(listener);
+}
+
+var PulledDiskCacheListener = function() {};
+PulledDiskCacheListener.prototype = new Http2CheckListener();
+PulledDiskCacheListener.prototype.EXPECTED_DATA = "this was pulled via h2";
+PulledDiskCacheListener.prototype.readData = "";
+PulledDiskCacheListener.prototype.onDataAvailable = function testOnDataAvailable(
+ request,
+ stream,
+ off,
+ cnt
+) {
+ this.onDataAvailableFired = true;
+ this.isHttp2Connection = checkIsHttp2(request);
+ this.accum += cnt;
+ this.readData += read_stream(stream, cnt);
+};
+PulledDiskCacheListener.prototype.onStopRequest = function testOnStopRequest(
+ request,
+ status
+) {
+ Assert.equal(this.EXPECTED_DATA, this.readData);
+ Http2CheckListener.prorotype.onStopRequest.call(this, request, status);
+};
+
+const DISK_CACHE_DATA = "this is from disk cache";
+
+var FromDiskCacheListener = function() {};
+FromDiskCacheListener.prototype = {
+ onStartRequestFired: false,
+ onDataAvailableFired: false,
+ readData: "",
+
+ onStartRequest: function testOnStartRequest(request) {
+ this.onStartRequestFired = true;
+ if (!Components.isSuccessCode(request.status)) {
+ do_throw("Channel should have a success code! (" + request.status + ")");
+ }
+
+ Assert.ok(request instanceof Ci.nsIHttpChannel);
+ Assert.ok(request.requestSucceeded);
+ Assert.equal(request.responseStatus, 200);
+ },
+
+ onDataAvailable: function testOnDataAvailable(request, stream, off, cnt) {
+ this.onDataAvailableFired = true;
+ this.readData += read_stream(stream, cnt);
+ },
+
+ onStopRequest: function testOnStopRequest(request, status) {
+ Assert.ok(this.onStartRequestFired);
+ Assert.ok(Components.isSuccessCode(status));
+ Assert.ok(this.onDataAvailableFired);
+ Assert.equal(this.readData, DISK_CACHE_DATA);
+
+ evict_cache_entries("disk");
+ syncWithCacheIOThread(() => {
+ // Now that we know the entry is out of the disk cache, check to make sure
+ // we don't have this hiding in the push cache somewhere - if we do, it
+ // didn't get cancelled, and we have a bug.
+ var chan = makeChan("https://localhost:" + serverPort + "/diskcache");
+ var listener = new PulledDiskCacheListener();
+ chan.loadGroup = loadGroup;
+ chan.asyncOpen(listener);
+ });
+ },
+};
+
+var Http2DiskCachePushListener = function() {};
+
+Http2DiskCachePushListener.prototype = new Http2CheckListener();
+
+Http2DiskCachePushListener.onStopRequest = function(request, status) {
+ Assert.ok(this.onStartRequestFired);
+ Assert.ok(Components.isSuccessCode(status));
+ Assert.ok(this.onDataAvailableFired);
+ Assert.ok(this.isHttp2Connection == this.shouldBeHttp2);
+
+ // Now we need to open a channel to ensure we get data from the disk cache
+ // for the pushed item, instead of from the push cache.
+ var chan = makeChan("https://localhost:" + serverPort + "/diskcache");
+ var listener = new FromDiskCacheListener();
+ chan.loadGroup = loadGroup;
+ chan.asyncOpen(listener);
+};
+
+function continue_test_http2_disk_cache_push(status, entry, appCache) {
+ // TODO - store stuff in cache entry, then open an h2 channel that will push
+ // this, once that completes, open a channel for the cache entry we made and
+ // ensure it came from disk cache, not the push cache.
+ var outputStream = entry.openOutputStream(0, -1);
+ outputStream.write(DISK_CACHE_DATA, DISK_CACHE_DATA.length);
+
+ // Now we open our URL that will push data for the URL above
+ var chan = makeChan("https://localhost:" + serverPort + "/pushindisk");
+ var listener = new Http2DiskCachePushListener();
+ chan.loadGroup = loadGroup;
+ chan.asyncOpen(listener);
+}
+
+function test_http2_disk_cache_push() {
+ asyncOpenCacheEntry(
+ "https://localhost:" + serverPort + "/diskcache",
+ "disk",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ null,
+ continue_test_http2_disk_cache_push,
+ false
+ );
+}
+
+function test_complete() {
+ resetPrefs();
+ do_test_pending();
+ httpserv.stop(do_test_finished);
+ do_test_pending();
+ httpserv2.stop(do_test_finished);
+
+ do_test_finished();
+ do_timeout(0, run_next_test);
+}
+
+var Http2DoublepushListener = function() {};
+Http2DoublepushListener.prototype = new Http2CheckListener();
+Http2DoublepushListener.prototype.onStopRequest = function(request, status) {
+ Assert.ok(this.onStartRequestFired);
+ Assert.ok(Components.isSuccessCode(status));
+ Assert.ok(this.onDataAvailableFired);
+ Assert.ok(this.isHttp2Connection == this.shouldBeHttp2);
+
+ var chan = makeChan("https://localhost:" + serverPort + "/doublypushed");
+ var listener = new Http2DoublypushedListener();
+ chan.loadGroup = loadGroup;
+ chan.asyncOpen(listener);
+};
+
+var Http2DoublypushedListener = function() {};
+Http2DoublypushedListener.prototype = new Http2CheckListener();
+Http2DoublypushedListener.prototype.readData = "";
+Http2DoublypushedListener.prototype.onDataAvailable = function(
+ request,
+ stream,
+ off,
+ cnt
+) {
+ this.onDataAvailableFired = true;
+ this.accum += cnt;
+ this.readData += read_stream(stream, cnt);
+};
+Http2DoublypushedListener.prototype.onStopRequest = function(request, status) {
+ Assert.ok(this.onStartRequestFired);
+ Assert.ok(Components.isSuccessCode(status));
+ Assert.ok(this.onDataAvailableFired);
+ Assert.equal(this.readData, "pushed");
+
+ run_next_test();
+ do_test_finished();
+};
+
+function test_http2_doublepush() {
+ var chan = makeChan("https://localhost:" + serverPort + "/doublepush");
+ var listener = new Http2DoublepushListener();
+ chan.loadGroup = loadGroup;
+ chan.asyncOpen(listener);
+}
+
+// hack - the header test resets the multiplex object on the server,
+// so make sure header is always run before the multiplex test.
+//
+// make sure post_big runs first to test race condition in restarting
+// a stalled stream when a SETTINGS frame arrives
+var tests = [
+ test_http2_post_big,
+ test_http2_basic,
+ test_http2_concurrent,
+ test_http2_concurrent_post,
+ test_http2_basic_unblocked_dep,
+ test_http2_nospdy,
+ test_http2_push1,
+ test_http2_push2,
+ test_http2_push3,
+ test_http2_push4,
+ test_http2_push5,
+ test_http2_push6,
+ test_http2_altsvc,
+ test_http2_doubleheader,
+ test_http2_xhr,
+ test_http2_header,
+ test_http2_cookie_crumbling,
+ test_http2_multiplex,
+ test_http2_big,
+ test_http2_huge_suspended,
+ test_http2_post,
+ test_http2_patch,
+ test_http2_pushapi_1,
+ test_http2_continuations,
+ test_http2_blocking_download,
+ test_http2_illegalhpacksoft,
+ test_http2_illegalhpackhard,
+ test_http2_folded_header,
+ test_http2_empty_data,
+ test_http2_status_phrase,
+ test_http2_doublepush,
+ test_http2_disk_cache_push,
+ // Add new tests above here - best to add new tests before h1
+ // streams get too involved
+ // These next two must always come in this order
+ test_http2_h11required_stream,
+ test_http2_h11required_session,
+ test_http2_retry_rst,
+ test_http2_wrongsuite_tls12,
+ test_http2_wrongsuite_tls13,
+ test_http2_push_firstparty1,
+ test_http2_push_firstparty2,
+ test_http2_push_firstparty3,
+ test_http2_push_userContext1,
+ test_http2_push_userContext2,
+ test_http2_push_userContext3,
+
+ // cleanup
+ test_complete,
+];
+var current_test = 0;
+
+function run_next_test() {
+ if (current_test < tests.length) {
+ dump("starting test number " + current_test + "\n");
+ tests[current_test]();
+ current_test++;
+ do_test_pending();
+ }
+}
+
+var prefs;
+var spdypref;
+var spdypush;
+var http2pref;
+var altsvcpref1;
+var altsvcpref2;
+var loadGroup;
+var serverPort;
+var speculativeLimit;
+
+function resetPrefs() {
+ prefs.setIntPref("network.http.speculative-parallel-limit", speculativeLimit);
+ prefs.setBoolPref("network.http.spdy.enabled", spdypref);
+ prefs.setBoolPref("network.http.spdy.allow-push", spdypush);
+ prefs.setBoolPref("network.http.spdy.enabled.http2", http2pref);
+ prefs.setBoolPref("network.http.altsvc.enabled", altsvcpref1);
+ prefs.setBoolPref("network.http.altsvc.oe", altsvcpref2);
+ prefs.clearUserPref("network.dns.localDomains");
+ prefs.clearUserPref("network.cookieJarSettings.unblocked_for_testing");
+}
+
+function run_test() {
+ var env = Cc["@mozilla.org/process/environment;1"].getService(
+ Ci.nsIEnvironment
+ );
+ serverPort = env.get("MOZHTTP2_PORT");
+ Assert.notEqual(serverPort, null);
+ dump("using port " + serverPort + "\n");
+
+ // Set to allow the cert presented by our H2 server
+ do_get_profile();
+ prefs = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch);
+ speculativeLimit = prefs.getIntPref(
+ "network.http.speculative-parallel-limit"
+ );
+ prefs.setIntPref("network.http.speculative-parallel-limit", 0);
+
+ // The moz-http2 cert is for foo.example.com and is signed by http2-ca.pem
+ // so add that cert to the trust list as a signing cert. Some older tests in
+ // this suite use localhost with a TOFU exception, but new ones should use
+ // foo.example.com
+ let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(
+ Ci.nsIX509CertDB
+ );
+ addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u");
+
+ // Enable all versions of spdy to see that we auto negotiate http/2
+ spdypref = prefs.getBoolPref("network.http.spdy.enabled");
+ spdypush = prefs.getBoolPref("network.http.spdy.allow-push");
+ http2pref = prefs.getBoolPref("network.http.spdy.enabled.http2");
+ altsvcpref1 = prefs.getBoolPref("network.http.altsvc.enabled");
+ altsvcpref2 = prefs.getBoolPref("network.http.altsvc.oe", true);
+
+ prefs.setBoolPref("network.http.spdy.enabled", true);
+ prefs.setBoolPref("network.http.spdy.allow-push", true);
+ prefs.setBoolPref("network.http.spdy.enabled.http2", true);
+ prefs.setBoolPref("network.http.altsvc.enabled", true);
+ prefs.setBoolPref("network.http.altsvc.oe", true);
+ prefs.setCharPref(
+ "network.dns.localDomains",
+ "foo.example.com, bar.example.com"
+ );
+ prefs.setBoolPref("network.cookieJarSettings.unblocked_for_testing", true);
+
+ loadGroup = Cc["@mozilla.org/network/load-group;1"].createInstance(
+ Ci.nsILoadGroup
+ );
+
+ httpserv = new HttpServer();
+ httpserv.registerPathHandler("/altsvc1", altsvcHttp1Server);
+ httpserv.registerPathHandler("/.well-known/http-opportunistic", h1ServerWK);
+ httpserv.start(-1);
+ httpserv.identity.setPrimary(
+ "http",
+ "foo.example.com",
+ httpserv.identity.primaryPort
+ );
+
+ httpserv2 = new HttpServer();
+ httpserv2.registerPathHandler("/altsvc2", altsvcHttp1Server2);
+ httpserv2.registerPathHandler("/.well-known/http-opportunistic", h1ServerWK2);
+ httpserv2.start(-1);
+ httpserv2.identity.setPrimary(
+ "http",
+ "foo.example.com",
+ httpserv2.identity.primaryPort
+ );
+
+ // And make go!
+ run_next_test();
+}
diff --git a/netwerk/test/unit/test_http3.js b/netwerk/test/unit/test_http3.js
new file mode 100644
index 0000000000..dcadde3c6b
--- /dev/null
+++ b/netwerk/test/unit/test_http3.js
@@ -0,0 +1,569 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+// Generate a post with known pre-calculated md5 sum.
+function generateContent(size) {
+ let content = "";
+ for (let i = 0; i < size; i++) {
+ content += "0";
+ }
+ return content;
+}
+
+let post = generateContent(10);
+
+// Max concurent stream number in neqo is 100.
+// Openning 120 streams will test queuing of streams.
+let number_of_parallel_requests = 120;
+let h1Server = null;
+let h3Route;
+let httpsOrigin;
+let httpOrigin;
+let h3AltSvc;
+
+let prefs;
+
+let tests = [
+ // This test must be the first because it setsup alt-svc connection, that
+ // other tests use.
+ test_https_alt_svc,
+ test_multiple_requests,
+ test_request_cancelled_by_server,
+ test_stream_cancelled_by_necko,
+ test_multiple_request_one_is_cancelled,
+ test_multiple_request_one_is_cancelled_by_necko,
+ test_post,
+ test_patch,
+ test_http_alt_svc,
+ test_slow_receiver,
+ // This test should be at the end, because it will close http3
+ // connection and the transaction will switch to already existing http2
+ // connection.
+ // TODO: Bug 1582667 should try to fix issue with connection being closed.
+ test_version_fallback,
+ testsDone,
+];
+
+let current_test = 0;
+
+function run_next_test() {
+ if (current_test < tests.length) {
+ dump("starting test number " + current_test + "\n");
+ tests[current_test]();
+ current_test++;
+ }
+}
+
+function run_test() {
+ let env = Cc["@mozilla.org/process/environment;1"].getService(
+ Ci.nsIEnvironment
+ );
+ let h2Port = env.get("MOZHTTP2_PORT");
+ Assert.notEqual(h2Port, null);
+ Assert.notEqual(h2Port, "");
+ let h3Port = env.get("MOZHTTP3_PORT");
+ Assert.notEqual(h3Port, null);
+ Assert.notEqual(h3Port, "");
+ h3AltSvc = ":" + h3Port;
+
+ h3Route = "foo.example.com:" + h3Port;
+ do_get_profile();
+ prefs = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch);
+
+ prefs.setBoolPref("network.http.http3.enabled", true);
+ prefs.setCharPref("network.dns.localDomains", "foo.example.com");
+ // We always resolve elements of localDomains as it's hardcoded without the
+ // following pref:
+ prefs.setBoolPref("network.proxy.allow_hijacking_localhost", true);
+
+ // The certificate for the http3server server is for foo.example.com and
+ // is signed by http2-ca.pem so add that cert to the trust list as a
+ // signing cert.
+ let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(
+ Ci.nsIX509CertDB
+ );
+ addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u");
+ httpsOrigin = "https://foo.example.com:" + h2Port + "/";
+
+ h1Server = new HttpServer();
+ h1Server.registerPathHandler("/http3-test", h1Response);
+ h1Server.registerPathHandler("/.well-known/http-opportunistic", h1ServerWK);
+ h1Server.registerPathHandler("/VersionFallback", h1Response);
+ h1Server.start(-1);
+ h1Server.identity.setPrimary(
+ "http",
+ "foo.example.com",
+ h1Server.identity.primaryPort
+ );
+ httpOrigin = "http://foo.example.com:" + h1Server.identity.primaryPort + "/";
+
+ run_next_test();
+}
+
+function h1Response(metadata, response) {
+ response.setStatusLine(metadata.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "text/plain", false);
+ response.setHeader("Connection", "close", false);
+ response.setHeader("Cache-Control", "no-cache", false);
+ response.setHeader("Access-Control-Allow-Origin", "*", false);
+ response.setHeader("Access-Control-Allow-Method", "GET", false);
+ response.setHeader("Access-Control-Allow-Headers", "x-altsvc", false);
+
+ try {
+ let hval = "h3-27=" + metadata.getHeader("x-altsvc");
+ response.setHeader("Alt-Svc", hval, false);
+ } catch (e) {}
+
+ let body = "Q: What did 0 say to 8? A: Nice Belt!\n";
+ response.bodyOutputStream.write(body, body.length);
+}
+
+function h1ServerWK(metadata, response) {
+ response.setStatusLine(metadata.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "application/json", false);
+ response.setHeader("Connection", "close", false);
+ response.setHeader("Cache-Control", "no-cache", false);
+ response.setHeader("Access-Control-Allow-Origin", "*", false);
+ response.setHeader("Access-Control-Allow-Method", "GET", false);
+ response.setHeader("Access-Control-Allow-Headers", "x-altsvc", false);
+
+ let body = '["http://foo.example.com:' + h1Server.identity.primaryPort + '"]';
+ response.bodyOutputStream.write(body, body.length);
+}
+
+function makeChan(uri) {
+ let chan = NetUtil.newChannel({
+ uri,
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+ chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI;
+ return chan;
+}
+
+let Http3CheckListener = function() {};
+
+Http3CheckListener.prototype = {
+ onDataAvailableFired: false,
+ expectedStatus: Cr.NS_OK,
+ expectedRoute: "",
+
+ onStartRequest: function testOnStartRequest(request) {
+ Assert.ok(request instanceof Ci.nsIHttpChannel);
+
+ Assert.equal(request.status, this.expectedStatus);
+ if (Components.isSuccessCode(this.expectedStatus)) {
+ Assert.equal(request.responseStatus, 200);
+ }
+ },
+
+ onDataAvailable: function testOnDataAvailable(request, stream, off, cnt) {
+ this.onDataAvailableFired = true;
+ read_stream(stream, cnt);
+ },
+
+ onStopRequest: function testOnStopRequest(request, status) {
+ Assert.equal(status, this.expectedStatus);
+ let routed = "NA";
+ try {
+ routed = request.getRequestHeader("Alt-Used");
+ } catch (e) {}
+ dump("routed is " + routed + "\n");
+
+ Assert.equal(routed, this.expectedRoute);
+
+ if (Components.isSuccessCode(this.expectedStatus)) {
+ let httpVersion = "";
+ try {
+ httpVersion = request.protocolVersion;
+ } catch (e) {}
+ Assert.equal(httpVersion, "h3");
+ Assert.equal(this.onDataAvailableFired, true);
+ }
+ run_next_test();
+ do_test_finished();
+ },
+};
+
+let WaitForHttp3Listener = function() {};
+
+WaitForHttp3Listener.prototype = new Http3CheckListener();
+
+WaitForHttp3Listener.prototype.uri = "";
+WaitForHttp3Listener.prototype.h3AltSvc = "";
+
+WaitForHttp3Listener.prototype.onStopRequest = function testOnStopRequest(
+ request,
+ status
+) {
+ Assert.equal(status, this.expectedStatus);
+
+ let routed = "NA";
+ try {
+ routed = request.getRequestHeader("Alt-Used");
+ } catch (e) {}
+ dump("routed is " + routed + "\n");
+
+ let httpVersion = "";
+ try {
+ httpVersion = request.protocolVersion;
+ } catch (e) {}
+
+ if (routed == this.expectedRoute) {
+ Assert.equal(routed, this.expectedRoute); // always true, but a useful log
+ Assert.equal(httpVersion, "h3");
+ run_next_test();
+ } else {
+ dump("poll later for alt svc mapping\n");
+ if (httpVersion == "h2") {
+ request.QueryInterface(Ci.nsIHttpChannelInternal);
+ Assert.ok(request.supportsHTTP3);
+ }
+ do_test_pending();
+ do_timeout(500, () => {
+ doTest(this.uri, this.expectedRoute, this.h3AltSvc);
+ });
+ }
+
+ do_test_finished();
+};
+
+function doTest(uri, expectedRoute, altSvc) {
+ let chan = makeChan(uri);
+ let listener = new WaitForHttp3Listener();
+ listener.uri = uri;
+ listener.expectedRoute = expectedRoute;
+ listener.h3AltSvc = altSvc;
+ chan.setRequestHeader("x-altsvc", altSvc, false);
+ chan.asyncOpen(listener);
+}
+
+// Test Alt-Svc for http3.
+// H2 server returns alt-svc=h3-27=:h3port
+function test_https_alt_svc() {
+ dump("test_https_alt_svc()\n");
+ do_test_pending();
+ doTest(httpsOrigin + "http3-test", h3Route, h3AltSvc);
+}
+
+// Listener for a number of parallel requests. if with_error is set, one of
+// the channels will be cancelled (by the server or in onStartRequest).
+let MultipleListener = function() {};
+
+MultipleListener.prototype = {
+ number_of_parallel_requests: 0,
+ with_error: Cr.NS_OK,
+ count_of_done_requests: 0,
+ error_found_onstart: false,
+ error_found_onstop: false,
+ need_cancel_found: false,
+
+ onStartRequest: function testOnStartRequest(request) {
+ Assert.ok(request instanceof Ci.nsIHttpChannel);
+
+ let need_cancel = "";
+ try {
+ need_cancel = request.getRequestHeader("CancelMe");
+ } catch (e) {}
+ if (need_cancel != "") {
+ this.need_cancel_found = true;
+ request.cancel(Cr.NS_ERROR_ABORT);
+ } else if (Components.isSuccessCode(request.status)) {
+ Assert.equal(request.responseStatus, 200);
+ } else if (this.error_found_onstart) {
+ do_throw("We should have only one request failing.");
+ } else {
+ Assert.equal(request.status, this.with_error);
+ this.error_found_onstart = true;
+ }
+ },
+
+ onDataAvailable: function testOnDataAvailable(request, stream, off, cnt) {
+ read_stream(stream, cnt);
+ },
+
+ onStopRequest: function testOnStopRequest(request, status) {
+ let routed = "";
+ try {
+ routed = request.getRequestHeader("Alt-Used");
+ } catch (e) {}
+ Assert.equal(routed, this.expectedRoute);
+
+ if (Components.isSuccessCode(request.status)) {
+ let httpVersion = "";
+ try {
+ httpVersion = request.protocolVersion;
+ } catch (e) {}
+ Assert.equal(httpVersion, "h3");
+ }
+
+ if (!Components.isSuccessCode(request.status)) {
+ if (this.error_found_onstop) {
+ do_throw("We should have only one request failing.");
+ } else {
+ Assert.equal(request.status, this.with_error);
+ this.error_found_onstop = true;
+ }
+ }
+ this.count_of_done_requests++;
+ if (this.count_of_done_requests == this.number_of_parallel_requests) {
+ if (Components.isSuccessCode(this.with_error)) {
+ Assert.equal(this.error_found_onstart, false);
+ Assert.equal(this.error_found_onstop, false);
+ } else {
+ Assert.ok(this.error_found_onstart || this.need_cancel_found);
+ Assert.equal(this.error_found_onstop, true);
+ }
+ run_next_test();
+ }
+ do_test_finished();
+ },
+};
+
+// Multiple requests
+function test_multiple_requests() {
+ dump("test_multiple_requests()\n");
+
+ let listener = new MultipleListener();
+ listener.number_of_parallel_requests = number_of_parallel_requests;
+ listener.expectedRoute = h3Route;
+
+ for (let i = 0; i < number_of_parallel_requests; i++) {
+ let chan = makeChan(httpsOrigin + "20000");
+ chan.asyncOpen(listener);
+ do_test_pending();
+ }
+}
+
+// A request cancelled by a server.
+function test_request_cancelled_by_server() {
+ dump("test_request_cancelled_by_server()\n");
+
+ let listener = new Http3CheckListener();
+ listener.expectedStatus = Cr.NS_ERROR_NET_INTERRUPT;
+ listener.expectedRoute = h3Route;
+ let chan = makeChan(httpsOrigin + "RequestCancelled");
+ chan.asyncOpen(listener);
+ do_test_pending();
+}
+
+let CancelRequestListener = function() {};
+
+CancelRequestListener.prototype = new Http3CheckListener();
+
+CancelRequestListener.prototype.expectedStatus = Cr.NS_ERROR_ABORT;
+
+CancelRequestListener.prototype.onStartRequest = function testOnStartRequest(
+ request
+) {
+ Assert.ok(request instanceof Ci.nsIHttpChannel);
+
+ Assert.equal(Components.isSuccessCode(request.status), true);
+ request.cancel(Cr.NS_ERROR_ABORT);
+};
+
+// Cancel stream after OnStartRequest.
+function test_stream_cancelled_by_necko() {
+ dump("test_stream_cancelled_by_necko()\n");
+
+ let listener = new CancelRequestListener();
+ listener.expectedRoute = h3Route;
+ let chan = makeChan(httpsOrigin + "20000");
+ chan.asyncOpen(listener);
+ do_test_pending();
+}
+
+// Multiple requests, one gets cancelled by the server, the other should finish normally.
+function test_multiple_request_one_is_cancelled() {
+ dump("test_multiple_request_one_is_cancelled()\n");
+
+ let listener = new MultipleListener();
+ listener.number_of_parallel_requests = number_of_parallel_requests;
+ listener.with_error = Cr.NS_ERROR_NET_INTERRUPT;
+ listener.expectedRoute = h3Route;
+
+ for (let i = 0; i < number_of_parallel_requests; i++) {
+ let uri = httpsOrigin + "20000";
+ if (i == 4) {
+ // Add a request that will be cancelled by the server.
+ uri = httpsOrigin + "RequestCancelled";
+ }
+ let chan = makeChan(uri);
+ chan.asyncOpen(listener);
+ do_test_pending();
+ }
+}
+
+// Multiple requests, one gets cancelled by us, the other should finish normally.
+function test_multiple_request_one_is_cancelled_by_necko() {
+ dump("test_multiple_request_one_is_cancelled_by_necko()\n");
+
+ let listener = new MultipleListener();
+ listener.number_of_parallel_requests = number_of_parallel_requests;
+ listener.with_error = Cr.NS_ERROR_ABORT;
+ listener.expectedRoute = h3Route;
+ for (let i = 0; i < number_of_parallel_requests; i++) {
+ let chan = makeChan(httpsOrigin + "20000");
+ if (i == 4) {
+ // MultipleListener will cancel request with this header.
+ chan.setRequestHeader("CancelMe", "true", false);
+ }
+ chan.asyncOpen(listener);
+ do_test_pending();
+ }
+}
+
+let PostListener = function() {};
+
+PostListener.prototype = new Http3CheckListener();
+
+PostListener.prototype.onDataAvailable = function(request, stream, off, cnt) {
+ this.onDataAvailableFired = true;
+ read_stream(stream, cnt);
+};
+
+// Support for doing a POST
+function do_post(content, chan, listener, method) {
+ let stream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(
+ Ci.nsIStringInputStream
+ );
+ stream.data = content;
+
+ let uchan = chan.QueryInterface(Ci.nsIUploadChannel);
+ uchan.setUploadStream(stream, "text/plain", stream.available());
+
+ chan.requestMethod = method;
+
+ chan.asyncOpen(listener);
+}
+
+// Test a simple POST
+function test_post() {
+ dump("test_post()");
+ let chan = makeChan(httpsOrigin + "post");
+ let listener = new PostListener();
+ listener.expectedRoute = h3Route;
+ do_post(post, chan, listener, "POST");
+ do_test_pending();
+}
+
+// Test a simple PATCH
+function test_patch() {
+ dump("test_patch()");
+ let chan = makeChan(httpsOrigin + "patch");
+ let listener = new PostListener();
+ listener.expectedRoute = h3Route;
+ do_post(post, chan, listener, "PATCH");
+ do_test_pending();
+}
+
+// Test alt-svc for http (without s)
+function test_http_alt_svc() {
+ dump("test_http_alt_svc()\n");
+
+ do_test_pending();
+ doTest(httpOrigin + "http3-test", h3Route, h3AltSvc);
+}
+
+let SlowReceiverListener = function() {};
+
+SlowReceiverListener.prototype = new Http3CheckListener();
+SlowReceiverListener.prototype.count = 0;
+
+SlowReceiverListener.prototype.onDataAvailable = function(
+ request,
+ stream,
+ off,
+ cnt
+) {
+ this.onDataAvailableFired = true;
+ this.count += cnt;
+ read_stream(stream, cnt);
+};
+
+SlowReceiverListener.prototype.onStopRequest = function(request, status) {
+ Assert.equal(status, this.expectedStatus);
+ Assert.equal(this.count, 10000000);
+ let routed = "NA";
+ try {
+ routed = request.getRequestHeader("Alt-Used");
+ } catch (e) {}
+ dump("routed is " + routed + "\n");
+
+ Assert.equal(routed, this.expectedRoute);
+
+ if (Components.isSuccessCode(this.expectedStatus)) {
+ let httpVersion = "";
+ try {
+ httpVersion = request.protocolVersion;
+ } catch (e) {}
+ Assert.equal(httpVersion, "h3");
+ Assert.equal(this.onDataAvailableFired, true);
+ }
+ run_next_test();
+ do_test_finished();
+};
+
+function test_slow_receiver() {
+ dump("test_slow_receiver()\n");
+ let chan = makeChan(httpsOrigin + "10000000");
+ let listener = new SlowReceiverListener();
+ listener.expectedRoute = h3Route;
+ chan.asyncOpen(listener);
+ do_test_pending();
+ chan.suspend();
+ do_timeout(1000, chan.resume);
+}
+
+let CheckFallbackListener = function() {};
+
+CheckFallbackListener.prototype = {
+ onStartRequest: function testOnStartRequest(request) {
+ Assert.ok(request instanceof Ci.nsIHttpChannel);
+
+ Assert.equal(request.status, Cr.NS_OK);
+ Assert.equal(request.responseStatus, 200);
+ },
+
+ onDataAvailable: function testOnDataAvailable(request, stream, off, cnt) {
+ read_stream(stream, cnt);
+ },
+
+ onStopRequest: function testOnStopRequest(request, status) {
+ Assert.equal(status, Cr.NS_OK);
+ let routed = "NA";
+ try {
+ routed = request.getRequestHeader("Alt-Used");
+ } catch (e) {}
+ dump("routed is " + routed + "\n");
+
+ Assert.equal(routed, "0");
+
+ let httpVersion = "";
+ try {
+ httpVersion = request.protocolVersion;
+ } catch (e) {}
+ Assert.equal(httpVersion, "http/1.1");
+ run_next_test();
+ do_test_finished();
+ },
+};
+
+// Server cancels request with VersionFallback.
+function test_version_fallback() {
+ dump("test_version_fallback()\n");
+
+ let chan = makeChan(httpsOrigin + "VersionFallback");
+ let listener = new CheckFallbackListener();
+ chan.asyncOpen(listener);
+ do_test_pending();
+}
+
+function testsDone() {
+ prefs.clearUserPref("network.http.http3.enabled");
+ prefs.clearUserPref("network.dns.localDomains");
+ prefs.clearUserPref("network.proxy.allow_hijacking_localhost");
+ dump("testDone\n");
+ do_test_pending();
+ h1Server.stop(do_test_finished);
+}
diff --git a/netwerk/test/unit/test_http3_421.js b/netwerk/test/unit/test_http3_421.js
new file mode 100644
index 0000000000..65853b1091
--- /dev/null
+++ b/netwerk/test/unit/test_http3_421.js
@@ -0,0 +1,175 @@
+"use strict";
+
+let h3Route;
+let httpsOrigin;
+let h3AltSvc;
+let prefs;
+
+let tests = [test_https_alt_svc, test_response_421, testsDone];
+let current_test = 0;
+
+function run_next_test() {
+ if (current_test < tests.length) {
+ dump("starting test number " + current_test + "\n");
+ tests[current_test]();
+ current_test++;
+ }
+}
+
+function run_test() {
+ let env = Cc["@mozilla.org/process/environment;1"].getService(
+ Ci.nsIEnvironment
+ );
+ let h2Port = env.get("MOZHTTP2_PORT");
+ Assert.notEqual(h2Port, null);
+ Assert.notEqual(h2Port, "");
+ let h3Port = env.get("MOZHTTP3_PORT");
+ Assert.notEqual(h3Port, null);
+ Assert.notEqual(h3Port, "");
+ h3AltSvc = ":" + h3Port;
+
+ h3Route = "foo.example.com:" + h3Port;
+ do_get_profile();
+ prefs = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch);
+
+ prefs.setBoolPref("network.http.http3.enabled", true);
+ prefs.setCharPref("network.dns.localDomains", "foo.example.com");
+ // We always resolve elements of localDomains as it's hardcoded without the
+ // following pref:
+ prefs.setBoolPref("network.proxy.allow_hijacking_localhost", true);
+
+ // The certificate for the http3server server is for foo.example.com and
+ // is signed by http2-ca.pem so add that cert to the trust list as a
+ // signing cert.
+ let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(
+ Ci.nsIX509CertDB
+ );
+ addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u");
+ httpsOrigin = "https://foo.example.com:" + h2Port + "/";
+
+ run_next_test();
+}
+
+function makeChan(uri) {
+ let chan = NetUtil.newChannel({
+ uri,
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+ chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI;
+ return chan;
+}
+
+let Http3Listener = function() {};
+
+Http3Listener.prototype = {
+ onDataAvailableFired: false,
+ buffer: "",
+ routed: "",
+ httpVersion: "",
+
+ onStartRequest: function testOnStartRequest(request) {
+ Assert.ok(request instanceof Ci.nsIHttpChannel);
+ Assert.equal(request.status, Cr.NS_OK);
+ Assert.equal(request.responseStatus, 200);
+ },
+
+ onDataAvailable: function testOnDataAvailable(request, stream, off, cnt) {
+ this.onDataAvailableFired = true;
+ this.buffer = this.buffer.concat(read_stream(stream, cnt));
+ },
+
+ onStopRequest: function testOnStopRequest(request, status) {
+ Assert.equal(status, Cr.NS_OK);
+ Assert.equal(this.onDataAvailableFired, true);
+ this.routed = "NA";
+ try {
+ this.routed = request.getRequestHeader("Alt-Used");
+ } catch (e) {}
+ dump("routed is " + this.routed + "\n");
+
+ this.httpVersion = "";
+ try {
+ this.httpVersion = request.protocolVersion;
+ } catch (e) {}
+ dump("httpVersion is " + this.httpVersion + "\n");
+ },
+};
+
+let WaitForHttp3Listener = function() {};
+
+WaitForHttp3Listener.prototype = new Http3Listener();
+
+WaitForHttp3Listener.prototype.uri = "";
+
+WaitForHttp3Listener.prototype.onStopRequest = function testOnStopRequest(
+ request,
+ status
+) {
+ Http3Listener.prototype.onStopRequest.call(this, request, status);
+
+ if (this.routed == h3Route) {
+ Assert.equal(this.httpVersion, "h3");
+ run_next_test();
+ } else {
+ dump("poll later for alt svc mapping\n");
+ do_test_pending();
+ do_timeout(500, () => {
+ doTest(this.uri);
+ });
+ }
+
+ do_test_finished();
+};
+
+function doTest(uri) {
+ let chan = makeChan(uri);
+ let listener = new WaitForHttp3Listener();
+ listener.uri = uri;
+ chan.setRequestHeader("x-altsvc", h3AltSvc, false);
+ chan.asyncOpen(listener);
+}
+
+// Test Alt-Svc for http3.
+// H2 server returns alt-svc=h3-27=:h3port
+function test_https_alt_svc() {
+ dump("test_https_alt_svc()\n");
+
+ do_test_pending();
+ doTest(httpsOrigin + "http3-test");
+}
+
+let Resp421Listener = function() {};
+
+Resp421Listener.prototype = new Http3Listener();
+
+Resp421Listener.prototype.onStopRequest = function testOnStopRequest(
+ request,
+ status
+) {
+ Http3Listener.prototype.onStopRequest.call(this, request, status);
+
+ Assert.equal(this.routed, "0");
+ Assert.equal(this.httpVersion, "h2");
+ Assert.ok(this.buffer.match("You Win! [(]by requesting/Response421[)]"));
+
+ run_next_test();
+ do_test_finished();
+};
+
+function test_response_421() {
+ dump("test_response_421()\n");
+
+ let listener = new Resp421Listener();
+ let chan = makeChan(httpsOrigin + "Response421");
+ chan.asyncOpen(listener);
+ do_test_pending();
+}
+
+function testsDone() {
+ prefs.clearUserPref("network.http.http3.enabled");
+ prefs.clearUserPref("network.dns.localDomains");
+ prefs.clearUserPref("network.proxy.allow_hijacking_localhost");
+ dump("testDone\n");
+ do_test_pending();
+ do_test_finished();
+}
diff --git a/netwerk/test/unit/test_http3_alt_svc.js b/netwerk/test/unit/test_http3_alt_svc.js
new file mode 100644
index 0000000000..77e7498a9c
--- /dev/null
+++ b/netwerk/test/unit/test_http3_alt_svc.js
@@ -0,0 +1,129 @@
+"use strict";
+
+let httpsOrigin;
+let h3AltSvc;
+let h3Port;
+let h3Route;
+let prefs;
+
+let tests = [test_https_alt_svc, testsDone];
+
+let current_test = 0;
+
+function run_next_test() {
+ if (current_test < tests.length) {
+ dump("starting test number " + current_test + "\n");
+ tests[current_test]();
+ current_test++;
+ }
+}
+
+function run_test() {
+ let env = Cc["@mozilla.org/process/environment;1"].getService(
+ Ci.nsIEnvironment
+ );
+ let h2Port = env.get("MOZHTTP2_PORT");
+ Assert.notEqual(h2Port, null);
+ Assert.notEqual(h2Port, "");
+ let h3Port = env.get("MOZHTTP3_PORT");
+ Assert.notEqual(h3Port, null);
+ Assert.notEqual(h3Port, "");
+ h3AltSvc = ":" + h3Port;
+
+ h3Route = "foo.example.com:" + h3Port;
+ do_get_profile();
+ prefs = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch);
+
+ prefs.setBoolPref("network.http.http3.enabled", true);
+ prefs.setCharPref("network.dns.localDomains", "foo.example.com");
+ // We always resolve elements of localDomains as it's hardcoded without the
+ // following pref:
+ prefs.setBoolPref("network.proxy.allow_hijacking_localhost", true);
+
+ // The certificate for the http3server server is for foo.example.com and
+ // is signed by http2-ca.pem so add that cert to the trust list as a
+ // signing cert.
+ let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(
+ Ci.nsIX509CertDB
+ );
+ addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u");
+ httpsOrigin = "https://foo.example.com:" + h2Port + "/";
+
+ run_next_test();
+}
+
+function makeChan(uri) {
+ let chan = NetUtil.newChannel({
+ uri,
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+ chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI;
+ return chan;
+}
+
+let WaitForHttp3Listener = function() {};
+
+WaitForHttp3Listener.prototype = {
+ onDataAvailableFired: false,
+ expectedRoute: "",
+
+ onStartRequest: function testOnStartRequest(request) {
+ Assert.ok(request instanceof Ci.nsIHttpChannel);
+ Assert.equal(request.responseStatus, 200);
+ },
+
+ onDataAvailable: function testOnDataAvailable(request, stream, off, cnt) {
+ this.onDataAvailableFired = true;
+ read_stream(stream, cnt);
+ },
+
+ onStopRequest: function testOnStopRequest(request, status) {
+ let routed = "NA";
+ try {
+ routed = request.getRequestHeader("Alt-Used");
+ } catch (e) {}
+ dump("routed is " + routed + "\n");
+
+ if (routed == this.expectedRoute) {
+ let httpVersion = "";
+ try {
+ httpVersion = request.protocolVersion;
+ } catch (e) {}
+ Assert.equal(httpVersion, "h3");
+ run_next_test();
+ } else {
+ dump("poll later for alt svc mapping\n");
+ do_test_pending();
+ do_timeout(500, () => {
+ doTest(this.uri, this.expectedRoute, this.h3AltSvc);
+ });
+ }
+
+ do_test_finished();
+ },
+};
+
+function doTest(uri, expectedRoute, altSvc) {
+ let chan = makeChan(uri);
+ let listener = new WaitForHttp3Listener();
+ listener.uri = uri;
+ listener.expectedRoute = expectedRoute;
+ listener.h3AltSvc = altSvc;
+ chan.setRequestHeader("x-altsvc", altSvc, false);
+ chan.asyncOpen(listener);
+}
+
+// Test Alt-Svc for http3.
+// H2 server returns alt-svc=h2=foo2.example.com:8000,h3-27=:h3port,h3-29=foo2.example.com:8443
+function test_https_alt_svc() {
+ dump("test_https_alt_svc()\n");
+ do_test_pending();
+ doTest(httpsOrigin + "http3-test2", h3Route, h3AltSvc);
+}
+
+function testsDone() {
+ prefs.clearUserPref("network.http.http3.enabled");
+ prefs.clearUserPref("network.dns.localDomains");
+ prefs.clearUserPref("network.proxy.allow_hijacking_localhost");
+ dump("testDone\n");
+}
diff --git a/netwerk/test/unit/test_http3_error_before_connect.js b/netwerk/test/unit/test_http3_error_before_connect.js
new file mode 100644
index 0000000000..fd1fdaf543
--- /dev/null
+++ b/netwerk/test/unit/test_http3_error_before_connect.js
@@ -0,0 +1,123 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+let httpsUri;
+
+registerCleanupFunction(async () => {
+ Services.prefs.clearUserPref("network.http.http3.enabled");
+ Services.prefs.clearUserPref("network.dns.localDomains");
+ Services.prefs.clearUserPref("network.dns.disableIPv6");
+ Services.prefs.clearUserPref(
+ "network.http.http3.alt-svc-mapping-for-testing"
+ );
+ Services.prefs.clearUserPref("network.http.http3.backup_timer_delay");
+ dump("cleanup done\n");
+});
+
+function chanPromise(chan, listener) {
+ return new Promise(resolve => {
+ function finish() {
+ resolve();
+ }
+ listener.finish = finish;
+ chan.asyncOpen(listener);
+ });
+}
+
+function makeChan() {
+ let chan = NetUtil.newChannel({
+ uri: httpsUri,
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+ chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI;
+ return chan;
+}
+
+add_task(async function test_setup() {
+ let env = Cc["@mozilla.org/process/environment;1"].getService(
+ Ci.nsIEnvironment
+ );
+
+ let h2Port = env.get("MOZHTTP2_PORT");
+ Assert.notEqual(h2Port, null);
+ let h3Port = env.get("MOZHTTP3_PORT_NO_RESPONSE");
+ Assert.notEqual(h3Port, null);
+ Assert.notEqual(h3Port, "");
+
+ Services.prefs.setBoolPref("network.http.http3.enabled", true);
+ Services.prefs.setCharPref("network.dns.localDomains", "foo.example.com");
+ Services.prefs.setBoolPref("network.dns.disableIPv6", true);
+ // Set AltSvc to point to not existing HTTP3 server on port 443
+ Services.prefs.setCharPref(
+ "network.http.http3.alt-svc-mapping-for-testing",
+ "foo.example.com;h3-27=:" + h3Port
+ );
+ Services.prefs.setIntPref("network.http.http3.backup_timer_delay", 0);
+
+ let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(
+ Ci.nsIX509CertDB
+ );
+ addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u");
+
+ httpsUri = "https://foo.example.com:" + h2Port + "/";
+});
+
+add_task(async function test_fatal_stream_error() {
+ let result = 1;
+ // We need to loop here because we need to wait for AltSvc storage to
+ // to be started.
+ // We also do not have a way to verify that HTTP3 has been tried, because
+ // the fallback is automatic, so try a couple of times.
+ do {
+ // We need to close HTTP2 connections, otherwise our connection pooling
+ // will dispatch the request over already existing HTTP2 connection.
+ Services.obs.notifyObservers(null, "net:prune-all-connections");
+ let chan = makeChan();
+ let listener = new CheckOnlyHttp2Listener();
+ await altsvcSetupPromise(chan, listener);
+ result++;
+ } while (result < 5);
+});
+
+let CheckOnlyHttp2Listener = function() {};
+
+CheckOnlyHttp2Listener.prototype = {
+ onStartRequest: function testOnStartRequest(request) {},
+
+ onDataAvailable: function testOnDataAvailable(request, stream, off, cnt) {
+ read_stream(stream, cnt);
+ },
+
+ onStopRequest: function testOnStopRequest(request, status) {
+ Assert.equal(status, Cr.NS_OK);
+ let httpVersion = "";
+ try {
+ httpVersion = request.protocolVersion;
+ } catch (e) {}
+ Assert.equal(httpVersion, "h2");
+
+ let routed = "NA";
+ try {
+ routed = request.getRequestHeader("Alt-Used");
+ } catch (e) {}
+ dump("routed is " + routed + "\n");
+ Assert.ok(routed === "0" || routed === "NA");
+ this.finish();
+ },
+};
+
+add_task(async function test_no_http3_after_error() {
+ let chan = makeChan();
+ let listener = new CheckOnlyHttp2Listener();
+ await altsvcSetupPromise(chan, listener);
+});
+
+// also after all connections are closed.
+add_task(async function test_no_http3_after_error2() {
+ Services.obs.notifyObservers(null, "net:prune-all-connections");
+ let chan = makeChan();
+ let listener = new CheckOnlyHttp2Listener();
+ await altsvcSetupPromise(chan, listener);
+});
diff --git a/netwerk/test/unit/test_http3_fast_fallback.js b/netwerk/test/unit/test_http3_fast_fallback.js
new file mode 100644
index 0000000000..9a2d78847b
--- /dev/null
+++ b/netwerk/test/unit/test_http3_fast_fallback.js
@@ -0,0 +1,597 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+ChromeUtils.import("resource://gre/modules/NetUtil.jsm");
+var { setTimeout } = ChromeUtils.import("resource://gre/modules/Timer.jsm");
+
+let prefs;
+let h2Port;
+let h3Port;
+let listen;
+let trrServer;
+
+const dns = Cc["@mozilla.org/network/dns-service;1"].getService(
+ Ci.nsIDNSService
+);
+const certOverrideService = Cc[
+ "@mozilla.org/security/certoverride;1"
+].getService(Ci.nsICertOverrideService);
+const threadManager = Cc["@mozilla.org/thread-manager;1"].getService(
+ Ci.nsIThreadManager
+);
+const mainThread = threadManager.currentThread;
+
+const defaultOriginAttributes = {};
+
+function setup() {
+ let env = Cc["@mozilla.org/process/environment;1"].getService(
+ Ci.nsIEnvironment
+ );
+ h2Port = env.get("MOZHTTP2_PORT");
+ Assert.notEqual(h2Port, null);
+ Assert.notEqual(h2Port, "");
+
+ h3Port = env.get("MOZHTTP3_PORT_NO_RESPONSE");
+ Assert.notEqual(h3Port, null);
+ Assert.notEqual(h3Port, "");
+
+ // Set to allow the cert presented by our H2 server
+ do_get_profile();
+ prefs = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch);
+
+ prefs.setBoolPref("network.security.esni.enabled", false);
+ prefs.setBoolPref("network.http.spdy.enabled", true);
+ prefs.setBoolPref("network.http.spdy.enabled.http2", true);
+ // the TRR server is on 127.0.0.1
+ prefs.setCharPref("network.trr.bootstrapAddress", "127.0.0.1");
+
+ // make all native resolve calls "secretly" resolve localhost instead
+ prefs.setBoolPref("network.dns.native-is-localhost", true);
+
+ // 0 - off, 1 - race, 2 TRR first, 3 TRR only, 4 shadow
+ prefs.setIntPref("network.trr.mode", 2); // TRR first
+ prefs.setBoolPref("network.trr.wait-for-portal", false);
+ // don't confirm that TRR is working, just go!
+ prefs.setCharPref("network.trr.confirmationNS", "skip");
+
+ // So we can change the pref without clearing the cache to check a pushed
+ // record with a TRR path that fails.
+ Services.prefs.setBoolPref("network.trr.clear-cache-on-pref-change", false);
+
+ Services.prefs.setBoolPref("network.http.http3.enabled", true);
+
+ // The moz-http2 cert is for foo.example.com and is signed by http2-ca.pem
+ // so add that cert to the trust list as a signing cert. // the foo.example.com domain name.
+ const certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(
+ Ci.nsIX509CertDB
+ );
+ addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u");
+}
+
+setup();
+registerCleanupFunction(async () => {
+ prefs.clearUserPref("network.security.esni.enabled");
+ prefs.clearUserPref("network.http.spdy.enabled");
+ prefs.clearUserPref("network.http.spdy.enabled.http2");
+ prefs.clearUserPref("network.dns.localDomains");
+ prefs.clearUserPref("network.dns.native-is-localhost");
+ prefs.clearUserPref("network.trr.mode");
+ prefs.clearUserPref("network.trr.uri");
+ prefs.clearUserPref("network.trr.credentials");
+ prefs.clearUserPref("network.trr.wait-for-portal");
+ prefs.clearUserPref("network.trr.allow-rfc1918");
+ prefs.clearUserPref("network.trr.useGET");
+ prefs.clearUserPref("network.trr.confirmationNS");
+ prefs.clearUserPref("network.trr.bootstrapAddress");
+ prefs.clearUserPref("network.trr.request-timeout");
+ prefs.clearUserPref("network.trr.clear-cache-on-pref-change");
+ prefs.clearUserPref("network.dns.upgrade_with_https_rr");
+ prefs.clearUserPref("network.dns.use_https_rr_as_altsvc");
+ prefs.clearUserPref("network.dns.echconfig.enabled");
+ prefs.clearUserPref("network.dns.echconfig.fallback_to_origin");
+ prefs.clearUserPref("network.dns.httpssvc.reset_exclustion_list");
+ prefs.clearUserPref("network.http.http3.enabled");
+ prefs.clearUserPref("network.dns.httpssvc.http3_fast_fallback_timeout");
+ Services.prefs.clearUserPref(
+ "network.http.http3.alt-svc-mapping-for-testing"
+ );
+ Services.prefs.clearUserPref("network.http.http3.backup_timer_delay");
+ Services.prefs.clearUserPref("network.http.speculative-parallel-limit");
+ if (trrServer) {
+ await trrServer.stop();
+ }
+});
+
+class DNSListener {
+ constructor() {
+ this.promise = new Promise(resolve => {
+ this.resolve = resolve;
+ });
+ }
+ onLookupComplete(inRequest, inRecord, inStatus) {
+ this.resolve([inRequest, inRecord, inStatus]);
+ }
+ // So we can await this as a promise.
+ then() {
+ return this.promise.then.apply(this.promise, arguments);
+ }
+}
+
+DNSListener.prototype.QueryInterface = ChromeUtils.generateQI([
+ "nsIDNSListener",
+]);
+
+function makeChan(url) {
+ let chan = NetUtil.newChannel({
+ uri: url,
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+ chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI;
+ return chan;
+}
+
+function channelOpenPromise(chan, flags) {
+ return new Promise(resolve => {
+ function finish(req, buffer) {
+ resolve([req, buffer]);
+ certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData(
+ false
+ );
+ }
+ let internal = chan.QueryInterface(Ci.nsIHttpChannelInternal);
+ internal.setWaitForHTTPSSVCRecord();
+ certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData(
+ true
+ );
+ chan.asyncOpen(new ChannelListener(finish, null, flags));
+ });
+}
+
+let CheckOnlyHttp2Listener = function() {};
+
+CheckOnlyHttp2Listener.prototype = {
+ onStartRequest: function testOnStartRequest(request) {},
+
+ onDataAvailable: function testOnDataAvailable(request, stream, off, cnt) {
+ read_stream(stream, cnt);
+ },
+
+ onStopRequest: function testOnStopRequest(request, status) {
+ Assert.equal(status, Cr.NS_OK);
+ let httpVersion = "";
+ try {
+ httpVersion = request.protocolVersion;
+ } catch (e) {}
+ Assert.equal(httpVersion, "h2");
+
+ let routed = "NA";
+ try {
+ routed = request.getRequestHeader("Alt-Used");
+ } catch (e) {}
+ dump("routed is " + routed + "\n");
+ Assert.ok(routed === "0" || routed === "NA");
+ this.finish();
+ },
+};
+
+async function fast_fallback_test() {
+ let result = 1;
+ // We need to loop here because we need to wait for AltSvc storage to
+ // to be started.
+ // We also do not have a way to verify that HTTP3 has been tried, because
+ // the fallback is automatic, so try a couple of times.
+ do {
+ // We need to close HTTP2 connections, otherwise our connection pooling
+ // will dispatch the request over already existing HTTP2 connection.
+ Services.obs.notifyObservers(null, "net:prune-all-connections");
+ let chan = makeChan(`https://foo.example.com:${h2Port}/`);
+ let listener = new CheckOnlyHttp2Listener();
+ await altsvcSetupPromise(chan, listener);
+ result++;
+ } while (result < 3);
+}
+
+// Test the case when speculative connection is enabled. In this case, when the
+// backup connection is ready, the http transaction is still in pending
+// queue because the h3 connection is never ready to accept transactions.
+add_task(async function test_fast_fallback_with_speculative_connection() {
+ Services.prefs.setBoolPref("network.http.http3.enabled", true);
+ Services.prefs.setCharPref("network.dns.localDomains", "foo.example.com");
+ // Set AltSvc to point to not existing HTTP3 server on port 443
+ Services.prefs.setCharPref(
+ "network.http.http3.alt-svc-mapping-for-testing",
+ "foo.example.com;h3-27=:" + h3Port
+ );
+ Services.prefs.setBoolPref("network.dns.disableIPv6", true);
+ Services.prefs.setIntPref("network.http.speculative-parallel-limit", 6);
+
+ await fast_fallback_test();
+});
+
+let HTTPObserver = {
+ observeActivity(
+ aChannel,
+ aType,
+ aSubtype,
+ aTimestamp,
+ aSizeData,
+ aStringData
+ ) {
+ aChannel.QueryInterface(Ci.nsIChannel);
+ if (aChannel.URI.spec == `https://foo.example.com:${h2Port}/`) {
+ if (
+ aType == Ci.nsIHttpActivityDistributor.ACTIVITY_TYPE_HTTP_TRANSACTION &&
+ aSubtype ==
+ Ci.nsIHttpActivityDistributor.ACTIVITY_SUBTYPE_REQUEST_HEADER
+ ) {
+ // We need to enable speculative connection again, since the backup
+ // connection is done by using speculative connection.
+ Services.prefs.setIntPref("network.http.speculative-parallel-limit", 6);
+ let observerService = Cc[
+ "@mozilla.org/network/http-activity-distributor;1"
+ ].getService(Ci.nsIHttpActivityDistributor);
+ observerService.removeObserver(HTTPObserver);
+ }
+ }
+ },
+};
+
+// Test the case when speculative connection is disabled. In this case, when the
+// back connection is ready, the http transaction is already activated,
+// but the socket is not ready to write.
+add_task(async function test_fast_fallback_without_speculative_connection() {
+ // Make sure the h3 connection created by the previous test is cleared.
+ Services.obs.notifyObservers(null, "net:cancel-all-connections");
+ await new Promise(resolve => setTimeout(resolve, 1000));
+ // Clear the h3 excluded list, otherwise the Alt-Svc mapping will not be used.
+ Services.obs.notifyObservers(null, "network:reset-http3-excluded-list");
+ Services.prefs.setIntPref("network.http.speculative-parallel-limit", 0);
+
+ let observerService = Cc[
+ "@mozilla.org/network/http-activity-distributor;1"
+ ].getService(Ci.nsIHttpActivityDistributor);
+ observerService.addObserver(HTTPObserver);
+
+ await fast_fallback_test();
+
+ Services.prefs.clearUserPref(
+ "network.http.http3.alt-svc-mapping-for-testing"
+ );
+});
+
+// Test when echConfig is disabled and we have https rr for http3. We use a
+// longer timeout in this test, so when fast fallback timer is triggered, the
+// http transaction is already activated.
+add_task(async function testFastfallback() {
+ trrServer = new TRRServer();
+ await trrServer.start();
+ Services.prefs.setBoolPref("network.dns.upgrade_with_https_rr", true);
+ Services.prefs.setBoolPref("network.dns.use_https_rr_as_altsvc", true);
+ Services.prefs.setBoolPref("network.dns.echconfig.enabled", false);
+
+ Services.prefs.setIntPref("network.trr.mode", 3);
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${trrServer.port}/dns-query`
+ );
+ Services.prefs.setBoolPref("network.http.http3.enabled", true);
+
+ Services.prefs.setIntPref(
+ "network.dns.httpssvc.http3_fast_fallback_timeout",
+ 1000
+ );
+
+ await trrServer.registerDoHAnswers("test.fastfallback.com", "HTTPS", [
+ {
+ name: "test.fastfallback.com",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 1,
+ name: "test.fastfallback1.com",
+ values: [
+ { key: "alpn", value: "h3-27" },
+ { key: "port", value: h3Port },
+ { key: "echconfig", value: "456..." },
+ ],
+ },
+ },
+ {
+ name: "test.fastfallback.com",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 2,
+ name: "test.fastfallback2.com",
+ values: [
+ { key: "alpn", value: "h2" },
+ { key: "port", value: h2Port },
+ { key: "echconfig", value: "456..." },
+ ],
+ },
+ },
+ ]);
+
+ await trrServer.registerDoHAnswers("test.fastfallback1.com", "A", [
+ {
+ name: "test.fastfallback1.com",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "127.0.0.1",
+ },
+ ]);
+
+ await trrServer.registerDoHAnswers("test.fastfallback2.com", "A", [
+ {
+ name: "test.fastfallback2.com",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "127.0.0.1",
+ },
+ ]);
+
+ let chan = makeChan(`https://test.fastfallback.com/server-timing`);
+ let [req] = await channelOpenPromise(chan);
+ Assert.equal(req.protocolVersion, "h2");
+ let internal = req.QueryInterface(Ci.nsIHttpChannelInternal);
+ Assert.equal(internal.remotePort, h2Port);
+
+ await trrServer.stop();
+});
+
+// Like the previous test, but with a shorter timeout, so when fast fallback
+// timer is triggered, the http transaction is still in pending queue.
+add_task(async function testFastfallback1() {
+ trrServer = new TRRServer();
+ await trrServer.start();
+ Services.prefs.setBoolPref("network.dns.upgrade_with_https_rr", true);
+ Services.prefs.setBoolPref("network.dns.use_https_rr_as_altsvc", true);
+ Services.prefs.setBoolPref("network.dns.echconfig.enabled", false);
+
+ Services.prefs.setIntPref("network.trr.mode", 3);
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${trrServer.port}/dns-query`
+ );
+ Services.prefs.setBoolPref("network.http.http3.enabled", true);
+
+ Services.prefs.setIntPref(
+ "network.dns.httpssvc.http3_fast_fallback_timeout",
+ 10
+ );
+
+ await trrServer.registerDoHAnswers("test.fastfallback.org", "HTTPS", [
+ {
+ name: "test.fastfallback.org",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 1,
+ name: "test.fastfallback1.org",
+ values: [
+ { key: "alpn", value: "h3-27" },
+ { key: "port", value: h3Port },
+ { key: "echconfig", value: "456..." },
+ ],
+ },
+ },
+ {
+ name: "test.fastfallback.org",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 2,
+ name: "test.fastfallback2.org",
+ values: [
+ { key: "alpn", value: "h2" },
+ { key: "port", value: h2Port },
+ { key: "echconfig", value: "456..." },
+ ],
+ },
+ },
+ ]);
+
+ await trrServer.registerDoHAnswers("test.fastfallback1.org", "A", [
+ {
+ name: "test.fastfallback1.org",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "127.0.0.1",
+ },
+ ]);
+
+ await trrServer.registerDoHAnswers("test.fastfallback2.org", "A", [
+ {
+ name: "test.fastfallback2.org",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "127.0.0.1",
+ },
+ ]);
+
+ let chan = makeChan(`https://test.fastfallback.org/server-timing`);
+ let [req] = await channelOpenPromise(chan);
+ Assert.equal(req.protocolVersion, "h2");
+ let internal = req.QueryInterface(Ci.nsIHttpChannelInternal);
+ Assert.equal(internal.remotePort, h2Port);
+
+ await trrServer.stop();
+});
+
+// Test when echConfig is enabled, we can sucessfully fallback to the last
+// record.
+add_task(async function testFastfallbackWithEchConfig() {
+ trrServer = new TRRServer();
+ await trrServer.start();
+ Services.prefs.setBoolPref("network.dns.upgrade_with_https_rr", true);
+ Services.prefs.setBoolPref("network.dns.use_https_rr_as_altsvc", true);
+ Services.prefs.setBoolPref("network.dns.echconfig.enabled", true);
+
+ Services.prefs.setIntPref("network.trr.mode", 3);
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${trrServer.port}/dns-query`
+ );
+ Services.prefs.setBoolPref("network.http.http3.enabled", true);
+
+ Services.prefs.setIntPref(
+ "network.dns.httpssvc.http3_fast_fallback_timeout",
+ 1000
+ );
+
+ await trrServer.registerDoHAnswers("test.ech.org", "HTTPS", [
+ {
+ name: "test.ech.org",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 1,
+ name: "test.ech1.org",
+ values: [
+ { key: "alpn", value: "h3-27" },
+ { key: "port", value: h3Port },
+ { key: "echconfig", value: "456..." },
+ ],
+ },
+ },
+ {
+ name: "test.ech.org",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 2,
+ name: "test.ech2.org",
+ values: [
+ { key: "alpn", value: "h2" },
+ { key: "port", value: h2Port },
+ { key: "echconfig", value: "456..." },
+ ],
+ },
+ },
+ {
+ name: "test.ech.org",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 3,
+ name: "test.ech3.org",
+ values: [
+ { key: "alpn", value: "h2" },
+ { key: "port", value: h2Port },
+ { key: "echconfig", value: "456..." },
+ ],
+ },
+ },
+ ]);
+
+ await trrServer.registerDoHAnswers("test.ech1.org", "A", [
+ {
+ name: "test.ech1.org",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "127.0.0.1",
+ },
+ ]);
+
+ await trrServer.registerDoHAnswers("test.ech3.org", "A", [
+ {
+ name: "test.ech3.org",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "127.0.0.1",
+ },
+ ]);
+
+ let chan = makeChan(`https://test.ech.org/server-timing`);
+ let [req] = await channelOpenPromise(chan);
+ Assert.equal(req.protocolVersion, "h2");
+ let internal = req.QueryInterface(Ci.nsIHttpChannelInternal);
+ Assert.equal(internal.remotePort, h2Port);
+
+ await trrServer.stop();
+});
+
+// Test when echConfig is enabled, the connection should fail when not all
+// records have echConfig.
+add_task(async function testFastfallbackWithpartialEchConfig() {
+ trrServer = new TRRServer();
+ await trrServer.start();
+ Services.prefs.setBoolPref("network.dns.upgrade_with_https_rr", true);
+ Services.prefs.setBoolPref("network.dns.use_https_rr_as_altsvc", true);
+ Services.prefs.setBoolPref("network.dns.echconfig.enabled", true);
+
+ Services.prefs.setIntPref("network.trr.mode", 3);
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${trrServer.port}/dns-query`
+ );
+ Services.prefs.setBoolPref("network.http.http3.enabled", true);
+
+ Services.prefs.setIntPref(
+ "network.dns.httpssvc.http3_fast_fallback_timeout",
+ 1000
+ );
+
+ await trrServer.registerDoHAnswers("test.partial_ech.org", "HTTPS", [
+ {
+ name: "test.partial_ech.org",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 1,
+ name: "test.partial_ech1.org",
+ values: [
+ { key: "alpn", value: "h3-27" },
+ { key: "port", value: h3Port },
+ { key: "echconfig", value: "456..." },
+ ],
+ },
+ },
+ {
+ name: "test.partial_ech.org",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 2,
+ name: "test.partial_ech2.org",
+ values: [
+ { key: "alpn", value: "h2" },
+ { key: "port", value: h2Port },
+ ],
+ },
+ },
+ ]);
+
+ await trrServer.registerDoHAnswers("test.partial_ech1.org", "A", [
+ {
+ name: "test.partial_ech1.org",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "127.0.0.1",
+ },
+ ]);
+
+ let chan = makeChan(`https://test.partial_ech.org/server-timing`);
+ await channelOpenPromise(chan, CL_EXPECT_LATE_FAILURE | CL_ALLOW_UNKNOWN_CL);
+
+ await trrServer.stop();
+});
diff --git a/netwerk/test/unit/test_http3_fatal_stream_error.js b/netwerk/test/unit/test_http3_fatal_stream_error.js
new file mode 100644
index 0000000000..48b194fbba
--- /dev/null
+++ b/netwerk/test/unit/test_http3_fatal_stream_error.js
@@ -0,0 +1,149 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+let httpsUri;
+
+registerCleanupFunction(async () => {
+ Services.prefs.clearUserPref("network.http.http3.enabled");
+ Services.prefs.clearUserPref("network.dns.localDomains");
+ Services.prefs.clearUserPref("network.dns.disableIPv6");
+ Services.prefs.clearUserPref(
+ "network.http.http3.alt-svc-mapping-for-testing"
+ );
+ Services.prefs.clearUserPref("network.http.http3.backup_timer_delay");
+ dump("cleanup done\n");
+});
+
+let Http3FailedListener = function() {};
+
+Http3FailedListener.prototype = {
+ onStartRequest: function testOnStartRequest(request) {},
+
+ onDataAvailable: function testOnDataAvailable(request, stream, off, cnt) {
+ this.amount += cnt;
+ read_stream(stream, cnt);
+ },
+
+ onStopRequest: function testOnStopRequest(request, status) {
+ if (Components.isSuccessCode(status)) {
+ // This is still HTTP2 connection
+ let httpVersion = "";
+ try {
+ httpVersion = request.protocolVersion;
+ } catch (e) {}
+ Assert.equal(httpVersion, "h2");
+ this.finish(false);
+ } else {
+ Assert.equal(status, Cr.NS_ERROR_NET_PARTIAL_TRANSFER);
+ this.finish(true);
+ }
+ },
+};
+
+function chanPromise(chan, listener) {
+ return new Promise(resolve => {
+ function finish(result) {
+ resolve(result);
+ }
+ listener.finish = finish;
+ chan.asyncOpen(listener);
+ });
+}
+
+function makeChan() {
+ let chan = NetUtil.newChannel({
+ uri: httpsUri,
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+ chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI;
+ return chan;
+}
+
+function altsvcSetupPromise(chan, listener) {
+ return new Promise(resolve => {
+ function finish(result) {
+ resolve(result);
+ }
+ listener.finish = finish;
+ chan.asyncOpen(listener);
+ });
+}
+
+add_task(async function test_fatal_error() {
+ let env = Cc["@mozilla.org/process/environment;1"].getService(
+ Ci.nsIEnvironment
+ );
+
+ let h2Port = env.get("MOZHTTP2_PORT");
+ Assert.notEqual(h2Port, null);
+
+ let h3Port = env.get("MOZHTTP3_PORT_FAILED");
+ Assert.notEqual(h3Port, null);
+ Assert.notEqual(h3Port, "");
+
+ Services.prefs.setBoolPref("network.http.http3.enabled", true);
+ Services.prefs.setCharPref("network.dns.localDomains", "foo.example.com");
+ Services.prefs.setBoolPref("network.dns.disableIPv6", true);
+ Services.prefs.setCharPref(
+ "network.http.http3.alt-svc-mapping-for-testing",
+ "foo.example.com;h3-27=:" + h3Port
+ );
+ Services.prefs.setIntPref("network.http.http3.backup_timer_delay", 0);
+
+ let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(
+ Ci.nsIX509CertDB
+ );
+ addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u");
+
+ httpsUri = "https://foo.example.com:" + h2Port + "/";
+});
+
+add_task(async function test_fatal_stream_error() {
+ let result = false;
+ // We need to loop here because we need to wait for AltSvc storage to
+ // to be started.
+ do {
+ // We need to close HTTP2 connections, otherwise our connection pooling
+ // will dispatch the request over already existing HTTP2 connection.
+ Services.obs.notifyObservers(null, "net:prune-all-connections");
+ let chan = makeChan();
+ let listener = new Http3FailedListener();
+ result = await altsvcSetupPromise(chan, listener);
+ } while (result === false);
+});
+
+let CheckOnlyHttp2Listener = function() {};
+
+CheckOnlyHttp2Listener.prototype = {
+ onStartRequest: function testOnStartRequest(request) {},
+
+ onDataAvailable: function testOnDataAvailable(request, stream, off, cnt) {
+ read_stream(stream, cnt);
+ },
+
+ onStopRequest: function testOnStopRequest(request, status) {
+ Assert.equal(status, Cr.NS_OK);
+ let httpVersion = "";
+ try {
+ httpVersion = request.protocolVersion;
+ } catch (e) {}
+ Assert.equal(httpVersion, "h2");
+ this.finish(false);
+ },
+};
+
+add_task(async function test_no_http3_after_error() {
+ let chan = makeChan();
+ let listener = new CheckOnlyHttp2Listener();
+ await altsvcSetupPromise(chan, listener);
+});
+
+// also after all connections are closed.
+add_task(async function test_no_http3_after_error2() {
+ Services.obs.notifyObservers(null, "net:prune-all-connections");
+ let chan = makeChan();
+ let listener = new CheckOnlyHttp2Listener();
+ await altsvcSetupPromise(chan, listener);
+});
diff --git a/netwerk/test/unit/test_http3_large_post.js b/netwerk/test/unit/test_http3_large_post.js
new file mode 100644
index 0000000000..e309c2c00d
--- /dev/null
+++ b/netwerk/test/unit/test_http3_large_post.js
@@ -0,0 +1,101 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+registerCleanupFunction(async () => {
+ http3_clear_prefs();
+});
+
+add_task(async function setup() {
+ await http3_setup_tests();
+});
+
+let Http3Listener = function() {};
+
+Http3Listener.prototype = {
+ expectedStatus: Cr.NS_OK,
+ amount: 0,
+
+ onStartRequest: function testOnStartRequest(request) {
+ Assert.equal(request.status, this.expectedStatus);
+ if (Components.isSuccessCode(this.expectedStatus)) {
+ Assert.equal(request.responseStatus, 200);
+ }
+ Assert.equal(
+ this.amount,
+ request.getResponseHeader("x-data-received-length")
+ );
+ },
+
+ onDataAvailable: function testOnDataAvailable(request, stream, off, cnt) {
+ read_stream(stream, cnt);
+ },
+
+ onStopRequest: function testOnStopRequest(request, status) {
+ let httpVersion = "";
+ try {
+ httpVersion = request.protocolVersion;
+ } catch (e) {}
+ Assert.equal(httpVersion, "h3");
+
+ this.finish();
+ },
+};
+
+function chanPromise(chan, listener) {
+ return new Promise(resolve => {
+ function finish(result) {
+ resolve(result);
+ }
+ listener.finish = finish;
+ chan.asyncOpen(listener);
+ });
+}
+
+function makeChan(uri, amount) {
+ let chan = NetUtil.newChannel({
+ uri,
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+ chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI;
+
+ let stream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(
+ Ci.nsIStringInputStream
+ );
+ stream.data = generateContent(amount);
+ let uchan = chan.QueryInterface(Ci.nsIUploadChannel);
+ uchan.setUploadStream(stream, "text/plain", stream.available());
+ chan.requestMethod = "POST";
+ return chan;
+}
+
+// Generate a post.
+function generateContent(size) {
+ let content = "";
+ for (let i = 0; i < size; i++) {
+ content += "0";
+ }
+ return content;
+}
+
+// Send a large post that can fit into a neqo stream buffer at once.
+// But the amount of data is larger than the necko's default stream
+// buffer side, therefore ReadSegments will be called multiple times.
+add_task(async function test_large_post() {
+ let amount = 1 << 16;
+ let listener = new Http3Listener();
+ listener.amount = amount;
+ let chan = makeChan("https://foo.example.com/post", amount);
+ await chanPromise(chan, listener);
+});
+
+// Send a large post that cannot fit into a neqo stream buffer at once.
+// StreamWritable events will trigger sending more data when the buffer
+// space is freed
+add_task(async function test_large_post2() {
+ let amount = 1 << 23;
+ let listener = new Http3Listener();
+ listener.amount = amount;
+ let chan = makeChan("https://foo.example.com/post", amount);
+ await chanPromise(chan, listener);
+});
diff --git a/netwerk/test/unit/test_http3_perf.js b/netwerk/test/unit/test_http3_perf.js
new file mode 100644
index 0000000000..679450a163
--- /dev/null
+++ b/netwerk/test/unit/test_http3_perf.js
@@ -0,0 +1,262 @@
+"use strict";
+
+var perfMetadata = {
+ owner: "Network Team",
+ name: "http3 raw",
+ description:
+ "XPCShell tests that verifies the lib integration against a local server",
+ options: {
+ default: {
+ perfherder: true,
+ perfherder_metrics: [
+ {
+ name: "speed",
+ unit: "bps",
+ },
+ ],
+ xpcshell_cycles: 13,
+ verbose: true,
+ try_platform: ["linux", "mac"],
+ },
+ },
+ tags: ["network", "http3", "quic"],
+};
+
+var performance = performance || {};
+performance.now = (function() {
+ return (
+ performance.now ||
+ performance.mozNow ||
+ performance.msNow ||
+ performance.oNow ||
+ performance.webkitNow ||
+ Date.now
+ );
+})();
+
+let h3Route;
+let httpsOrigin;
+let h3AltSvc;
+
+let prefs;
+
+let tests = [
+ // This test must be the first because it setsup alt-svc connection, that
+ // other tests use.
+ test_https_alt_svc,
+ test_download,
+ testsDone,
+];
+
+let current_test = 0;
+
+function run_next_test() {
+ if (current_test < tests.length) {
+ dump("starting test number " + current_test + "\n");
+ tests[current_test]();
+ current_test++;
+ }
+}
+
+function run_test() {
+ let env = Cc["@mozilla.org/process/environment;1"].getService(
+ Ci.nsIEnvironment
+ );
+ let h2Port = env.get("MOZHTTP2_PORT");
+ Assert.notEqual(h2Port, null);
+ Assert.notEqual(h2Port, "");
+ let h3Port = env.get("MOZHTTP3_PORT");
+ Assert.notEqual(h3Port, null);
+ Assert.notEqual(h3Port, "");
+ h3AltSvc = ":" + h3Port;
+
+ h3Route = "foo.example.com:" + h3Port;
+ do_get_profile();
+ prefs = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch);
+
+ prefs.setBoolPref("network.http.http3.enabled", true);
+ prefs.setCharPref("network.dns.localDomains", "foo.example.com");
+ // We always resolve elements of localDomains as it's hardcoded without the
+ // following pref:
+ prefs.setBoolPref("network.proxy.allow_hijacking_localhost", true);
+
+ // The certificate for the http3server server is for foo.example.com and
+ // is signed by http2-ca.pem so add that cert to the trust list as a
+ // signing cert.
+ let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(
+ Ci.nsIX509CertDB
+ );
+ addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u");
+ httpsOrigin = "https://foo.example.com:" + h2Port + "/";
+
+ run_next_test();
+}
+
+function makeChan(uri) {
+ let chan = NetUtil.newChannel({
+ uri,
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+ chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI;
+ return chan;
+}
+
+let Http3CheckListener = function() {};
+
+Http3CheckListener.prototype = {
+ onDataAvailableFired: false,
+ expectedRoute: "",
+
+ onStartRequest: function testOnStartRequest(request) {
+ Assert.ok(request instanceof Ci.nsIHttpChannel);
+ Assert.equal(request.status, Cr.NS_OK);
+ Assert.equal(request.responseStatus, 200);
+ },
+
+ onDataAvailable: function testOnDataAvailable(request, stream, off, cnt) {
+ this.onDataAvailableFired = true;
+ read_stream(stream, cnt);
+ },
+
+ onStopRequest: function testOnStopRequest(request, status) {
+ dump("status is " + status + "\n");
+ // Assert.equal(status, Cr.NS_OK);
+ let routed = "NA";
+ try {
+ routed = request.getRequestHeader("Alt-Used");
+ } catch (e) {}
+ dump("routed is " + routed + "\n");
+
+ Assert.equal(routed, this.expectedRoute);
+
+ let httpVersion = "";
+ try {
+ httpVersion = request.protocolVersion;
+ } catch (e) {}
+ Assert.equal(httpVersion, "h3");
+ Assert.equal(this.onDataAvailableFired, true);
+ },
+};
+
+let WaitForHttp3Listener = function() {};
+
+WaitForHttp3Listener.prototype = new Http3CheckListener();
+
+WaitForHttp3Listener.prototype.uri = "";
+WaitForHttp3Listener.prototype.h3AltSvc = "";
+
+WaitForHttp3Listener.prototype.onStopRequest = function testOnStopRequest(
+ request,
+ status
+) {
+ Assert.equal(status, Cr.NS_OK);
+ let routed = "NA";
+ try {
+ routed = request.getRequestHeader("Alt-Used");
+ } catch (e) {}
+ dump("routed is " + routed + "\n");
+
+ if (routed == this.expectedRoute) {
+ Assert.equal(routed, this.expectedRoute); // always true, but a useful log
+
+ let httpVersion = "";
+ try {
+ httpVersion = request.protocolVersion;
+ } catch (e) {}
+ Assert.equal(httpVersion, "h3");
+ run_next_test();
+ } else {
+ dump("poll later for alt svc mapping\n");
+ do_test_pending();
+ do_timeout(500, () => {
+ doTest(this.uri, this.expectedRoute, this.h3AltSvc);
+ });
+ }
+
+ do_test_finished();
+};
+
+function doTest(uri, expectedRoute, altSvc) {
+ let chan = makeChan(uri);
+ let listener = new WaitForHttp3Listener();
+ listener.uri = uri;
+ listener.expectedRoute = expectedRoute;
+ listener.h3AltSvc = altSvc;
+ chan.setRequestHeader("x-altsvc", altSvc, false);
+ chan.asyncOpen(listener);
+}
+
+// Test Alt-Svc for http3.
+// H2 server returns alt-svc=h3-27=:h3port
+function test_https_alt_svc() {
+ dump("test_https_alt_svc()\n");
+
+ do_test_pending();
+ doTest(httpsOrigin + "http3-test", h3Route, h3AltSvc);
+}
+
+let PerfHttp3Listener = function() {};
+
+PerfHttp3Listener.prototype = new Http3CheckListener();
+PerfHttp3Listener.prototype.amount = 0;
+PerfHttp3Listener.prototype.bytesRead = 0;
+PerfHttp3Listener.prototype.startTime = 0;
+
+PerfHttp3Listener.prototype.onStartRequest = function testOnStartRequest(
+ request
+) {
+ this.startTime = performance.now();
+ Http3CheckListener.prototype.onStartRequest.call(this, request);
+};
+
+PerfHttp3Listener.prototype.onDataAvailable = function testOnStopRequest(
+ request,
+ stream,
+ off,
+ cnt
+) {
+ this.bytesRead += cnt;
+ Http3CheckListener.prototype.onDataAvailable.call(
+ this,
+ request,
+ stream,
+ off,
+ cnt
+ );
+};
+
+PerfHttp3Listener.prototype.onStopRequest = function testOnStopRequest(
+ request,
+ status
+) {
+ let stopTime = performance.now();
+ Http3CheckListener.prototype.onStopRequest.call(this, request, status);
+ if (this.bytesRead != this.amount) {
+ dump("Wrong number of bytes...");
+ } else {
+ let speed = (this.bytesRead * 1000) / (stopTime - this.startTime);
+ info("perfMetrics", { speed });
+ }
+ run_next_test();
+ do_test_finished();
+};
+
+function test_download() {
+ dump("test_download()\n");
+
+ let listener = new PerfHttp3Listener();
+ listener.expectedRoute = h3Route;
+ listener.amount = 1024 * 1024;
+ let chan = makeChan(httpsOrigin + listener.amount.toString());
+ chan.asyncOpen(listener);
+ do_test_pending();
+}
+
+function testsDone() {
+ prefs.clearUserPref("network.http.http3.enabled");
+ prefs.clearUserPref("network.dns.localDomains");
+ prefs.clearUserPref("network.proxy.allow_hijacking_localhost");
+ dump("testDone\n");
+ do_test_pending();
+ do_test_finished();
+}
diff --git a/netwerk/test/unit/test_http3_server_not_existing.js b/netwerk/test/unit/test_http3_server_not_existing.js
new file mode 100644
index 0000000000..31ce77cc36
--- /dev/null
+++ b/netwerk/test/unit/test_http3_server_not_existing.js
@@ -0,0 +1,123 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+let httpsUri;
+
+registerCleanupFunction(async () => {
+ Services.prefs.clearUserPref("network.http.http3.enabled");
+ Services.prefs.clearUserPref("network.dns.localDomains");
+ Services.prefs.clearUserPref("network.dns.disableIPv6");
+ Services.prefs.clearUserPref(
+ "network.http.http3.alt-svc-mapping-for-testing"
+ );
+ Services.prefs.clearUserPref("network.http.http3.backup_timer_delay");
+ dump("cleanup done\n");
+});
+
+function chanPromise(chan, listener) {
+ return new Promise(resolve => {
+ function finish() {
+ resolve();
+ }
+ listener.finish = finish;
+ chan.asyncOpen(listener);
+ });
+}
+
+function makeChan() {
+ let chan = NetUtil.newChannel({
+ uri: httpsUri,
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+ chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI;
+ return chan;
+}
+
+function altsvcSetupPromise(chan, listener) {
+ return new Promise(resolve => {
+ function finish(result) {
+ resolve(result);
+ }
+ listener.finish = finish;
+ chan.asyncOpen(listener);
+ });
+}
+
+add_task(async function test_fatal_error() {
+ let env = Cc["@mozilla.org/process/environment;1"].getService(
+ Ci.nsIEnvironment
+ );
+
+ let h2Port = env.get("MOZHTTP2_PORT");
+ Assert.notEqual(h2Port, null);
+
+ Services.prefs.setBoolPref("network.http.http3.enabled", true);
+ Services.prefs.setCharPref("network.dns.localDomains", "foo.example.com");
+ Services.prefs.setBoolPref("network.dns.disableIPv6", true);
+ // Set AltSvc to point to not existing HTTP3 server on port 443
+ Services.prefs.setCharPref(
+ "network.http.http3.alt-svc-mapping-for-testing",
+ "foo.example.com;h3-27=:443"
+ );
+ Services.prefs.setIntPref("network.http.http3.backup_timer_delay", 0);
+
+ let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(
+ Ci.nsIX509CertDB
+ );
+ addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u");
+
+ httpsUri = "https://foo.example.com:" + h2Port + "/";
+});
+
+add_task(async function test_fatal_stream_error() {
+ let result = 1;
+ // We need to loop here because we need to wait for AltSvc storage to
+ // to be started.
+ // We also do not have a way to verify that HTTP3 has been tried, because
+ // the fallback is automatic, so try a couple of times.
+ do {
+ // We need to close HTTP2 connections, otherwise our connection pooling
+ // will dispatch the request over already existing HTTP2 connection.
+ Services.obs.notifyObservers(null, "net:prune-all-connections");
+ let chan = makeChan();
+ let listener = new CheckOnlyHttp2Listener();
+ await altsvcSetupPromise(chan, listener);
+ result++;
+ } while (result < 5);
+});
+
+let CheckOnlyHttp2Listener = function() {};
+
+CheckOnlyHttp2Listener.prototype = {
+ onStartRequest: function testOnStartRequest(request) {},
+
+ onDataAvailable: function testOnDataAvailable(request, stream, off, cnt) {
+ read_stream(stream, cnt);
+ },
+
+ onStopRequest: function testOnStopRequest(request, status) {
+ Assert.equal(status, Cr.NS_OK);
+ let httpVersion = "";
+ try {
+ httpVersion = request.protocolVersion;
+ } catch (e) {}
+ Assert.equal(httpVersion, "h2");
+ this.finish();
+ },
+};
+
+add_task(async function test_no_http3_after_error() {
+ let chan = makeChan();
+ let listener = new CheckOnlyHttp2Listener();
+ await altsvcSetupPromise(chan, listener);
+});
+
+// also after all connections are closed.
+add_task(async function test_no_http3_after_error2() {
+ Services.obs.notifyObservers(null, "net:prune-all-connections");
+ let chan = makeChan();
+ let listener = new CheckOnlyHttp2Listener();
+ await altsvcSetupPromise(chan, listener);
+});
diff --git a/netwerk/test/unit/test_http3_trans_close.js b/netwerk/test/unit/test_http3_trans_close.js
new file mode 100644
index 0000000000..a7525a5d4b
--- /dev/null
+++ b/netwerk/test/unit/test_http3_trans_close.js
@@ -0,0 +1,84 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+registerCleanupFunction(async () => {
+ http3_clear_prefs();
+});
+
+add_task(async function setup() {
+ await http3_setup_tests();
+});
+
+let Http3Listener = function() {};
+
+Http3Listener.prototype = {
+ expectedAmount: 0,
+ expectedStatus: Cr.NS_OK,
+ amount: 0,
+
+ onStartRequest: function testOnStartRequest(request) {
+ Assert.equal(request.status, this.expectedStatus);
+ if (Components.isSuccessCode(this.expectedStatus)) {
+ Assert.equal(request.responseStatus, 200);
+ }
+ },
+
+ onDataAvailable: function testOnDataAvailable(request, stream, off, cnt) {
+ this.amount += cnt;
+ read_stream(stream, cnt);
+ },
+
+ onStopRequest: function testOnStopRequest(request, status) {
+ let httpVersion = "";
+ try {
+ httpVersion = request.protocolVersion;
+ } catch (e) {}
+ Assert.equal(httpVersion, "h3");
+ Assert.equal(this.amount, this.expectedAmount);
+
+ this.finish();
+ },
+};
+
+function chanPromise(chan, listener) {
+ return new Promise(resolve => {
+ function finish(result) {
+ resolve(result);
+ }
+ listener.finish = finish;
+ chan.asyncOpen(listener);
+ });
+}
+
+function makeChan(uri) {
+ let chan = NetUtil.newChannel({
+ uri,
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+ chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI;
+ return chan;
+}
+
+add_task(async function test_response_without_body() {
+ let chan = makeChan("https://foo.example.com/no_body");
+ let listener = new Http3Listener();
+ listener.expectedAmount = 0;
+ await chanPromise(chan, listener);
+});
+
+add_task(async function test_response_without_content_length() {
+ let chan = makeChan("https://foo.example.com/no_content_length");
+ let listener = new Http3Listener();
+ listener.expectedAmount = 4000;
+ await chanPromise(chan, listener);
+});
+
+add_task(async function test_content_length_smaller_than_data_len() {
+ let chan = makeChan("https://foo.example.com/content_length_smaller");
+ let listener = new Http3Listener();
+ // content-lentgth is 4000, but data length is 8000.
+ // We should return an error here - bug 1670086.
+ listener.expectedAmount = 4000;
+ await chanPromise(chan, listener);
+});
diff --git a/netwerk/test/unit/test_httpResponseTimeout.js b/netwerk/test/unit/test_httpResponseTimeout.js
new file mode 100644
index 0000000000..6c82758156
--- /dev/null
+++ b/netwerk/test/unit/test_httpResponseTimeout.js
@@ -0,0 +1,162 @@
+/* -*- Mode: javascript; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+var baseURL;
+const kResponseTimeoutPref = "network.http.response.timeout";
+const kResponseTimeout = 1;
+const kShortLivedKeepalivePref =
+ "network.http.tcp_keepalive.short_lived_connections";
+const kLongLivedKeepalivePref =
+ "network.http.tcp_keepalive.long_lived_connections";
+
+const prefService = Cc["@mozilla.org/preferences-service;1"].getService(
+ Ci.nsIPrefBranch
+);
+
+var server = new HttpServer();
+
+function TimeoutListener(expectResponse) {
+ this.expectResponse = expectResponse;
+}
+
+TimeoutListener.prototype = {
+ onStartRequest(request) {},
+
+ onDataAvailable(request, stream) {},
+
+ onStopRequest(request, status) {
+ if (this.expectResponse) {
+ Assert.equal(status, Cr.NS_OK);
+ } else {
+ Assert.equal(status, Cr.NS_ERROR_NET_TIMEOUT);
+ }
+
+ run_next_test();
+ },
+};
+
+function serverStopListener() {
+ do_test_finished();
+}
+
+function testTimeout(timeoutEnabled, expectResponse) {
+ // Set timeout pref.
+ if (timeoutEnabled) {
+ prefService.setIntPref(kResponseTimeoutPref, kResponseTimeout);
+ } else {
+ prefService.setIntPref(kResponseTimeoutPref, 0);
+ }
+
+ var chan = NetUtil.newChannel({
+ uri: baseURL,
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+ var listener = new TimeoutListener(expectResponse);
+ chan.asyncOpen(listener);
+}
+
+function testTimeoutEnabled() {
+ // Set a timeout value; expect a timeout and no response.
+ testTimeout(true, false);
+}
+
+function testTimeoutDisabled() {
+ // Set a timeout value of 0; expect a response.
+ testTimeout(false, true);
+}
+
+function testTimeoutDisabledByShortLivedKeepalives() {
+ // Enable TCP Keepalives for short lived HTTP connections.
+ prefService.setBoolPref(kShortLivedKeepalivePref, true);
+ prefService.setBoolPref(kLongLivedKeepalivePref, false);
+
+ // Try to set a timeout value, but expect a response without timeout.
+ testTimeout(true, true);
+}
+
+function testTimeoutDisabledByLongLivedKeepalives() {
+ // Enable TCP Keepalives for long lived HTTP connections.
+ prefService.setBoolPref(kShortLivedKeepalivePref, false);
+ prefService.setBoolPref(kLongLivedKeepalivePref, true);
+
+ // Try to set a timeout value, but expect a response without timeout.
+ testTimeout(true, true);
+}
+
+function testTimeoutDisabledByBothKeepalives() {
+ // Enable TCP Keepalives for short and long lived HTTP connections.
+ prefService.setBoolPref(kShortLivedKeepalivePref, true);
+ prefService.setBoolPref(kLongLivedKeepalivePref, true);
+
+ // Try to set a timeout value, but expect a response without timeout.
+ testTimeout(true, true);
+}
+
+function setup_tests() {
+ // Start tests with timeout enabled, i.e. disable TCP keepalives for HTTP.
+ // Reset pref in cleanup.
+ if (prefService.getBoolPref(kShortLivedKeepalivePref)) {
+ prefService.setBoolPref(kShortLivedKeepalivePref, false);
+ registerCleanupFunction(function() {
+ prefService.setBoolPref(kShortLivedKeepalivePref, true);
+ });
+ }
+ if (prefService.getBoolPref(kLongLivedKeepalivePref)) {
+ prefService.setBoolPref(kLongLivedKeepalivePref, false);
+ registerCleanupFunction(function() {
+ prefService.setBoolPref(kLongLivedKeepalivePref, true);
+ });
+ }
+
+ var tests = [
+ // Enable with a timeout value >0;
+ testTimeoutEnabled,
+ // Disable with a timeout value of 0;
+ testTimeoutDisabled,
+ // Disable by enabling TCP keepalive for short-lived HTTP connections.
+ testTimeoutDisabledByShortLivedKeepalives,
+ // Disable by enabling TCP keepalive for long-lived HTTP connections.
+ testTimeoutDisabledByLongLivedKeepalives,
+ // Disable by enabling TCP keepalive for both HTTP connection types.
+ testTimeoutDisabledByBothKeepalives,
+ ];
+
+ for (var i = 0; i < tests.length; i++) {
+ add_test(tests[i]);
+ }
+}
+
+function setup_http_server() {
+ // Start server; will be stopped at test cleanup time.
+ server.start(-1);
+ baseURL = "http://localhost:" + server.identity.primaryPort + "/";
+ info("Using baseURL: " + baseURL);
+ server.registerPathHandler("/", function(metadata, response) {
+ // Wait until the timeout should have passed, then respond.
+ response.processAsync();
+
+ do_timeout((kResponseTimeout + 1) * 1000 /* ms */, function() {
+ response.setStatusLine(metadata.httpVersion, 200, "OK");
+ response.write("Hello world");
+ response.finish();
+ });
+ });
+ registerCleanupFunction(function() {
+ server.stop(serverStopListener);
+ });
+}
+
+function run_test() {
+ setup_http_server();
+
+ setup_tests();
+
+ run_next_test();
+}
diff --git a/netwerk/test/unit/test_http_headers.js b/netwerk/test/unit/test_http_headers.js
new file mode 100644
index 0000000000..e87d4cc2bb
--- /dev/null
+++ b/netwerk/test/unit/test_http_headers.js
@@ -0,0 +1,75 @@
+"use strict";
+
+function check_request_header(chan, name, value) {
+ var chanValue;
+ try {
+ chanValue = chan.getRequestHeader(name);
+ } catch (e) {
+ do_throw("Expected to find header '" + name + "' but didn't find it");
+ }
+ Assert.equal(chanValue, value);
+}
+
+function run_test() {
+ var chan = NetUtil.newChannel({
+ uri: "http://www.mozilla.org/",
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+
+ check_request_header(chan, "host", "www.mozilla.org");
+ check_request_header(chan, "Host", "www.mozilla.org");
+
+ chan.setRequestHeader("foopy", "bar", false);
+ check_request_header(chan, "foopy", "bar");
+
+ chan.setRequestHeader("foopy", "baz", true);
+ check_request_header(chan, "foopy", "bar, baz");
+
+ for (var i = 0; i < 100; ++i) {
+ chan.setRequestHeader("foopy" + i, i, false);
+ }
+
+ for (var i = 0; i < 100; ++i) {
+ check_request_header(chan, "foopy" + i, i);
+ }
+
+ var x = false;
+ try {
+ chan.setRequestHeader("foo:py", "baz", false);
+ } catch (e) {
+ x = true;
+ }
+ if (!x) {
+ do_throw("header with colon not rejected");
+ }
+
+ x = false;
+ try {
+ chan.setRequestHeader("foopy", "b\naz", false);
+ } catch (e) {
+ x = true;
+ }
+ if (!x) {
+ do_throw("header value with newline not rejected");
+ }
+
+ x = false;
+ try {
+ chan.setRequestHeader("foopy\u0080", "baz", false);
+ } catch (e) {
+ x = true;
+ }
+ if (!x) {
+ do_throw("header name with non-ASCII not rejected");
+ }
+
+ x = false;
+ try {
+ chan.setRequestHeader("foopy", "b\u0000az", false);
+ } catch (e) {
+ x = true;
+ }
+ if (!x) {
+ do_throw("header value with null-byte not rejected");
+ }
+}
diff --git a/netwerk/test/unit/test_http_sfv.js b/netwerk/test/unit/test_http_sfv.js
new file mode 100644
index 0000000000..c1254865fb
--- /dev/null
+++ b/netwerk/test/unit/test_http_sfv.js
@@ -0,0 +1,590 @@
+"use strict";
+
+var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+var { AppConstants } = ChromeUtils.import(
+ "resource://gre/modules/AppConstants.jsm"
+);
+const gService = Cc["@mozilla.org/http-sfv-service;1"].getService(
+ Ci.nsISFVService
+);
+
+add_task(async function test_sfv_bare_item() {
+ // tests bare item
+ let item_int = gService.newInteger(19);
+ Assert.equal(item_int.value, 19, "check bare item value");
+
+ let item_bool = gService.newBool(true);
+ Assert.equal(item_bool.value, true, "check bare item value");
+ item_bool.value = false;
+ Assert.equal(item_bool.value, false, "check bare item value");
+
+ let item_float = gService.newDecimal(145.45);
+ Assert.equal(item_float.value, 145.45);
+
+ let item_str = gService.newString("some_string");
+ Assert.equal(item_str.value, "some_string", "check bare item value");
+
+ let item_byte_seq = gService.newByteSequence("aGVsbG8=");
+ Assert.equal(item_byte_seq.value, "aGVsbG8=", "check bare item value");
+
+ let item_token = gService.newToken("*a");
+ Assert.equal(item_token.value, "*a", "check bare item value");
+});
+
+add_task(async function test_sfv_params() {
+ // test params
+ let params = gService.newParameters();
+ let bool_param = gService.newBool(false);
+ let int_param = gService.newInteger(15);
+ let decimal_param = gService.newDecimal(15.45);
+
+ params.set("bool_param", bool_param);
+ params.set("int_param", int_param);
+ params.set("decimal_param", decimal_param);
+
+ Assert.throws(
+ () => {
+ params.get("some_param");
+ },
+ /NS_ERROR_UNEXPECTED/,
+ "must throw exception as key does not exist in parameters"
+ );
+ Assert.equal(
+ params.get("bool_param").QueryInterface(Ci.nsISFVBool).value,
+ false,
+ "get parameter by key and check its value"
+ );
+ Assert.equal(
+ params.get("int_param").QueryInterface(Ci.nsISFVInteger).value,
+ 15,
+ "get parameter by key and check its value"
+ );
+ Assert.equal(
+ params.get("decimal_param").QueryInterface(Ci.nsISFVDecimal).value,
+ 15.45,
+ "get parameter by key and check its value"
+ );
+ Assert.deepEqual(
+ params.keys(),
+ ["bool_param", "int_param", "decimal_param"],
+ "check that parameters contain all the expected keys"
+ );
+
+ params.delete("int_param");
+ Assert.deepEqual(
+ params.keys(),
+ ["bool_param", "decimal_param"],
+ "check that parameter has been deleted"
+ );
+
+ Assert.throws(
+ () => {
+ params.delete("some_param");
+ },
+ /NS_ERROR_UNEXPECTED/,
+ "must throw exception upon attempt to delete by non-existing key"
+ );
+});
+
+add_task(async function test_sfv_inner_list() {
+ // create primitives for inner list
+ let item1_params = gService.newParameters();
+ item1_params.set("param_1", gService.newToken("*smth"));
+ let item1 = gService.newItem(gService.newDecimal(172.145865), item1_params);
+
+ let item2_params = gService.newParameters();
+ item2_params.set("param_1", gService.newBool(true));
+ item2_params.set("param_2", gService.newInteger(145454));
+ let item2 = gService.newItem(
+ gService.newByteSequence("weather"),
+ item2_params
+ );
+
+ // create inner list
+ let inner_list_params = gService.newParameters();
+ inner_list_params.set("inner_param", gService.newByteSequence("tests"));
+ let inner_list = gService.newInnerList([item1, item2], inner_list_params);
+
+ // check inner list members & params
+ let inner_list_members = inner_list.QueryInterface(Ci.nsISFVInnerList).items;
+ let inner_list_parameters = inner_list
+ .QueryInterface(Ci.nsISFVInnerList)
+ .params.QueryInterface(Ci.nsISFVParams);
+ Assert.equal(inner_list_members.length, 2, "check inner list length");
+
+ let inner_item1 = inner_list_members[0].QueryInterface(Ci.nsISFVItem);
+ Assert.equal(
+ inner_item1.value.QueryInterface(Ci.nsISFVDecimal).value,
+ 172.145865,
+ "check inner list member value"
+ );
+
+ let inner_item2 = inner_list_members[1].QueryInterface(Ci.nsISFVItem);
+ Assert.equal(
+ inner_item2.value.QueryInterface(Ci.nsISFVByteSeq).value,
+ "weather",
+ "check inner list member value"
+ );
+
+ Assert.equal(
+ inner_list_parameters.get("inner_param").QueryInterface(Ci.nsISFVByteSeq)
+ .value,
+ "tests",
+ "get inner list parameter by key and check its value"
+ );
+});
+
+add_task(async function test_sfv_item() {
+ // create parameters for item
+ let params = gService.newParameters();
+ let param1 = gService.newBool(false);
+ let param2 = gService.newString("str_value");
+ let param3 = gService.newBool(true);
+ params.set("param_1", param1);
+ params.set("param_2", param2);
+ params.set("param_3", param3);
+
+ // create item field
+ let item = gService.newItem(gService.newToken("*abc"), params);
+
+ Assert.equal(
+ item.value.QueryInterface(Ci.nsISFVToken).value,
+ "*abc",
+ "check items's value"
+ );
+ Assert.equal(
+ item.params.get("param_1").QueryInterface(Ci.nsISFVBool).value,
+ false,
+ "get item parameter by key and check its value"
+ );
+ Assert.equal(
+ item.params.get("param_2").QueryInterface(Ci.nsISFVString).value,
+ "str_value",
+ "get item parameter by key and check its value"
+ );
+ Assert.equal(
+ item.params.get("param_3").QueryInterface(Ci.nsISFVBool).value,
+ true,
+ "get item parameter by key and check its value"
+ );
+
+ // check item field serialization
+ let serialized = item.serialize();
+ Assert.equal(
+ serialized,
+ `*abc;param_1=?0;param_2="str_value";param_3`,
+ "serialized output must match expected one"
+ );
+});
+
+add_task(async function test_sfv_list() {
+ // create primitives for List
+ let item1_params = gService.newParameters();
+ item1_params.set("param_1", gService.newToken("*smth"));
+ let item1 = gService.newItem(gService.newDecimal(145454.14568), item1_params);
+
+ let item2_params = gService.newParameters();
+ item2_params.set("param_1", gService.newBool(true));
+ let item2 = gService.newItem(
+ gService.newByteSequence("weather"),
+ item2_params
+ );
+
+ let inner_list = gService.newInnerList(
+ [item1, item2],
+ gService.newParameters()
+ );
+
+ // create list field
+ let list = gService.newList([item1, inner_list]);
+
+ // check list's members
+ let list_members = list.members;
+ Assert.equal(list_members.length, 2, "check list length");
+
+ // check list's member of item type
+ let member1 = list_members[0].QueryInterface(Ci.nsISFVItem);
+ Assert.equal(
+ member1.value.QueryInterface(Ci.nsISFVDecimal).value,
+ 145454.14568,
+ "check list member's value"
+ );
+ let member1_parameters = member1.params;
+ Assert.equal(
+ member1_parameters.get("param_1").QueryInterface(Ci.nsISFVToken).value,
+ "*smth",
+ "get list member's parameter by key and check its value"
+ );
+
+ // check list's member of inner list type
+ let inner_list_members = list_members[1].QueryInterface(Ci.nsISFVInnerList)
+ .items;
+ Assert.equal(inner_list_members.length, 2, "check inner list length");
+
+ let inner_item1 = inner_list_members[0].QueryInterface(Ci.nsISFVItem);
+ Assert.equal(
+ inner_item1.value.QueryInterface(Ci.nsISFVDecimal).value,
+ 145454.14568,
+ "check inner list member's value"
+ );
+
+ let inner_item2 = inner_list_members[1].QueryInterface(Ci.nsISFVItem);
+ Assert.equal(
+ inner_item2.value.QueryInterface(Ci.nsISFVByteSeq).value,
+ "weather",
+ "check inner list member's value"
+ );
+
+ // check inner list member's params
+ list_members[1]
+ .QueryInterface(Ci.nsISFVInnerList)
+ .params.QueryInterface(Ci.nsISFVParams);
+
+ // check serialization of list field
+ let expected_serialized =
+ "145454.146;param_1=*smth, (145454.146;param_1=*smth :d2VhdGhlcg==:;param_1)";
+ let actual_serialized = list.serialize();
+ Assert.equal(
+ actual_serialized,
+ expected_serialized,
+ "serialized output must match expected one"
+ );
+});
+
+add_task(async function test_sfv_dictionary() {
+ // create primitives for dictionary field
+
+ // dict member1
+ let params1 = gService.newParameters();
+ params1.set("mp_1", gService.newBool(true));
+ params1.set("mp_2", gService.newDecimal(68.758602));
+ let member1 = gService.newItem(gService.newString("member_1"), params1);
+
+ // dict member2
+ let params2 = gService.newParameters();
+ let inner_item1 = gService.newItem(
+ gService.newString("inner_item_1"),
+ gService.newParameters()
+ );
+ let inner_item2 = gService.newItem(
+ gService.newToken("tok"),
+ gService.newParameters()
+ );
+ let member2 = gService.newInnerList([inner_item1, inner_item2], params2);
+
+ // dict member3
+ let params_3 = gService.newParameters();
+ params_3.set("mp_1", gService.newInteger(6586));
+ let member3 = gService.newItem(gService.newString("member_3"), params_3);
+
+ // create dictionary field
+ let dict = gService.newDictionary();
+ dict.set("key_1", member1);
+ dict.set("key_2", member2);
+ dict.set("key_3", member3);
+
+ // check dictionary keys
+ let expected = ["key_1", "key_2", "key_3"];
+ Assert.deepEqual(
+ expected,
+ dict.keys(),
+ "check dictionary contains all the expected keys"
+ );
+
+ // check dictionary members
+ Assert.throws(
+ () => {
+ dict.get("key_4");
+ },
+ /NS_ERROR_UNEXPECTED/,
+ "must throw exception as key does not exist in dictionary"
+ );
+
+ // let dict_member1 = dict.get("key_1").QueryInterface(Ci.nsISFVItem);
+ let dict_member2 = dict.get("key_2").QueryInterface(Ci.nsISFVInnerList);
+ let dict_member3 = dict.get("key_3").QueryInterface(Ci.nsISFVItem);
+
+ // Assert.equal(
+ // dict_member1.value.QueryInterface(Ci.nsISFVString).value,
+ // "member_1",
+ // "check dictionary member's value"
+ // );
+ // Assert.equal(
+ // dict_member1.params.get("mp_1").QueryInterface(Ci.nsISFVBool).value,
+ // true,
+ // "get dictionary member's parameter by key and check its value"
+ // );
+ // Assert.equal(
+ // dict_member1.params.get("mp_2").QueryInterface(Ci.nsISFVDecimal).value,
+ // "68.758602",
+ // "get dictionary member's parameter by key and check its value"
+ // );
+
+ let dict_member2_items = dict_member2.QueryInterface(Ci.nsISFVInnerList)
+ .items;
+ let dict_member2_params = dict_member2
+ .QueryInterface(Ci.nsISFVInnerList)
+ .params.QueryInterface(Ci.nsISFVParams);
+ Assert.equal(
+ dict_member2_items[0]
+ .QueryInterface(Ci.nsISFVItem)
+ .value.QueryInterface(Ci.nsISFVString).value,
+ "inner_item_1",
+ "get dictionary member of inner list type, and check inner list member's value"
+ );
+ Assert.equal(
+ dict_member2_items[1]
+ .QueryInterface(Ci.nsISFVItem)
+ .value.QueryInterface(Ci.nsISFVToken).value,
+ "tok",
+ "get dictionary member of inner list type, and check inner list member's value"
+ );
+ Assert.throws(
+ () => {
+ dict_member2_params.get("some_param");
+ },
+ /NS_ERROR_UNEXPECTED/,
+ "must throw exception as dict member's parameters are empty"
+ );
+
+ Assert.equal(
+ dict_member3.value.QueryInterface(Ci.nsISFVString).value,
+ "member_3",
+ "check dictionary member's value"
+ );
+ Assert.equal(
+ dict_member3.params.get("mp_1").QueryInterface(Ci.nsISFVInteger).value,
+ 6586,
+ "get dictionary member's parameter by key and check its value"
+ );
+
+ // check serialization of Dictionary field
+ let expected_serialized = `key_1="member_1";mp_1;mp_2=68.759, key_2=("inner_item_1" tok), key_3="member_3";mp_1=6586`;
+ let actual_serialized = dict.serialize();
+ Assert.equal(
+ actual_serialized,
+ expected_serialized,
+ "serialized output must match expected one"
+ );
+
+ // check deleting dict member
+ dict.delete("key_2");
+ Assert.deepEqual(
+ dict.keys(),
+ ["key_1", "key_3"],
+ "check that dictionary member has been deleted"
+ );
+
+ Assert.throws(
+ () => {
+ dict.delete("some_key");
+ },
+ /NS_ERROR_UNEXPECTED/,
+ "must throw exception upon attempt to delete by non-existing key"
+ );
+});
+
+add_task(async function test_sfv_item_parsing() {
+ Assert.ok(gService.parseItem(`"str"`), "input must be parsed into Item");
+ Assert.ok(gService.parseItem("12.35;a "), "input must be parsed into Item");
+ Assert.ok(gService.parseItem("12.35; a "), "input must be parsed into Item");
+ Assert.ok(gService.parseItem("12.35 "), "input must be parsed into Item");
+
+ Assert.throws(
+ () => {
+ gService.parseItem("12.35;\ta ");
+ },
+ /NS_ERROR_FAILURE/,
+ "item parsing must fail: invalid parameters delimiter"
+ );
+
+ Assert.throws(
+ () => {
+ gService.parseItem("125666.3565648855455");
+ },
+ /NS_ERROR_FAILURE/,
+ "item parsing must fail: decimal too long"
+ );
+});
+
+add_task(async function test_sfv_list_parsing() {
+ Assert.ok(
+ gService.parseList(
+ "(?1;param_1=*smth :d2VhdGhlcg==:;param_1;param_2=145454);inner_param=:d2VpcmR0ZXN0cw==:"
+ ),
+ "input must be parsed into List"
+ );
+ Assert.ok("a, (b c)", "input must be parsed into List");
+
+ Assert.throws(() => {
+ gService.parseList("?tok", "list parsing must fail");
+ }, /NS_ERROR_FAILURE/);
+
+ Assert.throws(() => {
+ gService.parseList(
+ "a, (b, c)",
+ "list parsing must fail: invalid delimiter within inner list"
+ );
+ }, /NS_ERROR_FAILURE/);
+
+ Assert.throws(
+ () => {
+ gService.parseList("a, b c");
+ },
+ /NS_ERROR_FAILURE/,
+ "list parsing must fail: invalid delimiter"
+ );
+});
+
+add_task(async function test_sfv_dict_parsing() {
+ Assert.ok(
+ gService.parseDictionary(`abc=123;a=1;b=2, def=456, ghi=789;q=9;r="+w"`),
+ "input must be parsed into Dictionary"
+ );
+ Assert.ok(
+ gService.parseDictionary("a=1\t,\t\t\t c=*"),
+ "input must be parsed into Dictionary"
+ );
+ Assert.ok(
+ gService.parseDictionary("a=1\t,\tc=* \t\t"),
+ "input must be parsed into Dictionary"
+ );
+
+ Assert.throws(
+ () => {
+ gService.parseDictionary("a=1\t,\tc=*,");
+ },
+ /NS_ERROR_FAILURE/,
+ "dictionary parsing must fail: trailing comma"
+ );
+
+ Assert.throws(
+ () => {
+ gService.parseDictionary("a=1 c=*");
+ },
+ /NS_ERROR_FAILURE/,
+ "dictionary parsing must fail: invalid delimiter"
+ );
+
+ Assert.throws(
+ () => {
+ gService.parseDictionary("INVALID_key=1, c=*");
+ },
+ /NS_ERROR_FAILURE/,
+ "dictionary parsing must fail: invalid key format, can't be in uppercase"
+ );
+});
+
+add_task(async function test_sfv_list_parse_serialize() {
+ let list_field = gService.parseList("1 , 42, (42 43)");
+ Assert.equal(
+ list_field.serialize(),
+ "1, 42, (42 43)",
+ "serialized output must match expected one"
+ );
+
+ // create new inner list with parameters
+ let inner_list_params = gService.newParameters();
+ inner_list_params.set("key1", gService.newString("value1"));
+ inner_list_params.set("key2", gService.newBool(true));
+ inner_list_params.set("key3", gService.newBool(false));
+ let inner_list_items = [
+ gService.newItem(
+ gService.newDecimal(-1865.75653),
+ gService.newParameters()
+ ),
+ gService.newItem(gService.newToken("token"), gService.newParameters()),
+ gService.newItem(gService.newString(`no"yes`), gService.newParameters()),
+ ];
+ let new_list_member = gService.newInnerList(
+ inner_list_items,
+ inner_list_params
+ );
+
+ // set one of list members to inner list and check it's serialized as expected
+ let members = list_field.members;
+ members[1] = new_list_member;
+ list_field.members = members;
+ Assert.equal(
+ list_field.serialize(),
+ `1, (-1865.757 token "no\\"yes");key1="value1";key2;key3=?0, (42 43)`,
+ "update list member and check list is serialized as expected"
+ );
+});
+
+add_task(async function test_sfv_dict_parse_serialize() {
+ let dict_field = gService.parseDictionary(
+ "a=1, b; foo=*, \tc=3, \t \tabc=123;a=1;b=2\t"
+ );
+ Assert.equal(
+ dict_field.serialize(),
+ "a=1, b;foo=*, c=3, abc=123;a=1;b=2",
+ "serialized output must match expected one"
+ );
+
+ // set new value for existing dict's key
+ dict_field.set(
+ "a",
+ gService.newItem(gService.newInteger(165), gService.newParameters())
+ );
+
+ // add new member to dict
+ dict_field.set(
+ "key",
+ gService.newItem(gService.newDecimal(45.0), gService.newParameters())
+ );
+
+ // check dict is serialized properly after the above changes
+ Assert.equal(
+ dict_field.serialize(),
+ "a=165, b;foo=*, c=3, abc=123;a=1;b=2, key=45.0",
+ "update dictionary members and dictionary list is serialized as expected"
+ );
+});
+
+add_task(async function test_sfv_list_parse_more() {
+ // check parsing of multiline header of List type
+ let list_field = gService.parseList("(12 abc), 12.456\t\t ");
+ list_field.parseMore("11, 15, tok");
+ Assert.equal(
+ list_field.serialize(),
+ "(12 abc), 12.456, 11, 15, tok",
+ "multi-line field value parsed and serialized successfully"
+ );
+
+ // should fail parsing one more line
+ Assert.throws(
+ () => {
+ list_field.parseMore("(tk\t1)");
+ },
+ /NS_ERROR_FAILURE/,
+ "line parsing must fail: invalid delimiter in inner list"
+ );
+ Assert.equal(
+ list_field.serialize(),
+ "(12 abc), 12.456, 11, 15, tok",
+ "parsed value must not change if parsing one more line of header fails"
+ );
+});
+
+add_task(async function test_sfv_dict_parse_more() {
+ // check parsing of multiline header of Dictionary type
+ let dict_field = gService.parseDictionary("");
+ dict_field.parseMore("key2=?0, key3=?1, key4=itm");
+ dict_field.parseMore("key1, key5=11, key4=45");
+
+ Assert.equal(
+ dict_field.serialize(),
+ "key2=?0, key3, key4=45, key1, key5=11",
+ "multi-line field value parsed and serialized successfully"
+ );
+
+ // should fail parsing one more line
+ Assert.throws(
+ () => {
+ dict_field.parseMore("c=12, _k=13");
+ },
+ /NS_ERROR_FAILURE/,
+ "line parsing must fail: invalid key format"
+ );
+});
diff --git a/netwerk/test/unit/test_httpauth.js b/netwerk/test/unit/test_httpauth.js
new file mode 100644
index 0000000000..9c9de82618
--- /dev/null
+++ b/netwerk/test/unit/test_httpauth.js
@@ -0,0 +1,204 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// This test makes sure the HTTP authenticated sessions are correctly cleared
+// when entering and leaving the private browsing mode.
+
+"use strict";
+
+function run_test() {
+ var am = Cc["@mozilla.org/network/http-auth-manager;1"].getService(
+ Ci.nsIHttpAuthManager
+ );
+
+ const kHost1 = "pbtest3.example.com";
+ const kHost2 = "pbtest4.example.com";
+ const kPort = 80;
+ const kHTTP = "http";
+ const kBasic = "basic";
+ const kRealm = "realm";
+ const kDomain = "example.com";
+ const kUser = "user";
+ const kUser2 = "user2";
+ const kPassword = "pass";
+ const kPassword2 = "pass2";
+ const kEmpty = "";
+
+ const PRIVATE = true;
+ const NOT_PRIVATE = false;
+
+ try {
+ var domain = { value: kEmpty },
+ user = { value: kEmpty },
+ pass = { value: kEmpty };
+ // simulate a login via HTTP auth outside of the private mode
+ am.setAuthIdentity(
+ kHTTP,
+ kHost1,
+ kPort,
+ kBasic,
+ kRealm,
+ kEmpty,
+ kDomain,
+ kUser,
+ kPassword
+ );
+ // make sure the recently added auth entry is available outside the private browsing mode
+ am.getAuthIdentity(
+ kHTTP,
+ kHost1,
+ kPort,
+ kBasic,
+ kRealm,
+ kEmpty,
+ domain,
+ user,
+ pass,
+ NOT_PRIVATE
+ );
+ Assert.equal(domain.value, kDomain);
+ Assert.equal(user.value, kUser);
+ Assert.equal(pass.value, kPassword);
+
+ // make sure the added auth entry is no longer accessible in private
+ domain = { value: kEmpty };
+ user = { value: kEmpty };
+ pass = { value: kEmpty };
+ try {
+ // should throw
+ am.getAuthIdentity(
+ kHTTP,
+ kHost1,
+ kPort,
+ kBasic,
+ kRealm,
+ kEmpty,
+ domain,
+ user,
+ pass,
+ PRIVATE
+ );
+ do_throw(
+ "Auth entry should not be retrievable after entering the private browsing mode"
+ );
+ } catch (e) {
+ Assert.equal(domain.value, kEmpty);
+ Assert.equal(user.value, kEmpty);
+ Assert.equal(pass.value, kEmpty);
+ }
+
+ // simulate a login via HTTP auth inside of the private mode
+ am.setAuthIdentity(
+ kHTTP,
+ kHost2,
+ kPort,
+ kBasic,
+ kRealm,
+ kEmpty,
+ kDomain,
+ kUser2,
+ kPassword2,
+ PRIVATE
+ );
+ // make sure the recently added auth entry is available inside the private browsing mode
+ domain = { value: kEmpty };
+ user = { value: kEmpty };
+ pass = { value: kEmpty };
+ am.getAuthIdentity(
+ kHTTP,
+ kHost2,
+ kPort,
+ kBasic,
+ kRealm,
+ kEmpty,
+ domain,
+ user,
+ pass,
+ PRIVATE
+ );
+ Assert.equal(domain.value, kDomain);
+ Assert.equal(user.value, kUser2);
+ Assert.equal(pass.value, kPassword2);
+
+ try {
+ // make sure the recently added auth entry is not available outside the private browsing mode
+ domain = { value: kEmpty };
+ user = { value: kEmpty };
+ pass = { value: kEmpty };
+ am.getAuthIdentity(
+ kHTTP,
+ kHost2,
+ kPort,
+ kBasic,
+ kRealm,
+ kEmpty,
+ domain,
+ user,
+ pass,
+ NOT_PRIVATE
+ );
+ do_throw(
+ "Auth entry should not be retrievable outside of private browsing mode"
+ );
+ } catch (x) {
+ Assert.equal(domain.value, kEmpty);
+ Assert.equal(user.value, kEmpty);
+ Assert.equal(pass.value, kEmpty);
+ }
+
+ // simulate leaving private browsing mode
+ Services.obs.notifyObservers(null, "last-pb-context-exited");
+
+ // make sure the added auth entry is no longer accessible in any privacy state
+ domain = { value: kEmpty };
+ user = { value: kEmpty };
+ pass = { value: kEmpty };
+ try {
+ // should throw (not available in public mode)
+ am.getAuthIdentity(
+ kHTTP,
+ kHost2,
+ kPort,
+ kBasic,
+ kRealm,
+ kEmpty,
+ domain,
+ user,
+ pass,
+ NOT_PRIVATE
+ );
+ do_throw(
+ "Auth entry should not be retrievable after exiting the private browsing mode"
+ );
+ } catch (e) {
+ Assert.equal(domain.value, kEmpty);
+ Assert.equal(user.value, kEmpty);
+ Assert.equal(pass.value, kEmpty);
+ }
+ try {
+ // should throw (no longer available in private mode)
+ am.getAuthIdentity(
+ kHTTP,
+ kHost2,
+ kPort,
+ kBasic,
+ kRealm,
+ kEmpty,
+ domain,
+ user,
+ pass,
+ PRIVATE
+ );
+ do_throw(
+ "Auth entry should not be retrievable in private mode after exiting the private browsing mode"
+ );
+ } catch (x) {
+ Assert.equal(domain.value, kEmpty);
+ Assert.equal(user.value, kEmpty);
+ Assert.equal(pass.value, kEmpty);
+ }
+ } catch (e) {
+ do_throw("Unexpected exception while testing HTTP auth manager: " + e);
+ }
+}
diff --git a/netwerk/test/unit/test_httpcancel.js b/netwerk/test/unit/test_httpcancel.js
new file mode 100644
index 0000000000..85b4f99d89
--- /dev/null
+++ b/netwerk/test/unit/test_httpcancel.js
@@ -0,0 +1,260 @@
+// This file ensures that canceling a channel early does not
+// send the request to the server (bug 350790)
+//
+// I've also shoehorned in a test that ENSURE_CALLED_BEFORE_CONNECT works as
+// expected: see comments that start with ENSURE_CALLED_BEFORE_CONNECT:
+//
+// This test also checks that cancelling a channel before asyncOpen, after
+// onStopRequest, or during onDataAvailable works as expected.
+
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+function inChildProcess() {
+ return (
+ Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime)
+ .processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT
+ );
+}
+
+var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
+var ReferrerInfo = Components.Constructor(
+ "@mozilla.org/referrer-info;1",
+ "nsIReferrerInfo",
+ "init"
+);
+var observer = {
+ QueryInterface: ChromeUtils.generateQI(["nsIObserver"]),
+
+ observe(subject, topic, data) {
+ subject = subject.QueryInterface(Ci.nsIRequest);
+ subject.cancel(Cr.NS_BINDING_ABORTED);
+
+ // ENSURE_CALLED_BEFORE_CONNECT: setting values should still work
+ try {
+ subject.QueryInterface(Ci.nsIHttpChannel);
+ let currentReferrer = subject.getRequestHeader("Referer");
+ Assert.equal(currentReferrer, "http://site1.com/");
+ var uri = ios.newURI("http://site2.com");
+ subject.referrerInfo = new ReferrerInfo(
+ Ci.nsIReferrerInfo.EMPTY,
+ true,
+ uri
+ );
+ } catch (ex) {
+ do_throw("Exception: " + ex);
+ }
+ },
+};
+
+let cancelDuringOnStartListener = {
+ onStartRequest: function test_onStartR(request) {
+ Assert.equal(request.status, Cr.NS_BINDING_ABORTED);
+
+ // ENSURE_CALLED_BEFORE_CONNECT: setting referrer should now fail
+ try {
+ request.QueryInterface(Ci.nsIHttpChannel);
+ let currentReferrer = request.getRequestHeader("Referer");
+ Assert.equal(currentReferrer, "http://site2.com/");
+ var uri = ios.newURI("http://site3.com/");
+
+ // Need to set NECKO_ERRORS_ARE_FATAL=0 else we'll abort process
+ var env = Cc["@mozilla.org/process/environment;1"].getService(
+ Ci.nsIEnvironment
+ );
+ env.set("NECKO_ERRORS_ARE_FATAL", "0");
+ // we expect setting referrer to fail
+ try {
+ request.referrerInfo = new ReferrerInfo(
+ Ci.nsIReferrerInfo.EMPTY,
+ true,
+ uri
+ );
+ do_throw("Error should have been thrown before getting here");
+ } catch (ex) {}
+ } catch (ex) {
+ do_throw("Exception: " + ex);
+ }
+ },
+
+ onDataAvailable: function test_ODA() {
+ do_throw("Should not get any data!");
+ },
+
+ onStopRequest: function test_onStopR(request, status) {
+ this.resolved();
+ },
+};
+
+var cancelDuringOnDataListener = {
+ data: "",
+ channel: null,
+ receivedSomeData: null,
+ onStartRequest: function test_onStartR(request, ctx) {
+ Assert.equal(request.status, Cr.NS_OK);
+ },
+
+ onDataAvailable: function test_ODA(request, stream, offset, count) {
+ let string = NetUtil.readInputStreamToString(stream, count);
+ Assert.ok(!string.includes("b"));
+ this.data += string;
+ this.channel.cancel(Cr.NS_BINDING_ABORTED);
+ if (this.receivedSomeData) {
+ this.receivedSomeData();
+ }
+ },
+
+ onStopRequest: function test_onStopR(request, ctx, status) {
+ Assert.ok(this.data.includes("a"), `data: ${this.data}`);
+ Assert.equal(request.status, Cr.NS_BINDING_ABORTED);
+ this.resolved();
+ },
+};
+
+function makeChan(url) {
+ var chan = NetUtil.newChannel({
+ uri: url,
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+
+ // ENSURE_CALLED_BEFORE_CONNECT: set original value
+ var uri = ios.newURI("http://site1.com");
+ chan.referrerInfo = new ReferrerInfo(Ci.nsIReferrerInfo.EMPTY, true, uri);
+ return chan;
+}
+
+var httpserv = null;
+
+add_task(async function setup() {
+ httpserv = new HttpServer();
+ httpserv.registerPathHandler("/failtest", failtest);
+ httpserv.registerPathHandler("/cancel_middle", cancel_middle);
+ httpserv.registerPathHandler("/normal_response", normal_response);
+ httpserv.start(-1);
+
+ registerCleanupFunction(async () => {
+ await new Promise(resolve => httpserv.stop(resolve));
+ });
+});
+
+add_task(async function test_cancel_during_onModifyRequest() {
+ var chan = makeChan(
+ "http://localhost:" + httpserv.identity.primaryPort + "/failtest"
+ );
+
+ if (!inChildProcess()) {
+ Services.obs.addObserver(observer, "http-on-modify-request");
+ } else {
+ do_send_remote_message("register-observer");
+ await do_await_remote_message("register-observer-done");
+ }
+
+ await new Promise(resolve => {
+ cancelDuringOnStartListener.resolved = resolve;
+ chan.asyncOpen(cancelDuringOnStartListener);
+ });
+
+ if (!inChildProcess()) {
+ Services.obs.removeObserver(observer, "http-on-modify-request");
+ } else {
+ do_send_remote_message("unregister-observer");
+ await do_await_remote_message("unregister-observer-done");
+ }
+});
+
+add_task(async function test_cancel_before_asyncOpen() {
+ var chan = makeChan(
+ "http://localhost:" + httpserv.identity.primaryPort + "/failtest"
+ );
+
+ chan.cancel(Cr.NS_BINDING_ABORTED);
+
+ Assert.throws(
+ () => {
+ chan.asyncOpen(cancelDuringOnStartListener);
+ },
+ /NS_BINDING_ABORTED/,
+ "cannot open if already cancelled"
+ );
+});
+
+add_task(async function test_cancel_during_onData() {
+ var chan = makeChan(
+ "http://localhost:" + httpserv.identity.primaryPort + "/cancel_middle"
+ );
+
+ await new Promise(resolve => {
+ cancelDuringOnDataListener.resolved = resolve;
+ cancelDuringOnDataListener.channel = chan;
+ chan.asyncOpen(cancelDuringOnDataListener);
+ });
+});
+
+var cancelAfterOnStopListener = {
+ data: "",
+ channel: null,
+ onStartRequest: function test_onStartR(request, ctx) {
+ Assert.equal(request.status, Cr.NS_OK);
+ },
+
+ onDataAvailable: function test_ODA(request, stream, offset, count) {
+ let string = NetUtil.readInputStreamToString(stream, count);
+ this.data += string;
+ },
+
+ onStopRequest: function test_onStopR(request, status) {
+ info("onStopRequest");
+ Assert.equal(request.status, Cr.NS_OK);
+ this.resolved();
+ },
+};
+
+add_task(async function test_cancel_after_onStop() {
+ var chan = makeChan(
+ "http://localhost:" + httpserv.identity.primaryPort + "/normal_response"
+ );
+
+ await new Promise(resolve => {
+ cancelAfterOnStopListener.resolved = resolve;
+ cancelAfterOnStopListener.channel = chan;
+ chan.asyncOpen(cancelAfterOnStopListener);
+ });
+ Assert.equal(chan.status, Cr.NS_OK);
+
+ // For now it's unclear if cancelling after onStop should throw,
+ // silently fail, or overwrite the channel's status as we currently do.
+ // See discussion in bug 1553083
+ chan.cancel(Cr.NS_BINDING_ABORTED);
+ Assert.equal(chan.status, Cr.NS_BINDING_ABORTED);
+});
+
+// PATHS
+
+// /failtest
+function failtest(metadata, response) {
+ do_throw("This should not be reached");
+}
+
+function cancel_middle(metadata, response) {
+ response.processAsync();
+ response.setStatusLine(metadata.httpVersion, 200, "OK");
+ let str1 = "a".repeat(128 * 1024);
+ response.write(str1, str1.length);
+ response.bodyOutputStream.flush();
+
+ let p = new Promise(resolve => {
+ cancelDuringOnDataListener.receivedSomeData = resolve;
+ });
+ p.then(() => {
+ let str1 = "b".repeat(128 * 1024);
+ response.write(str1, str1.length);
+ response.finish();
+ });
+}
+
+function normal_response(metadata, response) {
+ response.setStatusLine(metadata.httpVersion, 200, "OK");
+ let str1 = "Is this normal?";
+ response.write(str1, str1.length);
+}
diff --git a/netwerk/test/unit/test_httpssvc_https_upgrade.js b/netwerk/test/unit/test_httpssvc_https_upgrade.js
new file mode 100644
index 0000000000..789ee0e7a6
--- /dev/null
+++ b/netwerk/test/unit/test_httpssvc_https_upgrade.js
@@ -0,0 +1,298 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+ChromeUtils.import("resource://gre/modules/NetUtil.jsm");
+
+let prefs;
+let h2Port;
+let listen;
+
+const dns = Cc["@mozilla.org/network/dns-service;1"].getService(
+ Ci.nsIDNSService
+);
+const certOverrideService = Cc[
+ "@mozilla.org/security/certoverride;1"
+].getService(Ci.nsICertOverrideService);
+const threadManager = Cc["@mozilla.org/thread-manager;1"].getService(
+ Ci.nsIThreadManager
+);
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+const mainThread = threadManager.currentThread;
+
+const defaultOriginAttributes = {};
+
+function setup() {
+ let env = Cc["@mozilla.org/process/environment;1"].getService(
+ Ci.nsIEnvironment
+ );
+ h2Port = env.get("MOZHTTP2_PORT");
+ Assert.notEqual(h2Port, null);
+ Assert.notEqual(h2Port, "");
+
+ // Set to allow the cert presented by our H2 server
+ do_get_profile();
+ prefs = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch);
+
+ prefs.setBoolPref("network.http.spdy.enabled", true);
+ prefs.setBoolPref("network.http.spdy.enabled.http2", true);
+ // the TRR server is on 127.0.0.1
+ prefs.setCharPref("network.trr.bootstrapAddress", "127.0.0.1");
+
+ // make all native resolve calls "secretly" resolve localhost instead
+ prefs.setBoolPref("network.dns.native-is-localhost", true);
+
+ // 0 - off, 1 - race, 2 TRR first, 3 TRR only, 4 shadow
+ prefs.setIntPref("network.trr.mode", 2); // TRR first
+ prefs.setBoolPref("network.trr.wait-for-portal", false);
+ // don't confirm that TRR is working, just go!
+ prefs.setCharPref("network.trr.confirmationNS", "skip");
+
+ // So we can change the pref without clearing the cache to check a pushed
+ // record with a TRR path that fails.
+ Services.prefs.setBoolPref("network.trr.clear-cache-on-pref-change", false);
+
+ Services.prefs.setBoolPref("network.dns.upgrade_with_https_rr", true);
+ Services.prefs.setBoolPref("network.dns.use_https_rr_as_altsvc", true);
+
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ "https://foo.example.com:" + h2Port + "/httpssvc_as_altsvc"
+ );
+
+ // The moz-http2 cert is for foo.example.com and is signed by http2-ca.pem
+ // so add that cert to the trust list as a signing cert. // the foo.example.com domain name.
+ const certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(
+ Ci.nsIX509CertDB
+ );
+ addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u");
+}
+
+setup();
+registerCleanupFunction(() => {
+ prefs.clearUserPref("network.http.spdy.enabled");
+ prefs.clearUserPref("network.http.spdy.enabled.http2");
+ prefs.clearUserPref("network.dns.localDomains");
+ prefs.clearUserPref("network.dns.native-is-localhost");
+ prefs.clearUserPref("network.trr.mode");
+ prefs.clearUserPref("network.trr.uri");
+ prefs.clearUserPref("network.trr.credentials");
+ prefs.clearUserPref("network.trr.wait-for-portal");
+ prefs.clearUserPref("network.trr.allow-rfc1918");
+ prefs.clearUserPref("network.trr.useGET");
+ prefs.clearUserPref("network.trr.confirmationNS");
+ prefs.clearUserPref("network.trr.bootstrapAddress");
+ prefs.clearUserPref("network.trr.request-timeout");
+ prefs.clearUserPref("network.trr.clear-cache-on-pref-change");
+ prefs.clearUserPref("network.dns.upgrade_with_https_rr");
+ prefs.clearUserPref("network.dns.use_https_rr_as_altsvc");
+});
+
+class DNSListener {
+ constructor() {
+ this.promise = new Promise(resolve => {
+ this.resolve = resolve;
+ });
+ }
+ onLookupComplete(inRequest, inRecord, inStatus) {
+ this.resolve([inRequest, inRecord, inStatus]);
+ }
+ // So we can await this as a promise.
+ then() {
+ return this.promise.then.apply(this.promise, arguments);
+ }
+}
+
+DNSListener.prototype.QueryInterface = ChromeUtils.generateQI([
+ "nsIDNSListener",
+]);
+
+function makeChan(url) {
+ let chan = NetUtil.newChannel({
+ uri: url,
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+ return chan;
+}
+
+// When observer is specified, the channel will be suspended when receiving
+// "http-on-modify-request".
+function channelOpenPromise(chan, flags, observer) {
+ return new Promise(resolve => {
+ function finish(req, buffer) {
+ certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData(
+ false
+ );
+ resolve([req, buffer]);
+ }
+ certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData(
+ true
+ );
+
+ if (observer) {
+ let topic = "http-on-modify-request";
+ Services.obs.addObserver(observer, topic);
+ }
+ chan.asyncOpen(new ChannelListener(finish, null, flags));
+ });
+}
+
+class EventSinkListener {
+ getInterface(iid) {
+ if (iid.equals(Ci.nsIChannelEventSink)) {
+ return this;
+ }
+ }
+ asyncOnChannelRedirect(oldChan, newChan, flags, callback) {
+ Assert.equal(oldChan.URI.hostPort, newChan.URI.hostPort);
+ Assert.equal(oldChan.URI.scheme, "http");
+ Assert.equal(newChan.URI.scheme, "https");
+ callback.onRedirectVerifyCallback(Cr.NS_OK);
+ }
+}
+
+EventSinkListener.prototype.QueryInterface = ChromeUtils.generateQI([
+ "nsIInterfaceRequestor",
+ "nsIChannelEventSink",
+]);
+
+// Test if the request is upgraded to https with a HTTPSSVC record.
+add_task(async function testUseHTTPSSVCAsHSTS() {
+ dns.clearCache(true);
+
+ let dnsListener = new DNSListener();
+
+ // Do DNS resolution before creating the channel, so the HTTPSSVC record will
+ // be resolved from the cache.
+ let request = dns.asyncResolve(
+ "test.httpssvc.com",
+ dns.RESOLVE_TYPE_HTTPSSVC,
+ 0,
+ null, // resolverInfo
+ dnsListener,
+ mainThread,
+ defaultOriginAttributes
+ );
+
+ let [inRequest, , inStatus] = await dnsListener;
+ Assert.equal(inRequest, request, "correct request was used");
+ Assert.equal(inStatus, Cr.NS_OK, "status OK");
+
+ // Since the HTTPS RR should be served from cache, the DNS record is available
+ // before nsHttpChannel::MaybeUseHTTPSRRForUpgrade() is called.
+ let chan = makeChan(`http://test.httpssvc.com:80/server-timing`);
+ let listener = new EventSinkListener();
+ chan.notificationCallbacks = listener;
+
+ let [req] = await channelOpenPromise(chan);
+
+ req.QueryInterface(Ci.nsIHttpChannel);
+ Assert.equal(req.getResponseHeader("x-connection-http2"), "yes");
+
+ chan = makeChan(`http://test.httpssvc.com:80/server-timing`);
+ listener = new EventSinkListener();
+ chan.notificationCallbacks = listener;
+
+ [req] = await channelOpenPromise(chan);
+
+ req.QueryInterface(Ci.nsIHttpChannel);
+ Assert.equal(req.getResponseHeader("x-connection-http2"), "yes");
+});
+
+// Test the case that we got an invalid DNS response. In this case,
+// nsHttpChannel::OnHTTPSRRAvailable is called after
+// nsHttpChannel::MaybeUseHTTPSRRForUpgrade.
+add_task(async function testInvalidDNSResult() {
+ dns.clearCache(true);
+
+ let httpserv = new HttpServer();
+ let content = "ok";
+ httpserv.registerPathHandler("/", function handler(metadata, response) {
+ response.setHeader("Content-Length", `${content.length}`);
+ response.bodyOutputStream.write(content, content.length);
+ });
+ httpserv.start(-1);
+ httpserv.identity.setPrimary(
+ "http",
+ "foo.notexisted.com",
+ httpserv.identity.primaryPort
+ );
+
+ let chan = makeChan(
+ `http://foo.notexisted.com:${httpserv.identity.primaryPort}/`
+ );
+ let [, response] = await channelOpenPromise(chan);
+ Assert.equal(response, content);
+ await new Promise(resolve => httpserv.stop(resolve));
+});
+
+// The same test as above, but nsHttpChannel::MaybeUseHTTPSRRForUpgrade is
+// called after nsHttpChannel::OnHTTPSRRAvailable.
+add_task(async function testInvalidDNSResult1() {
+ dns.clearCache(true);
+
+ let httpserv = new HttpServer();
+ let content = "ok";
+ httpserv.registerPathHandler("/", function handler(metadata, response) {
+ response.setHeader("Content-Length", `${content.length}`);
+ response.bodyOutputStream.write(content, content.length);
+ });
+ httpserv.start(-1);
+ httpserv.identity.setPrimary(
+ "http",
+ "foo.notexisted.com",
+ httpserv.identity.primaryPort
+ );
+
+ let chan = makeChan(
+ `http://foo.notexisted.com:${httpserv.identity.primaryPort}/`
+ );
+
+ let topic = "http-on-modify-request";
+ let observer = {
+ QueryInterface: ChromeUtils.generateQI(["nsIObserver"]),
+ observe(aSubject, aTopic, aData) {
+ if (aTopic == topic) {
+ Services.obs.removeObserver(observer, topic);
+ let channel = aSubject.QueryInterface(Ci.nsIChannel);
+ channel.suspend();
+ let dnsListener = {
+ QueryInterface: ChromeUtils.generateQI(["nsIDNSListener"]),
+ onLookupComplete(inRequest, inRecord, inStatus) {
+ channel.resume();
+ },
+ };
+ dns.asyncResolve(
+ "foo.notexisted.com",
+ dns.RESOLVE_TYPE_HTTPSSVC,
+ 0,
+ null, // resolverInfo
+ dnsListener,
+ mainThread,
+ defaultOriginAttributes
+ );
+ }
+ },
+ };
+
+ let [, response] = await channelOpenPromise(chan, 0, observer);
+ Assert.equal(response, content);
+ await new Promise(resolve => httpserv.stop(resolve));
+});
+
+add_task(async function testLiteralIP() {
+ let httpserv = new HttpServer();
+ let content = "ok";
+ httpserv.registerPathHandler("/", function handler(metadata, response) {
+ response.setHeader("Content-Length", `${content.length}`);
+ response.bodyOutputStream.write(content, content.length);
+ });
+ httpserv.start(-1);
+
+ let chan = makeChan(`http://127.0.0.1:${httpserv.identity.primaryPort}/`);
+ let [, response] = await channelOpenPromise(chan);
+ Assert.equal(response, content);
+ await new Promise(resolve => httpserv.stop(resolve));
+});
diff --git a/netwerk/test/unit/test_httpssvc_iphint.js b/netwerk/test/unit/test_httpssvc_iphint.js
new file mode 100644
index 0000000000..f54fa9f457
--- /dev/null
+++ b/netwerk/test/unit/test_httpssvc_iphint.js
@@ -0,0 +1,301 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+ChromeUtils.import("resource://gre/modules/NetUtil.jsm");
+
+let prefs;
+let h2Port;
+let listen;
+
+const dns = Cc["@mozilla.org/network/dns-service;1"].getService(
+ Ci.nsIDNSService
+);
+const certOverrideService = Cc[
+ "@mozilla.org/security/certoverride;1"
+].getService(Ci.nsICertOverrideService);
+const threadManager = Cc["@mozilla.org/thread-manager;1"].getService(
+ Ci.nsIThreadManager
+);
+const mainThread = threadManager.currentThread;
+
+const defaultOriginAttributes = {};
+
+function setup() {
+ let env = Cc["@mozilla.org/process/environment;1"].getService(
+ Ci.nsIEnvironment
+ );
+ h2Port = env.get("MOZHTTP2_PORT");
+ Assert.notEqual(h2Port, null);
+ Assert.notEqual(h2Port, "");
+
+ // Set to allow the cert presented by our H2 server
+ do_get_profile();
+ prefs = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch);
+
+ prefs.setBoolPref("network.http.spdy.enabled", true);
+ prefs.setBoolPref("network.http.spdy.enabled.http2", true);
+ // the TRR server is on 127.0.0.1
+ prefs.setCharPref("network.trr.bootstrapAddress", "127.0.0.1");
+
+ // make all native resolve calls "secretly" resolve localhost instead
+ prefs.setBoolPref("network.dns.native-is-localhost", true);
+
+ // 0 - off, 1 - race, 2 TRR first, 3 TRR only, 4 shadow
+ prefs.setIntPref("network.trr.mode", 2); // TRR first
+ prefs.setBoolPref("network.trr.wait-for-portal", false);
+ // don't confirm that TRR is working, just go!
+ prefs.setCharPref("network.trr.confirmationNS", "skip");
+
+ // So we can change the pref without clearing the cache to check a pushed
+ // record with a TRR path that fails.
+ Services.prefs.setBoolPref("network.trr.clear-cache-on-pref-change", false);
+
+ Services.prefs.setBoolPref("network.dns.upgrade_with_https_rr", true);
+ Services.prefs.setBoolPref("network.dns.use_https_rr_as_altsvc", true);
+
+ // The moz-http2 cert is for foo.example.com and is signed by http2-ca.pem
+ // so add that cert to the trust list as a signing cert. // the foo.example.com domain name.
+ const certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(
+ Ci.nsIX509CertDB
+ );
+ addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u");
+}
+
+setup();
+registerCleanupFunction(() => {
+ prefs.clearUserPref("network.http.spdy.enabled");
+ prefs.clearUserPref("network.http.spdy.enabled.http2");
+ prefs.clearUserPref("network.dns.localDomains");
+ prefs.clearUserPref("network.dns.native-is-localhost");
+ prefs.clearUserPref("network.trr.mode");
+ prefs.clearUserPref("network.trr.uri");
+ prefs.clearUserPref("network.trr.credentials");
+ prefs.clearUserPref("network.trr.wait-for-portal");
+ prefs.clearUserPref("network.trr.allow-rfc1918");
+ prefs.clearUserPref("network.trr.useGET");
+ prefs.clearUserPref("network.trr.confirmationNS");
+ prefs.clearUserPref("network.trr.bootstrapAddress");
+ prefs.clearUserPref("network.trr.request-timeout");
+ prefs.clearUserPref("network.trr.clear-cache-on-pref-change");
+ prefs.clearUserPref("network.dns.upgrade_with_https_rr");
+ prefs.clearUserPref("network.dns.use_https_rr_as_altsvc");
+});
+
+class DNSListener {
+ constructor() {
+ this.promise = new Promise(resolve => {
+ this.resolve = resolve;
+ });
+ }
+ onLookupComplete(inRequest, inRecord, inStatus) {
+ this.resolve([inRequest, inRecord, inStatus]);
+ }
+ // So we can await this as a promise.
+ then() {
+ return this.promise.then.apply(this.promise, arguments);
+ }
+}
+
+DNSListener.prototype.QueryInterface = ChromeUtils.generateQI([
+ "nsIDNSListener",
+]);
+
+// Test if IP hint addresses can be accessed as regular A/AAAA records.
+add_task(async function testStoreIPHint() {
+ let trrServer = new TRRServer();
+ registerCleanupFunction(async () => {
+ await trrServer.stop();
+ });
+ await trrServer.start();
+
+ Services.prefs.setIntPref("network.trr.mode", 3);
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${trrServer.port}/dns-query`
+ );
+
+ await trrServer.registerDoHAnswers("test.IPHint.com", "HTTPS", [
+ {
+ name: "test.IPHint.com",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 1,
+ name: "test.IPHint.com",
+ values: [
+ { key: "alpn", value: ["h2", "h3"] },
+ { key: "port", value: 8888 },
+ { key: "ipv4hint", value: ["1.2.3.4", "5.6.7.8"] },
+ { key: "ipv6hint", value: ["::1", "fe80::794f:6d2c:3d5e:7836"] },
+ ],
+ },
+ },
+ ]);
+
+ let listener = new DNSListener();
+
+ let request = dns.asyncResolve(
+ "test.IPHint.com",
+ dns.RESOLVE_TYPE_HTTPSSVC,
+ 0,
+ null, // resolverInfo
+ listener,
+ mainThread,
+ defaultOriginAttributes
+ );
+
+ let [inRequest, inRecord, inStatus] = await listener;
+ Assert.equal(inRequest, request, "correct request was used");
+ Assert.equal(inStatus, Cr.NS_OK, "status OK");
+
+ let answer = inRecord.QueryInterface(Ci.nsIDNSHTTPSSVCRecord).records;
+ Assert.equal(answer[0].priority, 1);
+ Assert.equal(answer[0].name, "test.IPHint.com");
+ Assert.equal(answer[0].values.length, 4);
+ Assert.deepEqual(
+ answer[0].values[0].QueryInterface(Ci.nsISVCParamAlpn).alpn,
+ ["h2", "h3"],
+ "got correct answer"
+ );
+ Assert.equal(
+ answer[0].values[1].QueryInterface(Ci.nsISVCParamPort).port,
+ 8888,
+ "got correct answer"
+ );
+ Assert.equal(
+ answer[0].values[2].QueryInterface(Ci.nsISVCParamIPv4Hint).ipv4Hint[0]
+ .address,
+ "1.2.3.4",
+ "got correct answer"
+ );
+ Assert.equal(
+ answer[0].values[2].QueryInterface(Ci.nsISVCParamIPv4Hint).ipv4Hint[1]
+ .address,
+ "5.6.7.8",
+ "got correct answer"
+ );
+ Assert.equal(
+ answer[0].values[3].QueryInterface(Ci.nsISVCParamIPv6Hint).ipv6Hint[0]
+ .address,
+ "::1",
+ "got correct answer"
+ );
+ Assert.equal(
+ answer[0].values[3].QueryInterface(Ci.nsISVCParamIPv6Hint).ipv6Hint[1]
+ .address,
+ "fe80::794f:6d2c:3d5e:7836",
+ "got correct answer"
+ );
+
+ async function verifyAnswer(flags, answer) {
+ let listener = new DNSListener();
+ let request = dns.asyncResolve(
+ "test.IPHint.com",
+ dns.RESOLVE_TYPE_DEFAULT,
+ flags,
+ null, // resolverInfo
+ listener,
+ mainThread,
+ defaultOriginAttributes
+ );
+
+ let [inRequest, inRecord, inStatus] = await listener;
+ Assert.equal(inRequest, request, "correct request was used");
+ Assert.equal(inStatus, Cr.NS_OK, "status OK");
+ inRecord.QueryInterface(Ci.nsIDNSAddrRecord);
+ let addresses = [];
+ while (inRecord.hasMore()) {
+ addresses.push(inRecord.getNextAddrAsString());
+ }
+ Assert.deepEqual(addresses, answer);
+ }
+
+ await verifyAnswer(Ci.nsIDNSService.RESOLVE_IP_HINT, [
+ "1.2.3.4",
+ "5.6.7.8",
+ "::1",
+ "fe80::794f:6d2c:3d5e:7836",
+ ]);
+
+ await verifyAnswer(
+ Ci.nsIDNSService.RESOLVE_IP_HINT | Ci.nsIDNSService.RESOLVE_DISABLE_IPV4,
+ ["::1", "fe80::794f:6d2c:3d5e:7836"]
+ );
+
+ await verifyAnswer(
+ Ci.nsIDNSService.RESOLVE_IP_HINT | Ci.nsIDNSService.RESOLVE_DISABLE_IPV6,
+ ["1.2.3.4", "5.6.7.8"]
+ );
+});
+
+function makeChan(url) {
+ let chan = NetUtil.newChannel({
+ uri: url,
+ loadUsingSystemPrincipal: true,
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT,
+ }).QueryInterface(Ci.nsIHttpChannel);
+ return chan;
+}
+
+function channelOpenPromise(chan) {
+ return new Promise(resolve => {
+ function finish(req, buffer) {
+ resolve([req, buffer]);
+ }
+ let internal = chan.QueryInterface(Ci.nsIHttpChannelInternal);
+ internal.setWaitForHTTPSSVCRecord();
+ chan.asyncOpen(new ChannelListener(finish, null, CL_ALLOW_UNKNOWN_CL));
+ });
+}
+
+// Test if we can connect to the server with the IP hint address.
+add_task(async function testConnectionWithIPHint() {
+ dns.clearCache(true);
+ prefs.setIntPref("network.trr.mode", 3);
+ prefs.setCharPref(
+ "network.trr.uri",
+ "https://127.0.0.1:" + h2Port + "/httpssvc_use_iphint"
+ );
+
+ // Resolving test.iphint.com should be failed.
+ let listener = new DNSListener();
+ let request = dns.asyncResolve(
+ "test.iphint.com",
+ dns.RESOLVE_TYPE_DEFAULT,
+ 0,
+ null, // resolverInfo
+ listener,
+ mainThread,
+ defaultOriginAttributes
+ );
+
+ let [inRequest, , inStatus] = await listener;
+ Assert.equal(inRequest, request, "correct request was used");
+ Assert.equal(
+ inStatus,
+ Cr.NS_ERROR_UNKNOWN_HOST,
+ "status is NS_ERROR_UNKNOWN_HOST"
+ );
+
+ certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData(
+ true
+ );
+
+ // The connection should be succeeded since the IP hint is 127.0.0.1.
+ let chan = makeChan(`http://test.iphint.com:8080/`);
+ // Note that the partitionKey stored in DNS cache would be
+ // "%28https%2Ciphint.com%29". The http request to test.iphint.com will be
+ // upgraded to https and the ip hint address will be used by the https
+ // request in the end.
+ let [req] = await channelOpenPromise(chan);
+ req.QueryInterface(Ci.nsIHttpChannel);
+ Assert.equal(req.getResponseHeader("x-connection-http2"), "yes");
+
+ certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData(
+ false
+ );
+});
diff --git a/netwerk/test/unit/test_httpssvc_priority.js b/netwerk/test/unit/test_httpssvc_priority.js
new file mode 100644
index 0000000000..140adb9ab8
--- /dev/null
+++ b/netwerk/test/unit/test_httpssvc_priority.js
@@ -0,0 +1,235 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+ChromeUtils.import("resource://gre/modules/NetUtil.jsm");
+
+let prefs;
+let h2Port;
+
+const dns = Cc["@mozilla.org/network/dns-service;1"].getService(
+ Ci.nsIDNSService
+);
+const certOverrideService = Cc[
+ "@mozilla.org/security/certoverride;1"
+].getService(Ci.nsICertOverrideService);
+const threadManager = Cc["@mozilla.org/thread-manager;1"].getService(
+ Ci.nsIThreadManager
+);
+const mainThread = threadManager.currentThread;
+
+const defaultOriginAttributes = {};
+
+function setup() {
+ let env = Cc["@mozilla.org/process/environment;1"].getService(
+ Ci.nsIEnvironment
+ );
+ h2Port = env.get("MOZHTTP2_PORT");
+ Assert.notEqual(h2Port, null);
+ Assert.notEqual(h2Port, "");
+
+ // Set to allow the cert presented by our H2 server
+ do_get_profile();
+ prefs = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch);
+
+ prefs.setBoolPref("network.http.spdy.enabled", true);
+ prefs.setBoolPref("network.http.spdy.enabled.http2", true);
+ // the TRR server is on 127.0.0.1
+ prefs.setCharPref("network.trr.bootstrapAddress", "127.0.0.1");
+
+ // make all native resolve calls "secretly" resolve localhost instead
+ prefs.setBoolPref("network.dns.native-is-localhost", true);
+
+ // 0 - off, 1 - race, 2 TRR first, 3 TRR only, 4 shadow
+ prefs.setIntPref("network.trr.mode", 2); // TRR first
+ prefs.setBoolPref("network.trr.wait-for-portal", false);
+ // don't confirm that TRR is working, just go!
+ prefs.setCharPref("network.trr.confirmationNS", "skip");
+
+ // So we can change the pref without clearing the cache to check a pushed
+ // record with a TRR path that fails.
+ Services.prefs.setBoolPref("network.trr.clear-cache-on-pref-change", false);
+
+ // The moz-http2 cert is for foo.example.com and is signed by http2-ca.pem
+ // so add that cert to the trust list as a signing cert. // the foo.example.com domain name.
+ const certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(
+ Ci.nsIX509CertDB
+ );
+ addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u");
+}
+
+setup();
+registerCleanupFunction(() => {
+ prefs.clearUserPref("network.http.spdy.enabled");
+ prefs.clearUserPref("network.http.spdy.enabled.http2");
+ prefs.clearUserPref("network.dns.localDomains");
+ prefs.clearUserPref("network.dns.native-is-localhost");
+ prefs.clearUserPref("network.trr.mode");
+ prefs.clearUserPref("network.trr.uri");
+ prefs.clearUserPref("network.trr.credentials");
+ prefs.clearUserPref("network.trr.wait-for-portal");
+ prefs.clearUserPref("network.trr.allow-rfc1918");
+ prefs.clearUserPref("network.trr.useGET");
+ prefs.clearUserPref("network.trr.confirmationNS");
+ prefs.clearUserPref("network.trr.bootstrapAddress");
+ prefs.clearUserPref("network.trr.request-timeout");
+ prefs.clearUserPref("network.trr.clear-cache-on-pref-change");
+ prefs.clearUserPref("network.dns.echconfig.enabled");
+});
+
+class DNSListener {
+ constructor() {
+ this.promise = new Promise(resolve => {
+ this.resolve = resolve;
+ });
+ }
+ onLookupComplete(inRequest, inRecord, inStatus) {
+ this.resolve([inRequest, inRecord, inStatus]);
+ }
+ // So we can await this as a promise.
+ then() {
+ return this.promise.then.apply(this.promise, arguments);
+ }
+}
+
+DNSListener.prototype.QueryInterface = ChromeUtils.generateQI([
+ "nsIDNSListener",
+]);
+
+add_task(async function testPriorityAndECHConfig() {
+ let trrServer = new TRRServer();
+ registerCleanupFunction(async () => {
+ await trrServer.stop();
+ });
+ await trrServer.start();
+
+ Services.prefs.setBoolPref("network.dns.echconfig.enabled", false);
+ Services.prefs.setIntPref("network.trr.mode", 3);
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${trrServer.port}/dns-query`
+ );
+
+ await trrServer.registerDoHAnswers("test.priority.com", "HTTPS", [
+ {
+ name: "test.priority.com",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 1,
+ name: "test.p1.com",
+ values: [{ key: "alpn", value: ["h2", "h3"] }],
+ },
+ },
+ {
+ name: "test.priority.com",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 4,
+ name: "test.p4.com",
+ values: [{ key: "echconfig", value: "456..." }],
+ },
+ },
+ {
+ name: "test.priority.com",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 3,
+ name: "test.p3.com",
+ values: [{ key: "ipv4hint", value: "1.2.3.4" }],
+ },
+ },
+ {
+ name: "test.priority.com",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 2,
+ name: "test.p2.com",
+ values: [{ key: "echconfig", value: "123..." }],
+ },
+ },
+ ]);
+
+ let listener = new DNSListener();
+
+ let request = dns.asyncResolve(
+ "test.priority.com",
+ dns.RESOLVE_TYPE_HTTPSSVC,
+ 0,
+ null, // resolverInfo
+ listener,
+ mainThread,
+ defaultOriginAttributes
+ );
+
+ let [inRequest, inRecord, inStatus] = await listener;
+ Assert.equal(inRequest, request, "correct request was used");
+ Assert.equal(inStatus, Cr.NS_OK, "status OK");
+
+ let answer = inRecord.QueryInterface(Ci.nsIDNSHTTPSSVCRecord).records;
+ Assert.equal(answer.length, 4);
+
+ Assert.equal(answer[0].priority, 1);
+ Assert.equal(answer[0].name, "test.p1.com");
+
+ Assert.equal(answer[1].priority, 2);
+ Assert.equal(answer[1].name, "test.p2.com");
+
+ Assert.equal(answer[2].priority, 3);
+ Assert.equal(answer[2].name, "test.p3.com");
+
+ Assert.equal(answer[3].priority, 4);
+ Assert.equal(answer[3].name, "test.p4.com");
+
+ Services.prefs.setBoolPref("network.dns.echconfig.enabled", true);
+ dns.clearCache(true);
+ listener = new DNSListener();
+
+ request = dns.asyncResolve(
+ "test.priority.com",
+ dns.RESOLVE_TYPE_HTTPSSVC,
+ 0,
+ null, // resolverInfo
+ listener,
+ mainThread,
+ defaultOriginAttributes
+ );
+
+ [inRequest, inRecord, inStatus] = await listener;
+ Assert.equal(inRequest, request, "correct request was used");
+ Assert.equal(inStatus, Cr.NS_OK, "status OK");
+
+ answer = inRecord.QueryInterface(Ci.nsIDNSHTTPSSVCRecord).records;
+ Assert.equal(answer.length, 4);
+
+ Assert.equal(answer[0].priority, 2);
+ Assert.equal(answer[0].name, "test.p2.com");
+ Assert.equal(
+ answer[0].values[0].QueryInterface(Ci.nsISVCParamEchConfig).echconfig,
+ "123...",
+ "got correct answer"
+ );
+
+ Assert.equal(answer[1].priority, 4);
+ Assert.equal(answer[1].name, "test.p4.com");
+ Assert.equal(
+ answer[1].values[0].QueryInterface(Ci.nsISVCParamEchConfig).echconfig,
+ "456...",
+ "got correct answer"
+ );
+
+ Assert.equal(answer[2].priority, 1);
+ Assert.equal(answer[2].name, "test.p1.com");
+
+ Assert.equal(answer[3].priority, 3);
+ Assert.equal(answer[3].name, "test.p3.com");
+});
diff --git a/netwerk/test/unit/test_httpssvc_retry_with_ech.js b/netwerk/test/unit/test_httpssvc_retry_with_ech.js
new file mode 100644
index 0000000000..b0fbc26867
--- /dev/null
+++ b/netwerk/test/unit/test_httpssvc_retry_with_ech.js
@@ -0,0 +1,288 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+ChromeUtils.import("resource://gre/modules/NetUtil.jsm");
+
+let prefs;
+let h2Port;
+let listen;
+let trrServer;
+
+const dns = Cc["@mozilla.org/network/dns-service;1"].getService(
+ Ci.nsIDNSService
+);
+const certOverrideService = Cc[
+ "@mozilla.org/security/certoverride;1"
+].getService(Ci.nsICertOverrideService);
+const threadManager = Cc["@mozilla.org/thread-manager;1"].getService(
+ Ci.nsIThreadManager
+);
+const mainThread = threadManager.currentThread;
+
+const defaultOriginAttributes = {};
+
+function setup() {
+ let env = Cc["@mozilla.org/process/environment;1"].getService(
+ Ci.nsIEnvironment
+ );
+ h2Port = env.get("MOZHTTP2_PORT");
+ Assert.notEqual(h2Port, null);
+ Assert.notEqual(h2Port, "");
+
+ // Set to allow the cert presented by our H2 server
+ do_get_profile();
+ prefs = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch);
+
+ prefs.setBoolPref("network.security.esni.enabled", false);
+ prefs.setBoolPref("network.http.spdy.enabled", true);
+ prefs.setBoolPref("network.http.spdy.enabled.http2", true);
+ // the TRR server is on 127.0.0.1
+ prefs.setCharPref("network.trr.bootstrapAddress", "127.0.0.1");
+
+ // make all native resolve calls "secretly" resolve localhost instead
+ prefs.setBoolPref("network.dns.native-is-localhost", true);
+
+ // 0 - off, 1 - race, 2 TRR first, 3 TRR only, 4 shadow
+ prefs.setIntPref("network.trr.mode", 2); // TRR first
+ prefs.setBoolPref("network.trr.wait-for-portal", false);
+ // don't confirm that TRR is working, just go!
+ prefs.setCharPref("network.trr.confirmationNS", "skip");
+
+ // So we can change the pref without clearing the cache to check a pushed
+ // record with a TRR path that fails.
+ Services.prefs.setBoolPref("network.trr.clear-cache-on-pref-change", false);
+
+ Services.prefs.setBoolPref("network.dns.upgrade_with_https_rr", true);
+ Services.prefs.setBoolPref("network.dns.use_https_rr_as_altsvc", true);
+ Services.prefs.setBoolPref("network.dns.echconfig.enabled", true);
+
+ // The moz-http2 cert is for foo.example.com and is signed by http2-ca.pem
+ // so add that cert to the trust list as a signing cert. // the foo.example.com domain name.
+ const certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(
+ Ci.nsIX509CertDB
+ );
+ addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u");
+
+ add_tls_server_setup(
+ "EncryptedClientHelloServer",
+ "../../../security/manager/ssl/tests/unit/test_encrypted_client_hello"
+ );
+}
+
+setup();
+registerCleanupFunction(async () => {
+ prefs.clearUserPref("network.security.esni.enabled");
+ prefs.clearUserPref("network.http.spdy.enabled");
+ prefs.clearUserPref("network.http.spdy.enabled.http2");
+ prefs.clearUserPref("network.dns.localDomains");
+ prefs.clearUserPref("network.dns.native-is-localhost");
+ prefs.clearUserPref("network.trr.mode");
+ prefs.clearUserPref("network.trr.uri");
+ prefs.clearUserPref("network.trr.credentials");
+ prefs.clearUserPref("network.trr.wait-for-portal");
+ prefs.clearUserPref("network.trr.allow-rfc1918");
+ prefs.clearUserPref("network.trr.useGET");
+ prefs.clearUserPref("network.trr.confirmationNS");
+ prefs.clearUserPref("network.trr.bootstrapAddress");
+ prefs.clearUserPref("network.trr.request-timeout");
+ prefs.clearUserPref("network.trr.clear-cache-on-pref-change");
+ prefs.clearUserPref("network.dns.upgrade_with_https_rr");
+ prefs.clearUserPref("network.dns.use_https_rr_as_altsvc");
+ prefs.clearUserPref("network.dns.echconfig.enabled");
+ prefs.clearUserPref("network.dns.echconfig.fallback_to_origin");
+ if (trrServer) {
+ await trrServer.stop();
+ }
+});
+
+class DNSListener {
+ constructor() {
+ this.promise = new Promise(resolve => {
+ this.resolve = resolve;
+ });
+ }
+ onLookupComplete(inRequest, inRecord, inStatus) {
+ this.resolve([inRequest, inRecord, inStatus]);
+ }
+ // So we can await this as a promise.
+ then() {
+ return this.promise.then.apply(this.promise, arguments);
+ }
+}
+
+DNSListener.prototype.QueryInterface = ChromeUtils.generateQI([
+ "nsIDNSListener",
+]);
+
+function makeChan(url) {
+ let chan = NetUtil.newChannel({
+ uri: url,
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+ return chan;
+}
+
+function channelOpenPromise(chan, flags) {
+ return new Promise(resolve => {
+ function finish(req, buffer) {
+ certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData(
+ false
+ );
+ resolve([req, buffer]);
+ }
+ certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData(
+ true
+ );
+ let internal = chan.QueryInterface(Ci.nsIHttpChannelInternal);
+ internal.setWaitForHTTPSSVCRecord();
+ chan.asyncOpen(new ChannelListener(finish, null, flags));
+ });
+}
+
+add_task(async function testConnectWithECH() {
+ const ECH_CONFIG_FIXED =
+ "AEr+CABGABZlY2gtcHVibGljLmV4YW1wbGUuY29tACCKB1Y5SfrGIyk27W82xPpzWTDs3q72c04xSurDWlb9CgAgAAQAAQADADIAAA==";
+ trrServer = new TRRServer();
+ await trrServer.start();
+
+ Services.prefs.setIntPref("network.trr.mode", 3);
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${trrServer.port}/dns-query`
+ );
+
+ // Only the last record is valid to use.
+ await trrServer.registerDoHAnswers("ech-private.example.com", "HTTPS", [
+ {
+ name: "ech-private.example.com",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 1,
+ name: "ech-private.example.com",
+ values: [
+ { key: "alpn", value: "http/1.1" },
+ { key: "port", value: 8443 },
+ {
+ key: "echconfig",
+ value: ECH_CONFIG_FIXED,
+ needBase64Decode: true,
+ },
+ ],
+ },
+ },
+ ]);
+
+ await trrServer.registerDoHAnswers("ech-private.example.com", "A", [
+ {
+ name: "ech-private.example.com",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "127.0.0.1",
+ },
+ ]);
+
+ let listener = new DNSListener();
+
+ let request = dns.asyncResolve(
+ "ech-private.example.com",
+ dns.RESOLVE_TYPE_HTTPSSVC,
+ 0,
+ null, // resolverInfo
+ listener,
+ mainThread,
+ defaultOriginAttributes
+ );
+
+ let [inRequest, , inStatus] = await listener;
+ Assert.equal(inRequest, request, "correct request was used");
+ Assert.equal(inStatus, Cr.NS_OK, "status OK");
+
+ let chan = makeChan(`https://ech-private.example.com`);
+ await channelOpenPromise(chan, CL_ALLOW_UNKNOWN_CL);
+ let securityInfo = chan.securityInfo.QueryInterface(
+ Ci.nsITransportSecurityInfo
+ );
+ Assert.ok(securityInfo.isAcceptedEch, "This host should have accepted ECH");
+
+ await trrServer.stop();
+});
+
+add_task(async function testEchRetry() {
+ dns.clearCache(true);
+
+ const ECH_CONFIG_TRUSTED_RETRY =
+ "AEr+CABGABZlY2gtcHVibGljLmV4YW1wbGUuY29tACCKB1Y5SfrGIyk27W82xPpzWTDs3q72c04xSurDWlb9CgAgAAQAAQABADIAAA==";
+ trrServer = new TRRServer();
+ await trrServer.start();
+
+ Services.prefs.setIntPref("network.trr.mode", 3);
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${trrServer.port}/dns-query`
+ );
+
+ // Only the last record is valid to use.
+ await trrServer.registerDoHAnswers("ech-private.example.com", "HTTPS", [
+ {
+ name: "ech-private.example.com",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 1,
+ name: "ech-private.example.com",
+ values: [
+ { key: "alpn", value: "http/1.1" },
+ { key: "port", value: 8443 },
+ {
+ key: "echconfig",
+ value: ECH_CONFIG_TRUSTED_RETRY,
+ needBase64Decode: true,
+ },
+ ],
+ },
+ },
+ ]);
+
+ await trrServer.registerDoHAnswers("ech-private.example.com", "A", [
+ {
+ name: "ech-private.example.com",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "127.0.0.1",
+ },
+ ]);
+
+ let listener = new DNSListener();
+
+ let request = dns.asyncResolve(
+ "ech-private.example.com",
+ dns.RESOLVE_TYPE_HTTPSSVC,
+ 0,
+ null, // resolverInfo
+ listener,
+ mainThread,
+ defaultOriginAttributes
+ );
+
+ let [inRequest, , inStatus] = await listener;
+ Assert.equal(inRequest, request, "correct request was used");
+ Assert.equal(inStatus, Cr.NS_OK, "status OK");
+
+ Services.prefs.setBoolPref("network.dns.echconfig.enabled", true);
+ let chan = makeChan(`https://ech-private.example.com`);
+ await channelOpenPromise(chan, CL_ALLOW_UNKNOWN_CL);
+ let securityInfo = chan.securityInfo.QueryInterface(
+ Ci.nsITransportSecurityInfo
+ );
+ Assert.ok(securityInfo.isAcceptedEch, "This host should have accepted ECH");
+
+ await trrServer.stop();
+});
diff --git a/netwerk/test/unit/test_httpssvc_retry_without_ech.js b/netwerk/test/unit/test_httpssvc_retry_without_ech.js
new file mode 100644
index 0000000000..b149fcbdcf
--- /dev/null
+++ b/netwerk/test/unit/test_httpssvc_retry_without_ech.js
@@ -0,0 +1,223 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+ChromeUtils.import("resource://gre/modules/NetUtil.jsm");
+
+let prefs;
+let h2Port;
+let listen;
+let trrServer;
+
+const dns = Cc["@mozilla.org/network/dns-service;1"].getService(
+ Ci.nsIDNSService
+);
+const certOverrideService = Cc[
+ "@mozilla.org/security/certoverride;1"
+].getService(Ci.nsICertOverrideService);
+const threadManager = Cc["@mozilla.org/thread-manager;1"].getService(
+ Ci.nsIThreadManager
+);
+const mainThread = threadManager.currentThread;
+
+const defaultOriginAttributes = {};
+
+function setup() {
+ let env = Cc["@mozilla.org/process/environment;1"].getService(
+ Ci.nsIEnvironment
+ );
+ h2Port = env.get("MOZHTTP2_PORT");
+ Assert.notEqual(h2Port, null);
+ Assert.notEqual(h2Port, "");
+
+ // Set to allow the cert presented by our H2 server
+ do_get_profile();
+ prefs = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch);
+
+ prefs.setBoolPref("network.security.esni.enabled", false);
+ prefs.setBoolPref("network.http.spdy.enabled", true);
+ prefs.setBoolPref("network.http.spdy.enabled.http2", true);
+ // the TRR server is on 127.0.0.1
+ prefs.setCharPref("network.trr.bootstrapAddress", "127.0.0.1");
+
+ // make all native resolve calls "secretly" resolve localhost instead
+ prefs.setBoolPref("network.dns.native-is-localhost", true);
+
+ // 0 - off, 1 - race, 2 TRR first, 3 TRR only, 4 shadow
+ prefs.setIntPref("network.trr.mode", 2); // TRR first
+ prefs.setBoolPref("network.trr.wait-for-portal", false);
+ // don't confirm that TRR is working, just go!
+ prefs.setCharPref("network.trr.confirmationNS", "skip");
+
+ // So we can change the pref without clearing the cache to check a pushed
+ // record with a TRR path that fails.
+ Services.prefs.setBoolPref("network.trr.clear-cache-on-pref-change", false);
+
+ Services.prefs.setBoolPref("network.dns.upgrade_with_https_rr", true);
+ Services.prefs.setBoolPref("network.dns.use_https_rr_as_altsvc", true);
+ Services.prefs.setBoolPref("network.dns.echconfig.enabled", true);
+
+ // The moz-http2 cert is for foo.example.com and is signed by http2-ca.pem
+ // so add that cert to the trust list as a signing cert. // the foo.example.com domain name.
+ const certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(
+ Ci.nsIX509CertDB
+ );
+ addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u");
+
+ // An arbitrary, non-ECH server.
+ add_tls_server_setup(
+ "DelegatedCredentialsServer",
+ "../../../security/manager/ssl/tests/unit/test_delegated_credentials"
+ );
+
+ let nssComponent = Cc["@mozilla.org/psm;1"].getService(Ci.nsINSSComponent);
+ nssComponent.clearSSLExternalAndInternalSessionCache();
+}
+
+setup();
+registerCleanupFunction(async () => {
+ prefs.clearUserPref("network.security.esni.enabled");
+ prefs.clearUserPref("network.http.spdy.enabled");
+ prefs.clearUserPref("network.http.spdy.enabled.http2");
+ prefs.clearUserPref("network.dns.localDomains");
+ prefs.clearUserPref("network.dns.native-is-localhost");
+ prefs.clearUserPref("network.trr.mode");
+ prefs.clearUserPref("network.trr.uri");
+ prefs.clearUserPref("network.trr.credentials");
+ prefs.clearUserPref("network.trr.wait-for-portal");
+ prefs.clearUserPref("network.trr.allow-rfc1918");
+ prefs.clearUserPref("network.trr.useGET");
+ prefs.clearUserPref("network.trr.confirmationNS");
+ prefs.clearUserPref("network.trr.bootstrapAddress");
+ prefs.clearUserPref("network.trr.request-timeout");
+ prefs.clearUserPref("network.trr.clear-cache-on-pref-change");
+ prefs.clearUserPref("network.dns.upgrade_with_https_rr");
+ prefs.clearUserPref("network.dns.use_https_rr_as_altsvc");
+ prefs.clearUserPref("network.dns.echconfig.enabled");
+ prefs.clearUserPref("network.dns.echconfig.fallback_to_origin");
+ if (trrServer) {
+ await trrServer.stop();
+ }
+});
+
+class DNSListener {
+ constructor() {
+ this.promise = new Promise(resolve => {
+ this.resolve = resolve;
+ });
+ }
+ onLookupComplete(inRequest, inRecord, inStatus) {
+ this.resolve([inRequest, inRecord, inStatus]);
+ }
+ // So we can await this as a promise.
+ then() {
+ return this.promise.then.apply(this.promise, arguments);
+ }
+}
+
+DNSListener.prototype.QueryInterface = ChromeUtils.generateQI([
+ "nsIDNSListener",
+]);
+
+function makeChan(url) {
+ let chan = NetUtil.newChannel({
+ uri: url,
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+ return chan;
+}
+
+function channelOpenPromise(chan, flags) {
+ return new Promise(resolve => {
+ function finish(req, buffer) {
+ certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData(
+ false
+ );
+ resolve([req, buffer]);
+ }
+ certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData(
+ false
+ );
+ let internal = chan.QueryInterface(Ci.nsIHttpChannelInternal);
+ internal.setWaitForHTTPSSVCRecord();
+ chan.asyncOpen(new ChannelListener(finish, null, flags));
+ });
+}
+
+add_task(async function testRetryWithoutECH() {
+ const ECH_CONFIG_FIXED =
+ "AFH+CABNAB1kZWxlZ2F0ZWQtZW5hYmxlZC5leGFtcGxlLmNvbQAgigdWOUn6xiMpNu1vNsT6c1kw7N6u9nNOMUrqw1pW/QoAIAAEAAEAAwAyAAA=";
+ trrServer = new TRRServer();
+ await trrServer.start();
+
+ Services.prefs.setIntPref("network.trr.mode", 3);
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${trrServer.port}/dns-query`
+ );
+
+ // Only the last record is valid to use.
+ await trrServer.registerDoHAnswers(
+ "delegated-disabled.example.com",
+ "HTTPS",
+ [
+ {
+ name: "delegated-disabled.example.com",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 1,
+ name: "delegated-disabled.example.com",
+ values: [
+ {
+ key: "echconfig",
+ value: ECH_CONFIG_FIXED,
+ needBase64Decode: true,
+ },
+ ],
+ },
+ },
+ ]
+ );
+
+ await trrServer.registerDoHAnswers("delegated-disabled.example.com", "A", [
+ {
+ name: "delegated-disabled.example.com",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "127.0.0.1",
+ },
+ ]);
+
+ let listener = new DNSListener();
+
+ let request = dns.asyncResolve(
+ "delegated-disabled.example.com",
+ dns.RESOLVE_TYPE_HTTPSSVC,
+ 0,
+ null, // resolverInfo
+ listener,
+ mainThread,
+ defaultOriginAttributes
+ );
+
+ let [inRequest, , inStatus] = await listener;
+ Assert.equal(inRequest, request, "correct request was used");
+ Assert.equal(inStatus, Cr.NS_OK, "status OK");
+
+ let chan = makeChan(`https://delegated-disabled.example.com:8443`);
+ await channelOpenPromise(chan, CL_ALLOW_UNKNOWN_CL);
+ let securityInfo = chan.securityInfo.QueryInterface(
+ Ci.nsITransportSecurityInfo
+ );
+
+ Assert.ok(
+ !securityInfo.isAcceptedEch,
+ "This host should not have accepted ECH"
+ );
+ await trrServer.stop();
+});
diff --git a/netwerk/test/unit/test_httpsuspend.js b/netwerk/test/unit/test_httpsuspend.js
new file mode 100644
index 0000000000..8675b3e6bd
--- /dev/null
+++ b/netwerk/test/unit/test_httpsuspend.js
@@ -0,0 +1,84 @@
+// This file ensures that suspending a channel directly after opening it
+// suspends future notifications correctly.
+
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+XPCOMUtils.defineLazyGetter(this, "URL", function() {
+ return "http://localhost:" + httpserv.identity.primaryPort;
+});
+
+const MIN_TIME_DIFFERENCE = 3000;
+const RESUME_DELAY = 5000;
+
+var listener = {
+ _lastEvent: 0,
+ _gotData: false,
+
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIStreamListener",
+ "nsIRequestObserver",
+ ]),
+
+ onStartRequest(request) {
+ this._lastEvent = Date.now();
+ request.QueryInterface(Ci.nsIRequest);
+
+ // Insert a delay between this and the next callback to ensure message buffering
+ // works correctly
+ request.suspend();
+ request.suspend();
+ do_timeout(RESUME_DELAY, function() {
+ request.resume();
+ });
+ do_timeout(RESUME_DELAY + 1000, function() {
+ request.resume();
+ });
+ },
+
+ onDataAvailable(request, stream, offset, count) {
+ Assert.ok(Date.now() - this._lastEvent >= MIN_TIME_DIFFERENCE);
+ read_stream(stream, count);
+
+ // Ensure that suspending and resuming inside a callback works correctly
+ request.suspend();
+ request.suspend();
+ request.resume();
+ request.resume();
+
+ this._gotData = true;
+ },
+
+ onStopRequest(request, status) {
+ Assert.ok(this._gotData);
+ httpserv.stop(do_test_finished);
+ },
+};
+
+function makeChan(url) {
+ return NetUtil.newChannel({
+ uri: url,
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+}
+
+var httpserv = null;
+
+function run_test() {
+ httpserv = new HttpServer();
+ httpserv.registerPathHandler("/woo", data);
+ httpserv.start(-1);
+
+ var chan = makeChan(URL + "/woo");
+ chan.QueryInterface(Ci.nsIRequest);
+ chan.asyncOpen(listener);
+
+ do_test_pending();
+}
+
+function data(metadata, response) {
+ let httpbody = "0123456789";
+ response.setHeader("Content-Type", "text/plain", false);
+ response.bodyOutputStream.write(httpbody, httpbody.length);
+}
diff --git a/netwerk/test/unit/test_idn_blacklist.js b/netwerk/test/unit/test_idn_blacklist.js
new file mode 100644
index 0000000000..d0c77d87a1
--- /dev/null
+++ b/netwerk/test/unit/test_idn_blacklist.js
@@ -0,0 +1,173 @@
+// Test that URLs containing characters in the IDN blacklist are
+// always displayed as punycode
+
+"use strict";
+
+const testcases = [
+ // Original Punycode or
+ // normalized form
+ //
+ ["\u00BC", "xn--14-c6t"],
+ ["\u00BD", "xn--12-c6t"],
+ ["\u00BE", "xn--34-c6t"],
+ ["\u01C3", "xn--ija"],
+ ["\u02D0", "xn--6qa"],
+ ["\u0337", "xn--4ta"],
+ ["\u0338", "xn--5ta"],
+ ["\u0589", "xn--3bb"],
+ ["\u05C3", "xn--rdb"],
+ ["\u05F4", "xn--5eb"],
+ ["\u0609", "xn--rfb"],
+ ["\u060A", "xn--sfb"],
+ ["\u066A", "xn--jib"],
+ ["\u06D4", "xn--klb"],
+ ["\u0701", "xn--umb"],
+ ["\u0702", "xn--vmb"],
+ ["\u0703", "xn--wmb"],
+ ["\u0704", "xn--xmb"],
+ ["\u115F", "xn--osd"],
+ ["\u1160", "xn--psd"],
+ ["\u1735", "xn--d0e"],
+ ["\u2027", "xn--svg"],
+ ["\u2028", "xn--tvg"],
+ ["\u2029", "xn--uvg"],
+ ["\u2039", "xn--bwg"],
+ ["\u203A", "xn--cwg"],
+ ["\u2041", "xn--jwg"],
+ ["\u2044", "xn--mwg"],
+ ["\u2052", "xn--0wg"],
+ ["\u2153", "xn--13-c6t"],
+ ["\u2154", "xn--23-c6t"],
+ ["\u2155", "xn--15-c6t"],
+ ["\u2156", "xn--25-c6t"],
+ ["\u2157", "xn--35-c6t"],
+ ["\u2158", "xn--45-c6t"],
+ ["\u2159", "xn--16-c6t"],
+ ["\u215A", "xn--56-c6t"],
+ ["\u215B", "xn--18-c6t"],
+ ["\u215C", "xn--38-c6t"],
+ ["\u215D", "xn--58-c6t"],
+ ["\u215E", "xn--78-c6t"],
+ ["\u215F", "xn--1-zjn"],
+ ["\u2215", "xn--w9g"],
+ ["\u2236", "xn--ubh"],
+ ["\u23AE", "xn--lmh"],
+ ["\u2571", "xn--hzh"],
+ ["\u29F6", "xn--jxi"],
+ ["\u29F8", "xn--lxi"],
+ ["\u2AFB", "xn--z4i"],
+ ["\u2AFD", "xn--14i"],
+ ["\u2FF0", "xn--85j"],
+ ["\u2FF1", "xn--95j"],
+ ["\u2FF2", "xn--b6j"],
+ ["\u2FF3", "xn--c6j"],
+ ["\u2FF4", "xn--d6j"],
+ ["\u2FF5", "xn--e6j"],
+ ["\u2FF6", "xn--f6j"],
+ ["\u2FF7", "xn--g6j"],
+ ["\u2FF8", "xn--h6j"],
+ ["\u2FF9", "xn--i6j"],
+ ["\u2FFA", "xn--j6j"],
+ ["\u2FFB", "xn--k6j"],
+ ["\u3014", "xn--96j"],
+ ["\u3015", "xn--b7j"],
+ ["\u3033", "xn--57j"],
+ ["\u3164", "xn--psd"],
+ ["\u321D", "xn--()-357j35d"],
+ ["\u321E", "xn--()-357jf36c"],
+ ["\u33AE", "xn--rads-id9a"],
+ ["\u33AF", "xn--rads2-4d6b"],
+ ["\u33C6", "xn--ckg-tc2a"],
+ ["\u33DF", "xn--am-6bv"],
+ ["\uA789", "xn--058a"],
+ ["\uFE3F", "xn--x6j"],
+ ["\uFE5D", "xn--96j"],
+ ["\uFE5E", "xn--b7j"],
+ ["\uFFA0", "xn--psd"],
+ ["\uFFF9", "xn--vn7c"],
+ ["\uFFFA", "xn--wn7c"],
+ ["\uFFFB", "xn--xn7c"],
+ ["\uFFFC", "xn--yn7c"],
+ ["\uFFFD", "xn--zn7c"],
+
+ // Characters from the IDN blacklist that normalize to ASCII
+ // If we start using STD3ASCIIRules these will be blocked (bug 316444)
+ ["\u00A0", " "],
+ ["\u2000", " "],
+ ["\u2001", " "],
+ ["\u2002", " "],
+ ["\u2003", " "],
+ ["\u2004", " "],
+ ["\u2005", " "],
+ ["\u2006", " "],
+ ["\u2007", " "],
+ ["\u2008", " "],
+ ["\u2009", " "],
+ ["\u200A", " "],
+ ["\u2024", "."],
+ ["\u202F", " "],
+ ["\u205F", " "],
+ ["\u3000", " "],
+ ["\u3002", "."],
+ ["\uFE14", ";"],
+ ["\uFE15", "!"],
+ ["\uFF0E", "."],
+ ["\uFF0F", "/"],
+ ["\uFF61", "."],
+
+ // Characters from the IDN blacklist that are stripped by Nameprep
+ ["\u200B", ""],
+ ["\uFEFF", ""],
+];
+
+function run_test() {
+ var pbi = Cc["@mozilla.org/preferences-service;1"].getService(
+ Ci.nsIPrefBranch
+ );
+ var oldProfile = pbi.getCharPref(
+ "network.IDN.restriction_profile",
+ "moderate"
+ );
+ var oldWhitelistCom = pbi.getBoolPref("network.IDN.whitelist.com", false);
+ var idnService = Cc["@mozilla.org/network/idn-service;1"].getService(
+ Ci.nsIIDNService
+ );
+
+ pbi.setCharPref("network.IDN.restriction_profile", "moderate");
+ pbi.setBoolPref("network.IDN.whitelist.com", false);
+
+ for (var j = 0; j < testcases.length; ++j) {
+ var test = testcases[j];
+ var URL = test[0] + ".com";
+ var punycodeURL = test[1] + ".com";
+ var isASCII = {};
+
+ var result;
+ try {
+ result = idnService.convertToDisplayIDN(URL, isASCII);
+ } catch (e) {
+ result = ".com";
+ }
+ // If the punycode URL is equivalent to \ufffd.com (i.e. the
+ // blacklisted character has been replaced by a unicode
+ // REPLACEMENT CHARACTER, skip the test
+ if (result != "xn--zn7c.com") {
+ if (punycodeURL.substr(0, 4) == "xn--") {
+ // test convertToDisplayIDN with a Unicode URL and with a
+ // Punycode URL if we have one
+ equal(escape(result), escape(punycodeURL));
+
+ result = idnService.convertToDisplayIDN(punycodeURL, isASCII);
+ equal(escape(result), escape(punycodeURL));
+ } else {
+ // The "punycode" URL isn't punycode. This happens in testcases
+ // where the Unicode URL has become normalized to an ASCII URL,
+ // so, even though expectedUnicode is true, the expected result
+ // is equal to punycodeURL
+ equal(escape(result), escape(punycodeURL));
+ }
+ }
+ }
+ pbi.setBoolPref("network.IDN.whitelist.com", oldWhitelistCom);
+ pbi.setCharPref("network.IDN.restriction_profile", oldProfile);
+}
diff --git a/netwerk/test/unit/test_idn_urls.js b/netwerk/test/unit/test_idn_urls.js
new file mode 100644
index 0000000000..2e00ca6932
--- /dev/null
+++ b/netwerk/test/unit/test_idn_urls.js
@@ -0,0 +1,441 @@
+// Test algorithm for unicode display of IDNA URL (bug 722299)
+
+"use strict";
+
+const testcases = [
+ // Original Punycode or Expected UTF-8 by profile
+ // URL normalized form ASCII-Only, High, Moderate
+ //
+ // Latin script
+ ["cuillère", "xn--cuillre-6xa", false, true, true],
+
+ // repeated non-spacing marks
+ ["gruz̀̀ere", "xn--gruzere-ogea", false, false, false],
+
+ // non-XID character
+ ["I♥NY", "xn--iny-zx5a", false, false, false],
+
+ /*
+ Behaviour of this test changed in IDNA2008, replacing the non-XID
+ character with U+FFFD replacement character - when all platforms use
+ IDNA2008 it can be uncommented and the punycode URL changed to
+ "xn--mgbl3eb85703a"
+
+ // new non-XID character in Unicode 6.3
+ ["حلا\u061cل", "xn--bgbvr6gc", false, false, false],
+*/
+
+ // U+30FB KATAKANA MIDDLE DOT is excluded from non-XID characters (bug 857490)
+ ["乾燥肌・石けん", "xn--08j4gylj12hz80b0uhfup", false, true, true],
+
+ // Cyrillic alone
+ ["толсто́й", "xn--lsa83dealbred", false, true, true],
+
+ // Mixed script Cyrillic/Latin
+ [
+ "толсто́й-in-Russian",
+ "xn---in-russian-1jg071b0a8bb4cpd",
+ false,
+ false,
+ false,
+ ],
+
+ // Mixed script Latin/Cyrillic
+ ["war-and-миръ", "xn--war-and--b9g3b7b3h", false, false, false],
+
+ // Cherokee (Restricted script)
+ ["ᏣᎳᎩ", "xn--f9dt7l", false, false, false],
+
+ // Yi (former Aspirational script, now Restricted per Unicode 10.0 update to UAX 31)
+ ["ꆈꌠꁱꂷ", "xn--4o7a6e1x64c", false, false, false],
+
+ // Greek alone
+ ["πλάτων", "xn--hxa3ahjw4a", false, true, true],
+
+ // Mixed script Greek/Latin
+ [
+ "πλάτωνicrelationship",
+ "xn--icrelationship-96j4t9a3cwe2e",
+ false,
+ false,
+ false,
+ ],
+
+ // Mixed script Latin/Greek
+ ["spaceὈδύσσεια", "xn--space-h9dui0b0ga2j1562b", false, false, false],
+
+ // Devanagari alone
+ ["मराठी", "xn--d2b1ag0dl", false, true, true],
+
+ // Devanagari with Armenian
+ ["मराठीՀայաստան", "xn--y9aaa1d0ai1cq964f8dwa2o1a", false, false, false],
+
+ // Devanagari with common
+ ["मराठी123", "xn--123-mhh3em2hra", false, true, true],
+
+ // Common with Devanagari
+ ["123मराठी", "xn--123-phh3em2hra", false, true, true],
+
+ // Latin with Han
+ ["chairman毛", "xn--chairman-k65r", false, true, true],
+
+ // Han with Latin
+ ["山葵sauce", "xn--sauce-6j9ii40v", false, true, true],
+
+ // Latin with Han, Hiragana and Katakana
+ ["van語ではドイ", "xn--van-ub4bpb6w0in486d", false, true, true],
+
+ // Latin with Han, Katakana and Hiragana
+ ["van語ドイでは", "xn--van-ub4bpb4w0ip486d", false, true, true],
+
+ // Latin with Hiragana, Han and Katakana
+ ["vanでは語ドイ", "xn--van-ub4bpb6w0ip486d", false, true, true],
+
+ // Latin with Hiragana, Katakana and Han
+ ["vanではドイ語", "xn--van-ub4bpb6w0ir486d", false, true, true],
+
+ // Latin with Katakana, Han and Hiragana
+ ["vanドイ語では", "xn--van-ub4bpb4w0ir486d", false, true, true],
+
+ // Latin with Katakana, Hiragana and Han
+ ["vanドイでは語", "xn--van-ub4bpb4w0it486d", false, true, true],
+
+ // Han with Latin, Hiragana and Katakana
+ ["語vanではドイ", "xn--van-ub4bpb6w0ik486d", false, true, true],
+
+ // Han with Latin, Katakana and Hiragana
+ ["語vanドイでは", "xn--van-ub4bpb4w0im486d", false, true, true],
+
+ // Han with Hiragana, Latin and Katakana
+ ["語ではvanドイ", "xn--van-rb4bpb9w0ik486d", false, true, true],
+
+ // Han with Hiragana, Katakana and Latin
+ ["語ではドイvan", "xn--van-rb4bpb6w0in486d", false, true, true],
+
+ // Han with Katakana, Latin and Hiragana
+ ["語ドイvanでは", "xn--van-ub4bpb1w0ip486d", false, true, true],
+
+ // Han with Katakana, Hiragana and Latin
+ ["語ドイではvan", "xn--van-rb4bpb4w0ip486d", false, true, true],
+
+ // Hiragana with Latin, Han and Katakana
+ ["イツvan語ではド", "xn--van-ub4bpb1wvhsbx330n", false, true, true],
+
+ // Hiragana with Latin, Katakana and Han
+ ["ではvanドイ語", "xn--van-rb4bpb9w0ir486d", false, true, true],
+
+ // Hiragana with Han, Latin and Katakana
+ ["では語vanドイ", "xn--van-rb4bpb9w0im486d", false, true, true],
+
+ // Hiragana with Han, Katakana and Latin
+ ["では語ドイvan", "xn--van-rb4bpb6w0ip486d", false, true, true],
+
+ // Hiragana with Katakana, Latin and Han
+ ["ではドイvan語", "xn--van-rb4bpb6w0iu486d", false, true, true],
+
+ // Hiragana with Katakana, Han and Latin
+ ["ではドイ語van", "xn--van-rb4bpb6w0ir486d", false, true, true],
+
+ // Katakana with Latin, Han and Hiragana
+ ["ドイvan語では", "xn--van-ub4bpb1w0iu486d", false, true, true],
+
+ // Katakana with Latin, Hiragana and Han
+ ["ドイvanでは語", "xn--van-ub4bpb1w0iw486d", false, true, true],
+
+ // Katakana with Han, Latin and Hiragana
+ ["ドイ語vanでは", "xn--van-ub4bpb1w0ir486d", false, true, true],
+
+ // Katakana with Han, Hiragana and Latin
+ ["ドイ語ではvan", "xn--van-rb4bpb4w0ir486d", false, true, true],
+
+ // Katakana with Hiragana, Latin and Han
+ ["ドイではvan語", "xn--van-rb4bpb4w0iw486d", false, true, true],
+
+ // Katakana with Hiragana, Han and Latin
+ ["ドイでは語van", "xn--van-rb4bpb4w0it486d", false, true, true],
+
+ // Han with common
+ ["中国123", "xn--123-u68dy61b", false, true, true],
+
+ // common with Han
+ ["123中国", "xn--123-x68dy61b", false, true, true],
+
+ // Characters that normalize to permitted characters
+ // (also tests Plane 1 supplementary characters)
+ ["super𝟖", "super8", true, true, true],
+
+ // Han from Plane 2
+ ["𠀀𠀁𠀂", "xn--j50icd", false, true, true],
+
+ // Han from Plane 2 with js (UTF-16) escapes
+ ["\uD840\uDC00\uD840\uDC01\uD840\uDC02", "xn--j50icd", false, true, true],
+
+ // Same with a lone high surrogate at the end
+ ["\uD840\uDC00\uD840\uDC01\uD840", "xn--zn7c0336bda", false, false, false],
+
+ // Latin text and Bengali digits
+ ["super৪", "xn--super-k2l", false, false, true],
+
+ // Bengali digits and Latin text
+ ["৫ab", "xn--ab-x5f", false, false, true],
+
+ // Bengali text and Latin digits
+ ["অঙ্কুর8", "xn--8-70d2cp0j6dtd", false, true, true],
+
+ // Latin digits and Bengali text
+ ["5াব", "xn--5-h3d7c", false, true, true],
+
+ // Mixed numbering systems
+ ["٢٠۰٠", "xn--8hbae38c", false, false, false],
+
+ // Traditional Chinese
+ ["萬城", "xn--uis754h", false, true, true],
+
+ // Simplified Chinese
+ ["万城", "xn--chq31v", false, true, true],
+
+ // Simplified-only and Traditional-only Chinese in the same label
+ ["万萬城", "xn--chq31vsl1b", false, true, true],
+
+ // Traditional-only and Simplified-only Chinese in the same label
+ ["萬万城", "xn--chq31vrl1b", false, true, true],
+
+ // Han and Latin and Bopomofo
+ [
+ "注音符号bopomofoㄅㄆㄇㄈ",
+ "xn--bopomofo-hj5gkalm1637i876cuw0brk5f",
+ false,
+ true,
+ true,
+ ],
+
+ // Han, bopomofo, Latin
+ [
+ "注音符号ㄅㄆㄇㄈbopomofo",
+ "xn--bopomofo-8i5gkalm9637i876cuw0brk5f",
+ false,
+ true,
+ true,
+ ],
+
+ // Latin, Han, Bopomofo
+ [
+ "bopomofo注音符号ㄅㄆㄇㄈ",
+ "xn--bopomofo-hj5gkalm9637i876cuw0brk5f",
+ false,
+ true,
+ true,
+ ],
+
+ // Latin, Bopomofo, Han
+ [
+ "bopomofoㄅㄆㄇㄈ注音符号",
+ "xn--bopomofo-hj5gkalm3737i876cuw0brk5f",
+ false,
+ true,
+ true,
+ ],
+
+ // Bopomofo, Han, Latin
+ [
+ "ㄅㄆㄇㄈ注音符号bopomofo",
+ "xn--bopomofo-8i5gkalm3737i876cuw0brk5f",
+ false,
+ true,
+ true,
+ ],
+
+ // Bopomofo, Latin, Han
+ [
+ "ㄅㄆㄇㄈbopomofo注音符号",
+ "xn--bopomofo-8i5gkalm1837i876cuw0brk5f",
+ false,
+ true,
+ true,
+ ],
+
+ // Han, bopomofo and katakana
+ [
+ "注音符号ㄅㄆㄇㄈボポモフォ",
+ "xn--jckteuaez1shij0450gylvccz9asi4e",
+ false,
+ false,
+ false,
+ ],
+
+ // Han, katakana, bopomofo
+ [
+ "注音符号ボポモフォㄅㄆㄇㄈ",
+ "xn--jckteuaez6shij5350gylvccz9asi4e",
+ false,
+ false,
+ false,
+ ],
+
+ // bopomofo, han, katakana
+ [
+ "ㄅㄆㄇㄈ注音符号ボポモフォ",
+ "xn--jckteuaez1shij4450gylvccz9asi4e",
+ false,
+ false,
+ false,
+ ],
+
+ // bopomofo, katakana, han
+ [
+ "ㄅㄆㄇㄈボポモフォ注音符号",
+ "xn--jckteuaez1shij9450gylvccz9asi4e",
+ false,
+ false,
+ false,
+ ],
+
+ // katakana, Han, bopomofo
+ [
+ "ボポモフォ注音符号ㄅㄆㄇㄈ",
+ "xn--jckteuaez6shij0450gylvccz9asi4e",
+ false,
+ false,
+ false,
+ ],
+
+ // katakana, bopomofo, Han
+ [
+ "ボポモフォㄅㄆㄇㄈ注音符号",
+ "xn--jckteuaez6shij4450gylvccz9asi4e",
+ false,
+ false,
+ false,
+ ],
+
+ // Han, Hangul and Latin
+ ["韓한글hangul", "xn--hangul-2m5ti09k79ze", false, true, true],
+
+ // Han, Latin and Hangul
+ ["韓hangul한글", "xn--hangul-2m5to09k79ze", false, true, true],
+
+ // Hangul, Han and Latin
+ ["한글韓hangul", "xn--hangul-2m5th09k79ze", false, true, true],
+
+ // Hangul, Latin and Han
+ ["한글hangul韓", "xn--hangul-8m5t898k79ze", false, true, true],
+
+ // Latin, Han and Hangul
+ ["hangul韓한글", "xn--hangul-8m5ti09k79ze", false, true, true],
+
+ // Latin, Hangul and Han
+ ["hangul한글韓", "xn--hangul-8m5th09k79ze", false, true, true],
+
+ // Hangul and katakana
+ ["한글ハングル", "xn--qck1c2d4a9266lkmzb", false, false, false],
+
+ // Katakana and Hangul
+ ["ハングル한글", "xn--qck1c2d4a2366lkmzb", false, false, false],
+
+ // Thai (also tests that node with over 63 UTF-8 octets doesn't fail)
+ [
+ "เครื่องทําน้ําทําน้ําแข็ง",
+ "xn--22cdjb2fanb9fyepcbbb9dwh4a3igze4fdcd",
+ false,
+ true,
+ true,
+ ],
+
+ // Effect of adding valid or invalid subdomains (bug 1399540)
+ ["䕮䕵䕶䕱.ascii", "xn--google.ascii", false, true, true],
+ ["ascii.䕮䕵䕶䕱", "ascii.xn--google", false, true, true],
+ ["中国123.䕮䕵䕶䕱", "xn--123-u68dy61b.xn--google", false, true, true],
+ ["䕮䕵䕶䕱.中国123", "xn--google.xn--123-u68dy61b", false, true, true],
+ [
+ "xn--accountlogin.䕮䕵䕶䕱",
+ "xn--accountlogin.xn--google",
+ false,
+ true,
+ true,
+ ],
+ [
+ "䕮䕵䕶䕱.xn--accountlogin",
+ "xn--google.xn--accountlogin",
+ false,
+ true,
+ true,
+ ],
+
+ // Arabic diacritic not allowed in Latin text (bug 1370497)
+ ["goo\u0650gle", "xn--google-yri", false, false, false],
+ // ...but Arabic diacritics are allowed on Arabic text
+ ["العَرَبِي", "xn--mgbc0a5a6cxbzabt", false, true, true],
+
+ // Hebrew diacritic also not allowed in Latin text (bug 1404349)
+ ["goo\u05b4gle", "xn--google-rvh", false, false, false],
+
+ // Accents above dotless-i are not allowed
+ ["na\u0131\u0308ve", "xn--nave-mza04z", false, false, false],
+ ["d\u0131\u0302ner", "xn--dner-lza40z", false, false, false],
+ // but the corresponding accented-i (based on dotted i) is OK
+ ["na\u00efve.com", "xn--nave-6pa.com", false, true, true],
+ ["d\u00eener.com", "xn--dner-0pa.com", false, true, true],
+];
+
+const profiles = ["ASCII", "high", "moderate"];
+
+function run_test() {
+ var pbi = Cc["@mozilla.org/preferences-service;1"].getService(
+ Ci.nsIPrefBranch
+ );
+ var oldProfile = pbi.getCharPref(
+ "network.IDN.restriction_profile",
+ "moderate"
+ );
+ var oldWhitelistCom = pbi.getBoolPref("network.IDN.whitelist.com", false);
+ var idnService = Cc["@mozilla.org/network/idn-service;1"].getService(
+ Ci.nsIIDNService
+ );
+
+ for (var i = 0; i < profiles.length; ++i) {
+ pbi.setCharPref("network.IDN.restriction_profile", profiles[i]);
+ pbi.setBoolPref("network.IDN.whitelist.com", false);
+
+ dump("testing " + profiles[i] + " profile");
+
+ for (var j = 0; j < testcases.length; ++j) {
+ var test = testcases[j];
+ var URL = test[0] + ".com";
+ var punycodeURL = test[1] + ".com";
+ var expectedUnicode = test[2 + i];
+ var isASCII = {};
+
+ var result;
+ try {
+ result = idnService.convertToDisplayIDN(URL, isASCII);
+ } catch (e) {
+ result = ".com";
+ }
+ if (
+ punycodeURL.substr(0, 4) == "xn--" ||
+ punycodeURL.indexOf(".xn--") > 0
+ ) {
+ // test convertToDisplayIDN with a Unicode URL and with a
+ // Punycode URL if we have one
+ Assert.equal(
+ escape(result),
+ expectedUnicode ? escape(URL) : escape(punycodeURL)
+ );
+
+ result = idnService.convertToDisplayIDN(punycodeURL, isASCII);
+ Assert.equal(
+ escape(result),
+ expectedUnicode ? escape(URL) : escape(punycodeURL)
+ );
+ } else {
+ // The "punycode" URL isn't punycode. This happens in testcases
+ // where the Unicode URL has become normalized to an ASCII URL,
+ // so, even though expectedUnicode is true, the expected result
+ // is equal to punycodeURL
+ Assert.equal(escape(result), escape(punycodeURL));
+ }
+ }
+ }
+ pbi.setBoolPref("network.IDN.whitelist.com", oldWhitelistCom);
+ pbi.setCharPref("network.IDN.restriction_profile", oldProfile);
+}
diff --git a/netwerk/test/unit/test_idna2008.js b/netwerk/test/unit/test_idna2008.js
new file mode 100644
index 0000000000..0e0290ce79
--- /dev/null
+++ b/netwerk/test/unit/test_idna2008.js
@@ -0,0 +1,65 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const kTransitionalProcessing = false;
+
+// Four characters map differently under non-transitional processing:
+const labels = [
+ // U+00DF LATIN SMALL LETTER SHARP S to "ss"
+ "stra\u00dfe",
+ // U+03C2 GREEK SMALL LETTER FINAL SIGMA to U+03C3 GREEK SMALL LETTER SIGMA
+ "\u03b5\u03bb\u03bb\u03ac\u03c2",
+ // U+200C ZERO WIDTH NON-JOINER in Indic script
+ "\u0646\u0627\u0645\u0647\u200c\u0627\u06cc",
+ // U+200D ZERO WIDTH JOINER in Arabic script
+ "\u0dc1\u0dca\u200d\u0dbb\u0dd3",
+
+ // But CONTEXTJ rules prohibit ZWJ and ZWNJ in non-Arabic or Indic scripts
+ // U+200C ZERO WIDTH NON-JOINER in Latin script
+ "m\u200cn",
+ // U+200D ZERO WIDTH JOINER in Latin script
+ "p\u200dq",
+];
+
+const transitionalExpected = [
+ "strasse",
+ "xn--hxarsa5b",
+ "xn--mgba3gch31f",
+ "xn--10cl1a0b",
+ "",
+ "",
+];
+
+const nonTransitionalExpected = [
+ "xn--strae-oqa",
+ "xn--hxarsa0b",
+ "xn--mgba3gch31f060k",
+ "xn--10cl1a0b660p",
+ "",
+ "",
+];
+
+// Test options for converting IDN URLs under IDNA2008
+function run_test() {
+ var idnService = Cc["@mozilla.org/network/idn-service;1"].getService(
+ Ci.nsIIDNService
+ );
+
+ for (var i = 0; i < labels.length; ++i) {
+ var result;
+ try {
+ result = idnService.convertUTF8toACE(labels[i]);
+ } catch (e) {
+ result = "";
+ }
+
+ if (kTransitionalProcessing) {
+ equal(result, transitionalExpected[i]);
+ } else {
+ equal(result, nonTransitionalExpected[i]);
+ }
+ }
+}
diff --git a/netwerk/test/unit/test_idnservice.js b/netwerk/test/unit/test_idnservice.js
new file mode 100644
index 0000000000..0c52f300e3
--- /dev/null
+++ b/netwerk/test/unit/test_idnservice.js
@@ -0,0 +1,39 @@
+// Tests nsIIDNService
+
+"use strict";
+
+const idnService = Cc["@mozilla.org/network/idn-service;1"].getService(
+ Ci.nsIIDNService
+);
+
+add_task(async function test_simple() {
+ let reference = [
+ // The 3rd element indicates whether the second element
+ // is ACE-encoded
+ ["asciihost", "asciihost", false],
+ ["b\u00FCcher", "xn--bcher-kva", true],
+ ];
+
+ for (var i = 0; i < reference.length; ++i) {
+ dump("Testing " + reference[i] + "\n");
+ // We test the following:
+ // - Converting UTF-8 to ACE and back gives us the expected answer
+ // - Converting the ASCII string UTF-8 -> ACE leaves the string unchanged
+ // - isACE returns true when we expect it to (third array elem true)
+ Assert.equal(idnService.convertUTF8toACE(reference[i][0]), reference[i][1]);
+ Assert.equal(idnService.convertUTF8toACE(reference[i][1]), reference[i][1]);
+ Assert.equal(idnService.convertACEtoUTF8(reference[i][1]), reference[i][0]);
+ Assert.equal(idnService.isACE(reference[i][1]), reference[i][2]);
+ }
+});
+
+add_task(async function test_extra_blocked() {
+ let isAscii = {};
+ equal(idnService.convertToDisplayIDN("xn--gou-2lb.ro", isAscii), "goșu.ro");
+ Services.prefs.setStringPref("network.IDN.extra_blocked_chars", "ș");
+ equal(
+ idnService.convertToDisplayIDN("xn--gou-2lb.ro", isAscii),
+ "xn--gou-2lb.ro"
+ );
+ Services.prefs.clearUserPref("network.IDN.extra_blocked_chars");
+});
diff --git a/netwerk/test/unit/test_immutable.js b/netwerk/test/unit/test_immutable.js
new file mode 100644
index 0000000000..50b7967a6a
--- /dev/null
+++ b/netwerk/test/unit/test_immutable.js
@@ -0,0 +1,167 @@
+"use strict";
+
+var prefs;
+var spdypref;
+var http2pref;
+var origin;
+var rcwnpref;
+
+function run_test() {
+ var env = Cc["@mozilla.org/process/environment;1"].getService(
+ Ci.nsIEnvironment
+ );
+ var h2Port = env.get("MOZHTTP2_PORT");
+ Assert.notEqual(h2Port, null);
+ Assert.notEqual(h2Port, "");
+
+ // Set to allow the cert presented by our H2 server
+ do_get_profile();
+ prefs = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch);
+
+ spdypref = prefs.getBoolPref("network.http.spdy.enabled");
+ http2pref = prefs.getBoolPref("network.http.spdy.enabled.http2");
+ rcwnpref = prefs.getBoolPref("network.http.rcwn.enabled");
+
+ prefs.setBoolPref("network.http.spdy.enabled", true);
+ prefs.setBoolPref("network.http.spdy.enabled.http2", true);
+ prefs.setCharPref(
+ "network.dns.localDomains",
+ "foo.example.com, bar.example.com"
+ );
+ // Disable rcwn to make cache behavior deterministic.
+ prefs.setBoolPref("network.http.rcwn.enabled", false);
+
+ // The moz-http2 cert is for foo.example.com and is signed by http2-ca.pem
+ // so add that cert to the trust list as a signing cert. // the foo.example.com domain name.
+ let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(
+ Ci.nsIX509CertDB
+ );
+ addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u");
+
+ origin = "https://foo.example.com:" + h2Port;
+ dump("origin - " + origin + "\n");
+ doTest1();
+}
+
+function resetPrefs() {
+ prefs.setBoolPref("network.http.spdy.enabled", spdypref);
+ prefs.setBoolPref("network.http.spdy.enabled.http2", http2pref);
+ prefs.setBoolPref("network.http.rcwn.enabled", rcwnpref);
+ prefs.clearUserPref("network.dns.localDomains");
+}
+
+function makeChan(origin, path) {
+ return NetUtil.newChannel({
+ uri: origin + path,
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+}
+
+var nextTest;
+var expectPass = true;
+var expectConditional = false;
+
+var Listener = function() {};
+Listener.prototype = {
+ onStartRequest: function testOnStartRequest(request) {
+ Assert.ok(request instanceof Ci.nsIHttpChannel);
+
+ if (expectPass) {
+ if (!Components.isSuccessCode(request.status)) {
+ do_throw(
+ "Channel should have a success code! (" + request.status + ")"
+ );
+ }
+ Assert.equal(request.responseStatus, 200);
+ } else {
+ Assert.equal(Components.isSuccessCode(request.status), false);
+ }
+ },
+
+ onDataAvailable: function testOnDataAvailable(request, stream, off, cnt) {
+ read_stream(stream, cnt);
+ },
+
+ onStopRequest: function testOnStopRequest(request, status) {
+ if (expectConditional) {
+ Assert.equal(request.getResponseHeader("x-conditional"), "true");
+ } else {
+ try {
+ Assert.notEqual(request.getResponseHeader("x-conditional"), "true");
+ } catch (e) {
+ Assert.ok(true);
+ }
+ }
+ nextTest();
+ do_test_finished();
+ },
+};
+
+function testsDone() {
+ dump("testDone\n");
+ resetPrefs();
+}
+
+function doTest1() {
+ dump("execute doTest1 - resource without immutable. initial request\n");
+ do_test_pending();
+ expectConditional = false;
+ var chan = makeChan(origin, "/immutable-test-without-attribute");
+ var listener = new Listener();
+ nextTest = doTest2;
+ chan.asyncOpen(listener);
+}
+
+function doTest2() {
+ dump("execute doTest2 - resource without immutable. reload\n");
+ do_test_pending();
+ expectConditional = true;
+ var chan = makeChan(origin, "/immutable-test-without-attribute");
+ var listener = new Listener();
+ nextTest = doTest3;
+ chan.loadFlags = Ci.nsIRequest.VALIDATE_ALWAYS;
+ chan.asyncOpen(listener);
+}
+
+function doTest3() {
+ dump("execute doTest3 - resource without immutable. shift reload\n");
+ do_test_pending();
+ expectConditional = false;
+ var chan = makeChan(origin, "/immutable-test-without-attribute");
+ var listener = new Listener();
+ nextTest = doTest4;
+ chan.loadFlags = Ci.nsIRequest.LOAD_BYPASS_CACHE;
+ chan.asyncOpen(listener);
+}
+
+function doTest4() {
+ dump("execute doTest1 - resource with immutable. initial request\n");
+ do_test_pending();
+ expectConditional = false;
+ var chan = makeChan(origin, "/immutable-test-with-attribute");
+ var listener = new Listener();
+ nextTest = doTest5;
+ chan.asyncOpen(listener);
+}
+
+function doTest5() {
+ dump("execute doTest5 - resource with immutable. reload\n");
+ do_test_pending();
+ expectConditional = false;
+ var chan = makeChan(origin, "/immutable-test-with-attribute");
+ var listener = new Listener();
+ nextTest = doTest6;
+ chan.loadFlags = Ci.nsIRequest.VALIDATE_ALWAYS;
+ chan.asyncOpen(listener);
+}
+
+function doTest6() {
+ dump("execute doTest3 - resource with immutable. shift reload\n");
+ do_test_pending();
+ expectConditional = false;
+ var chan = makeChan(origin, "/immutable-test-with-attribute");
+ var listener = new Listener();
+ nextTest = testsDone;
+ chan.loadFlags = Ci.nsIRequest.LOAD_BYPASS_CACHE;
+ chan.asyncOpen(listener);
+}
diff --git a/netwerk/test/unit/test_inhibit_caching.js b/netwerk/test/unit/test_inhibit_caching.js
new file mode 100644
index 0000000000..1af75aaa33
--- /dev/null
+++ b/netwerk/test/unit/test_inhibit_caching.js
@@ -0,0 +1,92 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+var first = true;
+function contentHandler(metadata, response) {
+ response.setHeader("Content-Type", "text/plain");
+ var body = "first";
+ if (!first) {
+ body = "second";
+ }
+ first = false;
+ response.bodyOutputStream.write(body, body.length);
+}
+
+XPCOMUtils.defineLazyGetter(this, "uri", function() {
+ return "http://localhost:" + httpserver.identity.primaryPort;
+});
+
+var httpserver = null;
+
+function run_test() {
+ // setup test
+ httpserver = new HttpServer();
+ httpserver.registerPathHandler("/test", contentHandler);
+ httpserver.start(-1);
+
+ add_test(test_first_response);
+ add_test(test_inhibit_caching);
+
+ run_next_test();
+}
+
+// Makes a regular request
+function test_first_response() {
+ var chan = NetUtil.newChannel({
+ uri: uri + "/test",
+ loadUsingSystemPrincipal: true,
+ });
+ chan.asyncOpen(new ChannelListener(check_first_response, null));
+}
+
+// Checks that we got the appropriate response
+function check_first_response(request, buffer) {
+ request.QueryInterface(Ci.nsIHttpChannel);
+ Assert.equal(request.responseStatus, 200);
+ Assert.equal(buffer, "first");
+ // Open the cache entry to check its contents
+ asyncOpenCacheEntry(
+ uri + "/test",
+ "disk",
+ Ci.nsICacheStorage.OPEN_READONLY,
+ null,
+ cache_entry_callback
+ );
+}
+
+// Checks that the cache entry has the correct contents
+function cache_entry_callback(status, entry) {
+ equal(status, Cr.NS_OK);
+ var inputStream = entry.openInputStream(0);
+ pumpReadStream(inputStream, function(read) {
+ inputStream.close();
+ equal(read, "first");
+ run_next_test();
+ });
+}
+
+// Makes a request with the INHIBIT_CACHING load flag
+function test_inhibit_caching() {
+ var chan = NetUtil.newChannel({
+ uri: uri + "/test",
+ loadUsingSystemPrincipal: true,
+ });
+ chan.QueryInterface(Ci.nsIRequest).loadFlags |= Ci.nsIRequest.INHIBIT_CACHING;
+ chan.asyncOpen(new ChannelListener(check_second_response, null));
+}
+
+// Checks that we got a different response from the first request
+function check_second_response(request, buffer) {
+ request.QueryInterface(Ci.nsIHttpChannel);
+ Assert.equal(request.responseStatus, 200);
+ Assert.equal(buffer, "second");
+ // Checks that the cache entry still contains the content from the first request
+ asyncOpenCacheEntry(
+ uri + "/test",
+ "disk",
+ Ci.nsICacheStorage.OPEN_READONLY,
+ null,
+ cache_entry_callback
+ );
+}
diff --git a/netwerk/test/unit/test_ioservice.js b/netwerk/test/unit/test_ioservice.js
new file mode 100644
index 0000000000..d218d77a7a
--- /dev/null
+++ b/netwerk/test/unit/test_ioservice.js
@@ -0,0 +1,19 @@
+"use strict";
+
+add_task(function test_extractScheme() {
+ equal(Services.io.extractScheme("HtTp://example.com"), "http");
+ Assert.throws(
+ () => {
+ Services.io.extractScheme("://example.com");
+ },
+ /NS_ERROR_MALFORMED_URI/,
+ "missing scheme"
+ );
+ Assert.throws(
+ () => {
+ Services.io.extractScheme("ht%tp://example.com");
+ },
+ /NS_ERROR_MALFORMED_URI/,
+ "bad scheme"
+ );
+});
diff --git a/netwerk/test/unit/test_large_port.js b/netwerk/test/unit/test_large_port.js
new file mode 100644
index 0000000000..492a03047f
--- /dev/null
+++ b/netwerk/test/unit/test_large_port.js
@@ -0,0 +1,68 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Ensure that non-16-bit URIs are rejected
+
+"use strict";
+
+function run_test() {
+ let mutator = Cc[
+ "@mozilla.org/network/standard-url-mutator;1"
+ ].createInstance(Ci.nsIURIMutator);
+ Assert.ok(mutator, "Mutator constructor works");
+
+ let url = Cc["@mozilla.org/network/standard-url-mutator;1"]
+ .createInstance(Ci.nsIStandardURLMutator)
+ .init(
+ Ci.nsIStandardURL.URLTYPE_AUTHORITY,
+ 65535,
+ "http://localhost",
+ "UTF-8",
+ null
+ )
+ .finalize();
+
+ // Bug 1301621 makes invalid ports throw
+ Assert.throws(
+ () => {
+ url = Cc["@mozilla.org/network/standard-url-mutator;1"]
+ .createInstance(Ci.nsIStandardURLMutator)
+ .init(
+ Ci.nsIStandardURL.URLTYPE_AUTHORITY,
+ 65536,
+ "http://localhost",
+ "UTF-8",
+ null
+ )
+ .finalize();
+ },
+ /NS_ERROR_MALFORMED_URI/,
+ "invalid port during creation"
+ );
+
+ Assert.throws(
+ () => {
+ url = url
+ .mutate()
+ .QueryInterface(Ci.nsIStandardURLMutator)
+ .setDefaultPort(65536)
+ .finalize();
+ },
+ /NS_ERROR_MALFORMED_URI/,
+ "invalid port in setDefaultPort"
+ );
+ Assert.throws(
+ () => {
+ url = url
+ .mutate()
+ .setPort(65536)
+ .finalize();
+ },
+ /NS_ERROR_MALFORMED_URI/,
+ "invalid port in port setter"
+ );
+
+ Assert.equal(url.port, -1);
+ do_test_finished();
+}
diff --git a/netwerk/test/unit/test_link.desktop b/netwerk/test/unit/test_link.desktop
new file mode 100644
index 0000000000..b1798202e3
--- /dev/null
+++ b/netwerk/test/unit/test_link.desktop
@@ -0,0 +1,3 @@
+[Desktop Entry]
+Type=Link
+URL=http://www.mozilla.org/
diff --git a/netwerk/test/unit/test_link.lnk b/netwerk/test/unit/test_link.lnk
new file mode 100644
index 0000000000..125d859f43
--- /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..accfede36a
--- /dev/null
+++ b/netwerk/test/unit/test_loadgroup_cancel.js
@@ -0,0 +1,94 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+function makeChan(url) {
+ return NetUtil.newChannel({
+ uri: url,
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+}
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+function request_handler(metadata, response) {
+ response.processAsync();
+ do_timeout(500, () => {
+ const body = "some body once told me...";
+ response.setStatusLine(metadata.httpVersion, 200, "Ok");
+ response.setHeader("Content-Type", "text/plain", false);
+ response.setHeader("Content-Length", "" + body.length, false);
+ response.bodyOutputStream.write(body, body.length);
+ response.finish();
+ });
+}
+
+// This test checks that when canceling a loadgroup by the time the loadgroup's
+// groupObserver is sent OnStopRequest for a request, that request has been
+// canceled.
+add_task(async function test_cancelledInOnStop() {
+ let http_server = new HttpServer();
+ http_server.registerPathHandler("/test1", request_handler);
+ http_server.registerPathHandler("/test2", request_handler);
+ http_server.registerPathHandler("/test3", request_handler);
+ http_server.start(-1);
+ const port = http_server.identity.primaryPort;
+
+ let loadGroup = Cc["@mozilla.org/network/load-group;1"].createInstance(
+ Ci.nsILoadGroup
+ );
+
+ let loadListener = {
+ onStartRequest: aRequest => {
+ info("onStartRequest");
+ },
+ onStopRequest: (aRequest, aStatusCode) => {
+ equal(
+ aStatusCode,
+ Cr.NS_ERROR_ABORT,
+ "aStatusCode must be the cancellation code"
+ );
+ equal(
+ aRequest.status,
+ Cr.NS_ERROR_ABORT,
+ "aRequest.status must be the cancellation code"
+ );
+ },
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIRequestObserver",
+ "nsISupportsWeakReference",
+ ]),
+ };
+ loadGroup.groupObserver = loadListener;
+
+ let chan1 = makeChan(`http://localhost:${port}/test1`);
+ chan1.loadGroup = loadGroup;
+ let chan2 = makeChan(`http://localhost:${port}/test2`);
+ chan2.loadGroup = loadGroup;
+ let chan3 = makeChan(`http://localhost:${port}/test3`);
+ chan3.loadGroup = loadGroup;
+
+ await new Promise(resolve => do_timeout(500, resolve));
+
+ let promises = [
+ new Promise(resolve => {
+ chan1.asyncOpen(new ChannelListener(resolve, null, CL_EXPECT_FAILURE));
+ }),
+ new Promise(resolve => {
+ chan2.asyncOpen(new ChannelListener(resolve, null, CL_EXPECT_FAILURE));
+ }),
+ new Promise(resolve => {
+ chan3.asyncOpen(new ChannelListener(resolve, null, CL_EXPECT_FAILURE));
+ }),
+ ];
+
+ loadGroup.cancel(Cr.NS_ERROR_ABORT);
+
+ await Promise.all(promises);
+
+ await new Promise(resolve => {
+ http_server.stop(resolve);
+ });
+});
diff --git a/netwerk/test/unit/test_localstreams.js b/netwerk/test/unit/test_localstreams.js
new file mode 100644
index 0000000000..a4b76a7668
--- /dev/null
+++ b/netwerk/test/unit/test_localstreams.js
@@ -0,0 +1,90 @@
+// Tests bug 304414
+
+"use strict";
+
+const PR_RDONLY = 0x1; // see prio.h
+
+// Does some sanity checks on the stream and returns the number of bytes read
+// when the checks passed.
+function test_stream(stream) {
+ // This test only handles blocking streams; that's desired for file streams
+ // anyway.
+ Assert.equal(stream.isNonBlocking(), false);
+
+ // Check that the stream is not buffered
+ Assert.equal(
+ Cc["@mozilla.org/io-util;1"]
+ .getService(Ci.nsIIOUtil)
+ .inputStreamIsBuffered(stream),
+ false
+ );
+
+ // Wrap it in a binary stream (to avoid wrong results that
+ // scriptablestream would produce with binary content)
+ var binstream = Cc["@mozilla.org/binaryinputstream;1"].createInstance(
+ Ci.nsIBinaryInputStream
+ );
+ binstream.setInputStream(stream);
+
+ var numread = 0;
+ for (;;) {
+ Assert.equal(stream.available(), binstream.available());
+ var avail = stream.available();
+ Assert.notEqual(avail, -1);
+
+ // PR_UINT32_MAX and PR_INT32_MAX; the files we're testing with aren't that
+ // large.
+ Assert.notEqual(avail, Math.pow(2, 32) - 1);
+ Assert.notEqual(avail, Math.pow(2, 31) - 1);
+
+ if (!avail) {
+ // For blocking streams, available() only returns 0 on EOF
+ // Make sure that there is really no data left
+ var could_read = false;
+ try {
+ binstream.readByteArray(1);
+ could_read = true;
+ } catch (e) {
+ // We expect the exception, so do nothing here
+ }
+ if (could_read) {
+ do_throw("Data readable when available indicated EOF!");
+ }
+ return numread;
+ }
+
+ dump("Trying to read " + avail + " bytes\n");
+ // Note: Verification that this does return as much bytes as we asked for is
+ // done in the binarystream implementation
+ binstream.readByteArray(avail);
+
+ numread += avail;
+ }
+}
+
+function stream_for_file(file) {
+ var str = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(
+ Ci.nsIFileInputStream
+ );
+ str.init(file, PR_RDONLY, 0, 0);
+ return str;
+}
+
+function stream_from_channel(file) {
+ var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
+ var uri = ios.newFileURI(file);
+ return NetUtil.newChannel({
+ uri,
+ loadUsingSystemPrincipal: true,
+ }).open();
+}
+
+function run_test() {
+ // Get a file and a directory in order to do some testing
+ var file = do_get_file("../unit/data/test_readline6.txt");
+ var len = file.fileSize;
+ Assert.equal(test_stream(stream_for_file(file)), len);
+ Assert.equal(test_stream(stream_from_channel(file)), len);
+ var dir = file.parent;
+ test_stream(stream_from_channel(dir)); // Can't do size checking
+}
diff --git a/netwerk/test/unit/test_mismatch_last-modified.js b/netwerk/test/unit/test_mismatch_last-modified.js
new file mode 100644
index 0000000000..5b59464b53
--- /dev/null
+++ b/netwerk/test/unit/test_mismatch_last-modified.js
@@ -0,0 +1,155 @@
+"use strict";
+
+const BinaryInputStream = Components.Constructor(
+ "@mozilla.org/binaryinputstream;1",
+ "nsIBinaryInputStream",
+ "setInputStream"
+);
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+var httpserver = new HttpServer();
+
+var ios;
+
+// Test the handling of a cache revalidation with mismatching last-modified
+// headers. If we get such a revalidation the cache entry should be purged.
+// see bug 717350
+
+// In this test the wrong data is from 11-16-1994 with a value of 'A',
+// and the right data is from 11-15-1994 with a value of 'B'.
+
+// the same URL is requested 3 times. the first time the wrong data comes
+// back, the second time that wrong data is revalidated with a 304 but
+// a L-M header of the right data (this triggers a cache purge), and
+// the third time the right data is returned.
+
+var listener_3 = {
+ // this listener is used to process the the request made after
+ // the cache invalidation. it expects to see the 'right data'
+
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIStreamListener",
+ "nsIRequestObserver",
+ ]),
+
+ onStartRequest: function test_onStartR(request) {},
+
+ onDataAvailable: function test_ODA(request, inputStream, offset, count) {
+ var data = new BinaryInputStream(inputStream).readByteArray(count);
+
+ Assert.equal(data[0], "B".charCodeAt(0));
+ },
+
+ onStopRequest: function test_onStopR(request, status) {
+ httpserver.stop(do_test_finished);
+ },
+};
+
+XPCOMUtils.defineLazyGetter(this, "listener_2", function() {
+ return {
+ // this listener is used to process the revalidation of the
+ // corrupted cache entry. its revalidation prompts it to be cleaned
+
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIStreamListener",
+ "nsIRequestObserver",
+ ]),
+
+ onStartRequest: function test_onStartR(request) {},
+
+ onDataAvailable: function test_ODA(request, inputStream, offset, count) {
+ var data = new BinaryInputStream(inputStream).readByteArray(count);
+
+ // This is 'A' from a cache revalidation, but that reval will clean the cache
+ // because of mismatched last-modified response headers
+
+ Assert.equal(data[0], "A".charCodeAt(0));
+ },
+
+ onStopRequest: function test_onStopR(request, status) {
+ request.QueryInterface(Ci.nsIHttpChannel);
+ var chan = NetUtil.newChannel({
+ uri: "http://localhost:" + httpserver.identity.primaryPort + "/test1",
+ loadUsingSystemPrincipal: true,
+ });
+ chan.asyncOpen(listener_3);
+ },
+ };
+});
+
+XPCOMUtils.defineLazyGetter(this, "listener_1", function() {
+ return {
+ // this listener processes the initial request from a empty cache.
+ // the server responds with the wrong data ('A')
+
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIStreamListener",
+ "nsIRequestObserver",
+ ]),
+
+ onStartRequest: function test_onStartR(request) {},
+
+ onDataAvailable: function test_ODA(request, inputStream, offset, count) {
+ var data = new BinaryInputStream(inputStream).readByteArray(count);
+ Assert.equal(data[0], "A".charCodeAt(0));
+ },
+
+ onStopRequest: function test_onStopR(request, status) {
+ request.QueryInterface(Ci.nsIHttpChannel);
+ var chan = NetUtil.newChannel({
+ uri: "http://localhost:" + httpserver.identity.primaryPort + "/test1",
+ loadUsingSystemPrincipal: true,
+ });
+ chan.asyncOpen(listener_2);
+ },
+ };
+});
+
+function run_test() {
+ do_get_profile();
+ ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
+
+ evict_cache_entries();
+
+ httpserver.registerPathHandler("/test1", handler);
+ httpserver.start(-1);
+
+ var port = httpserver.identity.primaryPort;
+ var chan = NetUtil.newChannel({
+ uri: "http://localhost:" + port + "/test1",
+ loadUsingSystemPrincipal: true,
+ });
+ chan.asyncOpen(listener_1);
+
+ do_test_pending();
+}
+
+var iter = 0;
+function handler(metadata, response) {
+ iter++;
+ if (metadata.hasHeader("If-Modified-Since")) {
+ response.setStatusLine(metadata.httpVersion, 304, "Not Modified");
+ response.setHeader("Last-Modified", "Tue, 15 Nov 1994 12:45:26 GMT", false);
+ } else {
+ response.setStatusLine(metadata.httpVersion, 200, "OK");
+ response.setHeader("Cache-Control", "max-age=0", false);
+ if (iter == 1) {
+ // simulated wrong response
+ response.setHeader(
+ "Last-Modified",
+ "Wed, 16 Nov 1994 00:00:00 GMT",
+ false
+ );
+ response.bodyOutputStream.write("A", 1);
+ }
+ if (iter == 3) {
+ // 'correct' response
+ response.setHeader(
+ "Last-Modified",
+ "Tue, 15 Nov 1994 12:45:26 GMT",
+ false
+ );
+ response.bodyOutputStream.write("B", 1);
+ }
+ }
+}
diff --git a/netwerk/test/unit/test_mozTXTToHTMLConv.js b/netwerk/test/unit/test_mozTXTToHTMLConv.js
new file mode 100644
index 0000000000..620d527e1c
--- /dev/null
+++ b/netwerk/test/unit/test_mozTXTToHTMLConv.js
@@ -0,0 +1,395 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Test that mozITXTToHTMLConv works properly.
+ */
+
+"use strict";
+
+function run_test() {
+ let converter = Cc["@mozilla.org/txttohtmlconv;1"].getService(
+ Ci.mozITXTToHTMLConv
+ );
+
+ const scanTXTtests = [
+ // -- RFC1738
+ {
+ input: "RFC1738: <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: ["moz-smiley-s1"],
+ },
+ {
+ input: "this is a smiley :-)",
+ results: ["moz-smiley-s1"],
+ },
+ {
+ input: "this is a smiley :-(",
+ results: ["moz-smiley-s2"],
+ },
+ ];
+
+ const scanTXTstrings = [
+ "underline", // ASCII
+ "äöüßáéíóúî", // Latin-1
+ "a\u0301c\u0327c\u030Ce\u0309n\u0303t\u0326e\u0308d\u0323",
+ // áçčẻñțëḍ Latin
+ "\u016B\u00F1\u0257\u0119\u0211\u0142\u00ED\u00F1\u0119",
+ // Pseudo-ese ūñɗęȑłíñę
+ "\u01DDu\u0131\u0283\u0279\u01DDpun", // Upside down ǝuıʃɹǝpun
+ "\u03C5\u03C0\u03BF\u03B3\u03C1\u03AC\u03BC\u03BC\u03B9\u03C3\u03B7",
+ // Greek υπογράμμιση
+ "\u0441\u0438\u043B\u044C\u043D\u0443\u044E", // Russian сильную
+ "\u0C2C\u0C32\u0C2E\u0C46\u0C56\u0C28", // Telugu బలమైన
+ "\u508D\u7DDA\u3059\u308B", // Japanese 傍線する
+ "\uD841\uDF0E\uD841\uDF31\uD841\uDF79\uD843\uDC53\uD843\uDC78",
+ // Chinese (supplementary plane)
+ "\uD801\uDC14\uD801\uDC2F\uD801\uDC45\uD801\uDC28\uD801\uDC49\uD801\uDC2F\uD801\uDC3B",
+ // Deseret 𐐔𐐯𐑅𐐨𐑉𐐯𐐻
+ ];
+
+ const scanTXTstructs = [
+ {
+ delimiter: "/",
+ tag: "i",
+ class: "moz-txt-slash",
+ },
+ {
+ delimiter: "*",
+ tag: "b",
+ class: "moz-txt-star",
+ },
+ {
+ delimiter: "_",
+ tag: "span",
+ class: "moz-txt-underscore",
+ },
+ {
+ delimiter: "|",
+ tag: "code",
+ class: "moz-txt-verticalline",
+ },
+ ];
+
+ const scanHTMLtests = [
+ {
+ input: "http://foo.example.com",
+ shouldChange: true,
+ },
+ {
+ input: " <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..122d32e7d5
--- /dev/null
+++ b/netwerk/test/unit/test_multipart_byteranges.js
@@ -0,0 +1,141 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+var httpserver = null;
+
+XPCOMUtils.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..e8921fd2c4
--- /dev/null
+++ b/netwerk/test/unit/test_multipart_streamconv-byte-by-byte.js
@@ -0,0 +1,113 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+var httpserver = null;
+
+XPCOMUtils.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..421a8465cf
--- /dev/null
+++ b/netwerk/test/unit/test_multipart_streamconv.js
@@ -0,0 +1,98 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+var httpserver = null;
+
+XPCOMUtils.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..68bc5e6be8
--- /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, status) {
+ resolve([status, 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..ebe61854ae
--- /dev/null
+++ b/netwerk/test/unit/test_multipart_streamconv_missing_boundary_lead_dashes.js
@@ -0,0 +1,90 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+var httpserver = null;
+
+XPCOMUtils.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..1367a3499f
--- /dev/null
+++ b/netwerk/test/unit/test_multipart_streamconv_missing_lead_boundary.js
@@ -0,0 +1,90 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+var httpserver = null;
+
+XPCOMUtils.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..f7d8881ce1
--- /dev/null
+++ b/netwerk/test/unit/test_nestedabout_serialize.js
@@ -0,0 +1,41 @@
+"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");
+
+const kNestedAboutCID = "{2f277c00-0eaf-4ddb-b936-41326ba48aae}";
+
+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..5cbc3c7baf
--- /dev/null
+++ b/netwerk/test/unit/test_net_addr.js
@@ -0,0 +1,222 @@
+"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;
+
+/**
+ * Connections have 5 seconds to be made, or a timeout function fails this
+ * test. This prevents the test from hanging and bringing down the entire
+ * xpcshell test chain.
+ */
+var connectTimeout = 5 * 1000;
+
+/**
+ * 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
+ );
+ /*
+ * 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_activity.js b/netwerk/test/unit/test_network_activity.js
new file mode 100644
index 0000000000..77058dbf77
--- /dev/null
+++ b/netwerk/test/unit/test_network_activity.js
@@ -0,0 +1,61 @@
+// test for networkactivity
+//
+
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+var httpserver = new HttpServer();
+var results = [];
+var prefs = Cc["@mozilla.org/preferences-service;1"].getService(
+ Ci.nsIPrefBranch
+);
+
+function createChannel() {
+ var chan = NetUtil.newChannel({
+ uri: "http://localhost:" + httpserver.identity.primaryPort + "/ok",
+ loadUsingSystemPrincipal: true,
+ });
+ var httpChan = chan.QueryInterface(Ci.nsIHttpChannel);
+ return httpChan;
+}
+
+function handler(metadata, response) {
+ var body = "meh";
+ response.setHeader("Content-Type", "text/plain", false);
+ response.setStatusLine(metadata.httpVersion, 200, "OK");
+ response.bodyOutputStream.write(body, body.length);
+}
+
+async function checkValueAndTrigger(request, data) {
+ // give Firefox 150 ms to send notifications out
+ do_timeout(150, doTest);
+}
+
+function doTest() {
+ ok(results.length > 0);
+ ok(results[0].host == "127.0.0.1");
+ ok(results[0].rx > 0 || results[0].tx > 0);
+ httpserver.stop(do_test_finished);
+}
+
+function run_test() {
+ // setting up an observer
+ let networkActivity = function(subject, topic, value) {
+ subject.QueryInterface(Ci.nsIMutableArray);
+ for (let data of subject.enumerate()) {
+ results.push(data);
+ }
+ };
+
+ Services.obs.addObserver(networkActivity, "network-activity");
+
+ // why do I have to do this ??
+ Services.obs.notifyObservers(null, "profile-initial-state");
+
+ do_test_pending();
+ httpserver.registerPathHandler("/ok", handler);
+ httpserver.start(-1);
+ var channel = createChannel();
+ channel.asyncOpen(new ChannelListener(checkValueAndTrigger, null));
+}
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..ef66823f55
--- /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.import("resource://testing-common/httpd.js");
+
+/**
+ * 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, topic, data) {
+ let matches = typeof matchFunc != "function" || matchFunc(subject, data);
+ if (!matches) {
+ return;
+ }
+ Services.obs.removeObserver(observe, topic);
+ resolve({ subject, data });
+ }, topic);
+ });
+}
+
+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;
+XPCOMUtils.defineLazyGetter(this, "URL", function() {
+ return "http://localhost:" + httpserver.identity.primaryPort + "/content";
+});
+
+XPCOMUtils.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 DEFAULT_WAIT_TIME = 200; // ms
+
+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_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..06bdd29515
--- /dev/null
+++ b/netwerk/test/unit/test_no_cookies_after_last_pb_exit.js
@@ -0,0 +1,134 @@
+/* globals ChromeUtils, Assert, add_task */
+"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.import("resource://testing-common/httpd.js");
+
+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 = Cc["@mozilla.org/cookiemanager;1"].getService(Ci.nsICookieManager);
+ 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 == 0) {
+ 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..6af8f46ebf
--- /dev/null
+++ b/netwerk/test/unit/test_nojsredir.js
@@ -0,0 +1,65 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+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_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_obs-fold.js b/netwerk/test/unit/test_obs-fold.js
new file mode 100644
index 0000000000..84915aa748
--- /dev/null
+++ b/netwerk/test/unit/test_obs-fold.js
@@ -0,0 +1,73 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+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..292fd68a9d
--- /dev/null
+++ b/netwerk/test/unit/test_offline_status.js
@@ -0,0 +1,19 @@
+"use strict";
+
+function run_test() {
+ var ioService = Cc["@mozilla.org/network/io-service;1"].getService(
+ Ci.nsIIOService
+ );
+
+ 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(ioService.offline, linkService.isLinkUp);
+ } catch (e) {
+ // The network link service might not be available
+ Assert.equal(ioService.offline, false);
+ }
+}
diff --git a/netwerk/test/unit/test_offlinecache_custom-directory.js b/netwerk/test/unit/test_offlinecache_custom-directory.js
new file mode 100644
index 0000000000..495399da95
--- /dev/null
+++ b/netwerk/test/unit/test_offlinecache_custom-directory.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/. */
+
+/**
+ * This test executes nsIOfflineCacheUpdateService.scheduleAppUpdate API
+ * 1. preloads an app with a manifest to a custom sudir in the profile (for simplicity)
+ * 2. observes progress and completion of the update
+ * 3. checks presence of index.sql and files in the expected location
+ */
+
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+var httpServer = null;
+var cacheUpdateObserver = null;
+var systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal();
+
+function make_channel(url, callback, ctx) {
+ return NetUtil.newChannel({
+ uri: url,
+ loadUsingSystemPrincipal: true,
+ });
+}
+
+function make_uri(url) {
+ var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
+ return ios.newURI(url);
+}
+
+// start the test with loading this master entry referencing the manifest
+function masterEntryHandler(metadata, response) {
+ var masterEntryContent = "<html manifest='/manifest'></html>";
+ response.setHeader("Content-Type", "text/html");
+ response.bodyOutputStream.write(
+ masterEntryContent,
+ masterEntryContent.length
+ );
+}
+
+// manifest defines fallback namespace from any /redirect path to /content
+function manifestHandler(metadata, response) {
+ var manifestContent = "CACHE MANIFEST\n";
+ response.setHeader("Content-Type", "text/cache-manifest");
+ response.bodyOutputStream.write(manifestContent, manifestContent.length);
+}
+
+// finally check we got fallback content
+function finish_test(customDir) {
+ var offlineCacheDir = customDir.clone();
+ offlineCacheDir.append("OfflineCache");
+
+ var indexSqlFile = offlineCacheDir.clone();
+ indexSqlFile.append("index.sqlite");
+ Assert.equal(indexSqlFile.exists(), true);
+
+ var file1 = offlineCacheDir.clone();
+ file1.append("2");
+ file1.append("E");
+ file1.append("2C99DE6E7289A5-0");
+ Assert.equal(file1.exists(), true);
+
+ var file2 = offlineCacheDir.clone();
+ file2.append("8");
+ file2.append("6");
+ file2.append("0B457F75198B29-0");
+ Assert.equal(file2.exists(), true);
+
+ // This must not throw an exception. After the update has finished
+ // the index file can be freely removed. This way we check this process
+ // is no longer keeping the file open. Check like this will probably
+ // work only Windows systems.
+
+ // This test could potentially randomaly fail when we start closing
+ // the offline cache database off the main thread. Tries in a loop
+ // may be a solution then.
+ try {
+ indexSqlFile.remove(false);
+ Assert.ok(true);
+ } catch (ex) {
+ do_throw(
+ "Could not remove the sqlite.index file, we still keep it open \n" +
+ ex +
+ "\n"
+ );
+ }
+
+ httpServer.stop(do_test_finished);
+}
+
+function run_test() {
+ httpServer = new HttpServer();
+ httpServer.registerPathHandler("/masterEntry", masterEntryHandler);
+ httpServer.registerPathHandler("/manifest", manifestHandler);
+ httpServer.start(4444);
+
+ var profileDir = do_get_profile();
+ var customDir = profileDir.clone();
+ customDir.append("customOfflineCacheDir" + Math.random());
+
+ var pm = Cc["@mozilla.org/permissionmanager;1"].getService(
+ Ci.nsIPermissionManager
+ );
+ var ssm = Cc["@mozilla.org/scriptsecuritymanager;1"].getService(
+ Ci.nsIScriptSecurityManager
+ );
+ var uri = make_uri("http://localhost:4444");
+ var principal = ssm.createContentPrincipal(uri, {});
+
+ if (pm.testPermissionFromPrincipal(principal, "offline-app") != 0) {
+ dump(
+ "Previous test failed to clear offline-app permission! Expect failures.\n"
+ );
+ }
+ pm.addFromPrincipal(
+ principal,
+ "offline-app",
+ Ci.nsIPermissionManager.ALLOW_ACTION
+ );
+
+ var ps = Cc["@mozilla.org/preferences-service;1"].getService(
+ Ci.nsIPrefBranch
+ );
+ ps.setBoolPref("browser.cache.offline.enable", true);
+ ps.setBoolPref("browser.cache.offline.storage.enable", true);
+ // Set this pref to mimic the default browser behavior.
+ ps.setComplexValue(
+ "browser.cache.offline.parent_directory",
+ Ci.nsIFile,
+ profileDir
+ );
+
+ var us = Cc["@mozilla.org/offlinecacheupdate-service;1"].getService(
+ Ci.nsIOfflineCacheUpdateService
+ );
+ var update = us.scheduleAppUpdate(
+ make_uri("http://localhost:4444/manifest"),
+ make_uri("http://localhost:4444/masterEntry"),
+ systemPrincipal,
+ customDir
+ );
+
+ var expectedStates = [
+ Ci.nsIOfflineCacheUpdateObserver.STATE_DOWNLOADING,
+ Ci.nsIOfflineCacheUpdateObserver.STATE_ITEMSTARTED,
+ Ci.nsIOfflineCacheUpdateObserver.STATE_ITEMPROGRESS,
+ Ci.nsIOfflineCacheUpdateObserver.STATE_ITEMCOMPLETED,
+ Ci.nsIOfflineCacheUpdateObserver.STATE_FINISHED,
+ ];
+
+ update.addObserver({
+ updateStateChanged(update, state) {
+ Assert.equal(state, expectedStates.shift());
+
+ if (state == Ci.nsIOfflineCacheUpdateObserver.STATE_FINISHED) {
+ finish_test(customDir);
+ }
+ },
+
+ applicationCacheAvailable(appCache) {},
+ });
+
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_origin.js b/netwerk/test/unit/test_origin.js
new file mode 100644
index 0000000000..a14fff8308
--- /dev/null
+++ b/netwerk/test/unit/test_origin.js
@@ -0,0 +1,330 @@
+"use strict";
+
+var h2Port;
+var prefs;
+var spdypref;
+var http2pref;
+var extpref;
+var loadGroup;
+
+function run_test() {
+ var env = Cc["@mozilla.org/process/environment;1"].getService(
+ Ci.nsIEnvironment
+ );
+ h2Port = env.get("MOZHTTP2_PORT");
+ Assert.notEqual(h2Port, null);
+ Assert.notEqual(h2Port, "");
+
+ // Set to allow the cert presented by our H2 server
+ do_get_profile();
+ prefs = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch);
+
+ spdypref = prefs.getBoolPref("network.http.spdy.enabled");
+ http2pref = prefs.getBoolPref("network.http.spdy.enabled.http2");
+ extpref = prefs.getBoolPref("network.http.originextension");
+
+ prefs.setBoolPref("network.http.spdy.enabled", true);
+ prefs.setBoolPref("network.http.spdy.enabled.http2", true);
+ prefs.setBoolPref("network.http.originextension", true);
+ prefs.setCharPref(
+ "network.dns.localDomains",
+ "foo.example.com, alt1.example.com"
+ );
+
+ // The moz-http2 cert is for {foo, alt1, alt2}.example.com and is signed by http2-ca.pem
+ // so add that cert to the trust list as a signing cert.
+ let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(
+ Ci.nsIX509CertDB
+ );
+ addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u");
+
+ doTest1();
+}
+
+function resetPrefs() {
+ prefs.setBoolPref("network.http.spdy.enabled", spdypref);
+ prefs.setBoolPref("network.http.spdy.enabled.http2", http2pref);
+ prefs.setBoolPref("network.http.originextension", extpref);
+ prefs.clearUserPref("network.dns.localDomains");
+}
+
+function makeChan(origin) {
+ return NetUtil.newChannel({
+ uri: origin,
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+}
+
+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..f2851c725b
--- /dev/null
+++ b/netwerk/test/unit/test_original_sent_received_head.js
@@ -0,0 +1,246 @@
+//
+// 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.import("resource://testing-common/httpd.js");
+
+XPCOMUtils.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");
+ }
+
+ try {
+ var etag = metadata.getHeader("If-None-Match");
+ } catch (ex) {
+ var 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_parse_content_type.js b/netwerk/test/unit/test_parse_content_type.js
new file mode 100644
index 0000000000..4abf5e4763
--- /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 = Cc["@mozilla.org/network/util;1"].getService(Ci.nsINetUtil);
+
+ 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..bbef41218f
--- /dev/null
+++ b/netwerk/test/unit/test_partial_response_entry_size_smart_shrink.js
@@ -0,0 +1,104 @@
+/*
+
+ 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.import("resource://testing-common/httpd.js");
+
+XPCOMUtils.defineLazyGetter(this, "URL", function() {
+ return "http://localhost:" + httpServer.identity.primaryPort;
+});
+
+var httpServer = null;
+
+function make_channel(url, callback, ctx) {
+ return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true });
+}
+
+// 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();
+ var slice = responseBody.slice(0, 100);
+ response.bodyOutputStream.write(slice, slice.length);
+ response.finish();
+ } else {
+ var 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..4936e367f9
--- /dev/null
+++ b/netwerk/test/unit/test_permmgr.js
@@ -0,0 +1,131 @@
+// 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 = Cc["@mozilla.org/permissionmanager;1"].getService(
+ Ci.nsIPermissionManager
+ );
+
+ var ioService = Cc["@mozilla.org/network/io-service;1"].getService(
+ Ci.nsIIOService
+ );
+
+ var secMan = Cc["@mozilla.org/scriptsecuritymanager;1"].getService(
+ Ci.nsIScriptSecurityManager
+ );
+
+ // nsIPermissionManager implementation is an extension; don't fail if it's not there
+ if (!pm) {
+ return;
+ }
+
+ // put a few hosts in
+ for (var 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 (var 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 (var j = 0; j < perms.length; ++j) {
+ pm.removePermission(perms[j]);
+ }
+
+ // ... ensure each and every element is equal ...
+ for (var i = 0; i < hosts.length; ++i) {
+ for (var 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..60d947a31f
--- /dev/null
+++ b/netwerk/test/unit/test_ping_aboutnetworking.js
@@ -0,0 +1,105 @@
+/* -*- 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].tcp, 1);
+
+ do_test_finished();
+ });
+}
+
+function run_test() {
+ var ps = Cc["@mozilla.org/preferences-service;1"].getService(
+ Ci.nsIPrefBranch
+ );
+ // 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_pinned_app_cache.js b/netwerk/test/unit/test_pinned_app_cache.js
new file mode 100644
index 0000000000..db8d9d6f04
--- /dev/null
+++ b/netwerk/test/unit/test_pinned_app_cache.js
@@ -0,0 +1,302 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/*
+ * This testcase performs 3 requests against the offline cache. They
+ * are
+ *
+ * - start_cache_nonpinned_app1()
+ *
+ * - Request nsOfflineCacheService to skip pages (4) of app1 on
+ * the cache storage.
+ *
+ * - The offline cache storage is empty at this monent.
+ *
+ * - start_cache_nonpinned_app2_for_partial()
+ *
+ * - Request nsOfflineCacheService to skip pages of app2 on the
+ * cache storage.
+ *
+ * - The offline cache storage has only enough space for one more
+ * additional page. Only first of pages is really in the cache.
+ *
+ * - start_cache_pinned_app2_for_success()
+ *
+ * - Request nsOfflineCacheService to skip pages of app2 on the
+ * cache storage.
+ *
+ * - The offline cache storage has only enough space for one
+ * additional page. But, this is a pinned request,
+ * nsOfflineCacheService will make more space for this request
+ * by discarding app1 (non-pinned)
+ *
+ */
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+const { PermissionTestUtils } = ChromeUtils.import(
+ "resource://testing-common/PermissionTestUtils.jsm"
+);
+
+// const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+const kNS_OFFLINECACHEUPDATESERVICE_CONTRACTID =
+ "@mozilla.org/offlinecacheupdate-service;1";
+
+const kManifest1 =
+ "CACHE MANIFEST\n" +
+ "/pages/foo1\n" +
+ "/pages/foo2\n" +
+ "/pages/foo3\n" +
+ "/pages/foo4\n";
+const kManifest2 =
+ "CACHE MANIFEST\n" +
+ "/pages/foo5\n" +
+ "/pages/foo6\n" +
+ "/pages/foo7\n" +
+ "/pages/foo8\n";
+
+const kDataFileSize = 1024; // file size for each content page
+const kCacheSize = kDataFileSize * 5; // total space for offline cache storage
+
+XPCOMUtils.defineLazyGetter(this, "kHttpLocation", function() {
+ return "http://localhost:" + httpServer.identity.primaryPort + "/";
+});
+
+XPCOMUtils.defineLazyGetter(this, "kHttpLocation_ip", function() {
+ return "http://127.0.0.1:" + httpServer.identity.primaryPort + "/";
+});
+
+function manifest1_handler(metadata, response) {
+ info("manifest1\n");
+ response.setHeader("content-type", "text/cache-manifest");
+
+ response.write(kManifest1);
+}
+
+function manifest2_handler(metadata, response) {
+ info("manifest2\n");
+ response.setHeader("content-type", "text/cache-manifest");
+
+ response.write(kManifest2);
+}
+
+function app_handler(metadata, response) {
+ info("app_handler\n");
+ response.setHeader("content-type", "text/html");
+
+ response.write("<html></html>");
+}
+
+function datafile_handler(metadata, response) {
+ info("datafile_handler\n");
+ let data = "";
+
+ while (data.length < kDataFileSize) {
+ data =
+ data +
+ Math.random()
+ .toString(36)
+ .substring(2, 15);
+ }
+
+ response.setHeader("content-type", "text/plain");
+ response.write(data.substring(0, kDataFileSize));
+}
+
+var httpServer;
+
+function init_profile() {
+ var ps = Cc["@mozilla.org/preferences-service;1"].getService(
+ Ci.nsIPrefBranch
+ );
+ dump(ps.getBoolPref("browser.cache.offline.enable"));
+ ps.setBoolPref("browser.cache.offline.enable", true);
+ ps.setBoolPref("browser.cache.offline.storage.enable", true);
+ ps.setComplexValue(
+ "browser.cache.offline.parent_directory",
+ Ci.nsIFile,
+ do_get_profile()
+ );
+}
+
+function init_http_server() {
+ httpServer = new HttpServer();
+ httpServer.registerPathHandler("/app1", app_handler);
+ httpServer.registerPathHandler("/app2", app_handler);
+ httpServer.registerPathHandler("/app1.appcache", manifest1_handler);
+ httpServer.registerPathHandler("/app2.appcache", manifest2_handler);
+ for (let i = 1; i <= 8; i++) {
+ httpServer.registerPathHandler("/pages/foo" + i, datafile_handler);
+ }
+ httpServer.start(-1);
+}
+
+function init_cache_capacity() {
+ let prefs = Cc["@mozilla.org/preferences-service;1"].getService(
+ Ci.nsIPrefBranch
+ );
+ prefs.setIntPref("browser.cache.offline.capacity", kCacheSize / 1024);
+}
+
+function clean_app_cache() {
+ evict_cache_entries("appcache");
+}
+
+function do_app_cache(manifestURL, pageURL, pinned) {
+ let update_service = Cc[kNS_OFFLINECACHEUPDATESERVICE_CONTRACTID].getService(
+ Ci.nsIOfflineCacheUpdateService
+ );
+
+ PermissionTestUtils.add(
+ manifestURL,
+ "pin-app",
+ pinned
+ ? Ci.nsIPermissionManager.ALLOW_ACTION
+ : Ci.nsIPermissionManager.DENY_ACTION
+ );
+
+ let update = update_service.scheduleUpdate(
+ manifestURL,
+ pageURL,
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ null
+ ); /* no window */
+
+ return update;
+}
+
+function watch_update(update, stateChangeHandler, cacheAvailHandler) {
+ let observer = {
+ QueryInterface: ChromeUtils.generateQI([]),
+
+ updateStateChanged: stateChangeHandler,
+ applicationCacheAvailable: cacheAvailHandler,
+ };
+ update.addObserver(observer);
+
+ return update;
+}
+
+function start_and_watch_app_cache(
+ manifestURL,
+ pageURL,
+ pinned,
+ stateChangeHandler,
+ cacheAvailHandler
+) {
+ let ioService = Cc["@mozilla.org/network/io-service;1"].getService(
+ Ci.nsIIOService
+ );
+ let update = do_app_cache(
+ ioService.newURI(manifestURL),
+ ioService.newURI(pageURL),
+ pinned
+ );
+ watch_update(update, stateChangeHandler, cacheAvailHandler);
+ return update;
+}
+
+const {
+ STATE_FINISHED: STATE_FINISHED,
+ STATE_CHECKING: STATE_CHECKING,
+ STATE_ERROR: STATE_ERROR,
+} = Ci.nsIOfflineCacheUpdateObserver;
+
+/*
+ * Start caching app1 as a non-pinned app.
+ */
+function start_cache_nonpinned_app() {
+ info("Start non-pinned App1");
+ start_and_watch_app_cache(
+ kHttpLocation + "app1.appcache",
+ kHttpLocation + "app1",
+ false,
+ function(update, state) {
+ switch (state) {
+ case STATE_FINISHED:
+ start_cache_nonpinned_app2_for_partial();
+ break;
+
+ case STATE_ERROR:
+ do_throw("App1 cache state = " + state);
+ break;
+ }
+ },
+ function(appcache) {
+ info("app1 avail " + appcache + "\n");
+ }
+ );
+}
+
+/*
+ * Start caching app2 as a non-pinned app.
+ *
+ * This cache request is supposed to be saved partially in the cache
+ * storage for running out of the cache storage. The offline cache
+ * storage can hold 5 files at most. (kDataFileSize bytes for each
+ * file)
+ */
+function start_cache_nonpinned_app2_for_partial() {
+ info("Start non-pinned App2 for partial\n");
+ start_and_watch_app_cache(
+ kHttpLocation_ip + "app2.appcache",
+ kHttpLocation_ip + "app2",
+ false,
+ function(update, state) {
+ switch (state) {
+ case STATE_FINISHED:
+ start_cache_pinned_app2_for_success();
+ break;
+
+ case STATE_ERROR:
+ do_throw("App2 cache state = " + state);
+ break;
+ }
+ },
+ function(appcahe) {}
+ );
+}
+
+/*
+ * Start caching app2 as a pinned app.
+ *
+ * This request use IP address (127.0.0.1) as the host name instead of
+ * the one used by app1. Because, app1 is also pinned when app2 is
+ * pinned if they have the same host name (localhost).
+ */
+function start_cache_pinned_app2_for_success() {
+ let error_count = [0];
+ info("Start pinned App2 for success\n");
+ start_and_watch_app_cache(
+ kHttpLocation_ip + "app2.appcache",
+ kHttpLocation_ip + "app2",
+ true,
+ function(update, state) {
+ switch (state) {
+ case STATE_FINISHED:
+ Assert.ok(error_count[0] == 0, "Do not discard app1?");
+ httpServer.stop(do_test_finished);
+ break;
+
+ case STATE_ERROR:
+ info("STATE_ERROR\n");
+ error_count[0]++;
+ break;
+ }
+ },
+ function(appcache) {
+ info("app2 avail " + appcache + "\n");
+ }
+ );
+}
+
+function run_test() {
+ if (typeof _XPCSHELL_PROCESS == "undefined" || _XPCSHELL_PROCESS != "child") {
+ init_profile();
+ init_cache_capacity();
+ clean_app_cache();
+ }
+
+ init_http_server();
+ start_cache_nonpinned_app();
+ do_test_pending();
+}
diff --git a/netwerk/test/unit/test_plaintext_sniff.js b/netwerk/test/unit/test_plaintext_sniff.js
new file mode 100644
index 0000000000..fb9620e04c
--- /dev/null
+++ b/netwerk/test/unit/test_plaintext_sniff.js
@@ -0,0 +1,209 @@
+// Test the plaintext-or-binary sniffer
+
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+// 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..537d8d6f46
--- /dev/null
+++ b/netwerk/test/unit/test_port_remapping.js
@@ -0,0 +1,48 @@
+// 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.import("resource://testing-common/httpd.js");
+
+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..9424d53538
--- /dev/null
+++ b/netwerk/test/unit/test_post.js
@@ -0,0 +1,139 @@
+//
+// POST test
+//
+
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+XPCOMUtils.defineLazyGetter(this, "URL", function() {
+ return "http://localhost:" + httpserver.identity.primaryPort;
+});
+
+var httpserver = new HttpServer();
+var testpath = "/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..9f9474a6cb
--- /dev/null
+++ b/netwerk/test/unit/test_predictor.js
@@ -0,0 +1,771 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const ReferrerInfo = Components.Constructor(
+ "@mozilla.org/referrer-info;1",
+ "nsIReferrerInfo",
+ "init"
+);
+
+var running_single_process = false;
+
+var predictor = null;
+
+function is_child_process() {
+ return (
+ Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime)
+ .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, appCache) {
+ return Ci.nsICacheEntryOpenCallback.ENTRY_WANTED;
+ },
+
+ onCacheEntryAvailable(entry, isnew, appCache, 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, appCache) {
+ return Ci.nsICacheEntryOpenCallback.ENTRY_WANTED;
+ },
+
+ onCacheEntryAvailable(entry, isNew, appCache, 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,
+ false
+ );
+
+ 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
+ );
+ });
+ });
+ });
+ });
+ });
+ });
+}
+
+function test_redirect() {
+ open_and_continue([redirect_inituri, redirect_targeturi], function() {
+ if (running_single_process) {
+ continue_test_redirect();
+ } else {
+ sendCommand("continue_test_redirect();");
+ }
+ });
+}
+
+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 origin = extract_origin(sruri);
+ if (!preconns.includes(origin)) {
+ preconns.push(origin);
+ }
+
+ sruri = newURI(subresources[2]);
+ 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);
+ }
+
+ 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 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,
+ // 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..baf79ecbf5
--- /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..bf8bae9201
--- /dev/null
+++ b/netwerk/test/unit/test_private_necko_channel.js
@@ -0,0 +1,56 @@
+//
+// Private channel test
+//
+
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+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(
+ count
+ ) {
+ Assert.equal(count, 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..4aa8256f22
--- /dev/null
+++ b/netwerk/test/unit/test_progress.js
@@ -0,0 +1,132 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+XPCOMUtils.defineLazyGetter(this, "URL", function() {
+ return "http://localhost:" + httpserver.identity.primaryPort;
+});
+
+var httpserver = new HttpServer();
+var testpath = "/simple";
+var httpbody = "0123456789";
+
+var last = 0,
+ max = 0;
+
+const STATUS_RECEIVING_FROM = 0x804b0006;
+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 = {
+ _listener: null,
+ _got_onstartrequest: false,
+ _got_onstatus_after_onstartrequest: false,
+ _last_callback_handled: 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;
+ },
+
+ 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, "localhost");
+ this.mStatus = status;
+ },
+
+ mStatus: 0,
+};
+
+function run_test() {
+ httpserver.registerPathHandler(testpath, serverHandler);
+ httpserver.start(-1);
+ var channel = setupChannel(testpath);
+ channel.asyncOpen(progressCallback);
+ do_test_pending();
+}
+
+function setupChannel(path) {
+ var chan = NetUtil.newChannel({
+ uri: URL + path,
+ loadUsingSystemPrincipal: true,
+ });
+ chan.QueryInterface(Ci.nsIHttpChannel);
+ chan.requestMethod = "GET";
+ chan.notificationCallbacks = progressCallback;
+ return chan;
+}
+
+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);
+ httpserver.stop(do_test_finished);
+}
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..cf41769486
--- /dev/null
+++ b/netwerk/test/unit/test_protocolproxyservice-async-filters.js
@@ -0,0 +1,440 @@
+/* -*- 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 ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
+var pps = Cc["@mozilla.org/network/protocol-proxy-service;1"].getService();
+var prefs = Cc["@mozilla.org/preferences-service;1"].getService(
+ Ci.nsIPrefBranch
+);
+
+/**
+ * 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(delegate, iid) {
+ return new TestProtocolHandler().QueryInterface(iid);
+ },
+ lockFactory(lock) {},
+};
+
+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..d72d552fc8
--- /dev/null
+++ b/netwerk/test/unit/test_protocolproxyservice.js
@@ -0,0 +1,1049 @@
+/* -*- 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 = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
+var pps = Cc["@mozilla.org/network/protocol-proxy-service;1"].getService();
+var prefs = Cc["@mozilla.org/preferences-service;1"].getService(
+ Ci.nsIPrefBranch
+);
+
+/**
+ * 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(delegate, iid) {
+ return new TestProtocolHandler().QueryInterface(iid);
+ },
+ lockFactory(lock) {},
+};
+
+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 protocol_handler_test_1(pi) {
+ Assert.equal(pi, null);
+ prefs.setCharPref("network.proxy.autoconfig_url", "");
+ prefs.setIntPref("network.proxy.type", 0);
+
+ run_pac_cancel_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 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;
+
+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 chan = NetUtil.newChannel({
+ uri: "http://127.0.0.1:7247",
+ loadUsingSystemPrincipal: true,
+ });
+ 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
+ ) {
+ 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);
+ do_test_finished();
+}
+
+function run_test() {
+ register_test_protocol_handler();
+
+ // 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..fea774ba74
--- /dev/null
+++ b/netwerk/test/unit/test_proxy-failover_canceled.js
@@ -0,0 +1,58 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+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 prefserv = Cc["@mozilla.org/preferences-service;1"].getService(
+ Ci.nsIPrefService
+ );
+ var prefs = prefserv.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..47a41ab2ff
--- /dev/null
+++ b/netwerk/test/unit/test_proxy-failover_passing.js
@@ -0,0 +1,46 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+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 prefserv = Cc["@mozilla.org/preferences-service;1"].getService(
+ Ci.nsIPrefService
+ );
+ var prefs = prefserv.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..586ddb2d35
--- /dev/null
+++ b/netwerk/test/unit/test_proxy-replace_canceled.js
@@ -0,0 +1,58 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+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 prefserv = Cc["@mozilla.org/preferences-service;1"].getService(
+ Ci.nsIPrefService
+ );
+ var prefs = prefserv.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..d27394d339
--- /dev/null
+++ b/netwerk/test/unit/test_proxy-replace_passing.js
@@ -0,0 +1,46 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+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 prefserv = Cc["@mozilla.org/preferences-service;1"].getService(
+ Ci.nsIPrefService
+ );
+ var prefs = prefserv.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_proxyconnect.js b/netwerk/test/unit/test_proxyconnect.js
new file mode 100644
index 0000000000..71120232c5
--- /dev/null
+++ b/netwerk/test/unit/test_proxyconnect.js
@@ -0,0 +1,358 @@
+/* -*- 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(socket, 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);
+
+ streamIn.asyncWait(connectHandler, 0, 0, threadManager.mainThread);
+ } catch (e) {
+ transport.close(Cr.NS_BINDING_ABORTED);
+ do_throw(e);
+ }
+}
+
+function stopListening(socket, status) {
+ 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 == 0) {
+ 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..e85301317b
--- /dev/null
+++ b/netwerk/test/unit/test_psl.js
@@ -0,0 +1,45 @@
+"use strict";
+
+var etld = Cc["@mozilla.org/network/effective-tld-service;1"].getService(
+ Ci.nsIEffectiveTLDService
+);
+
+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 ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
+ var uri = ios.newFileURI(file);
+ var scriptLoader = Cc["@mozilla.org/moz/jssubscript-loader;1"].getService(
+ Ci.mozIJSSubScriptLoader
+ );
+ var srvScope = {};
+ scriptLoader.loadSubScript(uri.spec, srvScope);
+}
+
+function checkPublicSuffix(host, expectedSuffix) {
+ var actualSuffix = null;
+ try {
+ actualSuffix = 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..9944988dda
--- /dev/null
+++ b/netwerk/test/unit/test_race_cache_with_network.js
@@ -0,0 +1,284 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+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");
+
+ try {
+ var etag = metadata.getHeader("If-None-Match");
+ } catch (ex) {
+ var 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.
+ var 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.
+ var 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.
+ var 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.
+ var 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.
+ var channel = make_channel("http://localhost:" + PORT + "/rcwn");
+ channel
+ .QueryInterface(Ci.nsIRaceCacheWithNetwork)
+ .test_delayCacheEntryOpeningBy(100000);
+ channel.asyncOpen(new ChannelListener(checkContent, null));
+ // Trigger network after 50 ms.
+ channel.QueryInterface(Ci.nsIRaceCacheWithNetwork).test_triggerNetwork(50);
+ 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.
+ var 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_triggerNetwork(0);
+ executeSoon(() => {
+ channel
+ .QueryInterface(Ci.nsIRaceCacheWithNetwork)
+ .test_triggerDelayedOpenCacheEntry();
+ });
+ });
+ 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.
+ var 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();
+ channel.QueryInterface(Ci.nsIRaceCacheWithNetwork).test_triggerNetwork(0);
+ });
+ yield undefined;
+ equal(gResponseCounter, 7);
+ equal(g200Counter, 3, "check number of 200 responses");
+ equal(g304Counter, 4, "check number of 304 responses");
+
+ // Load the cached handler so we don't need to revalidate
+ var 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
+ var 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;
+ var channel = make_channel("http://localhost:" + PORT + "/rcwn_cached");
+ channel
+ .QueryInterface(Ci.nsIRaceCacheWithNetwork)
+ .test_delayCacheEntryOpeningBy(100000);
+ channel.asyncOpen(new ChannelListener(checkContent, null));
+ // trigger network after 50 ms
+ channel.QueryInterface(Ci.nsIRaceCacheWithNetwork).test_triggerNetwork(50);
+ 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.
+ var channel = make_channel("http://localhost:" + PORT + "/rcwn_cached");
+ channel
+ .QueryInterface(Ci.nsIRaceCacheWithNetwork)
+ .test_delayCacheEntryOpeningBy(100000);
+ channel.asyncOpen(new ChannelListener(checkContent, null));
+ do_timeout(50, function() {
+ channel.QueryInterface(Ci.nsIRaceCacheWithNetwork).test_triggerNetwork(0);
+ channel
+ .QueryInterface(Ci.nsIRaceCacheWithNetwork)
+ .test_triggerDelayedOpenCacheEntry();
+ });
+ 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++) {
+ var channel = make_channel("http://localhost:" + PORT + "/rcwn_cached");
+ channel
+ .QueryInterface(Ci.nsIRaceCacheWithNetwork)
+ .test_delayCacheEntryOpeningBy(100000);
+ channel.asyncOpen(new ChannelListener(checkContent, null));
+ channel.QueryInterface(Ci.nsIRaceCacheWithNetwork).test_triggerNetwork(10);
+ // This may be racy. The delay was chosen because the distribution of net-cache
+ // results was around 25-25 on my machine.
+ do_timeout(i * 100, function() {
+ try {
+ channel
+ .QueryInterface(Ci.nsIRaceCacheWithNetwork)
+ .test_triggerDelayedOpenCacheEntry();
+ } catch (e) {}
+ });
+
+ 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..a6c50287c0
--- /dev/null
+++ b/netwerk/test/unit/test_range_requests.js
@@ -0,0 +1,527 @@
+//
+// 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.import("resource://testing-common/httpd.js");
+
+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 decodedBody = [
+ 0x54,
+ 0x68,
+ 0x69,
+ 0x73,
+ 0x20,
+ 0x69,
+ 0x73,
+ 0x20,
+ 0x61,
+ 0x20,
+ 0x73,
+ 0x6c,
+ 0x69,
+ 0x67,
+ 0x68,
+ 0x74,
+ 0x6c,
+ 0x79,
+ 0x20,
+ 0x6c,
+ 0x6f,
+ 0x6e,
+ 0x67,
+ 0x65,
+ 0x72,
+ 0x20,
+ 0x74,
+ 0x65,
+ 0x73,
+ 0x74,
+ 0x0a,
+ 0x0a,
+];
+
+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) {
+ 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) {},
+
+ 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
+ var 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
+ var 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
+ var chan = make_channel("http://localhost:" + port + "/test_4");
+ chan.asyncOpen(new MyListener(received_partial_4));
+
+ // Case 5: conditional request-header set by client
+ var 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)
+ var chan = make_channel("http://localhost:" + port + "/test_6");
+ chan.asyncOpen(new MyListener(received_partial_6));
+
+ // Case 7: a basic positive test
+ var 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
+ var 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
+ var 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..b8cc069126
--- /dev/null
+++ b/netwerk/test/unit/test_rcwn_always_cache_new_content.js
@@ -0,0 +1,115 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+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");
+}
+
+let gIsFromCache = 0;
+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.
+ var 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.
+ var channel = make_channel("http://localhost:" + PORT + "/rcwn");
+ channel
+ .QueryInterface(Ci.nsIRaceCacheWithNetwork)
+ .test_delayCacheEntryOpeningBy(100000);
+ channel.asyncOpen(new ChannelListener(checkContent, null));
+ // Trigger network after 50 ms.
+ channel.QueryInterface(Ci.nsIRaceCacheWithNetwork).test_triggerNetwork(50);
+ yield undefined;
+ equal(gRequestCounter, 2);
+
+ // Simulate navigation back by specifying VALIDATE_NEVER flag.
+ var 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_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..c6a817ed5a
--- /dev/null
+++ b/netwerk/test/unit/test_redirect-caching_canceled.js
@@ -0,0 +1,62 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+XPCOMUtils.defineLazyGetter(this, "URL", function() {
+ return "http://localhost:" + httpServer.identity.primaryPort;
+});
+
+var httpServer = null;
+// Need to randomize, because apparently no one clears our cache
+var randomPath = "/redirect/" + Math.random();
+
+XPCOMUtils.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..2fe3643a8b
--- /dev/null
+++ b/netwerk/test/unit/test_redirect-caching_failure.js
@@ -0,0 +1,82 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+/*
+ * 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 (
+ Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime)
+ .processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT
+ );
+}
+
+XPCOMUtils.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();
+
+XPCOMUtils.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..fb3cb311ab
--- /dev/null
+++ b/netwerk/test/unit/test_redirect-caching_passing.js
@@ -0,0 +1,54 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+XPCOMUtils.defineLazyGetter(this, "URL", function() {
+ return "http://localhost:" + httpserver.identity.primaryPort;
+});
+
+var httpserver = null;
+// Need to randomize, because apparently no one clears our cache
+var randomPath = "/redirect/" + Math.random();
+
+XPCOMUtils.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..fd3c47197c
--- /dev/null
+++ b/netwerk/test/unit/test_redirect_baduri.js
@@ -0,0 +1,42 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+/*
+ * Test whether we fail bad URIs in HTTP redirect as CORRUPTED_CONTENT.
+ */
+
+var httpServer = null;
+
+var BadRedirectPath = "/BadRedirect";
+XPCOMUtils.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..fc0f0f268a
--- /dev/null
+++ b/netwerk/test/unit/test_redirect_canceled.js
@@ -0,0 +1,48 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+XPCOMUtils.defineLazyGetter(this, "URL", function() {
+ return "http://localhost:" + httpServer.identity.primaryPort;
+});
+
+var httpServer = null;
+// Need to randomize, because apparently no one clears our cache
+var randomPath = "/redirect/" + Math.random();
+
+XPCOMUtils.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..baafdf021a
--- /dev/null
+++ b/netwerk/test/unit/test_redirect_different-protocol.js
@@ -0,0 +1,54 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+XPCOMUtils.defineLazyGetter(this, "URL", function() {
+ return "http://localhost:" + httpServer.identity.primaryPort;
+});
+
+var httpServer = null;
+// Need to randomize, because apparently no one clears our cache
+var randomPath = "/redirect/" + Math.random();
+
+XPCOMUtils.defineLazyGetter(this, "randomURI", function() {
+ return URL + randomPath;
+});
+
+function inChildProcess() {
+ return (
+ Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime)
+ .processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT
+ );
+}
+
+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..e9b85d1f46
--- /dev/null
+++ b/netwerk/test/unit/test_redirect_failure.js
@@ -0,0 +1,60 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+/*
+ * 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 (
+ Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime)
+ .processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT
+ );
+}
+
+XPCOMUtils.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();
+
+XPCOMUtils.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..7fc63a40ae
--- /dev/null
+++ b/netwerk/test/unit/test_redirect_from_script.js
@@ -0,0 +1,249 @@
+/*
+ * 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.import("resource://testing-common/httpd.js");
+
+// 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;
+
+XPCOMUtils.defineLazyGetter(this, "port1", function() {
+ return httpServer.identity.primaryPort;
+});
+
+XPCOMUtils.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";
+XPCOMUtils.defineLazyGetter(this, "baitURI", function() {
+ return "http://localhost:" + port1 + baitPath;
+});
+var baitText = "you got the worm";
+
+var redirectedPath = "/switch";
+XPCOMUtils.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";
+XPCOMUtils.defineLazyGetter(this, "bait2URI", function() {
+ return "http://localhost:" + port1 + bait2Path;
+});
+
+XPCOMUtils.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";
+XPCOMUtils.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";
+XPCOMUtils.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() {
+ Cc["@mozilla.org/observer-service;1"]
+ .getService(Ci.nsIObserverService)
+ .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 ioservice = Cc["@mozilla.org/network/io-service;1"].getService(
+ Ci.nsIIOService
+ );
+ 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 = ioservice.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);
+ });
+}
+
+var redirector = 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..586a6c0966
--- /dev/null
+++ b/netwerk/test/unit/test_redirect_from_script_after-open_passing.js
@@ -0,0 +1,249 @@
+/*
+ * 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.import("resource://testing-common/httpd.js");
+
+// 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;
+
+XPCOMUtils.defineLazyGetter(this, "port1", function() {
+ return httpServer.identity.primaryPort;
+});
+
+XPCOMUtils.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";
+XPCOMUtils.defineLazyGetter(this, "baitURI", function() {
+ return "http://localhost:" + port1 + baitPath;
+});
+var baitText = "you got the worm";
+
+var redirectedPath = "/switch";
+XPCOMUtils.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";
+XPCOMUtils.defineLazyGetter(this, "bait2URI", function() {
+ return "http://localhost:" + port1 + bait2Path;
+});
+
+XPCOMUtils.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";
+XPCOMUtils.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";
+XPCOMUtils.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() {
+ Cc["@mozilla.org/observer-service;1"]
+ .getService(Ci.nsIObserverService)
+ .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 ioservice = Cc["@mozilla.org/network/io-service;1"].getService(
+ Ci.nsIIOService
+ );
+ 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 = ioservice.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);
+ });
+}
+
+var redirector = 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..a4d7974115
--- /dev/null
+++ b/netwerk/test/unit/test_redirect_history.js
@@ -0,0 +1,74 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+const ReferrerInfo = Components.Constructor(
+ "@mozilla.org/referrer-info;1",
+ "nsIReferrerInfo",
+ "init"
+);
+
+XPCOMUtils.defineLazyGetter(this, "URL", function() {
+ return "http://localhost:" + httpServer.identity.primaryPort;
+});
+
+var httpServer = null;
+var randomPath = "/redirect/" + Math.random();
+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..08ef96a2cb
--- /dev/null
+++ b/netwerk/test/unit/test_redirect_loop.js
@@ -0,0 +1,86 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+/*
+ * 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..3f4178663a
--- /dev/null
+++ b/netwerk/test/unit/test_redirect_passing.js
@@ -0,0 +1,53 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+XPCOMUtils.defineLazyGetter(this, "URL", function() {
+ return "http://localhost:" + httpServer.identity.primaryPort;
+});
+
+var httpServer = null;
+// Need to randomize, because apparently no one clears our cache
+var randomPath = "/redirect/" + Math.random();
+
+XPCOMUtils.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..c127f7ffe1
--- /dev/null
+++ b/netwerk/test/unit/test_redirect_protocol_telemetry.js
@@ -0,0 +1,66 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+const { TelemetryTestUtils } = ChromeUtils.import(
+ "resource://testing-common/TelemetryTestUtils.jsm"
+);
+
+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_reentrancy.js b/netwerk/test/unit/test_reentrancy.js
new file mode 100644
index 0000000000..f06b9b2c44
--- /dev/null
+++ b/netwerk/test/unit/test_reentrancy.js
@@ -0,0 +1,107 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+XPCOMUtils.defineLazyGetter(this, "URL", function() {
+ return "http://localhost:" + httpserver.identity.primaryPort;
+});
+
+var httpserver = new HttpServer();
+var testpath = "/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..ecb67983bc
--- /dev/null
+++ b/netwerk/test/unit/test_referrer.js
@@ -0,0 +1,247 @@
+"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 = Cc["@mozilla.org/preferences-service;1"].getService(
+ Ci.nsIPrefBranch
+ );
+
+ 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), referer_uri);
+
+ // 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..c6b69a1050
--- /dev/null
+++ b/netwerk/test/unit/test_referrer_cross_origin.js
@@ -0,0 +1,319 @@
+/* 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);
+ }
+
+ 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");
+}
diff --git a/netwerk/test/unit/test_referrer_policy.js b/netwerk/test/unit/test_referrer_policy.js
new file mode 100644
index 0000000000..987d3b4bc1
--- /dev/null
+++ b/netwerk/test/unit/test_referrer_policy.js
@@ -0,0 +1,143 @@
+"use strict";
+
+const ReferrerInfo = Components.Constructor(
+ "@mozilla.org/referrer-info;1",
+ "nsIReferrerInfo",
+ "init"
+);
+
+function test_policy(test) {
+ info("Running test: " + test.toSource());
+
+ var prefs = Cc["@mozilla.org/preferences-service;1"].getService(
+ Ci.nsIPrefBranch
+ );
+ if (test.defaultReferrerPolicyPref !== undefined) {
+ prefs.setIntPref(
+ "network.http.referer.defaultPolicy",
+ test.defaultReferrerPolicyPref
+ );
+ } else {
+ prefs.setIntPref("network.http.referer.defaultPolicy", 3);
+ }
+
+ 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));
+}
diff --git a/netwerk/test/unit/test_reopen.js b/netwerk/test/unit/test_reopen.js
new file mode 100644
index 0000000000..74c20e9947
--- /dev/null
+++ b/netwerk/test/unit/test_reopen.js
@@ -0,0 +1,147 @@
+// This testcase verifies that channels can't be reopened
+// See https://bugzilla.mozilla.org/show_bug.cgi?id=372486
+
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+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,
+ // Commented by default as it relies on external ressources
+ //test_ftp_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) {
+ var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
+ return NetUtil.newChannel({
+ uri: ios.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);
+ });
+}
+
+// Uncomment test_ftp_channel in test_array to test this
+function test_ftp_channel() {
+ test_channel(function() {
+ return makeChan("ftp://ftp.mozilla.org/pub/mozilla.org/README");
+ });
+}
+
+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..2ad2c9a150
--- /dev/null
+++ b/netwerk/test/unit/test_reply_without_content_type.js
@@ -0,0 +1,150 @@
+//
+// 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.import("resource://testing-common/httpd.js");
+
+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 buffer = "";
+
+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..68ebf5d404
--- /dev/null
+++ b/netwerk/test/unit/test_resumable_channel.js
@@ -0,0 +1,424 @@
+/* Tests various aspects of nsIResumableChannel in combination with HTTP */
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+XPCOMUtils.defineLazyGetter(this, "URL", function() {
+ return "http://localhost:" + httpserver.identity.primaryPort;
+});
+
+var httpserver = null;
+
+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 0x80004001;
+ },
+};
+
+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..72785a5b4b
--- /dev/null
+++ b/netwerk/test/unit/test_resumable_truncate.js
@@ -0,0 +1,93 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+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_safeoutputstream.js b/netwerk/test/unit/test_safeoutputstream.js
new file mode 100644
index 0000000000..e9aec3beed
--- /dev/null
+++ b/netwerk/test/unit/test_safeoutputstream.js
@@ -0,0 +1,72 @@
+/* -*- 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 = Cc["@mozilla.org/file/directory_service;1"]
+ .getService(Ci.nsIProperties)
+ .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..4ee0b08be2
--- /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.cookiemgr.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.cookiemgr.countCookiesFromHost("foo.com"), 20);
+
+ // 2) All expired, unique cookies exist.
+ Assert.equal(Services.cookiemgr.countCookiesFromHost("bar.com"), 20);
+
+ // 3) Only one cookie remains, and it's the one with the highest expiration
+ // time.
+ Assert.equal(Services.cookiemgr.countCookiesFromHost("baz.com"), 1);
+ let cookies = Services.cookiemgr.getCookiesFromHost("baz.com", {});
+ let cookie = cookies[0];
+ Assert.equal(cookie.expiry, futureExpiry + 40);
+
+ finish_test();
+}
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..3d9698060a
--- /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.cookiemgr.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.cookiemgr.countCookiesFromHost("foo.com"), 20);
+
+ // 2) All expired, unique cookies exist.
+ Assert.equal(Services.cookiemgr.countCookiesFromHost("bar.com"), 20);
+
+ // 3) Only one cookie remains, and it's the one with the highest expiration
+ // time.
+ Assert.equal(Services.cookiemgr.countCookiesFromHost("baz.com"), 1);
+ let cookies = Services.cookiemgr.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) {
+ let cookie = new Cookie(
+ "oh" + i,
+ "hai",
+ "foo.com",
+ "/",
+ futureExpiry,
+ now,
+ now + i,
+ false,
+ false,
+ false
+ );
+
+ schema2db.insertCookie(cookie);
+ }
+ for (let i = 80; i < 100; ++i) {
+ let cookie = new Cookie(
+ "oh" + i,
+ "hai",
+ "cat.com",
+ "/",
+ futureExpiry,
+ now,
+ now + i,
+ false,
+ false,
+ false
+ );
+
+ schema2db.insertCookie(cookie);
+ }
+
+ // 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.cookiemgr.countCookiesFromHost("foo.com"), 40);
+ Assert.equal(Services.cookiemgr.countCookiesFromHost("bar.com"), 20);
+ Assert.equal(Services.cookiemgr.countCookiesFromHost("baz.com"), 1);
+ Assert.equal(Services.cookiemgr.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.cookiemgr.countCookiesFromHost("foo.com"), 40);
+ Assert.equal(Services.cookiemgr.countCookiesFromHost("bar.com"), 20);
+ Assert.equal(Services.cookiemgr.countCookiesFromHost("baz.com"), 1);
+ Assert.equal(Services.cookiemgr.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.cookiemgr.countCookiesFromHost("foo.com"), 40);
+ Assert.equal(Services.cookiemgr.countCookiesFromHost("bar.com"), 20);
+ Assert.equal(Services.cookiemgr.countCookiesFromHost("baz.com"), 1);
+ Assert.equal(Services.cookiemgr.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..46230b403f
--- /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.cookiemgr.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.cookiemgr.countCookiesFromHost("foo.com"), 20);
+
+ // 2) All expired, unique cookies exist.
+ Assert.equal(Services.cookiemgr.countCookiesFromHost("bar.com"), 20);
+
+ // 3) Only one cookie remains, and it's the one with the highest expiration
+ // time.
+ Assert.equal(Services.cookiemgr.countCookiesFromHost("baz.com"), 1);
+ let cookies = Services.cookiemgr.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..6c3d777a59
--- /dev/null
+++ b/netwerk/test/unit/test_separate_connections.js
@@ -0,0 +1,102 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+XPCOMUtils.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_signature_extraction.js b/netwerk/test/unit/test_signature_extraction.js
new file mode 100644
index 0000000000..c2da8b568a
--- /dev/null
+++ b/netwerk/test/unit/test_signature_extraction.js
@@ -0,0 +1,215 @@
+/* -*- 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.defineModuleGetter(
+ this,
+ "FileUtils",
+ "resource://gre/modules/FileUtils.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ this,
+ "NetUtil",
+ "resource://gre/modules/NetUtil.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ this,
+ "FileTestUtils",
+ "resource://testing-common/FileTestUtils.jsm"
+);
+
+const BackgroundFileSaverOutputStream = Components.Constructor(
+ "@mozilla.org/network/background-file-saver;1?mode=outputstream",
+ "nsIBackgroundFileSaver"
+);
+
+const 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(aSaver, aTarget) {
+ if (aOnTargetChangeFn) {
+ aOnTargetChangeFn(aTarget);
+ }
+ },
+ onSaveComplete: function BFSO_onSaveComplete(aSaver, aStatus) {
+ if (Components.isSuccessCode(aStatus)) {
+ resolve();
+ } else {
+ reject(new Components.Exception("Saver failed.", aStatus));
+ }
+ },
+ };
+ });
+}
+
+/**
+ * Feeds a string to a BackgroundFileSaverOutputStream.
+ *
+ * @param aSourceString
+ * The source data to copy.
+ * @param aSaverOutputStream
+ * The BackgroundFileSaverOutputStream to feed.
+ * @param aCloseWhenDone
+ * If true, the output stream will be closed when the copy finishes.
+ *
+ * @return {Promise}
+ * @resolves When the copy completes with a success code.
+ * @rejects With an exception, if the copy fails.
+ */
+function promiseCopyToSaver(aSourceString, aSaverOutputStream, aCloseWhenDone) {
+ return new Promise((resolve, reject) => {
+ let inputStream = new StringInputStream(
+ aSourceString,
+ aSourceString.length
+ );
+ let copier = Cc[
+ "@mozilla.org/network/async-stream-copier;1"
+ ].createInstance(Ci.nsIAsyncStreamCopier);
+ copier.init(
+ inputStream,
+ aSaverOutputStream,
+ null,
+ false,
+ true,
+ 0x8000,
+ true,
+ aCloseWhenDone
+ );
+ copier.asyncCopy(
+ {
+ onStartRequest() {},
+ onStopRequest(aRequest, 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..30fcddebb3
--- /dev/null
+++ b/netwerk/test/unit/test_simple.js
@@ -0,0 +1,69 @@
+//
+// Simple HTTP test: fetches page
+//
+
+// Note: sets Cc and Ci variables
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+var httpserver = new HttpServer();
+var testpath = "/simple";
+var httpbody = "0123456789";
+var buffer = "";
+
+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..5df48b1f76
--- /dev/null
+++ b/netwerk/test/unit/test_socks.js
@@ -0,0 +1,519 @@
+"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 script = do_get_file(script);
+ var proc = new Process(bin);
+ var args = [script.path].concat(args);
+
+ proc.run(false, args, args.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 == 0) {
+ 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);
+ },
+
+ 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..73024dfa0b
--- /dev/null
+++ b/netwerk/test/unit/test_speculative_connect.js
@@ -0,0 +1,368 @@
+/* -*- 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_speculative_connect,
+ test_hostnames_resolving_to_local_addresses,
+ test_proxies_with_local_addresses,
+];
+
+var testDescription = [
+ "Expect pass with localhost",
+ "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_speculative_connect
+ *
+ * Tests a basic positive case using nsIOService.SpeculativeConnect:
+ * connecting to localhost.
+ */
+function test_speculative_connect() {
+ serv = new TestServer();
+ var ssm = Cc["@mozilla.org/scriptsecuritymanager;1"].getService(
+ Ci.nsIScriptSecurityManager
+ );
+ 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);
+}
+
+/* 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);
+ 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 = Cc["@mozilla.org/thread-manager;1"].getService(
+ Ci.nsIThreadManager
+ );
+ 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);
+ 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 = Cc["@mozilla.org/thread-manager;1"].getService(
+ Ci.nsIThreadManager
+ );
+ 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 = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
+
+ 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..3c8b73cee2
--- /dev/null
+++ b/netwerk/test/unit/test_stale-while-revalidate_loop.js
@@ -0,0 +1,46 @@
+/*
+
+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();
+ let env = Cc["@mozilla.org/process/environment;1"].getService(
+ Ci.nsIEnvironment
+ );
+ const PORT = 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..110086faa1
--- /dev/null
+++ b/netwerk/test/unit/test_stale-while-revalidate_max-age-0.js
@@ -0,0 +1,111 @@
+/*
+
+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.import("resource://testing-common/httpd.js");
+
+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..5cc8278723
--- /dev/null
+++ b/netwerk/test/unit/test_stale-while-revalidate_negative.js
@@ -0,0 +1,90 @@
+/*
+
+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.import("resource://testing-common/httpd.js");
+
+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..d39d96087b
--- /dev/null
+++ b/netwerk/test/unit/test_stale-while-revalidate_positive.js
@@ -0,0 +1,111 @@
+/*
+
+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.import("resource://testing-common/httpd.js");
+
+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..f6d54bc9b1
--- /dev/null
+++ b/netwerk/test/unit/test_standardurl.js
@@ -0,0 +1,1299 @@
+"use strict";
+
+var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+var { AppConstants } = ChromeUtils.import(
+ "resource://gre/modules/AppConstants.jsm"
+);
+const gPrefs = Cc["@mozilla.org/preferences-service;1"].getService(
+ Ci.nsIPrefBranch
+);
+
+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 = Cc["@mozilla.org/network/io-service;1"]
+ .getService(Ci.nsIIOService)
+ .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 = Cc["@mozilla.org/preferences-service;1"].getService(
+ Ci.nsIPrefService
+ );
+ 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() {
+ var 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 escape \r\n\t, not filter them.
+ var url = stringToURL("http://test.com/path?query#hash");
+ url = url
+ .mutate()
+ .setFilePath("pa\r\n\tth")
+ .finalize();
+ Assert.equal(url.spec, "http://test.com/pa%0D%0A%09th?query#hash");
+ url = url
+ .mutate()
+ .setQuery("que\r\n\try")
+ .finalize();
+ Assert.equal(url.spec, "http://test.com/pa%0D%0A%09th?query#hash");
+ url = url
+ .mutate()
+ .setRef("ha\r\n\tsh")
+ .finalize();
+ Assert.equal(url.spec, "http://test.com/pa%0D%0A%09th?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;
+ }
+ var 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.
+ var 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);
+
+ var 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://0xfffffffff/",
+ "http://0x100000000/",
+ "http://4294967296/",
+ "http://1.2.0x10000/",
+ "http://1.0x1000000/",
+ "http://256.0.0.1/",
+ "http://1.256.1/",
+ "http://-1.0.0.0/",
+ "http://1.2.3.4.5/",
+ "http://010000000000000000/",
+ "http://2+3/",
+ "http://0.0.0.-1/",
+ "http://1.2.3.4../",
+ "http://1..2/",
+ "http://.1.2.3.4/",
+ "resource://123/",
+ "resource://4294967296/",
+ ];
+ var spec;
+ for (spec of nonIPv4s) {
+ url = stringToURL(spec);
+ Assert.equal(url.spec, spec);
+ }
+
+ var 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++) {
+ 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_UNEXPECTED/,
+ "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_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..d6f9959450
--- /dev/null
+++ b/netwerk/test/unit/test_standardurl_default_port.js
@@ -0,0 +1,61 @@
+/* -*- 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..d8e11df90b
--- /dev/null
+++ b/netwerk/test/unit/test_standardurl_port.js
@@ -0,0 +1,71 @@
+"use strict";
+
+function run_test() {
+ function makeURI(aURLSpec, aCharset) {
+ var ios = Cc["@mozilla.org/network/io-service;1"].getService(
+ Ci.nsIIOService
+ );
+ return ios.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..655512409a
--- /dev/null
+++ b/netwerk/test/unit/test_suspend_channel_before_connect.js
@@ -0,0 +1,99 @@
+"use strict";
+
+var CC = Components.Constructor;
+
+const ServerSocket = CC(
+ "@mozilla.org/network/server-socket;1",
+ "nsIServerSocket",
+ "init"
+);
+
+var obs = Cc["@mozilla.org/observer-service;1"].getService(
+ Ci.nsIObserverService
+);
+
+var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
+
+// 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();
+ var obs = Cc["@mozilla.org/observer-service;1"].getService();
+ obs = obs.QueryInterface(Ci.nsIObserverService);
+ 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..bd9a6626cd
--- /dev/null
+++ b/netwerk/test/unit/test_suspend_channel_on_authRetry.js
@@ -0,0 +1,276 @@
+// This file tests async handling of a channel suspended in DoAuthRetry
+// notifying http-on-modify-request and http-on-before-connect observers.
+"use strict";
+
+var CC = Components.Constructor;
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+// Turn off the authentication dialog blocking for this test.
+var prefs = Cc["@mozilla.org/preferences-service;1"].getService(
+ Ci.nsIPrefBranch
+);
+
+XPCOMUtils.defineLazyGetter(this, "URL", function() {
+ return "http://localhost:" + httpserv.identity.primaryPort;
+});
+
+XPCOMUtils.defineLazyGetter(this, "PORT", function() {
+ return httpserv.identity.primaryPort;
+});
+
+var obs = Cc["@mozilla.org/observer-service;1"].getService(
+ Ci.nsIObserverService
+);
+
+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) {
+ var 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) {
+ var 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 ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
+ var ssm = Cc["@mozilla.org/scriptsecuritymanager;1"].getService(
+ Ci.nsIScriptSecurityManager
+ );
+ var principal = ssm.createContentPrincipal(ios.newURI(loadingUrl), {});
+ return NetUtil.newChannel({
+ uri: url,
+ loadingPrincipal: principal,
+ securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER,
+ });
+}
+
+var tests = [
+ test_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..05efb846e5
--- /dev/null
+++ b/netwerk/test/unit/test_suspend_channel_on_examine.js
@@ -0,0 +1,77 @@
+// This file tests async handling of a channel suspended in http-on-modify-request.
+"use strict";
+
+var CC = Components.Constructor;
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+var obs = Cc["@mozilla.org/observer-service;1"].getService(
+ Ci.nsIObserverService
+);
+
+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) {
+ var obs = Cc["@mozilla.org/observer-service;1"].getService();
+ obs = obs.QueryInterface(Ci.nsIObserverService);
+ obs.removeObserver(this, "http-on-examine-response");
+ callback(subject.QueryInterface(Ci.nsIHttpChannel));
+ },
+ },
+ "http-on-examine-response"
+ );
+}
+
+function startChannelRequest(baseUrl, flags, callback) {
+ var chan = NetUtil.newChannel({
+ uri: baseUrl,
+ 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");
+
+ var cm = Cc["@mozilla.org/cookiemanager;1"].getService(Ci.nsICookieManager);
+ Assert.equal(cm.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..d01bd4f2ba
--- /dev/null
+++ b/netwerk/test/unit/test_suspend_channel_on_examine_merged_response.js
@@ -0,0 +1,212 @@
+/* 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.import("resource://testing-common/httpd.js");
+
+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);
+
+ var obs = Cc["@mozilla.org/observer-service;1"].getService(
+ Ci.nsIObserverService
+ );
+ 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 {
+ var obs = Cc["@mozilla.org/observer-service;1"].getService(
+ Ci.nsIObserverService
+ );
+ 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..ca82897dc1
--- /dev/null
+++ b/netwerk/test/unit/test_suspend_channel_on_modified.js
@@ -0,0 +1,181 @@
+// This file tests async handling of a channel suspended in http-on-modify-request.
+"use strict";
+
+var CC = Components.Constructor;
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+var obs = Cc["@mozilla.org/observer-service;1"].getService(
+ Ci.nsIObserverService
+);
+
+var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
+
+// 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) {
+ var obs = Cc["@mozilla.org/observer-service;1"].getService();
+ obs = obs.QueryInterface(Ci.nsIObserverService);
+ obs.removeObserver(this, "http-on-modify-request");
+ callback(subject.QueryInterface(Ci.nsIHttpChannel));
+ },
+ },
+ "http-on-modify-request"
+ );
+}
+
+function startChannelRequest(baseUrl, flags, expectedResponse = null) {
+ var chan = NetUtil.newChannel({
+ uri: baseUrl,
+ 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..91a491388e
--- /dev/null
+++ b/netwerk/test/unit/test_synthesized_response.js
@@ -0,0 +1,294 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+XPCOMUtils.defineLazyGetter(this, "URL", function() {
+ return "http://localhost:" + httpServer.identity.primaryPort;
+});
+
+var httpServer = null;
+
+function make_uri(url) {
+ var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
+ return ios.newURI(url);
+}
+
+function isParentProcess() {
+ let appInfo = Cc["@mozilla.org/xre/app-info;1"];
+ return (
+ !appInfo ||
+ appInfo.getService(Ci.nsIXULRuntime).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.
+ Cc["@mozilla.org/netwerk/cache-storage-service;1"].getService(
+ Ci.nsICacheStorageService
+ );
+}
+
+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(chan) {
+ chan.resetInterception();
+ });
+ 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(chan) {
+ chan.resetInterception();
+ });
+ 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(chan) {
+ do_timeout(100, function() {
+ chan.resetInterception();
+ });
+ });
+ 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(chan) {
+ chan.resetInterception();
+ do_timeout(0, function() {
+ var gotexception = false;
+ try {
+ chan.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(chan) {
+ throw "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..c48d752091
--- /dev/null
+++ b/netwerk/test/unit/test_throttlechannel.js
@@ -0,0 +1,46 @@
+// Test nsIThrottledInputChannel interface.
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+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..6e34cb668c
--- /dev/null
+++ b/netwerk/test/unit/test_throttling.js
@@ -0,0 +1,64 @@
+// Test nsIThrottledInputChannel interface.
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+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..b00f2e92b6
--- /dev/null
+++ b/netwerk/test/unit/test_tldservice_nextsubdomain.js
@@ -0,0 +1,28 @@
+"use strict";
+
+function run_test() {
+ var tld = Cc["@mozilla.org/network/effective-tld-service;1"].getService(
+ Ci.nsIEffectiveTLDService
+ );
+
+ 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 = tld.getNextSubDomain(test.data);
+ Assert.equal(r, test.result);
+ } catch (e) {
+ Assert.ok(test.throw);
+ }
+ });
+}
diff --git a/netwerk/test/unit/test_tls_flags.js b/netwerk/test/unit/test_tls_flags.js
new file mode 100644
index 0000000000..669465560a
--- /dev/null
+++ b/netwerk/test/unit/test_tls_flags.js
@@ -0,0 +1,274 @@
+// -*- 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);
+
+function getCert() {
+ return new Promise((resolve, reject) => {
+ let certService = Cc[
+ "@mozilla.org/security/local-cert-service;1"
+ ].getService(Ci.nsILocalCertService);
+ certService.getOrCreateCert("tlsflags-test", {
+ handleCert(c, rv) {
+ if (rv) {
+ reject(rv);
+ return;
+ }
+ resolve(c);
+ },
+ });
+ });
+}
+
+class InputStreamCallback {
+ constructor(output) {
+ this.output = output;
+ this.stopped = false;
+ }
+
+ onInputStreamReady(stream) {
+ info("input stream ready");
+ if (this.stopped) {
+ info("input stream callback stopped - bailing");
+ return;
+ }
+ let available = 0;
+ try {
+ available = stream.available();
+ } catch (e) {
+ // onInputStreamReady may fire when the stream has been closed.
+ equal(
+ e.result,
+ Cr.NS_BASE_STREAM_CLOSED,
+ "error should be NS_BASE_STREAM_CLOSED"
+ );
+ }
+ if (available > 0) {
+ let request = NetUtil.readInputStreamToString(stream, available, {
+ charset: "utf8",
+ });
+ ok(
+ request.startsWith("GET / HTTP/1.1\r\n"),
+ "Should get a simple GET / HTTP/1.1 request"
+ );
+ let response =
+ "HTTP/1.1 200 OK\r\n" +
+ "Content-Length: 2\r\n" +
+ "Content-Type: text/plain\r\n" +
+ "\r\nOK";
+ let written = this.output.write(response, response.length);
+ equal(
+ written,
+ response.length,
+ "should have been able to write entire response"
+ );
+ }
+ this.output.close();
+ info("done with input stream ready");
+ }
+
+ stop() {
+ this.stopped = true;
+ this.output.close();
+ }
+}
+
+class TLSServerSecurityObserver {
+ constructor(input, output, 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.securityInfo.QueryInterface(
+ 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);
+ let overrideBits =
+ Ci.nsICertOverrideService.ERROR_UNTRUSTED |
+ Ci.nsICertOverrideService.ERROR_MISMATCH;
+ certOverrideService.rememberValidityOverride(
+ hostname,
+ port,
+ cert,
+ overrideBits,
+ 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 = await getCert();
+
+ // 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..1c629add35
--- /dev/null
+++ b/netwerk/test/unit/test_tls_flags_separate_connections.js
@@ -0,0 +1,119 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+XPCOMUtils.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..e9d2bc38ce
--- /dev/null
+++ b/netwerk/test/unit/test_tls_server.js
@@ -0,0 +1,301 @@
+/* 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 { PromiseUtils } = ChromeUtils.import(
+ "resource://gre/modules/PromiseUtils.jsm"
+);
+const certService = Cc["@mozilla.org/security/local-cert-service;1"].getService(
+ Ci.nsILocalCertService
+);
+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 = Cc["@mozilla.org/preferences-service;1"].getService(
+ Ci.nsIPrefBranch
+);
+
+function getCert() {
+ return new Promise((resolve, reject) => {
+ certService.getOrCreateCert("tls-test", {
+ handleCert(c, rv) {
+ if (rv) {
+ reject(rv);
+ return;
+ }
+ resolve(c);
+ },
+ });
+ });
+}
+
+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.securityInfo.QueryInterface(
+ 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(status.peerCert.equals(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_ECDSA_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(input) {
+ NetUtil.asyncCopy(input, 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) {
+ let overrideBits =
+ Ci.nsICertOverrideService.ERROR_UNTRUSTED |
+ Ci.nsICertOverrideService.ERROR_MISMATCH;
+ certOverrideService.rememberValidityOverride(
+ "127.0.0.1",
+ port,
+ cert,
+ overrideBits,
+ true
+ );
+}
+
+function startClient(port, cert, expectingAlert, tlsVersion) {
+ 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
+ );
+ let input;
+ let output;
+
+ let inputDeferred = PromiseUtils.defer();
+ let outputDeferred = PromiseUtils.defer();
+
+ let handler = {
+ onTransportStatus(transport, status) {
+ if (status === Ci.nsISocketTransport.STATUS_CONNECTED_TO) {
+ output.asyncWait(handler, 0, 0, Services.tm.currentThread);
+ }
+ },
+
+ onInputStreamReady(input) {
+ try {
+ let data = NetUtil.readInputStreamToString(input, input.available());
+ equal(data, "HELLO", "Echoed data received");
+ input.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");
+ input.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");
+ input.close();
+ output.close();
+ inputDeferred.resolve();
+ return;
+ }
+ }
+ inputDeferred.reject(e);
+ }
+ },
+
+ onOutputStreamReady(output) {
+ try {
+ // Set the client certificate as appropriate.
+ if (cert) {
+ let clientSecInfo = transport.securityInfo;
+ let tlsControl = clientSecInfo.QueryInterface(Ci.nsISSLSocketControl);
+ tlsControl.clientCert = cert;
+ }
+
+ output.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.
+do_load_manifest("client_cert_chooser.manifest");
+
+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 = await getCert();
+ 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 ? cert : null,
+ 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..d1fad1ba68
--- /dev/null
+++ b/netwerk/test/unit/test_tls_server_multiple_clients.js
@@ -0,0 +1,152 @@
+/* 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 { PromiseUtils } = ChromeUtils.import(
+ "resource://gre/modules/PromiseUtils.jsm"
+);
+const certService = Cc["@mozilla.org/security/local-cert-service;1"].getService(
+ Ci.nsILocalCertService
+);
+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 getCert() {
+ return new Promise((resolve, reject) => {
+ certService.getOrCreateCert("tls-test", {
+ handleCert(c, rv) {
+ if (rv) {
+ reject(rv);
+ return;
+ }
+ resolve(c);
+ },
+ });
+ });
+}
+
+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.securityInfo.QueryInterface(
+ 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(input) {
+ NetUtil.asyncCopy(input, output);
+ },
+ },
+ 0,
+ 0,
+ Services.tm.currentThread
+ );
+ },
+ onStopListening() {},
+ };
+
+ tlsServer.setSessionTickets(false);
+
+ tlsServer.asyncListen(listener);
+
+ return tlsServer.port;
+}
+
+function storeCertOverride(port, cert) {
+ let overrideBits =
+ Ci.nsICertOverrideService.ERROR_UNTRUSTED |
+ Ci.nsICertOverrideService.ERROR_MISMATCH;
+ certOverrideService.rememberValidityOverride(
+ "127.0.0.1",
+ port,
+ cert,
+ overrideBits,
+ true
+ );
+}
+
+function startClient(port) {
+ let transport = socketTransportService.createTransport(
+ ["ssl"],
+ "127.0.0.1",
+ port,
+ null
+ );
+ let input;
+ let output;
+
+ let inputDeferred = PromiseUtils.defer();
+ let outputDeferred = PromiseUtils.defer();
+
+ let handler = {
+ onTransportStatus(transport, status) {
+ if (status === Ci.nsISocketTransport.STATUS_CONNECTED_TO) {
+ output.asyncWait(handler, 0, 0, Services.tm.currentThread);
+ }
+ },
+
+ onInputStreamReady(input) {
+ try {
+ let data = NetUtil.readInputStreamToString(input, input.available());
+ equal(data, "HELLO", "Echoed data received");
+ input.close();
+ output.close();
+ inputDeferred.resolve();
+ } catch (e) {
+ inputDeferred.reject(e);
+ }
+ },
+
+ onOutputStreamReady(output) {
+ try {
+ output.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 = await getCert();
+ 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..8dc6ed95bb
--- /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.import("resource://testing-common/httpd.js");
+
+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() {
+ Cc["@mozilla.org/observer-service;1"]
+ .getService(Ci.nsIObserverService)
+ .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..5c31b0b0ef
--- /dev/null
+++ b/netwerk/test/unit/test_trackingProtection_annotateChannels.js
@@ -0,0 +1,388 @@
+"use strict";
+
+const { UrlClassifierTestUtils } = ChromeUtils.import(
+ "resource://testing-common/UrlClassifierTestUtils.jsm"
+);
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+// 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..fa2d915c20
--- /dev/null
+++ b/netwerk/test/unit/test_trr.js
@@ -0,0 +1,2136 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+const dns = Cc["@mozilla.org/network/dns-service;1"].getService(
+ Ci.nsIDNSService
+);
+const { MockRegistrar } = ChromeUtils.import(
+ "resource://testing-common/MockRegistrar.jsm"
+);
+const mainThread = Services.tm.currentThread;
+
+const gDefaultPref = Services.prefs.getDefaultBranch("");
+const gOverride = Cc["@mozilla.org/network/native-dns-override;1"].getService(
+ Ci.nsINativeDNSResolverOverride
+);
+
+const defaultOriginAttributes = {};
+let h2Port = null;
+
+async function SetParentalControlEnabled(aEnabled) {
+ let parentalControlsService = {
+ parentalControlsEnabled: aEnabled,
+ QueryInterface: ChromeUtils.generateQI(["nsIParentalControlsService"]),
+ };
+ let cid = MockRegistrar.register(
+ "@mozilla.org/parental-controls-service;1",
+ parentalControlsService
+ );
+ dns.reloadParentalControlEnabled();
+ MockRegistrar.unregister(cid);
+}
+
+function setup() {
+ dump("start!\n");
+
+ let env = Cc["@mozilla.org/process/environment;1"].getService(
+ Ci.nsIEnvironment
+ );
+ h2Port = env.get("MOZHTTP2_PORT");
+ Assert.notEqual(h2Port, null);
+ Assert.notEqual(h2Port, "");
+
+ // Set to allow the cert presented by our H2 server
+ do_get_profile();
+
+ Services.prefs.setBoolPref("network.http.spdy.enabled", true);
+ Services.prefs.setBoolPref("network.http.spdy.enabled.http2", true);
+ // the TRR server is on 127.0.0.1
+ Services.prefs.setCharPref("network.trr.bootstrapAddress", "127.0.0.1");
+
+ // use the h2 server as DOH provider
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${h2Port}/doh`
+ );
+ // make all native resolve calls "secretly" resolve localhost instead
+ Services.prefs.setBoolPref("network.dns.native-is-localhost", true);
+
+ // 0 - off, 1 - reserved, 2 - TRR first, 3 - TRR only, 4 - reserved
+ Services.prefs.setIntPref("network.trr.mode", 2); // TRR first
+ Services.prefs.setBoolPref("network.trr.wait-for-portal", false);
+ // By default wait for all responses before notifying the listeners.
+ Services.prefs.setBoolPref("network.trr.wait-for-A-and-AAAA", true);
+ // don't confirm that TRR is working, just go!
+ Services.prefs.setCharPref("network.trr.confirmationNS", "skip");
+ // some tests rely on the cache not being cleared on pref change.
+ // we specifically test that this works
+ Services.prefs.setBoolPref("network.trr.clear-cache-on-pref-change", false);
+
+ // The moz-http2 cert is for foo.example.com and is signed by http2-ca.pem
+ // so add that cert to the trust list as a signing cert. // the foo.example.com domain name.
+ let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(
+ Ci.nsIX509CertDB
+ );
+ addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u");
+
+ SetParentalControlEnabled(false);
+}
+
+setup();
+registerCleanupFunction(async () => {
+ trr_clear_prefs();
+});
+
+// This is an IP that is local, so we don't crash when connecting to it,
+// but also connecting to it fails, so we attempt to retry with regular DNS.
+const BAD_IP = (() => {
+ if (mozinfo.os == "linux" || mozinfo.os == "android") {
+ return "127.9.9.9";
+ }
+ return "0.0.0.0";
+})();
+
+class DNSListener {
+ constructor(
+ name,
+ expectedAnswer,
+ expectedSuccess = true,
+ delay,
+ trrServer = "",
+ expectEarlyFail = false,
+ flags = 0
+ ) {
+ this.name = name;
+ this.expectedAnswer = expectedAnswer;
+ this.expectedSuccess = expectedSuccess;
+ this.delay = delay;
+ this.promise = new Promise(resolve => {
+ this.resolve = resolve;
+ });
+
+ let resolverInfo =
+ trrServer == "" ? null : dns.newTRRResolverInfo(trrServer);
+ try {
+ this.request = dns.asyncResolve(
+ name,
+ Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT,
+ flags,
+ resolverInfo,
+ this,
+ mainThread,
+ defaultOriginAttributes
+ );
+ Assert.ok(!expectEarlyFail);
+ } catch (e) {
+ Assert.ok(expectEarlyFail);
+ this.resolve([e]);
+ }
+ }
+
+ onLookupComplete(inRequest, inRecord, inStatus) {
+ Assert.ok(
+ inRequest == this.request,
+ "Checking that this is the correct callback"
+ );
+
+ // If we don't expect success here, just resolve and the caller will
+ // decide what to do with the results.
+ if (!this.expectedSuccess) {
+ this.resolve([inRequest, inRecord, inStatus]);
+ return;
+ }
+
+ Assert.equal(inStatus, Cr.NS_OK, "Checking status");
+ inRecord.QueryInterface(Ci.nsIDNSAddrRecord);
+ let answer = inRecord.getNextAddrAsString();
+ Assert.equal(
+ answer,
+ this.expectedAnswer,
+ `Checking result for ${this.name}`
+ );
+
+ 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);
+ }
+}
+
+add_task(async function test0_nodeExecute() {
+ // 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"));
+});
+
+function makeChan(url, mode) {
+ let chan = NetUtil.newChannel({
+ uri: url,
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+ chan.setTRRMode(mode);
+ return chan;
+}
+
+add_task(
+ { skip_if: () => mozinfo.os == "mac" },
+ async function test_trr_flags() {
+ Services.prefs.setBoolPref("network.trr.fallback-on-zero-response", true);
+ 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}/`;
+
+ dns.clearCache(true);
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://localhost:${h2Port}/doh?responseIP=${BAD_IP}`
+ );
+
+ Services.prefs.setIntPref("network.trr.mode", 0);
+ dns.clearCache(true);
+ let chan = makeChan(URL, Ci.nsIRequest.TRR_DEFAULT_MODE);
+ await new Promise(resolve => chan.asyncOpen(new ChannelListener(resolve)));
+ equal(chan.getTRRMode(), Ci.nsIRequest.TRR_DEFAULT_MODE);
+ dns.clearCache(true);
+ chan = makeChan(URL, Ci.nsIRequest.TRR_DISABLED_MODE);
+ await new Promise(resolve => chan.asyncOpen(new ChannelListener(resolve)));
+ equal(chan.getTRRMode(), Ci.nsIRequest.TRR_DISABLED_MODE);
+ dns.clearCache(true);
+ chan = makeChan(URL, Ci.nsIRequest.TRR_FIRST_MODE);
+ await new Promise(resolve => chan.asyncOpen(new ChannelListener(resolve)));
+ equal(chan.getTRRMode(), Ci.nsIRequest.TRR_FIRST_MODE);
+ dns.clearCache(true);
+ chan = makeChan(
+ `http://example.com:${httpserv.identity.primaryPort}/`,
+ Ci.nsIRequest.TRR_ONLY_MODE
+ );
+ // Should fail as it tries to connect to local but unavailable IP
+ await new Promise(resolve =>
+ chan.asyncOpen(new ChannelListener(resolve, null, CL_EXPECT_FAILURE))
+ );
+ equal(chan.getTRRMode(), Ci.nsIRequest.TRR_ONLY_MODE);
+
+ dns.clearCache(true);
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://localhost:${h2Port}/doh?responseIP=${BAD_IP}`
+ );
+ Services.prefs.setIntPref("network.trr.mode", 2);
+
+ dns.clearCache(true);
+ chan = makeChan(URL, Ci.nsIRequest.TRR_DEFAULT_MODE);
+ // Does get the IP from TRR, but failure means it falls back to DNS.
+ await new Promise(resolve => chan.asyncOpen(new ChannelListener(resolve)));
+ dns.clearCache(true);
+ chan = makeChan(URL, Ci.nsIRequest.TRR_DISABLED_MODE);
+ await new Promise(resolve => chan.asyncOpen(new ChannelListener(resolve)));
+ dns.clearCache(true);
+ // Does get the IP from TRR, but failure means it falls back to DNS.
+ chan = makeChan(URL, Ci.nsIRequest.TRR_FIRST_MODE);
+ await new Promise(resolve => chan.asyncOpen(new ChannelListener(resolve)));
+ dns.clearCache(true);
+ chan = makeChan(URL, Ci.nsIRequest.TRR_ONLY_MODE);
+ await new Promise(resolve =>
+ chan.asyncOpen(new ChannelListener(resolve, null, CL_EXPECT_FAILURE))
+ );
+
+ dns.clearCache(true);
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://localhost:${h2Port}/doh?responseIP=${BAD_IP}`
+ );
+ Services.prefs.setIntPref("network.trr.mode", 3);
+
+ dns.clearCache(true);
+ chan = makeChan(URL, Ci.nsIRequest.TRR_DEFAULT_MODE);
+ await new Promise(resolve =>
+ chan.asyncOpen(new ChannelListener(resolve, null, CL_EXPECT_FAILURE))
+ );
+ dns.clearCache(true);
+ chan = makeChan(URL, Ci.nsIRequest.TRR_DISABLED_MODE);
+ await new Promise(resolve => chan.asyncOpen(new ChannelListener(resolve)));
+ dns.clearCache(true);
+ chan = makeChan(URL, Ci.nsIRequest.TRR_FIRST_MODE);
+ await new Promise(resolve => chan.asyncOpen(new ChannelListener(resolve)));
+ dns.clearCache(true);
+ chan = makeChan(URL, Ci.nsIRequest.TRR_ONLY_MODE);
+ await new Promise(resolve =>
+ chan.asyncOpen(new ChannelListener(resolve, null, CL_EXPECT_FAILURE))
+ );
+
+ dns.clearCache(true);
+ Services.prefs.setIntPref("network.trr.mode", 5);
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://localhost:${h2Port}/doh?responseIP=1.1.1.1`
+ );
+
+ dns.clearCache(true);
+ chan = makeChan(URL, Ci.nsIRequest.TRR_DEFAULT_MODE);
+ await new Promise(resolve => chan.asyncOpen(new ChannelListener(resolve)));
+ dns.clearCache(true);
+ chan = makeChan(URL, Ci.nsIRequest.TRR_DISABLED_MODE);
+ await new Promise(resolve => chan.asyncOpen(new ChannelListener(resolve)));
+ dns.clearCache(true);
+ chan = makeChan(URL, Ci.nsIRequest.TRR_FIRST_MODE);
+ await new Promise(resolve => chan.asyncOpen(new ChannelListener(resolve)));
+ dns.clearCache(true);
+ chan = makeChan(URL, Ci.nsIRequest.TRR_ONLY_MODE);
+ await new Promise(resolve => chan.asyncOpen(new ChannelListener(resolve)));
+
+ await new Promise(resolve => httpserv.stop(resolve));
+ Services.prefs.clearUserPref("network.trr.fallback-on-zero-response");
+ }
+);
+
+// verify basic A record
+add_task(async function test1() {
+ dns.clearCache(true);
+ Services.prefs.setIntPref("network.trr.mode", 2); // TRR-first
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${h2Port}/doh?responseIP=2.2.2.2`
+ );
+
+ await new DNSListener("bar.example.com", "2.2.2.2");
+});
+
+// verify basic A record - without bootstrapping
+add_task(async function test1b() {
+ dns.clearCache(true);
+ Services.prefs.setIntPref("network.trr.mode", 3); // TRR-only
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${h2Port}/doh?responseIP=3.3.3.3`
+ );
+ Services.prefs.clearUserPref("network.trr.bootstrapAddress");
+ Services.prefs.setCharPref("network.dns.localDomains", "foo.example.com");
+
+ await new DNSListener("bar.example.com", "3.3.3.3");
+
+ Services.prefs.setCharPref("network.trr.bootstrapAddress", "127.0.0.1");
+ Services.prefs.clearUserPref("network.dns.localDomains");
+});
+
+// verify that the name was put in cache - it works with bad DNS URI
+add_task(async function test2() {
+ // Don't clear the cache. That is what we're checking.
+ Services.prefs.setIntPref("network.trr.mode", 3); // TRR-only
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${h2Port}/404`
+ );
+
+ await new DNSListener("bar.example.com", "3.3.3.3");
+});
+
+// verify working credentials in DOH request
+add_task(async function test3() {
+ dns.clearCache(true);
+ Services.prefs.setIntPref("network.trr.mode", 3); // TRR-only
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${h2Port}/doh?responseIP=4.4.4.4&auth=true`
+ );
+ Services.prefs.setCharPref("network.trr.credentials", "user:password");
+
+ await new DNSListener("bar.example.com", "4.4.4.4");
+});
+
+// verify failing credentials in DOH request
+add_task(async function test4() {
+ dns.clearCache(true);
+ Services.prefs.setIntPref("network.trr.mode", 3); // TRR-only
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${h2Port}/doh?responseIP=4.4.4.4&auth=true`
+ );
+ Services.prefs.setCharPref("network.trr.credentials", "evil:person");
+
+ let [, , inStatus] = await new DNSListener(
+ "wrong.example.com",
+ undefined,
+ false
+ );
+ Assert.ok(
+ !Components.isSuccessCode(inStatus),
+ `${inStatus} should be an error code`
+ );
+});
+
+// verify DOH push, part A
+add_task(async function test5() {
+ dns.clearCache(true);
+ Services.prefs.setIntPref("network.trr.mode", 3); // TRR-only
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${h2Port}/doh?responseIP=5.5.5.5&push=true`
+ );
+
+ await new DNSListener("first.example.com", "5.5.5.5");
+});
+
+add_task(async function test5b() {
+ // 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.
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${h2Port}/404`
+ );
+ dump("test5b - resolve push.example.org please\n");
+
+ await new DNSListener("push.example.org", "2018::2018");
+});
+
+// verify AAAA entry
+add_task(async function test6() {
+ Services.prefs.setBoolPref("network.trr.wait-for-A-and-AAAA", true);
+
+ dns.clearCache(true);
+ Services.prefs.setBoolPref("network.trr.early-AAAA", true); // ignored when wait-for-A-and-AAAA is true
+ Services.prefs.setIntPref("network.trr.mode", 3); // TRR-only
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${h2Port}/doh?responseIP=2020:2020::2020&delayIPv4=100`
+ );
+ await new DNSListener("aaaa.example.com", "2020:2020::2020");
+
+ dns.clearCache(true);
+ Services.prefs.setBoolPref("network.trr.early-AAAA", false); // ignored when wait-for-A-and-AAAA is true
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${h2Port}/doh?responseIP=2020:2020::2030&delayIPv4=100`
+ );
+ await new DNSListener("aaaa.example.com", "2020:2020::2030");
+
+ dns.clearCache(true);
+ Services.prefs.setBoolPref("network.trr.early-AAAA", true); // ignored when wait-for-A-and-AAAA is true
+ Services.prefs.setIntPref("network.trr.mode", 3); // TRR-only
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${h2Port}/doh?responseIP=2020:2020::2020&delayIPv6=100`
+ );
+ await new DNSListener("aaaa.example.com", "2020:2020::2020");
+
+ dns.clearCache(true);
+ Services.prefs.setBoolPref("network.trr.early-AAAA", false); // ignored when wait-for-A-and-AAAA is true
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${h2Port}/doh?responseIP=2020:2020::2030&delayIPv6=100`
+ );
+ await new DNSListener("aaaa.example.com", "2020:2020::2030");
+
+ dns.clearCache(true);
+ Services.prefs.setBoolPref("network.trr.early-AAAA", true); // ignored when wait-for-A-and-AAAA is true
+ Services.prefs.setIntPref("network.trr.mode", 3); // TRR-only
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${h2Port}/doh?responseIP=2020:2020::2020`
+ );
+ await new DNSListener("aaaa.example.com", "2020:2020::2020");
+
+ dns.clearCache(true);
+ Services.prefs.setBoolPref("network.trr.early-AAAA", false); // ignored when wait-for-A-and-AAAA is true
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${h2Port}/doh?responseIP=2020:2020::2030`
+ );
+ await new DNSListener("aaaa.example.com", "2020:2020::2030");
+
+ Services.prefs.setBoolPref("network.trr.wait-for-A-and-AAAA", false);
+
+ dns.clearCache(true);
+ Services.prefs.setBoolPref("network.trr.early-AAAA", true);
+ Services.prefs.setIntPref("network.trr.mode", 3); // TRR-only
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${h2Port}/doh?responseIP=2020:2020::2020&delayIPv4=100`
+ );
+ await new DNSListener("aaaa.example.com", "2020:2020::2020");
+
+ dns.clearCache(true);
+ Services.prefs.setBoolPref("network.trr.early-AAAA", false);
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${h2Port}/doh?responseIP=2020:2020::2030&delayIPv4=100`
+ );
+ await new DNSListener("aaaa.example.com", "2020:2020::2030");
+
+ dns.clearCache(true);
+ Services.prefs.setBoolPref("network.trr.early-AAAA", true);
+ Services.prefs.setIntPref("network.trr.mode", 3); // TRR-only
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${h2Port}/doh?responseIP=2020:2020::2020&delayIPv6=100`
+ );
+ await new DNSListener("aaaa.example.com", "2020:2020::2020");
+
+ dns.clearCache(true);
+ Services.prefs.setBoolPref("network.trr.early-AAAA", false);
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${h2Port}/doh?responseIP=2020:2020::2030&delayIPv6=100`
+ );
+ await new DNSListener("aaaa.example.com", "2020:2020::2030");
+
+ dns.clearCache(true);
+ Services.prefs.setBoolPref("network.trr.early-AAAA", true);
+ Services.prefs.setIntPref("network.trr.mode", 3); // TRR-only
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${h2Port}/doh?responseIP=2020:2020::2020`
+ );
+ await new DNSListener("aaaa.example.com", "2020:2020::2020");
+
+ dns.clearCache(true);
+ Services.prefs.setBoolPref("network.trr.early-AAAA", false);
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${h2Port}/doh?responseIP=2020:2020::2030`
+ );
+ await new DNSListener("aaaa.example.com", "2020:2020::2030");
+
+ Services.prefs.clearUserPref("network.trr.early-AAAA");
+ Services.prefs.clearUserPref("network.trr.wait-for-A-and-AAAA");
+});
+
+// verify RFC1918 address from the server is rejected
+add_task(async function test7() {
+ dns.clearCache(true);
+ Services.prefs.setIntPref("network.trr.mode", 3); // TRR-only
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${h2Port}/doh?responseIP=192.168.0.1`
+ );
+ let [, , inStatus] = await new DNSListener(
+ "rfc1918.example.com",
+ undefined,
+ false
+ );
+ Assert.ok(
+ !Components.isSuccessCode(inStatus),
+ `${inStatus} should be an error code`
+ );
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${h2Port}/doh?responseIP=::ffff:192.168.0.1`
+ );
+ [, , inStatus] = await new DNSListener(
+ "rfc1918-ipv6.example.com",
+ undefined,
+ false
+ );
+ Assert.ok(
+ !Components.isSuccessCode(inStatus),
+ `${inStatus} should be an error code`
+ );
+});
+
+// verify RFC1918 address from the server is fine when told so
+add_task(async function test8() {
+ dns.clearCache(true);
+ Services.prefs.setIntPref("network.trr.mode", 3); // TRR-only
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${h2Port}/doh?responseIP=192.168.0.1`
+ );
+ Services.prefs.setBoolPref("network.trr.allow-rfc1918", true);
+ await new DNSListener("rfc1918.example.com", "192.168.0.1");
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${h2Port}/doh?responseIP=::ffff:192.168.0.1`
+ );
+ await new DNSListener("rfc1918-ipv6.example.com", "::ffff:192.168.0.1");
+});
+
+// use GET and disable ECS (makes a larger request)
+// verify URI template cutoff
+add_task(async function test8b() {
+ dns.clearCache(true);
+ Services.prefs.setIntPref("network.trr.mode", 3); // TRR-only
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${h2Port}/doh{?dns}`
+ );
+ Services.prefs.clearUserPref("network.trr.allow-rfc1918");
+ Services.prefs.setBoolPref("network.trr.useGET", true);
+ Services.prefs.setBoolPref("network.trr.disable-ECS", true);
+ await new DNSListener("ecs.example.com", "5.5.5.5");
+});
+
+// use GET
+add_task(async function test9() {
+ dns.clearCache(true);
+ Services.prefs.setIntPref("network.trr.mode", 3); // TRR-only
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${h2Port}/doh`
+ );
+ Services.prefs.clearUserPref("network.trr.allow-rfc1918");
+ Services.prefs.setBoolPref("network.trr.useGET", true);
+ Services.prefs.setBoolPref("network.trr.disable-ECS", false);
+ await new DNSListener("get.example.com", "5.5.5.5");
+});
+
+// confirmationNS set without confirmed NS yet
+add_task(async function test10() {
+ dns.clearCache(true);
+ Services.prefs.setIntPref("network.trr.mode", 3); // TRR-only
+ Services.prefs.clearUserPref("network.trr.useGET");
+ Services.prefs.clearUserPref("network.trr.disable-ECS");
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${h2Port}/doh?responseIP=1::ffff`
+ );
+ Services.prefs.setCharPref(
+ "network.trr.confirmationNS",
+ "confirm.example.com"
+ );
+
+ await new DNSListener("skipConfirmationForMode3.example.com", "1::ffff");
+});
+
+// use a slow server and short timeout!
+add_task(async function test11() {
+ dns.clearCache(true);
+ Services.prefs.setIntPref("network.trr.mode", 3); // TRR-only
+ Services.prefs.setCharPref("network.trr.confirmationNS", "skip");
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${h2Port}/dns-750ms`
+ );
+ Services.prefs.setIntPref("network.trr.request_timeout_mode_trronly_ms", 10);
+ let [, , inStatus] = await new DNSListener(
+ "test11.example.com",
+ undefined,
+ false
+ );
+ Assert.ok(
+ !Components.isSuccessCode(inStatus),
+ `${inStatus} should be an error code`
+ );
+});
+
+// gets an no answers back from DoH. Falls back to regular DNS
+add_task(async function test12() {
+ dns.clearCache(true);
+ Services.prefs.setIntPref("network.trr.mode", 2); // TRR-first
+ Services.prefs.setIntPref("network.trr.request_timeout_ms", 10);
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${h2Port}/doh?responseIP=none`
+ );
+ Services.prefs.clearUserPref("network.trr.request_timeout_ms");
+ Services.prefs.clearUserPref("network.trr.request_timeout_mode_trronly_ms");
+ await new DNSListener("confirm.example.com", "127.0.0.1");
+});
+
+// TRR-first gets a 404 back from DOH
+add_task(async function test13() {
+ dns.clearCache(true);
+ Services.prefs.setIntPref("network.trr.mode", 2); // TRR-first
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${h2Port}/404`
+ );
+ await new DNSListener("test13.example.com", "127.0.0.1");
+});
+
+// Test that MODE_RESERVED4 (previously MODE_SHADOW) is treated as TRR off.
+add_task(async function test14() {
+ dns.clearCache(true);
+ Services.prefs.setIntPref("network.trr.mode", 4); // MODE_RESERVED4. Interpreted as TRR off.
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${h2Port}/404`
+ );
+ await new DNSListener("test14.example.com", "127.0.0.1");
+
+ Services.prefs.setIntPref("network.trr.mode", 4); // MODE_RESERVED4. Interpreted as TRR off.
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${h2Port}/doh?responseIP=2.2.2.2`
+ );
+ await new DNSListener("bar.example.com", "127.0.0.1");
+});
+
+// Test that MODE_RESERVED4 (previously MODE_SHADOW) is treated as TRR off.
+add_task(async function test15() {
+ dns.clearCache(true);
+ Services.prefs.setIntPref("network.trr.mode", 4); // MODE_RESERVED4. Interpreted as TRR off.
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${h2Port}/dns-750ms`
+ );
+ Services.prefs.setIntPref("network.trr.request_timeout_ms", 10);
+ Services.prefs.setIntPref("network.trr.request_timeout_mode_trronly_ms", 10);
+ await new DNSListener("test15.example.com", "127.0.0.1");
+
+ Services.prefs.setIntPref("network.trr.mode", 4); // MODE_RESERVED4. Interpreted as TRR off.
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${h2Port}/doh?responseIP=2.2.2.2`
+ );
+ await new DNSListener("bar.example.com", "127.0.0.1");
+});
+
+// TRR-first and timed out TRR
+add_task(async function test16() {
+ dns.clearCache(true);
+ Services.prefs.setIntPref("network.trr.mode", 2); // TRR-first
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${h2Port}/dns-750ms`
+ );
+ Services.prefs.setIntPref("network.trr.request_timeout_ms", 10);
+ Services.prefs.setIntPref("network.trr.request_timeout_mode_trronly_ms", 10);
+ await new DNSListener("test16.example.com", "127.0.0.1");
+});
+
+// TRR-only and chase CNAME
+add_task(async function test17() {
+ dns.clearCache(true);
+ Services.prefs.setIntPref("network.trr.mode", 3); // TRR-only
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${h2Port}/dns-cname`
+ );
+ Services.prefs.clearUserPref("network.trr.request_timeout_ms");
+ Services.prefs.clearUserPref("network.trr.request_timeout_mode_trronly_ms");
+ await new DNSListener("cname.example.com", "99.88.77.66");
+});
+
+// TRR-only and a CNAME loop
+add_task(async function test18() {
+ dns.clearCache(true);
+ Services.prefs.setIntPref("network.trr.mode", 3); // TRR-only
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${h2Port}/doh?responseIP=none&cnameloop=true`
+ );
+ let [, , inStatus] = await new DNSListener(
+ "test18.example.com",
+ undefined,
+ false
+ );
+ Assert.ok(
+ !Components.isSuccessCode(inStatus),
+ `${inStatus} should be an error code`
+ );
+});
+
+// Test that MODE_RESERVED1 (previously MODE_PARALLEL) is treated as TRR off.
+add_task(async function test19() {
+ dns.clearCache(true);
+ Services.prefs.setIntPref("network.trr.mode", 1); // MODE_RESERVED1. Interpreted as TRR off.
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${h2Port}/doh?responseIP=none&cnameloop=true`
+ );
+ await new DNSListener("test19.example.com", "127.0.0.1");
+
+ Services.prefs.setIntPref("network.trr.mode", 1); // MODE_RESERVED1. Interpreted as TRR off.
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${h2Port}/doh?responseIP=2.2.2.2`
+ );
+ await new DNSListener("bar.example.com", "127.0.0.1");
+});
+
+// TRR-first and a CNAME loop
+add_task(async function test20() {
+ dns.clearCache(true);
+ Services.prefs.setIntPref("network.trr.mode", 2); // TRR-first
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${h2Port}/doh?responseIP=none&cnameloop=true`
+ );
+ await new DNSListener("test20.example.com", "127.0.0.1");
+});
+
+// Test that MODE_RESERVED4 (previously MODE_SHADOW) is treated as TRR off.
+add_task(async function test21() {
+ dns.clearCache(true);
+ Services.prefs.setIntPref("network.trr.mode", 4); // MODE_RESERVED4. Interpreted as TRR off.
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${h2Port}/doh?responseIP=none&cnameloop=true`
+ );
+ await new DNSListener("test21.example.com", "127.0.0.1");
+
+ Services.prefs.setIntPref("network.trr.mode", 4); // MODE_RESERVED4. Interpreted as TRR off.
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${h2Port}/doh?responseIP=2.2.2.2`
+ );
+ await new DNSListener("bar.example.com", "127.0.0.1");
+});
+
+// verify that basic A record name mismatch gets rejected.
+// Gets a response for bar.example.com instead of what it requested
+add_task(async function test22() {
+ dns.clearCache(true);
+ Services.prefs.setIntPref("network.trr.mode", 3); // TRR-only to avoid native fallback
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${h2Port}/doh?hostname=bar.example.com`
+ );
+ let [, , inStatus] = await new DNSListener(
+ "mismatch.example.com",
+ undefined,
+ false
+ );
+ Assert.ok(
+ !Components.isSuccessCode(inStatus),
+ `${inStatus} should be an error code`
+ );
+});
+
+// TRR-only, with a CNAME response with a bundled A record for that CNAME!
+add_task(async function test23() {
+ dns.clearCache(true);
+ Services.prefs.setIntPref("network.trr.mode", 3); // TRR-only
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${h2Port}/dns-cname-a`
+ );
+ await new DNSListener("cname-a.example.com", "9.8.7.6");
+});
+
+// TRR-first check that TRR result is used
+add_task(async function test24() {
+ dns.clearCache(true);
+ Services.prefs.setIntPref("network.trr.mode", 2); // TRR-first
+ Services.prefs.setCharPref("network.trr.excluded-domains", "");
+ Services.prefs.setCharPref("network.trr.builtin-excluded-domains", "");
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${h2Port}/doh?responseIP=192.192.192.192`
+ );
+ await new DNSListener("bar.example.com", "192.192.192.192");
+});
+
+// TRR-first check that DNS result is used if domain is part of the excluded-domains pref
+add_task(async function test24b() {
+ dns.clearCache(true);
+ Services.prefs.setCharPref("network.trr.excluded-domains", "bar.example.com");
+ await new DNSListener("bar.example.com", "127.0.0.1");
+});
+
+// TRR-first check that DNS result is used if domain is part of the excluded-domains pref
+add_task(async function test24c() {
+ dns.clearCache(true);
+ Services.prefs.setCharPref("network.trr.excluded-domains", "example.com");
+ await new DNSListener("bar.example.com", "127.0.0.1");
+});
+
+// TRR-first check that DNS result is used if domain is part of the excluded-domains pref that contains things before it.
+add_task(async function test24d() {
+ dns.clearCache(true);
+ Services.prefs.setCharPref(
+ "network.trr.excluded-domains",
+ "foo.test.com, bar.example.com"
+ );
+ await new DNSListener("bar.example.com", "127.0.0.1");
+});
+
+// TRR-first check that DNS result is used if domain is part of the excluded-domains pref that contains things after it.
+add_task(async function test24e() {
+ dns.clearCache(true);
+ Services.prefs.setCharPref(
+ "network.trr.excluded-domains",
+ "bar.example.com, foo.test.com"
+ );
+ await new DNSListener("bar.example.com", "127.0.0.1");
+});
+
+function observerPromise(topic) {
+ return new Promise(resolve => {
+ let observer = {
+ QueryInterface: ChromeUtils.generateQI(["nsIObserver"]),
+ observe(aSubject, aTopic, aData) {
+ dump(`observe: ${aSubject}, ${aTopic}, ${aData} \n`);
+ if (aTopic == topic) {
+ Services.obs.removeObserver(observer, topic);
+ resolve(aData);
+ }
+ },
+ };
+ Services.obs.addObserver(observer, topic);
+ });
+}
+
+// TRR-first check that captivedetect.canonicalURL is resolved via native DNS
+add_task(async function test24f() {
+ dns.clearCache(true);
+
+ 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 = observerPromise("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 DNSListener("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));
+});
+
+// TRR-first check that a domain is resolved via native DNS when parental control is enabled.
+add_task(async function test24g() {
+ dns.clearCache(true);
+ await SetParentalControlEnabled(true);
+ await new DNSListener("www.example.com", "127.0.0.1");
+ await SetParentalControlEnabled(false);
+});
+
+// TRR-first check that DNS result is used if domain is part of the builtin-excluded-domains pref
+add_task(async function test24h() {
+ dns.clearCache(true);
+ Services.prefs.setCharPref("network.trr.excluded-domains", "");
+ Services.prefs.setCharPref(
+ "network.trr.builtin-excluded-domains",
+ "bar.example.com"
+ );
+ await new DNSListener("bar.example.com", "127.0.0.1");
+});
+
+// TRR-first check that DNS result is used if domain is part of the builtin-excluded-domains pref
+add_task(async function test24i() {
+ dns.clearCache(true);
+ Services.prefs.setCharPref(
+ "network.trr.builtin-excluded-domains",
+ "example.com"
+ );
+ await new DNSListener("bar.example.com", "127.0.0.1");
+});
+
+// TRR-first check that DNS result is used if domain is part of the builtin-excluded-domains pref that contains things before it.
+add_task(async function test24j() {
+ dns.clearCache(true);
+ Services.prefs.setCharPref(
+ "network.trr.builtin-excluded-domains",
+ "foo.test.com, bar.example.com"
+ );
+ await new DNSListener("bar.example.com", "127.0.0.1");
+});
+
+// TRR-first check that DNS result is used if domain is part of the builtin-excluded-domains pref that contains things after it.
+add_task(async function test24k() {
+ dns.clearCache(true);
+ Services.prefs.setCharPref(
+ "network.trr.builtin-excluded-domains",
+ "bar.example.com, foo.test.com"
+ );
+ await new DNSListener("bar.example.com", "127.0.0.1");
+});
+
+// TRR-only that resolving excluded with TRR-only mode will use the remote
+// resolver if it's not in the excluded domains
+add_task(async function test25() {
+ dns.clearCache(true);
+ Services.prefs.setIntPref("network.trr.mode", 3); // TRR-only
+ Services.prefs.setCharPref("network.trr.excluded-domains", "");
+ Services.prefs.setCharPref("network.trr.builtin-excluded-domains", "");
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${h2Port}/doh?responseIP=192.192.192.192`
+ );
+
+ await new DNSListener("excluded", "192.192.192.192", true);
+});
+
+// TRR-only check that excluded goes directly to native lookup when in the excluded-domains
+add_task(async function test25b() {
+ dns.clearCache(true);
+ Services.prefs.setIntPref("network.trr.mode", 3); // TRR-only
+ Services.prefs.setCharPref("network.trr.excluded-domains", "excluded");
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${h2Port}/doh?responseIP=192.192.192.192`
+ );
+
+ await new DNSListener("excluded", "127.0.0.1");
+});
+
+// TRR-only check that test.local is resolved via native DNS
+add_task(async function test25c() {
+ dns.clearCache(true);
+ Services.prefs.setIntPref("network.trr.mode", 3); // TRR-only
+ Services.prefs.setCharPref("network.trr.excluded-domains", "excluded,local");
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${h2Port}/doh?responseIP=192.192.192.192`
+ );
+
+ await new DNSListener("test.local", "127.0.0.1");
+});
+
+// TRR-only check that .other is resolved via native DNS when the pref is set
+add_task(async function test25d() {
+ dns.clearCache(true);
+ Services.prefs.setIntPref("network.trr.mode", 3); // TRR-only
+ Services.prefs.setCharPref(
+ "network.trr.excluded-domains",
+ "excluded,local,other"
+ );
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${h2Port}/doh?responseIP=192.192.192.192`
+ );
+
+ await new DNSListener("domain.other", "127.0.0.1");
+});
+
+// TRR-only check that captivedetect.canonicalURL is resolved via native DNS
+add_task({ skip_if: () => true }, async function test25e() {
+ dns.clearCache(true);
+ Services.prefs.setIntPref("network.trr.mode", 3); // TRR-only
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${h2Port}/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 = observerPromise("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 DNSListener("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));
+});
+
+// TRR-only check that a domain is resolved via native DNS when parental control is enabled.
+add_task(async function test25f() {
+ dns.clearCache(true);
+ Services.prefs.setIntPref("network.trr.mode", 3); // TRR-only
+ await SetParentalControlEnabled(true);
+ await new DNSListener("www.example.com", "127.0.0.1");
+ await SetParentalControlEnabled(false);
+});
+
+// TRR-only check that excluded goes directly to native lookup when in the builtin-excluded-domains
+add_task(async function test25g() {
+ dns.clearCache(true);
+ Services.prefs.setIntPref("network.trr.mode", 3); // TRR-only
+ Services.prefs.setCharPref("network.trr.excluded-domains", "");
+ Services.prefs.setCharPref(
+ "network.trr.builtin-excluded-domains",
+ "excluded"
+ );
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${h2Port}/doh?responseIP=192.192.192.192`
+ );
+
+ await new DNSListener("excluded", "127.0.0.1");
+});
+
+// TRR-only check that test.local is resolved via native DNS
+add_task(async function test25h() {
+ dns.clearCache(true);
+ Services.prefs.setIntPref("network.trr.mode", 3); // TRR-only
+ Services.prefs.setCharPref(
+ "network.trr.builtin-excluded-domains",
+ "excluded,local"
+ );
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${h2Port}/doh?responseIP=192.192.192.192`
+ );
+
+ await new DNSListener("test.local", "127.0.0.1");
+});
+
+// TRR-only check that .other is resolved via native DNS when the pref is set
+add_task(async function test25i() {
+ dns.clearCache(true);
+ Services.prefs.setIntPref("network.trr.mode", 3); // TRR-only
+ Services.prefs.setCharPref(
+ "network.trr.builtin-excluded-domains",
+ "excluded,local,other"
+ );
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${h2Port}/doh?responseIP=192.192.192.192`
+ );
+
+ await new DNSListener("domain.other", "127.0.0.1");
+});
+
+// Check that none of the requests have set any cookies.
+add_task(async function count_cookies() {
+ let cm = Cc["@mozilla.org/cookiemanager;1"].getService(Ci.nsICookieManager);
+ Assert.equal(cm.countCookiesFromHost("example.com"), 0);
+ Assert.equal(cm.countCookiesFromHost("foo.example.com."), 0);
+});
+
+add_task(async function test_connection_closed() {
+ dns.clearCache(true);
+ Services.prefs.setIntPref("network.trr.mode", 3); // TRR-only
+ 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);
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${h2Port}/doh?responseIP=2.2.2.2`
+ );
+ // bootstrap
+ Services.prefs.clearUserPref("network.dns.localDomains");
+ Services.prefs.setCharPref("network.trr.bootstrapAddress", "127.0.0.1");
+
+ await new DNSListener("bar.example.com", "2.2.2.2");
+
+ // makes the TRR connection shut down.
+ let [, , inStatus] = await new DNSListener("closeme.com", undefined, false);
+ Assert.ok(
+ !Components.isSuccessCode(inStatus),
+ `${inStatus} should be an error code`
+ );
+ await new DNSListener("bar2.example.com", "2.2.2.2");
+});
+
+add_task(async function test_connection_closed_no_bootstrap() {
+ dns.clearCache(true);
+ Services.prefs.setIntPref("network.trr.mode", 3); // TRR-only
+ Services.prefs.setCharPref("network.trr.excluded-domains", "excluded,local");
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${h2Port}/doh?responseIP=3.3.3.3`
+ );
+ Services.prefs.setCharPref("network.dns.localDomains", "foo.example.com");
+ Services.prefs.clearUserPref("network.trr.bootstrapAddress");
+
+ await new DNSListener("bar.example.com", "3.3.3.3");
+
+ // makes the TRR connection shut down.
+ let [, , inStatus] = await new DNSListener("closeme.com", undefined, false);
+ Assert.ok(
+ !Components.isSuccessCode(inStatus),
+ `${inStatus} should be an error code`
+ );
+ await new DNSListener("bar2.example.com", "3.3.3.3");
+});
+
+add_task(async function test_connection_closed_no_bootstrap_localhost() {
+ dns.clearCache(true);
+ Services.prefs.setIntPref("network.trr.mode", 3); // TRR-only
+ Services.prefs.setCharPref("network.trr.excluded-domains", "excluded");
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://localhost:${h2Port}/doh?responseIP=3.3.3.3`
+ );
+ Services.prefs.clearUserPref("network.dns.localDomains");
+ Services.prefs.clearUserPref("network.trr.bootstrapAddress");
+
+ await new DNSListener("bar.example.com", "3.3.3.3");
+
+ // makes the TRR connection shut down.
+ let [, , inStatus] = await new DNSListener("closeme.com", undefined, false);
+ Assert.ok(
+ !Components.isSuccessCode(inStatus),
+ `${inStatus} should be an error code`
+ );
+ await new DNSListener("bar2.example.com", "3.3.3.3");
+});
+
+add_task(async function test_connection_closed_no_bootstrap_no_excluded() {
+ // This test makes 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.
+ dns.clearCache(true);
+ Services.prefs.setIntPref("network.trr.mode", 3); // TRR-only
+ Services.prefs.setCharPref("network.trr.excluded-domains", "");
+ Services.prefs.setCharPref("network.trr.builtin-excluded-domains", "");
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://localhost:${h2Port}/doh?responseIP=3.3.3.3`
+ );
+ Services.prefs.clearUserPref("network.dns.localDomains");
+ Services.prefs.clearUserPref("network.trr.bootstrapAddress");
+
+ await new DNSListener("bar.example.com", "3.3.3.3");
+
+ // makes the TRR connection shut down.
+ let [, , inStatus] = await new DNSListener("closeme.com", undefined, false);
+ Assert.ok(
+ !Components.isSuccessCode(inStatus),
+ `${inStatus} should be an error code`
+ );
+ dns.clearCache(true);
+ await new DNSListener("bar2.example.com", "3.3.3.3");
+});
+
+add_task(async function test_connection_closed_trr_first() {
+ // 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
+ dns.clearCache(true);
+ Services.prefs.setIntPref("network.trr.mode", 2); // TRR-first
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://localhost:${h2Port}/doh?responseIP=9.9.9.9`
+ );
+ Services.prefs.setCharPref("network.dns.localDomains", "closeme.com");
+ Services.prefs.clearUserPref("network.trr.bootstrapAddress");
+
+ await new DNSListener("bar.example.com", "9.9.9.9");
+
+ // makes the TRR connection shut down. Should fallback to DNS
+ await new DNSListener("closeme.com", "127.0.0.1");
+ // TRR should be back up again
+ await new DNSListener("bar2.example.com", "9.9.9.9");
+});
+
+add_task(async function test_clearCacheOnURIChange() {
+ dns.clearCache(true);
+ Services.prefs.setBoolPref("network.trr.clear-cache-on-pref-change", true);
+ Services.prefs.setIntPref("network.trr.mode", 2); // TRR-first
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://localhost:${h2Port}/doh?responseIP=7.7.7.7`
+ );
+
+ await new DNSListener("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 DNSListener("bar.example.com", "8.8.8.8");
+ Services.prefs.setBoolPref("network.trr.clear-cache-on-pref-change", false);
+});
+
+add_task(async function test_dnsSuffix() {
+ async function checkDnsSuffixInMode(mode) {
+ dns.clearCache(true);
+ Services.prefs.setIntPref("network.trr.mode", mode);
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${h2Port}/doh?responseIP=1.2.3.4&push=true`
+ );
+ await new DNSListener("example.org", "1.2.3.4");
+ await new DNSListener("push.example.org", "2018::2018");
+ await new DNSListener("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 DNSListener("test.com", "1.2.3.4");
+ if (Services.prefs.getBoolPref("network.trr.split_horizon_mitigations")) {
+ await new DNSListener("example.org", "127.0.0.1");
+ // Also test that we don't use the pushed entry.
+ await new DNSListener("push.example.org", "127.0.0.1");
+ } else {
+ await new DNSListener("example.org", "1.2.3.4");
+ await new DNSListener("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.bootstrapAddress", "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.bootstrapAddress");
+});
+
+// Test AsyncResoleWithTrrServer.
+add_task(async function test_async_resolve_with_trr_server_1() {
+ dns.clearCache(true);
+ Services.prefs.setIntPref("network.trr.mode", 0); // TRR-disabled
+
+ await new DNSListener(
+ "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 DNSListener("bar_with_trr1.example.com", "127.0.0.1");
+});
+
+// Test AsyncResoleWithTrrServer.
+add_task(async function test_async_resolve_with_trr_server_2() {
+ dns.clearCache(true);
+ Services.prefs.setIntPref("network.trr.mode", 2); // TRR-first
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${h2Port}/doh?responseIP=2.2.2.2`
+ );
+
+ await new DNSListener(
+ "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 DNSListener("bar_with_trr2.example.com", "2.2.2.2");
+});
+
+// Test AsyncResoleWithTrrServer.
+add_task(async function test_async_resolve_with_trr_server_3() {
+ dns.clearCache(true);
+ Services.prefs.setIntPref("network.trr.mode", 3); // TRR-only
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${h2Port}/doh?responseIP=2.2.2.2`
+ );
+
+ await new DNSListener(
+ "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 DNSListener("bar_with_trr3.example.com", "2.2.2.2");
+});
+
+// Test AsyncResoleWithTrrServer.
+add_task(async function test_async_resolve_with_trr_server_5() {
+ dns.clearCache(true);
+ Services.prefs.setIntPref("network.trr.mode", 5); // TRR-user-disabled
+
+ // When dns is resolved in socket process, we can't set |expectEarlyFail| to true.
+ let inSocketProcess = mozinfo.socketprocess_networking;
+ await new DNSListener(
+ "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 DNSListener("bar_with_trr3.example.com", "127.0.0.1");
+});
+
+// Test AsyncResoleWithTrrServer.
+add_task(async function test_async_resolve_with_trr_server_different_cache() {
+ dns.clearCache(true);
+ Services.prefs.setIntPref("network.trr.mode", 3); // TRR-only
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${h2Port}/doh?responseIP=2.2.2.2`
+ );
+
+ await new DNSListener("bar_with_trr4.example.com", "2.2.2.2", true);
+
+ // The record will be fetch again.
+ await new DNSListener(
+ "bar_with_trr4.example.com",
+ "3.3.3.3",
+ true,
+ undefined,
+ `https://foo.example.com:${h2Port}/doh?responseIP=3.3.3.3`
+ );
+});
+
+// Test AsyncResoleWithTrrServer.
+add_task(async function test_async_resolve_with_trr_server_different_servers() {
+ dns.clearCache(true);
+ Services.prefs.setIntPref("network.trr.mode", 3); // TRR-only
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${h2Port}/doh?responseIP=2.2.2.2`
+ );
+
+ await new DNSListener("bar_with_trr5.example.com", "2.2.2.2", true);
+
+ // The record will be fetch again.
+ await new DNSListener(
+ "bar_with_trr5.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 DNSListener(
+ "bar_with_trr5.example.com",
+ "4.4.4.4",
+ true,
+ undefined,
+ `https://foo.example.com:${h2Port}/doh?responseIP=4.4.4.4`
+ );
+});
+
+// Test AsyncResoleWithTrrServer.
+// AsyncResoleWithTrrServer will failed.
+// There will be no fallback to native and the host will not be blacklisted.
+add_task(async function test_async_resolve_with_trr_server_no_blacklist() {
+ dns.clearCache(true);
+ Services.prefs.setIntPref("network.trr.mode", 2); // TRR-first
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${h2Port}/doh?responseIP=2.2.2.2`
+ );
+
+ let [, , inStatus] = await new DNSListener(
+ "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 DNSListener("bar_with_trr6.example.com", "2.2.2.2", true);
+});
+
+// verify that DOH push does not work with AsyncResoleWithTrrServer.
+add_task(async function test_async_resolve_with_trr_server_no_push() {
+ dns.clearCache(true);
+ Services.prefs.setIntPref("network.trr.mode", 2); // TRR-first
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${h2Port}/doh?responseIP=2.2.2.2`
+ );
+
+ await new DNSListener(
+ "bar_with_trr7.example.com",
+ "3.3.3.3",
+ true,
+ undefined,
+ `https://foo.example.com:${h2Port}/doh?responseIP=3.3.3.3&push=true`
+ );
+});
+
+add_task(async function test_async_resolve_with_trr_server_no_push_part_2() {
+ // AsyncResoleWithTrrServer rejects server pushes and the entry for push.example.org
+ // shouldn't be neither in the default cache not in AsyncResoleWithTrrServer cache.
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${h2Port}/404`
+ );
+ dump(
+ "test_async_resolve_with_trr_server_no_push_part_2 - resolve push.example.org will not be in the cache.\n"
+ );
+
+ await new DNSListener(
+ "push.example.org",
+ "3.3.3.3",
+ true,
+ undefined,
+ `https://foo.example.com:${h2Port}/doh?responseIP=3.3.3.3&push=true`
+ );
+
+ await new DNSListener("push.example.org", "127.0.0.1");
+});
+
+// Verify that AsyncResoleWithTrrServer is not block on confirmationNS of the defaut serveer.
+add_task(async function test_async_resolve_with_trr_server_confirmation_ns() {
+ dns.clearCache(true);
+ Services.prefs.setIntPref("network.trr.mode", 2); // TRR-only
+ Services.prefs.clearUserPref("network.trr.useGET");
+ Services.prefs.clearUserPref("network.trr.disable-ECS");
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${h2Port}/doh?responseIP=1::ffff`
+ );
+ Services.prefs.setCharPref(
+ "network.trr.confirmationNS",
+ "confirm.example.com"
+ );
+
+ // AsyncResoleWithTrrServer will succeed
+ await new DNSListener(
+ "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");
+});
+
+// verify TRR timings
+add_task(async function test_fetch_time() {
+ dns.clearCache(true);
+ Services.prefs.setIntPref("network.trr.mode", 2); // TRR-first
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${h2Port}/doh?responseIP=2.2.2.2&delayIPv4=20`
+ );
+
+ await new DNSListener("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.
+add_task(async function test_no_fetch_time_on_trr_error() {
+ dns.clearCache(true);
+ Services.prefs.setIntPref("network.trr.mode", 2); // TRR-first
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${h2Port}/404&delayIPv4=20`
+ );
+
+ await new DNSListener("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.
+add_task(async function test_no_fetch_time_for_excluded_domains() {
+ dns.clearCache(true);
+ Services.prefs.setCharPref(
+ "network.trr.excluded-domains",
+ "bar_time2.example.com"
+ );
+ Services.prefs.setIntPref("network.trr.mode", 2); // TRR-first
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${h2Port}/doh?responseIP=2.2.2.2&delayIPv4=20`
+ );
+
+ await new DNSListener("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.
+add_task(async function test_no_fetch_time_for_rfc1918_not_allowed() {
+ dns.clearCache(true);
+ Services.prefs.setIntPref("network.trr.mode", 2); // TRR-first
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${h2Port}/doh?responseIP=192.168.0.1&delayIPv4=20`
+ );
+ await new DNSListener("rfc1918_time.example.com", "127.0.0.1", true, 0);
+});
+
+add_task(async function test_content_encoding_gzip() {
+ dns.clearCache(true);
+ Services.prefs.setBoolPref(
+ "network.trr.send_empty_accept-encoding_headers",
+ false
+ );
+ Services.prefs.setIntPref("network.trr.mode", 3);
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${h2Port}/doh?responseIP=2.2.2.2`
+ );
+
+ await new DNSListener("bar.example.com", "2.2.2.2");
+});
+
+add_task(async function test_redirect_get() {
+ dns.clearCache(true);
+ Services.prefs.setIntPref("network.trr.mode", 3); // TRR-only
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${h2Port}/doh?redirect=4.4.4.4{&dns}`
+ );
+ Services.prefs.clearUserPref("network.trr.allow-rfc1918");
+ Services.prefs.setBoolPref("network.trr.useGET", true);
+ Services.prefs.setBoolPref("network.trr.disable-ECS", true);
+ await new DNSListener("ecs.example.com", "4.4.4.4");
+});
+
+// test redirect
+add_task(async function test_redirect_post() {
+ dns.clearCache(true);
+ Services.prefs.setIntPref("network.trr.mode", 3);
+ Services.prefs.setBoolPref("network.trr.useGET", false);
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${h2Port}/doh?redirect=4.4.4.4`
+ );
+
+ await new DNSListener("bar.example.com", "4.4.4.4");
+});
+
+// confirmationNS set without confirmed NS yet
+// checks that we properly fall back to DNS is confirmation is not ready yet
+add_task(async function test_resolve_not_confirmed() {
+ dns.clearCache(true);
+ Services.prefs.setIntPref("network.trr.mode", 2); // TRR-first
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${h2Port}/doh?responseIP=7.7.7.7`
+ );
+ Services.prefs.setCharPref(
+ "network.trr.confirmationNS",
+ "confirm.example.com"
+ );
+
+ await new DNSListener("example.org", "127.0.0.1");
+
+ // 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 DNSListener(
+ `ip${count}.example.org`,
+ undefined,
+ false
+ );
+ inRecord.QueryInterface(Ci.nsIDNSAddrRecord);
+ let responseIP = inRecord.getNextAddrAsString();
+ if (responseIP == "7.7.7.7") {
+ break;
+ }
+ count--;
+ }
+
+ Assert.greater(count, 0, "Finished confirmation before 100 iterations");
+
+ Services.prefs.setCharPref("network.trr.confirmationNS", "skip");
+});
+
+// Tests that we handle FQDN encoding and decoding properly
+add_task(async function test_fqdn() {
+ dns.clearCache(true);
+ Services.prefs.setIntPref("network.trr.mode", 3);
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${h2Port}/doh?responseIP=9.8.7.6`
+ );
+ await new DNSListener("fqdn.example.org.", "9.8.7.6");
+});
+
+add_task(async function test_fqdn_get() {
+ dns.clearCache(true);
+ Services.prefs.setIntPref("network.trr.mode", 3);
+ Services.prefs.setBoolPref("network.trr.useGET", true);
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${h2Port}/doh?responseIP=9.8.7.6`
+ );
+ await new DNSListener("fqdn_get.example.org.", "9.8.7.6");
+
+ Services.prefs.setBoolPref("network.trr.useGET", false);
+});
+
+add_task(async function test_detected_uri() {
+ dns.clearCache(true);
+ Services.prefs.setIntPref("network.trr.mode", 3);
+ Services.prefs.clearUserPref("network.trr.uri");
+ let defaultURI = gDefaultPref.getCharPref("network.trr.uri");
+ gDefaultPref.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${h2Port}/doh?responseIP=3.4.5.6`
+ );
+ await new DNSListener("domainA.example.org.", "3.4.5.6");
+ dns.setDetectedTrrURI(
+ `https://foo.example.com:${h2Port}/doh?responseIP=1.2.3.4`
+ );
+ await new DNSListener("domainB.example.org.", "1.2.3.4");
+ gDefaultPref.setCharPref("network.trr.uri", defaultURI);
+});
+
+add_task(async function test_detected_uri_userSet() {
+ dns.clearCache(true);
+ Services.prefs.setIntPref("network.trr.mode", 3);
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${h2Port}/doh?responseIP=4.5.6.7`
+ );
+ await new DNSListener("domainA.example.org.", "4.5.6.7");
+ // This should be a no-op, since we have a user-set URI
+ dns.setDetectedTrrURI(
+ `https://foo.example.com:${h2Port}/doh?responseIP=1.2.3.4`
+ );
+ await new DNSListener("domainB.example.org.", "4.5.6.7");
+});
+
+add_task(async function test_detected_uri_link_change() {
+ dns.clearCache(true);
+ Services.prefs.setIntPref("network.trr.mode", 3);
+ Services.prefs.clearUserPref("network.trr.uri");
+ let defaultURI = gDefaultPref.getCharPref("network.trr.uri");
+ gDefaultPref.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${h2Port}/doh?responseIP=3.4.5.6`
+ );
+ await new DNSListener("domainA.example.org.", "3.4.5.6");
+ dns.setDetectedTrrURI(
+ `https://foo.example.com:${h2Port}/doh?responseIP=1.2.3.4`
+ );
+ await new DNSListener("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 DNSListener("domainC.example.org.", "3.4.5.6");
+
+ gDefaultPref.setCharPref("network.trr.uri", defaultURI);
+});
+
+add_task(async function test_pref_changes() {
+ Services.prefs.clearUserPref("network.trr.uri");
+ let defaultURI = gDefaultPref.getCharPref("network.trr.uri");
+
+ async function doThenCheckURI(closure, expectedURI, expectChange = false) {
+ let uriChanged;
+ if (expectChange) {
+ uriChanged = observerPromise("network:trr-uri-changed");
+ }
+ closure();
+ if (expectChange) {
+ await uriChanged;
+ }
+ equal(dns.currentTrrURI, expectedURI);
+ }
+
+ // setting the default value of the pref should be reflected in the URI
+ await doThenCheckURI(() => {
+ gDefaultPref.setCharPref(
+ "network.trr.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(() => {
+ 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(
+ () => {
+ 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.uri", defaultURI);
+});
+
+add_task(async function test_async_resolve_with_trr_server_bad_port() {
+ dns.clearCache(true);
+ Services.prefs.setIntPref("network.trr.mode", 2); // TRR-first
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${h2Port}/doh?responseIP=2.2.2.2`
+ );
+
+ let [, , inStatus] = await new DNSListener(
+ "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(async function test_dohrollout_mode() {
+ Services.prefs.clearUserPref("network.trr.mode");
+ Services.prefs.clearUserPref("doh-rollout.mode");
+
+ equal(dns.currentTrrMode, 0);
+
+ async function doThenCheckMode(trrMode, rolloutMode, expectedMode, message) {
+ let modeChanged;
+ if (dns.currentTrrMode != expectedMode) {
+ modeChanged = observerPromise("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(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(dns.currentTrrMode, 2);
+ Services.prefs.clearUserPref("doh-rollout.mode");
+ equal(dns.currentTrrMode, 0);
+});
+
+add_task(async function test_ipv6_trr_fallback() {
+ dns.clearCache(true);
+ let httpserver = new HttpServer();
+ httpserver.registerPathHandler("/content", (metadata, response) => {
+ response.setHeader("Content-Type", "text/plain");
+ response.setHeader("Cache-Control", "no-cache");
+
+ const responseBody = "anybody";
+ response.bodyOutputStream.write(responseBody, responseBody.length);
+ });
+ httpserver.start(-1);
+
+ Services.prefs.setBoolPref("network.captive-portal-service.testMode", true);
+ let url = `http://127.0.0.1:666/doom_port_should_not_be_open`;
+ Services.prefs.setCharPref("network.connectivity-service.IPv6.url", url);
+ let ncs = Cc[
+ "@mozilla.org/network/network-connectivity-service;1"
+ ].getService(Ci.nsINetworkConnectivityService);
+
+ function promiseObserverNotification(topic, 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 });
+ }, topic);
+ });
+ }
+
+ let checks = promiseObserverNotification(
+ "network:connectivity-service:ip-checks-complete"
+ );
+
+ ncs.recheckIPConnectivity();
+
+ await checks;
+ equal(
+ ncs.IPv6,
+ Ci.nsINetworkConnectivityService.NOT_AVAILABLE,
+ "Check IPv6 support (expect NOT_AVAILABLE)"
+ );
+
+ Services.prefs.setIntPref("network.trr.mode", 2);
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${h2Port}/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");
+
+ await new DNSListener(
+ "ipv6.host.com",
+ "1:1::2",
+ true,
+ 0,
+ "",
+ false,
+ Ci.nsIDNSService.RESOLVE_DISABLE_IPV4
+ );
+
+ Services.prefs.clearUserPref("network.captive-portal-service.testMode");
+ Services.prefs.clearUserPref("network.connectivity-service.IPv6.url");
+
+ override.clearOverrides();
+ await httpserver.stop();
+});
+
+add_task(async function test_no_retry_without_doh() {
+ // See 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) {
+ Services.prefs.setIntPref("network.trr.mode", 2);
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${h2Port}/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[0x804b000b],
+ 1,
+ "Expecting only one instance of NS_NET_STATUS_RESOLVED_HOST"
+ );
+ equal(
+ statusCounter.statusCount[0x804b0007],
+ 1,
+ "Expecting only one instance of NS_NET_STATUS_CONNECTING_TO"
+ );
+ }
+
+ await test(`http://unknown.ipv4.stuff:666/path`, "0.0.0.0");
+ await test(`http://unknown.ipv6.stuff:666/path`, "::");
+});
+
+// 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() {
+ 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 DNSListener("cached.example.com", "3.3.3.3");
+ Services.prefs.clearUserPref("doh-rollout.mode");
+
+ await new DNSListener("cached.example.com", "127.0.0.1");
+
+ Services.prefs.setBoolPref("network.trr.clear-cache-on-pref-change", false);
+});
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..aff39d98df
--- /dev/null
+++ b/netwerk/test/unit/test_trr_additional_section.js
@@ -0,0 +1,314 @@
+/* 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 dns = Cc["@mozilla.org/network/dns-service;1"].getService(
+ Ci.nsIDNSService
+);
+
+trr_test_setup();
+registerCleanupFunction(async () => {
+ trr_clear_prefs();
+});
+
+function makeChan(url) {
+ let chan = NetUtil.newChannel({
+ uri: url,
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+ return chan;
+}
+
+let processId;
+
+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?bla=some</h1>");
+});
+
+add_task(async function test_parse_additional_section() {
+ 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",
+ [
+ {
+ name: "something.foo",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "1.2.3.4",
+ },
+ ],
+ [
+ {
+ 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",
+ [
+ {
+ name: "a.foo",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "1.2.3.4",
+ },
+ ],
+ [
+ {
+ name: "b.foo",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "2.3.4.5",
+ },
+ ]
+ );
+ await trrServer.registerDoHAnswers("b.foo", "A", [
+ {
+ 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",
+ [
+ {
+ name: "xyz.foo",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "1.2.3.4",
+ },
+ ],
+ [
+ {
+ 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",
+ [
+ {
+ name: "ipv6.foo",
+ ttl: 55,
+ type: "AAAA",
+ flush: false,
+ data: "2001::a:b:c:d",
+ },
+ ],
+ [
+ {
+ 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",
+ [
+ {
+ name: "ipv6b.foo",
+ ttl: 55,
+ type: "AAAA",
+ flush: false,
+ data: "2001::a:b:c:d",
+ },
+ ],
+ [
+ {
+ 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",
+ [
+ {
+ name: "multiple.foo",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "9.9.9.9",
+ },
+ ],
+ [
+ {
+ // 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", [
+ {
+ 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",
+ [
+ {
+ name: "second.foo",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "1.2.3.4",
+ },
+ ],
+ [
+ {
+ 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" });
+});
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..6880017c5c
--- /dev/null
+++ b/netwerk/test/unit/test_trr_case_sensitivity.js
@@ -0,0 +1,143 @@
+/* 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 dns = Cc["@mozilla.org/network/dns-service;1"].getService(
+ Ci.nsIDNSService
+);
+
+trr_test_setup();
+registerCleanupFunction(async () => {
+ trr_clear_prefs();
+});
+
+function makeChan(url) {
+ let chan = NetUtil.newChannel({
+ uri: url,
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+ return chan;
+}
+
+let processId;
+
+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?bla=some</h1>");
+
+ 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", [
+ {
+ 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", [
+ {
+ 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", [
+ {
+ 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", [
+ {
+ 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", [
+ {
+ 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", [
+ {
+ name: "B.test.com",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "9.9.9.9",
+ },
+ ]);
+ await trrServer.registerDoHAnswers("b.test.com", "A", [
+ {
+ 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", [
+ {
+ 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..8a70f6a9a3
--- /dev/null
+++ b/netwerk/test/unit/test_trr_cname_chain.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";
+
+const dns = Cc["@mozilla.org/network/dns-service;1"].getService(
+ Ci.nsIDNSService
+);
+
+trr_test_setup();
+registerCleanupFunction(async () => {
+ trr_clear_prefs();
+});
+
+function makeChan(url) {
+ let chan = NetUtil.newChannel({
+ uri: url,
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+ return chan;
+}
+
+let processId;
+
+function channelOpenPromise(chan) {
+ return new Promise(resolve => {
+ function finish(req, buffer) {
+ resolve([req, buffer]);
+ }
+ chan.asyncOpen(new ChannelListener(finish));
+ });
+}
+
+add_task(async function test_follow_cnames_same_response() {
+ 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?bla=some</h1>");
+
+ 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", [
+ {
+ 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", [
+ {
+ name: "a.foo",
+ ttl: 55,
+ type: "CNAME",
+ flush: false,
+ data: "b.foo",
+ },
+ ]);
+ await trrServer.registerDoHAnswers("b.foo", "A", [
+ {
+ name: "b.foo",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "2.3.4.5",
+ },
+ ]);
+ await new TRRDNSListener("a.foo", { expectedAnswer: "2.3.4.5" });
+
+ await trrServer.stop();
+});
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..179a753fe6
--- /dev/null
+++ b/netwerk/test/unit/test_trr_extended_error.js
@@ -0,0 +1,340 @@
+/* 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 dns = Cc["@mozilla.org/network/dns-service;1"].getService(
+ Ci.nsIDNSService
+);
+
+trr_test_setup();
+registerCleanupFunction(async () => {
+ trr_clear_prefs();
+});
+
+function makeChan(url) {
+ let chan = NetUtil.newChannel({
+ uri: url,
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+ return chan;
+}
+
+let processId;
+
+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?bla=some</h1>");
+
+ 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_bogus() {
+ await trrServer.registerDoHAnswers("something.foo", "A", [
+ {
+ 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",
+ [],
+ [
+ {
+ name: ".",
+ type: "OPT",
+ class: "IN",
+ options: [
+ {
+ code: "EDNS_ERROR",
+ extended_error: 6, // DNSSEC_BOGUS
+ text: "DNSSec bogus",
+ },
+ ],
+ },
+ ]
+ );
+
+ // 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",
+ [],
+ [
+ {
+ 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",
+ [],
+ [
+ {
+ 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", [
+ {
+ name: "delay1.com",
+ ttl: 55,
+ type: "AAAA",
+ flush: false,
+ data: "::a:b:c:d",
+ },
+ ]);
+ await trrServer.registerDoHAnswers(
+ "delay1.com",
+ "A",
+ [],
+ [
+ {
+ name: ".",
+ type: "OPT",
+ class: "IN",
+ options: [
+ {
+ code: "EDNS_ERROR",
+ extended_error: 17, // Filtered
+ text: "Filtered",
+ },
+ ],
+ },
+ ],
+ 200 // delay
+ );
+
+ // 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",
+ [
+ {
+ name: "delay2.com",
+ ttl: 55,
+ type: "AAAA",
+ flush: false,
+ data: "::a:b:c:d",
+ },
+ ],
+ [],
+ 200 // delay
+ );
+ await trrServer.registerDoHAnswers(
+ "delay2.com",
+ "A",
+ [],
+ [
+ {
+ 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", [
+ {
+ name: "delay3.com",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "1.2.3.4",
+ },
+ ]);
+ await trrServer.registerDoHAnswers(
+ "delay3.com",
+ "AAAA",
+ [],
+ [
+ {
+ name: ".",
+ type: "OPT",
+ class: "IN",
+ options: [
+ {
+ code: "EDNS_ERROR",
+ extended_error: 17, // Filtered
+ text: "Filtered",
+ },
+ ],
+ },
+ ],
+ 200 // delay
+ );
+
+ // 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",
+ [
+ {
+ name: "delay4.com",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "1.2.3.4",
+ },
+ ],
+ [],
+ 200 // delay
+ );
+ await trrServer.registerDoHAnswers(
+ "delay4.com",
+ "AAAA",
+ [],
+ [
+ {
+ 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",
+ [],
+ [
+ {
+ 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..4349149a10
--- /dev/null
+++ b/netwerk/test/unit/test_trr_https_fallback.js
@@ -0,0 +1,1090 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+ChromeUtils.import("resource://gre/modules/NetUtil.jsm");
+
+let prefs;
+let h2Port;
+let h3Port;
+let listen;
+let trrServer;
+
+const dns = Cc["@mozilla.org/network/dns-service;1"].getService(
+ Ci.nsIDNSService
+);
+const certOverrideService = Cc[
+ "@mozilla.org/security/certoverride;1"
+].getService(Ci.nsICertOverrideService);
+const threadManager = Cc["@mozilla.org/thread-manager;1"].getService(
+ Ci.nsIThreadManager
+);
+const mainThread = threadManager.currentThread;
+
+const defaultOriginAttributes = {};
+
+function setup() {
+ let env = Cc["@mozilla.org/process/environment;1"].getService(
+ Ci.nsIEnvironment
+ );
+ h2Port = env.get("MOZHTTP2_PORT");
+ Assert.notEqual(h2Port, null);
+ Assert.notEqual(h2Port, "");
+
+ h3Port = env.get("MOZHTTP3_PORT");
+ Assert.notEqual(h3Port, null);
+ Assert.notEqual(h3Port, "");
+
+ // Set to allow the cert presented by our H2 server
+ do_get_profile();
+ prefs = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch);
+
+ prefs.setBoolPref("network.security.esni.enabled", false);
+ prefs.setBoolPref("network.http.spdy.enabled", true);
+ prefs.setBoolPref("network.http.spdy.enabled.http2", true);
+ // the TRR server is on 127.0.0.1
+ prefs.setCharPref("network.trr.bootstrapAddress", "127.0.0.1");
+
+ // make all native resolve calls "secretly" resolve localhost instead
+ prefs.setBoolPref("network.dns.native-is-localhost", true);
+
+ // 0 - off, 1 - race, 2 TRR first, 3 TRR only, 4 shadow
+ prefs.setIntPref("network.trr.mode", 2); // TRR first
+ prefs.setBoolPref("network.trr.wait-for-portal", false);
+ // don't confirm that TRR is working, just go!
+ prefs.setCharPref("network.trr.confirmationNS", "skip");
+
+ // So we can change the pref without clearing the cache to check a pushed
+ // record with a TRR path that fails.
+ Services.prefs.setBoolPref("network.trr.clear-cache-on-pref-change", false);
+
+ Services.prefs.setBoolPref("network.dns.upgrade_with_https_rr", true);
+ Services.prefs.setBoolPref("network.dns.use_https_rr_as_altsvc", true);
+ Services.prefs.setBoolPref("network.dns.echconfig.enabled", true);
+
+ // The moz-http2 cert is for foo.example.com and is signed by http2-ca.pem
+ // so add that cert to the trust list as a signing cert. // the foo.example.com domain name.
+ const certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(
+ Ci.nsIX509CertDB
+ );
+ addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u");
+}
+
+setup();
+registerCleanupFunction(async () => {
+ prefs.clearUserPref("network.security.esni.enabled");
+ prefs.clearUserPref("network.http.spdy.enabled");
+ prefs.clearUserPref("network.http.spdy.enabled.http2");
+ prefs.clearUserPref("network.dns.localDomains");
+ prefs.clearUserPref("network.dns.native-is-localhost");
+ prefs.clearUserPref("network.trr.mode");
+ prefs.clearUserPref("network.trr.uri");
+ prefs.clearUserPref("network.trr.credentials");
+ prefs.clearUserPref("network.trr.wait-for-portal");
+ prefs.clearUserPref("network.trr.allow-rfc1918");
+ prefs.clearUserPref("network.trr.useGET");
+ prefs.clearUserPref("network.trr.confirmationNS");
+ prefs.clearUserPref("network.trr.bootstrapAddress");
+ prefs.clearUserPref("network.trr.request-timeout");
+ prefs.clearUserPref("network.trr.clear-cache-on-pref-change");
+ prefs.clearUserPref("network.dns.upgrade_with_https_rr");
+ prefs.clearUserPref("network.dns.use_https_rr_as_altsvc");
+ prefs.clearUserPref("network.dns.echconfig.enabled");
+ prefs.clearUserPref("network.dns.echconfig.fallback_to_origin");
+ prefs.clearUserPref("network.dns.httpssvc.reset_exclustion_list");
+ prefs.clearUserPref("network.http.http3.enabled");
+ prefs.clearUserPref("network.dns.httpssvc.http3_fast_fallback_timeout");
+ if (trrServer) {
+ await trrServer.stop();
+ }
+});
+
+class DNSListener {
+ constructor() {
+ this.promise = new Promise(resolve => {
+ this.resolve = resolve;
+ });
+ }
+ onLookupComplete(inRequest, inRecord, inStatus) {
+ this.resolve([inRequest, inRecord, inStatus]);
+ }
+ // So we can await this as a promise.
+ then() {
+ return this.promise.then.apply(this.promise, arguments);
+ }
+}
+
+DNSListener.prototype.QueryInterface = ChromeUtils.generateQI([
+ "nsIDNSListener",
+]);
+
+function makeChan(url) {
+ let chan = NetUtil.newChannel({
+ uri: url,
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+ return chan;
+}
+
+function channelOpenPromise(chan, flags) {
+ return new Promise(resolve => {
+ function finish(req, buffer) {
+ 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", [
+ {
+ name: "test.fallback.com",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 1,
+ name: "test.fallback1.com",
+ values: [
+ { key: "alpn", value: ["h2", "h3"] },
+ { 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"] },
+ { 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"] },
+ { 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"] },
+ { key: "echconfig", value: "456..." },
+ ],
+ },
+ },
+ ]);
+
+ let listener = new DNSListener();
+
+ let request = dns.asyncResolve(
+ "test.fallback.com",
+ dns.RESOLVE_TYPE_HTTPSSVC,
+ 0,
+ null, // resolverInfo
+ listener,
+ mainThread,
+ defaultOriginAttributes
+ );
+
+ let [inRequest, , inStatus] = await listener;
+ Assert.equal(inRequest, request, "correct request was used");
+ Assert.equal(inStatus, Cr.NS_OK, "status OK");
+
+ let chan = makeChan(`https://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.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", [
+ {
+ name: "test.foo.com",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 1,
+ name: "test.foo1.com",
+ values: [
+ { key: "alpn", value: ["h2", "h3"] },
+ { 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"] },
+ { 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"] },
+ { key: "echconfig", value: "456..." },
+ ],
+ },
+ },
+ ]);
+
+ await trrServer.registerDoHAnswers("test.foo.com", "A", [
+ {
+ name: "test.foo.com",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "127.0.0.1",
+ },
+ ]);
+
+ let listener = new DNSListener();
+
+ let request = dns.asyncResolve(
+ "test.foo.com",
+ dns.RESOLVE_TYPE_HTTPSSVC,
+ 0,
+ null, // resolverInfo
+ listener,
+ mainThread,
+ defaultOriginAttributes
+ );
+
+ let [inRequest, , inStatus] = await listener;
+ Assert.equal(inRequest, request, "correct request was used");
+ Assert.equal(inStatus, Cr.NS_OK, "status OK");
+
+ let chan = makeChan(`https://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", false);
+
+ await trrServer.registerDoHAnswers("test.bar.com", "HTTPS", [
+ {
+ name: "test.bar.com",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 1,
+ name: "test.bar1.com",
+ values: [
+ { key: "alpn", value: ["h2", "h3"] },
+ { 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"] },
+ { 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"] },
+ { key: "echconfig", value: "456..." },
+ ],
+ },
+ },
+ ]);
+
+ let listener = new DNSListener();
+
+ let request = dns.asyncResolve(
+ "test.bar.com",
+ dns.RESOLVE_TYPE_HTTPSSVC,
+ 0,
+ null, // resolverInfo
+ listener,
+ mainThread,
+ defaultOriginAttributes
+ );
+
+ let [inRequest, , inStatus] = await listener;
+ Assert.equal(inRequest, request, "correct request was used");
+ Assert.equal(inStatus, Cr.NS_OK, "status OK");
+
+ // 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", [
+ {
+ name: "test.example.com",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 1,
+ name: "test.example1.com",
+ values: [{ key: "alpn", value: ["h2", "h3"] }],
+ },
+ },
+ {
+ name: "test.example.com",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 3,
+ name: "test.example3.com",
+ values: [{ key: "alpn", value: ["h2", "h3"] }],
+ },
+ },
+ ]);
+
+ let listener = new DNSListener();
+
+ let request = dns.asyncResolve(
+ "test.example.com",
+ dns.RESOLVE_TYPE_HTTPSSVC,
+ 0,
+ null, // resolverInfo
+ listener,
+ mainThread,
+ defaultOriginAttributes
+ );
+
+ let [inRequest, , inStatus] = await listener;
+ Assert.equal(inRequest, request, "correct request was used");
+ Assert.equal(inStatus, Cr.NS_OK, "status OK");
+
+ let chan = makeChan(`https://test.example.com:${h2Port}/server-timing`);
+ await channelOpenPromise(chan, CL_EXPECT_LATE_FAILURE | CL_ALLOW_UNKNOWN_CL);
+
+ await trrServer.registerDoHAnswers("test.example.com", "A", [
+ {
+ 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() {
+ 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", [
+ {
+ name: "vulnerable.com",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "127.0.0.1",
+ },
+ ]);
+
+ await trrServer.registerDoHAnswers("vulnerable.com", "HTTPS", [
+ {
+ name: "vulnerable.com",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 1,
+ name: "vulnerable1.com",
+ values: [
+ { key: "alpn", value: ["h2", "h3"] },
+ { 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"] },
+ { 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"] }],
+ },
+ },
+ ]);
+
+ let listener = new DNSListener();
+
+ let request = dns.asyncResolve(
+ "vulnerable.com",
+ dns.RESOLVE_TYPE_HTTPSSVC,
+ 0,
+ null, // resolverInfo
+ listener,
+ mainThread,
+ defaultOriginAttributes
+ );
+
+ let [inRequest, , inStatus] = await listener;
+ Assert.equal(inRequest, request, "correct request was used");
+ Assert.equal(inStatus, Cr.NS_OK, "status OK");
+
+ let chan = makeChan(`https://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", [
+ {
+ name: "test.reset.com",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 1,
+ name: "test.reset1.com",
+ values: [
+ { key: "alpn", value: ["h2", "h3"] },
+ { 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"] },
+ { key: "echconfig", value: "456..." },
+ ],
+ },
+ },
+ ]);
+
+ let listener = new DNSListener();
+
+ let request = dns.asyncResolve(
+ "test.reset.com",
+ dns.RESOLVE_TYPE_HTTPSSVC,
+ 0,
+ null, // resolverInfo
+ listener,
+ mainThread,
+ defaultOriginAttributes
+ );
+
+ let [inRequest, , inStatus] = await listener;
+ Assert.equal(inRequest, request, "correct request was used");
+ Assert.equal(inStatus, Cr.NS_OK, "status OK");
+
+ // 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", [
+ {
+ 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.enabled", true);
+
+ await trrServer.registerDoHAnswers("test.h3.com", "HTTPS", [
+ {
+ name: "test.h3.com",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 1,
+ name: "www.h3.com",
+ values: [
+ { key: "alpn", value: "h3-27" },
+ { key: "port", value: h3Port },
+ { key: "echconfig", value: "456..." },
+ ],
+ },
+ },
+ ]);
+
+ await trrServer.registerDoHAnswers("www.h3.com", "A", [
+ {
+ name: "www.h3.com",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "127.0.0.1",
+ },
+ ]);
+
+ let listener = new DNSListener();
+
+ let request = dns.asyncResolve(
+ "test.h3.com",
+ dns.RESOLVE_TYPE_HTTPSSVC,
+ 0,
+ null, // resolverInfo
+ listener,
+ mainThread,
+ defaultOriginAttributes
+ );
+
+ let [inRequest, , inStatus] = await listener;
+ Assert.equal(inRequest, request, "correct request was used");
+ Assert.equal(inStatus, Cr.NS_OK, "status OK");
+
+ let chan = makeChan(`https://test.h3.com`);
+ let [req] = await channelOpenPromise(chan);
+ Assert.equal(req.protocolVersion, "h3");
+ 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.enabled", 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
+ );
+
+ await trrServer.registerDoHAnswers("test.fastfallback.com", "HTTPS", [
+ {
+ name: "test.fastfallback.com",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 1,
+ name: "test.fastfallback1.com",
+ values: [
+ { key: "alpn", value: "h3-27" },
+ { key: "port", value: h3Port },
+ { key: "echconfig", value: "456..." },
+ ],
+ },
+ },
+ {
+ name: "test.fastfallback.com",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 2,
+ name: "test.fastfallback2.com",
+ values: [
+ { key: "alpn", value: "h2" },
+ { key: "port", value: h2Port },
+ { key: "echconfig", value: "456..." },
+ ],
+ },
+ },
+ ]);
+
+ await trrServer.registerDoHAnswers("test.fastfallback2.com", "A", [
+ {
+ name: "test.fastfallback2.com",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "127.0.0.1",
+ },
+ ]);
+
+ let listener = new DNSListener();
+
+ let request = dns.asyncResolve(
+ "test.fastfallback.com",
+ dns.RESOLVE_TYPE_HTTPSSVC,
+ 0,
+ null, // resolverInfo
+ listener,
+ mainThread,
+ defaultOriginAttributes
+ );
+
+ let [inRequest, , inStatus] = await listener;
+ Assert.equal(inRequest, request, "correct request was used");
+ Assert.equal(inStatus, Cr.NS_OK, "status OK");
+
+ let chan = makeChan(`https://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();
+ 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.enabled", true);
+ Services.prefs.setIntPref(
+ "network.dns.httpssvc.http3_fast_fallback_timeout",
+ 0
+ );
+
+ await trrServer.registerDoHAnswers("test.h3.org", "HTTPS", [
+ {
+ name: "test.h3.org",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 1,
+ name: "www.h3.org",
+ values: [
+ { key: "alpn", value: "h3-27" },
+ { key: "port", value: h3Port },
+ { key: "echconfig", value: "456..." },
+ ],
+ },
+ },
+ ]);
+
+ let listener = new DNSListener();
+
+ let request = dns.asyncResolve(
+ "test.h3.org",
+ dns.RESOLVE_TYPE_HTTPSSVC,
+ 0,
+ null, // resolverInfo
+ listener,
+ mainThread,
+ defaultOriginAttributes
+ );
+
+ let [inRequest, , inStatus] = await listener;
+ Assert.equal(inRequest, request, "correct request was used");
+ Assert.equal(inStatus, Cr.NS_OK, "status OK");
+
+ let chan = makeChan(`https://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();
+ 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.enabled", 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-27=:" + 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", [
+ {
+ name: "test.h3_excluded.org",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 1,
+ name: "www.h3_fail.org",
+ values: [
+ { key: "alpn", value: "h3-27" },
+ { 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-27" },
+ { key: "port", value: h3Port },
+ ],
+ },
+ },
+ ]);
+
+ let listener = new DNSListener();
+
+ let request = dns.asyncResolve(
+ "test.h3_excluded.org",
+ dns.RESOLVE_TYPE_HTTPSSVC,
+ 0,
+ null, // resolverInfo
+ listener,
+ mainThread,
+ defaultOriginAttributes
+ );
+
+ let [inRequest, , inStatus] = await listener;
+ Assert.equal(inRequest, request, "correct request was used");
+ Assert.equal(inStatus, Cr.NS_OK, "status OK");
+
+ chan = makeChan(`https://test.h3_excluded.org`);
+ let [req] = await channelOpenPromise(chan);
+ Assert.equal(req.protocolVersion, "h3");
+ 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();
+ 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.enabled", 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-27=:" + h3Port
+ );
+
+ await trrServer.registerDoHAnswers("www.h3_all_excluded.org", "A", [
+ {
+ 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-27=:" + 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", [
+ {
+ 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-27" },
+ { 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-27" },
+ { key: "port", value: h3Port },
+ { key: "echconfig", value: "456..." },
+ ],
+ },
+ },
+ ]);
+
+ let listener = new DNSListener();
+
+ let request = dns.asyncResolve(
+ "www.h3_all_excluded.org",
+ dns.RESOLVE_TYPE_HTTPSSVC,
+ 0,
+ null, // resolverInfo
+ listener,
+ mainThread,
+ defaultOriginAttributes
+ );
+
+ let [inRequest, , inStatus] = await listener;
+ Assert.equal(inRequest, request, "correct request was used");
+ Assert.equal(inStatus, Cr.NS_OK, "status OK");
+
+ // 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", [
+ {
+ 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");
+ let internal = req.QueryInterface(Ci.nsIHttpChannelInternal);
+ Assert.equal(internal.remotePort, h3Port);
+
+ 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..5a398dc113
--- /dev/null
+++ b/netwerk/test/unit/test_trr_httpssvc.js
@@ -0,0 +1,669 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+ChromeUtils.import("resource://gre/modules/NetUtil.jsm");
+
+let prefs;
+let h2Port;
+let listen;
+
+function inChildProcess() {
+ return (
+ Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime)
+ .processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT
+ );
+}
+
+const dns = Cc["@mozilla.org/network/dns-service;1"].getService(
+ Ci.nsIDNSService
+);
+const threadManager = Cc["@mozilla.org/thread-manager;1"].getService(
+ Ci.nsIThreadManager
+);
+const mainThread = threadManager.currentThread;
+
+const defaultOriginAttributes = {};
+
+function setup() {
+ let env = Cc["@mozilla.org/process/environment;1"].getService(
+ Ci.nsIEnvironment
+ );
+ h2Port = env.get("MOZHTTP2_PORT");
+ Assert.notEqual(h2Port, null);
+ Assert.notEqual(h2Port, "");
+
+ // Set to allow the cert presented by our H2 server
+ do_get_profile();
+ prefs = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch);
+
+ prefs.setBoolPref("network.http.spdy.enabled", true);
+ prefs.setBoolPref("network.http.spdy.enabled.http2", true);
+ // the TRR server is on 127.0.0.1
+ prefs.setCharPref("network.trr.bootstrapAddress", "127.0.0.1");
+
+ // make all native resolve calls "secretly" resolve localhost instead
+ prefs.setBoolPref("network.dns.native-is-localhost", true);
+
+ // 0 - off, 1 - race, 2 TRR first, 3 TRR only, 4 shadow
+ prefs.setIntPref("network.trr.mode", 2); // TRR first
+ prefs.setBoolPref("network.trr.wait-for-portal", false);
+ // don't confirm that TRR is working, just go!
+ prefs.setCharPref("network.trr.confirmationNS", "skip");
+
+ // So we can change the pref without clearing the cache to check a pushed
+ // record with a TRR path that fails.
+ Services.prefs.setBoolPref("network.trr.clear-cache-on-pref-change", false);
+
+ // The moz-http2 cert is for foo.example.com and is signed by http2-ca.pem
+ // so add that cert to the trust list as a signing cert. // the foo.example.com domain name.
+ const certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(
+ Ci.nsIX509CertDB
+ );
+ addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u");
+}
+
+if (!inChildProcess()) {
+ setup();
+ registerCleanupFunction(() => {
+ prefs.clearUserPref("network.http.spdy.enabled");
+ prefs.clearUserPref("network.http.spdy.enabled.http2");
+ prefs.clearUserPref("network.dns.localDomains");
+ prefs.clearUserPref("network.dns.native-is-localhost");
+ prefs.clearUserPref("network.trr.mode");
+ prefs.clearUserPref("network.trr.uri");
+ prefs.clearUserPref("network.trr.credentials");
+ prefs.clearUserPref("network.trr.wait-for-portal");
+ prefs.clearUserPref("network.trr.allow-rfc1918");
+ prefs.clearUserPref("network.trr.useGET");
+ prefs.clearUserPref("network.trr.confirmationNS");
+ prefs.clearUserPref("network.trr.bootstrapAddress");
+ prefs.clearUserPref("network.trr.blacklist-duration");
+ prefs.clearUserPref("network.trr.request-timeout");
+ prefs.clearUserPref("network.trr.clear-cache-on-pref-change");
+ });
+}
+
+class DNSListener {
+ constructor() {
+ this.promise = new Promise(resolve => {
+ this.resolve = resolve;
+ });
+ }
+ onLookupComplete(inRequest, inRecord, inStatus) {
+ this.resolve([inRequest, inRecord, inStatus]);
+ }
+ // So we can await this as a promise.
+ then() {
+ return this.promise.then.apply(this.promise, arguments);
+ }
+}
+
+DNSListener.prototype.QueryInterface = ChromeUtils.generateQI([
+ "nsIDNSListener",
+]);
+
+add_task(async function testHTTPSSVC() {
+ // use the h2 server as DOH provider
+ if (!inChildProcess()) {
+ prefs.setCharPref(
+ "network.trr.uri",
+ "https://foo.example.com:" + h2Port + "/httpssvc"
+ );
+ }
+
+ let listenerEsni = new DNSListener();
+ let request = dns.asyncResolve(
+ "test.httpssvc.com",
+ dns.RESOLVE_TYPE_HTTPSSVC,
+ 0,
+ null, // resolverInfo
+ listenerEsni,
+ mainThread,
+ defaultOriginAttributes
+ );
+
+ let [inRequest, inRecord, inStatus] = await listenerEsni;
+ Assert.equal(inRequest, request, "correct request was used");
+ Assert.equal(inStatus, Cr.NS_OK, "status OK");
+ let answer = inRecord.QueryInterface(Ci.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() {
+ let trrServer = new TRRServer();
+ registerCleanupFunction(async () => {
+ await trrServer.stop();
+ });
+ 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`
+ );
+ }
+
+ // Test that HTTPS priority = 0 (AliasForm) behaves like a CNAME
+ await trrServer.registerDoHAnswers("test.com", "A", [
+ {
+ name: "test.com",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 0,
+ name: "something.com",
+ values: [],
+ },
+ },
+ ]);
+ await trrServer.registerDoHAnswers("something.com", "A", [
+ {
+ name: "something.com",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "1.2.3.4",
+ },
+ ]);
+
+ await new TRRDNSListener("test.com", { expectedAnswer: "1.2.3.4" });
+
+ // Test a chain of HTTPSSVC AliasForm and CNAMEs
+ await trrServer.registerDoHAnswers("x.com", "A", [
+ {
+ name: "x.com",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 0,
+ name: "y.com",
+ values: [],
+ },
+ },
+ ]);
+ await trrServer.registerDoHAnswers("y.com", "A", [
+ {
+ name: "y.com",
+ type: "CNAME",
+ ttl: 55,
+ class: "IN",
+ flush: false,
+ data: "z.com",
+ },
+ ]);
+ await trrServer.registerDoHAnswers("z.com", "A", [
+ {
+ name: "z.com",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 0,
+ name: "target.com",
+ values: [],
+ },
+ },
+ ]);
+ await trrServer.registerDoHAnswers("target.com", "A", [
+ {
+ name: "target.com",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "4.3.2.1",
+ },
+ ]);
+
+ await new TRRDNSListener("x.com", { expectedAnswer: "4.3.2.1" });
+
+ // We get a ServiceForm instead of a A answer, CNAME or AliasForm
+ await trrServer.registerDoHAnswers("no-ip-host.com", "A", [
+ {
+ 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" },
+ ],
+ },
+ },
+ ]);
+
+ let [, , 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", "A", [
+ {
+ name: "loop.com",
+ type: "CNAME",
+ ttl: 55,
+ class: "IN",
+ flush: false,
+ data: "loop2.com",
+ },
+ ]);
+ await trrServer.registerDoHAnswers("loop2.com", "A", [
+ {
+ name: "loop2.com",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 0,
+ name: "loop.com",
+ values: [],
+ },
+ },
+ ]);
+
+ [, , inStatus] = await new TRRDNSListener("loop.com", {
+ expectedSuccess: false,
+ });
+ Assert.ok(
+ !Components.isSuccessCode(inStatus),
+ `${inStatus} should be an error code`
+ );
+
+ // Alias form for .
+ await trrServer.registerDoHAnswers("empty.com", "A", [
+ {
+ 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", [
+ {
+ 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 listener = new DNSListener();
+ let request = dns.asyncResolve(
+ "multi.com",
+ dns.RESOLVE_TYPE_HTTPSSVC,
+ 0,
+ null, // resolverInfo
+ listener,
+ mainThread,
+ defaultOriginAttributes
+ );
+
+ let [inRequest, inRecord, inStatus2] = await listener;
+ Assert.equal(inRequest, request, "correct request was used");
+ Assert.ok(
+ !Components.isSuccessCode(inStatus2),
+ `${inStatus2} should be an error code`
+ );
+
+ // the svcparam keys are in reverse order
+ await trrServer.registerDoHAnswers("order.com", "HTTPS", [
+ {
+ 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"] },
+ ],
+ },
+ },
+ ]);
+
+ listener = new DNSListener();
+ request = dns.asyncResolve(
+ "order.com",
+ dns.RESOLVE_TYPE_HTTPSSVC,
+ 0,
+ null, // resolverInfo
+ listener,
+ mainThread,
+ defaultOriginAttributes
+ );
+
+ [inRequest, inRecord, inStatus2] = await listener;
+ Assert.equal(inRequest, request, "correct request was used");
+ Assert.ok(
+ !Components.isSuccessCode(inStatus2),
+ `${inStatus2} should be an error code`
+ );
+
+ // duplicate svcparam keys
+ await trrServer.registerDoHAnswers("duplicate.com", "HTTPS", [
+ {
+ 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"] },
+ ],
+ },
+ },
+ ]);
+
+ listener = new DNSListener();
+ request = dns.asyncResolve(
+ "duplicate.com",
+ dns.RESOLVE_TYPE_HTTPSSVC,
+ 0,
+ null, // resolverInfo
+ listener,
+ mainThread,
+ defaultOriginAttributes
+ );
+
+ [inRequest, inRecord, inStatus2] = await listener;
+ Assert.equal(inRequest, request, "correct request was used");
+ Assert.ok(
+ !Components.isSuccessCode(inStatus2),
+ `${inStatus2} should be an error code`
+ );
+
+ // mandatory svcparam
+ await trrServer.registerDoHAnswers("mandatory.com", "HTTPS", [
+ {
+ 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" },
+ ],
+ },
+ },
+ ]);
+
+ listener = new DNSListener();
+ request = dns.asyncResolve(
+ "mandatory.com",
+ dns.RESOLVE_TYPE_HTTPSSVC,
+ 0,
+ null, // resolverInfo
+ listener,
+ mainThread,
+ defaultOriginAttributes
+ );
+
+ [inRequest, inRecord, inStatus2] = await listener;
+ Assert.equal(inRequest, request, "correct request was used");
+ Assert.ok(!Components.isSuccessCode(inStatus2), `${inStatus2} should fail`);
+
+ // mandatory svcparam
+ await trrServer.registerDoHAnswers("mandatory2.com", "HTTPS", [
+ {
+ 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" },
+ ],
+ },
+ },
+ ]);
+
+ listener = new DNSListener();
+ request = dns.asyncResolve(
+ "mandatory2.com",
+ dns.RESOLVE_TYPE_HTTPSSVC,
+ 0,
+ null, // resolverInfo
+ listener,
+ mainThread,
+ defaultOriginAttributes
+ );
+
+ [inRequest, inRecord, inStatus2] = await listener;
+ Assert.equal(inRequest, request, "correct request was used");
+ Assert.ok(Components.isSuccessCode(inStatus2), `${inStatus2} should succeed`);
+
+ // alias-mode with . targetName
+ await trrServer.registerDoHAnswers("no-alias.com", "HTTPS", [
+ {
+ name: "no-alias.com",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 0,
+ name: ".",
+ values: [],
+ },
+ },
+ ]);
+
+ listener = new DNSListener();
+ request = dns.asyncResolve(
+ "no-alias.com",
+ dns.RESOLVE_TYPE_HTTPSSVC,
+ 0,
+ null, // resolverInfo
+ listener,
+ mainThread,
+ defaultOriginAttributes
+ );
+
+ [inRequest, inRecord, inStatus2] = await listener;
+ Assert.equal(inRequest, request, "correct request was used");
+ Assert.ok(!Components.isSuccessCode(inStatus2), `${inStatus2} should fail`);
+
+ // service-mode with . targetName
+ await trrServer.registerDoHAnswers("service.com", "HTTPS", [
+ {
+ name: "service.com",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 1,
+ name: ".",
+ values: [{ key: "alpn", value: ["h2", "h3"] }],
+ },
+ },
+ ]);
+
+ listener = new DNSListener();
+ request = dns.asyncResolve(
+ "service.com",
+ dns.RESOLVE_TYPE_HTTPSSVC,
+ 0,
+ null, // resolverInfo
+ listener,
+ mainThread,
+ defaultOriginAttributes
+ );
+
+ [inRequest, inRecord, inStatus2] = await listener;
+ Assert.equal(inRequest, request, "correct request was used");
+ Assert.ok(Components.isSuccessCode(inStatus2), `${inStatus2} should work`);
+ let answer = inRecord.QueryInterface(Ci.nsIDNSHTTPSSVCRecord).records;
+ Assert.equal(answer[0].priority, 1);
+ Assert.equal(answer[0].name, "service.com");
+});
diff --git a/netwerk/test/unit/test_trr_nat64.js b/netwerk/test/unit/test_trr_nat64.js
new file mode 100644
index 0000000000..fd48c1cbdf
--- /dev/null
+++ b/netwerk/test/unit/test_trr_nat64.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/. */
+
+"use strict";
+
+const override = Cc["@mozilla.org/network/native-dns-override;1"].getService(
+ Ci.nsINativeDNSResolverOverride
+);
+const dns = Cc["@mozilla.org/network/dns-service;1"].getService(
+ Ci.nsIDNSService
+);
+
+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, topic, 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;
+}
+
+let processId;
+
+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?bla=some</h1>");
+ dns.clearCache(true);
+ override.addIPOverride("ipv4only.arpa", "fe80::9b2b:c000:00aa");
+ Services.prefs.setCharPref(
+ "network.connectivity-service.nat64-prefix",
+ "ae80::3b1b:c343:1133"
+ );
+
+ let notification = promiseObserverNotification(
+ "network:connectivity-service:dns-checks-complete"
+ );
+ 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", [
+ {
+ 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_proxy.js b/netwerk/test/unit/test_trr_proxy.js
new file mode 100644
index 0000000000..c091239e8b
--- /dev/null
+++ b/netwerk/test/unit/test_trr_proxy.js
@@ -0,0 +1,133 @@
+/* globals dnsResolve */
+
+/* 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.import("resource://testing-common/httpd.js");
+const { MockRegistrar } = ChromeUtils.import(
+ "resource://testing-common/MockRegistrar.jsm"
+);
+const dns = Cc["@mozilla.org/network/dns-service;1"].getService(
+ Ci.nsIDNSService
+);
+
+trr_test_setup();
+registerCleanupFunction(async () => {
+ trr_clear_prefs();
+});
+
+function FindProxyForURL(url, host) {
+ alert(`PAC resolving: ${host}`);
+ alert(dnsResolve(host));
+ return "DIRECT";
+}
+
+const CID = Components.ID("{5645d2c1-d6d8-4091-b117-fe7ee4027db7}");
+XPCOMUtils.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
+);
+
+add_task(async function test_pac_dnsResolve() {
+ 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 env = Cc["@mozilla.org/process/environment;1"].getService(
+ Ci.nsIEnvironment
+ );
+ let h2Port = 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`
+ );
+
+ 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();
+});
diff --git a/netwerk/test/unit/test_udp_multicast.js b/netwerk/test/unit/test_udp_multicast.js
new file mode 100644
index 0000000000..76d2c93ba9
--- /dev/null
+++ b/netwerk/test/unit/test_udp_multicast.js
@@ -0,0 +1,114 @@
+// 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;
+
+const ua = Cc["@mozilla.org/network/protocol;1?name=http"].getService(
+ Ci.nsIHttpProtocolHandler
+).userAgent;
+
+var gConverter;
+
+function run_test() {
+ setup();
+ run_next_test();
+}
+
+function setup() {
+ gConverter = Cc[
+ "@mozilla.org/intl/scriptableunicodeconverter"
+ ].createInstance(Ci.nsIScriptableUnicodeConverter);
+ gConverter.charset = "utf8";
+}
+
+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 = gConverter.convertToByteArray(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(socket, 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")
+ );
+});
+
+add_test(() => {
+ 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
+ );
+});
+
+add_test(() => {
+ 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..8f6e351f7e
--- /dev/null
+++ b/netwerk/test/unit/test_udpsocket_offline.js
@@ -0,0 +1,108 @@
+/* -*- 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);
+ }
+
+ 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);
+ }
+
+ run_next_test();
+});
+
+function run_test() {
+ // jshint ignore:line
+ Services.io.offline = true;
+ registerCleanupFunction(() => {
+ Services.io.offline = false;
+ });
+ 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..a791983cb7
--- /dev/null
+++ b/netwerk/test/unit/test_unescapestring.js
@@ -0,0 +1,34 @@
+"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() {
+ var util = Cc["@mozilla.org/network/util;1"].getService(Ci.nsINetUtil);
+
+ for (var i = 0; i < tests.length; ++i) {
+ dump("Test " + i + " (" + tests[i][0] + ", " + tests[i][2] + ")\n");
+ Assert.equal(util.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..0172c034ef
--- /dev/null
+++ b/netwerk/test/unit/test_unix_domain.js
@@ -0,0 +1,705 @@
+// 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 IOService = Cc["@mozilla.org/network/io-service;1"].getService(
+ Ci.nsIIOService
+);
+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("directory-that-does-not-exist");
+ 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(aStream) {
+ 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(aStream) {
+ 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) {
+ IOService.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..dca1343474
--- /dev/null
+++ b/netwerk/test/unit/test_use_httpssvc.js
@@ -0,0 +1,325 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+ChromeUtils.import("resource://gre/modules/NetUtil.jsm");
+
+let prefs;
+let h2Port;
+let listen;
+
+const dns = Cc["@mozilla.org/network/dns-service;1"].getService(
+ Ci.nsIDNSService
+);
+const certOverrideService = Cc[
+ "@mozilla.org/security/certoverride;1"
+].getService(Ci.nsICertOverrideService);
+const threadManager = Cc["@mozilla.org/thread-manager;1"].getService(
+ Ci.nsIThreadManager
+);
+const mainThread = threadManager.currentThread;
+
+const defaultOriginAttributes = {};
+
+function setup() {
+ let env = Cc["@mozilla.org/process/environment;1"].getService(
+ Ci.nsIEnvironment
+ );
+ h2Port = env.get("MOZHTTP2_PORT");
+ Assert.notEqual(h2Port, null);
+ Assert.notEqual(h2Port, "");
+
+ // Set to allow the cert presented by our H2 server
+ do_get_profile();
+ prefs = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch);
+
+ prefs.setBoolPref("network.http.spdy.enabled", true);
+ prefs.setBoolPref("network.http.spdy.enabled.http2", true);
+ // the TRR server is on 127.0.0.1
+ prefs.setCharPref("network.trr.bootstrapAddress", "127.0.0.1");
+
+ // make all native resolve calls "secretly" resolve localhost instead
+ prefs.setBoolPref("network.dns.native-is-localhost", true);
+
+ // 0 - off, 1 - race, 2 TRR first, 3 TRR only, 4 shadow
+ prefs.setIntPref("network.trr.mode", 2); // TRR first
+ prefs.setBoolPref("network.trr.wait-for-portal", false);
+ // don't confirm that TRR is working, just go!
+ prefs.setCharPref("network.trr.confirmationNS", "skip");
+
+ // So we can change the pref without clearing the cache to check a pushed
+ // record with a TRR path that fails.
+ Services.prefs.setBoolPref("network.trr.clear-cache-on-pref-change", false);
+
+ Services.prefs.setBoolPref("network.dns.upgrade_with_https_rr", true);
+ Services.prefs.setBoolPref("network.dns.use_https_rr_as_altsvc", true);
+
+ // The moz-http2 cert is for foo.example.com and is signed by http2-ca.pem
+ // so add that cert to the trust list as a signing cert. // the foo.example.com domain name.
+ const certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(
+ Ci.nsIX509CertDB
+ );
+ addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u");
+}
+
+setup();
+registerCleanupFunction(() => {
+ prefs.clearUserPref("network.http.spdy.enabled");
+ prefs.clearUserPref("network.http.spdy.enabled.http2");
+ prefs.clearUserPref("network.dns.localDomains");
+ prefs.clearUserPref("network.dns.native-is-localhost");
+ prefs.clearUserPref("network.trr.mode");
+ prefs.clearUserPref("network.trr.uri");
+ prefs.clearUserPref("network.trr.credentials");
+ prefs.clearUserPref("network.trr.wait-for-portal");
+ prefs.clearUserPref("network.trr.allow-rfc1918");
+ prefs.clearUserPref("network.trr.useGET");
+ prefs.clearUserPref("network.trr.confirmationNS");
+ prefs.clearUserPref("network.trr.bootstrapAddress");
+ prefs.clearUserPref("network.trr.blacklist-duration");
+ prefs.clearUserPref("network.trr.request-timeout");
+ prefs.clearUserPref("network.trr.clear-cache-on-pref-change");
+ prefs.clearUserPref("network.dns.upgrade_with_https_rr");
+ prefs.clearUserPref("network.dns.use_https_rr_as_altsvc");
+});
+
+class DNSListener {
+ constructor() {
+ this.promise = new Promise(resolve => {
+ this.resolve = resolve;
+ });
+ }
+ onLookupComplete(inRequest, inRecord, inStatus) {
+ this.resolve([inRequest, inRecord, inStatus]);
+ }
+ // So we can await this as a promise.
+ then() {
+ return this.promise.then.apply(this.promise, arguments);
+ }
+}
+
+DNSListener.prototype.QueryInterface = ChromeUtils.generateQI([
+ "nsIDNSListener",
+]);
+
+function makeChan(url) {
+ let chan = NetUtil.newChannel({
+ uri: url,
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+ return chan;
+}
+
+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
+ prefs.setCharPref(
+ "network.trr.uri",
+ "https://foo.example.com:" + h2Port + "/httpssvc_as_altsvc"
+ );
+ 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;
+ }
+ }
+ 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
+ prefs.setCharPref(
+ "network.trr.uri",
+ "https://foo.example.com:" + h2Port + "/httpssvc_as_altsvc"
+ );
+ 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
+ prefs.setCharPref(
+ "network.trr.uri",
+ "https://foo.example.com:" + h2Port + "/httpssvc_as_altsvc"
+ );
+
+ let listener = new DNSListener();
+
+ // Do DNS resolution before creating the channel, so the HTTPSSVC record will
+ // be resolved from the cache.
+ let request = dns.asyncResolve(
+ "test.httpssvc.com",
+ dns.RESOLVE_TYPE_HTTPSSVC,
+ 0,
+ null, // resolverInfo
+ listener,
+ mainThread,
+ defaultOriginAttributes
+ );
+
+ let [inRequest, , inStatus] = await listener;
+ Assert.equal(inRequest, request, "correct request was used");
+ Assert.equal(inStatus, Cr.NS_OK, "status OK");
+
+ // 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", [
+ {
+ 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", [
+ {
+ name: "test.fallback.com",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 1,
+ name: "foo.example.com",
+ values: [{ key: "port", value: 8888 }],
+ },
+ },
+ ]);
+
+ let listener = new DNSListener();
+
+ let request = dns.asyncResolve(
+ "test.fallback.com",
+ dns.RESOLVE_TYPE_HTTPSSVC,
+ 0,
+ null, // resolverInfo
+ listener,
+ mainThread,
+ defaultOriginAttributes
+ );
+
+ let [inRequest, inRecord, inStatus] = await listener;
+ Assert.equal(inRequest, request, "correct request was used");
+ Assert.equal(inStatus, Cr.NS_OK, "status OK");
+
+ let 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_websocket_offline.js b/netwerk/test/unit/test_websocket_offline.js
new file mode 100644
index 0000000000..35aafddda8
--- /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_xmlhttprequest.js b/netwerk/test/unit/test_xmlhttprequest.js
new file mode 100644
index 0000000000..06dfbb7b69
--- /dev/null
+++ b/netwerk/test/unit/test_xmlhttprequest.js
@@ -0,0 +1,55 @@
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+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/xpcshell.ini b/netwerk/test/unit/xpcshell.ini
new file mode 100644
index 0000000000..d584907489
--- /dev/null
+++ b/netwerk/test/unit/xpcshell.ini
@@ -0,0 +1,517 @@
+[DEFAULT]
+head = head_channels.js head_cache.js head_cache2.js head_cookies.js head_trr.js head_http3.js
+support-files =
+ http2-ca.pem
+ client_cert_chooser.js
+ client_cert_chooser.manifest
+ 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
+
+[test_trr_nat64.js]
+skip-if = os == "android" || socketprocess_networking
+run-sequentially = node server exceptions dont replay well
+[test_nsIBufferedOutputStream_writeFrom_block.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-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_partial_response_entry_size_smart_shrink.js]
+[test_304_responses.js]
+[test_421.js]
+[test_cacheForOfflineUse_no-store.js]
+[test_307_redirect.js]
+[test_NetUtil.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_aboutblank.js]
+[test_auth_jar.js]
+[test_auth_proxy.js]
+[test_authentication.js]
+[test_authpromptwrapper.js]
+[test_auth_dialog_permission.js]
+[test_backgroundfilesaver.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_bug365133.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_bug412945.js]
+[test_bug414122.js]
+[test_bug427957.js]
+[test_bug429347.js]
+[test_bug455311.js]
+[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_bug484684.js]
+[test_bug490095.js]
+[test_bug504014.js]
+[test_bug510359.js]
+[test_bug515583.js]
+[test_bug526789.js]
+[test_bug528292.js]
+[test_bug536324_64bit_content_length.js]
+[test_bug540566.js]
+[test_bug543805.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_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_bug1218029.js]
+[test_udpsocket.js]
+[test_udpsocket_offline.js]
+[test_doomentry.js]
+[test_cacheflags.js]
+[test_cache_jar.js]
+[test_cache-entry-id.js]
+[test_channel_close.js]
+[test_compareURIs.js]
+[test_compressappend.js]
+[test_content_encoding_gzip.js]
+[test_content_sniffer.js]
+[test_cookie_header.js]
+[test_cookiejars.js]
+[test_cookiejars_safebrowsing.js]
+[test_cookies_async_failure.js]
+[test_cookies_persistence.js]
+skip-if = true # Bug 863738
+[test_cookies_privatebrowsing.js]
+[test_cookies_profile_close.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_dns_cancel.js]
+[test_data_protocol.js]
+[test_dns_service.js]
+[test_dns_offline.js]
+skip-if = socketprocess_networking # Bug 1640105
+[test_dns_onion.js]
+[test_dns_originAttributes.js]
+skip-if = socketprocess_networking # Bug 1640105
+[test_dns_localredirect.js]
+[test_dns_proxy_bypass.js]
+[test_dns_disabled.js]
+[test_domain_eviction.js]
+[test_duplicate_headers.js]
+[test_chunked_responses.js]
+[test_content_length_underrun.js]
+[test_event_sink.js]
+[test_eviction.js]
+[test_extract_charset_from_content_type.js]
+[test_fallback_no-cache-entry_canceled.js]
+[test_fallback_no-cache-entry_passing.js]
+[test_fallback_redirect-to-different-origin_canceled.js]
+[test_fallback_redirect-to-different-origin_passing.js]
+[test_fallback_request-error_canceled.js]
+[test_fallback_request-error_passing.js]
+[test_fallback_response-error_canceled.js]
+[test_fallback_response-error_passing.js]
+[test_file_protocol.js]
+[test_filestreams.js]
+[test_freshconnection.js]
+[test_gre_resources.js]
+[test_gzipped_206.js]
+[test_head.js]
+[test_header_Accept-Language.js]
+[test_header_Accept-Language_case.js]
+[test_headers.js]
+[test_hostnameIsLocalIPAddress.js]
+[test_hostnameIsSharedIPAddress.js]
+[test_http_headers.js]
+[test_httpauth.js]
+[test_httpcancel.js]
+[test_httpResponseTimeout.js]
+[test_httpsuspend.js]
+[test_idnservice.js]
+[test_idn_blacklist.js]
+[test_idn_urls.js]
+[test_idna2008.js]
+[test_immutable.js]
+run-sequentially = node server exceptions dont replay well
+[test_localstreams.js]
+[test_large_port.js]
+[test_mismatch_last-modified.js]
+[test_MIME_params.js]
+[test_mozTXTToHTMLConv.js]
+[test_multipart_byteranges.js]
+[test_multipart_streamconv.js]
+[test_multipart_streamconv_missing_lead_boundary.js]
+[test_multipart_streamconv_missing_boundary_lead_dashes.js]
+[test_multipart_streamconv-byte-by-byte.js]
+[test_nestedabout_serialize.js]
+[test_net_addr.js]
+# Bug 732363: test fails on windows for unknown reasons.
+skip-if = os == "win"
+[test_nojsredir.js]
+[test_offline_status.js]
+[test_origin.js]
+[test_anonymous-coalescing.js]
+[test_original_sent_received_head.js]
+[test_parse_content_type.js]
+[test_permmgr.js]
+[test_plaintext_sniff.js]
+skip-if = true # Causes sporatic oranges
+[test_post.js]
+[test_private_necko_channel.js]
+[test_private_cookie_changed.js]
+[test_progress.js]
+[test_protocolproxyservice.js]
+[test_protocolproxyservice-async-filters.js]
+[test_proxy-failover_canceled.js]
+[test_proxy-failover_passing.js]
+[test_proxy-replace_canceled.js]
+[test_proxy-replace_passing.js]
+[test_psl.js]
+[test_range_requests.js]
+[test_readline.js]
+[test_redirect-caching_canceled.js]
+[test_redirect-caching_failure.js]
+[test_redirect-caching_passing.js]
+[test_redirect_canceled.js]
+[test_redirect_failure.js]
+[test_redirect_from_script.js]
+[test_redirect_from_script_after-open_passing.js]
+[test_redirect_passing.js]
+[test_redirect_loop.js]
+[test_redirect_baduri.js]
+[test_redirect_different-protocol.js]
+[test_redirect_protocol_telemetry.js]
+[test_reentrancy.js]
+[test_reopen.js]
+[test_resumable_channel.js]
+[test_resumable_truncate.js]
+[test_safeoutputstream.js]
+[test_schema_2_migration.js]
+[test_schema_3_migration.js]
+[test_schema_10_migration.js]
+[test_simple.js]
+[test_sockettransportsvc_available.js]
+[test_socks.js]
+skip-if = (os == 'mac' && (verify || debug || os_version == '10.14')) || socketprocess_networking #Bug 1140656
+# 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_http2.js]
+run-sequentially = node server exceptions dont replay well
+[test_altsvc.js]
+run-sequentially = node server exceptions dont replay well
+[test_speculative_connect.js]
+[test_standardurl.js]
+[test_standardurl_default_port.js]
+[test_standardurl_port.js]
+[test_streamcopier.js]
+[test_traceable_channel.js]
+[test_unescapestring.js]
+[test_xmlhttprequest.js]
+[test_XHR_redirects.js]
+[test_pinned_app_cache.js]
+[test_offlinecache_custom-directory.js]
+run-sequentially = Hardcoded hash value includes port 4444.
+[test_bug767025.js]
+run-sequentially = Hardcoded hash value includes port 4444.
+[test_bug826063.js]
+[test_bug812167.js]
+[test_tldservice_nextsubdomain.js]
+[test_about_protocol.js]
+[test_bug856978.js]
+[test_unix_domain.js]
+[test_addr_in_use_error.js]
+[test_about_networking.js]
+[test_ping_aboutnetworking.js]
+skip-if = (verify && (os == 'mac'))
+[test_referrer.js]
+[test_referrer_cross_origin.js]
+[test_referrer_policy.js]
+[test_predictor.js]
+[test_signature_extraction.js]
+skip-if = os != "win"
+[test_synthesized_response.js]
+[test_udp_multicast.js]
+[test_redirect_history.js]
+[test_reply_without_content_type.js]
+[test_websocket_offline.js]
+[test_be_conservative.js]
+firefox-appdir = browser
+[test_be_conservative_error_handling.js]
+firefox-appdir = browser
+[test_tls_server.js]
+firefox-appdir = browser
+skip-if = socketprocess_networking
+[test_tls_server_multiple_clients.js]
+skip-if = socketprocess_networking
+[test_1073747.js]
+[test_safeoutputstream_append.js]
+[test_suspend_channel_before_connect.js]
+[test_suspend_channel_on_examine.js]
+[test_suspend_channel_on_modified.js]
+[test_inhibit_caching.js]
+[test_dns_disable_ipv4.js]
+[test_dns_disable_ipv6.js]
+[test_bug1195415.js]
+[test_cookie_blacklist.js]
+[test_getHost.js]
+[test_bug412457.js]
+skip-if = appname == "thunderbird"
+[test_bug464591.js]
+skip-if = appname == "thunderbird"
+[test_alt-data_simple.js]
+[test_alt-data_stream.js]
+[test_alt-data_too_big.js]
+[test_alt-data_overwrite.js]
+[test_alt-data_closeWithStatus.js]
+[test_cache-control_request.js]
+[test_bug1279246.js]
+[test_throttlequeue.js]
+[test_throttlechannel.js]
+[test_throttling.js]
+[test_separate_connections.js]
+[test_trackingProtection_annotateChannels.js]
+[test_race_cache_with_network.js]
+skip-if = (verify && !debug && (os == 'win'))
+[test_rcwn_always_cache_new_content.js]
+[test_channel_priority.js]
+[test_bug1312774_http1.js]
+[test_bug1312782_http1.js]
+[test_bug1355539_http1.js]
+[test_bug1378385_http1.js]
+[test_tls_flags_separate_connections.js]
+[test_tls_flags.js]
+skip-if = (verify && (os == 'linux')) || (os == "android" && processor == "x86_64")
+[test_uri_mutator.js]
+[test_bug1411316_http1.js]
+[test_header_Server_Timing.js]
+run-sequentially = node server exceptions dont replay well
+[test_trr.js]
+# test_trr.js is not working in Thunderbird due to bug 1608066.
+skip-if = appname == "thunderbird"
+[test_ioservice.js]
+[test_substituting_protocol_handler.js]
+[test_proxyconnect.js]
+skip-if = tsan || socketprocess_networking # Bug 1614708
+[test_captive_portal_service.js]
+run-sequentially = node server exceptions dont replay well
+skip-if = socketprocess_networking
+[test_dns_by_type_resolve.js]
+[test_network_connectivity_service.js]
+[test_suspend_channel_on_authRetry.js]
+[test_suspend_channel_on_examine_merged_response.js]
+[test_bug1527293.js]
+[test_stale-while-revalidate_negative.js]
+[test_stale-while-revalidate_positive.js]
+[test_stale-while-revalidate_loop.js]
+[test_stale-while-revalidate_max-age-0.js]
+[test_http1-proxy.js]
+[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_head_request_no_response_body.js]
+[test_disabled_ftp.js]
+[test_cache_204_response.js]
+[test_http3.js]
+# asan - bug 1616239
+# tsan - bug 1622845
+# win - bug 1616238
+# android - bug 1622901
+# mac - bug 1669892
+skip-if = asan || tsan || os == 'win' || os =='android' || os == 'mac'
+[test_http3_421.js]
+skip-if = asan || tsan || os == 'win' || os =='android'
+[test_http3_perf.js]
+skip-if = asan || tsan || os == 'win' || os =='android'
+[test_node_execute.js]
+[test_loadgroup_cancel.js]
+[test_obs-fold.js]
+[test_defaultURI.js]
+[test_port_remapping.js]
+[test_dns_override.js]
+[test_dns_override_for_localhost.js]
+skip-if = socketprocess_networking # Bug 1640105
+[test_no_cookies_after_last_pb_exit.js]
+[test_trr_httpssvc.js]
+skip-if = os == "android"
+run-sequentially = node server exceptions dont replay well
+[test_trr_case_sensitivity.js]
+skip-if = os == "android"
+run-sequentially = node server exceptions dont replay well
+[test_trr_proxy.js]
+[test_trr_cname_chain.js]
+skip-if = os == "android"
+run-sequentially = node server exceptions dont replay well
+[test_http_sfv.js]
+[test_blob_channelname.js]
+[test_altsvc_pref.js]
+skip-if = asan || tsan || os == 'win' || os =='android'
+[test_http3_alt_svc.js]
+skip-if = asan || tsan || os == 'win' || os =='android'
+[test_use_httpssvc.js]
+skip-if = os == "android"
+run-sequentially = node server exceptions dont replay well
+[test_trr_additional_section.js]
+skip-if = os == "android"
+run-sequentially = node server exceptions dont replay well
+[test_trr_extended_error.js]
+skip-if = os == "android"
+run-sequentially = node server exceptions dont replay well
+[test_httpssvc_iphint.js]
+skip-if = os == "android"
+run-sequentially = node server exceptions dont replay well
+[test_multipart_streamconv_empty.js]
+[test_httpssvc_priority.js]
+skip-if = os == "android"
+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_http3_trans_close.js]
+skip-if = asan || tsan || os == 'win' || os =='android'
+[test_brotli_http.js]
+[test_altsvc_http3.js]
+skip-if =
+ true # Bug 1675008
+ asan
+ tsan
+ os == 'win'
+ os =='android'
+[test_http3_fatal_stream_error.js]
+skip-if = asan || tsan || os == 'win' || os =='android' || socketprocess_networking
+run-sequentially = node server exceptions dont replay well
+[test_http3_large_post.js]
+skip-if = asan || tsan || os == 'win' || os =='android'
+[test_http3_error_before_connect.js]
+skip-if = asan || tsan || os == 'win' || os =='android' || socketprocess_networking
+run-sequentially = node server exceptions dont replay well
+[test_http3_server_not_existing.js]
+skip-if = asan || tsan || os == 'win' || os =='android' || socketprocess_networking
+run-sequentially = node server exceptions dont replay well
+[test_http3_fast_fallback.js]
+skip-if = asan || tsan || os == 'win' || os =='android' || socketprocess_networking
+run-sequentially = node server exceptions dont replay well
+[test_cookie_ipv6.js]
+[test_httpssvc_retry_with_ech.js]
+skip-if = asan || tsan || os == 'win' || os =='android'
+run-sequentially = node server exceptions dont replay well
+[test_httpssvc_retry_without_ech.js]
+skip-if = asan || tsan || os == 'win' || os =='android'
+run-sequentially = node server exceptions dont replay well
+[test_httpssvc_https_upgrade.js]
+[test_bug1683176.js]
+skip-if = os == "android" || !debug
+[test_SuperfluousAuth.js]