diff options
author | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
---|---|---|
committer | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
commit | 5f8de423f190bbb79a62f804151bc24824fa32d8 (patch) | |
tree | 10027f336435511475e392454359edea8e25895d /netwerk/test | |
parent | 49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff) | |
download | uxp-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz |
Add m-esr52 at 52.6.0
Diffstat (limited to 'netwerk/test')
512 files changed, 65150 insertions, 0 deletions
diff --git a/netwerk/test/NetwerkTestLogging.h b/netwerk/test/NetwerkTestLogging.h new file mode 100644 index 0000000000..68e955fd39 --- /dev/null +++ b/netwerk/test/NetwerkTestLogging.h @@ -0,0 +1,23 @@ +/* -*- Mode: C++; 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/. */ + +#ifndef NetwerkTestLogging_h +#define NetwerkTestLogging_h + +#include "mozilla/Logging.h" + +// The netwerk standalone cpp unit tests will just use PR_LogPrint as they don't +// have access to mozilla::detail::log_print. To support that MOZ_LOG is +// redefined. +#undef MOZ_LOG +#define MOZ_LOG(_module,_level,_args) \ + PR_BEGIN_MACRO \ + if (MOZ_LOG_TEST(_module,_level)) { \ + PR_LogPrint _args; \ + } \ + PR_END_MACRO + +#endif diff --git a/netwerk/test/PropertiesTest.cpp b/netwerk/test/PropertiesTest.cpp new file mode 100644 index 0000000000..5099d7b5b9 --- /dev/null +++ b/netwerk/test/PropertiesTest.cpp @@ -0,0 +1,131 @@ +/* -*- 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/. */ + +#include "TestCommon.h" +#include "mozilla/Sprintf.h" +#include "nsXPCOM.h" +#include "nsStringAPI.h" +#include "nsIPersistentProperties2.h" +#include "nsIServiceManager.h" +#include "nsIComponentRegistrar.h" +#include "nsIURL.h" +#include "nsNetCID.h" +#include "nsIChannel.h" +#include "nsIComponentManager.h" +#include <stdio.h> +#include "nsComponentManagerUtils.h" +#include "nsServiceManagerUtils.h" +#include "nsISimpleEnumerator.h" +#include "nsIScriptSecurityManager.h" +#include "nsILoadInfo.h" +#include "nsNetUtil.h" + +#define TEST_URL "resource:/res/test.properties" +static NS_DEFINE_CID(kPersistentPropertiesCID, NS_IPERSISTENTPROPERTIES_CID); + +/***************************************************************************/ + +int +main(int argc, char* argv[]) +{ + if (test_common_init(&argc, &argv) != 0) + return -1; + + nsresult ret; + + nsCOMPtr<nsIServiceManager> servMan; + NS_InitXPCOM2(getter_AddRefs(servMan), nullptr, nullptr); + + nsIInputStream* in = nullptr; + + nsCOMPtr<nsIScriptSecurityManager> secman = + do_GetService(NS_SCRIPTSECURITYMANAGER_CONTRACTID, &ret); + if (NS_FAILED(ret)) return 1; + nsCOMPtr<nsIPrincipal> systemPrincipal; + ret = secman->GetSystemPrincipal(getter_AddRefs(systemPrincipal)); + if (NS_FAILED(ret)) return 1; + + nsCOMPtr<nsIURI> uri; + ret = NS_NewURI(getter_AddRefs(uri), NS_LITERAL_CSTRING(TEST_URL)); + if (NS_FAILED(ret)) return 1; + + nsIChannel *channel = nullptr; + ret = NS_NewChannel(&channel, + uri, + systemPrincipal, + nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL, + nsIContentPolicy::TYPE_OTHER); + if (NS_FAILED(ret)) return 1; + + ret = channel->Open2(&in); + if (NS_FAILED(ret)) return 1; + + nsIPersistentProperties* props; + ret = CallCreateInstance(kPersistentPropertiesCID, &props); + if (NS_FAILED(ret) || (!props)) { + printf("create nsIPersistentProperties failed\n"); + return 1; + } + ret = props->Load(in); + if (NS_FAILED(ret)) { + printf("cannot load properties\n"); + return 1; + } + int i = 1; + while (true) { + char name[16]; + name[0] = 0; + SprintfLiteral(name, "%d", i); + nsAutoString v; + ret = props->GetStringProperty(nsDependentCString(name), v); + if (NS_FAILED(ret) || (!v.Length())) { + break; + } + printf("\"%d\"=\"%s\"\n", i, NS_ConvertUTF16toUTF8(v).get()); + i++; + } + + nsCOMPtr<nsISimpleEnumerator> propEnum; + ret = props->Enumerate(getter_AddRefs(propEnum)); + + if (NS_FAILED(ret)) { + printf("cannot enumerate properties\n"); + return 1; + } + + + printf("\nKey\tValue\n"); + printf( "---\t-----\n"); + + bool hasMore; + while (NS_SUCCEEDED(propEnum->HasMoreElements(&hasMore)) && + hasMore) { + nsCOMPtr<nsISupports> sup; + ret = propEnum->GetNext(getter_AddRefs(sup)); + + nsCOMPtr<nsIPropertyElement> propElem = do_QueryInterface(sup, &ret); + if (NS_FAILED(ret)) { + printf("failed to get current item\n"); + return 1; + } + + nsAutoCString key; + nsAutoString value; + + ret = propElem->GetKey(key); + if (NS_FAILED(ret)) { + printf("failed to get current element's key\n"); + return 1; + } + ret = propElem->GetValue(value); + if (NS_FAILED(ret)) { + printf("failed to get current element's value\n"); + return 1; + } + + printf("%s\t%s\n", key.get(), NS_ConvertUTF16toUTF8(value).get()); + } + return 0; +} diff --git a/netwerk/test/ReadNTLM.cpp b/netwerk/test/ReadNTLM.cpp new file mode 100644 index 0000000000..20411e3369 --- /dev/null +++ b/netwerk/test/ReadNTLM.cpp @@ -0,0 +1,325 @@ +/* vim: set ts=2 sw=2 et cindent: */ +/* 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/. */ + +#include <stdlib.h> +#include <stdio.h> +#include <ctype.h> + +#include "plbase64.h" +#include "nsStringAPI.h" +#include "prmem.h" + +/* + * ReadNTLM : reads NTLM messages. + * + * based on http://davenport.sourceforge.net/ntlm.html + */ + +#define kNegotiateUnicode 0x00000001 +#define kNegotiateOEM 0x00000002 +#define kRequestTarget 0x00000004 +#define kUnknown1 0x00000008 +#define kNegotiateSign 0x00000010 +#define kNegotiateSeal 0x00000020 +#define kNegotiateDatagramStyle 0x00000040 +#define kNegotiateLanManagerKey 0x00000080 +#define kNegotiateNetware 0x00000100 +#define kNegotiateNTLMKey 0x00000200 +#define kUnknown2 0x00000400 +#define kUnknown3 0x00000800 +#define kNegotiateDomainSupplied 0x00001000 +#define kNegotiateWorkstationSupplied 0x00002000 +#define kNegotiateLocalCall 0x00004000 +#define kNegotiateAlwaysSign 0x00008000 +#define kTargetTypeDomain 0x00010000 +#define kTargetTypeServer 0x00020000 +#define kTargetTypeShare 0x00040000 +#define kNegotiateNTLM2Key 0x00080000 +#define kRequestInitResponse 0x00100000 +#define kRequestAcceptResponse 0x00200000 +#define kRequestNonNTSessionKey 0x00400000 +#define kNegotiateTargetInfo 0x00800000 +#define kUnknown4 0x01000000 +#define kUnknown5 0x02000000 +#define kUnknown6 0x04000000 +#define kUnknown7 0x08000000 +#define kUnknown8 0x10000000 +#define kNegotiate128 0x20000000 +#define kNegotiateKeyExchange 0x40000000 +#define kNegotiate56 0x80000000 + +static const char NTLM_SIGNATURE[] = "NTLMSSP"; +static const char NTLM_TYPE1_MARKER[] = { 0x01, 0x00, 0x00, 0x00 }; +static const char NTLM_TYPE2_MARKER[] = { 0x02, 0x00, 0x00, 0x00 }; +static const char NTLM_TYPE3_MARKER[] = { 0x03, 0x00, 0x00, 0x00 }; + +#define NTLM_MARKER_LEN 4 +#define NTLM_TYPE1_HEADER_LEN 32 +#define NTLM_TYPE2_HEADER_LEN 32 +#define NTLM_TYPE3_HEADER_LEN 64 + +#define LM_HASH_LEN 16 +#define LM_RESP_LEN 24 + +#define NTLM_HASH_LEN 16 +#define NTLM_RESP_LEN 24 + +static void PrintFlags(uint32_t flags) +{ +#define TEST(_flag) \ + if (flags & k ## _flag) \ + printf(" 0x%08x (" # _flag ")\n", k ## _flag) + + TEST(NegotiateUnicode); + TEST(NegotiateOEM); + TEST(RequestTarget); + TEST(Unknown1); + TEST(NegotiateSign); + TEST(NegotiateSeal); + TEST(NegotiateDatagramStyle); + TEST(NegotiateLanManagerKey); + TEST(NegotiateNetware); + TEST(NegotiateNTLMKey); + TEST(Unknown2); + TEST(Unknown3); + TEST(NegotiateDomainSupplied); + TEST(NegotiateWorkstationSupplied); + TEST(NegotiateLocalCall); + TEST(NegotiateAlwaysSign); + TEST(TargetTypeDomain); + TEST(TargetTypeServer); + TEST(TargetTypeShare); + TEST(NegotiateNTLM2Key); + TEST(RequestInitResponse); + TEST(RequestAcceptResponse); + TEST(RequestNonNTSessionKey); + TEST(NegotiateTargetInfo); + TEST(Unknown4); + TEST(Unknown5); + TEST(Unknown6); + TEST(Unknown7); + TEST(Unknown8); + TEST(Negotiate128); + TEST(NegotiateKeyExchange); + TEST(Negotiate56); + +#undef TEST +} + +static void +PrintBuf(const char *tag, const uint8_t *buf, uint32_t bufLen) +{ + int i; + + printf("%s =\n", tag); + while (bufLen > 0) + { + int count = bufLen; + if (count > 8) + count = 8; + + printf(" "); + for (i=0; i<count; ++i) + { + printf("0x%02x ", int(buf[i])); + } + for (; i<8; ++i) + { + printf(" "); + } + + printf(" "); + for (i=0; i<count; ++i) + { + if (isprint(buf[i])) + printf("%c", buf[i]); + else + printf("."); + } + printf("\n"); + + bufLen -= count; + buf += count; + } +} + +static uint16_t +ReadUint16(const uint8_t *&buf) +{ + uint16_t x; +#ifdef IS_BIG_ENDIAN + x = ((uint16_t) buf[1]) | ((uint16_t) buf[0] << 8); +#else + x = ((uint16_t) buf[0]) | ((uint16_t) buf[1] << 8); +#endif + buf += sizeof(x); + return x; +} + +static uint32_t +ReadUint32(const uint8_t *&buf) +{ + uint32_t x; +#ifdef IS_BIG_ENDIAN + x = ( (uint32_t) buf[3]) | + (((uint32_t) buf[2]) << 8) | + (((uint32_t) buf[1]) << 16) | + (((uint32_t) buf[0]) << 24); +#else + x = ( (uint32_t) buf[0]) | + (((uint32_t) buf[1]) << 8) | + (((uint32_t) buf[2]) << 16) | + (((uint32_t) buf[3]) << 24); +#endif + buf += sizeof(x); + return x; +} + +typedef struct { + uint16_t length; + uint16_t capacity; + uint32_t offset; +} SecBuf; + +static void +ReadSecBuf(SecBuf *s, const uint8_t *&buf) +{ + s->length = ReadUint16(buf); + s->capacity = ReadUint16(buf); + s->offset = ReadUint32(buf); +} + +static void +ReadType1MsgBody(const uint8_t *inBuf, uint32_t start) +{ + const uint8_t *cursor = inBuf + start; + uint32_t flags; + + PrintBuf("flags", cursor, 4); + // read flags + flags = ReadUint32(cursor); + PrintFlags(flags); + + // type 1 message may not include trailing security buffers + if ((flags & kNegotiateDomainSupplied) | + (flags & kNegotiateWorkstationSupplied)) + { + SecBuf secbuf; + ReadSecBuf(&secbuf, cursor); + PrintBuf("supplied domain", inBuf + secbuf.offset, secbuf.length); + + ReadSecBuf(&secbuf, cursor); + PrintBuf("supplied workstation", inBuf + secbuf.offset, secbuf.length); + } +} + +static void +ReadType2MsgBody(const uint8_t *inBuf, uint32_t start) +{ + uint16_t targetLen, offset; + uint32_t flags; + const uint8_t *target; + const uint8_t *cursor = inBuf + start; + + // read target name security buffer + targetLen = ReadUint16(cursor); + ReadUint16(cursor); // discard next 16-bit value + offset = ReadUint32(cursor); // get offset from inBuf + target = inBuf + offset; + + PrintBuf("target", target, targetLen); + + PrintBuf("flags", cursor, 4); + // read flags + flags = ReadUint32(cursor); + PrintFlags(flags); + + // read challenge + PrintBuf("challenge", cursor, 8); + cursor += 8; + + PrintBuf("context", cursor, 8); + cursor += 8; + + SecBuf secbuf; + ReadSecBuf(&secbuf, cursor); + PrintBuf("target information", inBuf + secbuf.offset, secbuf.length); +} + +static void +ReadType3MsgBody(const uint8_t *inBuf, uint32_t start) +{ + const uint8_t *cursor = inBuf + start; + + SecBuf secbuf; + + ReadSecBuf(&secbuf, cursor); // LM response + PrintBuf("LM response", inBuf + secbuf.offset, secbuf.length); + + ReadSecBuf(&secbuf, cursor); // NTLM response + PrintBuf("NTLM response", inBuf + secbuf.offset, secbuf.length); + + ReadSecBuf(&secbuf, cursor); // domain name + PrintBuf("domain name", inBuf + secbuf.offset, secbuf.length); + + ReadSecBuf(&secbuf, cursor); // user name + PrintBuf("user name", inBuf + secbuf.offset, secbuf.length); + + ReadSecBuf(&secbuf, cursor); // workstation name + PrintBuf("workstation name", inBuf + secbuf.offset, secbuf.length); + + ReadSecBuf(&secbuf, cursor); // session key + PrintBuf("session key", inBuf + secbuf.offset, secbuf.length); + + uint32_t flags = ReadUint32(cursor); + PrintBuf("flags", (const uint8_t *) &flags, sizeof(flags)); + PrintFlags(flags); +} + +static void +ReadMsg(const char *base64buf, uint32_t bufLen) +{ + uint8_t *inBuf = (uint8_t *) PL_Base64Decode(base64buf, bufLen, nullptr); + if (!inBuf) + { + printf("PL_Base64Decode failed\n"); + return; + } + + const uint8_t *cursor = inBuf; + + PrintBuf("signature", cursor, 8); + + // verify NTLMSSP signature + if (memcmp(cursor, NTLM_SIGNATURE, sizeof(NTLM_SIGNATURE)) != 0) + { + printf("### invalid or corrupt NTLM signature\n"); + } + cursor += sizeof(NTLM_SIGNATURE); + + PrintBuf("message type", cursor, 4); + + if (memcmp(cursor, NTLM_TYPE1_MARKER, sizeof(NTLM_MARKER_LEN)) == 0) + ReadType1MsgBody(inBuf, 12); + else if (memcmp(cursor, NTLM_TYPE2_MARKER, sizeof(NTLM_MARKER_LEN)) == 0) + ReadType2MsgBody(inBuf, 12); + else if (memcmp(cursor, NTLM_TYPE3_MARKER, sizeof(NTLM_MARKER_LEN)) == 0) + ReadType3MsgBody(inBuf, 12); + else + printf("### invalid or unknown message type\n"); + + PR_Free(inBuf); +} + +int main(int argc, char **argv) +{ + if (argc == 1) + { + printf("usage: ntlmread <msg>\n"); + return -1; + } + ReadMsg(argv[1], (uint32_t) strlen(argv[1])); + return 0; +} diff --git a/netwerk/test/TestBind.cpp b/netwerk/test/TestBind.cpp new file mode 100644 index 0000000000..3ce7438b37 --- /dev/null +++ b/netwerk/test/TestBind.cpp @@ -0,0 +1,210 @@ +/* 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/. */ + +#include "TestCommon.h" +#include "TestHarness.h" +#include "nsISocketTransportService.h" +#include "nsISocketTransport.h" +#include "nsIServerSocket.h" +#include "nsIAsyncInputStream.h" +#include "nsINetAddr.h" +#include "mozilla/net/DNS.h" +#include "prerror.h" + +using namespace mozilla::net; +using namespace mozilla; + +class ServerListener: public nsIServerSocketListener +{ +public: + NS_DECL_ISUPPORTS + NS_DECL_NSISERVERSOCKETLISTENER + + ServerListener(); + + // Port that is got from server side will be store here. + uint32_t mClientPort; + bool mFailed; +private: + virtual ~ServerListener(); +}; + +NS_IMPL_ISUPPORTS(ServerListener, nsIServerSocketListener) + +ServerListener::ServerListener() + : mClientPort(-1) + , mFailed(false) +{ +} + +ServerListener::~ServerListener() = default; + +NS_IMETHODIMP +ServerListener::OnSocketAccepted(nsIServerSocket *aServ, + nsISocketTransport *aTransport) +{ + // Run on STS thread. + NetAddr peerAddr; + nsresult rv = aTransport->GetPeerAddr(&peerAddr); + if (NS_FAILED(rv)) { + mFailed = true; + fail("Server: not able to get peer address."); + QuitPumpingEvents(); + return NS_OK; + } + mClientPort = PR_ntohs(peerAddr.inet.port); + passed("Server: received connection"); + QuitPumpingEvents(); + return NS_OK; +} + +NS_IMETHODIMP +ServerListener::OnStopListening(nsIServerSocket *aServ, + nsresult aStatus) +{ + return NS_OK; +} + +class ClientInputCallback : public nsIInputStreamCallback +{ +public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIINPUTSTREAMCALLBACK + + ClientInputCallback(); + + bool mFailed; +private: + virtual ~ClientInputCallback(); +}; + +NS_IMPL_ISUPPORTS(ClientInputCallback, nsIInputStreamCallback) + +ClientInputCallback::ClientInputCallback() + : mFailed(false) +{ +} + +ClientInputCallback::~ClientInputCallback() = default; + +NS_IMETHODIMP +ClientInputCallback::OnInputStreamReady(nsIAsyncInputStream *aStream) +{ + // Server doesn't send. That means if we are here, we probably have run into + // an error. + uint64_t avail; + nsresult rv = aStream->Available(&avail); + if (NS_FAILED(rv)) { + mFailed = true; + } + QuitPumpingEvents(); + return NS_OK; +} + +int +main(int32_t argc, char *argv[]) +{ + ScopedXPCOM xpcom("SocketTransport"); + if (xpcom.failed()) { + fail("Unable to initalize XPCOM."); + return -1; + } + + // + // Server side. + // + nsCOMPtr<nsIServerSocket> server = do_CreateInstance("@mozilla.org/network/server-socket;1"); + if (!server) { + fail("Failed to create server socket."); + return -1; + } + + nsresult rv = server->Init(-1, true, -1); + if (NS_FAILED(rv)) { + fail("Failed to initialize server."); + return -1; + } + + int32_t serverPort; + rv = server->GetPort(&serverPort); + if (NS_FAILED(rv)) { + fail("Unable to get server port."); + return -1; + } + + // Listening. + RefPtr<ServerListener> serverListener = new ServerListener(); + rv = server->AsyncListen(serverListener); + if (NS_FAILED(rv)) { + fail("Server fail to start listening."); + return -1; + } + + // + // Client side + // + uint32_t bindingPort = 20000; + nsCOMPtr<nsISocketTransportService> sts = + do_GetService("@mozilla.org/network/socket-transport-service;1", &rv); + if (NS_FAILED(rv)) { + fail("Unable to get socket transport service."); + return -1; + } + + for (int32_t tried = 0; tried < 100; tried++) { + nsCOMPtr<nsISocketTransport> client; + rv = sts->CreateTransport(nullptr, 0, NS_LITERAL_CSTRING("127.0.0.1"), + serverPort, nullptr, getter_AddRefs(client)); + if (NS_FAILED(rv)) { + fail("Unable to create transport."); + return -1; + } + + // Bind to a port. It's possible that we are binding to a port that is + // currently in use. If we failed to bind, we try next port. + NetAddr bindingAddr; + bindingAddr.inet.family = AF_INET; + bindingAddr.inet.ip = 0; + bindingAddr.inet.port = PR_htons(bindingPort); + rv = client->Bind(&bindingAddr); + if (NS_FAILED(rv)) { + fail("Unable to bind a port."); + return -1; + } + + // Open IO streams, to make client SocketTransport connect to server. + RefPtr<ClientInputCallback> clientCallback = new ClientInputCallback(); + nsCOMPtr<nsIInputStream> inputStream; + rv = client->OpenInputStream(nsITransport::OPEN_UNBUFFERED, + 0, 0, getter_AddRefs(inputStream)); + if (NS_FAILED(rv)) { + fail("Failed to open an input stream."); + return -1; + } + nsCOMPtr<nsIAsyncInputStream> asyncInputStream = do_QueryInterface(inputStream); + rv = asyncInputStream->AsyncWait(clientCallback, 0, 0, nullptr); + + // Wait for server's response or callback of input stream. + PumpEvents(); + if (clientCallback->mFailed) { + // if client received error, we likely have bound a port that is in use. + // we can try another port. + bindingPort++; + } else { + // We are unlocked by server side, leave the loop and check result. + break; + } + } + + if (serverListener->mFailed) { + fail("Server failure."); + return -1; + } + if (serverListener->mClientPort != bindingPort) { + fail("Port that server got doesn't match what we are expecting."); + return -1; + } + passed("Port matched"); + return 0; +} diff --git a/netwerk/test/TestBlockingSocket.cpp b/netwerk/test/TestBlockingSocket.cpp new file mode 100644 index 0000000000..d4b4a615fb --- /dev/null +++ b/netwerk/test/TestBlockingSocket.cpp @@ -0,0 +1,127 @@ +/* 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/. */ + +#include "TestCommon.h" +#include "nsIComponentRegistrar.h" +#include "nsISocketTransportService.h" +#include "nsISocketTransport.h" +#include "nsIServiceManager.h" +#include "nsIComponentManager.h" +#include "nsCOMPtr.h" +#include "nsStringAPI.h" +#include "nsIFile.h" +#include "nsNetUtil.h" +#include "nsIInputStream.h" +#include "nsServiceManagerUtils.h" +#include "nsIOutputStream.h" +#include "NetwerkTestLogging.h" +#include "prenv.h" +#include "prthread.h" +#include <stdlib.h> + +//////////////////////////////////////////////////////////////////////////////// + +// +// set NSPR_LOG_MODULES=Test:5 +// +static PRLogModuleInfo *gTestLog = nullptr; +#define LOG(args) MOZ_LOG(gTestLog, mozilla::LogLevel::Debug, args) + +//////////////////////////////////////////////////////////////////////////////// + +static NS_DEFINE_CID(kSocketTransportServiceCID, NS_SOCKETTRANSPORTSERVICE_CID); + +//////////////////////////////////////////////////////////////////////////////// + +static nsresult +RunBlockingTest(const nsACString &host, int32_t port, nsIFile *file) +{ + nsresult rv; + + LOG(("RunBlockingTest\n")); + + nsCOMPtr<nsISocketTransportService> sts = + do_GetService(kSocketTransportServiceCID, &rv); + if (NS_FAILED(rv)) return rv; + + nsCOMPtr<nsIInputStream> input; + rv = NS_NewLocalFileInputStream(getter_AddRefs(input), file); + if (NS_FAILED(rv)) return rv; + + nsCOMPtr<nsISocketTransport> trans; + rv = sts->CreateTransport(nullptr, 0, host, port, nullptr, getter_AddRefs(trans)); + if (NS_FAILED(rv)) return rv; + + nsCOMPtr<nsIOutputStream> output; + rv = trans->OpenOutputStream(nsITransport::OPEN_BLOCKING, 100, 10, getter_AddRefs(output)); + if (NS_FAILED(rv)) return rv; + + char buf[120]; + uint32_t nr, nw; + for (;;) { + rv = input->Read(buf, sizeof(buf), &nr); + if (NS_FAILED(rv) || (nr == 0)) return rv; + +/* + const char *p = buf; + while (nr) { + rv = output->Write(p, nr, &nw); + if (NS_FAILED(rv)) return rv; + + nr -= nw; + p += nw; + } +*/ + + rv = output->Write(buf, nr, &nw); + if (NS_FAILED(rv)) return rv; + + NS_ASSERTION(nr == nw, "not all written"); + } + + LOG((" done copying data.\n")); + return NS_OK; +} + +//////////////////////////////////////////////////////////////////////////////// + +int +main(int argc, char* argv[]) +{ + if (test_common_init(&argc, &argv) != 0) + return -1; + + nsresult rv; + + if (argc < 4) { + printf("usage: %s <host> <port> <file-to-read>\n", argv[0]); + return -1; + } + char* hostName = argv[1]; + int32_t port = atoi(argv[2]); + char* fileName = argv[3]; + { + nsCOMPtr<nsIServiceManager> servMan; + NS_InitXPCOM2(getter_AddRefs(servMan), nullptr, nullptr); + + gTestLog = PR_NewLogModule("Test"); + + nsCOMPtr<nsIFile> file; + rv = NS_NewNativeLocalFile(nsDependentCString(fileName), false, getter_AddRefs(file)); + if (NS_FAILED(rv)) return -1; + + rv = RunBlockingTest(nsDependentCString(hostName), port, file); + if (NS_FAILED(rv)) + LOG(("RunBlockingTest failed [rv=%x]\n", rv)); + + // give background threads a chance to finish whatever work they may + // be doing. + LOG(("sleeping for 5 seconds...\n")); + PR_Sleep(PR_SecondsToInterval(5)); + } // this scopes the nsCOMPtrs + // no nsCOMPtrs are allowed to be alive when you call NS_ShutdownXPCOM + rv = NS_ShutdownXPCOM(nullptr); + NS_ASSERTION(NS_SUCCEEDED(rv), "NS_ShutdownXPCOM failed"); + return 0; +} diff --git a/netwerk/test/TestCacheBlockFiles.cpp b/netwerk/test/TestCacheBlockFiles.cpp new file mode 100644 index 0000000000..0a309fdf9c --- /dev/null +++ b/netwerk/test/TestCacheBlockFiles.cpp @@ -0,0 +1,873 @@ +/* + TestCacheBlockFiles.cpp +*/ + + +#include <stdio.h> +#include <stdlib.h> +#include <utime.h> + +#include <Files.h> +#include <Strings.h> +#include <Errors.h> +#include <Resources.h> +#include <Aliases.h> + +#include "nsCOMPtr.h" +#include "nsCRT.h" +#include "nsDirectoryServiceDefs.h" +#include "nsError.h" +#include "nsIComponentManager.h" +#include "nsIComponentRegistrar.h" +#include "nsIFile.h" +#include "nsIFileStreams.h" +#include "nsMemory.h" +#include "nsIComponentRegistrar.h" +#include "nsANSIFileStreams.h" +#include "nsDiskCacheBlockFile.h" + +#include "prclist.h" + +/** + * StressTest() + */ + +typedef struct Allocation { + int32_t start; + int32_t count; +} Allocation; + +nsresult +StressTest(nsIFile * localFile, int32_t testNumber, bool readWrite) +{ + nsresult rv = NS_OK; + +#define ITERATIONS 1024 +#define MAX_ALLOCATIONS 256 + Allocation block[MAX_ALLOCATIONS]; + int32_t currentAllocations = 0; + int32_t i; + uint32_t a; + + char * writeBuf[4]; + char readBuf[256 * 4]; + + + if (readWrite) { + for (i = 0; i < 4; i++) { + writeBuf[i] = new char[256 * i]; + if (!writeBuf[i]) { + printf("Test %d: failed - out of memory\n", testNumber); + rv = NS_ERROR_OUT_OF_MEMORY; + goto exit; + } + + memset(writeBuf[i], i, 256 * i); + } + } + + nsDiskCacheBlockFile * blockFile = new nsDiskCacheBlockFile; + if (!blockFile) { + printf("Test %d failed (unable to allocate nsDiskCacheBlockFile", testNumber); + rv = NS_ERROR_OUT_OF_MEMORY; + goto exit; + } + + rv = blockFile->Open(localFile, 256); + if (NS_FAILED(rv)) { + printf("Test %d: failed (Open returned: 0x%.8x)\n", testNumber, rv); + goto exit; + } + + i = ITERATIONS; + while (i > 0) { + if ((currentAllocations >= MAX_ALLOCATIONS) || + ((currentAllocations > 0) && (rand() % 4 == 0))) { + // deallocate if we've reached the limit, or 25% of the time we have allocations + a = rand() % currentAllocations; + + if (readWrite) { + // read verify deallocation + rv = blockFile->ReadBlocks(readBuf, block[a].start, block[a].count); + if (NS_FAILED(rv)) { + printf("Test %d: failed (ReadBlocks() returned 0x%.8x)\n", testNumber, rv); + goto exit; + } + + // Verify buffer + for (i = 0; i < 256 * block[a].count; i++) { + if (readBuf[i] != block[a].count) { + printf("Test %d: failed (verifying buffer 1)\n", testNumber); + rv = NS_ERROR_FAILURE; + goto exit; + } + } + } + + rv = blockFile->DeallocateBlocks(block[a].start, block[a].count); + if (NS_FAILED(rv)) { + printf("Test %d: failed (DeallocateBlocks() returned %d)\n", testNumber, rv); + goto exit; + } + + --currentAllocations; + if (currentAllocations > 0) + block[a] = block[currentAllocations]; + + } else { + // allocate blocks + --i; + a = currentAllocations++; + block[a].count = rand() % 4 + 1; // allocate 1 to 4 blocks + block[a].start = blockFile->AllocateBlocks(block[a].count); + if (block[a].start < 0) { + printf("Test %d: failed (AllocateBlocks() failed.)\n", testNumber); + goto exit; + } + + if (readWrite) { + // write buffer + rv = blockFile->WriteBlocks(writeBuf[block[a].count], block[a].start, block[a].count); + if (NS_FAILED(rv)) { + printf("Test %d: failed (WriteBlocks() returned 0x%.8x)\n",testNumber, rv); + goto exit; + } + } + } + } + + // now deallocate remaining allocations + i = currentAllocations; + while (i--) { + + if (readWrite) { + // read verify deallocation + rv = blockFile->ReadBlocks(readBuf, block[a].start, block[a].count); + if (NS_FAILED(rv)) { + printf("Test %d: failed (ReadBlocks(1) returned 0x%.8x)\n", testNumber, rv); + goto exit; + } + + // Verify buffer + for (i = 0; i < 256 * block[a].count; i++) { + if (readBuf[i] != block[a].count) { + printf("Test %d: failed (verifying buffer 1)\n", testNumber); + rv = NS_ERROR_FAILURE; + goto exit; + } + } + } + + rv = blockFile->DeallocateBlocks(block[i].start, block[i].count); + if (NS_FAILED(rv)) { + printf("Test %d: failed (DeallocateBlocks() returned %d)\n", testNumber, rv); + goto exit; + } + } + + + +exit: + nsresult rv2 = blockFile->Close(); + if (NS_FAILED(rv2)) { + printf("Test %d: failed (Close returned: 0x%.8x)\n", testNumber, rv2); + } + + return rv ? rv : rv2; +} + +/** + * main() + */ + +int +main(void) +{ +// OSErr err; + printf("hello world\n"); + + unsigned long now = time(0); + srand(now); + + nsCOMPtr<nsIFile> file; + nsCOMPtr<nsIFile> localFile; + nsresult rv = NS_OK; + { + // Start up XPCOM + nsCOMPtr<nsIServiceManager> servMan; + NS_InitXPCOM2(getter_AddRefs(servMan), nullptr, nullptr); + nsCOMPtr<nsIComponentRegistrar> registrar = do_QueryInterface(servMan); + NS_ASSERTION(registrar, "Null nsIComponentRegistrar"); + if (registrar) + registrar->AutoRegister(nullptr); + + // Get default directory + rv = NS_GetSpecialDirectory(NS_XPCOM_CURRENT_PROCESS_DIR, + getter_AddRefs(file)); + if (NS_FAILED(rv)) { + printf("NS_GetSpecialDirectory() failed : 0x%.8x\n", rv); + goto exit; + } + char * currentDirPath; + rv = file->GetPath(¤tDirPath); + if (NS_FAILED(rv)) { + printf("currentProcessDir->GetPath() failed : 0x%.8x\n", rv); + goto exit; + } + + printf("Current Process Directory: %s\n", currentDirPath); + + + // Generate name for cache block file + rv = file->Append("_CACHE_001_"); + if (NS_FAILED(rv)) goto exit; + + // Delete existing file + rv = file->Delete(false); + if (NS_FAILED(rv) && rv != NS_ERROR_FILE_NOT_FOUND) goto exit; + + // Need nsIFile to open + localFile = do_QueryInterface(file, &rv); + if (NS_FAILED(rv)) { + printf("do_QueryInterface(file) failed : 0x%.8x\n", rv); + goto exit; + } + + nsDiskCacheBlockFile * blockFile = new nsDiskCacheBlockFile; + if (!blockFile) { + rv = NS_ERROR_OUT_OF_MEMORY; + goto exit; + } + + //---------------------------------------------------------------- + // local variables used in tests + //---------------------------------------------------------------- + uint32_t bytesWritten = 0; + int32_t startBlock; + int32_t i = 0; + + + //---------------------------------------------------------------- + // Test 1: Open nonexistent file + //---------------------------------------------------------------- + rv = blockFile->Open(localFile, 256); + if (NS_FAILED(rv)) { + printf("Test 1: failed (Open returned: 0x%.8x)\n", rv); + goto exit; + } + + rv = blockFile->Close(); + if (NS_FAILED(rv)) { + printf("Test 1: failed (Close returned: 0x%.8x)\n", rv); + goto exit; + } + + printf("Test 1: passed\n"); + + + //---------------------------------------------------------------- + // Test 2: Open existing file (with no allocation) + //---------------------------------------------------------------- + rv = blockFile->Open(localFile, 256); + if (NS_FAILED(rv)) { + printf("Test 2: failed (Open returned: 0x%.8x)\n", rv); + goto exit; + } + + rv = blockFile->Close(); + if (NS_FAILED(rv)) { + printf("Test 2: failed (Close returned: 0x%.8x)\n", rv); + goto exit; + } + + printf("Test 2: passed\n"); + + + //---------------------------------------------------------------- + // Test 3: Open existing file (bad format) size < kBitMapBytes + //---------------------------------------------------------------- + + // Delete existing file + rv = localFile->Delete(false); + if (NS_FAILED(rv)) { + printf("Test 3 failed (Delete returned: 0x%.8x)\n", rv); + goto exit; + } + + // write < kBitMapBytes to file + nsANSIFileStream * stream = new nsANSIFileStream; + if (!stream) { + printf("Test 3 failed (unable to allocate stream\n", rv); + goto exit; + } + NS_ADDREF(stream); + rv = stream->Open(localFile); + if (NS_FAILED(rv)) { + NS_RELEASE(stream); + printf("Test 3 failed (stream->Open returned: 0x%.8x)\n", rv); + goto exit; + } + + bytesWritten = 0; + rv = stream->Write("Tell me something good.\n", 24, &bytesWritten); + if (NS_FAILED(rv)) { + NS_RELEASE(stream); + printf("Test 3 failed (stream->Write returned: 0x%.8x)\n", rv); + goto exit; + } + + rv = stream->Close(); + if (NS_FAILED(rv)) { + NS_RELEASE(stream); + printf("Test 3 failed (stream->Close returned: 0x%.8x)\n", rv); + goto exit; + } + NS_RELEASE(stream); + + rv = blockFile->Open(localFile, 256); + if (NS_SUCCEEDED(rv)) { + printf("Test 3: failed (Open erroneously succeeded)\n", rv); + + (void) blockFile->Close(); + goto exit; + } + + printf("Test 3: passed\n"); + + + //---------------------------------------------------------------- + // Test 4: Open nonexistent file (again) + //---------------------------------------------------------------- + + // Delete existing file + rv = localFile->Delete(false); + if (NS_FAILED(rv)) { + printf("Test 4 failed (Delete returned: 0x%.8x)\n", rv); + goto exit; + } + + rv = blockFile->Open(localFile, 256); + if (NS_FAILED(rv)) { + printf("Test 4: failed (Open returned: 0x%.8x)\n", rv); + goto exit; + } + + printf("Test 4: passed\n"); + + + //---------------------------------------------------------------- + // Test 5: AllocateBlocks: invalid block count (0, 5) + //---------------------------------------------------------------- + + + startBlock = blockFile->AllocateBlocks(0); + if (startBlock > -1) { + printf("Test 5: failed (AllocateBlocks(0) erroneously succeeded)\n"); + goto exit; + } + + startBlock = blockFile->AllocateBlocks(5); + if (startBlock > -1) { + printf("Test 5: failed (AllocateBlocks(5) erroneously succeeded)\n"); + goto exit; + } + printf("Test 5: passed\n"); + + + //---------------------------------------------------------------- + // Test 6: AllocateBlocks: valid block count (1, 2, 3, 4) + //---------------------------------------------------------------- + startBlock = blockFile->AllocateBlocks(1); + if (startBlock != 0) { + printf("Test 6: failed (AllocateBlocks(1) failed)\n"); + goto exit; + } + + startBlock = blockFile->AllocateBlocks(2); + if (startBlock != 1) { + printf("Test 6: failed (AllocateBlocks(2) failed)\n"); + goto exit; + } + + startBlock = blockFile->AllocateBlocks(3); + if (startBlock != 4) { + printf("Test 6: failed (AllocateBlocks(3) failed)\n"); + goto exit; + } + + startBlock = blockFile->AllocateBlocks(4); + if (startBlock != 8) { + printf("Test 6: failed (AllocateBlocks(4) failed)\n"); + goto exit; + } + + // blocks allocated should be 1220 3330 4444 + printf("Test 6: passed\n"); // but bits could be mis-allocated + + + + //---------------------------------------------------------------- + // Test 7: VerifyAllocation + //---------------------------------------------------------------- + rv = blockFile->VerifyAllocation(0,1); + if (NS_FAILED(rv)) { + printf("Test 7: failed (VerifyAllocation(0,1) returned: 0x%.8x)\n", rv); + goto exit; + } + + rv = blockFile->VerifyAllocation(1,2); + if (NS_FAILED(rv)) { + printf("Test 7: failed (VerifyAllocation(1,2) returned: 0x%.8x)\n", rv); + goto exit; + } + + rv = blockFile->VerifyAllocation(4,3); + if (NS_FAILED(rv)) { + printf("Test 7: failed (VerifyAllocation(4,3) returned: 0x%.8x)\n", rv); + goto exit; + } + + rv = blockFile->VerifyAllocation(8,4); + if (NS_FAILED(rv)) { + printf("Test 7: failed (VerifyAllocation(8,4) returned: 0x%.8x)\n", rv); + goto exit; + } + printf("Test 7: passed\n"); + + + //---------------------------------------------------------------- + // Test 8: LastBlock + //---------------------------------------------------------------- + int32_t lastBlock = blockFile->LastBlock(); + if (lastBlock != 11) { + printf("Test 8: failed (LastBlock() returned: %d)\n", lastBlock); + goto exit; + } + printf("Test 8: passed\n"); + + + //---------------------------------------------------------------- + // Test 9: DeallocateBlocks: bad startBlock ( < 0) + //---------------------------------------------------------------- + rv = blockFile->DeallocateBlocks(-1, 4); + if (NS_SUCCEEDED(rv)) { + printf("Test 9: failed (DeallocateBlocks(-1, 4) erroneously succeeded)\n"); + goto exit; + } + printf("Test 9: passed\n"); + + + //---------------------------------------------------------------- + // Test 10: DeallocateBlocks: bad numBlocks (0, 5) + //---------------------------------------------------------------- + rv = blockFile->DeallocateBlocks(0, 0); + if (NS_SUCCEEDED(rv)) { + printf("Test 10: failed (DeallocateBlocks(0, 0) erroneously succeeded)\n"); + goto exit; + } + + rv = blockFile->DeallocateBlocks(0, 5); + if (NS_SUCCEEDED(rv)) { + printf("Test 10: failed (DeallocateBlocks(0, 5) erroneously succeeded)\n"); + goto exit; + } + + printf("Test 10: passed\n"); + + + //---------------------------------------------------------------- + // Test 11: DeallocateBlocks: unallocated blocks + //---------------------------------------------------------------- + rv = blockFile->DeallocateBlocks(12, 1); + if (NS_SUCCEEDED(rv)) { + printf("Test 11: failed (DeallocateBlocks(12, 1) erroneously succeeded)\n"); + goto exit; + } + + printf("Test 11: passed\n"); + + + //---------------------------------------------------------------- + // Test 12: DeallocateBlocks: 1, 2, 3, 4 (allocated in Test 6) + //---------------------------------------------------------------- + rv = blockFile->DeallocateBlocks(0, 1); + if (NS_FAILED(rv)) { + printf("Test 12: failed (DeallocateBlocks(12, 1) returned: 0x%.8x)\n", rv); + goto exit; + } + + rv = blockFile->DeallocateBlocks(1, 2); + if (NS_FAILED(rv)) { + printf("Test 12: failed (DeallocateBlocks(1, 2) returned: 0x%.8x)\n", rv); + goto exit; + } + + rv = blockFile->DeallocateBlocks(4, 3); + if (NS_FAILED(rv)) { + printf("Test 12: failed (DeallocateBlocks(4, 3) returned: 0x%.8x)\n", rv); + goto exit; + } + + rv = blockFile->DeallocateBlocks(8, 4); + if (NS_FAILED(rv)) { + printf("Test 12: failed (DeallocateBlocks(8, 4) returned: 0x%.8x)\n", rv); + goto exit; + } + + // zero blocks should be allocated + rv = blockFile->Close(); + if (NS_FAILED(rv)) { + printf("Test 12: failed (Close returned: 0x%.8x)\n", rv); + goto exit; + } + + printf("Test 12: passed\n"); + + + //---------------------------------------------------------------- + // Test 13: Allocate/Deallocate boundary test + //---------------------------------------------------------------- + + rv = blockFile->Open(localFile, 256); + if (NS_FAILED(rv)) { + printf("Test 13: failed (Open returned: 0x%.8x)\n", rv); + goto exit; + } + + // fully allocate, 1 block at a time + for (i=0; i< kBitMapBytes * 8; ++i) { + startBlock = blockFile->AllocateBlocks(1); + if (startBlock < 0) { + printf("Test 13: failed (AllocateBlocks(1) failed on i=%d)\n", i); + goto exit; + } + } + + // attempt allocation with full bit map + startBlock = blockFile->AllocateBlocks(1); + if (startBlock >= 0) { + printf("Test 13: failed (AllocateBlocks(1) erroneously succeeded i=%d)\n", i); + goto exit; + } + + // deallocate all the bits + for (i=0; i< kBitMapBytes * 8; ++i) { + rv = blockFile->DeallocateBlocks(i,1); + if (NS_FAILED(rv)) { + printf("Test 13: failed (DeallocateBlocks(%d,1) returned: 0x%.8x)\n", i,rv); + goto exit; + } + } + + // attempt deallocation beyond end of bit map + rv = blockFile->DeallocateBlocks(i,1); + if (NS_SUCCEEDED(rv)) { + printf("Test 13: failed (DeallocateBlocks(%d,1) erroneously succeeded)\n", i); + goto exit; + } + + // bit map should be empty + + // fully allocate, 2 block at a time + for (i=0; i< kBitMapBytes * 8; i+=2) { + startBlock = blockFile->AllocateBlocks(2); + if (startBlock < 0) { + printf("Test 13: failed (AllocateBlocks(2) failed on i=%d)\n", i); + goto exit; + } + } + + // attempt allocation with full bit map + startBlock = blockFile->AllocateBlocks(2); + if (startBlock >= 0) { + printf("Test 13: failed (AllocateBlocks(2) erroneously succeeded i=%d)\n", i); + goto exit; + } + + // deallocate all the bits + for (i=0; i< kBitMapBytes * 8; i+=2) { + rv = blockFile->DeallocateBlocks(i,2); + if (NS_FAILED(rv)) { + printf("Test 13: failed (DeallocateBlocks(%d,2) returned: 0x%.8x)\n", i,rv); + goto exit; + } + } + + // bit map should be empty + + // fully allocate, 4 block at a time + for (i=0; i< kBitMapBytes * 8; i+=4) { + startBlock = blockFile->AllocateBlocks(4); + if (startBlock < 0) { + printf("Test 13: failed (AllocateBlocks(4) failed on i=%d)\n", i); + goto exit; + } + } + + // attempt allocation with full bit map + startBlock = blockFile->AllocateBlocks(4); + if (startBlock >= 0) { + printf("Test 13: failed (AllocateBlocks(4) erroneously succeeded i=%d)\n", i); + goto exit; + } + + // deallocate all the bits + for (i=0; i< kBitMapBytes * 8; i+=4) { + rv = blockFile->DeallocateBlocks(i,4); + if (NS_FAILED(rv)) { + printf("Test 13: failed (DeallocateBlocks(%d,4) returned: 0x%.8x)\n", i,rv); + goto exit; + } + } + + // bit map should be empty + + // allocate as many triple-blocks as possible + for (i=0; i< kBitMapBytes * 8; i+=4) { + startBlock = blockFile->AllocateBlocks(3); + if (startBlock < 0) { + printf("Test 13: failed (AllocateBlocks(3) failed on i=%d)\n", i); + goto exit; + } + } + + // attempt allocation with "full" bit map + startBlock = blockFile->AllocateBlocks(3); + if (startBlock >= 0) { + printf("Test 13: failed (AllocateBlocks(3) erroneously succeeded i=%d)\n", i); + goto exit; + } + + // leave some blocks allocated + + rv = blockFile->Close(); + if (NS_FAILED(rv)) { + printf("Test 13: failed (Close returned: 0x%.8x)\n", rv); + goto exit; + } + + printf("Test 13: passed\n"); + + + //---------------------------------------------------------------- + // Test 14: ValidateFile (open existing file w/size < allocated blocks + //---------------------------------------------------------------- + rv = blockFile->Open(localFile, 256); + if (NS_SUCCEEDED(rv)) { + printf("Test 14: failed (Open erroneously succeeded)\n"); + goto exit; + } + + // Delete existing file + rv = localFile->Delete(false); + if (NS_FAILED(rv)) { + printf("Test 14 failed (Delete returned: 0x%.8x)\n", rv); + goto exit; + } + printf("Test 14: passed\n"); + + + //---------------------------------------------------------------- + // Test 15: Allocate/Deallocate stress test + //---------------------------------------------------------------- + + rv = StressTest(localFile, 15, false); + if (NS_FAILED(rv)) + goto exit; + + printf("Test 15: passed\n"); + + + //---------------------------------------------------------------- + // Test 16: WriteBlocks + //---------------------------------------------------------------- + + rv = blockFile->Open(localFile, 256); + if (NS_FAILED(rv)) { + printf("Test 16: failed (Open returned: 0x%.8x)\n", rv); + goto exit; + } + + char * one = new char[256 * 1]; + char * two = new char[256 * 2]; + char * three = new char[256 * 3]; + char * four = new char[256 * 4]; + if (!one || !two || !three || !four) { + printf("Test 16: failed - out of memory\n"); + rv = NS_ERROR_OUT_OF_MEMORY; + goto exit; + } + + memset(one, 1, 256); + memset(two, 2, 256 * 2); + memset(three, 3, 256 * 3); + memset(four, 4, 256 * 4); + + startBlock = blockFile->AllocateBlocks(1); + if (startBlock != 0) { + printf("Test 16: failed (AllocateBlocks(1) failed)\n"); + goto exit; + } + + rv = blockFile->WriteBlocks(one, startBlock, 1); + if (NS_FAILED(rv)) { + printf("Test 16: failed (WriteBlocks(1) returned 0x%.8x)\n", rv); + goto exit; + } + + startBlock = blockFile->AllocateBlocks(2); + if (startBlock != 1) { // starting with empy map, this allocation should begin at block 1 + printf("Test 16: failed (AllocateBlocks(2) failed)\n"); + goto exit; + } + + rv = blockFile->WriteBlocks(two, startBlock, 2); + if (NS_FAILED(rv)) { + printf("Test 16: failed (WriteBlocks(2) returned 0x%.8x)\n", rv); + goto exit; + } + + startBlock = blockFile->AllocateBlocks(3); + if (startBlock != 4) { // starting with empy map, this allocation should begin at block 4 + printf("Test 16: failed (AllocateBlocks(3) failed)\n"); + goto exit; + } + + rv = blockFile->WriteBlocks(three, startBlock, 3); + if (NS_FAILED(rv)) { + printf("Test 16: failed (WriteBlocks(3) returned 0x%.8x)\n", rv); + goto exit; + } + + startBlock = blockFile->AllocateBlocks(4); + if (startBlock != 8) { // starting with empy map, this allocation should begin at block 8 + printf("Test 16: failed (AllocateBlocks(4) failed)\n"); + goto exit; + } + + rv = blockFile->WriteBlocks(four, startBlock, 4); + if (NS_FAILED(rv)) { + printf("Test 16: failed (WriteBlocks(4) returned 0x%.8x)\n", rv); + goto exit; + } + + printf("Test 16: passed\n"); + + + //---------------------------------------------------------------- + // Test 17: ReadBlocks + //---------------------------------------------------------------- + + rv = blockFile->ReadBlocks(one, 0, 1); + if (NS_FAILED(rv)) { + printf("Test 17: failed (ReadBlocks(1) returned 0x%.8x)\n", rv); + goto exit; + } + + // Verify buffer + for (i = 0; i < 256; i++) { + if (one[i] != 1) { + printf("Test 17: failed (verifying buffer 1)\n"); + rv = NS_ERROR_FAILURE; + goto exit; + } + } + + rv = blockFile->ReadBlocks(two, 1, 2); + if (NS_FAILED(rv)) { + printf("Test 17: failed (ReadBlocks(2) returned 0x%.8x)\n", rv); + goto exit; + } + + // Verify buffer + for (i = 0; i < 256 * 2; i++) { + if (two[i] != 2) { + printf("Test 17: failed (verifying buffer 2)\n"); + rv = NS_ERROR_FAILURE; + goto exit; + } + } + + rv = blockFile->ReadBlocks(three, 4, 3); + if (NS_FAILED(rv)) { + printf("Test 17: failed (ReadBlocks(3) returned 0x%.8x)\n", rv); + goto exit; + } + + // Verify buffer + for (i = 0; i < 256 * 3; i++) { + if (three[i] != 3) { + printf("Test 17: failed (verifying buffer 3)\n"); + rv = NS_ERROR_FAILURE; + goto exit; + } + } + + rv = blockFile->ReadBlocks(four, 8, 4); + if (NS_FAILED(rv)) { + printf("Test 17: failed (ReadBlocks(4) returned 0x%.8x)\n", rv); + goto exit; + } + + // Verify buffer + for (i = 0; i < 256 * 4; i++) { + if (four[i] != 4) { + printf("Test 17: failed (verifying buffer 4)\n"); + rv = NS_ERROR_FAILURE; + goto exit; + } + } + + rv = blockFile->Close(); + if (NS_FAILED(rv)) { + printf("Test 17: failed (Close returned: 0x%.8x)\n", rv); + goto exit; + } + + printf("Test 17: passed\n"); + + + //---------------------------------------------------------------- + // Test 18: ValidateFile (open existing file with blocks allocated) + //---------------------------------------------------------------- + rv = blockFile->Open(localFile, 256); + if (NS_FAILED(rv)) { + printf("Test 18: failed (Open returned: 0x%.8x)\n", rv); + goto exit; + } + + rv = blockFile->Close(); + if (NS_FAILED(rv)) { + printf("Test 18: failed (Close returned: 0x%.8x)\n", rv); + goto exit; + } + + printf("Test 18: passed\n"); + + //---------------------------------------------------------------- + // Test 19: WriteBlocks/ReadBlocks stress + //---------------------------------------------------------------- + + rv = StressTest(localFile, 19, false); + if (NS_FAILED(rv)) + goto exit; + + printf("Test 19: passed\n"); + + +exit: + + if (currentDirPath) + free(currentDirPath); + } // this scopes the nsCOMPtrs + // no nsCOMPtrs are allowed to be alive when you call NS_ShutdownXPCOM + if (NS_FAILED(rv)) + printf("Test failed: 0x%.8x\n", rv); + + rv = NS_ShutdownXPCOM(nullptr); + NS_ASSERTION(NS_SUCCEEDED(rv), "NS_ShutdownXPCOM failed"); + + printf("XPCOM shut down.\n\n"); + return 0; +} + diff --git a/netwerk/test/TestCachePrefixKeyParser.cpp b/netwerk/test/TestCachePrefixKeyParser.cpp new file mode 100644 index 0000000000..88e544accf --- /dev/null +++ b/netwerk/test/TestCachePrefixKeyParser.cpp @@ -0,0 +1,78 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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/. */ + +#include "TestHarness.h" +#include "nsILoadContextInfo.h" +#include "../cache2/CacheFileUtils.h" + +int +main(int32_t argc, char *argv[]) +{ + nsCOMPtr<nsILoadContextInfo> info; + nsAutoCString key, enh; + +#define CHECK(a) MOZ_ASSERT(a) + + info = ParseKey(NS_LITERAL_CSTRING("")); + CHECK(info && !info->IsPrivate() && !info->IsAnonymous() && !info->IsInBrowserElement() && info->AppId() == 0); + info = ParseKey(NS_LITERAL_CSTRING(":")); + CHECK(info && !info->IsPrivate() && !info->IsAnonymous() && !info->IsInBrowserElement() && info->AppId() == 0); + info = ParseKey(NS_LITERAL_CSTRING("a,")); + CHECK(info && !info->IsPrivate() && info->IsAnonymous() && !info->IsInBrowserElement() && info->AppId() == 0); + info = ParseKey(NS_LITERAL_CSTRING("a,:")); + CHECK(info && !info->IsPrivate() && info->IsAnonymous() && !info->IsInBrowserElement() && info->AppId() == 0); + info = ParseKey(NS_LITERAL_CSTRING("a,:xxx"), &enh, &key); + CHECK(info && !info->IsPrivate() && info->IsAnonymous() && !info->IsInBrowserElement() && info->AppId() == 0); + CHECK(NS_LITERAL_CSTRING("xxx").Equals(key)); + info = ParseKey(NS_LITERAL_CSTRING("b,:xxx")); + CHECK(info && !info->IsPrivate() && !info->IsAnonymous() && info->IsInBrowserElement() && info->AppId() == 0); + info = ParseKey(NS_LITERAL_CSTRING("a,b,:xxx"), &enh, &key); + CHECK(info && !info->IsPrivate() && info->IsAnonymous() && info->IsInBrowserElement() && info->AppId() == 0); + CHECK(NS_LITERAL_CSTRING("xxx").Equals(key)); + CHECK(enh.IsEmpty()); + info = ParseKey(NS_LITERAL_CSTRING("a,b,i123,:xxx")); + CHECK(info && !info->IsPrivate() && info->IsAnonymous() && info->IsInBrowserElement() && info->AppId() == 123); + info = ParseKey(NS_LITERAL_CSTRING("a,b,c,h***,i123,:xxx"), &enh, &key); + CHECK(info && !info->IsPrivate() && info->IsAnonymous() && info->IsInBrowserElement() && info->AppId() == 123); + CHECK(NS_LITERAL_CSTRING("xxx").Equals(key)); + info = ParseKey(NS_LITERAL_CSTRING("a,b,c,h***,i123,~enh,:xxx"), &enh, &key); + CHECK(info && !info->IsPrivate() && info->IsAnonymous() && info->IsInBrowserElement() && info->AppId() == 123); + CHECK(NS_LITERAL_CSTRING("xxx").Equals(key)); + CHECK(NS_LITERAL_CSTRING("enh").Equals(enh)); + info = ParseKey(NS_LITERAL_CSTRING("0x,1,a,b,i123,:xxx")); + CHECK(info && !info->IsPrivate() && info->IsAnonymous() && info->IsInBrowserElement() && info->AppId() == 123); + + nsAutoCString test; + AppendTagWithValue(test, '~', NS_LITERAL_CSTRING("e,nh,")); + info = ParseKey(test, &enh, &key); + CHECK(info && !info->IsPrivate() && !info->IsAnonymous() && !info->IsInBrowserElement() && info->AppId() == 0); + CHECK(NS_LITERAL_CSTRING("e,nh,").Equals(enh)); + + info = ParseKey(NS_LITERAL_CSTRING("a,i123,b,:xxx")); + CHECK(!info); + info = ParseKey(NS_LITERAL_CSTRING("a")); + CHECK(!info); + info = ParseKey(NS_LITERAL_CSTRING("a:")); + CHECK(!info); + info = ParseKey(NS_LITERAL_CSTRING("a:xxx")); + CHECK(!info); + info = ParseKey(NS_LITERAL_CSTRING("i123")); + CHECK(!info); + info = ParseKey(NS_LITERAL_CSTRING("i123:")); + CHECK(!info); + info = ParseKey(NS_LITERAL_CSTRING("i123:xxx")); + CHECK(!info); + info = ParseKey(NS_LITERAL_CSTRING("i123,x:")); + CHECK(!info); + info = ParseKey(NS_LITERAL_CSTRING("i,x,:")); + CHECK(!info); + info = ParseKey(NS_LITERAL_CSTRING("i:")); + CHECK(!info); + +#undef CHECK + + passed("ok"); + return 0; +} diff --git a/netwerk/test/TestCommon.h b/netwerk/test/TestCommon.h new file mode 100644 index 0000000000..72fb89afad --- /dev/null +++ b/netwerk/test/TestCommon.h @@ -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/. */ + +#ifndef TestCommon_h__ +#define TestCommon_h__ + +#include <stdlib.h> +#include "nsThreadUtils.h" +#include "mozilla/Attributes.h" + +inline int test_common_init(int *argc, char ***argv) +{ + return 0; +} + +//----------------------------------------------------------------------------- + +static bool gKeepPumpingEvents = false; + +class nsQuitPumpingEvent final : public nsIRunnable { + ~nsQuitPumpingEvent() {} +public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_IMETHOD Run() override { + gKeepPumpingEvents = false; + return NS_OK; + } +}; +NS_IMPL_ISUPPORTS(nsQuitPumpingEvent, nsIRunnable) + +static inline void PumpEvents() +{ + nsCOMPtr<nsIThread> thread = do_GetCurrentThread(); + + gKeepPumpingEvents = true; + while (gKeepPumpingEvents) + NS_ProcessNextEvent(thread); + + NS_ProcessPendingEvents(thread); +} + +static inline void QuitPumpingEvents() +{ + // Dispatch a task that toggles gKeepPumpingEvents so that we flush all + // of the pending tasks before exiting from PumpEvents. + nsCOMPtr<nsIRunnable> event = new nsQuitPumpingEvent(); + NS_DispatchToMainThread(event); +} + +#endif diff --git a/netwerk/test/TestCookie.cpp b/netwerk/test/TestCookie.cpp new file mode 100644 index 0000000000..68c7ff1b3e --- /dev/null +++ b/netwerk/test/TestCookie.cpp @@ -0,0 +1,921 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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/. */ + +#include "TestCommon.h" +#include "TestHarness.h" +#include "nsIServiceManager.h" +#include "nsICookieService.h" +#include "nsICookieManager.h" +#include "nsICookieManager2.h" +#include "nsICookie2.h" +#include <stdio.h> +#include "plstr.h" +#include "prprf.h" +#include "nsNetUtil.h" +#include "nsISimpleEnumerator.h" +#include "nsServiceManagerUtils.h" +#include "nsNetCID.h" +#include "nsStringAPI.h" +#include "nsIPrefBranch.h" +#include "nsIPrefService.h" + +static NS_DEFINE_CID(kCookieServiceCID, NS_COOKIESERVICE_CID); +static NS_DEFINE_CID(kPrefServiceCID, NS_PREFSERVICE_CID); + +// various pref strings +static const char kCookiesPermissions[] = "network.cookie.cookieBehavior"; +static const char kCookiesLifetimeEnabled[] = "network.cookie.lifetime.enabled"; +static const char kCookiesLifetimeDays[] = "network.cookie.lifetime.days"; +static const char kCookiesLifetimeCurrentSession[] = "network.cookie.lifetime.behavior"; +static const char kCookiesMaxPerHost[] = "network.cookie.maxPerHost"; +static const char kCookieLeaveSecurityAlone[] = "network.cookie.leave-secure-alone"; + +static char *sBuffer; + +#define OFFSET_ONE_WEEK int64_t(604800) * PR_USEC_PER_SEC +#define OFFSET_ONE_DAY int64_t(86400) * PR_USEC_PER_SEC + +//Set server time or expiry time +void +SetTime(PRTime offsetTime,nsAutoCString& serverString,nsAutoCString& cookieString,bool expiry) +{ + char timeStringPreset[40]; + PRTime CurrentTime = PR_Now(); + PRTime SetCookieTime = CurrentTime + offsetTime; + PRTime SetExpiryTime; + if (expiry) { + SetExpiryTime = SetCookieTime - OFFSET_ONE_DAY; + } else { + SetExpiryTime = SetCookieTime + OFFSET_ONE_DAY; + } + + // Set server time string + PRExplodedTime explodedTime; + PR_ExplodeTime(SetCookieTime , PR_GMTParameters, &explodedTime); + PR_FormatTimeUSEnglish(timeStringPreset, 40, "%c GMT", &explodedTime); + serverString.Assign(timeStringPreset); + + // Set cookie string + PR_ExplodeTime(SetExpiryTime , PR_GMTParameters, &explodedTime); + PR_FormatTimeUSEnglish(timeStringPreset, 40, "%c GMT", &explodedTime); + cookieString.Replace(0, strlen("test=expiry; expires=") + strlen(timeStringPreset) + 1, "test=expiry; expires="); + cookieString.Append(timeStringPreset); +} + +nsresult +SetACookie(nsICookieService *aCookieService, const char *aSpec1, const char *aSpec2, const char* aCookieString, const char *aServerTime) +{ + nsCOMPtr<nsIURI> uri1, uri2; + NS_NewURI(getter_AddRefs(uri1), aSpec1); + if (aSpec2) + NS_NewURI(getter_AddRefs(uri2), aSpec2); + + sBuffer = PR_sprintf_append(sBuffer, R"( for host "%s": SET )", aSpec1); + nsresult rv = aCookieService->SetCookieStringFromHttp(uri1, uri2, nullptr, (char *)aCookieString, aServerTime, nullptr); + // the following code is useless. the cookieservice blindly returns NS_OK + // from SetCookieString. we have to call GetCookie to see if the cookie was + // set correctly... + if (NS_FAILED(rv)) { + sBuffer = PR_sprintf_append(sBuffer, "nothing\n"); + } else { + sBuffer = PR_sprintf_append(sBuffer, "\"%s\"\n", aCookieString); + } + return rv; +} + +nsresult +SetACookieNoHttp(nsICookieService *aCookieService, const char *aSpec, const char* aCookieString) +{ + nsCOMPtr<nsIURI> uri; + NS_NewURI(getter_AddRefs(uri), aSpec); + + sBuffer = PR_sprintf_append(sBuffer, R"( for host "%s": SET )", aSpec); + nsresult rv = aCookieService->SetCookieString(uri, nullptr, (char *)aCookieString, nullptr); + // the following code is useless. the cookieservice blindly returns NS_OK + // from SetCookieString. we have to call GetCookie to see if the cookie was + // set correctly... + if (NS_FAILED(rv)) { + sBuffer = PR_sprintf_append(sBuffer, "nothing\n"); + } else { + sBuffer = PR_sprintf_append(sBuffer, "\"%s\"\n", aCookieString); + } + return rv; +} + +// returns true if cookie(s) for the given host were found; else false. +// the cookie string is returned via aCookie. +bool +GetACookie(nsICookieService *aCookieService, const char *aSpec1, const char *aSpec2, char **aCookie) +{ + nsCOMPtr<nsIURI> uri1, uri2; + NS_NewURI(getter_AddRefs(uri1), aSpec1); + if (aSpec2) + NS_NewURI(getter_AddRefs(uri2), aSpec2); + + sBuffer = PR_sprintf_append(sBuffer, R"( "%s": GOT )", aSpec1); + nsresult rv = aCookieService->GetCookieStringFromHttp(uri1, uri2, nullptr, aCookie); + if (NS_FAILED(rv)) { + sBuffer = PR_sprintf_append(sBuffer, "XXX GetCookieString() failed!\n"); + } + if (!*aCookie) { + sBuffer = PR_sprintf_append(sBuffer, "nothing\n"); + } else { + sBuffer = PR_sprintf_append(sBuffer, "\"%s\"\n", *aCookie); + } + return *aCookie != nullptr; +} + +// returns true if cookie(s) for the given host were found; else false. +// the cookie string is returned via aCookie. +bool +GetACookieNoHttp(nsICookieService *aCookieService, const char *aSpec, char **aCookie) +{ + nsCOMPtr<nsIURI> uri; + NS_NewURI(getter_AddRefs(uri), aSpec); + + sBuffer = PR_sprintf_append(sBuffer, R"( "%s": GOT )", aSpec); + nsresult rv = aCookieService->GetCookieString(uri, nullptr, aCookie); + if (NS_FAILED(rv)) { + sBuffer = PR_sprintf_append(sBuffer, "XXX GetCookieString() failed!\n"); + } + if (!*aCookie) { + sBuffer = PR_sprintf_append(sBuffer, "nothing\n"); + } else { + sBuffer = PR_sprintf_append(sBuffer, "\"%s\"\n", *aCookie); + } + return *aCookie != nullptr; +} + +// some #defines for comparison rules +#define MUST_BE_NULL 0 +#define MUST_EQUAL 1 +#define MUST_CONTAIN 2 +#define MUST_NOT_CONTAIN 3 +#define MUST_NOT_EQUAL 4 + +// a simple helper function to improve readability: +// takes one of the #defined rules above, and performs the appropriate test. +// true means the test passed; false means the test failed. +static inline bool +CheckResult(const char *aLhs, uint32_t aRule, const char *aRhs = nullptr) +{ + switch (aRule) { + case MUST_BE_NULL: + return !aLhs || !*aLhs; + + case MUST_EQUAL: + return !PL_strcmp(aLhs, aRhs); + + case MUST_NOT_EQUAL: + return PL_strcmp(aLhs, aRhs); + + case MUST_CONTAIN: + return PL_strstr(aLhs, aRhs) != nullptr; + + case MUST_NOT_CONTAIN: + return PL_strstr(aLhs, aRhs) == nullptr; + + default: + return false; // failure + } +} + +// helper function that ensures the first aSize elements of aResult are +// true (i.e. all tests succeeded). prints the result of the tests (if any +// tests failed, it prints the zero-based index of each failed test). +bool +PrintResult(const bool aResult[], uint32_t aSize) +{ + bool failed = false; + sBuffer = PR_sprintf_append(sBuffer, "*** tests "); + for (uint32_t i = 0; i < aSize; ++i) { + if (!aResult[i]) { + failed = true; + sBuffer = PR_sprintf_append(sBuffer, "%d ", i); + } + } + if (failed) { + sBuffer = PR_sprintf_append(sBuffer, "FAILED!\a\n"); + } else { + sBuffer = PR_sprintf_append(sBuffer, "passed.\n"); + } + return !failed; +} + +void +InitPrefs(nsIPrefBranch *aPrefBranch) +{ + // init some relevant prefs, so the tests don't go awry. + // we use the most restrictive set of prefs we can; + // however, we don't test third party blocking here. + aPrefBranch->SetIntPref(kCookiesPermissions, 0); // accept all + aPrefBranch->SetBoolPref(kCookiesLifetimeEnabled, true); + aPrefBranch->SetIntPref(kCookiesLifetimeCurrentSession, 0); + aPrefBranch->SetIntPref(kCookiesLifetimeDays, 1); + aPrefBranch->SetBoolPref(kCookieLeaveSecurityAlone, true); + // Set the base domain limit to 50 so we have a known value. + aPrefBranch->SetIntPref(kCookiesMaxPerHost, 50); +} + + +int +main(int32_t argc, char *argv[]) +{ + if (test_common_init(&argc, &argv) != 0) + return -1; + + bool allTestsPassed = true; + + ScopedXPCOM xpcom("TestCookie"); + + { + nsresult rv0; + + nsCOMPtr<nsICookieService> cookieService = + do_GetService(kCookieServiceCID, &rv0); + if (NS_FAILED(rv0)) return -1; + + nsCOMPtr<nsIPrefBranch> prefBranch = + do_GetService(kPrefServiceCID, &rv0); + if (NS_FAILED(rv0)) return -1; + + InitPrefs(prefBranch); + + bool rv[20]; + nsCString cookie; + + /* The basic idea behind these tests is the following: + * + * we set() some cookie, then try to get() it in various ways. we have + * several possible tests we perform on the cookie string returned from + * get(): + * + * a) check whether the returned string is null (i.e. we got no cookies + * back). this is used e.g. to ensure a given cookie was deleted + * correctly, or to ensure a certain cookie wasn't returned to a given + * host. + * b) check whether the returned string exactly matches a given string. + * this is used where we want to make sure our cookie service adheres to + * some strict spec (e.g. ordering of multiple cookies), or where we + * just know exactly what the returned string should be. + * c) check whether the returned string contains/does not contain a given + * string. this is used where we don't know/don't care about the + * ordering of multiple cookies - we just want to make sure the cookie + * string contains them all, in some order. + * + * the results of each individual testing operation from CheckResult() is + * stored in an array of bools, which is then checked against the expected + * outcomes (all successes), by PrintResult(). the overall result of all + * tests to date is kept in |allTestsPassed|, for convenient display at the + * end. + * + * Interpreting the output: + * each setting/getting operation will print output saying exactly what + * it's doing and the outcome, respectively. this information is only + * useful for debugging purposes; the actual result of the tests is + * printed at the end of each block of tests. this will either be "all + * tests passed" or "tests X Y Z failed", where X, Y, Z are the indexes + * of rv (i.e. zero-based). at the conclusion of all tests, the overall + * passed/failed result is printed. + * + * NOTE: this testsuite is not yet comprehensive or complete, and is + * somewhat contrived - still under development, and needs improving! + */ + + // *** basic tests + sBuffer = PR_sprintf_append(sBuffer, "*** Beginning basic tests...\n"); + + // test some basic variations of the domain & path + SetACookie(cookieService, "http://www.basic.com", nullptr, "test=basic", nullptr); + GetACookie(cookieService, "http://www.basic.com", nullptr, getter_Copies(cookie)); + rv[0] = CheckResult(cookie.get(), MUST_EQUAL, "test=basic"); + GetACookie(cookieService, "http://www.basic.com/testPath/testfile.txt", nullptr, getter_Copies(cookie)); + rv[1] = CheckResult(cookie.get(), MUST_EQUAL, "test=basic"); + GetACookie(cookieService, "http://www.basic.com./", nullptr, getter_Copies(cookie)); + rv[2] = CheckResult(cookie.get(), MUST_BE_NULL); + GetACookie(cookieService, "http://www.basic.com.", nullptr, getter_Copies(cookie)); + rv[3] = CheckResult(cookie.get(), MUST_BE_NULL); + GetACookie(cookieService, "http://www.basic.com./testPath/testfile.txt", nullptr, getter_Copies(cookie)); + rv[4] = CheckResult(cookie.get(), MUST_BE_NULL); + GetACookie(cookieService, "http://www.basic2.com/", nullptr, getter_Copies(cookie)); + rv[5] = CheckResult(cookie.get(), MUST_BE_NULL); + SetACookie(cookieService, "http://www.basic.com", nullptr, "test=basic; max-age=-1", nullptr); + GetACookie(cookieService, "http://www.basic.com/", nullptr, getter_Copies(cookie)); + rv[6] = CheckResult(cookie.get(), MUST_BE_NULL); + + allTestsPassed = PrintResult(rv, 7) && allTestsPassed; + + + // *** domain tests + sBuffer = PR_sprintf_append(sBuffer, "*** Beginning domain tests...\n"); + + // test some variations of the domain & path, for different domains of + // a domain cookie + SetACookie(cookieService, "http://www.domain.com", nullptr, "test=domain; domain=domain.com", nullptr); + GetACookie(cookieService, "http://domain.com", nullptr, getter_Copies(cookie)); + rv[0] = CheckResult(cookie.get(), MUST_EQUAL, "test=domain"); + GetACookie(cookieService, "http://domain.com.", nullptr, getter_Copies(cookie)); + rv[1] = CheckResult(cookie.get(), MUST_BE_NULL); + GetACookie(cookieService, "http://www.domain.com", nullptr, getter_Copies(cookie)); + rv[2] = CheckResult(cookie.get(), MUST_EQUAL, "test=domain"); + GetACookie(cookieService, "http://foo.domain.com", nullptr, getter_Copies(cookie)); + rv[3] = CheckResult(cookie.get(), MUST_EQUAL, "test=domain"); + SetACookie(cookieService, "http://www.domain.com", nullptr, "test=domain; domain=domain.com; max-age=-1", nullptr); + GetACookie(cookieService, "http://domain.com", nullptr, getter_Copies(cookie)); + rv[4] = CheckResult(cookie.get(), MUST_BE_NULL); + + SetACookie(cookieService, "http://www.domain.com", nullptr, "test=domain; domain=.domain.com", nullptr); + GetACookie(cookieService, "http://domain.com", nullptr, getter_Copies(cookie)); + rv[5] = CheckResult(cookie.get(), MUST_EQUAL, "test=domain"); + GetACookie(cookieService, "http://www.domain.com", nullptr, getter_Copies(cookie)); + rv[6] = CheckResult(cookie.get(), MUST_EQUAL, "test=domain"); + GetACookie(cookieService, "http://bah.domain.com", nullptr, getter_Copies(cookie)); + rv[7] = CheckResult(cookie.get(), MUST_EQUAL, "test=domain"); + SetACookie(cookieService, "http://www.domain.com", nullptr, "test=domain; domain=.domain.com; max-age=-1", nullptr); + GetACookie(cookieService, "http://domain.com", nullptr, getter_Copies(cookie)); + rv[8] = CheckResult(cookie.get(), MUST_BE_NULL); + + SetACookie(cookieService, "http://www.domain.com", nullptr, "test=domain; domain=.foo.domain.com", nullptr); + GetACookie(cookieService, "http://foo.domain.com", nullptr, getter_Copies(cookie)); + rv[9] = CheckResult(cookie.get(), MUST_BE_NULL); + + SetACookie(cookieService, "http://www.domain.com", nullptr, "test=domain; domain=moose.com", nullptr); + GetACookie(cookieService, "http://foo.domain.com", nullptr, getter_Copies(cookie)); + rv[10] = CheckResult(cookie.get(), MUST_BE_NULL); + + SetACookie(cookieService, "http://www.domain.com", nullptr, "test=domain; domain=domain.com.", nullptr); + GetACookie(cookieService, "http://foo.domain.com", nullptr, getter_Copies(cookie)); + rv[11] = CheckResult(cookie.get(), MUST_BE_NULL); + + SetACookie(cookieService, "http://www.domain.com", nullptr, "test=domain; domain=..domain.com", nullptr); + GetACookie(cookieService, "http://foo.domain.com", nullptr, getter_Copies(cookie)); + rv[12] = CheckResult(cookie.get(), MUST_BE_NULL); + + SetACookie(cookieService, "http://www.domain.com", nullptr, "test=domain; domain=..domain.com.", nullptr); + GetACookie(cookieService, "http://foo.domain.com", nullptr, getter_Copies(cookie)); + rv[13] = CheckResult(cookie.get(), MUST_BE_NULL); + + SetACookie(cookieService, "http://path.net/path/file", nullptr, R"(test=taco; path="/bogus")", nullptr); + GetACookie(cookieService, "http://path.net/path/file", nullptr, getter_Copies(cookie)); + rv[14] = CheckResult(cookie.get(), MUST_EQUAL, "test=taco"); + SetACookie(cookieService, "http://path.net/path/file", nullptr, "test=taco; max-age=-1", nullptr); + GetACookie(cookieService, "http://path.net/path/file", nullptr, getter_Copies(cookie)); + rv[15] = CheckResult(cookie.get(), MUST_BE_NULL); + + allTestsPassed = PrintResult(rv, 16) && allTestsPassed; + + + // *** path tests + sBuffer = PR_sprintf_append(sBuffer, "*** Beginning path tests...\n"); + + // test some variations of the domain & path, for different paths of + // a path cookie + SetACookie(cookieService, "http://path.net/path/file", nullptr, "test=path; path=/path", nullptr); + GetACookie(cookieService, "http://path.net/path", nullptr, getter_Copies(cookie)); + rv[0] = CheckResult(cookie.get(), MUST_EQUAL, "test=path"); + GetACookie(cookieService, "http://path.net/path/", nullptr, getter_Copies(cookie)); + rv[1] = CheckResult(cookie.get(), MUST_EQUAL, "test=path"); + GetACookie(cookieService, "http://path.net/path/hithere.foo", nullptr, getter_Copies(cookie)); + rv[2] = CheckResult(cookie.get(), MUST_EQUAL, "test=path"); + GetACookie(cookieService, "http://path.net/path?hithere/foo", nullptr, getter_Copies(cookie)); + rv[3] = CheckResult(cookie.get(), MUST_EQUAL, "test=path"); + GetACookie(cookieService, "http://path.net/path2", nullptr, getter_Copies(cookie)); + rv[4] = CheckResult(cookie.get(), MUST_BE_NULL); + GetACookie(cookieService, "http://path.net/path2/", nullptr, getter_Copies(cookie)); + rv[5] = CheckResult(cookie.get(), MUST_BE_NULL); + SetACookie(cookieService, "http://path.net/path/file", nullptr, "test=path; path=/path; max-age=-1", nullptr); + GetACookie(cookieService, "http://path.net/path/", nullptr, getter_Copies(cookie)); + rv[6] = CheckResult(cookie.get(), MUST_BE_NULL); + + SetACookie(cookieService, "http://path.net/path/file", nullptr, "test=path; path=/path/", nullptr); + GetACookie(cookieService, "http://path.net/path", nullptr, getter_Copies(cookie)); + rv[7] = CheckResult(cookie.get(), MUST_EQUAL, "test=path"); + GetACookie(cookieService, "http://path.net/path/", nullptr, getter_Copies(cookie)); + rv[8] = CheckResult(cookie.get(), MUST_EQUAL, "test=path"); + SetACookie(cookieService, "http://path.net/path/file", nullptr, "test=path; path=/path/; max-age=-1", nullptr); + GetACookie(cookieService, "http://path.net/path/", nullptr, getter_Copies(cookie)); + rv[9] = CheckResult(cookie.get(), MUST_BE_NULL); + + // note that a site can set a cookie for a path it's not on. + // this is an intentional deviation from spec (see comments in + // nsCookieService::CheckPath()), so we test this functionality too + SetACookie(cookieService, "http://path.net/path/file", nullptr, "test=path; path=/foo/", nullptr); + GetACookie(cookieService, "http://path.net/path", nullptr, getter_Copies(cookie)); + rv[10] = CheckResult(cookie.get(), MUST_BE_NULL); + GetACookie(cookieService, "http://path.net/foo", nullptr, getter_Copies(cookie)); + rv[11] = CheckResult(cookie.get(), MUST_EQUAL, "test=path"); + SetACookie(cookieService, "http://path.net/path/file", nullptr, "test=path; path=/foo/; max-age=-1", nullptr); + GetACookie(cookieService, "http://path.net/foo/", nullptr, getter_Copies(cookie)); + rv[12] = CheckResult(cookie.get(), MUST_BE_NULL); + + // bug 373228: make sure cookies with paths longer than 1024 bytes, + // and cookies with paths or names containing tabs, are rejected. + // the following cookie has a path > 1024 bytes explicitly specified in the cookie + SetACookie(cookieService, "http://path.net/", nullptr, "test=path; path=/1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890/", nullptr); + GetACookie(cookieService, "http://path.net/1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890", nullptr, getter_Copies(cookie)); + rv[13] = CheckResult(cookie.get(), MUST_BE_NULL); + // the following cookie has a path > 1024 bytes implicitly specified by the uri path + SetACookie(cookieService, "http://path.net/1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890/", nullptr, "test=path", nullptr); + GetACookie(cookieService, "http://path.net/1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890/", nullptr, getter_Copies(cookie)); + rv[14] = CheckResult(cookie.get(), MUST_BE_NULL); + // the following cookie includes a tab in the path + SetACookie(cookieService, "http://path.net/", nullptr, "test=path; path=/foo\tbar/", nullptr); + GetACookie(cookieService, "http://path.net/foo\tbar/", nullptr, getter_Copies(cookie)); + rv[15] = CheckResult(cookie.get(), MUST_BE_NULL); + // the following cookie includes a tab in the name + SetACookie(cookieService, "http://path.net/", nullptr, "test\ttabs=tab", nullptr); + GetACookie(cookieService, "http://path.net/", nullptr, getter_Copies(cookie)); + rv[16] = CheckResult(cookie.get(), MUST_BE_NULL); + // the following cookie includes a tab in the value - allowed + SetACookie(cookieService, "http://path.net/", nullptr, "test=tab\ttest", nullptr); + GetACookie(cookieService, "http://path.net/", nullptr, getter_Copies(cookie)); + rv[17] = CheckResult(cookie.get(), MUST_EQUAL, "test=tab\ttest"); + SetACookie(cookieService, "http://path.net/", nullptr, "test=tab\ttest; max-age=-1", nullptr); + GetACookie(cookieService, "http://path.net/", nullptr, getter_Copies(cookie)); + rv[18] = CheckResult(cookie.get(), MUST_BE_NULL); + + allTestsPassed = PrintResult(rv, 19) && allTestsPassed; + + + // *** expiry & deletion tests + // XXX add server time str parsing tests here + sBuffer = PR_sprintf_append(sBuffer, "*** Beginning expiry & deletion tests...\n"); + + // test some variations of the expiry time, + // and test deletion of previously set cookies + SetACookie(cookieService, "http://expireme.org/", nullptr, "test=expiry; max-age=-1", nullptr); + GetACookie(cookieService, "http://expireme.org/", nullptr, getter_Copies(cookie)); + rv[0] = CheckResult(cookie.get(), MUST_BE_NULL); + SetACookie(cookieService, "http://expireme.org/", nullptr, "test=expiry; max-age=0", nullptr); + GetACookie(cookieService, "http://expireme.org/", nullptr, getter_Copies(cookie)); + rv[1] = CheckResult(cookie.get(), MUST_BE_NULL); + SetACookie(cookieService, "http://expireme.org/", nullptr, "test=expiry; expires=bad", nullptr); + GetACookie(cookieService, "http://expireme.org/", nullptr, getter_Copies(cookie)); + rv[2] = CheckResult(cookie.get(), MUST_EQUAL, "test=expiry"); + SetACookie(cookieService, "http://expireme.org/", nullptr, "test=expiry; expires=Thu, 10 Apr 1980 16:33:12 GMT", nullptr); + GetACookie(cookieService, "http://expireme.org/", nullptr, getter_Copies(cookie)); + rv[3] = CheckResult(cookie.get(), MUST_BE_NULL); + SetACookie(cookieService, "http://expireme.org/", nullptr, R"(test=expiry; expires="Thu, 10 Apr 1980 16:33:12 GMT)", nullptr); + GetACookie(cookieService, "http://expireme.org/", nullptr, getter_Copies(cookie)); + rv[4] = CheckResult(cookie.get(), MUST_BE_NULL); + SetACookie(cookieService, "http://expireme.org/", nullptr, R"(test=expiry; expires="Thu, 10 Apr 1980 16:33:12 GMT")", nullptr); + GetACookie(cookieService, "http://expireme.org/", nullptr, getter_Copies(cookie)); + rv[5] = CheckResult(cookie.get(), MUST_BE_NULL); + + SetACookie(cookieService, "http://expireme.org/", nullptr, "test=expiry; max-age=60", nullptr); + GetACookie(cookieService, "http://expireme.org/", nullptr, getter_Copies(cookie)); + rv[6] = CheckResult(cookie.get(), MUST_EQUAL, "test=expiry"); + SetACookie(cookieService, "http://expireme.org/", nullptr, "test=expiry; max-age=-20", nullptr); + GetACookie(cookieService, "http://expireme.org/", nullptr, getter_Copies(cookie)); + rv[7] = CheckResult(cookie.get(), MUST_BE_NULL); + SetACookie(cookieService, "http://expireme.org/", nullptr, "test=expiry; max-age=60", nullptr); + GetACookie(cookieService, "http://expireme.org/", nullptr, getter_Copies(cookie)); + rv[8] = CheckResult(cookie.get(), MUST_EQUAL, "test=expiry"); + SetACookie(cookieService, "http://expireme.org/", nullptr, "test=expiry; expires=Thu, 10 Apr 1980 16:33:12 GMT", nullptr); + GetACookie(cookieService, "http://expireme.org/", nullptr, getter_Copies(cookie)); + rv[9] = CheckResult(cookie.get(), MUST_BE_NULL); + SetACookie(cookieService, "http://expireme.org/", nullptr, "test=expiry; max-age=60", nullptr); + SetACookie(cookieService, "http://expireme.org/", nullptr, "newtest=expiry; max-age=60", nullptr); + GetACookie(cookieService, "http://expireme.org/", nullptr, getter_Copies(cookie)); + rv[10] = CheckResult(cookie.get(), MUST_CONTAIN, "test=expiry"); + rv[11] = CheckResult(cookie.get(), MUST_CONTAIN, "newtest=expiry"); + SetACookie(cookieService, "http://expireme.org/", nullptr, "test=differentvalue; max-age=0", nullptr); + GetACookie(cookieService, "http://expireme.org/", nullptr, getter_Copies(cookie)); + rv[12] = CheckResult(cookie.get(), MUST_EQUAL, "newtest=expiry"); + SetACookie(cookieService, "http://expireme.org/", nullptr, "newtest=evendifferentvalue; max-age=0", nullptr); + GetACookie(cookieService, "http://expireme.org/", nullptr, getter_Copies(cookie)); + rv[13] = CheckResult(cookie.get(), MUST_BE_NULL); + + SetACookie(cookieService, "http://foo.expireme.org/", nullptr, "test=expiry; domain=.expireme.org; max-age=60", nullptr); + GetACookie(cookieService, "http://expireme.org/", nullptr, getter_Copies(cookie)); + rv[14] = CheckResult(cookie.get(), MUST_EQUAL, "test=expiry"); + SetACookie(cookieService, "http://bar.expireme.org/", nullptr, "test=differentvalue; domain=.expireme.org; max-age=0", nullptr); + GetACookie(cookieService, "http://expireme.org/", nullptr, getter_Copies(cookie)); + rv[15] = CheckResult(cookie.get(), MUST_BE_NULL); + + nsAutoCString ServerTime; + nsAutoCString CookieString; + + SetTime(-OFFSET_ONE_WEEK, ServerTime, CookieString, true); + SetACookie(cookieService, "http://expireme.org/", nullptr, CookieString.get(), ServerTime.get()); + GetACookie(cookieService, "http://expireme.org/", nullptr, getter_Copies(cookie)); + rv[16] = CheckResult(cookie.get(), MUST_BE_NULL); + // Set server time earlier than client time for one year + one day, and expirty time earlier than server time for one day. + SetTime(-(OFFSET_ONE_DAY + OFFSET_ONE_WEEK), ServerTime, CookieString, false); + SetACookie(cookieService, "http://expireme.org/", nullptr, CookieString.get(), ServerTime.get()); + GetACookie(cookieService, "http://expireme.org/", nullptr, getter_Copies(cookie)); + rv[17] = CheckResult(cookie.get(), MUST_BE_NULL); + // Set server time later than client time for one year, and expiry time later than server time for one day. + SetTime(OFFSET_ONE_WEEK, ServerTime, CookieString, false); + SetACookie(cookieService, "http://expireme.org/", nullptr, CookieString.get(), ServerTime.get()); + GetACookie(cookieService, "http://expireme.org/", nullptr, getter_Copies(cookie)); + rv[18] = CheckResult(cookie.get(), MUST_EQUAL, "test=expiry"); + // Set server time later than client time for one year + one day, and expiry time earlier than server time for one day. + SetTime((OFFSET_ONE_DAY + OFFSET_ONE_WEEK), ServerTime, CookieString, true); + SetACookie(cookieService, "http://expireme.org/", nullptr, CookieString.get(), ServerTime.get()); + GetACookie(cookieService, "http://expireme.org/", nullptr, getter_Copies(cookie)); + rv[19] = CheckResult(cookie.get(), MUST_EQUAL, "test=expiry"); + allTestsPassed = PrintResult(rv, 20) && allTestsPassed; + + // *** multiple cookie tests + sBuffer = PR_sprintf_append(sBuffer, "*** Beginning multiple cookie tests...\n"); + + // test the setting of multiple cookies, and test the order of precedence + // (a later cookie overwriting an earlier one, in the same header string) + SetACookie(cookieService, "http://multiple.cookies/", nullptr, "test=multiple; domain=.multiple.cookies \n test=different \n test=same; domain=.multiple.cookies \n newtest=ciao \n newtest=foo; max-age=-6 \n newtest=reincarnated", nullptr); + GetACookie(cookieService, "http://multiple.cookies/", nullptr, getter_Copies(cookie)); + rv[0] = CheckResult(cookie.get(), MUST_NOT_CONTAIN, "test=multiple"); + rv[1] = CheckResult(cookie.get(), MUST_CONTAIN, "test=different"); + rv[2] = CheckResult(cookie.get(), MUST_CONTAIN, "test=same"); + rv[3] = CheckResult(cookie.get(), MUST_NOT_CONTAIN, "newtest=ciao"); + rv[4] = CheckResult(cookie.get(), MUST_NOT_CONTAIN, "newtest=foo"); + rv[5] = CheckResult(cookie.get(), MUST_CONTAIN, "newtest=reincarnated"); + SetACookie(cookieService, "http://multiple.cookies/", nullptr, "test=expiry; domain=.multiple.cookies; max-age=0", nullptr); + GetACookie(cookieService, "http://multiple.cookies/", nullptr, getter_Copies(cookie)); + rv[6] = CheckResult(cookie.get(), MUST_NOT_CONTAIN, "test=same"); + SetACookie(cookieService, "http://multiple.cookies/", nullptr, "\n test=different; max-age=0 \n", nullptr); + GetACookie(cookieService, "http://multiple.cookies/", nullptr, getter_Copies(cookie)); + rv[7] = CheckResult(cookie.get(), MUST_NOT_CONTAIN, "test=different"); + SetACookie(cookieService, "http://multiple.cookies/", nullptr, "newtest=dead; max-age=0", nullptr); + GetACookie(cookieService, "http://multiple.cookies/", nullptr, getter_Copies(cookie)); + rv[8] = CheckResult(cookie.get(), MUST_BE_NULL); + + allTestsPassed = PrintResult(rv, 9) && allTestsPassed; + + + // *** parser tests + sBuffer = PR_sprintf_append(sBuffer, "*** Beginning parser tests...\n"); + + // test the cookie header parser, under various circumstances. + SetACookie(cookieService, "http://parser.test/", nullptr, "test=parser; domain=.parser.test; ;; ;=; ,,, ===,abc,=; abracadabra! max-age=20;=;;", nullptr); + GetACookie(cookieService, "http://parser.test/", nullptr, getter_Copies(cookie)); + rv[0] = CheckResult(cookie.get(), MUST_EQUAL, "test=parser"); + SetACookie(cookieService, "http://parser.test/", nullptr, "test=parser; domain=.parser.test; max-age=0", nullptr); + GetACookie(cookieService, "http://parser.test/", nullptr, getter_Copies(cookie)); + rv[1] = CheckResult(cookie.get(), MUST_BE_NULL); + SetACookie(cookieService, "http://parser.test/", nullptr, "test=\"fubar! = foo;bar\\\";\" parser; domain=.parser.test; max-age=6\nfive; max-age=2.63,", nullptr); + GetACookie(cookieService, "http://parser.test/", nullptr, getter_Copies(cookie)); + rv[2] = CheckResult(cookie.get(), MUST_CONTAIN, R"(test="fubar! = foo)"); + rv[3] = CheckResult(cookie.get(), MUST_CONTAIN, "five"); + SetACookie(cookieService, "http://parser.test/", nullptr, "test=kill; domain=.parser.test; max-age=0 \n five; max-age=0", nullptr); + GetACookie(cookieService, "http://parser.test/", nullptr, getter_Copies(cookie)); + rv[4] = CheckResult(cookie.get(), MUST_BE_NULL); + + // test the handling of VALUE-only cookies (see bug 169091), + // i.e. "six" should assume an empty NAME, which allows other VALUE-only + // cookies to overwrite it + SetACookie(cookieService, "http://parser.test/", nullptr, "six", nullptr); + GetACookie(cookieService, "http://parser.test/", nullptr, getter_Copies(cookie)); + rv[5] = CheckResult(cookie.get(), MUST_EQUAL, "six"); + SetACookie(cookieService, "http://parser.test/", nullptr, "seven", nullptr); + GetACookie(cookieService, "http://parser.test/", nullptr, getter_Copies(cookie)); + rv[6] = CheckResult(cookie.get(), MUST_EQUAL, "seven"); + SetACookie(cookieService, "http://parser.test/", nullptr, " =eight", nullptr); + GetACookie(cookieService, "http://parser.test/", nullptr, getter_Copies(cookie)); + rv[7] = CheckResult(cookie.get(), MUST_EQUAL, "eight"); + SetACookie(cookieService, "http://parser.test/", nullptr, "test=six", nullptr); + GetACookie(cookieService, "http://parser.test/", nullptr, getter_Copies(cookie)); + rv[9] = CheckResult(cookie.get(), MUST_CONTAIN, "test=six"); + + allTestsPassed = PrintResult(rv, 10) && allTestsPassed; + + + // *** path ordering tests + sBuffer = PR_sprintf_append(sBuffer, "*** Beginning path ordering tests...\n"); + + // test that cookies are returned in path order - longest to shortest. + // if the header doesn't specify a path, it's taken from the host URI. + SetACookie(cookieService, "http://multi.path.tests/", nullptr, "test1=path; path=/one/two/three", nullptr); + SetACookie(cookieService, "http://multi.path.tests/", nullptr, "test2=path; path=/one \n test3=path; path=/one/two/three/four \n test4=path; path=/one/two \n test5=path; path=/one/two/", nullptr); + SetACookie(cookieService, "http://multi.path.tests/one/two/three/four/five/", nullptr, "test6=path", nullptr); + SetACookie(cookieService, "http://multi.path.tests/one/two/three/four/five/six/", nullptr, "test7=path; path=", nullptr); + SetACookie(cookieService, "http://multi.path.tests/", nullptr, "test8=path; path=/", nullptr); + GetACookie(cookieService, "http://multi.path.tests/one/two/three/four/five/six/", nullptr, getter_Copies(cookie)); + rv[0] = CheckResult(cookie.get(), MUST_EQUAL, "test7=path; test6=path; test3=path; test1=path; test5=path; test4=path; test2=path; test8=path"); + + allTestsPassed = PrintResult(rv, 1) && allTestsPassed; + + + // *** httponly tests + sBuffer = PR_sprintf_append(sBuffer, "*** Beginning httponly tests...\n"); + + // Since this cookie is NOT set via http, setting it fails + SetACookieNoHttp(cookieService, "http://httponly.test/", "test=httponly; httponly"); + GetACookie(cookieService, "http://httponly.test/", nullptr, getter_Copies(cookie)); + rv[0] = CheckResult(cookie.get(), MUST_BE_NULL); + // Since this cookie is set via http, it can be retrieved + SetACookie(cookieService, "http://httponly.test/", nullptr, "test=httponly; httponly", nullptr); + GetACookie(cookieService, "http://httponly.test/", nullptr, getter_Copies(cookie)); + rv[1] = CheckResult(cookie.get(), MUST_EQUAL, "test=httponly"); + // ... but not by web content + GetACookieNoHttp(cookieService, "http://httponly.test/", getter_Copies(cookie)); + rv[2] = CheckResult(cookie.get(), MUST_BE_NULL); + // Non-Http cookies should not replace HttpOnly cookies + SetACookie(cookieService, "http://httponly.test/", nullptr, "test=httponly; httponly", nullptr); + SetACookieNoHttp(cookieService, "http://httponly.test/", "test=not-httponly"); + GetACookie(cookieService, "http://httponly.test/", nullptr, getter_Copies(cookie)); + rv[3] = CheckResult(cookie.get(), MUST_EQUAL, "test=httponly"); + // ... and, if an HttpOnly cookie already exists, should not be set at all + GetACookieNoHttp(cookieService, "http://httponly.test/", getter_Copies(cookie)); + rv[4] = CheckResult(cookie.get(), MUST_BE_NULL); + // Non-Http cookies should not delete HttpOnly cookies + SetACookie(cookieService, "http://httponly.test/", nullptr, "test=httponly; httponly", nullptr); + SetACookieNoHttp(cookieService, "http://httponly.test/", "test=httponly; max-age=-1"); + GetACookie(cookieService, "http://httponly.test/", nullptr, getter_Copies(cookie)); + rv[5] = CheckResult(cookie.get(), MUST_EQUAL, "test=httponly"); + // ... but HttpOnly cookies should + SetACookie(cookieService, "http://httponly.test/", nullptr, "test=httponly; httponly; max-age=-1", nullptr); + GetACookie(cookieService, "http://httponly.test/", nullptr, getter_Copies(cookie)); + rv[6] = CheckResult(cookie.get(), MUST_BE_NULL); + // Non-Httponly cookies can replace HttpOnly cookies when set over http + SetACookie(cookieService, "http://httponly.test/", nullptr, "test=httponly; httponly", nullptr); + SetACookie(cookieService, "http://httponly.test/", nullptr, "test=not-httponly", nullptr); + GetACookieNoHttp(cookieService, "http://httponly.test/", getter_Copies(cookie)); + rv[7] = CheckResult(cookie.get(), MUST_EQUAL, "test=not-httponly"); + // scripts should not be able to set httponly cookies by replacing an existing non-httponly cookie + SetACookie(cookieService, "http://httponly.test/", nullptr, "test=not-httponly", nullptr); + SetACookieNoHttp(cookieService, "http://httponly.test/", "test=httponly; httponly"); + GetACookieNoHttp(cookieService, "http://httponly.test/", getter_Copies(cookie)); + rv[8] = CheckResult(cookie.get(), MUST_EQUAL, "test=not-httponly"); + + allTestsPassed = PrintResult(rv, 9) && allTestsPassed; + + + // *** Cookie prefix tests + sBuffer = PR_sprintf_append(sBuffer, "*** Beginning cookie prefix tests...\n"); + + // prefixed cookies can't be set from insecure HTTP + SetACookie(cookieService, "http://prefixed.test/", nullptr, "__Secure-test1=test", nullptr); + SetACookie(cookieService, "http://prefixed.test/", nullptr, "__Secure-test2=test; secure", nullptr); + SetACookie(cookieService, "http://prefixed.test/", nullptr, "__Host-test1=test", nullptr); + SetACookie(cookieService, "http://prefixed.test/", nullptr, "__Host-test2=test; secure", nullptr); + GetACookie(cookieService, "http://prefixed.test/", nullptr, getter_Copies(cookie)); + rv[0] = CheckResult(cookie.get(), MUST_BE_NULL); + + // prefixed cookies won't be set without the secure flag + SetACookie(cookieService, "https://prefixed.test/", nullptr, "__Secure-test=test", nullptr); + SetACookie(cookieService, "https://prefixed.test/", nullptr, "__Host-test=test", nullptr); + GetACookie(cookieService, "https://prefixed.test/", nullptr, getter_Copies(cookie)); + rv[1] = CheckResult(cookie.get(), MUST_BE_NULL); + + // prefixed cookies can be set when done correctly + SetACookie(cookieService, "https://prefixed.test/", nullptr, "__Secure-test=test; secure", nullptr); + SetACookie(cookieService, "https://prefixed.test/", nullptr, "__Host-test=test; secure", nullptr); + GetACookie(cookieService, "https://prefixed.test/", nullptr, getter_Copies(cookie)); + rv[2] = CheckResult(cookie.get(), MUST_CONTAIN, "__Secure-test=test"); + rv[3] = CheckResult(cookie.get(), MUST_CONTAIN, "__Host-test=test"); + + // but when set must not be returned to the host insecurely + GetACookie(cookieService, "http://prefixed.test/", nullptr, getter_Copies(cookie)); + rv[4] = CheckResult(cookie.get(), MUST_BE_NULL); + + // Host-prefixed cookies cannot specify a domain + SetACookie(cookieService, "https://host.prefixed.test/", nullptr, "__Host-a=test; secure; domain=prefixed.test", nullptr); + SetACookie(cookieService, "https://host.prefixed.test/", nullptr, "__Host-b=test; secure; domain=.prefixed.test", nullptr); + SetACookie(cookieService, "https://host.prefixed.test/", nullptr, "__Host-c=test; secure; domain=host.prefixed.test", nullptr); + SetACookie(cookieService, "https://host.prefixed.test/", nullptr, "__Host-d=test; secure; domain=.host.prefixed.test", nullptr); + GetACookie(cookieService, "https://host.prefixed.test/", nullptr, getter_Copies(cookie)); + rv[5] = CheckResult(cookie.get(), MUST_BE_NULL); + + // Host-prefixed cookies can only have a path of "/" + SetACookie(cookieService, "https://host.prefixed.test/some/path", nullptr, "__Host-e=test; secure", nullptr); + SetACookie(cookieService, "https://host.prefixed.test/some/path", nullptr, "__Host-f=test; secure; path=/", nullptr); + SetACookie(cookieService, "https://host.prefixed.test/some/path", nullptr, "__Host-g=test; secure; path=/some", nullptr); + GetACookie(cookieService, "https://host.prefixed.test/", nullptr, getter_Copies(cookie)); + rv[6] = CheckResult(cookie.get(), MUST_EQUAL, "__Host-f=test"); + + allTestsPassed = PrintResult(rv, 7) && allTestsPassed; + + // *** leave-secure-alone tests + sBuffer = PR_sprintf_append(sBuffer, "*** Beginning leave-secure-alone tests...\n"); + + // testing items 0 & 1 for 3.1 of spec Deprecate modification of ’secure’ + // cookies from non-secure origins + SetACookie(cookieService, "http://www.security.test/", nullptr, "test=non-security; secure", nullptr); + GetACookieNoHttp(cookieService, "https://www.security.test/", getter_Copies(cookie)); + rv[0] = CheckResult(cookie.get(), MUST_BE_NULL); + SetACookie(cookieService, "https://www.security.test/path/", nullptr, "test=security; secure; path=/path/", nullptr); + GetACookieNoHttp(cookieService, "https://www.security.test/path/", getter_Copies(cookie)); + rv[1] = CheckResult(cookie.get(), MUST_EQUAL, "test=security"); + // testing items 2 & 3 & 4 for 3.2 of spec Deprecate modification of ’secure’ + // cookies from non-secure origins + // Secure site can modify cookie value + SetACookie(cookieService, "https://www.security.test/path/", nullptr, "test=security2; secure; path=/path/", nullptr); + GetACookieNoHttp(cookieService, "https://www.security.test/path/", getter_Copies(cookie)); + rv[2] = CheckResult(cookie.get(), MUST_EQUAL, "test=security2"); + // If new cookie contains same name, same host and partially matching path with + // an existing security cookie on non-security site, it can't modify an existing + // security cookie. + SetACookie(cookieService, "http://www.security.test/path/foo/", nullptr, "test=non-security; path=/path/foo", nullptr); + GetACookieNoHttp(cookieService, "https://www.security.test/path/foo/", getter_Copies(cookie)); + rv[3] = CheckResult(cookie.get(), MUST_EQUAL, "test=security2"); + // Non-secure cookie can set by same name, same host and non-matching path. + SetACookie(cookieService, "http://www.security.test/bar/", nullptr, "test=non-security; path=/bar", nullptr); + GetACookieNoHttp(cookieService, "http://www.security.test/bar/", getter_Copies(cookie)); + rv[4] = CheckResult(cookie.get(), MUST_EQUAL, "test=non-security"); + // Modify value and downgrade secure level. + SetACookie(cookieService, "https://www.security.test/", nullptr, "test_modify_cookie=security-cookie; secure; domain=.security.test", nullptr); + GetACookieNoHttp(cookieService, "https://www.security.test/", getter_Copies(cookie)); + rv[5] = CheckResult(cookie.get(), MUST_EQUAL, "test_modify_cookie=security-cookie"); + SetACookie(cookieService, "https://www.security.test/", nullptr, "test_modify_cookie=non-security-cookie; domain=.security.test", nullptr); + GetACookieNoHttp(cookieService, "https://www.security.test/", getter_Copies(cookie)); + rv[6] = CheckResult(cookie.get(), MUST_EQUAL, "test_modify_cookie=non-security-cookie"); + // Test the non-security cookie can set when domain or path not same to secure cookie of same name. + SetACookie(cookieService, "https://www.security.test/", nullptr, "test=security3", nullptr); + GetACookieNoHttp(cookieService, "http://www.security.test/", getter_Copies(cookie)); + rv[7] = CheckResult(cookie.get(), MUST_CONTAIN, "test=security3"); + SetACookie(cookieService, "http://www.security.test/", nullptr, "test=non-security2; domain=security.test", nullptr); + GetACookieNoHttp(cookieService, "http://www.security.test/", getter_Copies(cookie)); + rv[8] = CheckResult(cookie.get(), MUST_CONTAIN, "test=non-security2"); + + allTestsPassed = PrintResult(rv, 9) && allTestsPassed; + + // *** nsICookieManager{2} interface tests + sBuffer = PR_sprintf_append(sBuffer, "*** Beginning nsICookieManager{2} interface tests...\n"); + nsCOMPtr<nsICookieManager> cookieMgr = do_GetService(NS_COOKIEMANAGER_CONTRACTID, &rv0); + if (NS_FAILED(rv0)) return -1; + nsCOMPtr<nsICookieManager2> cookieMgr2 = do_QueryInterface(cookieMgr); + if (!cookieMgr2) return -1; + + mozilla::NeckoOriginAttributes attrs; + + // first, ensure a clean slate + rv[0] = NS_SUCCEEDED(cookieMgr->RemoveAll()); + // add some cookies + rv[1] = NS_SUCCEEDED(cookieMgr2->AddNative(NS_LITERAL_CSTRING("cookiemgr.test"), // domain + NS_LITERAL_CSTRING("/foo"), // path + NS_LITERAL_CSTRING("test1"), // name + NS_LITERAL_CSTRING("yes"), // value + false, // is secure + false, // is httponly + true, // is session + INT64_MAX, // expiry time + &attrs)); // originAttributes + rv[2] = NS_SUCCEEDED(cookieMgr2->AddNative(NS_LITERAL_CSTRING("cookiemgr.test"), // domain + NS_LITERAL_CSTRING("/foo"), // path + NS_LITERAL_CSTRING("test2"), // name + NS_LITERAL_CSTRING("yes"), // value + false, // is secure + true, // is httponly + true, // is session + PR_Now() / PR_USEC_PER_SEC + 2, // expiry time + &attrs)); // originAttributes + rv[3] = NS_SUCCEEDED(cookieMgr2->AddNative(NS_LITERAL_CSTRING("new.domain"), // domain + NS_LITERAL_CSTRING("/rabbit"), // path + NS_LITERAL_CSTRING("test3"), // name + NS_LITERAL_CSTRING("yes"), // value + false, // is secure + false, // is httponly + true, // is session + INT64_MAX, // expiry time + &attrs)); // originAttributes + // confirm using enumerator + nsCOMPtr<nsISimpleEnumerator> enumerator; + rv[4] = NS_SUCCEEDED(cookieMgr->GetEnumerator(getter_AddRefs(enumerator))); + int32_t i = 0; + bool more; + nsCOMPtr<nsICookie2> expiredCookie, newDomainCookie; + while (NS_SUCCEEDED(enumerator->HasMoreElements(&more)) && more) { + nsCOMPtr<nsISupports> cookie; + if (NS_FAILED(enumerator->GetNext(getter_AddRefs(cookie)))) break; + ++i; + + // keep tabs on the second and third cookies, so we can check them later + nsCOMPtr<nsICookie2> cookie2(do_QueryInterface(cookie)); + if (!cookie2) break; + nsAutoCString name; + cookie2->GetName(name); + if (name.EqualsLiteral("test2")) + expiredCookie = cookie2; + else if (name.EqualsLiteral("test3")) + newDomainCookie = cookie2; + } + rv[5] = i == 3; + // check the httpOnly attribute of the second cookie is honored + GetACookie(cookieService, "http://cookiemgr.test/foo/", nullptr, getter_Copies(cookie)); + rv[6] = CheckResult(cookie.get(), MUST_CONTAIN, "test2=yes"); + GetACookieNoHttp(cookieService, "http://cookiemgr.test/foo/", getter_Copies(cookie)); + rv[7] = CheckResult(cookie.get(), MUST_NOT_CONTAIN, "test2=yes"); + // check CountCookiesFromHost() + uint32_t hostCookies = 0; + rv[8] = NS_SUCCEEDED(cookieMgr2->CountCookiesFromHost(NS_LITERAL_CSTRING("cookiemgr.test"), &hostCookies)) && + hostCookies == 2; + // check CookieExistsNative() using the third cookie + bool found; + rv[9] = NS_SUCCEEDED(cookieMgr2->CookieExistsNative(newDomainCookie, &attrs, &found)) && found; + + + // remove the cookie, block it, and ensure it can't be added again + rv[10] = NS_SUCCEEDED(cookieMgr->RemoveNative(NS_LITERAL_CSTRING("new.domain"), // domain + NS_LITERAL_CSTRING("test3"), // name + NS_LITERAL_CSTRING("/rabbit"), // path + true, // is blocked + &attrs)); // originAttributes + rv[11] = NS_SUCCEEDED(cookieMgr2->CookieExistsNative(newDomainCookie, &attrs, &found)) && !found; + rv[12] = NS_SUCCEEDED(cookieMgr2->AddNative(NS_LITERAL_CSTRING("new.domain"), // domain + NS_LITERAL_CSTRING("/rabbit"), // path + NS_LITERAL_CSTRING("test3"), // name + NS_LITERAL_CSTRING("yes"), // value + false, // is secure + false, // is httponly + true, // is session + INT64_MIN, // expiry time + &attrs)); // originAttributes + rv[13] = NS_SUCCEEDED(cookieMgr2->CookieExistsNative(newDomainCookie, &attrs, &found)) && !found; + // sleep four seconds, to make sure the second cookie has expired + PR_Sleep(4 * PR_TicksPerSecond()); + // check that both CountCookiesFromHost() and CookieExistsNative() count the + // expired cookie + rv[14] = NS_SUCCEEDED(cookieMgr2->CountCookiesFromHost(NS_LITERAL_CSTRING("cookiemgr.test"), &hostCookies)) && + hostCookies == 2; + rv[15] = NS_SUCCEEDED(cookieMgr2->CookieExistsNative(expiredCookie, &attrs, &found)) && found; + // double-check RemoveAll() using the enumerator + rv[16] = NS_SUCCEEDED(cookieMgr->RemoveAll()); + rv[17] = NS_SUCCEEDED(cookieMgr->GetEnumerator(getter_AddRefs(enumerator))) && + NS_SUCCEEDED(enumerator->HasMoreElements(&more)) && + !more; + + allTestsPassed = PrintResult(rv, 18) && allTestsPassed; + + + // *** eviction and creation ordering tests + sBuffer = PR_sprintf_append(sBuffer, "*** Beginning eviction and creation ordering tests...\n"); + + // test that cookies are + // a) returned by order of creation time (oldest first, newest last) + // b) evicted by order of lastAccessed time, if the limit on cookies per host (50) is reached + nsAutoCString name; + nsAutoCString expected; + for (int32_t i = 0; i < 60; ++i) { + name = NS_LITERAL_CSTRING("test"); + name.AppendInt(i); + name += NS_LITERAL_CSTRING("=creation"); + SetACookie(cookieService, "http://creation.ordering.tests/", nullptr, name.get(), nullptr); + + if (i >= 10) { + expected += name; + if (i < 59) + expected += NS_LITERAL_CSTRING("; "); + } + } + GetACookie(cookieService, "http://creation.ordering.tests/", nullptr, getter_Copies(cookie)); + rv[0] = CheckResult(cookie.get(), MUST_EQUAL, expected.get()); + + allTestsPassed = PrintResult(rv, 1) && allTestsPassed; + + // *** eviction and creation ordering tests after enable network.cookie.leave-secure-alone + sBuffer = PR_sprintf_append(sBuffer, "*** Beginning eviction and creation tests after enable nework.cookie.leave-secure-alone...\n"); + // reset cookie + cookieMgr->RemoveAll(); + + for (int32_t i = 0; i < 60; ++i) { + name = NS_LITERAL_CSTRING("test"); + name.AppendInt(i); + name += NS_LITERAL_CSTRING("=delete_non_security"); + + // Create 50 cookies that include the secure flag. + if (i < 50) { + name += NS_LITERAL_CSTRING("; secure"); + SetACookie(cookieService, "https://creation.ordering.tests/", nullptr, name.get(), nullptr); + } else { + // non-security cookies will be removed beside the latest cookie that be created. + SetACookie(cookieService, "http://creation.ordering.tests/", nullptr, name.get(), nullptr); + } + } + GetACookie(cookieService, "http://creation.ordering.tests/", nullptr, getter_Copies(cookie)); + rv[0] = CheckResult(cookie.get(), MUST_BE_NULL); + + allTestsPassed = PrintResult(rv, 1) && allTestsPassed; + + + // XXX the following are placeholders: add these tests please! + // *** "noncompliant cookie" tests + // *** IP address tests + // *** speed tests + + + sBuffer = PR_sprintf_append(sBuffer, "\n*** Result: %s!\n\n", allTestsPassed ? "all tests passed" : "TEST(S) FAILED"); + } + + if (!allTestsPassed) { + // print the entire log + printf("%s", sBuffer); + return 1; + } + + PR_smprintf_free(sBuffer); + sBuffer = nullptr; + + return 0; +} + +// Stubs to make this test happy + +mozilla::dom::OriginAttributesDictionary::OriginAttributesDictionary() + : mAppId(0), + mInIsolatedMozBrowser(false), + mPrivateBrowsingId(0), + mUserContextId(0) +{} diff --git a/netwerk/test/TestDNS.cpp b/netwerk/test/TestDNS.cpp new file mode 100644 index 0000000000..94a09a62a8 --- /dev/null +++ b/netwerk/test/TestDNS.cpp @@ -0,0 +1,130 @@ +/* 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/. */ + +#include "TestCommon.h" +#include <stdio.h> +#include <stdlib.h> +#include "nsIServiceManager.h" +#include "nsPIDNSService.h" +#include "nsIDNSListener.h" +#include "nsIDNSRecord.h" +#include "nsICancelable.h" +#include "nsCOMPtr.h" +#include "nsStringAPI.h" +#include "nsNetCID.h" +#include "prinrval.h" +#include "prthread.h" +#include "prnetdb.h" +#include "nsXPCOM.h" +#include "nsServiceManagerUtils.h" + +class myDNSListener : public nsIDNSListener +{ +public: + NS_DECL_THREADSAFE_ISUPPORTS + + myDNSListener(const char *host, int32_t index) + : mHost(host) + , mIndex(index) {} + + NS_IMETHOD OnLookupComplete(nsICancelable *request, + nsIDNSRecord *rec, + nsresult status) override + { + printf("%d: OnLookupComplete called [host=%s status=%x rec=%p]\n", + mIndex, mHost.get(), static_cast<uint32_t>(status), (void*)rec); + + if (NS_SUCCEEDED(status)) { + nsAutoCString buf; + + rec->GetCanonicalName(buf); + printf("%d: canonname=%s\n", mIndex, buf.get()); + + bool hasMore; + while (NS_SUCCEEDED(rec->HasMore(&hasMore)) && hasMore) { + rec->GetNextAddrAsString(buf); + printf("%d: => %s\n", mIndex, buf.get()); + } + } + + return NS_OK; + } + +private: + virtual ~myDNSListener() = default; + + nsCString mHost; + int32_t mIndex; +}; + + +NS_IMPL_ISUPPORTS(myDNSListener, nsIDNSListener) + +static bool IsAscii(const char *s) +{ + for (; *s; ++s) { + if (*s & 0x80) + return false; + } + + return true; +} + +int main(int argc, char **argv) +{ + if (test_common_init(&argc, &argv) != 0) + return -1; + + int sleepLen = 10; // default: 10 seconds + + if (argc == 1) { + printf("usage: TestDNS [-N] hostname1 [hostname2 ...]\n"); + return -1; + } + + { + nsCOMPtr<nsIServiceManager> servMan; + NS_InitXPCOM2(getter_AddRefs(servMan), nullptr, nullptr); + + nsCOMPtr<nsPIDNSService> dns = do_GetService(NS_DNSSERVICE_CONTRACTID); + if (!dns) + return -1; + + if (argv[1][0] == '-') { + sleepLen = atoi(argv[1]+1); + argv++; + argc--; + } + + for (int j=0; j<2; ++j) { + for (int i=1; i<argc; ++i) { + // assume non-ASCII input is given in the native charset + nsAutoCString hostBuf; + if (IsAscii(argv[i])) + hostBuf.Assign(argv[i]); + else + hostBuf = NS_ConvertUTF16toUTF8(NS_ConvertASCIItoUTF16(argv[i])); + + nsCOMPtr<nsIDNSListener> listener = new myDNSListener(argv[i], i); + + nsCOMPtr<nsICancelable> req; + nsresult rv = dns->AsyncResolve(hostBuf, + nsIDNSService::RESOLVE_CANONICAL_NAME, + listener, nullptr, getter_AddRefs(req)); + if (NS_FAILED(rv)) + printf("### AsyncResolve failed [rv=%x]\n", + static_cast<uint32_t>(rv)); + } + + printf("main thread sleeping for %d seconds...\n", sleepLen); + PR_Sleep(PR_SecondsToInterval(sleepLen)); + } + + printf("shutting down main thread...\n"); + dns->Shutdown(); + } + + NS_ShutdownXPCOM(nullptr); + return 0; +} diff --git a/netwerk/test/TestDNSDaemon.cpp b/netwerk/test/TestDNSDaemon.cpp new file mode 100644 index 0000000000..fa347aa0c7 --- /dev/null +++ b/netwerk/test/TestDNSDaemon.cpp @@ -0,0 +1,274 @@ +/* -*- 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/. */ + +#include <stdio.h> +#include <sys/types.h> +#include <sys/socket.h> +#include <sys/un.h> +#include <unistd.h> +#include <errno.h> + +#if defined(AIX) || defined(__linux) +#include <sys/select.h> // for fd_set +#endif + +#if defined(__linux) +// Didn't find gettdtablehi() or gettdtablesize() on linux. Using FD_SETSIZE +#define getdtablehi() FD_SETSIZE +#else +#define getdtablehi() getdtablesize() + +// If you find a system doesn't have getdtablesize try #define getdtablesize +// to FD_SETSIZE. And if you encounter a system that doesn't even have +// FD_SETSIZE, just grab your ankles and use 255. +#endif + + +#include "nspr.h" +#include "nsCRT.h" +#include "unix_dns.h" + +struct sockaddr_un unix_addr; + +int async_dns_lookup(char* hostName) +{ + fprintf(stderr, "start async_dns_lookup\n"); + int socket_fd = socket(PF_UNIX, SOCK_STREAM, 0); + if (socket_fd == -1) { + fprintf(stderr, "socket returned error.\n"); + return -1; + } + + unix_addr.sun_family = AF_UNIX; + strcpy(unix_addr.sun_path, DNS_SOCK_NAME); + + int err = connect(socket_fd,(struct sockaddr*)&unix_addr, sizeof(unix_addr)); + if (err == -1) { + fprintf(stderr, "connect failed (errno = %d).\n",errno); + close(socket_fd); + return -1; + } + + char buf[256]; + strcpy(buf, "lookup: "); + strcpy(&buf[8], hostName); + + err = send(socket_fd, buf, strlen(buf)+1, 0); + if (err < 0) + fprintf(stderr, "send(%s) returned error (errno=%d).\n",buf, errno); + + // receive 4 byte ID + err = recv(socket_fd, buf, 256, 0); + if (err < 0) + fprintf(stderr, "recv() returned error (errno=%d).\n", errno); + else + { + // printf("recv() returned %d bytes."); + int id = *(int *)buf; + fprintf(stderr, "id: %d\n", id); + } + + return socket_fd; +} + +static char * +string_trim(char *s) +{ + char *s2; + if (!s) return 0; + s2 = s + strlen(s) - 1; + while (s2 > s && (*s2 == '\n' || *s2 == '\r' || *s2 == ' ' || *s2 == '\t')) + *s2-- = 0; + while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') + s++; + return s; +} + +hostent * +bytesToHostent(char *buf) +{ + int i; + // int size = 0; + int len, aliasCount, addressCount; + int addrtype, addrlength; + char* p = buf; + char s[1024]; + + len = *(int *)p; // length of name + p += sizeof(int); // advance past name length + + memcpy(s, p, len); s[len] = 0; + fprintf(stderr, "hostname: %s\n", s); + + p += len; // advance past name + aliasCount = *(int *)p; // number of aliases + p += sizeof(int); // advance past alias count + + for (i=0; i<aliasCount; i++) { + len = *(int *)p; // length of alias name + p += sizeof(int); // advance past alias name length + + memcpy(s, p, len); s[len] = 0; + fprintf(stderr, "alias: %s\n", s); + + p += len; // advance past alias name + } + + addrtype = *(int *)p; + + fprintf(stderr, "addrtype: %d\n", addrtype); + + p += sizeof(int); + addrlength = *(int *)p; + + fprintf(stderr, "addrlength: %d\n", addrlength); + + p += sizeof(int); + addressCount = *(int *)p; + p += sizeof(int); + + for (i=0; i<addressCount; i++) { + len = *(int *)p; + p += sizeof(int); + + fprintf(stderr, "addr len: %d\n", len); + fprintf(stderr, "addr : %x\n", *(int *)p); + + p += len; + } + + // size = p - buf; + // size += 1 + aliasCount; + return 0; +} + +int +main(int argc, char* argv[]) +{ + PRStatus status; + + // launch daemon + printf("### launch daemon...\n"); + + PRProcessAttr *attributes = PR_NewProcessAttr(); + if (attributes == nullptr) { + printf("PR_NewProcessAttr() failed.\n"); + return -1; + } + + PRProcess *daemon = PR_CreateProcess("nsDnsAsyncLookup", nullptr, nullptr, attributes); + if (daemon == nullptr) { + printf("PR_CreateProcess failed.\n"); + } else { + // status = PR_DetachProcess(daemon); + //if (status != 0) + // printf("PR_DetachProcess returned %d\n", status); + //daemon = nullptr; + } + + PR_DestroyProcessAttr(attributes); + + // create socket and connect to daemon + int socket_fd = 0; + + + bool notDone = true; + char buf[1024]; + + while(notDone) { + int status = 0; + fd_set fdset; + + FD_ZERO(&fdset); + FD_SET(fileno(stdin), &fdset); + if (socket_fd > 0) + FD_SET(socket_fd, &fdset); + + status = select(getdtablehi(), &fdset, 0, 0, 0); + if (status <= 0) + { + fprintf(stderr, "%s: select() returned %d\n", argv[0], status); + exit(-1); + } + + // which fd is set? + + if (FD_ISSET(fileno(stdin), &fdset)) + { + char *line = fgets(buf, sizeof(buf)-1, stdin); + line = string_trim(line); + + if(!strcmp(line, "quit") || !strcmp(line, "exit")) + { + fprintf(stderr, "bye now.\n"); + notDone = false; + } + else if (!strncmp(line, "abort ", 6)) + { + // abort id + } + else if (strchr(line, ' ') || strchr(line, '\t')) + { + fprintf(stderr, "%s: unrecognized command %s.\n", argv[0], line); + } + else + { + fprintf(stderr, "%s: looking up %s...\n", argv[0], line); + // initiate dns lookup + socket_fd = async_dns_lookup(line); + } + } + + if (socket_fd && FD_ISSET(socket_fd, &fdset)) + { + // read from socket, parse results + int size = read(socket_fd, buf, 1024); + if (size > 0) + { + // parse buffer into hostent + char *p = buf; + fprintf(stderr, "bytes read: %d\n", size); + fprintf(stderr, "response code: %d\n", *(int *)p); + p += sizeof(int); + + for (int i=0; i < size; i++) { + if (!(i%8)) + fprintf(stderr, "\n"); + fprintf(stderr, "%2.2x ",(unsigned char)buf[i]); + } + fprintf(stderr, "\n"); + hostent *h; + h = bytesToHostent(p); + } + close(socket_fd); + socket_fd = 0; + } + } + + return 0; +} + +/* +buffer + +int nameLen; + +if (nameLen > 0) + char [nameLen+1] name + +int aliasCount +for each alias + int aliasNameLen + char [aliasNameLen+1] aliasName + +int h_addrtype +int h_length +int addrCount +for each addr + char[h_length] addr + + + +*/ diff --git a/netwerk/test/TestFileInput2.cpp b/netwerk/test/TestFileInput2.cpp new file mode 100644 index 0000000000..f51988010e --- /dev/null +++ b/netwerk/test/TestFileInput2.cpp @@ -0,0 +1,480 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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/. */ + +#include "nsIServiceManager.h" +#include "nsIComponentRegistrar.h" +#include "nsIInputStream.h" +#include "nsIOutputStream.h" +#include "nsIRunnable.h" +#include "nsIThread.h" +#include "nsCOMArray.h" +#include "nsISimpleEnumerator.h" +#include "prinrval.h" +#include "nsIFileStreams.h" +#include "nsIFileChannel.h" +#include "nsIFile.h" +#include "nsNetUtil.h" +#include <stdio.h> + +//////////////////////////////////////////////////////////////////////////////// + +#include <math.h> +#include "prprf.h" +#include "nsAutoLock.h" + +class nsTimeSampler { +public: + nsTimeSampler(); + void Reset(); + void StartTime(); + void EndTime(); + void AddTime(PRIntervalTime time); + PRIntervalTime LastInterval() { return mLastInterval; } + char* PrintStats(); +protected: + PRIntervalTime mStartTime; + double mSquares; + double mTotalTime; + uint32_t mCount; + PRIntervalTime mLastInterval; +}; + +nsTimeSampler::nsTimeSampler() +{ + Reset(); +} + +void +nsTimeSampler::Reset() +{ + mStartTime = 0; + mSquares = 0; + mTotalTime = 0; + mCount = 0; + mLastInterval = 0; +} + +void +nsTimeSampler::StartTime() +{ + mStartTime = PR_IntervalNow(); +} + +void +nsTimeSampler::EndTime() +{ + NS_ASSERTION(mStartTime != 0, "Forgot to call StartTime"); + PRIntervalTime endTime = PR_IntervalNow(); + mLastInterval = endTime - mStartTime; + AddTime(mLastInterval); + mStartTime = 0; +} + +void +nsTimeSampler::AddTime(PRIntervalTime time) +{ + nsAutoCMonitor mon(this); + mTotalTime += time; + mSquares += (double)time * (double)time; + mCount++; +} + +char* +nsTimeSampler::PrintStats() +{ + double mean = mTotalTime / mCount; + double variance = fabs(mSquares / mCount - mean * mean); + double stddev = sqrt(variance); + uint32_t imean = (uint32_t)mean; + uint32_t istddev = (uint32_t)stddev; + return PR_smprintf("%d +/- %d ms", + PR_IntervalToMilliseconds(imean), + PR_IntervalToMilliseconds(istddev)); +} + +//////////////////////////////////////////////////////////////////////////////// + +nsTimeSampler gTimeSampler; + +typedef nsresult (*CreateFun)(nsIRunnable* *result, + nsIFile* inPath, + nsIFile* outPath, + uint32_t bufferSize); + +//////////////////////////////////////////////////////////////////////////////// + +nsresult +Copy(nsIInputStream* inStr, nsIOutputStream* outStr, + char* buf, uint32_t bufSize, uint32_t *copyCount) +{ + nsresult rv; + while (true) { + uint32_t count; + rv = inStr->Read(buf, bufSize, &count); + if (NS_FAILED(rv)) return rv; + if (count == 0) break; + + uint32_t writeCount; + rv = outStr->Write(buf, count, &writeCount); + if (NS_FAILED(rv)) return rv; + NS_ASSERTION(writeCount == count, "didn't write all the data"); + *copyCount += writeCount; + } + rv = outStr->Flush(); + return rv; +} + +//////////////////////////////////////////////////////////////////////////////// + +class FileSpecWorker : public nsIRunnable { +public: + + NS_IMETHOD Run() override { + nsresult rv; + + PRIntervalTime startTime = PR_IntervalNow(); + PRIntervalTime endTime; + nsCOMPtr<nsIInputStream> inStr; + nsCOMPtr<nsIOutputStream> outStr; + uint32_t copyCount = 0; + + // Open the input stream: + nsCOMPtr<nsIInputStream> fileIn; + rv = NS_NewLocalFileInputStream(getter_AddRefs(fileIn), mInPath); + if (NS_FAILED(rv)) return rv; + + rv = NS_NewBufferedInputStream(getter_AddRefs(inStr), fileIn, 65535); + if (NS_FAILED(rv)) return rv; + + // Open the output stream: + nsCOMPtr<nsIOutputStream> fileOut; + rv = NS_NewLocalFileOutputStream(getter_AddRefs(fileOut), + mOutPath, + PR_CREATE_FILE | PR_WRONLY | PR_TRUNCATE, + 0664); + if (NS_FAILED(rv)) return rv; + + rv = NS_NewBufferedOutputStream(getter_AddRefs(outStr), fileOut, 65535); + if (NS_FAILED(rv)) return rv; + + // Copy from one to the other + rv = Copy(inStr, outStr, mBuffer, mBufferSize, ©Count); + if (NS_FAILED(rv)) return rv; + + endTime = PR_IntervalNow(); + gTimeSampler.AddTime(endTime - startTime); + + return rv; + } + + NS_DECL_ISUPPORTS + + FileSpecWorker() + : mInPath(nullptr), mOutPath(nullptr), mBuffer(nullptr), + mBufferSize(0) + { + } + + nsresult Init(nsIFile* inPath, nsIFile* outPath, + uint32_t bufferSize) + { + mInPath = inPath; + mOutPath = outPath; + mBuffer = new char[bufferSize]; + mBufferSize = bufferSize; + return (mInPath && mOutPath && mBuffer) + ? NS_OK : NS_ERROR_OUT_OF_MEMORY; + } + + static nsresult Create(nsIRunnable* *result, + nsIFile* inPath, + nsIFile* outPath, + uint32_t bufferSize) + { + FileSpecWorker* worker = new FileSpecWorker(); + if (worker == nullptr) + return NS_ERROR_OUT_OF_MEMORY; + NS_ADDREF(worker); + + nsresult rv = worker->Init(inPath, outPath, bufferSize); + if (NS_FAILED(rv)) { + NS_RELEASE(worker); + return rv; + } + *result = worker; + return NS_OK; + } + + virtual ~FileSpecWorker() { + delete[] mBuffer; + } + +protected: + nsCOMPtr<nsIFile> mInPath; + nsCOMPtr<nsIFile> mOutPath; + char* mBuffer; + uint32_t mBufferSize; +}; + +NS_IMPL_ISUPPORTS(FileSpecWorker, nsIRunnable) + +//////////////////////////////////////////////////////////////////////////////// + +#include "nsIIOService.h" +#include "nsIChannel.h" + +class FileChannelWorker : public nsIRunnable { +public: + + NS_IMETHOD Run() override { + nsresult rv; + + PRIntervalTime startTime = PR_IntervalNow(); + PRIntervalTime endTime; + uint32_t copyCount = 0; + nsCOMPtr<nsIFileChannel> inCh; + nsCOMPtr<nsIFileChannel> outCh; + nsCOMPtr<nsIInputStream> inStr; + nsCOMPtr<nsIOutputStream> outStr; + + rv = NS_NewLocalFileChannel(getter_AddRefs(inCh), mInPath); + if (NS_FAILED(rv)) return rv; + + rv = inCh->Open(getter_AddRefs(inStr)); + if (NS_FAILED(rv)) return rv; + + //rv = NS_NewLocalFileChannel(getter_AddRefs(outCh), mOutPath); + //if (NS_FAILED(rv)) return rv; + + //rv = outCh->OpenOutputStream(0, -1, 0, getter_AddRefs(outStr)); + //if (NS_FAILED(rv)) return rv; + + // Copy from one to the other + rv = Copy(inStr, outStr, mBuffer, mBufferSize, ©Count); + if (NS_FAILED(rv)) return rv; + + endTime = PR_IntervalNow(); + gTimeSampler.AddTime(endTime - startTime); + + return rv; + } + + NS_DECL_ISUPPORTS + + FileChannelWorker() + : mInPath(nullptr), mOutPath(nullptr), mBuffer(nullptr), + mBufferSize(0) + { + } + + nsresult Init(nsIFile* inPath, nsIFile* outPath, + uint32_t bufferSize) + { + mInPath = inPath; + mOutPath = outPath; + mBuffer = new char[bufferSize]; + mBufferSize = bufferSize; + return (mInPath && mOutPath && mBuffer) + ? NS_OK : NS_ERROR_OUT_OF_MEMORY; + } + + static nsresult Create(nsIRunnable* *result, + nsIFile* inPath, + nsIFile* outPath, + uint32_t bufferSize) + { + FileChannelWorker* worker = new FileChannelWorker(); + if (worker == nullptr) + return NS_ERROR_OUT_OF_MEMORY; + NS_ADDREF(worker); + + nsresult rv = worker->Init(inPath, outPath, bufferSize); + if (NS_FAILED(rv)) { + NS_RELEASE(worker); + return rv; + } + *result = worker; + return NS_OK; + } + + virtual ~FileChannelWorker() { + delete[] mBuffer; + } + +protected: + nsCOMPtr<nsIFile> mInPath; + nsCOMPtr<nsIFile> mOutPath; + char* mBuffer; + uint32_t mBufferSize; +}; + +NS_IMPL_ISUPPORTS(FileChannelWorker, nsIRunnable) + +//////////////////////////////////////////////////////////////////////////////// + +void +Test(CreateFun create, uint32_t count, + nsIFile* inDirSpec, nsIFile* outDirSpec, uint32_t bufSize) +{ + nsresult rv; + uint32_t i; + + nsAutoCString inDir; + nsAutoCString outDir; + (void)inDirSpec->GetNativePath(inDir); + (void)outDirSpec->GetNativePath(outDir); + printf("###########\nTest: from %s to %s, bufSize = %d\n", + inDir.get(), outDir.get(), bufSize); + gTimeSampler.Reset(); + nsTimeSampler testTime; + testTime.StartTime(); + + nsCOMArray<nsIThread> threads; + + nsCOMPtr<nsISimpleEnumerator> entries; + rv = inDirSpec->GetDirectoryEntries(getter_AddRefs(entries)); + NS_ASSERTION(NS_SUCCEEDED(rv), "GetDirectoryEntries failed"); + + i = 0; + bool hasMore; + while (i < count && NS_SUCCEEDED(entries->HasMoreElements(&hasMore)) && hasMore) { + nsCOMPtr<nsISupports> next; + rv = entries->GetNext(getter_AddRefs(next)); + if (NS_FAILED(rv)) goto done; + + nsCOMPtr<nsIFile> inSpec = do_QueryInterface(next, &rv); + if (NS_FAILED(rv)) goto done; + + nsCOMPtr<nsIFile> outSpec; + rv = outDirSpec->Clone(getter_AddRefs(outSpec)); // don't munge the original + if (NS_FAILED(rv)) goto done; + + nsAutoCString leafName; + rv = inSpec->GetNativeLeafName(leafName); + if (NS_FAILED(rv)) goto done; + + rv = outSpec->AppendNative(leafName); + if (NS_FAILED(rv)) goto done; + + bool exists; + rv = outSpec->Exists(&exists); + if (NS_FAILED(rv)) goto done; + + if (exists) { + rv = outSpec->Remove(false); + if (NS_FAILED(rv)) goto done; + } + + nsCOMPtr<nsIThread> thread; + nsCOMPtr<nsIRunnable> worker; + rv = create(getter_AddRefs(worker), + inSpec, + outSpec, + bufSize); + if (NS_FAILED(rv)) goto done; + + rv = NS_NewThread(getter_AddRefs(thread), worker, 0, PR_JOINABLE_THREAD); + if (NS_FAILED(rv)) goto done; + + bool inserted = threads.InsertObjectAt(thread, i); + NS_ASSERTION(inserted, "not inserted"); + + i++; + } + + uint32_t j; + for (j = 0; j < i; j++) { + nsIThread* thread = threads.ObjectAt(j); + thread->Join(); + } + + done: + NS_ASSERTION(rv == NS_OK, "failed"); + + testTime.EndTime(); + char* testStats = testTime.PrintStats(); + char* workerStats = gTimeSampler.PrintStats(); + printf(" threads = %d\n work time = %s,\n test time = %s\n", + i, workerStats, testStats); + PR_smprintf_free(workerStats); + PR_smprintf_free(testStats); +} + +//////////////////////////////////////////////////////////////////////////////// + +int +main(int argc, char* argv[]) +{ + nsresult rv; + + if (argc < 2) { + printf("usage: %s <in-dir> <out-dir>\n", argv[0]); + return -1; + } + char* inDir = argv[1]; + char* outDir = argv[2]; + + { + nsCOMPtr<nsIServiceManager> servMan; + NS_InitXPCOM2(getter_AddRefs(servMan), nullptr, nullptr); + nsCOMPtr<nsIComponentRegistrar> registrar = do_QueryInterface(servMan); + NS_ASSERTION(registrar, "Null nsIComponentRegistrar"); + if (registrar) + registrar->AutoRegister(nullptr); + + nsCOMPtr<nsIFile> inDirFile; + rv = NS_NewNativeLocalFile(nsDependentCString(inDir), false, getter_AddRefs(inDirFile)); + if (NS_FAILED(rv)) return rv; + + nsCOMPtr<nsIFile> outDirFile; + rv = NS_NewNativeLocalFile(nsDependentCString(outDir), false, getter_AddRefs(outDirFile)); + if (NS_FAILED(rv)) return rv; + + CreateFun create = FileChannelWorker::Create; + Test(create, 1, inDirFile, outDirFile, 16 * 1024); +#if 1 + printf("FileChannelWorker *****************************\n"); + Test(create, 20, inDirFile, outDirFile, 16 * 1024); + Test(create, 20, inDirFile, outDirFile, 16 * 1024); + Test(create, 20, inDirFile, outDirFile, 16 * 1024); + Test(create, 20, inDirFile, outDirFile, 16 * 1024); + Test(create, 20, inDirFile, outDirFile, 16 * 1024); + Test(create, 20, inDirFile, outDirFile, 16 * 1024); + Test(create, 20, inDirFile, outDirFile, 16 * 1024); + Test(create, 20, inDirFile, outDirFile, 16 * 1024); + Test(create, 20, inDirFile, outDirFile, 16 * 1024); +#endif + create = FileSpecWorker::Create; + printf("FileSpecWorker ********************************\n"); +#if 1 + Test(create, 20, inDirFile, outDirFile, 16 * 1024); + Test(create, 20, inDirFile, outDirFile, 16 * 1024); + Test(create, 20, inDirFile, outDirFile, 16 * 1024); + Test(create, 20, inDirFile, outDirFile, 16 * 1024); + Test(create, 20, inDirFile, outDirFile, 16 * 1024); + Test(create, 20, inDirFile, outDirFile, 16 * 1024); + Test(create, 20, inDirFile, outDirFile, 16 * 1024); + Test(create, 20, inDirFile, outDirFile, 16 * 1024); + Test(create, 20, inDirFile, outDirFile, 16 * 1024); +#endif +#if 1 + Test(create, 20, inDirFile, outDirFile, 4 * 1024); + Test(create, 20, inDirFile, outDirFile, 4 * 1024); + Test(create, 20, inDirFile, outDirFile, 4 * 1024); + Test(create, 20, inDirFile, outDirFile, 4 * 1024); + Test(create, 20, inDirFile, outDirFile, 4 * 1024); + Test(create, 20, inDirFile, outDirFile, 4 * 1024); + Test(create, 20, inDirFile, outDirFile, 4 * 1024); + Test(create, 20, inDirFile, outDirFile, 4 * 1024); + Test(create, 20, inDirFile, outDirFile, 4 * 1024); +#endif + } // this scopes the nsCOMPtrs + // no nsCOMPtrs are allowed to be alive when you call NS_ShutdownXPCOM + rv = NS_ShutdownXPCOM(nullptr); + NS_ASSERTION(NS_SUCCEEDED(rv), "NS_ShutdownXPCOM failed"); + return 0; +} + +//////////////////////////////////////////////////////////////////////////////// diff --git a/netwerk/test/TestIDN.cpp b/netwerk/test/TestIDN.cpp new file mode 100644 index 0000000000..77d57f14d6 --- /dev/null +++ b/netwerk/test/TestIDN.cpp @@ -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/. */ + +#include "TestCommon.h" +#include <stdio.h> +#include "nsIIDNService.h" +#include "nsCOMPtr.h" +#include "nsIServiceManager.h" +#include "nsServiceManagerUtils.h" +#include "nsNetCID.h" +#include "nsStringAPI.h" + +int main(int argc, char **argv) { + if (test_common_init(&argc, &argv) != 0) + return -1; + + // Test case from RFC 3492 (7.1 - Simplified Chinese) + const char plain[] = + "\xE4\xBB\x96\xE4\xBB\xAC\xE4\xB8\xBA\xE4\xBB\x80\xE4\xB9\x88\xE4\xB8\x8D\xE8\xAF\xB4\xE4\xB8\xAD\xE6\x96\x87"; + const char encoded[] = "xn--ihqwcrb4cv8a8dqg056pqjye"; + + nsCOMPtr<nsIIDNService> converter = do_GetService(NS_IDNSERVICE_CONTRACTID); + NS_ASSERTION(converter, "idnSDK not installed!"); + if (converter) { + nsAutoCString buf; + nsresult rv = converter->ConvertUTF8toACE(NS_LITERAL_CSTRING(plain), buf); + NS_ASSERTION(NS_SUCCEEDED(rv), "error ConvertUTF8toACE"); + NS_ASSERTION(buf.Equals(NS_LITERAL_CSTRING(encoded)), + "encode result incorrect"); + printf("encoded = %s\n", buf.get()); + + buf.Truncate(); + rv = converter->ConvertACEtoUTF8(NS_LITERAL_CSTRING(encoded), buf); + NS_ASSERTION(NS_SUCCEEDED(rv), "error ConvertACEtoUTF8"); + NS_ASSERTION(buf.Equals(NS_LITERAL_CSTRING(plain)), + "decode result incorrect"); + printf("decoded = "); + NS_ConvertUTF8toUTF16 utf(buf); + const char16_t *u = utf.get(); + for (int i = 0; u[i]; i++) { + printf("U+%.4X ", u[i]); + } + printf("\n"); + + bool isAce; + rv = converter->IsACE(NS_LITERAL_CSTRING("www.xn--ihqwcrb4cv8a8dqg056pqjye.com"), &isAce); + NS_ASSERTION(NS_SUCCEEDED(rv), "error IsACE"); + NS_ASSERTION(isAce, "IsACE incorrect result"); + } + return 0; +} diff --git a/netwerk/test/TestIOThreads.cpp b/netwerk/test/TestIOThreads.cpp new file mode 100644 index 0000000000..94aade27e3 --- /dev/null +++ b/netwerk/test/TestIOThreads.cpp @@ -0,0 +1,75 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "TestCommon.h" +#include "nsXPCOM.h" +#include "nsIServiceManager.h" +#include "nsServiceManagerUtils.h" +#include "nsIEventTarget.h" +#include "nsCOMPtr.h" +#include "nsNetCID.h" +#include "mozilla/Logging.h" + +// +// set NSPR_LOG_MODULES=Test:5 +// +static PRLogModuleInfo *gTestLog = nullptr; +#define LOG(args) MOZ_LOG(gTestLog, mozilla::LogLevel::Debug, args) + +class nsIOEvent : public nsIRunnable { +public: + NS_DECL_THREADSAFE_ISUPPORTS + + nsIOEvent(int i) : mIndex(i) {} + + NS_IMETHOD Run() override { + LOG(("Run [%d]\n", mIndex)); + return NS_OK; + } + +private: + int mIndex; +}; +NS_IMPL_ISUPPORTS(nsIOEvent, nsIRunnable) + +static nsresult RunTest() +{ + nsresult rv; + nsCOMPtr<nsIEventTarget> target = + do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID, &rv); + if (NS_FAILED(rv)) + return rv; + + for (int i=0; i<10; ++i) { + nsCOMPtr<nsIRunnable> event = new nsIOEvent(i); + LOG(("Dispatch %d\n", i)); + target->Dispatch(event, NS_DISPATCH_NORMAL); + } + + return NS_OK; +} + +int main(int argc, char **argv) +{ + if (test_common_init(&argc, &argv) != 0) + return -1; + + nsresult rv; + + gTestLog = PR_NewLogModule("Test"); + + rv = NS_InitXPCOM2(nullptr, nullptr, nullptr); + if (NS_FAILED(rv)) + return rv; + + rv = RunTest(); + if (NS_FAILED(rv)) + LOG(("RunTest failed [rv=%x]\n", rv)); + + LOG(("sleeping main thread for 2 seconds...\n")); + PR_Sleep(PR_SecondsToInterval(2)); + + NS_ShutdownXPCOM(nullptr); + return 0; +} diff --git a/netwerk/test/TestIncrementalDownload.cpp b/netwerk/test/TestIncrementalDownload.cpp new file mode 100644 index 0000000000..19308b2fa9 --- /dev/null +++ b/netwerk/test/TestIncrementalDownload.cpp @@ -0,0 +1,133 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 cin 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/. */ + +#include <inttypes.h> +#include <stdlib.h> +#include "TestCommon.h" +#include "nsNetUtil.h" +#include "nsComponentManagerUtils.h" +#include "nsIIncrementalDownload.h" +#include "nsIRequestObserver.h" +#include "nsIProgressEventSink.h" +#include "nsThreadUtils.h" +#include "nsAutoPtr.h" +#include "prprf.h" +#include "prenv.h" +#include "mozilla/Attributes.h" + +//----------------------------------------------------------------------------- + +class FetchObserver final : public nsIRequestObserver + , public nsIProgressEventSink +{ + ~FetchObserver() = default; +public: + NS_DECL_ISUPPORTS + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSIPROGRESSEVENTSINK +}; + +NS_IMPL_ISUPPORTS(FetchObserver, nsIRequestObserver, + nsIProgressEventSink) + +NS_IMETHODIMP +FetchObserver::OnStartRequest(nsIRequest *request, nsISupports *context) +{ + printf("FetchObserver::OnStartRequest\n"); + return NS_OK; +} + +NS_IMETHODIMP +FetchObserver::OnProgress(nsIRequest *request, nsISupports *context, + int64_t progress, int64_t progressMax) +{ + printf("FetchObserver::OnProgress [%" PRId64 "/%" PRId64 "]\n", + progress, progressMax); + return NS_OK; +} + +NS_IMETHODIMP +FetchObserver::OnStatus(nsIRequest *request, nsISupports *context, + nsresult status, const char16_t *statusText) +{ + return NS_OK; +} + +NS_IMETHODIMP +FetchObserver::OnStopRequest(nsIRequest *request, nsISupports *context, + nsresult status) +{ + printf("FetchObserver::OnStopRequest [status=%x]\n", + static_cast<uint32_t>(status)); + + QuitPumpingEvents(); + return NS_OK; +} + +//----------------------------------------------------------------------------- + +static nsresult +DoIncrementalFetch(const char *uriSpec, const char *resultPath, int32_t chunkSize, + int32_t interval) +{ + nsCOMPtr<nsIFile> resultFile; + nsresult rv = NS_NewNativeLocalFile(nsDependentCString(resultPath), + false, getter_AddRefs(resultFile)); + if (NS_FAILED(rv)) + return rv; + + nsCOMPtr<nsIURI> uri; + rv = NS_NewURI(getter_AddRefs(uri), uriSpec); + if (NS_FAILED(rv)) + return rv; + + nsCOMPtr<nsIRequestObserver> observer = new FetchObserver(); + if (!observer) + return NS_ERROR_OUT_OF_MEMORY; + + nsCOMPtr<nsIIncrementalDownload> download = + do_CreateInstance(NS_INCREMENTALDOWNLOAD_CONTRACTID, &rv); + if (NS_FAILED(rv)) + return rv; + + rv = download->Init(uri, resultFile, chunkSize, interval); + if (NS_FAILED(rv)) + return rv; + + rv = download->Start(observer, nullptr); + if (NS_FAILED(rv)) + return rv; + + PumpEvents(); + return NS_OK; +} + +int +main(int argc, char **argv) +{ + if (PR_GetEnv("MOZ_BREAK_ON_MAIN")) + NS_BREAK(); + + if (argc < 5) { + fprintf(stderr, "USAGE: TestIncrementalDownload <url> <file> <chunksize> <interval-in-seconds>\n"); + return -1; + } + + nsresult rv = NS_InitXPCOM2(nullptr, nullptr, nullptr); + if (NS_FAILED(rv)) + return -1; + + int32_t chunkSize = atoi(argv[3]); + int32_t interval = atoi(argv[4]); + + rv = DoIncrementalFetch(argv[1], argv[2], chunkSize, interval); + if (NS_FAILED(rv)) + fprintf(stderr, "ERROR: DoIncrementalFetch failed [%x]\n", + static_cast<uint32_t>(rv)); + + NS_ShutdownXPCOM(nullptr); + return 0; +} diff --git a/netwerk/test/TestMakeAbs.cpp b/netwerk/test/TestMakeAbs.cpp new file mode 100644 index 0000000000..51c21edd91 --- /dev/null +++ b/netwerk/test/TestMakeAbs.cpp @@ -0,0 +1,69 @@ +/* -*- 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/. */ + +#include "nspr.h" +#include "nscore.h" +#include "nsCOMPtr.h" +#include "nsIIOService.h" +#include "nsIServiceManager.h" +#include "nsIComponentRegistrar.h" +#include "nsIURI.h" + +static NS_DEFINE_CID(kIOServiceCID, NS_IOSERVICE_CID); + +nsresult ServiceMakeAbsolute(nsIURI *baseURI, char *relativeInfo, char **_retval) { + nsresult rv; + nsCOMPtr<nsIIOService> serv(do_GetService(kIOServiceCID, &rv)); + if (NS_FAILED(rv)) return rv; + + return serv->MakeAbsolute(relativeInfo, baseURI, _retval); +} + +nsresult URLMakeAbsolute(nsIURI *baseURI, char *relativeInfo, char **_retval) { + return baseURI->MakeAbsolute(relativeInfo, _retval); +} + +int +main(int argc, char* argv[]) +{ + nsresult rv = NS_OK; + if (argc < 4) { + printf("usage: %s int (loop count) baseURL relativeSpec\n", argv[0]); + return -1; + } + + uint32_t cycles = atoi(argv[1]); + char *base = argv[2]; + char *rel = argv[3]; + { + nsCOMPtr<nsIServiceManager> servMan; + NS_InitXPCOM2(getter_AddRefs(servMan), nullptr, nullptr); + nsCOMPtr<nsIComponentRegistrar> registrar = do_QueryInterface(servMan); + NS_ASSERTION(registrar, "Null nsIComponentRegistrar"); + if (registrar) + registrar->AutoRegister(nullptr); + + nsCOMPtr<nsIIOService> serv(do_GetService(kIOServiceCID, &rv)); + if (NS_FAILED(rv)) return rv; + + nsCOMPtr<nsIURI> uri; + rv = serv->NewURI(base, nullptr, getter_AddRefs(uri)); + if (NS_FAILED(rv)) return rv; + + char *absURLString; + uint32_t i = 0; + while (i++ < cycles) { + rv = ServiceMakeAbsolute(uri, rel, &absURLString); + if (NS_FAILED(rv)) return rv; + free(absURLString); + + rv = URLMakeAbsolute(uri, rel, &absURLString); + free(absURLString); + } + } // this scopes the nsCOMPtrs + // no nsCOMPtrs are allowed to be alive when you call NS_ShutdownXPCOM + NS_ShutdownXPCOM(nullptr); + return rv; +} diff --git a/netwerk/test/TestNamedPipeService.cpp b/netwerk/test/TestNamedPipeService.cpp new file mode 100644 index 0000000000..7d3f2b58cb --- /dev/null +++ b/netwerk/test/TestNamedPipeService.cpp @@ -0,0 +1,334 @@ +/* -*- 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/. */ + +#include "TestCommon.h" +#include "TestHarness.h" + +#include <Windows.h> + +#include "mozilla/Atomics.h" +#include "mozilla/Monitor.h" +#include "nsINamedPipeService.h" +#include "nsNetCID.h" + +#define PIPE_NAME "\\\\.\\pipe\\TestNPS" +#define TEST_STR "Hello World" + +using namespace mozilla; + +class nsNamedPipeDataObserver : public nsINamedPipeDataObserver +{ +public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSINAMEDPIPEDATAOBSERVER + + explicit nsNamedPipeDataObserver(HANDLE aPipe); + + int Read(void* aBuffer, uint32_t aSize); + int Write(const void* aBuffer, uint32_t aSize); + + uint32_t Transferred() const { return mBytesTransferred; } + +private: + ~nsNamedPipeDataObserver() = default; + + HANDLE mPipe; + OVERLAPPED mOverlapped; + Atomic<uint32_t> mBytesTransferred; + Monitor mMonitor; +}; + +NS_IMPL_ISUPPORTS(nsNamedPipeDataObserver, nsINamedPipeDataObserver) + +nsNamedPipeDataObserver::nsNamedPipeDataObserver(HANDLE aPipe) + : mPipe(aPipe) + , mOverlapped() + , mBytesTransferred(0) + , mMonitor("named-pipe") +{ + mOverlapped.hEvent = CreateEventA(nullptr, TRUE, TRUE, "named-pipe"); +} + +int +nsNamedPipeDataObserver::Read(void* aBuffer, uint32_t aSize) +{ + DWORD bytesRead = 0; + if (!ReadFile(mPipe, aBuffer, aSize, &bytesRead, &mOverlapped)) { + switch(GetLastError()) { + case ERROR_IO_PENDING: + { + MonitorAutoLock lock(mMonitor); + mMonitor.Wait(); + } + if (!GetOverlappedResult(mPipe, &mOverlapped, &bytesRead, FALSE)) { + fail("GetOverlappedResult failed"); + return -1; + } + if (mBytesTransferred != bytesRead) { + fail("GetOverlappedResult mismatch"); + return -1; + } + + break; + default: + fail("ReadFile error %d", GetLastError()); + return -1; + } + } else { + MonitorAutoLock lock(mMonitor); + mMonitor.Wait(); + + if (mBytesTransferred != bytesRead) { + fail("GetOverlappedResult mismatch"); + return -1; + } + } + + mBytesTransferred = 0; + passed("[read] match"); + return bytesRead; +} + +int +nsNamedPipeDataObserver::Write(const void* aBuffer, uint32_t aSize) +{ + DWORD bytesWritten = 0; + if (!WriteFile(mPipe, aBuffer, aSize, &bytesWritten, &mOverlapped)) { + switch(GetLastError()) { + case ERROR_IO_PENDING: + { + MonitorAutoLock lock(mMonitor); + mMonitor.Wait(); + } + if (!GetOverlappedResult(mPipe, &mOverlapped, &bytesWritten, FALSE)) { + fail("GetOverlappedResult failed"); + return -1; + } + if (mBytesTransferred != bytesWritten) { + fail("GetOverlappedResult mismatch"); + return -1; + } + + break; + default: + fail("WriteFile error %d", GetLastError()); + return -1; + } + } else { + MonitorAutoLock lock(mMonitor); + mMonitor.Wait(); + + if (mBytesTransferred != bytesWritten) { + fail("GetOverlappedResult mismatch"); + return -1; + } + } + + mBytesTransferred = 0; + passed("[write] match"); + return bytesWritten; +} + +NS_IMETHODIMP +nsNamedPipeDataObserver::OnDataAvailable(uint32_t aBytesTransferred, + void *aOverlapped) +{ + if (aOverlapped != &mOverlapped) { + fail("invalid overlapped object"); + return NS_ERROR_FAILURE; + } + + DWORD bytesTransferred = 0; + BOOL ret = GetOverlappedResult(mPipe, + reinterpret_cast<LPOVERLAPPED>(aOverlapped), + &bytesTransferred, + FALSE); + + if (!ret) { + fail("GetOverlappedResult failed"); + return NS_ERROR_FAILURE; + } + + if (bytesTransferred != aBytesTransferred) { + fail("GetOverlappedResult mismatch"); + return NS_ERROR_FAILURE; + } + + mBytesTransferred += aBytesTransferred; + MonitorAutoLock lock(mMonitor); + mMonitor.Notify(); + + return NS_OK; +} + +NS_IMETHODIMP +nsNamedPipeDataObserver::OnError(uint32_t aError, void *aOverlapped) +{ + return NS_ERROR_NOT_IMPLEMENTED; +} + +BOOL CreateAndConnectInstance(LPOVERLAPPED aOverlapped, LPHANDLE aPipe); +BOOL ConnectToNewClient(HANDLE aPipe, LPOVERLAPPED aOverlapped); + +BOOL CreateAndConnectInstance(LPOVERLAPPED aOverlapped, LPHANDLE aPipe) +{ + if (!aPipe) { + fail("Parameter aPipe is NULL\n"); + return FALSE; + } + + // FIXME: adjust parameters + *aPipe = CreateNamedPipeA( + PIPE_NAME, + PIPE_ACCESS_DUPLEX | + FILE_FLAG_OVERLAPPED, + PIPE_TYPE_MESSAGE | + PIPE_READMODE_MESSAGE | + PIPE_WAIT, + 1, + 65536, + 65536, + 3000, + NULL); + + if (*aPipe == INVALID_HANDLE_VALUE) { + fail("CreateNamedPipe failed [%d]\n", GetLastError()); + return FALSE; + } + + return ConnectToNewClient(*aPipe, aOverlapped); +} + +BOOL ConnectToNewClient(HANDLE aPipe, LPOVERLAPPED aOverlapped) +{ + if (ConnectNamedPipe(aPipe, aOverlapped)) { + fail("Unexpected, overlapped ConnectNamedPipe() always returns 0.\n"); + return FALSE; + } + + switch (GetLastError()) + { + case ERROR_IO_PENDING: + return TRUE; + + case ERROR_PIPE_CONNECTED: + if (SetEvent(aOverlapped->hEvent)) + break; + + default: // error + fail("ConnectNamedPipe failed [%d]\n", GetLastError()); + break; + } + + return FALSE; +} + +static nsresult +CreateNamedPipe(LPHANDLE aServer, LPHANDLE aClient) +{ + OVERLAPPED overlapped; + overlapped.hEvent = CreateEvent(NULL, TRUE, TRUE, NULL); + BOOL ret; + + ret = CreateAndConnectInstance(&overlapped, aServer); + if (!ret) { + fail("pipe server should be pending"); + return NS_ERROR_FAILURE; + } + + *aClient = CreateFileA(PIPE_NAME, + GENERIC_READ | GENERIC_WRITE, + FILE_SHARE_READ | FILE_SHARE_WRITE, + nullptr, + OPEN_EXISTING, + FILE_FLAG_OVERLAPPED, + nullptr); + + if (*aClient == INVALID_HANDLE_VALUE) { + fail("Unable to create pipe client"); + CloseHandle(*aServer); + return NS_ERROR_FAILURE; + } + + DWORD pipeMode = PIPE_READMODE_MESSAGE; + if (!SetNamedPipeHandleState(*aClient, &pipeMode, nullptr, nullptr)) { + fail("SetNamedPipeHandleState error (%d)", GetLastError()); + CloseHandle(*aServer); + CloseHandle(*aClient); + return NS_ERROR_FAILURE; + } + + WaitForSingleObjectEx(overlapped.hEvent, INFINITE, TRUE); + + return NS_OK; +} + +int +main(int32_t argc, char* argv[]) +{ + ScopedXPCOM xpcom("NamedPipeService"); + if (xpcom.failed()) { + fail("Unable to initalize XPCOM."); + return -1; + } + + nsresult rv; + nsCOMPtr<nsINamedPipeService> svc = + do_GetService(NS_NAMEDPIPESERVICE_CONTRACTID, &rv); + if (NS_FAILED(rv)) { + fail("Unable to create named pipe service"); + return -1; + } + + HANDLE readPipe, writePipe; + if (NS_FAILED(rv = CreateNamedPipe(&readPipe, &writePipe))) { + fail("Unable to create pipes %d", GetLastError()); + return -1; + } + + RefPtr<nsNamedPipeDataObserver> readObserver = + new nsNamedPipeDataObserver(readPipe); + RefPtr<nsNamedPipeDataObserver> writeObserver = + new nsNamedPipeDataObserver(writePipe); + + if (NS_WARN_IF(NS_FAILED(svc->AddDataObserver(readPipe, readObserver)))) { + fail("Unable to add read data observer"); + return -1; + } + + if (NS_WARN_IF(NS_FAILED(svc->AddDataObserver(writePipe, writeObserver)))) { + fail("Unable to add read data observer"); + return -1; + } + + if (writeObserver->Write(TEST_STR, sizeof(TEST_STR)) != sizeof(TEST_STR)) { + fail("write error"); + return -1; + } + + char buffer[sizeof(TEST_STR)]; + if (readObserver->Read(buffer, sizeof(buffer)) != sizeof(TEST_STR)) { + fail("read error"); + return -1; + } + + if (strcmp(buffer, TEST_STR) != 0) { + fail("I/O mismatch"); + return -1; + } + + if (NS_WARN_IF(NS_FAILED(svc->RemoveDataObserver(readPipe, readObserver)))) { + fail("Unable to remove read data observer"); + return -1; + } + + if (NS_WARN_IF(NS_FAILED(svc->RemoveDataObserver(writePipe, writeObserver)))) { + fail("Unable to remove read data observer"); + return -1; + } + + passed("Finish"); + return 0; +}
\ No newline at end of file diff --git a/netwerk/test/TestOpen.cpp b/netwerk/test/TestOpen.cpp new file mode 100644 index 0000000000..43d518b4e3 --- /dev/null +++ b/netwerk/test/TestOpen.cpp @@ -0,0 +1,90 @@ +/* -*- 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/. */ + +#include "TestCommon.h" +#include "nsCOMPtr.h" +#include "nsStringAPI.h" +#include "nsIURI.h" +#include "nsIChannel.h" +#include "nsIHttpChannel.h" +#include "nsIInputStream.h" +#include "nsNetUtil.h" +#include "nsServiceManagerUtils.h" +#include "mozilla/Unused.h" +#include "nsIScriptSecurityManager.h" + +#include <stdio.h> + +using namespace mozilla; + +/* + * Test synchronous Open. + */ + +#define RETURN_IF_FAILED(rv, what) \ + PR_BEGIN_MACRO \ + if (NS_FAILED(rv)) { \ + printf(what ": failed - %08x\n", static_cast<uint32_t>(rv)); \ + return -1; \ + } \ + PR_END_MACRO + +int +main(int argc, char **argv) +{ + if (test_common_init(&argc, &argv) != 0) + return -1; + + nsresult rv = NS_InitXPCOM2(nullptr, nullptr, nullptr); + if (NS_FAILED(rv)) return -1; + + char buf[256]; + + if (argc != 3) { + printf("Usage: TestOpen url filename\nLoads a URL using ::Open, writing it to a file\n"); + return -1; + } + + nsCOMPtr<nsIURI> uri; + nsCOMPtr<nsIInputStream> stream; + + rv = NS_NewURI(getter_AddRefs(uri), argv[1]); + RETURN_IF_FAILED(rv, "NS_NewURI"); + + nsCOMPtr<nsIScriptSecurityManager> secman = + do_GetService(NS_SCRIPTSECURITYMANAGER_CONTRACTID, &rv); + RETURN_IF_FAILED(rv, "Couldn't get script security manager!"); + nsCOMPtr<nsIPrincipal> systemPrincipal; + rv = secman->GetSystemPrincipal(getter_AddRefs(systemPrincipal)); + RETURN_IF_FAILED(rv, "Couldn't get system principal!"); + + nsCOMPtr<nsIChannel> channel; + rv = NS_NewChannel(getter_AddRefs(channel), + uri, + systemPrincipal, + nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL, + nsIContentPolicy::TYPE_OTHER); + RETURN_IF_FAILED(rv, "NS_NewChannel"); + + rv = channel->Open2(getter_AddRefs(stream)); + RETURN_IF_FAILED(rv, "channel->Open2()"); + + FILE* outfile = fopen(argv[2], "wb"); + if (!outfile) { + printf("error opening %s\n", argv[2]); + return 1; + } + + uint32_t read; + while (NS_SUCCEEDED(stream->Read(buf, sizeof(buf), &read)) && read) { + Unused << fwrite(buf, 1, read, outfile); + } + printf("Done\n"); + + fclose(outfile); + + NS_ShutdownXPCOM(nullptr); + return 0; +} diff --git a/netwerk/test/TestOverlappedIO.cpp b/netwerk/test/TestOverlappedIO.cpp new file mode 100644 index 0000000000..db954b4f62 --- /dev/null +++ b/netwerk/test/TestOverlappedIO.cpp @@ -0,0 +1,313 @@ +/* -*- 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/. */ +#include <stdio.h> +#include <signal.h> +#include <algorithm> + +#ifdef WIN32 +#include <windows.h> +#endif + +#include "nspr.h" +#include "nscore.h" +#include "nsISocketTransportService.h" +#include "nsIEventQueueService.h" +#include "nsIServiceManager.h" +#include "nsITransport.h" +#include "nsIRequest.h" +#include "nsIStreamProvider.h" +#include "nsIStreamListener.h" +#include "nsIPipe.h" +#include "nsIOutputStream.h" +#include "nsIInputStream.h" +#include "nsCRT.h" +#include "nsCOMPtr.h" +#include "nsIByteArrayInputStream.h" + +static PRLogModuleInfo *gTestSocketIOLog; +#define LOG(args) MOZ_LOG(gTestSocketIOLog, mozilla::LogLevel::Debug, args) + +static NS_DEFINE_CID(kSocketTransportServiceCID, NS_SOCKETTRANSPORTSERVICE_CID); +static NS_DEFINE_CID(kEventQueueServiceCID, NS_EVENTQUEUESERVICE_CID); + +static PRTime gElapsedTime; +static int gKeepRunning = 1; +static nsIEventQueue* gEventQ = nullptr; + +// +//---------------------------------------------------------------------------- +// Test Listener +//---------------------------------------------------------------------------- +// + +class TestListener : public nsIStreamListener +{ +public: + TestListener() { } + virtual ~TestListener() {} + + NS_DECL_ISUPPORTS + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSISTREAMLISTENER +}; + +NS_IMPL_ISUPPORTS(TestListener, + nsIRequestObserver, + nsIStreamListener); + +NS_IMETHODIMP +TestListener::OnStartRequest(nsIRequest* request, nsISupports* context) +{ + LOG(("TestListener::OnStartRequest\n")); + return NS_OK; +} + +NS_IMETHODIMP +TestListener::OnDataAvailable(nsIRequest* request, + nsISupports* context, + nsIInputStream *aIStream, + uint64_t aSourceOffset, + uint32_t aLength) +{ + LOG(("TestListener::OnDataAvailable [offset=%llu length=%u]\n", + aSourceOffset, aLength)); + char buf[1025]; + uint32_t amt; + while (1) { + aIStream->Read(buf, 1024, &amt); + if (amt == 0) + break; + buf[amt] = '\0'; + //puts(buf); + printf("read %d bytes\n", amt); + } + return NS_OK; +} + +NS_IMETHODIMP +TestListener::OnStopRequest(nsIRequest* request, nsISupports* context, + nsresult aStatus) +{ + LOG(("TestListener::OnStopRequest [aStatus=%x]\n", aStatus)); + //gKeepRunning = 0; + return NS_OK; +} + +// +//---------------------------------------------------------------------------- +// Test Provider +//---------------------------------------------------------------------------- +// + +class TestProvider : public nsIStreamProvider +{ +public: + TestProvider(char *data); + virtual ~TestProvider(); + + NS_DECL_ISUPPORTS + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSISTREAMPROVIDER + +protected: + char *mData; + uint32_t mOffset; + uint32_t mDataLen; + uint32_t mRequestCount; +}; + +NS_IMPL_ISUPPORTS(TestProvider, + nsIStreamProvider, + nsIRequestObserver) + +TestProvider::TestProvider(char *data) +{ + mData = data; + mDataLen = strlen(data); + mOffset = 0; + mRequestCount = 0; + LOG(("Constructing TestProvider [this=%p]\n", this)); +} + +TestProvider::~TestProvider() +{ + LOG(("Destroying TestProvider [this=%p]\n", this)); +} + +NS_IMETHODIMP +TestProvider::OnStartRequest(nsIRequest* request, nsISupports* context) +{ + LOG(("TestProvider::OnStartRequest [this=%p]\n", this)); + return NS_OK; +} + +NS_IMETHODIMP +TestProvider::OnStopRequest(nsIRequest* request, nsISupports* context, + nsresult aStatus) +{ + LOG(("TestProvider::OnStopRequest [status=%x]\n", aStatus)); + return NS_OK; +} + +NS_IMETHODIMP +TestProvider::OnDataWritable(nsIRequest *request, nsISupports *context, + nsIOutputStream *output, uint32_t offset, uint32_t count) +{ + LOG(("TestProvider::OnDataWritable [offset=%u, count=%u]\n", offset, count)); + + // Stop at 5 requests + if (mRequestCount == 5) + return NS_BASE_STREAM_CLOSED; + + uint32_t writeCount, amount; + amount = std::min(count, mDataLen - mOffset); + nsresult rv = output->Write(mData + mOffset, amount, &writeCount); + if (NS_SUCCEEDED(rv)) { + printf("wrote %u bytes\n", writeCount); + mOffset += writeCount; + if (mOffset == mDataLen) { + printf("done sending packet %u\n", mRequestCount); + mOffset = 0; + mRequestCount++; + } + } + return NS_OK; +} + +// +//---------------------------------------------------------------------------- +// Synchronous IO +//---------------------------------------------------------------------------- +// +nsresult +WriteRequest(nsIOutputStream *os, const char *request) +{ + LOG(("WriteRequest [request=%s]\n", request)); + uint32_t n; + return os->Write(request, strlen(request), &n); +} + +nsresult +ReadResponse(nsIInputStream *is) +{ + uint32_t bytesRead; + char buf[2048]; + do { + is->Read(buf, sizeof(buf), &bytesRead); + if (bytesRead > 0) + fwrite(buf, 1, bytesRead, stdout); + } while (bytesRead > 0); + return NS_OK; +} + +// +//---------------------------------------------------------------------------- +// Startup... +//---------------------------------------------------------------------------- +// + +void +sighandler(int sig) +{ + LOG(("got signal: %d\n", sig)); + NS_BREAK(); +} + +void +usage(char **argv) +{ + printf("usage: %s <host> <path>\n", argv[0]); + exit(1); +} + +int +main(int argc, char* argv[]) +{ + nsresult rv; + + signal(SIGSEGV, sighandler); + + gTestSocketIOLog = PR_NewLogModule("TestSocketIO"); + + if (argc < 3) + usage(argv); + + char *hostName = argv[1]; + char *fileName = argv[2]; + int port = 80; + + // Create the Event Queue for this thread... + nsCOMPtr<nsIEventQueueService> eventQService = + do_GetService(kEventQueueServiceCID, &rv); + if (NS_FAILED(rv)) { + NS_WARNING("failed to create: event queue service!"); + return rv; + } + + rv = eventQService->CreateMonitoredThreadEventQueue(); + if (NS_FAILED(rv)) { + NS_WARNING("failed to create: thread event queue!"); + return rv; + } + + eventQService->GetThreadEventQueue(NS_CURRENT_THREAD, &gEventQ); + + // Create the Socket transport service... + nsCOMPtr<nsISocketTransportService> sts = + do_GetService(kSocketTransportServiceCID, &rv); + if (NS_FAILED(rv)) { + NS_WARNING("failed to create: socket transport service!"); + return rv; + } + + char *buffer = PR_smprintf("GET %s HTTP/1.1" CRLF + "host: %s" CRLF + "user-agent: Mozilla/5.0 (X11; N; Linux 2.2.16-22smp i686; en-US; m18) Gecko/20001220" CRLF + "accept: */*" CRLF + "accept-language: en" CRLF + "accept-encoding: gzip,deflate,compress,identity" CRLF + "keep-alive: 300" CRLF + "connection: keep-alive" CRLF + CRLF, + fileName, hostName); + LOG(("Request [\n%s]\n", buffer)); + + // Create the socket transport... + nsCOMPtr<nsITransport> transport; + rv = sts->CreateTransport(hostName, port, nullptr, -1, 0, 0, getter_AddRefs(transport)); + if (NS_FAILED(rv)) { + NS_WARNING("failed to create: socket transport!"); + return rv; + } + + gElapsedTime = PR_Now(); + + nsCOMPtr<nsIRequest> writeRequest, readRequest; + + rv = transport->AsyncWrite(new TestProvider(buffer), nullptr, 0, 0, 0, getter_AddRefs(writeRequest)); + if (NS_FAILED(rv)) { + NS_WARNING("failed calling: AsyncWrite!"); + return rv; + } + rv = transport->AsyncRead(new TestListener(), nullptr, 0, 0, 0, getter_AddRefs(readRequest)); + if (NS_FAILED(rv)) { + NS_WARNING("failed calling: AsyncWrite!"); + return rv; + } + + // Enter the message pump + while ( gKeepRunning ) { + PLEvent *gEvent; + gEventQ->WaitForEvent(&gEvent); + gEventQ->HandleEvent(gEvent); + } + + PRTime endTime; + endTime = PR_Now(); + LOG(("Elapsed time: %d\n", (int32_t)(endTime/1000UL - gElapsedTime/1000UL))); + + sts->Shutdown(); + return 0; +} diff --git a/netwerk/test/TestProtocols.cpp b/netwerk/test/TestProtocols.cpp new file mode 100644 index 0000000000..7bec14f856 --- /dev/null +++ b/netwerk/test/TestProtocols.cpp @@ -0,0 +1,890 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=2 sw=2 et cindent: */ +/* 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/. */ + +/* + The TestProtocols tests the basic protocols architecture and can + be used to test individual protocols as well. If this grows too + big then we should split it to individual protocols. + + -Gagan Saksena 04/29/99 +*/ + +#include "TestCommon.h" +#include <algorithm> + +#include <stdio.h> +#ifdef WIN32 +#include <windows.h> +#endif +#ifdef XP_UNIX +#include <unistd.h> +#endif +#include "nspr.h" +#include "nscore.h" +#include "nsCOMPtr.h" +#include "nsIIOService.h" +#include "nsIServiceManager.h" +#include "nsIStreamListener.h" +#include "nsIInputStream.h" +#include "nsIInputStream.h" +#include "nsCRT.h" +#include "nsIChannel.h" +#include "nsIResumableChannel.h" +#include "nsIURL.h" +#include "nsIHttpChannel.h" +#include "nsIHttpChannelInternal.h" +#include "nsIHttpHeaderVisitor.h" +#include "nsIChannelEventSink.h" +#include "nsIAsyncVerifyRedirectCallback.h" +#include "nsIInterfaceRequestor.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsIDNSService.h" +#include "nsIAuthPrompt.h" +#include "nsIPrefService.h" +#include "nsIPrefBranch.h" +#include "nsIPropertyBag2.h" +#include "nsIWritablePropertyBag2.h" +#include "nsITimedChannel.h" +#include "mozilla/Attributes.h" +#include "mozilla/Unused.h" +#include "nsIScriptSecurityManager.h" + +#include "nsISimpleEnumerator.h" +#include "nsStringAPI.h" +#include "nsNetUtil.h" +#include "nsServiceManagerUtils.h" +#include "NetwerkTestLogging.h" + +using namespace mozilla; + +namespace TestProtocols { + +// +// set NSPR_LOG_MODULES=Test:5 +// +static PRLogModuleInfo *gTestLog = nullptr; +#define LOG(args) MOZ_LOG(gTestLog, mozilla::LogLevel::Debug, args) + +static NS_DEFINE_CID(kIOServiceCID, NS_IOSERVICE_CID); + +//static PRTime gElapsedTime; // enable when we time it... +static int gKeepRunning = 0; +static bool gVerbose = false; +static bool gAskUserForInput = false; +static bool gResume = false; +static uint64_t gStartAt = 0; + +static const char* gEntityID; + +//----------------------------------------------------------------------------- +// Set proxy preferences for testing +//----------------------------------------------------------------------------- + +static nsresult +SetHttpProxy(const char *proxy) +{ + const char *colon = strchr(proxy, ':'); + if (!colon) + { + NS_WARNING("invalid proxy token; use host:port"); + return NS_ERROR_UNEXPECTED; + } + int port = atoi(colon + 1); + if (port == 0) + { + NS_WARNING("invalid proxy port; must be an integer"); + return NS_ERROR_UNEXPECTED; + } + nsAutoCString proxyHost; + proxyHost = Substring(proxy, colon); + + nsCOMPtr<nsIPrefBranch> prefs = do_GetService(NS_PREFSERVICE_CONTRACTID); + if (prefs) + { + prefs->SetCharPref("network.proxy.http", proxyHost.get()); + prefs->SetIntPref("network.proxy.http_port", port); + prefs->SetIntPref("network.proxy.type", 1); // manual proxy config + } + LOG(("connecting via proxy=%s:%d\n", proxyHost.get(), port)); + return NS_OK; +} + +static nsresult +SetPACFile(const char* pacURL) +{ + nsCOMPtr<nsIPrefBranch> prefs = do_GetService(NS_PREFSERVICE_CONTRACTID); + if (prefs) + { + prefs->SetCharPref("network.proxy.autoconfig_url", pacURL); + prefs->SetIntPref("network.proxy.type", 2); // PAC file + } + LOG(("connecting using PAC file %s\n", pacURL)); + return NS_OK; +} + +//----------------------------------------------------------------------------- +// Timing information +//----------------------------------------------------------------------------- + +void PrintTimingInformation(nsITimedChannel* channel) { +#define PRINT_VALUE(property) \ + { \ + PRTime value; \ + channel->Get##property(&value); \ + if (value) { \ + PRExplodedTime exploded; \ + PR_ExplodeTime(value, PR_LocalTimeParameters, &exploded); \ + char buf[256]; \ + PR_FormatTime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &exploded); \ + LOG((" " #property ":\t%s (%i usec)", buf, exploded.tm_usec)); \ + } else { \ + LOG((" " #property ":\t0")); \ + } \ + } + LOG(("Timing data:")); + PRINT_VALUE(ChannelCreationTime) + PRINT_VALUE(AsyncOpenTime) + PRINT_VALUE(DomainLookupStartTime) + PRINT_VALUE(DomainLookupEndTime) + PRINT_VALUE(ConnectStartTime) + PRINT_VALUE(ConnectEndTime) + PRINT_VALUE(RequestStartTime) + PRINT_VALUE(ResponseStartTime) + PRINT_VALUE(ResponseEndTime) + PRINT_VALUE(CacheReadStartTime) + PRINT_VALUE(CacheReadEndTime) +} + +//----------------------------------------------------------------------------- +// HeaderVisitor +//----------------------------------------------------------------------------- + +class HeaderVisitor : public nsIHttpHeaderVisitor +{ + virtual ~HeaderVisitor() = default; +public: + NS_DECL_ISUPPORTS + NS_DECL_NSIHTTPHEADERVISITOR + + HeaderVisitor() { } +}; +NS_IMPL_ISUPPORTS(HeaderVisitor, nsIHttpHeaderVisitor) + +NS_IMETHODIMP +HeaderVisitor::VisitHeader(const nsACString &header, const nsACString &value) +{ + LOG((" %s: %s\n", + PromiseFlatCString(header).get(), + PromiseFlatCString(value).get())); + return NS_OK; +} + +//----------------------------------------------------------------------------- +// URLLoadInfo +//----------------------------------------------------------------------------- + +class URLLoadInfo : public nsISupports +{ + virtual ~URLLoadInfo(); + +public: + + explicit URLLoadInfo(const char* aUrl); + + // ISupports interface... + NS_DECL_THREADSAFE_ISUPPORTS + + const char* Name() { return mURLString.get(); } + int64_t mBytesRead; + PRTime mTotalTime; + PRTime mConnectTime; + nsCString mURLString; +}; + +URLLoadInfo::URLLoadInfo(const char *aUrl) : mURLString(aUrl) +{ + mBytesRead = 0; + mConnectTime = mTotalTime = PR_Now(); +} + +URLLoadInfo::~URLLoadInfo() = default; + + +NS_IMPL_ISUPPORTS0(URLLoadInfo) + +//----------------------------------------------------------------------------- +// TestChannelEventSink +//----------------------------------------------------------------------------- + +class TestChannelEventSink : public nsIChannelEventSink +{ + virtual ~TestChannelEventSink(); + +public: + NS_DECL_ISUPPORTS + NS_DECL_NSICHANNELEVENTSINK + + TestChannelEventSink(); +}; + +TestChannelEventSink::TestChannelEventSink() +{ +} + +TestChannelEventSink::~TestChannelEventSink() = default; + + +NS_IMPL_ISUPPORTS(TestChannelEventSink, nsIChannelEventSink) + +NS_IMETHODIMP +TestChannelEventSink::AsyncOnChannelRedirect(nsIChannel *channel, + nsIChannel *newChannel, + uint32_t flags, + nsIAsyncVerifyRedirectCallback *callback) +{ + LOG(("\n+++ TestChannelEventSink::OnChannelRedirect (with flags %x) +++\n", + flags)); + callback->OnRedirectVerifyCallback(NS_OK); + return NS_OK; +} + +//----------------------------------------------------------------------------- +// TestAuthPrompt +//----------------------------------------------------------------------------- + +class TestAuthPrompt : public nsIAuthPrompt +{ + virtual ~TestAuthPrompt(); + +public: + NS_DECL_ISUPPORTS + NS_DECL_NSIAUTHPROMPT + + TestAuthPrompt(); +}; + +NS_IMPL_ISUPPORTS(TestAuthPrompt, nsIAuthPrompt) + +TestAuthPrompt::TestAuthPrompt() +{ +} + +TestAuthPrompt::~TestAuthPrompt() = default; + +NS_IMETHODIMP +TestAuthPrompt::Prompt(const char16_t *dialogTitle, + const char16_t *text, + const char16_t *passwordRealm, + uint32_t savePassword, + const char16_t *defaultText, + char16_t **result, + bool *_retval) +{ + *_retval = false; + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +TestAuthPrompt::PromptUsernameAndPassword(const char16_t *dialogTitle, + const char16_t *dialogText, + const char16_t *passwordRealm, + uint32_t savePassword, + char16_t **user, + char16_t **pwd, + bool *_retval) +{ + NS_ConvertUTF16toUTF8 text(passwordRealm); + printf("* --------------------------------------------------------------------------- *\n"); + printf("* Authentication Required [%s]\n", text.get()); + printf("* --------------------------------------------------------------------------- *\n"); + + char buf[256]; + int n; + + printf("Enter username: "); + Unused << fgets(buf, sizeof(buf), stdin); + n = strlen(buf); + buf[n-1] = '\0'; // trim trailing newline + *user = NS_StringCloneData(NS_ConvertUTF8toUTF16(buf)); + + const char *p; +#if defined(XP_UNIX) && !defined(ANDROID) + p = getpass("Enter password: "); +#else + printf("Enter password: "); + fgets(buf, sizeof(buf), stdin); + n = strlen(buf); + buf[n-1] = '\0'; // trim trailing newline + p = buf; +#endif + *pwd = NS_StringCloneData(NS_ConvertUTF8toUTF16(p)); + + // zap buf + memset(buf, 0, sizeof(buf)); + + *_retval = true; + return NS_OK; +} + +NS_IMETHODIMP +TestAuthPrompt::PromptPassword(const char16_t *dialogTitle, + const char16_t *text, + const char16_t *passwordRealm, + uint32_t savePassword, + char16_t **pwd, + bool *_retval) +{ + *_retval = false; + return NS_ERROR_NOT_IMPLEMENTED; +} + +//----------------------------------------------------------------------------- +// InputTestConsumer +//----------------------------------------------------------------------------- + +class InputTestConsumer : public nsIStreamListener +{ + virtual ~InputTestConsumer(); + +public: + + explicit InputTestConsumer(URLLoadInfo* aURLLoadInfo); + + NS_DECL_ISUPPORTS + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSISTREAMLISTENER +private: + URLLoadInfo* mURLLoadInfo; +}; + +InputTestConsumer::InputTestConsumer(URLLoadInfo* aURLLoadInfo) +: mURLLoadInfo(aURLLoadInfo) +{ + NS_IF_ADDREF(mURLLoadInfo); +} + +InputTestConsumer::~InputTestConsumer() +{ + NS_RELEASE(mURLLoadInfo); +} + +NS_IMPL_ISUPPORTS(InputTestConsumer, nsIStreamListener, nsIRequestObserver) + +NS_IMETHODIMP +InputTestConsumer::OnStartRequest(nsIRequest *request, nsISupports* context) +{ + LOG(("InputTestConsumer::OnStartRequest\n")); + + NS_ASSERTION(!context, "context needs to be null when calling asyncOpen2"); + + if (mURLLoadInfo) + mURLLoadInfo->mConnectTime = PR_Now() - mURLLoadInfo->mConnectTime; + + if (gVerbose) { + LOG(("\nStarted loading: %s\n", mURLLoadInfo ? mURLLoadInfo->Name() : "UNKNOWN URL")); + } + + nsAutoCString value; + + nsCOMPtr<nsIChannel> channel = do_QueryInterface(request); + if (channel) { + nsresult status; + channel->GetStatus(&status); + LOG(("Channel Status: %08x\n", status)); + if (NS_SUCCEEDED(status)) { + LOG(("Channel Info:\n")); + + channel->GetName(value); + LOG(("\tName: %s\n", value.get())); + + channel->GetContentType(value); + LOG(("\tContent-Type: %s\n", value.get())); + + channel->GetContentCharset(value); + LOG(("\tContent-Charset: %s\n", value.get())); + + int64_t length = -1; + if (NS_SUCCEEDED(channel->GetContentLength(&length))) { + LOG(("\tContent-Length: %lld\n", length)); + } else { + LOG(("\tContent-Length: Unknown\n")); + } + } + + nsCOMPtr<nsISupports> owner; + channel->GetOwner(getter_AddRefs(owner)); + LOG(("\tChannel Owner: %x\n", owner.get())); + } + + nsCOMPtr<nsIPropertyBag2> props = do_QueryInterface(request); + if (props) { + nsCOMPtr<nsIURI> foo; + props->GetPropertyAsInterface(NS_LITERAL_STRING("test.foo"), + NS_GET_IID(nsIURI), + getter_AddRefs(foo)); + if (foo) { + LOG(("\ttest.foo: %s\n", foo->GetSpecOrDefault().get())); + } + } + + nsCOMPtr<nsIHttpChannelInternal> httpChannelInt(do_QueryInterface(request)); + if (httpChannelInt) { + uint32_t majorVer, minorVer; + nsresult rv = httpChannelInt->GetResponseVersion(&majorVer, &minorVer); + if (NS_SUCCEEDED(rv)) { + LOG(("HTTP Response version: %u.%u\n", majorVer, minorVer)); + } + } + nsCOMPtr<nsIHttpChannel> httpChannel(do_QueryInterface(request)); + if (httpChannel) { + auto *visitor = new HeaderVisitor(); + if (!visitor) + return NS_ERROR_OUT_OF_MEMORY; + NS_ADDREF(visitor); + + LOG(("HTTP request headers:\n")); + httpChannel->VisitRequestHeaders(visitor); + + LOG(("HTTP response headers:\n")); + httpChannel->VisitResponseHeaders(visitor); + + NS_RELEASE(visitor); + } + + nsCOMPtr<nsIResumableChannel> resChannel = do_QueryInterface(request); + if (resChannel) { + LOG(("Resumable entity identification:\n")); + nsAutoCString entityID; + nsresult rv = resChannel->GetEntityID(entityID); + if (NS_SUCCEEDED(rv)) { + LOG(("\t|%s|\n", entityID.get())); + } + else { + LOG(("\t<none>\n")); + } + } + + return NS_OK; +} + +NS_IMETHODIMP +InputTestConsumer::OnDataAvailable(nsIRequest *request, + nsISupports* context, + nsIInputStream *aIStream, + uint64_t aSourceOffset, + uint32_t aLength) +{ + NS_ASSERTION(!context, "context needs to be null when calling asyncOpen2"); + + char buf[1025]; + uint32_t amt, size; + nsresult rv; + + while (aLength) { + size = std::min<uint32_t>(aLength, sizeof(buf)); + + rv = aIStream->Read(buf, size, &amt); + if (NS_FAILED(rv)) { + NS_ASSERTION((NS_BASE_STREAM_WOULD_BLOCK != rv), + "The stream should never block."); + return rv; + } + if (gVerbose) { + buf[amt] = '\0'; + puts(buf); + } + if (mURLLoadInfo) { + mURLLoadInfo->mBytesRead += amt; + } + + aLength -= amt; + } + return NS_OK; +} + +NS_IMETHODIMP +InputTestConsumer::OnStopRequest(nsIRequest *request, nsISupports* context, + nsresult aStatus) +{ + LOG(("InputTestConsumer::OnStopRequest [status=%x]\n", aStatus)); + + if (mURLLoadInfo) { + uint32_t httpStatus; + bool bHTTPURL = false; + + mURLLoadInfo->mTotalTime = PR_Now() - mURLLoadInfo->mTotalTime; + + double readTime = ((mURLLoadInfo->mTotalTime-mURLLoadInfo->mConnectTime)/1000.0)/1000.0; + + nsCOMPtr<nsIHttpChannel> pHTTPCon(do_QueryInterface(request)); + if (pHTTPCon) { + pHTTPCon->GetResponseStatus(&httpStatus); + bHTTPURL = true; + } + + LOG(("\nFinished loading: %s Status Code: %x\n", mURLLoadInfo->Name(), aStatus)); + if (bHTTPURL) { + LOG(("\tHTTP Status: %u\n", httpStatus)); + } + if (NS_ERROR_UNKNOWN_HOST == aStatus || + NS_ERROR_UNKNOWN_PROXY_HOST == aStatus) { + LOG(("\tDNS lookup failed.\n")); + } + LOG(("\tTime to connect: %.3f seconds\n", (mURLLoadInfo->mConnectTime/1000.0)/1000.0)); + LOG(("\tTime to read: %.3f seconds.\n", readTime)); + LOG(("\tRead: %lld bytes.\n", mURLLoadInfo->mBytesRead)); + if (mURLLoadInfo->mBytesRead == int64_t(0)) { + } else if (readTime > 0.0) { + LOG(("\tThroughput: %.0f bps.\n", (double)(mURLLoadInfo->mBytesRead*int64_t(8))/readTime)); + } else { + LOG(("\tThroughput: REAL FAST!!\n")); + } + + nsCOMPtr<nsITimedChannel> timed(do_QueryInterface(request)); + if (timed) + PrintTimingInformation(timed); + } else { + LOG(("\nFinished loading: UNKNOWN URL. Status Code: %x\n", aStatus)); + } + + if (--gKeepRunning == 0) + QuitPumpingEvents(); + return NS_OK; +} + +//----------------------------------------------------------------------------- +// NotificationCallbacks +//----------------------------------------------------------------------------- + +class NotificationCallbacks final : public nsIInterfaceRequestor { + + ~NotificationCallbacks() = default; + +public: + NS_DECL_ISUPPORTS + + NotificationCallbacks() { + } + + NS_IMETHOD GetInterface(const nsIID& iid, void* *result) override { + nsresult rv = NS_ERROR_FAILURE; + + if (iid.Equals(NS_GET_IID(nsIChannelEventSink))) { + TestChannelEventSink *sink; + + sink = new TestChannelEventSink(); + if (sink == nullptr) + return NS_ERROR_OUT_OF_MEMORY; + NS_ADDREF(sink); + rv = sink->QueryInterface(iid, result); + NS_RELEASE(sink); + } + + if (iid.Equals(NS_GET_IID(nsIAuthPrompt))) { + TestAuthPrompt *prompt; + + prompt = new TestAuthPrompt(); + if (prompt == nullptr) + return NS_ERROR_OUT_OF_MEMORY; + NS_ADDREF(prompt); + rv = prompt->QueryInterface(iid, result); + NS_RELEASE(prompt); + } + return rv; + } +}; + +NS_IMPL_ISUPPORTS(NotificationCallbacks, nsIInterfaceRequestor) + +//----------------------------------------------------------------------------- +// helpers... +//----------------------------------------------------------------------------- + +nsresult StartLoadingURL(const char* aUrlString) +{ + nsresult rv; + + nsCOMPtr<nsIIOService> pService(do_GetService(kIOServiceCID, &rv)); + if (pService) { + nsCOMPtr<nsIURI> pURL; + + rv = pService->NewURI(nsDependentCString(aUrlString), nullptr, nullptr, getter_AddRefs(pURL)); + if (NS_FAILED(rv)) { + LOG(("ERROR: NewURI failed for %s [rv=%x]\n", aUrlString)); + return rv; + } + nsCOMPtr<nsIChannel> pChannel; + + auto* callbacks = new NotificationCallbacks(); + if (!callbacks) { + LOG(("Failed to create a new consumer!")); + return NS_ERROR_OUT_OF_MEMORY;; + } + NS_ADDREF(callbacks); + + nsCOMPtr<nsIScriptSecurityManager> secman = + do_GetService(NS_SCRIPTSECURITYMANAGER_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIPrincipal> systemPrincipal; + rv = secman->GetSystemPrincipal(getter_AddRefs(systemPrincipal)); + NS_ENSURE_SUCCESS(rv, rv); + + // Async reading thru the calls of the event sink interface + rv = NS_NewChannel(getter_AddRefs(pChannel), + pURL, + systemPrincipal, + nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL, + nsIContentPolicy::TYPE_OTHER, + nullptr, // loadGroup + callbacks, + nsIRequest::LOAD_NORMAL, + pService); + + NS_RELEASE(callbacks); + if (NS_FAILED(rv)) { + LOG(("ERROR: NS_NewChannel failed for %s [rv=%x]\n", aUrlString, rv)); + return rv; + } + + nsCOMPtr<nsITimedChannel> timed(do_QueryInterface(pChannel)); + if (timed) + timed->SetTimingEnabled(true); + + nsCOMPtr<nsIWritablePropertyBag2> props = do_QueryInterface(pChannel); + if (props) { + rv = props->SetPropertyAsInterface(NS_LITERAL_STRING("test.foo"), + pURL); + if (NS_SUCCEEDED(rv)) { + LOG(("set prop 'test.foo'\n")); + } + } + + /* + You may optionally add/set other headers on this + request object. This is done by QI for the specific + protocolConnection. + */ + nsCOMPtr<nsIHttpChannel> pHTTPCon(do_QueryInterface(pChannel)); + + if (pHTTPCon) { + // Setting a sample header. + rv = pHTTPCon->SetRequestHeader(NS_LITERAL_CSTRING("sample-header"), + NS_LITERAL_CSTRING("Sample-Value"), + false); + if (NS_FAILED(rv)) return rv; + } + auto* info = new URLLoadInfo(aUrlString); + if (!info) { + NS_ERROR("Failed to create a load info!"); + return NS_ERROR_OUT_OF_MEMORY; + } + + auto* listener = new InputTestConsumer(info); + NS_IF_ADDREF(listener); + if (!listener) { + NS_ERROR("Failed to create a new stream listener!"); + return NS_ERROR_OUT_OF_MEMORY;; + } + + + if (gResume) { + nsCOMPtr<nsIResumableChannel> res = do_QueryInterface(pChannel); + if (!res) { + NS_ERROR("Channel is not resumable!"); + return NS_ERROR_UNEXPECTED; + } + nsAutoCString id; + if (gEntityID) + id = gEntityID; + LOG(("* resuming at %llu bytes, with entity id |%s|\n", gStartAt, id.get())); + res->ResumeAt(gStartAt, id); + } + rv = pChannel->AsyncOpen2(listener); + + if (NS_SUCCEEDED(rv)) { + gKeepRunning++; + } + else { + LOG(("ERROR: AsyncOpen failed [rv=%x]\n", rv)); + } + NS_RELEASE(listener); + } + + return rv; +} + +static int32_t +FindChar(nsCString& buffer, char c) +{ + const char *b; + int32_t len = NS_CStringGetData(buffer, &b); + + for (int32_t offset = 0; offset < len; ++offset) { + if (b[offset] == c) + return offset; + } + + return -1; +} + + +static void +StripChar(nsCString& buffer, char c) +{ + const char *b; + uint32_t len = NS_CStringGetData(buffer, &b) - 1; + + for (; len > 0; --len) { + if (b[len] == c) { + buffer.Cut(len, 1); + NS_CStringGetData(buffer, &b); + } + } +} + +nsresult LoadURLsFromFile(char *aFileName) +{ + nsresult rv = NS_OK; + int32_t len, offset; + PRFileDesc* fd; + char buffer[1024]; + nsCString fileBuffer; + nsCString urlString; + + fd = PR_Open(aFileName, PR_RDONLY, 777); + if (!fd) { + return NS_ERROR_FAILURE; + } + + // Keep reading the file until EOF (or an error) is reached... + do { + len = PR_Read(fd, buffer, sizeof(buffer)); + if (len>0) { + fileBuffer.Append(buffer, len); + // Treat each line as a URL... + while ((offset = FindChar(fileBuffer, '\n')) != -1) { + urlString = StringHead(fileBuffer, offset); + fileBuffer.Cut(0, offset+1); + + StripChar(urlString, '\r'); + if (urlString.Length()) { + LOG(("\t%s\n", urlString.get())); + rv = StartLoadingURL(urlString.get()); + if (NS_FAILED(rv)) { + // No need to log an error -- StartLoadingURL already + // did that for us, probably. + PR_Close(fd); + return rv; + } + } + } + } + } while (len>0); + + // If anything is left in the fileBuffer, treat it as a URL... + StripChar(fileBuffer, '\r'); + if (fileBuffer.Length()) { + LOG(("\t%s\n", fileBuffer.get())); + StartLoadingURL(fileBuffer.get()); + } + + PR_Close(fd); + return NS_OK; +} + + +nsresult LoadURLFromConsole() +{ + char buffer[1024]; + printf(R"(Enter URL ("q" to start): )"); + Unused << scanf("%s", buffer); + if (buffer[0]=='q') + gAskUserForInput = false; + else + StartLoadingURL(buffer); + return NS_OK; +} + +} // namespace TestProtocols + +using namespace TestProtocols; + +int +main(int argc, char* argv[]) +{ + if (test_common_init(&argc, &argv) != 0) + return -1; + + nsresult rv= (nsresult)-1; + if (argc < 2) { + printf("usage: %s [-verbose] [-file <name>] [-resume <startoffset>" + "[-entityid <entityid>]] [-proxy <proxy>] [-pac <pacURL>]" + "[-console] <url> <url> ... \n", argv[0]); + return -1; + } + + gTestLog = PR_NewLogModule("Test"); + + /* + The following code only deals with XPCOM registration stuff. and setting + up the event queues. Copied from TestSocketIO.cpp + */ + + rv = NS_InitXPCOM2(nullptr, nullptr, nullptr); + if (NS_FAILED(rv)) return -1; + + { + int i; + LOG(("Trying to load:\n")); + for (i=1; i<argc; i++) { + // Turn on verbose printing... + if (PL_strcasecmp(argv[i], "-verbose") == 0) { + gVerbose = true; + continue; + } + + // Turn on netlib tracing... + if (PL_strcasecmp(argv[i], "-file") == 0) { + LoadURLsFromFile(argv[++i]); + continue; + } + + if (PL_strcasecmp(argv[i], "-console") == 0) { + gAskUserForInput = true; + continue; + } + + if (PL_strcasecmp(argv[i], "-resume") == 0) { + gResume = true; + PR_sscanf(argv[++i], "%llu", &gStartAt); + continue; + } + + if (PL_strcasecmp(argv[i], "-entityid") == 0) { + gEntityID = argv[++i]; + continue; + } + + if (PL_strcasecmp(argv[i], "-proxy") == 0) { + SetHttpProxy(argv[++i]); + continue; + } + + if (PL_strcasecmp(argv[i], "-pac") == 0) { + SetPACFile(argv[++i]); + continue; + } + + LOG(("\t%s\n", argv[i])); + rv = StartLoadingURL(argv[i]); + } + // Enter the message pump to allow the URL load to proceed. + PumpEvents(); + } // this scopes the nsCOMPtrs + // no nsCOMPtrs are allowed to be alive when you call NS_ShutdownXPCOM + NS_ShutdownXPCOM(nullptr); + return NS_FAILED(rv) ? -1 : 0; +} diff --git a/netwerk/test/TestServ.cpp b/netwerk/test/TestServ.cpp new file mode 100644 index 0000000000..7ae41636f5 --- /dev/null +++ b/netwerk/test/TestServ.cpp @@ -0,0 +1,146 @@ +/* vim:set ts=4 sw=4 et cindent: */ +/* 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/. */ + +#include "TestCommon.h" +#include <stdlib.h> +#include "nsIServiceManager.h" +#include "nsIServerSocket.h" +#include "nsISocketTransport.h" +#include "nsIInputStream.h" +#include "nsIOutputStream.h" +#include "nsNetCID.h" +#include "nsComponentManagerUtils.h" +#include "nsStringAPI.h" +#include "nsCOMPtr.h" +#include "NetwerkTestLogging.h" + +// +// set NSPR_LOG_MODULES=Test:5 +// +static PRLogModuleInfo *gTestLog = nullptr; +#define LOG(args) MOZ_LOG(gTestLog, mozilla::LogLevel::Debug, args) + +class MySocketListener : public nsIServerSocketListener +{ +protected: + virtual ~MySocketListener() = default; + +public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSISERVERSOCKETLISTENER + + MySocketListener() {} +}; + +NS_IMPL_ISUPPORTS(MySocketListener, nsIServerSocketListener) + +NS_IMETHODIMP +MySocketListener::OnSocketAccepted(nsIServerSocket *serv, + nsISocketTransport *trans) +{ + LOG(("MySocketListener::OnSocketAccepted [serv=%p trans=%p]\n", serv, trans)); + + nsAutoCString host; + int32_t port; + + trans->GetHost(host); + trans->GetPort(&port); + + LOG((" -> %s:%d\n", host.get(), port)); + + nsCOMPtr<nsIInputStream> input; + nsCOMPtr<nsIOutputStream> output; + nsresult rv; + + rv = trans->OpenInputStream(nsITransport::OPEN_BLOCKING, 0, 0, getter_AddRefs(input)); + if (NS_FAILED(rv)) + return rv; + + rv = trans->OpenOutputStream(nsITransport::OPEN_BLOCKING, 0, 0, getter_AddRefs(output)); + if (NS_FAILED(rv)) + return rv; + + char buf[256]; + uint32_t n; + + rv = input->Read(buf, sizeof(buf), &n); + if (NS_FAILED(rv)) + return rv; + + const char response[] = "HTTP/1.0 200 OK\r\nContent-Type: text/plain\r\n\r\nFooooopy!!\r\n"; + rv = output->Write(response, sizeof(response) - 1, &n); + if (NS_FAILED(rv)) + return rv; + + input->Close(); + output->Close(); + return NS_OK; +} + +NS_IMETHODIMP +MySocketListener::OnStopListening(nsIServerSocket *serv, nsresult status) +{ + LOG(("MySocketListener::OnStopListening [serv=%p status=%x]\n", serv, status)); + QuitPumpingEvents(); + return NS_OK; +} + +static nsresult +MakeServer(int32_t port) +{ + nsresult rv; + nsCOMPtr<nsIServerSocket> serv = do_CreateInstance(NS_SERVERSOCKET_CONTRACTID, &rv); + if (NS_FAILED(rv)) + return rv; + + rv = serv->Init(port, true, 5); + if (NS_FAILED(rv)) + return rv; + + rv = serv->GetPort(&port); + if (NS_FAILED(rv)) + return rv; + LOG((" listening on port %d\n", port)); + + rv = serv->AsyncListen(new MySocketListener()); + return rv; +} + +int +main(int argc, char* argv[]) +{ + if (test_common_init(&argc, &argv) != 0) + return -1; + + nsresult rv= (nsresult)-1; + if (argc < 2) { + printf("usage: %s <port>\n", argv[0]); + return -1; + } + + gTestLog = PR_NewLogModule("Test"); + + /* + * The following code only deals with XPCOM registration stuff. and setting + * up the event queues. Copied from TestSocketIO.cpp + */ + + rv = NS_InitXPCOM2(nullptr, nullptr, nullptr); + if (NS_FAILED(rv)) return -1; + + { + rv = MakeServer(atoi(argv[1])); + if (NS_FAILED(rv)) { + LOG(("MakeServer failed [rv=%x]\n", rv)); + return -1; + } + + // Enter the message pump to allow the URL load to proceed. + PumpEvents(); + } // this scopes the nsCOMPtrs + // no nsCOMPtrs are allowed to be alive when you call NS_ShutdownXPCOM + NS_ShutdownXPCOM(nullptr); + return 0; +} diff --git a/netwerk/test/TestServ.js b/netwerk/test/TestServ.js new file mode 100644 index 0000000000..7716ac5040 --- /dev/null +++ b/netwerk/test/TestServ.js @@ -0,0 +1,164 @@ +/* vim:set ts=2 sw=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/. */ + +/* + * To use try out this JS server socket implementation, just copy this file + * into the "components" directory of a Mozilla build. Then load the URL + * http://localhost:4444/ in the browser. You should see a page get loaded + * that was served up by this component :-) + * + * This code requires Mozilla 1.6 or better. + */ + +const kTESTSERV_CONTRACTID = "@mozilla.org/network/test-serv;1"; +const kTESTSERV_CID = Components.ID("{a741fcd5-9695-42e8-a7f7-14f9a29f8200}"); +const nsISupports = Components.interfaces.nsISupports; +const nsIObserver = Components.interfaces.nsIObserver; +const nsIServerSocket = Components.interfaces.nsIServerSocket; +const nsIServerSocketListener = Components.interfaces.nsIServerSocketListener; +const nsITransport = Components.interfaces.nsITransport; +const nsIScriptableInputStream = Components.interfaces.nsIScriptableInputStream; + +/** we'll listen on this port for HTTP requests **/ +const kPORT = 4444; + +function nsTestServ() { dump(">>> creating nsTestServ instance\n"); }; + +nsTestServ.prototype = +{ + QueryInterface: function(iid) + { + if (iid.equals(nsIObserver) || + iid.equals(nsIServerSocketListener) || + iid.equals(nsISupports)) + return this; + + throw Components.results.NS_ERROR_NO_INTERFACE; + }, + + observe: function(subject, topic, data) + { + dump(">>> observe [" + topic + "]\n"); + this.startListening(); + }, + + /* this function is called when we receive a new connection */ + onSocketAccepted: function(serverSocket, clientSocket) + { + dump(">>> accepted connection on "+clientSocket.host+":"+clientSocket.port+"\n"); + + var input = clientSocket.openInputStream(nsITransport.OPEN_BLOCKING, 0, 0); + var output = clientSocket.openOutputStream(nsITransport.OPEN_BLOCKING, 0, 0); + + this.consumeInput(input); + + const fixedResponse = + "HTTP/1.0 200 OK\r\nContent-Type: text/plain\r\n\r\nFooooopy!!\r\n"; + var response = fixedResponse + "\r\n" + new Date().toString() + "\r\n"; + var n = output.write(response, response.length); + dump(">>> wrote "+n+" bytes\n"); + + input.close(); + output.close(); + }, + + onStopListening: function(serverSocket, status) + { + dump(">>> shutting down server socket\n"); + }, + + startListening: function() + { + const SERVERSOCKET_CONTRACTID = "@mozilla.org/network/server-socket;1"; + var socket = Components.classes[SERVERSOCKET_CONTRACTID].createInstance(nsIServerSocket); + socket.init(kPORT, true /* loopback only */, 5); + dump(">>> listening on port "+socket.port+"\n"); + socket.asyncListen(this); + }, + + consumeInput: function(input) + { + /* use nsIScriptableInputStream to consume all of the data on the stream */ + + var sin = Components.classes["@mozilla.org/scriptableinputstream;1"] + .createInstance(nsIScriptableInputStream); + sin.init(input); + + /* discard all data */ + while (sin.available() > 0) + sin.read(512); + } +} + +/** + * JS XPCOM component registration goop: + * + * We set ourselves up to observe the xpcom-startup category. This provides + * us with a starting point. + */ + +var servModule = new Object(); + +servModule.registerSelf = +function (compMgr, fileSpec, location, type) +{ + compMgr = compMgr.QueryInterface(Components.interfaces.nsIComponentRegistrar); + compMgr.registerFactoryLocation(kTESTSERV_CID, + "nsTestServ", + kTESTSERV_CONTRACTID, + fileSpec, + location, + type); + + const CATMAN_CONTRACTID = "@mozilla.org/categorymanager;1"; + const nsICategoryManager = Components.interfaces.nsICategoryManager; + var catman = Components.classes[CATMAN_CONTRACTID].getService(nsICategoryManager); + catman.addCategoryEntry("xpcom-startup", + "TestServ", + kTESTSERV_CONTRACTID, + true, + true); +} + +servModule.getClassObject = +function (compMgr, cid, iid) +{ + if (!cid.equals(kTESTSERV_CID)) + throw Components.results.NS_ERROR_NO_INTERFACE; + + if (!iid.equals(Components.interfaces.nsIFactory)) + throw Components.results.NS_ERROR_NOT_IMPLEMENTED; + + return servFactory; +} + +servModule.canUnload = +function (compMgr) +{ + dump(">>> unloading test serv.\n"); + return true; +} + +var servFactory = new Object(); + +servFactory.createInstance = +function (outer, iid) +{ + if (outer != null) + throw Components.results.NS_ERROR_NO_AGGREGATION; + + if (!iid.equals(nsIObserver) && + !iid.equals(nsISupports)) + throw Components.results.NS_ERROR_NO_INTERFACE; + + return TestServ; +} + +function NSGetModule(compMgr, fileSpec) +{ + return servModule; +} + +var TestServ = new nsTestServ(); diff --git a/netwerk/test/TestSocketIO.cpp b/netwerk/test/TestSocketIO.cpp new file mode 100644 index 0000000000..319e8e1a4b --- /dev/null +++ b/netwerk/test/TestSocketIO.cpp @@ -0,0 +1,343 @@ +/* -*- 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/. */ +#include <stdio.h> +#include <signal.h> + +#ifdef WIN32 +#include <windows.h> +#endif + +#include "nspr.h" +#include "nscore.h" +#include "nsISocketTransportService.h" +#include "nsIEventQueueService.h" +#include "nsIServiceManager.h" +#include "nsITransport.h" +#include "nsIRequest.h" +#include "nsIStreamProvider.h" +#include "nsIStreamListener.h" +#include "nsIPipe.h" +#include "nsIOutputStream.h" +#include "nsIInputStream.h" +#include "nsCRT.h" +#include "nsCOMPtr.h" +#include "nsIByteArrayInputStream.h" + +static PRLogModuleInfo *gTestSocketIOLog; +#define LOG(args) MOZ_LOG(gTestSocketIOLog, mozilla::LogLevel::Debug, args) + +static NS_DEFINE_CID(kSocketTransportServiceCID, NS_SOCKETTRANSPORTSERVICE_CID); +static NS_DEFINE_CID(kEventQueueServiceCID, NS_EVENTQUEUESERVICE_CID); + +static PRTime gElapsedTime; +static int gKeepRunning = 1; +static nsIEventQueue* gEventQ = nullptr; + +// +//---------------------------------------------------------------------------- +// Test Listener +//---------------------------------------------------------------------------- +// + +class TestListener : public nsIStreamListener +{ +public: + TestListener() {} + virtual ~TestListener() {} + + NS_DECL_ISUPPORTS + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSISTREAMLISTENER +}; + +NS_IMPL_ISUPPORTS(TestListener, + nsIRequestObserver, + nsIStreamListener); + +NS_IMETHODIMP +TestListener::OnStartRequest(nsIRequest* request, nsISupports* context) +{ + LOG(("TestListener::OnStartRequest\n")); + return NS_OK; +} + +NS_IMETHODIMP +TestListener::OnDataAvailable(nsIRequest* request, + nsISupports* context, + nsIInputStream *aIStream, + uint32_t aSourceOffset, + uint32_t aLength) +{ + LOG(("TestListener::OnDataAvailable [offset=%u length=%u]\n", + aSourceOffset, aLength)); + char buf[1025]; + uint32_t amt; + while (1) { + aIStream->Read(buf, 1024, &amt); + if (amt == 0) + break; + buf[amt] = '\0'; + puts(buf); + } + return NS_OK; +} + +NS_IMETHODIMP +TestListener::OnStopRequest(nsIRequest* request, nsISupports* context, + nsresult aStatus) +{ + LOG(("TestListener::OnStopRequest [aStatus=%x]\n", aStatus)); + gKeepRunning = 0; + return NS_OK; +} + +// +//---------------------------------------------------------------------------- +// Test Provider +//---------------------------------------------------------------------------- +// + +class TestProvider : public nsIStreamProvider +{ +public: + TestProvider(char *data); + virtual ~TestProvider(); + + NS_DECL_ISUPPORTS + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSISTREAMPROVIDER + +protected: + nsCOMPtr<nsIByteArrayInputStream> mData; +}; + +NS_IMPL_ISUPPORTS(TestProvider, + nsIStreamProvider, + nsIRequestObserver) + +TestProvider::TestProvider(char *data) +{ + NS_NewByteArrayInputStream(getter_AddRefs(mData), data, strlen(data)); + LOG(("Constructing TestProvider [this=%p]\n", this)); +} + +TestProvider::~TestProvider() +{ + LOG(("Destroying TestProvider [this=%p]\n", this)); +} + +NS_IMETHODIMP +TestProvider::OnStartRequest(nsIRequest* request, nsISupports* context) +{ + LOG(("TestProvider::OnStartRequest [this=%p]\n", this)); + return NS_OK; +} + +NS_IMETHODIMP +TestProvider::OnStopRequest(nsIRequest* request, nsISupports* context, + nsresult aStatus) +{ + LOG(("TestProvider::OnStopRequest [status=%x]\n", aStatus)); + + nsCOMPtr<nsIStreamListener> listener = do_QueryInterface(new TestListener()); + + if (NS_SUCCEEDED(aStatus)) { + nsCOMPtr<nsITransportRequest> treq = do_QueryInterface(request); + nsCOMPtr<nsITransport> transport; + treq->GetTransport(getter_AddRefs(transport)); + if (transport) { + nsCOMPtr<nsIRequest> readRequest; + transport->AsyncRead(listener, nullptr, 0, 0, 0, getter_AddRefs(readRequest)); + } + } else + gKeepRunning = 0; + + return NS_OK; +} + +NS_IMETHODIMP +TestProvider::OnDataWritable(nsIRequest *request, nsISupports *context, + nsIOutputStream *output, uint32_t offset, uint32_t count) +{ + LOG(("TestProvider::OnDataWritable [offset=%u, count=%u]\n", offset, count)); + uint32_t writeCount; + nsresult rv = output->WriteFrom(mData, count, &writeCount); + // Zero bytes written on success indicates EOF + if (NS_SUCCEEDED(rv) && (writeCount == 0)) + return NS_BASE_STREAM_CLOSED; + return rv; +} + +// +//---------------------------------------------------------------------------- +// Synchronous IO +//---------------------------------------------------------------------------- +// +nsresult +WriteRequest(nsIOutputStream *os, const char *request) +{ + LOG(("WriteRequest [request=%s]\n", request)); + uint32_t n; + return os->Write(request, strlen(request), &n); +} + +nsresult +ReadResponse(nsIInputStream *is) +{ + uint32_t bytesRead; + char buf[2048]; + do { + is->Read(buf, sizeof(buf), &bytesRead); + if (bytesRead > 0) + fwrite(buf, 1, bytesRead, stdout); + } while (bytesRead > 0); + return NS_OK; +} + +// +//---------------------------------------------------------------------------- +// Startup... +//---------------------------------------------------------------------------- +// + +void +sighandler(int sig) +{ + LOG(("got signal: %d\n", sig)); + NS_BREAK(); +} + +void +usage(char **argv) +{ + printf("usage: %s [-sync] <host> <path>\n", argv[0]); + exit(1); +} + +int +main(int argc, char* argv[]) +{ + nsresult rv; + + signal(SIGSEGV, sighandler); + + gTestSocketIOLog = PR_NewLogModule("TestSocketIO"); + + if (argc < 3) + usage(argv); + + int i=0; + bool sync = false; + if (nsCRT::strcasecmp(argv[1], "-sync") == 0) { + if (argc < 4) + usage(argv); + sync = true; + i = 1; + } + + char *hostName = argv[1+i]; + char *fileName = argv[2+i]; + int port = 80; + + // Create the Event Queue for this thread... + nsCOMPtr<nsIEventQueueService> eventQService = + do_GetService(kEventQueueServiceCID, &rv); + if (NS_FAILED(rv)) { + NS_WARNING("failed to create: event queue service!"); + return rv; + } + + rv = eventQService->CreateMonitoredThreadEventQueue(); + if (NS_FAILED(rv)) { + NS_WARNING("failed to create: thread event queue!"); + return rv; + } + + eventQService->GetThreadEventQueue(NS_CURRENT_THREAD, &gEventQ); + + // Create the Socket transport service... + nsCOMPtr<nsISocketTransportService> sts = + do_GetService(kSocketTransportServiceCID, &rv); + if (NS_FAILED(rv)) { + NS_WARNING("failed to create: socket transport service!"); + return rv; + } + + char *buffer = PR_smprintf("GET %s HTTP/1.1" CRLF + "host: %s" CRLF + "user-agent: Mozilla/5.0 (X11; N; Linux 2.2.16-22smp i686; en-US; m18) Gecko/20001220" CRLF + "accept: */*" CRLF + "accept-language: en" CRLF + "accept-encoding: gzip,deflate,compress,identity" CRLF + "keep-alive: 300" CRLF + "connection: keep-alive" CRLF + CRLF, + fileName, hostName); + LOG(("Request [\n%s]\n", buffer)); + + // Create the socket transport... + nsCOMPtr<nsITransport> transport; + rv = sts->CreateTransport(hostName, port, nullptr, 0, 0, getter_AddRefs(transport)); + if (NS_FAILED(rv)) { + NS_WARNING("failed to create: socket transport!"); + return rv; + } + + gElapsedTime = PR_Now(); + + if (!sync) { + nsCOMPtr<nsIRequest> request; + rv = transport->AsyncWrite(new TestProvider(buffer), nullptr, 0, 0, 0, getter_AddRefs(request)); + if (NS_FAILED(rv)) { + NS_WARNING("failed calling: AsyncWrite!"); + return rv; + } + + // Enter the message pump to allow the URL load to proceed. + while ( gKeepRunning ) { + PLEvent *gEvent; + gEventQ->WaitForEvent(&gEvent); + gEventQ->HandleEvent(gEvent); + } + } + else { + // synchronous write + { + nsCOMPtr<nsIOutputStream> os; + rv = transport->OpenOutputStream(0, 0, 0, getter_AddRefs(os)); + if (NS_FAILED(rv)) { + LOG(("OpenOutputStream failed [rv=%x]\n", rv)); + return rv; + } + rv = WriteRequest(os, buffer); + if (NS_FAILED(rv)) { + LOG(("WriteRequest failed [rv=%x]\n", rv)); + return rv; + } + } + // synchronous read + { + nsCOMPtr<nsIInputStream> is; + rv = transport->OpenInputStream(0, 0, 0, getter_AddRefs(is)); + if (NS_FAILED(rv)) { + LOG(("OpenInputStream failed [rv=%x]\n", rv)); + return rv; + } + rv = ReadResponse(is); + if (NS_FAILED(rv)) { + LOG(("ReadResponse failed [rv=%x]\n", rv)); + return rv; + } + } + } + + PRTime endTime; + endTime = PR_Now(); + LOG(("Elapsed time: %d\n", (int32_t)(endTime/1000UL - gElapsedTime/1000UL))); + + sts->Shutdown(); + return 0; +} + diff --git a/netwerk/test/TestSocketInput.cpp b/netwerk/test/TestSocketInput.cpp new file mode 100644 index 0000000000..c3e58bd1eb --- /dev/null +++ b/netwerk/test/TestSocketInput.cpp @@ -0,0 +1,154 @@ +/* -*- 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/. */ +#include <stdio.h> + +#ifdef WIN32 +#include <windows.h> +#endif + +#include "nscore.h" +#include "nsCOMPtr.h" +#include "nsISocketTransportService.h" +#include "nsIEventQueueService.h" +#include "nsIServiceManager.h" +#include "nsIComponentRegistrar.h" +#include "nsITransport.h" +#include "nsIRequest.h" +#include "nsIStreamListener.h" +#include "nsIInputStream.h" + +static NS_DEFINE_CID(kSocketTransportServiceCID, NS_SOCKETTRANSPORTSERVICE_CID); +static NS_DEFINE_CID(kEventQueueServiceCID, NS_EVENTQUEUESERVICE_CID); + +static int gKeepRunning = 1; + +class InputTestConsumer : public nsIStreamListener +{ +public: + + InputTestConsumer(); + virtual ~InputTestConsumer(); + + // ISupports interface... + NS_DECL_ISUPPORTS + + // IStreamListener interface... + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSISTREAMLISTENER +}; + + +InputTestConsumer::InputTestConsumer() +{ +} + +InputTestConsumer::~InputTestConsumer() +{ +} + + +NS_IMPL_ISUPPORTS(InputTestConsumer, nsIRequestObserver, nsIStreamListener) + + +NS_IMETHODIMP +InputTestConsumer::OnStartRequest(nsIRequest *request, nsISupports* context) +{ + printf("+++ OnStartRequest +++\n"); + return NS_OK; +} + + +NS_IMETHODIMP +InputTestConsumer::OnDataAvailable(nsIRequest *request, + nsISupports* context, + nsIInputStream *aIStream, + uint64_t aSourceOffset, + uint32_t aLength) +{ + char buf[1025]; + while (aLength > 0) { + uint32_t amt; + aIStream->Read(buf, 1024, &amt); + if (amt == 0) break; + buf[amt] = '\0'; + printf(buf); + aLength -= amt; + } + + return NS_OK; +} + + +NS_IMETHODIMP +InputTestConsumer::OnStopRequest(nsIRequest *request, nsISupports* context, + nsresult aStatus) +{ + gKeepRunning = 0; + printf("+++ OnStopRequest status %x +++\n", aStatus); + return NS_OK; +} + + +int +main(int argc, char* argv[]) +{ + nsresult rv; + + if (argc < 2) { + printf("usage: %s <host>\n", argv[0]); + return -1; + } + + int port; + char* hostName = argv[1]; +//nsString portString(argv[2]); + +//port = portString.ToInteger(&rv); + port = 13; + { + nsCOMPtr<nsIServiceManager> servMan; + NS_InitXPCOM2(getter_AddRefs(servMan), nullptr, nullptr); + nsCOMPtr<nsIComponentRegistrar> registrar = do_QueryInterface(servMan); + NS_ASSERTION(registrar, "Null nsIComponentRegistrar"); + if (registrar) + registrar->AutoRegister(nullptr); + + // Create the Event Queue for this thread... + nsCOMPtr<nsIEventQueueService> eventQService = + do_GetService(kEventQueueServiceCID, &rv); + if (NS_FAILED(rv)) return rv; + + nsCOMPtr<nsIEventQueue> eventQ; + rv = eventQService->GetThreadEventQueue(NS_CURRENT_THREAD, getter_AddRefs(eventQ)); + if (NS_FAILED(rv)) return rv; + + nsCOMPtr<nsISocketTransportService> sts = + do_GetService(kSocketTransportServiceCID, &rv); + if (NS_FAILED(rv)) return rv; + + nsITransport* transport; + + rv = sts->CreateTransport(hostName, port, nullptr, 0, 0, &transport); + if (NS_SUCCEEDED(rv)) { + nsCOMPtr<nsIRequest> request; + transport->AsyncRead(new InputTestConsumer, nullptr, 0, -1, 0, getter_AddRefs(request)); + + NS_RELEASE(transport); + } + + // Enter the message pump to allow the URL load to proceed. + while ( gKeepRunning ) { + PLEvent *gEvent; + eventQ->WaitForEvent(&gEvent); + eventQ->HandleEvent(gEvent); + } + + } // this scopes the nsCOMPtrs + // no nsCOMPtrs are allowed to be alive when you call NS_ShutdownXPCOM + rv = NS_ShutdownXPCOM(nullptr); + NS_ASSERTION(NS_SUCCEEDED(rv), "NS_ShutdownXPCOM failed"); + return 0; +} + diff --git a/netwerk/test/TestSocketTransport.cpp b/netwerk/test/TestSocketTransport.cpp new file mode 100644 index 0000000000..58422a38c2 --- /dev/null +++ b/netwerk/test/TestSocketTransport.cpp @@ -0,0 +1,307 @@ +/* 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/. */ + +#include "TestCommon.h" +#include "nsIComponentRegistrar.h" +#include "nsPISocketTransportService.h" +#include "nsISocketTransport.h" +#include "nsIAsyncInputStream.h" +#include "nsIAsyncOutputStream.h" +#include "nsIProgressEventSink.h" +#include "nsIInterfaceRequestor.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsIRequest.h" +#include "nsIServiceManager.h" +#include "nsIComponentManager.h" +#include "nsCOMPtr.h" +#include "nsMemory.h" +#include "nsStringAPI.h" +#include "nsIDNSService.h" +#include "nsIFileStreams.h" +#include "nsIStreamListener.h" +#include "nsIFile.h" +#include "nsAutoLock.h" +#include "mozilla/Logging.h" + +//////////////////////////////////////////////////////////////////////////////// + +// +// set NSPR_LOG_MODULES=Test:5 +// +static PRLogModuleInfo *gTestLog = nullptr; +#define LOG(args) MOZ_LOG(gTestLog, mozilla::LogLevel::Debug, args) + +//////////////////////////////////////////////////////////////////////////////// + +static NS_DEFINE_CID(kSocketTransportServiceCID, NS_SOCKETTRANSPORTSERVICE_CID); + +//////////////////////////////////////////////////////////////////////////////// + +class MyHandler : public nsIOutputStreamCallback + , public nsIInputStreamCallback +{ +public: + NS_DECL_THREADSAFE_ISUPPORTS + + MyHandler(const char *path, + nsIAsyncInputStream *in, + nsIAsyncOutputStream *out) + : mInput(in) + , mOutput(out) + , mWriteOffset(0) + { + mBuf.AssignLiteral("GET "); + mBuf.Append(path); + mBuf.AppendLiteral(" HTTP/1.0\r\n\r\n"); + } + virtual ~MyHandler() {} + + // called on any thread + NS_IMETHOD OnOutputStreamReady(nsIAsyncOutputStream *out) + { + LOG(("OnOutputStreamReady\n")); + + nsresult rv; + uint32_t n, count = mBuf.Length() - mWriteOffset; + + rv = out->Write(mBuf.get() + mWriteOffset, count, &n); + + LOG((" write returned [rv=%x count=%u]\n", rv, n)); + + if (NS_FAILED(rv) || (n == 0)) { + if (rv != NS_BASE_STREAM_WOULD_BLOCK) { + LOG((" done writing; starting to read\n")); + mInput->AsyncWait(this, 0, 0, nullptr); + return NS_OK; + } + } + + mWriteOffset += n; + + return out->AsyncWait(this, 0, 0, nullptr); + } + + // called on any thread + NS_IMETHOD OnInputStreamReady(nsIAsyncInputStream *in) + { + LOG(("OnInputStreamReady\n")); + + nsresult rv; + uint32_t n; + char buf[500]; + + rv = in->Read(buf, sizeof(buf), &n); + + LOG((" read returned [rv=%x count=%u]\n", rv, n)); + + if (NS_FAILED(rv) || (n == 0)) { + if (rv != NS_BASE_STREAM_WOULD_BLOCK) { + QuitPumpingEvents(); + return NS_OK; + } + } + + return in->AsyncWait(this, 0, 0, nullptr); + } + +private: + nsCOMPtr<nsIAsyncInputStream> mInput; + nsCOMPtr<nsIAsyncOutputStream> mOutput; + nsCString mBuf; + uint32_t mWriteOffset; +}; + +NS_IMPL_ISUPPORTS(MyHandler, + nsIOutputStreamCallback, + nsIInputStreamCallback) + +//////////////////////////////////////////////////////////////////////////////// + +/** + * create transport, open streams, and close + */ +static nsresult +RunCloseTest(nsISocketTransportService *sts, + const char *host, int port, + uint32_t inFlags, uint32_t outFlags) +{ + nsresult rv; + + LOG(("RunCloseTest\n")); + + nsCOMPtr<nsISocketTransport> transport; + rv = sts->CreateTransport(nullptr, 0, + nsDependentCString(host), port, nullptr, + getter_AddRefs(transport)); + if (NS_FAILED(rv)) return rv; + + nsCOMPtr<nsIInputStream> in; + rv = transport->OpenInputStream(inFlags, 0, 0, getter_AddRefs(in)); + nsCOMPtr<nsIAsyncInputStream> asyncIn = do_QueryInterface(in, &rv); + if (NS_FAILED(rv)) return rv; + + nsCOMPtr<nsIOutputStream> out; + rv = transport->OpenOutputStream(outFlags, 0, 0, getter_AddRefs(out)); + nsCOMPtr<nsIAsyncOutputStream> asyncOut = do_QueryInterface(out, &rv); + if (NS_FAILED(rv)) return rv; + + LOG(("waiting 1 second before closing transport and streams...\n")); + PR_Sleep(PR_SecondsToInterval(1)); + + // let nsCOMPtr destructors close everything... + return NS_OK; +} + + +/** + * asynchronously read socket stream + */ +static nsresult +RunTest(nsISocketTransportService *sts, + const char *host, int port, const char *path, + uint32_t inFlags, uint32_t outFlags) +{ + nsresult rv; + + LOG(("RunTest\n")); + + nsCOMPtr<nsISocketTransport> transport; + rv = sts->CreateTransport(nullptr, 0, + nsDependentCString(host), port, nullptr, + getter_AddRefs(transport)); + if (NS_FAILED(rv)) return rv; + + nsCOMPtr<nsIInputStream> in; + rv = transport->OpenInputStream(inFlags, 0, 0, getter_AddRefs(in)); + nsCOMPtr<nsIAsyncInputStream> asyncIn = do_QueryInterface(in, &rv); + if (NS_FAILED(rv)) return rv; + + nsCOMPtr<nsIOutputStream> out; + rv = transport->OpenOutputStream(outFlags, 0, 0, getter_AddRefs(out)); + nsCOMPtr<nsIAsyncOutputStream> asyncOut = do_QueryInterface(out, &rv); + if (NS_FAILED(rv)) return rv; + + MyHandler *handler = new MyHandler(path, asyncIn, asyncOut); + if (handler == nullptr) + return NS_ERROR_OUT_OF_MEMORY; + NS_ADDREF(handler); + + rv = asyncOut->AsyncWait(handler, 0, 0, nullptr); + + if (NS_SUCCEEDED(rv)) + PumpEvents(); + + NS_RELEASE(handler); + + return NS_OK; +} + +//////////////////////////////////////////////////////////////////////////////// + +int +main(int argc, char* argv[]) +{ + if (test_common_init(&argc, &argv) != 0) + return -1; + + nsresult rv; + + if (argc < 4) { + printf("usage: TestSocketTransport <host> <port> <path>\n"); + return -1; + } + + { + nsCOMPtr<nsIServiceManager> servMan; + NS_InitXPCOM2(getter_AddRefs(servMan), nullptr, nullptr); + nsCOMPtr<nsIComponentRegistrar> registrar = do_QueryInterface(servMan); + NS_ASSERTION(registrar, "Null nsIComponentRegistrar"); + if (registrar) + registrar->AutoRegister(nullptr); + + gTestLog = PR_NewLogModule("Test"); + + // Make sure the DNS service is initialized on the main thread + nsCOMPtr<nsIDNSService> dns = + do_GetService(NS_DNSSERVICE_CONTRACTID, &rv); + if (NS_FAILED(rv)) return rv; + + nsCOMPtr<nsPISocketTransportService> sts = + do_GetService(kSocketTransportServiceCID, &rv); + if (NS_FAILED(rv)) return rv; + + LOG(("phase 1 tests...\n")); + + LOG(("flags = { OPEN_UNBUFFERED, OPEN_UNBUFFERED }\n")); + rv = RunCloseTest(sts, argv[1], atoi(argv[2]), + nsITransport::OPEN_UNBUFFERED, + nsITransport::OPEN_UNBUFFERED); + NS_ASSERTION(NS_SUCCEEDED(rv), "RunCloseTest failed"); + + LOG(("flags = { OPEN_BUFFERED, OPEN_UNBUFFERED }\n")); + rv = RunCloseTest(sts, argv[1], atoi(argv[2]), + 0 /* nsITransport::OPEN_BUFFERED */, + nsITransport::OPEN_UNBUFFERED); + NS_ASSERTION(NS_SUCCEEDED(rv), "RunCloseTest failed"); + + LOG(("flags = { OPEN_UNBUFFERED, OPEN_BUFFERED }\n")); + rv = RunCloseTest(sts, argv[1], atoi(argv[2]), + nsITransport::OPEN_UNBUFFERED, + 0 /*nsITransport::OPEN_BUFFERED */); + NS_ASSERTION(NS_SUCCEEDED(rv), "RunCloseTest failed"); + + LOG(("flags = { OPEN_BUFFERED, OPEN_BUFFERED }\n")); + rv = RunCloseTest(sts, argv[1], atoi(argv[2]), + 0 /*nsITransport::OPEN_BUFFERED */, + 0 /*nsITransport::OPEN_BUFFERED */); + NS_ASSERTION(NS_SUCCEEDED(rv), "RunCloseTest failed"); + + LOG(("calling Shutdown on socket transport service:\n")); + sts->Shutdown(); + + LOG(("calling Init on socket transport service:\n")); + sts->Init(); + + LOG(("phase 2 tests...\n")); + + LOG(("flags = { OPEN_UNBUFFERED, OPEN_UNBUFFERED }\n")); + rv = RunTest(sts, argv[1], atoi(argv[2]), argv[3], + nsITransport::OPEN_UNBUFFERED, + nsITransport::OPEN_UNBUFFERED); + NS_ASSERTION(NS_SUCCEEDED(rv), "RunTest failed"); + + LOG(("flags = { OPEN_BUFFERED, OPEN_UNBUFFERED }\n")); + rv = RunTest(sts, argv[1], atoi(argv[2]), argv[3], + 0 /* nsITransport::OPEN_BUFFERED */, + nsITransport::OPEN_UNBUFFERED); + NS_ASSERTION(NS_SUCCEEDED(rv), "RunTest failed"); + + LOG(("flags = { OPEN_UNBUFFERED, OPEN_BUFFERED }\n")); + rv = RunTest(sts, argv[1], atoi(argv[2]), argv[3], + nsITransport::OPEN_UNBUFFERED, + 0 /*nsITransport::OPEN_BUFFERED */); + NS_ASSERTION(NS_SUCCEEDED(rv), "RunTest failed"); + + LOG(("flags = { OPEN_BUFFERED, OPEN_BUFFERED }\n")); + rv = RunTest(sts, argv[1], atoi(argv[2]), argv[3], + 0 /*nsITransport::OPEN_BUFFERED */, + 0 /*nsITransport::OPEN_BUFFERED */); + NS_ASSERTION(NS_SUCCEEDED(rv), "RunTest failed"); + + LOG(("waiting 1 second before calling Shutdown...\n")); + PR_Sleep(PR_SecondsToInterval(1)); + + LOG(("calling Shutdown on socket transport service:\n")); + sts->Shutdown(); + + // give background threads a chance to finish whatever work they may + // be doing. + LOG(("waiting 1 second before exiting...\n")); + PR_Sleep(PR_SecondsToInterval(1)); + } // this scopes the nsCOMPtrs + // no nsCOMPtrs are allowed to be alive when you call NS_ShutdownXPCOM + rv = NS_ShutdownXPCOM(nullptr); + NS_ASSERTION(NS_SUCCEEDED(rv), "NS_ShutdownXPCOM failed"); + return 0; +} diff --git a/netwerk/test/TestStreamLoader.cpp b/netwerk/test/TestStreamLoader.cpp new file mode 100644 index 0000000000..c01148f40d --- /dev/null +++ b/netwerk/test/TestStreamLoader.cpp @@ -0,0 +1,101 @@ +#include <stdio.h> +#include "TestCommon.h" +#include "nsNetUtil.h" +#include "nsServiceManagerUtils.h" +#include "nsThreadUtils.h" +#include "NetwerkTestLogging.h" +#include "mozilla/Attributes.h" +#include "nsIScriptSecurityManager.h" + +// +// set NSPR_LOG_MODULES=Test:5 +// +static PRLogModuleInfo *gTestLog = nullptr; +#define LOG(args) MOZ_LOG(gTestLog, mozilla::LogLevel::Debug, args) + +class MyStreamLoaderObserver final : public nsIStreamLoaderObserver +{ + ~MyStreamLoaderObserver() = default; + +public: + NS_DECL_ISUPPORTS + NS_DECL_NSISTREAMLOADEROBSERVER +}; + +NS_IMPL_ISUPPORTS(MyStreamLoaderObserver, nsIStreamLoaderObserver) + +NS_IMETHODIMP +MyStreamLoaderObserver::OnStreamComplete(nsIStreamLoader *loader, + nsISupports *ctxt, + nsresult status, + uint32_t resultLen, + const uint8_t *result) +{ + LOG(("OnStreamComplete [status=%x resultLen=%u]\n", status, resultLen)); + + nsCOMPtr<nsIRequest> request; + loader->GetRequest(getter_AddRefs(request)); + LOG((" request=%p\n", request.get())); + + QuitPumpingEvents(); + return NS_OK; +} + +int main(int argc, char **argv) +{ + if (test_common_init(&argc, &argv) != 0) + return -1; + + if (argc < 2) { + printf("usage: %s <url>\n", argv[0]); + return -1; + } + + gTestLog = PR_NewLogModule("Test"); + + nsresult rv = NS_InitXPCOM2(nullptr, nullptr, nullptr); + if (NS_FAILED(rv)) + return -1; + + { + nsCOMPtr<nsIURI> uri; + rv = NS_NewURI(getter_AddRefs(uri), nsDependentCString(argv[1])); + if (NS_FAILED(rv)) + return -1; + + nsCOMPtr<nsIScriptSecurityManager> secman = + do_GetService(NS_SCRIPTSECURITYMANAGER_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, -1); + nsCOMPtr<nsIPrincipal> systemPrincipal; + rv = secman->GetSystemPrincipal(getter_AddRefs(systemPrincipal)); + NS_ENSURE_SUCCESS(rv, -1); + + nsCOMPtr<nsIChannel> chan; + rv = NS_NewChannel(getter_AddRefs(chan), + uri, + systemPrincipal, + nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_DATA_INHERITS, + nsIContentPolicy::TYPE_OTHER); + + if (NS_FAILED(rv)) + return -1; + + nsCOMPtr<nsIStreamLoaderObserver> observer = new MyStreamLoaderObserver(); + if (!observer) + return -1; + + nsCOMPtr<nsIStreamLoader> loader; + rv = NS_NewStreamLoader(getter_AddRefs(loader), observer); + if (NS_FAILED(rv)) + return -1; + + rv = chan->AsyncOpen2(loader); + if (NS_FAILED(rv)) + return -1; + + PumpEvents(); + } // this scopes the nsCOMPtrs + // no nsCOMPtrs are allowed to be alive when you call NS_ShutdownXPCOM + NS_ShutdownXPCOM(nullptr); + return 0; +} diff --git a/netwerk/test/TestStreamPump.cpp b/netwerk/test/TestStreamPump.cpp new file mode 100644 index 0000000000..376b9deb7c --- /dev/null +++ b/netwerk/test/TestStreamPump.cpp @@ -0,0 +1,171 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "TestCommon.h" +#include "nsIComponentRegistrar.h" +#include "nsIStreamTransportService.h" +#include "nsIAsyncInputStream.h" +#include "nsIProgressEventSink.h" +#include "nsIInterfaceRequestor.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsIRequest.h" +#include "nsIServiceManager.h" +#include "nsIComponentManager.h" +#include "nsISeekableStream.h" +#include "nsCOMPtr.h" +#include "nsMemory.h" +#include "nsStringAPI.h" +#include "nsIFileStreams.h" +#include "nsIStreamListener.h" +#include "nsIFile.h" +#include "nsNetUtil.h" +#include "nsAutoLock.h" +#include "mozilla/Logging.h" +#include "prprf.h" +#include <algorithm> + +//////////////////////////////////////////////////////////////////////////////// + +// +// set NSPR_LOG_MODULES=Test:5 +// +static PRLogModuleInfo *gTestLog = nullptr; +#define LOG(args) MOZ_LOG(gTestLog, mozilla::LogLevel::Debug, args) + +//////////////////////////////////////////////////////////////////////////////// + +class MyListener : public nsIStreamListener +{ +public: + NS_DECL_ISUPPORTS + + MyListener() {} + virtual ~MyListener() {} + + NS_IMETHOD OnStartRequest(nsIRequest *req, nsISupports *ctx) + { + LOG(("MyListener::OnStartRequest\n")); + return NS_OK; + } + + NS_IMETHOD OnDataAvailable(nsIRequest *req, nsISupports *ctx, + nsIInputStream *stream, + uint64_t offset, uint32_t count) + { + LOG(("MyListener::OnDataAvailable [offset=%llu count=%u]\n", offset, count)); + + char buf[500]; + nsresult rv; + + while (count) { + uint32_t n, amt = std::min<uint32_t>(count, sizeof(buf)); + + rv = stream->Read(buf, amt, &n); + if (NS_FAILED(rv)) { + LOG((" read returned 0x%08x\n", rv)); + return rv; + } + + fwrite(buf, n, 1, stdout); + printf("\n"); + + LOG((" read %u bytes\n", n)); + count -= n; + } + + return NS_OK; + } + + NS_IMETHOD OnStopRequest(nsIRequest *req, nsISupports *ctx, nsresult status) + { + LOG(("MyListener::OnStopRequest [status=%x]\n", status)); + QuitPumpingEvents(); + return NS_OK; + } +}; + +NS_IMPL_ISUPPORTS(MyListener, + nsIRequestObserver, + nsIStreamListener) + +//////////////////////////////////////////////////////////////////////////////// + +/** + * asynchronously copy file. + */ +static nsresult +RunTest(nsIFile *file, int64_t offset, int64_t length) +{ + nsresult rv; + + LOG(("RunTest\n")); + + nsCOMPtr<nsIInputStream> stream; + rv = NS_NewLocalFileInputStream(getter_AddRefs(stream), file); + if (NS_FAILED(rv)) return rv; + + nsCOMPtr<nsIInputStreamPump> pump; + rv = NS_NewInputStreamPump(getter_AddRefs(pump), stream, offset, length); + if (NS_FAILED(rv)) return rv; + + rv = pump->AsyncRead(new MyListener(), nullptr); + if (NS_FAILED(rv)) return rv; + + PumpEvents(); + return NS_OK; +} + +//////////////////////////////////////////////////////////////////////////////// + +int +main(int argc, char* argv[]) +{ + if (test_common_init(&argc, &argv) != 0) + return -1; + + nsresult rv; + + if (argc < 4) { + printf("usage: %s <file-to-read> <start-offset> <read-length>\n", argv[0]); + return -1; + } + char* fileName = argv[1]; + int64_t offset, length; + int err = PR_sscanf(argv[2], "%lld", &offset); + if (err == -1) { + printf("Start offset must be an integer!\n"); + return 1; + } + err = PR_sscanf(argv[3], "%lld", &length); + if (err == -1) { + printf("Length must be an integer!\n"); + return 1; + } + + { + nsCOMPtr<nsIServiceManager> servMan; + NS_InitXPCOM2(getter_AddRefs(servMan), nullptr, nullptr); + nsCOMPtr<nsIComponentRegistrar> registrar = do_QueryInterface(servMan); + NS_ASSERTION(registrar, "Null nsIComponentRegistrar"); + if (registrar) + registrar->AutoRegister(nullptr); + + gTestLog = PR_NewLogModule("Test"); + + nsCOMPtr<nsIFile> file; + rv = NS_NewNativeLocalFile(nsDependentCString(fileName), false, getter_AddRefs(file)); + if (NS_FAILED(rv)) return rv; + + rv = RunTest(file, offset, length); + NS_ASSERTION(NS_SUCCEEDED(rv), "RunTest failed"); + + // give background threads a chance to finish whatever work they may + // be doing. + PR_Sleep(PR_SecondsToInterval(1)); + } // this scopes the nsCOMPtrs + // no nsCOMPtrs are allowed to be alive when you call NS_ShutdownXPCOM + rv = NS_ShutdownXPCOM(nullptr); + NS_ASSERTION(NS_SUCCEEDED(rv), "NS_ShutdownXPCOM failed"); + return NS_OK; +} diff --git a/netwerk/test/TestStreamTransport.cpp b/netwerk/test/TestStreamTransport.cpp new file mode 100644 index 0000000000..840c9578d6 --- /dev/null +++ b/netwerk/test/TestStreamTransport.cpp @@ -0,0 +1,319 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "TestCommon.h" +#include "nsIComponentRegistrar.h" +#include "nsIStreamTransportService.h" +#include "nsIAsyncInputStream.h" +#include "nsIProgressEventSink.h" +#include "nsIInterfaceRequestor.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsIRequest.h" +#include "nsIServiceManager.h" +#include "nsIComponentManager.h" +#include "nsCOMPtr.h" +#include "nsMemory.h" +#include "nsStringAPI.h" +#include "nsIFileStreams.h" +#include "nsIStreamListener.h" +#include "nsIFile.h" +#include "nsNetUtil.h" +#include "nsAutoLock.h" +#include "mozilla/Logging.h" +#include "prenv.h" + +//////////////////////////////////////////////////////////////////////////////// + +// +// set NSPR_LOG_MODULES=Test:5 +// +static PRLogModuleInfo *gTestLog = nullptr; +#define LOG(args) MOZ_LOG(gTestLog, mozilla::LogLevel::Debug, args) + +//////////////////////////////////////////////////////////////////////////////// + +static NS_DEFINE_CID(kStreamTransportServiceCID, NS_STREAMTRANSPORTSERVICE_CID); + +//////////////////////////////////////////////////////////////////////////////// + +#define CHUNK_SIZE 500 + +class MyCopier : public nsIInputStreamCallback + , public nsIOutputStreamCallback +{ +public: + NS_DECL_THREADSAFE_ISUPPORTS + + MyCopier() + : mLock(nullptr) + , mInputCondition(NS_OK) + { + } + + virtual ~MyCopier() + { + if (mLock) + nsAutoLock::DestroyLock(mLock); + if (mInput) + mInput->Close(); + if (mOutput) + mOutput->Close(); + } + + // called on any thread + NS_IMETHOD OnInputStreamReady(nsIAsyncInputStream *inStr) + { + LOG(("OnInputStreamReady\n")); + nsAutoLock lock(mLock); + NS_ASSERTION(inStr == mInput, "unexpected stream"); + Process_Locked(); + return NS_OK; + } + + // called on any thread + NS_IMETHOD OnOutputStreamReady(nsIAsyncOutputStream *outStr) + { + LOG(("OnOutputStreamReady\n")); + nsAutoLock lock(mLock); + NS_ASSERTION(outStr == mOutput, "unexpected stream"); + Process_Locked(); + return NS_OK; + } + + void Close_Locked() + { + LOG(("Close_Locked\n")); + + mOutput->Close(); + mOutput = 0; + mInput->Close(); + mInput = 0; + + // post done copying event + QuitPumpingEvents(); + } + + void Process_Locked() + { + while (1) { + mInputCondition = NS_OK; // reset + + uint32_t n; + nsresult rv = mOutput->WriteSegments(FillOutputBuffer, this, CHUNK_SIZE, &n); + if (NS_FAILED(rv) || (n == 0)) { + if (rv == NS_BASE_STREAM_WOULD_BLOCK) + mOutput->AsyncWait(this, 0, 0, nullptr); + else if (mInputCondition == NS_BASE_STREAM_WOULD_BLOCK) + mInput->AsyncWait(this, 0, 0, nullptr); + else + Close_Locked(); + break; + } + } + } + + nsresult AsyncCopy(nsITransport *srcTrans, nsITransport *destTrans) + { + mLock = nsAutoLock::NewLock("MyCopier::mLock"); + if (!mLock) + return NS_ERROR_OUT_OF_MEMORY; + + nsresult rv; + + nsCOMPtr<nsIInputStream> inStr; + rv = srcTrans->OpenInputStream(0, 0, 0, getter_AddRefs(inStr)); + if (NS_FAILED(rv)) return rv; + + nsCOMPtr<nsIOutputStream> outStr; + rv = destTrans->OpenOutputStream(0, 0, 0, getter_AddRefs(outStr)); + if (NS_FAILED(rv)) return rv; + + mInput = do_QueryInterface(inStr); + mOutput = do_QueryInterface(outStr); + + return mInput->AsyncWait(this, 0, 0, nullptr); + } + + static nsresult FillOutputBuffer(nsIOutputStream *outStr, + void *closure, + char *buffer, + uint32_t offset, + uint32_t count, + uint32_t *countRead) + { + MyCopier *self = (MyCopier *) closure; + + nsresult rv = self->mInput->Read(buffer, count, countRead); + if (NS_FAILED(rv)) + self->mInputCondition = rv; + else if (*countRead == 0) + self->mInputCondition = NS_BASE_STREAM_CLOSED; + + return self->mInputCondition; + } + +protected: + PRLock *mLock; + nsCOMPtr<nsIAsyncInputStream> mInput; + nsCOMPtr<nsIAsyncOutputStream> mOutput; + nsresult mInputCondition; +}; + +NS_IMPL_ISUPPORTS(MyCopier, + nsIInputStreamCallback, + nsIOutputStreamCallback) + +//////////////////////////////////////////////////////////////////////////////// + +/** + * asynchronously copy file. + */ +static nsresult +RunTest(nsIFile *srcFile, nsIFile *destFile) +{ + nsresult rv; + + LOG(("RunTest\n")); + + nsCOMPtr<nsIStreamTransportService> sts = + do_GetService(kStreamTransportServiceCID, &rv); + if (NS_FAILED(rv)) return rv; + + nsCOMPtr<nsIInputStream> srcStr; + rv = NS_NewLocalFileInputStream(getter_AddRefs(srcStr), srcFile); + if (NS_FAILED(rv)) return rv; + + nsCOMPtr<nsIOutputStream> destStr; + rv = NS_NewLocalFileOutputStream(getter_AddRefs(destStr), destFile); + if (NS_FAILED(rv)) return rv; + + nsCOMPtr<nsITransport> srcTransport; + rv = sts->CreateInputTransport(srcStr, int64_t(-1), int64_t(-1), true, + getter_AddRefs(srcTransport)); + if (NS_FAILED(rv)) return rv; + + nsCOMPtr<nsITransport> destTransport; + rv = sts->CreateOutputTransport(destStr, int64_t(-1), int64_t(-1), true, + getter_AddRefs(destTransport)); + if (NS_FAILED(rv)) return rv; + + MyCopier *copier = new MyCopier(); + if (copier == nullptr) + return NS_ERROR_OUT_OF_MEMORY; + NS_ADDREF(copier); + + rv = copier->AsyncCopy(srcTransport, destTransport); + if (NS_FAILED(rv)) return rv; + + PumpEvents(); + + NS_RELEASE(copier); + return NS_OK; +} + +//////////////////////////////////////////////////////////////////////////////// + +static nsresult +RunBlockingTest(nsIFile *srcFile, nsIFile *destFile) +{ + nsresult rv; + + LOG(("RunBlockingTest\n")); + + nsCOMPtr<nsIStreamTransportService> sts = + do_GetService(kStreamTransportServiceCID, &rv); + if (NS_FAILED(rv)) return rv; + + nsCOMPtr<nsIInputStream> srcIn; + rv = NS_NewLocalFileInputStream(getter_AddRefs(srcIn), srcFile); + if (NS_FAILED(rv)) return rv; + + nsCOMPtr<nsIOutputStream> fileOut; + rv = NS_NewLocalFileOutputStream(getter_AddRefs(fileOut), destFile); + if (NS_FAILED(rv)) return rv; + + nsCOMPtr<nsITransport> destTransport; + rv = sts->CreateOutputTransport(fileOut, int64_t(-1), int64_t(-1), + true, getter_AddRefs(destTransport)); + if (NS_FAILED(rv)) return rv; + + nsCOMPtr<nsIOutputStream> destOut; + rv = destTransport->OpenOutputStream(nsITransport::OPEN_BLOCKING, 100, 10, getter_AddRefs(destOut)); + if (NS_FAILED(rv)) return rv; + + char buf[120]; + uint32_t n; + for (;;) { + rv = srcIn->Read(buf, sizeof(buf), &n); + if (NS_FAILED(rv) || (n == 0)) return rv; + + rv = destOut->Write(buf, n, &n); + if (NS_FAILED(rv)) return rv; + } + + return NS_OK; +} + +//////////////////////////////////////////////////////////////////////////////// + +int +main(int argc, char* argv[]) +{ + if (test_common_init(&argc, &argv) != 0) + return -1; + + nsresult rv; + + if (argc < 2) { + printf("usage: %s <file-to-read>\n", argv[0]); + return -1; + } + char* fileName = argv[1]; + { + nsCOMPtr<nsIServiceManager> servMan; + NS_InitXPCOM2(getter_AddRefs(servMan), nullptr, nullptr); + nsCOMPtr<nsIComponentRegistrar> registrar = do_QueryInterface(servMan); + NS_ASSERTION(registrar, "Null nsIComponentRegistrar"); + if (registrar) + registrar->AutoRegister(nullptr); + + gTestLog = PR_NewLogModule("Test"); + + nsCOMPtr<nsIFile> srcFile; + rv = NS_NewNativeLocalFile(nsDependentCString(fileName), false, getter_AddRefs(srcFile)); + if (NS_FAILED(rv)) return rv; + + nsCOMPtr<nsIFile> destFile; + rv = srcFile->Clone(getter_AddRefs(destFile)); + if (NS_FAILED(rv)) return rv; + + nsAutoCString leafName; + rv = destFile->GetNativeLeafName(leafName); + if (NS_FAILED(rv)) return rv; + + nsAutoCString newName(leafName); + newName.AppendLiteral(".1"); + rv = destFile->SetNativeLeafName(newName); + if (NS_FAILED(rv)) return rv; + + rv = RunTest(srcFile, destFile); + NS_ASSERTION(NS_SUCCEEDED(rv), "RunTest failed"); + + newName = leafName; + newName.AppendLiteral(".2"); + rv = destFile->SetNativeLeafName(newName); + if (NS_FAILED(rv)) return rv; + + rv = RunBlockingTest(srcFile, destFile); + NS_ASSERTION(NS_SUCCEEDED(rv), "RunBlockingTest failed"); + + // give background threads a chance to finish whatever work they may + // be doing. + PR_Sleep(PR_SecondsToInterval(1)); + } // this scopes the nsCOMPtrs + // no nsCOMPtrs are allowed to be alive when you call NS_ShutdownXPCOM + rv = NS_ShutdownXPCOM(nullptr); + NS_ASSERTION(NS_SUCCEEDED(rv), "NS_ShutdownXPCOM failed"); + return NS_OK; +} diff --git a/netwerk/test/TestUDPSocket.cpp b/netwerk/test/TestUDPSocket.cpp new file mode 100644 index 0000000000..9236d2ea3a --- /dev/null +++ b/netwerk/test/TestUDPSocket.cpp @@ -0,0 +1,473 @@ +/* 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/. */ + +#include "TestCommon.h" +#include "TestHarness.h" +#include "nsIUDPSocket.h" +#include "nsISocketTransportService.h" +#include "nsISocketTransport.h" +#include "nsIOutputStream.h" +#include "nsIInputStream.h" +#include "nsINetAddr.h" +#include "nsIScriptSecurityManager.h" +#include "nsITimer.h" +#include "mozilla/net/DNS.h" +#ifdef XP_WIN +#include "mozilla/WindowsVersion.h" +#endif +#include "prerror.h" + +#define REQUEST 0x68656c6f +#define RESPONSE 0x6f6c6568 +#define MULTICAST_TIMEOUT 2000 + +#define EXPECT_SUCCESS(rv, ...) \ + PR_BEGIN_MACRO \ + if (NS_FAILED(rv)) { \ + fail(__VA_ARGS__); \ + return false; \ + } \ + PR_END_MACRO + + +#define EXPECT_FAILURE(rv, ...) \ + PR_BEGIN_MACRO \ + if (NS_SUCCEEDED(rv)) { \ + fail(__VA_ARGS__); \ + return false; \ + } \ + PR_END_MACRO + +#define REQUIRE_EQUAL(a, b, ...) \ + PR_BEGIN_MACRO \ + if (a != b) { \ + fail(__VA_ARGS__); \ + return false; \ + } \ + PR_END_MACRO + +enum TestPhase { + TEST_OUTPUT_STREAM, + TEST_SEND_API, + TEST_MULTICAST, + TEST_NONE +}; + +static TestPhase phase = TEST_NONE; + +static bool CheckMessageContent(nsIUDPMessage *aMessage, uint32_t aExpectedContent) +{ + nsCString data; + aMessage->GetData(data); + + const char* buffer = data.get(); + uint32_t len = data.Length(); + + FallibleTArray<uint8_t>& rawData = aMessage->GetDataAsTArray(); + uint32_t rawLen = rawData.Length(); + + if (len != rawLen) { + fail("Raw data length(%d) do not matches String data length(%d).", rawLen, len); + return false; + } + + for (uint32_t i = 0; i < len; i++) { + if (buffer[i] != rawData[i]) { + fail("Raw data(%s) do not matches String data(%s)", rawData.Elements() ,buffer); + return false; + } + } + + uint32_t input = 0; + for (uint32_t i = 0; i < len; i++) { + input += buffer[i] << (8 * i); + } + + if (len != sizeof(uint32_t) || input != aExpectedContent) + { + fail("Request 0x%x received, expected 0x%x", input, aExpectedContent); + return false; + } else { + passed("Request 0x%x received as expected", input); + return true; + } +} + +/* + * UDPClientListener: listens for incomming UDP packets + */ +class UDPClientListener : public nsIUDPSocketListener +{ +protected: + virtual ~UDPClientListener(); + +public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIUDPSOCKETLISTENER + nsresult mResult; +}; + +NS_IMPL_ISUPPORTS(UDPClientListener, nsIUDPSocketListener) + +UDPClientListener::~UDPClientListener() = default; + +NS_IMETHODIMP +UDPClientListener::OnPacketReceived(nsIUDPSocket* socket, nsIUDPMessage* message) +{ + mResult = NS_OK; + + uint16_t port; + nsCString ip; + nsCOMPtr<nsINetAddr> fromAddr; + message->GetFromAddr(getter_AddRefs(fromAddr)); + fromAddr->GetPort(&port); + fromAddr->GetAddress(ip); + passed("Packet received on client from %s:%d", ip.get(), port); + + if (TEST_SEND_API == phase && CheckMessageContent(message, REQUEST)) { + uint32_t count; + const uint32_t data = RESPONSE; + printf("*** Attempting to write response 0x%x to server by SendWithAddr...\n", RESPONSE); + mResult = socket->SendWithAddr(fromAddr, (const uint8_t*)&data, + sizeof(uint32_t), &count); + if (mResult == NS_OK && count == sizeof(uint32_t)) { + passed("Response written"); + } else { + fail("Response written"); + } + return NS_OK; + } else if (TEST_OUTPUT_STREAM != phase || !CheckMessageContent(message, RESPONSE)) { + mResult = NS_ERROR_FAILURE; + } + + // Notify thread + QuitPumpingEvents(); + return NS_OK; +} + +NS_IMETHODIMP +UDPClientListener::OnStopListening(nsIUDPSocket*, nsresult) +{ + QuitPumpingEvents(); + return NS_OK; +} + +/* + * UDPServerListener: listens for incomming UDP packets + */ +class UDPServerListener : public nsIUDPSocketListener +{ +protected: + virtual ~UDPServerListener(); + +public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIUDPSOCKETLISTENER + + nsresult mResult; +}; + +NS_IMPL_ISUPPORTS(UDPServerListener, nsIUDPSocketListener) + +UDPServerListener::~UDPServerListener() = default; + +NS_IMETHODIMP +UDPServerListener::OnPacketReceived(nsIUDPSocket* socket, nsIUDPMessage* message) +{ + mResult = NS_OK; + + uint16_t port; + nsCString ip; + nsCOMPtr<nsINetAddr> fromAddr; + message->GetFromAddr(getter_AddRefs(fromAddr)); + fromAddr->GetPort(&port); + fromAddr->GetAddress(ip); + passed("Packet received on server from %s:%d", ip.get(), port); + + if (TEST_OUTPUT_STREAM == phase && CheckMessageContent(message, REQUEST)) + { + nsCOMPtr<nsIOutputStream> outstream; + message->GetOutputStream(getter_AddRefs(outstream)); + + uint32_t count; + const uint32_t data = RESPONSE; + printf("*** Attempting to write response 0x%x to client by OutputStream...\n", RESPONSE); + mResult = outstream->Write((const char*)&data, sizeof(uint32_t), &count); + + if (mResult == NS_OK && count == sizeof(uint32_t)) { + passed("Response written"); + } else { + fail("Response written"); + } + return NS_OK; + } else if (TEST_MULTICAST == phase && CheckMessageContent(message, REQUEST)) { + mResult = NS_OK; + } else if (TEST_SEND_API != phase || !CheckMessageContent(message, RESPONSE)) { + mResult = NS_ERROR_FAILURE; + } + + // Notify thread + QuitPumpingEvents(); + return NS_OK; +} + +NS_IMETHODIMP +UDPServerListener::OnStopListening(nsIUDPSocket*, nsresult) +{ + QuitPumpingEvents(); + return NS_OK; +} + +/** + * Multicast timer callback: detects delivery failure + */ +class MulticastTimerCallback : public nsITimerCallback +{ +protected: + virtual ~MulticastTimerCallback(); + +public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSITIMERCALLBACK + + nsresult mResult; +}; + +NS_IMPL_ISUPPORTS(MulticastTimerCallback, nsITimerCallback) + +MulticastTimerCallback::~MulticastTimerCallback() = default; + +NS_IMETHODIMP +MulticastTimerCallback::Notify(nsITimer* timer) +{ + if (TEST_MULTICAST != phase) { + return NS_OK; + } + // Multicast ping failed + printf("Multicast ping timeout expired\n"); + mResult = NS_ERROR_FAILURE; + QuitPumpingEvents(); + return NS_OK; +} + +/**** Main ****/ +int +main(int32_t argc, char *argv[]) +{ + nsresult rv; + ScopedXPCOM xpcom("UDP ServerSocket"); + if (xpcom.failed()) + return -1; + + // Create UDPSocket + nsCOMPtr<nsIUDPSocket> server, client; + server = do_CreateInstance("@mozilla.org/network/udp-socket;1", &rv); + NS_ENSURE_SUCCESS(rv, -1); + client = do_CreateInstance("@mozilla.org/network/udp-socket;1", &rv); + NS_ENSURE_SUCCESS(rv, -1); + + // Create UDPServerListener to process UDP packets + RefPtr<UDPServerListener> serverListener = new UDPServerListener(); + + nsCOMPtr<nsIScriptSecurityManager> secman = + do_GetService(NS_SCRIPTSECURITYMANAGER_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, -1); + + nsCOMPtr<nsIPrincipal> systemPrincipal; + rv = secman->GetSystemPrincipal(getter_AddRefs(systemPrincipal)); + NS_ENSURE_SUCCESS(rv, -1); + + // Bind server socket to 0.0.0.0 + rv = server->Init(0, false, systemPrincipal, true, 0); + NS_ENSURE_SUCCESS(rv, -1); + int32_t serverPort; + server->GetPort(&serverPort); + server->AsyncListen(serverListener); + + // Bind clinet on arbitrary port + RefPtr<UDPClientListener> clientListener = new UDPClientListener(); + client->Init(0, false, systemPrincipal, true, 0); + client->AsyncListen(clientListener); + + // Write data to server + uint32_t count; + const uint32_t data = REQUEST; + + phase = TEST_OUTPUT_STREAM; + rv = client->Send(NS_LITERAL_CSTRING("127.0.0.1"), serverPort, (uint8_t*)&data, sizeof(uint32_t), &count); + NS_ENSURE_SUCCESS(rv, -1); + REQUIRE_EQUAL(count, sizeof(uint32_t), "Error"); + passed("Request written by Send"); + + // Wait for server + PumpEvents(); + NS_ENSURE_SUCCESS(serverListener->mResult, -1); + + // Read response from server + NS_ENSURE_SUCCESS(clientListener->mResult, -1); + + mozilla::net::NetAddr clientAddr; + rv = client->GetAddress(&clientAddr); + NS_ENSURE_SUCCESS(rv, -1); + // The client address is 0.0.0.0, but Windows won't receive packets there, so + // use 127.0.0.1 explicitly + clientAddr.inet.ip = PR_htonl(127 << 24 | 1); + + phase = TEST_SEND_API; + rv = server->SendWithAddress(&clientAddr, (uint8_t*)&data, sizeof(uint32_t), &count); + NS_ENSURE_SUCCESS(rv, -1); + REQUIRE_EQUAL(count, sizeof(uint32_t), "Error"); + passed("Request written by SendWithAddress"); + + // Wait for server + PumpEvents(); + NS_ENSURE_SUCCESS(serverListener->mResult, -1); + + // Read response from server + NS_ENSURE_SUCCESS(clientListener->mResult, -1); + + // Setup timer to detect multicast failure + nsCOMPtr<nsITimer> timer = do_CreateInstance("@mozilla.org/timer;1"); + if (NS_WARN_IF(!timer)) { + return -1; + } + RefPtr<MulticastTimerCallback> timerCb = new MulticastTimerCallback(); + + // The following multicast tests using multiple sockets require a firewall + // exception on Windows XP (the earliest version of Windows we now support) + // before they pass. For now, we'll skip them here. Later versions of Windows + // (Win2003 and onward) don't seem to have this issue. +#ifdef XP_WIN + if (!mozilla::IsWin2003OrLater()) { // i.e. if it is WinXP + goto close; + } +#endif + + // Join multicast group + printf("Joining multicast group\n"); + phase = TEST_MULTICAST; + mozilla::net::NetAddr multicastAddr; + multicastAddr.inet.family = AF_INET; + multicastAddr.inet.ip = PR_htonl(224 << 24 | 255); + multicastAddr.inet.port = PR_htons(serverPort); + rv = server->JoinMulticastAddr(multicastAddr, nullptr); + if (NS_WARN_IF(NS_FAILED(rv))) { + return -1; + } + + // Send multicast ping + timerCb->mResult = NS_OK; + timer->InitWithCallback(timerCb, MULTICAST_TIMEOUT, nsITimer::TYPE_ONE_SHOT); + rv = client->SendWithAddress(&multicastAddr, (uint8_t*)&data, sizeof(uint32_t), &count); + if (NS_WARN_IF(NS_FAILED(rv))) { + return -1; + } + REQUIRE_EQUAL(count, sizeof(uint32_t), "Error"); + passed("Multicast ping written by SendWithAddress"); + + // Wait for server to receive successfully + PumpEvents(); + if (NS_WARN_IF(NS_FAILED(serverListener->mResult))) { + return -1; + } + if (NS_WARN_IF(NS_FAILED(timerCb->mResult))) { + return -1; + } + timer->Cancel(); + passed("Server received ping successfully"); + + // Disable multicast loopback + printf("Disable multicast loopback\n"); + client->SetMulticastLoopback(false); + server->SetMulticastLoopback(false); + + // Send multicast ping + timerCb->mResult = NS_OK; + timer->InitWithCallback(timerCb, MULTICAST_TIMEOUT, nsITimer::TYPE_ONE_SHOT); + rv = client->SendWithAddress(&multicastAddr, (uint8_t*)&data, sizeof(uint32_t), &count); + if (NS_WARN_IF(NS_FAILED(rv))) { + return -1; + } + REQUIRE_EQUAL(count, sizeof(uint32_t), "Error"); + passed("Multicast ping written by SendWithAddress"); + + // Wait for server to fail to receive + PumpEvents(); + if (NS_WARN_IF(NS_SUCCEEDED(timerCb->mResult))) { + return -1; + } + timer->Cancel(); + passed("Server failed to receive ping correctly"); + + // Reset state + client->SetMulticastLoopback(true); + server->SetMulticastLoopback(true); + + // Change multicast interface + printf("Changing multicast interface\n"); + mozilla::net::NetAddr loopbackAddr; + loopbackAddr.inet.family = AF_INET; + loopbackAddr.inet.ip = PR_htonl(INADDR_LOOPBACK); + client->SetMulticastInterfaceAddr(loopbackAddr); + + // Send multicast ping + timerCb->mResult = NS_OK; + timer->InitWithCallback(timerCb, MULTICAST_TIMEOUT, nsITimer::TYPE_ONE_SHOT); + rv = client->SendWithAddress(&multicastAddr, (uint8_t*)&data, sizeof(uint32_t), &count); + if (NS_WARN_IF(NS_FAILED(rv))) { + return -1; + } + REQUIRE_EQUAL(count, sizeof(uint32_t), "Error"); + passed("Multicast ping written by SendWithAddress"); + + // Wait for server to fail to receive + PumpEvents(); + if (NS_WARN_IF(NS_SUCCEEDED(timerCb->mResult))) { + return -1; + } + timer->Cancel(); + passed("Server failed to receive ping correctly"); + + // Reset state + mozilla::net::NetAddr anyAddr; + anyAddr.inet.family = AF_INET; + anyAddr.inet.ip = PR_htonl(INADDR_ANY); + client->SetMulticastInterfaceAddr(anyAddr); + + // Leave multicast group + printf("Leave multicast group\n"); + rv = server->LeaveMulticastAddr(multicastAddr, nullptr); + if (NS_WARN_IF(NS_FAILED(rv))) { + return -1; + } + + // Send multicast ping + timerCb->mResult = NS_OK; + timer->InitWithCallback(timerCb, MULTICAST_TIMEOUT, nsITimer::TYPE_ONE_SHOT); + rv = client->SendWithAddress(&multicastAddr, (uint8_t*)&data, sizeof(uint32_t), &count); + if (NS_WARN_IF(NS_FAILED(rv))) { + return -1; + } + REQUIRE_EQUAL(count, sizeof(uint32_t), "Error"); + passed("Multicast ping written by SendWithAddress"); + + // Wait for server to fail to receive + PumpEvents(); + if (NS_WARN_IF(NS_SUCCEEDED(timerCb->mResult))) { + return -1; + } + timer->Cancel(); + passed("Server failed to receive ping correctly"); + goto close; + +close: + // Close server + printf("*** Attempting to close server ...\n"); + server->Close(); + client->Close(); + PumpEvents(); + passed("Server closed"); + + return 0; // failure is a non-zero return +} diff --git a/netwerk/test/TestUDPSocketProvider.cpp b/netwerk/test/TestUDPSocketProvider.cpp new file mode 100644 index 0000000000..921e610ab6 --- /dev/null +++ b/netwerk/test/TestUDPSocketProvider.cpp @@ -0,0 +1,159 @@ +/* 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/. */ + +#include "stdio.h" +#include "TestCommon.h" +#include "nsCOMPtr.h" +#include "nsIServiceManager.h" +#include "nsIComponentRegistrar.h" +#include "nspr.h" +#include "nsServiceManagerUtils.h" +#include "nsISocketTransportService.h" +#include "nsISocketTransport.h" +#include "nsIOutputStream.h" +#include "nsIInputStream.h" + +#define UDP_PORT 9050 + +#define UDP_ASSERT(condition, message) \ + PR_BEGIN_MACRO \ + NS_ASSERTION(condition, message); \ + if (!(condition)) { \ + returnCode = -1; \ + break; \ + } \ + PR_END_MACRO + +#define UDP_ASSERT_PRSTATUS(message) \ + PR_BEGIN_MACRO \ + NS_ASSERTION(status == PR_SUCCESS, message); \ + if (status != PR_SUCCESS) { \ + PRErrorCode err = PR_GetError(); \ + fprintf(stderr, \ + "FAIL nspr: %s: (%08x) %s\n", \ + message, \ + err, \ + PR_ErrorToString(err, PR_LANGUAGE_I_DEFAULT)); \ + returnCode = -1; \ + break; \ + } \ + PR_END_MACRO + +#define UDP_ASSERT_NSRESULT(message) \ + PR_BEGIN_MACRO \ + NS_ASSERTION(NS_SUCCEEDED(rv), message); \ + if (NS_FAILED(rv)) { \ + fprintf(stderr, "FAIL UDPSocket: %s: %08x\n", \ + message, rv); \ + returnCode = -1; \ + break; \ + } \ + PR_END_MACRO + +int +main(int argc, char* argv[]) +{ + if (test_common_init(&argc, &argv) != 0) + return -1; + + int returnCode = 0; + nsresult rv = NS_OK; + PRFileDesc *serverFD = nullptr; + + do { // act both as a scope for nsCOMPtrs to be released before XPCOM + // shutdown, as well as a easy way to abort the test + PRStatus status = PR_SUCCESS; + + nsCOMPtr<nsIServiceManager> servMan; + NS_InitXPCOM2(getter_AddRefs(servMan), nullptr, nullptr); + nsCOMPtr<nsIComponentRegistrar> registrar = do_QueryInterface(servMan); + UDP_ASSERT(registrar, "Null nsIComponentRegistrar"); + if (registrar) + registrar->AutoRegister(nullptr); + + // listen for a incoming UDP connection on localhost + serverFD = PR_OpenUDPSocket(PR_AF_INET); + UDP_ASSERT(serverFD, "Cannot open UDP socket for listening"); + + PRSocketOptionData socketOptions; + socketOptions.option = PR_SockOpt_Nonblocking; + socketOptions.value.non_blocking = false; + status = PR_SetSocketOption(serverFD, &socketOptions); + UDP_ASSERT_PRSTATUS("Failed to set server socket as blocking"); + + PRNetAddr addr; + status = PR_InitializeNetAddr(PR_IpAddrLoopback, UDP_PORT, &addr); + UDP_ASSERT_PRSTATUS("Failed to initialize loopback address"); + + status = PR_Bind(serverFD, &addr); + UDP_ASSERT_PRSTATUS("Failed to bind server socket"); + + // dummy IOService to get around bug 379890 + nsCOMPtr<nsISupports> ios = + do_GetService("@mozilla.org/network/io-service;1"); + + // and have a matching UDP connection for the client + nsCOMPtr<nsISocketTransportService> sts = + do_GetService("@mozilla.org/network/socket-transport-service;1", &rv); + UDP_ASSERT_NSRESULT("Cannot get socket transport service"); + + nsCOMPtr<nsISocketTransport> transport; + const char *protocol = "udp"; + rv = sts->CreateTransport(&protocol, 1, NS_LITERAL_CSTRING("localhost"), + UDP_PORT, nullptr, getter_AddRefs(transport)); + UDP_ASSERT_NSRESULT("Cannot create transport"); + + uint32_t count, read; + const uint32_t data = 0xFF0056A9; + + // write to the output stream + nsCOMPtr<nsIOutputStream> outstream; + rv = transport->OpenOutputStream(nsITransport::OPEN_BLOCKING, + 0, 0, getter_AddRefs(outstream)); + UDP_ASSERT_NSRESULT("Cannot open output stream"); + + rv = outstream->Write((const char*)&data, sizeof(uint32_t), &count); + UDP_ASSERT_NSRESULT("Cannot write to output stream"); + UDP_ASSERT(count == sizeof(uint32_t), + "Did not write enough bytes to output stream"); + + // read from NSPR to check it's the same + count = PR_RecvFrom(serverFD, &read, sizeof(uint32_t), 0, &addr, 1); + UDP_ASSERT(count == sizeof(uint32_t), + "Did not read enough bytes from NSPR"); + status = (read == data ? PR_SUCCESS : PR_FAILURE); + UDP_ASSERT_PRSTATUS("Did not read expected data from NSPR"); + + // write to NSPR + count = PR_SendTo(serverFD, &data, sizeof(uint32_t), 0, &addr, 1); + status = (count == sizeof(uint32_t) ? PR_SUCCESS : PR_FAILURE); + UDP_ASSERT_PRSTATUS("Did not write enough bytes to NSPR"); + + // read from stream + nsCOMPtr<nsIInputStream> instream; + rv = transport->OpenInputStream(nsITransport::OPEN_BLOCKING, + 0, 0, getter_AddRefs(instream)); + UDP_ASSERT_NSRESULT("Cannot open input stream"); + + rv = instream->Read((char*)&read, sizeof(uint32_t), &count); + UDP_ASSERT_NSRESULT("Cannot read from input stream"); + UDP_ASSERT(count == sizeof(uint32_t), + "Did not read enough bytes from input stream"); + UDP_ASSERT(read == data, "Did not read expected data from stream"); + + } while (false); // release all XPCOM things + if (serverFD) { + PRStatus status = PR_Close(serverFD); + if (status != PR_SUCCESS) { + PRErrorCode err = PR_GetError(); + fprintf(stderr, "FAIL: Cannot close server: (%08x) %s\n", + err, PR_ErrorToString(err, PR_LANGUAGE_I_DEFAULT)); + } + } + rv = NS_ShutdownXPCOM(nullptr); + NS_ASSERTION(NS_SUCCEEDED(rv), "NS_ShutdownXPCOM failed"); + + return returnCode; +} + diff --git a/netwerk/test/TestURLManipulation.html b/netwerk/test/TestURLManipulation.html new file mode 100644 index 0000000000..fd58652b7b --- /dev/null +++ b/netwerk/test/TestURLManipulation.html @@ -0,0 +1,130 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> +<html> +<head> + <title>URL manipulation</title> + <meta http-equiv="content-type" content="text/html; charset=ISO-8859-1"> + + <script type="text/javascript"> + var gIOService = null; + function getIOService() + { + if (gIOService) + return gIOService; + + try { + gIOService = Components.classes["@mozilla.org/network/io-service;1"] + .getService(Components.interfaces.nsIIOService); + } catch(e) { dump("problem creating nsIURL for: "+inURLString+"\n"); } + + return gIOService; + } + + function getnsIURL(inURLString) + { + var URL = null; + var ioserv = getIOService(); + try { + var URI = ioserv.newURI(inURLString, "", null); + URL = URI.QueryInterface(Components.interfaces.nsIURL); + } catch(e) { dump("problem creating nsIURL for: "+inURLString+"\n"); } + return URL; + } + + function getCommonSpec() + { + var URL1 = getnsIURL(document.foo.baseEdit.value); + var URL2 = getnsIURL(document.foo.compareEdit.value); + var result = ""; + try { + result = URL1.getCommonBaseSpec(URL2); + } catch(e) { dump("problem with getCommonSpec ("+e+")\n"); } + document.foo.resultEdit.value = result; + } + + function getRelativeSpec() + { + var URL1 = getnsIURL(document.foo.baseEdit.value); + var URL2 = getnsIURL(document.foo.compareEdit.value); + var result = ""; + try { + result = URL1.getRelativeSpec(URL2); + } catch(e) { dump("problem with getRelativeSpec ("+e+")\n"); } + document.foo.resultEdit.value = result; + } + + function doResolve() + { + var URL = getnsIURL(document.foo.baseEdit.value); + var result = ""; + try { + result = URL.resolve(document.foo.resultEdit.value); + } catch(e) { dump("problem with getRelativeSpec ("+e+")\n"); } + document.foo.compareEdit.value = result; + } + </script> +</head> +<body> +<h1>testing of URL manipulation:</h1> +<p> + <form name="foo"> + <p> + <label for="baseEdit">base url (absolute)</label><br> + <input type="input" name="baseEdit" size="80" value="http://www.mozilla.org/"> + + <p> + <label for="compareEdit">comparison uri (absolute)</label><br> + <input type="input" name="compareEdit" size="80"> + + <p> + <label for="resultEdit">resolved url</label><br> + <input type="input" name="resultEdit" size="80"> + + <p> + <input type="button" onclick="getCommonSpec();" value="Get Common Spec"> + <input type="button" onclick="getRelativeSpec();" value="Get Relative Spec"> + <input type="button" onclick="doResolve();" value="Resolve"> + <h5> note: results from "resolve" are placed in "comparison uri" edit field</h5> + </form> +<p> +<br> + +<h3>notes for testing</h3> +different types of uris:<br> +<ul> + <li>about:</li> + <li>about:blank</li> + <li>mailbox://nsmail-2.mcom.com/xxx</li> + <li>mailto:brade@netscape.com)</li> + <li>junk</li> + <li>http://foo/</li> + <li>http://foo.com/</li> + <li>https://foo.com/</li> + <li>ftp://ftp.mozilla.org/</li> + <li>http://foo.com:8080/</li> + <li>http://brade@foo.com/</li> + <li>http://brade:password@foo.com/</li> + <li>http://brade:@foo.com:8080/</li> + <li>file:///</li> + <li>file:///Quest/Desktop%20Folder/test.html</li> +</ul> +other variations:<br> +<ul> + <li>sub-directories on above list</li> + <li>files on above list</li> + <li>sub-directories and files on above list<br> + </li> + <li>directories which don't end in a '/'</li> + <li>files with queries</li> + <li>files with no extension</li> + <li>files with references</li> + <li>files with params</li> + <li>other schemes (chrome, ldap, news, finger, etc.)<br> + </li> +</ul> +<br> +This should be true:<br> + resultString = baseURL.getRelativeSpec(URL);<br> +<==><br> + baseURL.resolve(resultString) == URL.spec;<br> +</body> +</html> diff --git a/netwerk/test/TestURLParser.cpp b/netwerk/test/TestURLParser.cpp new file mode 100644 index 0000000000..43f126e727 --- /dev/null +++ b/netwerk/test/TestURLParser.cpp @@ -0,0 +1,135 @@ +#include "TestCommon.h" +#include <stdio.h> +#include "nsIURLParser.h" +#include "nsCOMPtr.h" +#include "nsIServiceManager.h" +#include "nsNetCID.h" +#include "nsServiceManagerUtils.h" + +static void +print_field(const char *label, char *str, int32_t len) +{ + char c = str[len]; + str[len] = '\0'; + printf("[%s=%s]\n", label, str); + str[len] = c; +} + +#define PRINT_FIELD(x) \ + print_field(# x, x, x ## Len) + +#define PRINT_SUBFIELD(base, x) \ + PR_BEGIN_MACRO \ + if (x ## Len != -1) \ + print_field(# x, base + x ## Pos, x ## Len); \ + PR_END_MACRO + +static void +parse_authority(nsIURLParser *urlParser, char *auth, int32_t authLen) +{ + PRINT_FIELD(auth); + + uint32_t usernamePos, passwordPos; + int32_t usernameLen, passwordLen; + uint32_t hostnamePos; + int32_t hostnameLen, port; + + urlParser->ParseAuthority(auth, authLen, + &usernamePos, &usernameLen, + &passwordPos, &passwordLen, + &hostnamePos, &hostnameLen, + &port); + + PRINT_SUBFIELD(auth, username); + PRINT_SUBFIELD(auth, password); + PRINT_SUBFIELD(auth, hostname); + if (port != -1) + printf("[port=%d]\n", port); +} + +static void +parse_file_path(nsIURLParser *urlParser, char *filepath, int32_t filepathLen) +{ + PRINT_FIELD(filepath); + + uint32_t dirPos, basePos, extPos; + int32_t dirLen, baseLen, extLen; + + urlParser->ParseFilePath(filepath, filepathLen, + &dirPos, &dirLen, + &basePos, &baseLen, + &extPos, &extLen); + + PRINT_SUBFIELD(filepath, dir); + PRINT_SUBFIELD(filepath, base); + PRINT_SUBFIELD(filepath, ext); +} + +static void +parse_path(nsIURLParser *urlParser, char *path, int32_t pathLen) +{ + PRINT_FIELD(path); + + uint32_t filePos, queryPos, refPos; + int32_t fileLen, queryLen, refLen; + + urlParser->ParsePath(path, pathLen, + &filePos, &fileLen, + &queryPos, &queryLen, + &refPos, &refLen); + + if (fileLen != -1) + parse_file_path(urlParser, path + filePos, fileLen); + PRINT_SUBFIELD(path, query); + PRINT_SUBFIELD(path, ref); +} + +int +main(int argc, char **argv) +{ + if (test_common_init(&argc, &argv) != 0) + return -1; + + if (argc < 2) { + printf("usage: TestURLParser [-std|-noauth|-auth] <url>\n"); + return -1; + } + nsCOMPtr<nsIURLParser> urlParser; + if (strcmp(argv[1], "-noauth") == 0) { + urlParser = do_GetService(NS_NOAUTHURLPARSER_CONTRACTID); + argv[1] = argv[2]; + } + else if (strcmp(argv[1], "-auth") == 0) { + urlParser = do_GetService(NS_AUTHURLPARSER_CONTRACTID); + argv[1] = argv[2]; + } + else { + urlParser = do_GetService(NS_STDURLPARSER_CONTRACTID); + if (strcmp(argv[1], "-std") == 0) + argv[1] = argv[2]; + else + printf("assuming -std\n"); + } + if (urlParser) { + printf("have urlParser @%p\n", static_cast<void*>(urlParser.get())); + + char *spec = argv[1]; + uint32_t schemePos, authPos, pathPos; + int32_t schemeLen, authLen, pathLen; + + urlParser->ParseURL(spec, -1, + &schemePos, &schemeLen, + &authPos, &authLen, + &pathPos, &pathLen); + + if (schemeLen != -1) + PRINT_SUBFIELD(spec, scheme); + if (authLen != -1) + parse_authority(urlParser, spec + authPos, authLen); + if (pathLen != -1) + parse_path(urlParser, spec + pathPos, pathLen); + } + else + printf("no urlParser\n"); + return 0; +} diff --git a/netwerk/test/TestUpload.cpp b/netwerk/test/TestUpload.cpp new file mode 100644 index 0000000000..5818a5ccb0 --- /dev/null +++ b/netwerk/test/TestUpload.cpp @@ -0,0 +1,169 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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/. */ + +#include "TestCommon.h" +#include <algorithm> +#ifdef WIN32 +#include <windows.h> +#endif + +#include "nsIComponentRegistrar.h" +#include "nsIScriptSecurityManager.h" +#include "nsServiceManagerUtils.h" +#include "nsIServiceManager.h" +#include "nsNetUtil.h" + +#include "nsIUploadChannel.h" + +#include "NetwerkTestLogging.h" +// +// set NSPR_LOG_MODULES=Test:5 +// +static PRLogModuleInfo *gTestLog = nullptr; +#define LOG(args) MOZ_LOG(gTestLog, mozilla::LogLevel::Debug, args) + +//----------------------------------------------------------------------------- +// InputTestConsumer +//----------------------------------------------------------------------------- + +class InputTestConsumer : public nsIStreamListener +{ + virtual ~InputTestConsumer(); + +public: + + InputTestConsumer(); + + NS_DECL_ISUPPORTS + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSISTREAMLISTENER +}; + +InputTestConsumer::InputTestConsumer() +{ +} + +InputTestConsumer::~InputTestConsumer() = default; + +NS_IMPL_ISUPPORTS(InputTestConsumer, + nsIStreamListener, + nsIRequestObserver) + +NS_IMETHODIMP +InputTestConsumer::OnStartRequest(nsIRequest *request, nsISupports* context) +{ + LOG(("InputTestConsumer::OnStartRequest\n")); + return NS_OK; +} + +NS_IMETHODIMP +InputTestConsumer::OnDataAvailable(nsIRequest *request, + nsISupports* context, + nsIInputStream *aIStream, + uint64_t aSourceOffset, + uint32_t aLength) +{ + char buf[1025]; + uint32_t amt, size; + nsresult rv; + + while (aLength) { + size = std::min<uint32_t>(aLength, sizeof(buf)); + rv = aIStream->Read(buf, size, &amt); + if (NS_FAILED(rv)) { + NS_ASSERTION((NS_BASE_STREAM_WOULD_BLOCK != rv), + "The stream should never block."); + return rv; + } + aLength -= amt; + } + return NS_OK; +} + + +NS_IMETHODIMP +InputTestConsumer::OnStopRequest(nsIRequest *request, nsISupports* context, + nsresult aStatus) +{ + LOG(("InputTestConsumer::OnStopRequest [status=%x]\n", aStatus)); + QuitPumpingEvents(); + return NS_OK; +} + + +int +main(int argc, char* argv[]) +{ + if (test_common_init(&argc, &argv) != 0) + return -1; + + nsresult rv; + + if (argc < 2) { + printf("usage: %s <url> <file-to-upload>\n", argv[0]); + return -1; + } + char* uriSpec = argv[1]; + char* fileName = argv[2]; + + gTestLog = PR_NewLogModule("Test"); + + { + nsCOMPtr<nsIServiceManager> servMan; + NS_InitXPCOM2(getter_AddRefs(servMan), nullptr, nullptr); + + // first thing to do is create ourselves a stream that + // is to be uploaded. + nsCOMPtr<nsIInputStream> uploadStream; + rv = NS_NewPostDataStream(getter_AddRefs(uploadStream), + true, + nsDependentCString(fileName)); // XXX UTF-8 + if (NS_FAILED(rv)) return -1; + + // create our url. + nsCOMPtr<nsIURI> uri; + rv = NS_NewURI(getter_AddRefs(uri), uriSpec); + if (NS_FAILED(rv)) return -1; + + nsCOMPtr<nsIScriptSecurityManager> secman = + do_GetService(NS_SCRIPTSECURITYMANAGER_CONTRACTID, &rv); + if (NS_FAILED(rv)) return -1; + nsCOMPtr<nsIPrincipal> systemPrincipal; + rv = secman->GetSystemPrincipal(getter_AddRefs(systemPrincipal)); + if (NS_FAILED(rv)) return -1; + + nsCOMPtr<nsIChannel> channel; + rv = NS_NewChannel(getter_AddRefs(channel), + uri, + systemPrincipal, + nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_DATA_INHERITS, + nsIContentPolicy::TYPE_OTHER); + if (NS_FAILED(rv)) return -1; + + // QI and set the upload stream + nsCOMPtr<nsIUploadChannel> uploadChannel(do_QueryInterface(channel)); + uploadChannel->SetUploadStream(uploadStream, EmptyCString(), -1); + + // create a dummy listener + InputTestConsumer* listener; + + listener = new InputTestConsumer; + if (!listener) { + NS_ERROR("Failed to create a new stream listener!"); + return -1; + } + NS_ADDREF(listener); + + channel->AsyncOpen2(listener); + + PumpEvents(); + } // this scopes the nsCOMPtrs + // no nsCOMPtrs are allowed to be alive when you call NS_ShutdownXPCOM + rv = NS_ShutdownXPCOM(nullptr); + NS_ASSERTION(NS_SUCCEEDED(rv), "NS_ShutdownXPCOM failed"); + + return 0; +} + diff --git a/netwerk/test/TestWriteSpeed.cpp b/netwerk/test/TestWriteSpeed.cpp new file mode 100644 index 0000000000..0b0260fdd5 --- /dev/null +++ b/netwerk/test/TestWriteSpeed.cpp @@ -0,0 +1,116 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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/. */ + +#include "prio.h" +#include "prinrval.h" +#include "prmem.h" +#include <stdio.h> +#include <math.h> + +void +NS_MeanAndStdDev(double n, double sumOfValues, double sumOfSquaredValues, + double *meanResult, double *stdDevResult) +{ + double mean = 0.0, var = 0.0, stdDev = 0.0; + if (n > 0.0 && sumOfValues >= 0) { + mean = sumOfValues / n; + double temp = (n * sumOfSquaredValues) - (sumOfValues * sumOfValues); + if (temp < 0.0 || n <= 1) + var = 0.0; + else + var = temp / (n * (n - 1)); + // for some reason, Windows says sqrt(0.0) is "-1.#J" (?!) so do this: + stdDev = var != 0.0 ? sqrt(var) : 0.0; + } + *meanResult = mean; + *stdDevResult = stdDev; +} + +int +Test(const char* filename, int32_t minSize, int32_t maxSize, + int32_t sizeIncrement, int32_t iterations) +{ + fprintf(stdout, " size write: mean stddev iters total: mean stddev iters\n"); + for (int32_t size = minSize; size <= maxSize; size += sizeIncrement) { + // create a buffer of stuff to write + char* buf = (char*)PR_Malloc(size); + if (buf == nullptr) + return -1; + + // initialize it with a pattern + int32_t i; + char hex[] = "0123456789ABCDEF"; + for (i = 0; i < size; i++) { + buf[i] = hex[i & 0xF]; + } + + double writeCount = 0, writeRate = 0, writeRateSquared = 0; + double totalCount = 0, totalRate = 0, totalRateSquared = 0; + for (i = 0; i < iterations; i++) { + PRIntervalTime start = PR_IntervalNow(); + + char name[1024]; + sprintf(name, "%s_%d", filename, i); + PRFileDesc* fd = PR_Open(name, PR_WRONLY | PR_CREATE_FILE | PR_TRUNCATE, 0664); + if (fd == nullptr) + return -1; + + PRIntervalTime writeStart = PR_IntervalNow(); + int32_t rv = PR_Write(fd, buf, size); + if (rv < 0) return rv; + if (rv != size) return -1; + PRIntervalTime writeStop = PR_IntervalNow(); + + PRStatus st = PR_Close(fd); + if (st == PR_FAILURE) return -1; + + PRIntervalTime stop = PR_IntervalNow(); + + PRIntervalTime writeTime = PR_IntervalToMilliseconds(writeStop - writeStart); + if (writeTime > 0) { + double wr = size / writeTime; + writeRate += wr; + writeRateSquared += wr * wr; + writeCount++; + } + + PRIntervalTime totalTime = PR_IntervalToMilliseconds(stop - start); + if (totalTime > 0) { + double t = size / totalTime; + totalRate += t; + totalRateSquared += t * t; + totalCount++; + } + } + + PR_Free(buf); + + double writeMean, writeStddev; + double totalMean, totalStddev; + NS_MeanAndStdDev(writeCount, writeRate, writeRateSquared, + &writeMean, &writeStddev); + NS_MeanAndStdDev(totalCount, totalRate, totalRateSquared, + &totalMean, &totalStddev); + fprintf(stdout, "%10d %10.2f %10.2f %10d %10.2f %10.2f %10d\n", + size, writeMean, writeStddev, (int32_t)writeCount, + totalMean, totalStddev, (int32_t)totalCount); + } + return 0; +} + +int +main(int argc, char* argv[]) +{ + if (argc != 5) { + printf("usage: %s <min buf size (K)> <max buf size (K)> <size increment (K)> <iterations>\n", argv[0]); + return -1; + } + Test("y:\\foo", + atoi(argv[1]) * 1024, + atoi(argv[2]) * 1024, + atoi(argv[3]) * 1024, + atoi(argv[4])); + return 0; +} diff --git a/netwerk/test/browser/browser.ini b/netwerk/test/browser/browser.ini new file mode 100644 index 0000000000..8611891fd7 --- /dev/null +++ b/netwerk/test/browser/browser.ini @@ -0,0 +1,11 @@ +[DEFAULT] +support-files = + dummy.html + +[browser_about_cache.js] +[browser_NetUtil.js] +[browser_child_resource.js] +skip-if = e10s && debug && os == "linux" && bits == 64 +[browser_post_file.js] +[browser_nsIFormPOSTActionChannel.js] +skip-if = e10s # protocol handler and channel does not work in content process diff --git a/netwerk/test/browser/browser_NetUtil.js b/netwerk/test/browser/browser_NetUtil.js new file mode 100644 index 0000000000..a6c4f2bcd6 --- /dev/null +++ b/netwerk/test/browser/browser_NetUtil.js @@ -0,0 +1,92 @@ +/* +Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ +*/ + +Components.utils.import("resource://gre/modules/NetUtil.jsm"); + +function test() { + waitForExplicitFinish(); + + // We overload this test to include verifying that httpd.js is + // importable as a testing-only JS module. + Components.utils.import("resource://testing-common/httpd.js", {}); + + nextTest(); +} + +function nextTest() { + if (tests.length) + executeSoon(tests.shift()); + else + executeSoon(finish); +} + +var tests = [ + test_asyncFetchBadCert, +]; + +function test_asyncFetchBadCert() { + // Try a load from an untrusted cert, with errors supressed + NetUtil.asyncFetch({ + uri: "https://untrusted.example.com", + loadUsingSystemPrincipal: true + }, function (aInputStream, aStatusCode, aRequest) { + ok(!Components.isSuccessCode(aStatusCode), "request failed"); + ok(aRequest instanceof Ci.nsIHttpChannel, "request is an nsIHttpChannel"); + + // Now try again with a channel whose notificationCallbacks doesn't suprress errors + let channel = NetUtil.newChannel({ + uri: "https://untrusted.example.com", + loadUsingSystemPrincipal: true}); + channel.notificationCallbacks = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIProgressEventSink, + Ci.nsIInterfaceRequestor]), + getInterface: function (aIID) { return this.QueryInterface(aIID); }, + onProgress: function () {}, + onStatus: function () {} + }; + NetUtil.asyncFetch(channel, function (aInputStream, aStatusCode, aRequest) { + ok(!Components.isSuccessCode(aStatusCode), "request failed"); + ok(aRequest instanceof Ci.nsIHttpChannel, "request is an nsIHttpChannel"); + + // Now try a valid request + NetUtil.asyncFetch({ + uri: "https://example.com", + loadUsingSystemPrincipal: true + }, function (aInputStream, aStatusCode, aRequest) { + info("aStatusCode for valid request: " + aStatusCode); + ok(Components.isSuccessCode(aStatusCode), "request succeeded"); + ok(aRequest instanceof Ci.nsIHttpChannel, "request is an nsIHttpChannel"); + ok(aRequest.requestSucceeded, "HTTP request succeeded"); + + nextTest(); + }); + }); + }); +} + +function WindowListener(aURL, aCallback) { + this.callback = aCallback; + this.url = aURL; +} +WindowListener.prototype = { + onOpenWindow: function(aXULWindow) { + var domwindow = aXULWindow.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindow); + var self = this; + domwindow.addEventListener("load", function() { + domwindow.removeEventListener("load", arguments.callee, false); + + if (domwindow.document.location.href != self.url) + return; + + // Allow other window load listeners to execute before passing to callback + executeSoon(function() { + self.callback(domwindow); + }); + }, false); + }, + onCloseWindow: function(aXULWindow) {}, + onWindowTitleChange: function(aXULWindow, aNewTitle) {} +} diff --git a/netwerk/test/browser/browser_about_cache.js b/netwerk/test/browser/browser_about_cache.js new file mode 100644 index 0000000000..38cfa3d028 --- /dev/null +++ b/netwerk/test/browser/browser_about_cache.js @@ -0,0 +1,71 @@ +"use strict"; + +/** + * Open a dummy page, then open about:cache and verify the opened page shows up in the cache. + */ +add_task(function*() { + const kRoot = getRootDirectory(gTestPath).replace("chrome://mochitests/content/", + "https://example.com/"); + const kTestPage = kRoot + "dummy.html"; + // Open the dummy page to get it cached. + let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, kTestPage, true); + yield BrowserTestUtils.removeTab(tab); + + tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:cache", true); + let expectedPageCheck = function(uri) { + info("Saw load for " + uri); + // Can't easily use searchParms and new URL() because it's an about: URI... + return uri.startsWith("about:cache?") && uri.includes("storage=disk"); + }; + let diskPageLoaded = BrowserTestUtils.browserLoaded(tab.linkedBrowser, false, expectedPageCheck); + yield ContentTask.spawn(tab.linkedBrowser, null, function() { + ok(!content.document.nodePrincipal.isSystemPrincipal, + "about:cache should not have system principal"); + let principalURI = content.document.nodePrincipal.URI; + let channel = content.document.docShell.currentDocumentChannel; + ok(!channel.loadInfo.loadingPrincipal, "Loading principal should be null."); + is(principalURI && principalURI.spec, content.document.location.href, "Principal matches location"); + let links = [... content.document.querySelectorAll("a[href*=disk]")]; + is(links.length, 1, "Should have 1 link to the disk entries"); + links[0].click(); + }); + yield diskPageLoaded; + info("about:cache disk subpage loaded"); + + expectedPageCheck = function(uri) { + info("Saw load for " + uri); + return uri.startsWith("about:cache-entry") && uri.includes("dummy.html"); + }; + let triggeringURISpec = tab.linkedBrowser.currentURI.spec; + let entryLoaded = BrowserTestUtils.browserLoaded(tab.linkedBrowser, false, expectedPageCheck); + yield ContentTask.spawn(tab.linkedBrowser, kTestPage, function(kTestPage) { + ok(!content.document.nodePrincipal.isSystemPrincipal, + "about:cache with query params should still not have system principal"); + let principalURI = content.document.nodePrincipal.URI; + is(principalURI && principalURI.spec, content.document.location.href, "Principal matches location"); + let channel = content.document.docShell.currentDocumentChannel; + principalURI = channel.loadInfo.triggeringPrincipal.URI; + is(principalURI && principalURI.spec, "about:cache", "Triggering principal matches previous location"); + ok(!channel.loadInfo.loadingPrincipal, "Loading principal should be null."); + let links = [... content.document.querySelectorAll("a[href*='" + kTestPage + "']")]; + is(links.length, 1, "Should have 1 link to the entry for " + kTestPage); + links[0].click(); + }); + yield entryLoaded; + info("about:cache entry loaded"); + + + yield ContentTask.spawn(tab.linkedBrowser, triggeringURISpec, function(triggeringURISpec) { + ok(!content.document.nodePrincipal.isSystemPrincipal, + "about:cache-entry should also not have system principal"); + let principalURI = content.document.nodePrincipal.URI; + is(principalURI && principalURI.spec, content.document.location.href, "Principal matches location"); + let channel = content.document.docShell.currentDocumentChannel; + principalURI = channel.loadInfo.triggeringPrincipal.URI; + is(principalURI && principalURI.spec, triggeringURISpec, "Triggering principal matches previous location"); + ok(!channel.loadInfo.loadingPrincipal, "Loading principal should be null."); + ok(content.document.querySelectorAll("th").length, + "Should have several table headers with data."); + }); + yield BrowserTestUtils.removeTab(tab); +}); diff --git a/netwerk/test/browser/browser_child_resource.js b/netwerk/test/browser/browser_child_resource.js new file mode 100644 index 0000000000..098e6bd844 --- /dev/null +++ b/netwerk/test/browser/browser_child_resource.js @@ -0,0 +1,256 @@ +/* +Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ +*/ + +// This must be loaded in the remote process for this test to be useful +const TEST_URL = "http://example.com/browser/netwerk/test/browser/dummy.html"; + +const expectedRemote = gMultiProcessBrowser ? "true" : ""; + +Components.utils.import("resource://gre/modules/Services.jsm"); +const resProtocol = Cc["@mozilla.org/network/protocol;1?name=resource"] + .getService(Ci.nsIResProtocolHandler); + +function getMinidumpDirectory() { + var dir = Services.dirsvc.get('ProfD', Ci.nsIFile); + dir.append("minidumps"); + return dir; +} + +// This observer is needed so we can clean up all evidence of the crash so +// the testrunner thinks things are peachy. +var CrashObserver = { + observe: function(subject, topic, data) { + is(topic, 'ipc:content-shutdown', 'Received correct observer topic.'); + ok(subject instanceof Ci.nsIPropertyBag2, + 'Subject implements nsIPropertyBag2.'); + // we might see this called as the process terminates due to previous tests. + // We are only looking for "abnormal" exits... + if (!subject.hasKey("abnormal")) { + info("This is a normal termination and isn't the one we are looking for..."); + return; + } + + var dumpID; + if ('nsICrashReporter' in Ci) { + dumpID = subject.getPropertyAsAString('dumpID'); + ok(dumpID, "dumpID is present and not an empty string"); + } + + if (dumpID) { + var minidumpDirectory = getMinidumpDirectory(); + let file = minidumpDirectory.clone(); + file.append(dumpID + '.dmp'); + file.remove(true); + file = minidumpDirectory.clone(); + file.append(dumpID + '.extra'); + file.remove(true); + } + } +} +Services.obs.addObserver(CrashObserver, 'ipc:content-shutdown', false); + +registerCleanupFunction(() => { + Services.obs.removeObserver(CrashObserver, 'ipc:content-shutdown'); +}); + +function frameScript() { + Components.utils.import("resource://gre/modules/Services.jsm"); + let resProtocol = Components.classes["@mozilla.org/network/protocol;1?name=resource"] + .getService(Components.interfaces.nsIResProtocolHandler); + + addMessageListener("Test:ResolveURI", function({ data: uri }) { + uri = Services.io.newURI(uri, null, null); + try { + let resolved = resProtocol.resolveURI(uri); + sendAsyncMessage("Test:ResolvedURI", resolved); + } + catch (e) { + sendAsyncMessage("Test:ResolvedURI", null); + } + }); + + addMessageListener("Test:Crash", function() { + dump("Crashing\n"); + privateNoteIntentionalCrash(); + Components.utils.import("resource://gre/modules/ctypes.jsm"); + let zero = new ctypes.intptr_t(8); + let badptr = ctypes.cast(zero, ctypes.PointerType(ctypes.int32_t)); + badptr.contents + }); +} + +function waitForEvent(obj, name, capturing, chromeEvent) { + info("Waiting for " + name); + return new Promise((resolve) => { + function listener(event) { + info("Saw " + name); + obj.removeEventListener(name, listener, capturing, chromeEvent); + resolve(event); + } + + obj.addEventListener(name, listener, capturing, chromeEvent); + }); +} + +function resolveURI(uri) { + uri = Services.io.newURI(uri, null, null); + try { + return resProtocol.resolveURI(uri); + } + catch (e) { + return null; + } +} + +function remoteResolveURI(uri) { + return new Promise((resolve) => { + let manager = gBrowser.selectedBrowser.messageManager; + + function listener({ data: resolved }) { + manager.removeMessageListener("Test:ResolvedURI", listener); + resolve(resolved); + } + + manager.addMessageListener("Test:ResolvedURI", listener); + manager.sendAsyncMessage("Test:ResolveURI", uri); + }); +} + +var loadTestTab = Task.async(function*() { + gBrowser.selectedTab = gBrowser.addTab(TEST_URL); + let browser = gBrowser.selectedBrowser; + yield BrowserTestUtils.browserLoaded(browser); + browser.messageManager.loadFrameScript("data:,(" + frameScript.toString() + ")();", true); + return browser; +}); + +// Restarts the child process by crashing it then reloading the tab +var restart = Task.async(function*() { + let browser = gBrowser.selectedBrowser; + // If the tab isn't remote this would crash the main process so skip it + if (browser.getAttribute("remote") != "true") + return browser; + + browser.messageManager.sendAsyncMessage("Test:Crash"); + yield waitForEvent(browser, "AboutTabCrashedLoad", false, true); + + browser.reload(); + + yield BrowserTestUtils.browserLoaded(browser); + is(browser.getAttribute("remote"), expectedRemote, "Browser should be in the right process"); + browser.messageManager.loadFrameScript("data:,(" + frameScript.toString() + ")();", true); + return browser; +}); + +// Sanity check that this test is going to be useful +add_task(function*() { + let browser = yield loadTestTab(); + + // This must be loaded in the remote process for this test to be useful + is(browser.getAttribute("remote"), expectedRemote, "Browser should be in the right process"); + + let local = resolveURI("resource://gre/modules/Services.jsm"); + let remote = yield remoteResolveURI("resource://gre/modules/Services.jsm"); + is(local, remote, "Services.jsm should resolve in both processes"); + + gBrowser.removeCurrentTab(); +}); + +// Add a mapping, update it then remove it +add_task(function*() { + let browser = yield loadTestTab(); + + info("Set"); + resProtocol.setSubstitution("testing", Services.io.newURI("chrome://global/content", null, null)); + let local = resolveURI("resource://testing/test.js"); + let remote = yield remoteResolveURI("resource://testing/test.js"); + is(local, "chrome://global/content/test.js", "Should resolve in main process"); + is(remote, "chrome://global/content/test.js", "Should resolve in child process"); + + info("Change"); + resProtocol.setSubstitution("testing", Services.io.newURI("chrome://global/skin", null, null)); + local = resolveURI("resource://testing/test.js"); + remote = yield remoteResolveURI("resource://testing/test.js"); + is(local, "chrome://global/skin/test.js", "Should resolve in main process"); + is(remote, "chrome://global/skin/test.js", "Should resolve in child process"); + + info("Clear"); + resProtocol.setSubstitution("testing", null); + local = resolveURI("resource://testing/test.js"); + remote = yield remoteResolveURI("resource://testing/test.js"); + is(local, null, "Shouldn't resolve in main process"); + is(remote, null, "Shouldn't resolve in child process"); + + gBrowser.removeCurrentTab(); +}); + +// Add a mapping, restart the child process then check it is still there +add_task(function*() { + let browser = yield loadTestTab(); + + info("Set"); + resProtocol.setSubstitution("testing", Services.io.newURI("chrome://global/content", null, null)); + let local = resolveURI("resource://testing/test.js"); + let remote = yield remoteResolveURI("resource://testing/test.js"); + is(local, "chrome://global/content/test.js", "Should resolve in main process"); + is(remote, "chrome://global/content/test.js", "Should resolve in child process"); + + yield restart(); + + local = resolveURI("resource://testing/test.js"); + remote = yield remoteResolveURI("resource://testing/test.js"); + is(local, "chrome://global/content/test.js", "Should resolve in main process"); + is(remote, "chrome://global/content/test.js", "Should resolve in child process"); + + info("Change"); + resProtocol.setSubstitution("testing", Services.io.newURI("chrome://global/skin", null, null)); + + yield restart(); + + local = resolveURI("resource://testing/test.js"); + remote = yield remoteResolveURI("resource://testing/test.js"); + is(local, "chrome://global/skin/test.js", "Should resolve in main process"); + is(remote, "chrome://global/skin/test.js", "Should resolve in child process"); + + info("Clear"); + resProtocol.setSubstitution("testing", null); + + yield restart(); + + local = resolveURI("resource://testing/test.js"); + remote = yield remoteResolveURI("resource://testing/test.js"); + is(local, null, "Shouldn't resolve in main process"); + is(remote, null, "Shouldn't resolve in child process"); + + gBrowser.removeCurrentTab(); +}); + +// Adding a mapping to a resource URI should work +add_task(function*() { + let browser = yield loadTestTab(); + + info("Set"); + resProtocol.setSubstitution("testing", Services.io.newURI("chrome://global/content", null, null)); + resProtocol.setSubstitution("testing2", Services.io.newURI("resource://testing", null, null)); + let local = resolveURI("resource://testing2/test.js"); + let remote = yield remoteResolveURI("resource://testing2/test.js"); + is(local, "chrome://global/content/test.js", "Should resolve in main process"); + is(remote, "chrome://global/content/test.js", "Should resolve in child process"); + + info("Clear"); + resProtocol.setSubstitution("testing", null); + local = resolveURI("resource://testing2/test.js"); + remote = yield remoteResolveURI("resource://testing2/test.js"); + is(local, "chrome://global/content/test.js", "Should resolve in main process"); + is(remote, "chrome://global/content/test.js", "Should resolve in child process"); + + resProtocol.setSubstitution("testing2", null); + local = resolveURI("resource://testing2/test.js"); + remote = yield remoteResolveURI("resource://testing2/test.js"); + is(local, null, "Shouldn't resolve in main process"); + is(remote, null, "Shouldn't resolve in child process"); + + gBrowser.removeCurrentTab(); +}); diff --git a/netwerk/test/browser/browser_nsIFormPOSTActionChannel.js b/netwerk/test/browser/browser_nsIFormPOSTActionChannel.js new file mode 100644 index 0000000000..150c4feca9 --- /dev/null +++ b/netwerk/test/browser/browser_nsIFormPOSTActionChannel.js @@ -0,0 +1,284 @@ +/* + * Tests for bug 1241377: A channel with nsIFormPOSTActionChannel interface + * should be able to accept form POST. + */ + +Components.utils.import("resource://gre/modules/Services.jsm"); +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); +Components.utils.import("resource://gre/modules/Task.jsm"); + +const SCHEME = "x-bug1241377"; + +const FORM_BASE = SCHEME + "://dummy/form/"; +const NORMAL_FORM_URI = FORM_BASE + "normal.html"; +const UPLOAD_FORM_URI = FORM_BASE + "upload.html"; +const POST_FORM_URI = FORM_BASE + "post.html"; + +const ACTION_BASE = SCHEME + "://dummy/action/"; +const NORMAL_ACTION_URI = ACTION_BASE + "normal.html"; +const UPLOAD_ACTION_URI = ACTION_BASE + "upload.html"; +const POST_ACTION_URI = ACTION_BASE + "post.html"; + +function CustomProtocolHandler() { +} +CustomProtocolHandler.prototype = { + /** nsIProtocolHandler */ + get scheme() { + return SCHEME; + }, + get defaultPort() { + return -1; + }, + get protocolFlags() { + return Ci.nsIProtocolHandler.URI_NORELATIVE | + Ci.nsIProtocolHandler.URI_IS_LOCAL_RESOURCE; + }, + newURI: function(aSpec, aOriginCharset, aBaseURI) { + var uri = Cc["@mozilla.org/network/standard-url;1"]. + createInstance(Ci.nsIURI); + uri.spec = aSpec; + return uri; + }, + newChannel2: function(aURI, aLoadInfo) { + return new CustomChannel(aURI, aLoadInfo); + }, + newChannel: function(aURI) { + throw Cr.NS_ERROR_NOT_IMPLEMENTED; + }, + allowPort: function(port, scheme) { + return port != -1; + }, + + /** nsIFactory */ + createInstance: function(aOuter, aIID) { + if (aOuter) { + throw Cr.NS_ERROR_NO_AGGREGATION; + } + return this.QueryInterface(aIID); + }, + lockFactory: function() {}, + + /** nsISupports */ + QueryInterface: XPCOMUtils.generateQI([Ci.nsIProtocolHandler, + Ci.nsIFactory]), + classID: Components.ID("{16d594bc-d9d8-47ae-a139-ea714dc0c35c}") +}; + +function CustomChannel(aURI, aLoadInfo) { + this.uri = aURI; + this.loadInfo = aLoadInfo; + + this._uploadStream = null; + + var interfaces = [Ci.nsIRequest, Ci.nsIChannel]; + if (this.uri.spec == POST_ACTION_URI) { + interfaces.push(Ci.nsIFormPOSTActionChannel); + } else if (this.uri.spec == UPLOAD_ACTION_URI) { + interfaces.push(Ci.nsIUploadChannel); + } + this.QueryInterface = XPCOMUtils.generateQI(interfaces); +} +CustomChannel.prototype = { + /** nsIUploadChannel */ + get uploadStream() { + return this._uploadStream; + }, + set uploadStream(val) { + throw Cr.NS_ERROR_NOT_IMPLEMENTED; + }, + setUploadStream: function(aStream, aContentType, aContentLength) { + this._uploadStream = aStream; + }, + + /** nsIChannel */ + get originalURI() { + return this.uri; + }, + get URI() { + return this.uri; + }, + owner: null, + notificationCallbacks: null, + get securityInfo() { + return null; + }, + get contentType() { + return "text/html"; + }, + set contentType(val) { + }, + contentCharset: "UTF-8", + get contentLength() { + return -1; + }, + set contentLength(val) { + throw Cr.NS_ERROR_NOT_IMPLEMENTED; + }, + open: function() { + throw Cr.NS_ERROR_NOT_IMPLEMENTED; + }, + open2: function() { + throw Cr.NS_ERROR_NOT_IMPLEMENTED; + }, + asyncOpen: function(aListener, aContext) { + var data = ` +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8"> +<title>test bug 1241377</title> +</head> +<body> +`; + + if (this.uri.spec.startsWith(FORM_BASE)) { + data += ` +<form id="form" action="${this.uri.spec.replace(FORM_BASE, ACTION_BASE)}" + method="post" enctype="text/plain" target="frame"> +<input type="hidden" name="foo" value="bar"> +<input type="submit"> +</form> + +<iframe id="frame" name="frame" width="200" height="200"></iframe> + +<script type="text/javascript"> +<!-- +document.getElementById('form').submit(); +//--> +</script> +`; + } else if (this.uri.spec.startsWith(ACTION_BASE)) { + var postData = ""; + if (this._uploadStream) { + var bstream = Cc["@mozilla.org/binaryinputstream;1"] + .createInstance(Ci.nsIBinaryInputStream); + bstream.setInputStream(this._uploadStream); + postData = bstream.readBytes(bstream.available()); + } + data += ` +<input id="upload_stream" value="${this._uploadStream ? "yes" : "no"}"> +<input id="post_data" value="${btoa(postData)}"> +`; + } + + data += ` +</body> +</html> +`; + + var stream = Cc["@mozilla.org/io/string-input-stream;1"] + .createInstance(Ci.nsIStringInputStream); + stream.setData(data, data.length); + + var runnable = { + run: () => { + try { + aListener.onStartRequest(this, aContext); + } catch(e) {} + try { + aListener.onDataAvailable(this, aContext, stream, 0, stream.available()); + } catch(e) {} + try { + aListener.onStopRequest(this, aContext, Cr.NS_OK); + } catch(e) {} + } + }; + Services.tm.currentThread.dispatch(runnable, Ci.nsIEventTarget.DISPATCH_NORMAL); + }, + asyncOpen2: function(aListener) { + this.asyncOpen(aListener, null); + }, + + /** nsIRequest */ + get name() { + return this.uri.spec; + }, + isPending: function () { + return false; + }, + get status() { + return Cr.NS_OK; + }, + cancel: function(status) {}, + loadGroup: null, + loadFlags: Ci.nsIRequest.LOAD_NORMAL | + Ci.nsIRequest.INHIBIT_CACHING | + Ci.nsIRequest.LOAD_BYPASS_CACHE, +}; + +function frameScript() { + addMessageListener("Test:WaitForIFrame", function() { + var check = function() { + if (content) { + var frame = content.document.getElementById("frame"); + if (frame) { + var upload_stream = frame.contentDocument.getElementById("upload_stream"); + var post_data = frame.contentDocument.getElementById("post_data"); + if (upload_stream && post_data) { + sendAsyncMessage("Test:IFrameLoaded", [upload_stream.value, post_data.value]); + return; + } + } + } + + setTimeout(check, 100); + }; + + check(); + }); +} + +function loadTestTab(uri) { + gBrowser.selectedTab = gBrowser.addTab(uri); + var browser = gBrowser.selectedBrowser; + + let manager = browser.messageManager; + browser.messageManager.loadFrameScript("data:,(" + frameScript.toString() + ")();", true); + + return new Promise(resolve => { + function listener({ data: [hasUploadStream, postData] }) { + manager.removeMessageListener("Test:IFrameLoaded", listener); + resolve([hasUploadStream, atob(postData)]); + } + + manager.addMessageListener("Test:IFrameLoaded", listener); + manager.sendAsyncMessage("Test:WaitForIFrame"); + }); +} + +add_task(function*() { + var handler = new CustomProtocolHandler(); + var registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar); + registrar.registerFactory(handler.classID, "", + "@mozilla.org/network/protocol;1?name=" + handler.scheme, + handler); + registerCleanupFunction(function() { + registrar.unregisterFactory(handler.classID, handler); + }); +}); + +add_task(function*() { + var [hasUploadStream, postData] = yield loadTestTab(NORMAL_FORM_URI); + is(hasUploadStream, "no", "normal action should not have uploadStream"); + + gBrowser.removeCurrentTab(); +}); + +add_task(function*() { + var [hasUploadStream, postData] = yield loadTestTab(UPLOAD_FORM_URI); + is(hasUploadStream, "no", "upload action should not have uploadStream"); + + gBrowser.removeCurrentTab(); +}); + +add_task(function*() { + var [hasUploadStream, postData] = yield loadTestTab(POST_FORM_URI); + is(hasUploadStream, "yes", "post action should have uploadStream"); + is(postData, + "Content-Type: text/plain\r\n" + + "Content-Length: 9\r\n" + + "\r\n" + + "foo=bar\r\n", "POST data is received correctly"); + + gBrowser.removeCurrentTab(); +}); diff --git a/netwerk/test/browser/browser_post_file.js b/netwerk/test/browser/browser_post_file.js new file mode 100644 index 0000000000..6c9fd7a2ff --- /dev/null +++ b/netwerk/test/browser/browser_post_file.js @@ -0,0 +1,101 @@ +/* + * Tests for bug 1241100: Post to local file should not overwrite the file. + */ + +Components.utils.import("resource://gre/modules/osfile.jsm"); + +function* createTestFile(filename, content) { + let path = OS.Path.join(OS.Constants.Path.tmpDir, filename); + yield OS.File.writeAtomic(path, content); + return path; +} + +function* readFile(path) { + var array = yield OS.File.read(path); + var decoder = new TextDecoder(); + return decoder.decode(array); +} + +function frameScript() { + addMessageListener("Test:WaitForIFrame", function() { + var check = function() { + if (content) { + var frame = content.document.getElementById("frame"); + if (frame) { + var okBox = frame.contentDocument.getElementById("action_file_ok"); + if (okBox) { + sendAsyncMessage("Test:IFrameLoaded"); + return; + } + } + } + + setTimeout(check, 100); + }; + + check(); + }); +} + +add_task(function*() { + var postFilename = "post_file.html"; + var actionFilename = "action_file.html"; + + var postFileContent = ` +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8"> +<title>post file</title> +</head> +<body onload="document.getElementById('form').submit();"> +<form id="form" action="${actionFilename}" method="post" enctype="text/plain" target="frame"> +<input type="hidden" name="foo" value="bar"> +<input type="submit"> +</form> +<iframe id="frame" name="frame"></iframe> +</body> +</html> +`; + + var actionFileContent = ` +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8"> +<title>action file</title> +</head> +<body> +<div id="action_file_ok">ok</div> +</body> +</html> +`; + + var postPath = yield* createTestFile(postFilename, postFileContent); + var actionPath = yield* createTestFile(actionFilename, actionFileContent); + + var postURI = OS.Path.toFileURI(postPath); + + gBrowser.selectedTab = gBrowser.addTab(postURI); + let browser = gBrowser.selectedBrowser; + browser.messageManager.loadFrameScript("data:,(" + frameScript.toString() + ")();", true); + yield new Promise(resolve => { + let manager = browser.messageManager; + + function listener() { + manager.removeMessageListener("Test:IFrameLoaded", listener); + resolve(); + } + + manager.addMessageListener("Test:IFrameLoaded", listener); + manager.sendAsyncMessage("Test:WaitForIFrame"); + }); + + var actionFileContentAfter = yield* readFile(actionPath); + is(actionFileContentAfter, actionFileContent, "action file is not modified"); + + yield OS.File.remove(postPath); + yield OS.File.remove(actionPath); + + gBrowser.removeCurrentTab(); +}); diff --git a/netwerk/test/browser/dummy.html b/netwerk/test/browser/dummy.html new file mode 100644 index 0000000000..6b28a248fb --- /dev/null +++ b/netwerk/test/browser/dummy.html @@ -0,0 +1,7 @@ +<!DOCTYPE html> + +<html> +<body> + <p>Dummy Page</p> +</body> +</html> diff --git a/netwerk/test/crashtests/1274044-1.html b/netwerk/test/crashtests/1274044-1.html new file mode 100644 index 0000000000..cb88e50bcd --- /dev/null +++ b/netwerk/test/crashtests/1274044-1.html @@ -0,0 +1,7 @@ +<script> + +var u = new URL("http://127.0.0.1:9607/"); +u.protocol = "resource:"; +u.port = ""; + +</script> diff --git a/netwerk/test/crashtests/1334468-1.html b/netwerk/test/crashtests/1334468-1.html new file mode 100644 index 0000000000..3d94d69949 --- /dev/null +++ b/netwerk/test/crashtests/1334468-1.html @@ -0,0 +1,25 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="UTF-8"> +<!-- +user_pref("privacy.firstparty.isolate", true); +--> +<script> + +let RESTRICTED_CHARS = "\001\002\003\004\005\006\007" + + "\010\011\012\013\014\015\016\017" + + "\020\021\022\023\024\025\026\027" + + "\030\031\032\033\034\035\036\037" + + "/:*?\"<>|\\"; + +function boom() { + for (let c of RESTRICTED_CHARS) { + window.location = 'http://s.s' + c; + } +} + +</script> +</head> +<body onload="boom();"></body> +</html> diff --git a/netwerk/test/crashtests/785753-1.html b/netwerk/test/crashtests/785753-1.html new file mode 100644 index 0000000000..7ccb823555 --- /dev/null +++ b/netwerk/test/crashtests/785753-1.html @@ -0,0 +1,253 @@ +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 0.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> +<html> +<head> +<title>Test for importing styles via incorrect link element</title> +<link type="text/css" href="data:text/css;charset=utf-8,p#one%-32519279132875%7Bbackground-color%32769A%340282366920938463463374607431768211436red%1B%7D%0D%0A"/> +<link rel="stylesheet" href="data:text/css;charset=utf-16,p#two%1%7Bbackground-color%65535A%4294967297lime%3B%7D%0D%0A"/> +</head> +<link href="data:text/css;charset=utf-8,p#three%1%7Bbackground-color%3A%20red%3B%7D%0D%0A"/> +<link type="text/css" rel="stylesheet" href="data:text/css;charset=utf-8,p#four%32767%7Bbackground-color%2147483649A%20lime%257B%7D%0D%0A"/> +</head> +<body> +<p id="one">This line should not have red background</p> +<p id="two">This line should have lime background</p> +<p id="three">This line should not have red background</p> +<p id="four">This line should have lime background</p> +</body> +<script type="text/javascript"> + function alert(msg){}; function confirm(msg){}; function prompt(msg){}; + try{ document.head.appendChild(document.createElement("style"));}catch(e){} + var styleSheet = document.styleSheets[document.styleSheets.length-1]; +try{if(styleSheet.length===undefined){styleSheet.insertRule(":root{}",0); styleSheet.disabled=false} + +styleSheet.insertRule("body {counter-reset:c}",0)}catch(e){} +var styleSheet0 = document.styleSheets[0]; +var styleSheet1 = document.styleSheets[1]; +var styleSheet2 = document.styleSheets[2]; +var test0=document.getElementById("four") +var test1=document.getElementById("one") +var test2=document.getElementById("two") +var test3=document.getElementById("three") +setTimeout(function(){ +try{test0.style['padding-top']='32px';}catch(e){} +try{test2.insertBefore(test3);}catch(e){} +try{test0.style.setProperty('background-image','url(data:image/gif;base64,R0lGODlhEAAOALMAAOazToeHh0tLS/7LZv/0jvb29t/f3//Ub//ge8WSLf/rhf/3kdbW1mxsbP//mf///yH5BAAAAAAALAAAAAAQAA4AAARe8L1Ekyky67QZ1hLnjM5UUde0ECwLJoExKcppV0aCcGCmTIHEIUEqjgaORCMxIC6e0CcguWw6aFjsVMkkIr7g77ZKPJjPZqIyd7sJAgVGoEGv2xsBxqNgYPj/gAwXEQA7)','important');}catch(e){} +try{test3.style['line-height']='-324px';}catch(e){} +try{test0.style.setProperty('border-image-repeat','repeat','important');}catch(e){} +},3); + +setTimeout(function(){ +try{test1.style['line-height']='43px';}catch(e){} +try{styleSheet0.insertRule(".undefined,.undefined{clear:right; }",styleSheet0.cssRules.length);}catch(e){} +try{test2.style.setProperty('bottom','inherit','important');}catch(e){} +try{test3.parentNode.removeChild(test3)}catch(e){}; +try{test0.style.setProperty('overflow-x','no-content','important');}catch(e){} +},0); + +setTimeout(function(){ +try{styleSheet0.insertRule(".undefined,.undefined{font-size:40px; overflow-x:no-content; -moz-transition-duration:-5.408991568721831s; -moz-column-span:483; }",styleSheet0.cssRules.length);}catch(e){} +try{test0.parentNode.removeChild(test0)}catch(e){}; +try{test0.insertBefore(test2);}catch(e){} +try{test3.style.setProperty('bottom','inherit','important');}catch(e){} +try{test2.style.setProperty('background-origin','border-box','important');}catch(e){} +},2); + +setTimeout(function(){ +window.resizeTo(1018,353) +try{test0.insertBefore(test1);}catch(e){} +try{test1.style.setProperty('bottom','auto','important');}catch(e){} +try{test1.style.setProperty('z-index','inherit','important');}catch(e){} +window.resizeTo(1018,353) +},1); + +setTimeout(function(){ +try{test0.innerHtml=test3.innerHtml;}catch(e){} +document.body.style.setProperty('-webkit-filter','blur(18px)','null') +try{test3.parentNode.removeChild(test3)}catch(e){}; +try{test0.style.setProperty('padding-right','18px','important');}catch(e){} +try{test3.style.setProperty('border-bottom-color','rgb(96%,328%,106)','important');}catch(e){} +},4); + +setTimeout(function(){ +try{test2.innerHtml=test0.innerHtml;}catch(e){} +try{styleSheet1.insertRule(".undefined,.undefined{list-style-type:sidama; background-clip:border-box; overflow-x:scroll; border-bottom-left-radius:70px; text-transform:uppercase; empty-cells:inherit; }",styleSheet1.cssRules.length);}catch(e){} +try{styleSheet1.insertRule(".undefined:active {min-width:759; }",styleSheet1.cssRules.length);}catch(e){} +try{test1.style.setProperty('top','343','important');}catch(e){} +try{test2.replaceChild(test0,test2.firstChild)}catch(e){} +},4); + +setTimeout(function(){ +try{styleSheet1.insertRule(".undefined,.undefined,.undefined,.undefined{background-attachment:inherit; flood-color:rgba(93%,364%,104,4.471563883125782); }",0);}catch(e){} +try{test1.style.setProperty('font-style','oblique','important');}catch(e){} +try{styleSheet0.insertRule(".undefined,.undefined,.undefined{border-right-style:double; }",styleSheet0.cssRules.length);}catch(e){} +document.execCommand("SelectAll", true); +try{test3.parentNode.removeChild(test3)}catch(e){}; +},1); + +setTimeout(function(){ +try{test1.parentNode.removeChild(test1)}catch(e){}; +try{test0.innerHtml=test2.innerHtml;}catch(e){} +try{test1.parentNode.removeChild(test1)}catch(e){}; +try{test0.parentNode.removeChild(test0)}catch(e){}; +try{test2.innerHtml=test2.innerHtml;}catch(e){} +},4); + +setTimeout(function(){ +try{test0.appendChild(test1);}catch(e){} +try{test1.style['position']='inherit';}catch(e){} +try{test2.replaceChild(test3,test2.lastChild)}catch(e){} +try{test1.style['border-left-color']='#6D8997';}catch(e){} +try{test1.innerHtml=test3.innerHtml;}catch(e){} +},6); + +setTimeout(function(){ +try{test2.insertBefore(test1);}catch(e){} +try{test2.innerHtml=test3.innerHtml;}catch(e){} +try{test0.appendChild(test2);}catch(e){} +try{styleSheet0.insertRule(".undefined,.undefined{resize:both; background-color:#A22225; position:relative; -moz-column-width:auto; letter-spacing:361px; border-top-width:151%; }",styleSheet0.cssRules.length);}catch(e){} +document.body.style.zoom=1.8764980849809945 +},6); + +setTimeout(function(){ +try{styleSheet1.insertRule(".undefined,.undefined,.undefined{outline-color:rgba(126,179,46,0.8964905887842178); width:183; }",styleSheet1.cssRules.length);}catch(e){} +try{test2.insertBefore(test3);}catch(e){} +try{test1.innerHtml=test3.innerHtml;}catch(e){} +try{styleSheet0.insertRule("article,footer,article,article{border-bottom-right-radius:7px; }",0);}catch(e){} +try{test1.insertBefore(test0);}catch(e){} +},4); + +setTimeout(function(){ +try{test0.parentNode.removeChild(test0)}catch(e){}; +try{styleSheet1.insertRule(".undefined,.undefined,.undefined,.undefined{display: table-header-group; content: counter(c, ethiopic); counter-increment:c;}",0);}catch(e){} +try{test0.style.setProperty('background-color','#8897D3','important');}catch(e){} +try{test3.appendChild(test0);}catch(e){} +try{styleSheet0.insertRule("hgroup,hgroup,hgroup{outline-color:rgba(167,242,90%,-0.10827295063063502); }",styleSheet0.cssRules.length);}catch(e){} +},4); + +setTimeout(function(){ +try{test3.style.setProperty('border-bottom-color','#55D7F6','important');}catch(e){} +try{test1.parentNode.removeChild(test1)}catch(e){}; +try{test2.insertBefore(test1);}catch(e){} +try{test2.innerHtml=test0.innerHtml;}catch(e){} +try{styleSheet1.insertRule("#one,#one,#three,#three{background-clip:border-box; border-top-width:85em; }",0);}catch(e){} +},5); + +setTimeout(function(){ +try{test3.appendChild(test0);}catch(e){} +try{test2.innerHtml=test1.innerHtml;}catch(e){} +try{test2.style['background-attachment']='inherit';}catch(e){} +try{test3.style['clip']='inherit';}catch(e){} +try{test3.parentNode.removeChild(test3)}catch(e){}; +},3); + +setTimeout(function(){ +try{test1.parentNode.removeChild(test1)}catch(e){}; +try{styleSheet0.insertRule(".undefined,.undefined{height:424; }",styleSheet0.cssRules.length);}catch(e){} +try{test2.style.setProperty('border-top-style','solid','important');}catch(e){} +try{styleSheet0.insertRule(".undefined,.undefined,.undefined,.undefined{page-break-inside:left; border-image-slice:fill; border-left-width:184pc; }",styleSheet0.cssRules.length);}catch(e){} +try{styleSheet0.insertRule(".undefined,.undefined{margin-left:-105px; text-transform:inherit; box-sizing:border-box; }",styleSheet0.cssRules.length);}catch(e){} +},4); + +setTimeout(function(){ +try{styleSheet0.insertRule("param,img,img,param{left:auto; background-clip:padding-box; }",styleSheet0.cssRules.length);}catch(e){} +try{test2.style.setProperty('min-height','605','important');}catch(e){} +try{test2.parentNode.removeChild(test2)}catch(e){}; +try{styleSheet1.insertRule(".undefined::first-line, #two::first-line {border-bottom-width:69pt; lighting-color:rgba(75%,166,81,-0.8728196211159229); text-shadow:85px 459px #2F1; }",styleSheet1.cssRules.length);}catch(e){} +try{test3.appendChild(document.createTextNode(unescape("!F幓[")))}catch(e){} +},4); + +setTimeout(function(){ +try{styleSheet0.insertRule("#two,#four{font-style:italic; list-style-position:inside; border-collapse:inherit; word-wrap:break-word; text-transform:uppercase; }",styleSheet0.cssRules.length);}catch(e){} +try{test1.style.setProperty('border-bottom-right-radius','2px','important');}catch(e){} +try{test3.style['text-shadow']='58px 64px rgba(61%,60,199,0.03203143551945686)';}catch(e){} +try{styleSheet1.insertRule("dir,dir,nav{display: inline-table; content: counter(c, upper-greek); counter-increment:c;}",styleSheet1.cssRules.length);}catch(e){} +try{styleSheet0.insertRule("#one:target, #three:after {color:#D9B; outline-style:hidden; flood-color:rgba(22,59%,99%,-0.008097740123048425); }",0);}catch(e){} +},6); + +setTimeout(function(){ +try{test2.parentNode.removeChild(test2)}catch(e){}; +document.execCommand("Copy", true); +try{test3.style['letter-spacing']='102px';}catch(e){} +try{test2.parentNode.removeChild(test2)}catch(e){}; +try{test3.appendChild(test0);}catch(e){} +},5); + +setTimeout(function(){ +try{test0.style['text-transform']='inherit';}catch(e){} +try{test0.style['word-break']='hyphenate';}catch(e){} +try{test3.insertBefore(test2);}catch(e){} +try{test2.style.setProperty('bottom','410','important');}catch(e){} +try{test1.style['background']='url(data:image/gif;base64,R0lGODlhEAAOALMAAOazToeHh0tLS/7LZv/0jvb29t/f3//Ub//ge8WSLf/rhf/3kdbW1mxsbP//mf///yH5BAAAAAAALAAAAAAQAA4AAARe8L1Ekyky67QZ1hLnjM5UUde0ECwLJoExKcppV0aCcGCmTIHEIUEqjgaORCMxIC6e0CcguWw6aFjsVMkkIr7g77ZKPJjPZqIyd7sJAgVGoEGv2xsBxqNgYPj/gAwXEQA7)';}catch(e){} +},5); + +setTimeout(function(){ +try{test2.appendChild(test2);}catch(e){} +try{test1.appendChild(test2);}catch(e){} +try{test2.appendChild(document.createTextNode(unescape("zn!쎔gw눢fb¤£kꄍ£3wa02fnpå0!äwC䰴頥!!a")))}catch(e){} +try{styleSheet0.insertRule(".undefined,.undefined,.undefined{display: inline; content: counter(c, khmer); counter-increment:c;}",0);}catch(e){} +try{test3.style.setProperty('line-height','42in','important');}catch(e){} +},7); + +setTimeout(function(){ +window.moveBy(133,126) +try{test0.style['padding-right']='20px';}catch(e){} +try{test1.replaceChild(test2,test1.firstChild)}catch(e){} +try{test1.style.setProperty('letter-spacing','120px','important');}catch(e){} +try{test1.style.setProperty('height','57','important');}catch(e){} +},4); + +setTimeout(function(){ +try{styleSheet1.insertRule(".undefined:nth-child(even), #one:nth-last-child(even) {margin-top:-241cm; font-size:23px; }",0);}catch(e){} +try{styleSheet0.insertRule(".undefined:nth-child(even), #three:default {stop-color:rgba(92%,-201%,31,0.8133529485203326); lighting-color:#E31; }",styleSheet0.cssRules.length);}catch(e){} +try{test1.style.setProperty('border-top-left-radius','63px','important');}catch(e){} +try{test0.style['letter-spacing']='36px';}catch(e){} +try{test1.appendChild(document.createTextNode(unescape("1u£Fⶵ隗(籬fsä⍉㯗cሮ銐k䆴n#蹹圭篺(1w馁")))}catch(e){} +},7); + +setTimeout(function(){ +try{test1.appendChild(test3);}catch(e){} +try{test2.style.setProperty('lighting-color','rgb(4,36%,95%)','important');}catch(e){} +try{test0.style.setProperty('border-right-width','71pt','important');}catch(e){} +try{test2.style['box-shadow']='-228px , 86px , 9px , #F0A134';}catch(e){} +try{styleSheet0.insertRule(".undefined,.undefined,.undefined{left:inherit; }",styleSheet0.cssRules.length);}catch(e){} +},7); + +setTimeout(function(){ +try{styleSheet1.insertRule(".undefined,.undefined,.undefined,.undefined{min-height:277; -moz-transition-property:none; }",0);}catch(e){} +try{test0.style.setProperty('word-wrap','break-word','important');}catch(e){} +try{test1.style.setProperty('top','inherit','important');}catch(e){} +try{styleSheet0.insertRule(".undefined,.undefined,.undefined{overflow-x:visible; border-bottom-left-radius:844488.660965659px; }",0);}catch(e){} +try{test2.style.setProperty('margin-bottom','auto','important');}catch(e){} +},0); + +setTimeout(function(){ +try{styleSheet1.insertRule("hgroup,hgroup,dl,dl{display: list-item; content: counter(c, hebrew); counter-increment:c;}",styleSheet1.cssRules.length);}catch(e){} +try{test0.style.setProperty('border-image-repeat','repeat','important');}catch(e){} +try{styleSheet0.insertRule("figure,footer,figure,table{-moz-border-image-repeat:stretch; word-wrap:normal; border-right-color:rgb(4%,19%,6%); caption-side:top; stop-color:rgba(450%,226,14%,1.5385327017866075); }",styleSheet0.cssRules.length);}catch(e){} +try{test3.innerHtml=test2.innerHtml;}catch(e){} +try{test2.appendChild(test0);}catch(e){} +},0); + +setTimeout(function(){ +try{test1.replaceChild(test0,test1.lastChild)}catch(e){} +try{test2.style.setProperty('border-collapse','inherit','important');}catch(e){} +try{test1.style['overflow-x']='visible';}catch(e){} +try{test1.style['text-indent']='-30.283706605434418cm';}catch(e){} +try{styleSheet0.insertRule("tt,hgroup{stroke-width:-439px; box-sizing:border-box; }",styleSheet0.cssRules.length);}catch(e){} +},1); + +setTimeout(function(){ +styleSheet0.disabled=true +styleSheet1.disabled=false +styleSheet1.disabled=true +document.body.style.setProperty('-webkit-filter','invert(338%)','null') +window.moveBy(302,115) +},0); + +setTimeout(function(){ +window.blur() +},4); + +</script> + +</html> diff --git a/netwerk/test/crashtests/785753-2.html b/netwerk/test/crashtests/785753-2.html new file mode 100644 index 0000000000..15a9865388 --- /dev/null +++ b/netwerk/test/crashtests/785753-2.html @@ -0,0 +1,3 @@ +<link rel="stylesheet" href="data:text/css;charset=utf-16,a"/> + +<link rel="stylesheet" href="data:text/css;charset=utf-16,p#two%1%7Bbackground-color%65535A%4294967297lime%3B%7D%0D%0A"/>
\ No newline at end of file diff --git a/netwerk/test/crashtests/crashtests.list b/netwerk/test/crashtests/crashtests.list new file mode 100644 index 0000000000..564df22b9d --- /dev/null +++ b/netwerk/test/crashtests/crashtests.list @@ -0,0 +1,4 @@ +load 785753-1.html +load 785753-2.html +load 1274044-1.html +skip-if(Android) pref(privacy.firstparty.isolate,true) load 1334468-1.html diff --git a/netwerk/test/gtest/TestProtocolProxyService.cpp b/netwerk/test/gtest/TestProtocolProxyService.cpp new file mode 100644 index 0000000000..a49e9f9617 --- /dev/null +++ b/netwerk/test/gtest/TestProtocolProxyService.cpp @@ -0,0 +1,128 @@ +#include "gtest/gtest.h" + +#include "nsCOMPtr.h" +#include "nsNetCID.h" +#include "nsIURL.h" +#include "nsString.h" +#include "nsComponentManagerUtils.h" +#include "../../base/nsProtocolProxyService.h" +#include "nsServiceManagerUtils.h" +#include "mozilla/Preferences.h" + +namespace mozilla { +namespace net { + +TEST(TestProtocolProxyService, LoadHostFilters) { + nsCOMPtr<nsIProtocolProxyService2> ps = do_GetService(NS_PROTOCOLPROXYSERVICE_CID); + ASSERT_TRUE(ps); + mozilla::net::nsProtocolProxyService* pps = static_cast<mozilla::net::nsProtocolProxyService*>(ps.get()); + + nsCOMPtr<nsIURL> url( do_CreateInstance(NS_STANDARDURL_CONTRACTID) ); + ASSERT_TRUE(url) << "couldn't create URL"; + + nsAutoCString spec; + + auto CheckLoopbackURLs = [&](bool expected) + { + // loopback IPs are always filtered + spec = "http://127.0.0.1"; + ASSERT_EQ(url->SetSpec(spec), NS_OK); + ASSERT_EQ(pps->CanUseProxy(url, 80), expected); + spec = "http://[::1]"; + ASSERT_EQ(url->SetSpec(spec), NS_OK); + ASSERT_EQ(pps->CanUseProxy(url, 80), expected); + }; + + auto CheckURLs = [&](bool expected) + { + spec = "http://example.com"; + ASSERT_EQ(url->SetSpec(spec), NS_OK); + ASSERT_EQ(pps->CanUseProxy(url, 80), expected); + + spec = "https://10.2.3.4"; + ASSERT_EQ(url->SetSpec(spec), NS_OK); + ASSERT_EQ(pps->CanUseProxy(url, 443), expected); + + spec = "http://1.2.3.4"; + ASSERT_EQ(url->SetSpec(spec), NS_OK); + ASSERT_EQ(pps->CanUseProxy(url, 80), expected); + + spec = "http://1.2.3.4:8080"; + ASSERT_EQ(url->SetSpec(spec), NS_OK); + ASSERT_EQ(pps->CanUseProxy(url, 80), expected); + + spec = "http://[2001::1]"; + ASSERT_EQ(url->SetSpec(spec), NS_OK); + ASSERT_EQ(pps->CanUseProxy(url, 80), expected); + + spec = "http://2.3.4.5:7777"; + ASSERT_EQ(url->SetSpec(spec), NS_OK); + ASSERT_EQ(pps->CanUseProxy(url, 80), expected); + + spec = "http://[abcd::2]:123"; + ASSERT_EQ(url->SetSpec(spec), NS_OK); + ASSERT_EQ(pps->CanUseProxy(url, 80), expected); + + spec = "http://bla.test.com"; + ASSERT_EQ(url->SetSpec(spec), NS_OK); + ASSERT_EQ(pps->CanUseProxy(url, 80), expected); + }; + + auto CheckPortDomain = [&](bool expected) + { + spec = "http://blabla.com:10"; + ASSERT_EQ(url->SetSpec(spec), NS_OK); + ASSERT_EQ(pps->CanUseProxy(url, 80), expected); + }; + + auto CheckLocalDomain = [&](bool expected) + { + spec = "http://test"; + ASSERT_EQ(url->SetSpec(spec), NS_OK); + ASSERT_EQ(pps->CanUseProxy(url, 80), expected); + }; + + // -------------------------------------------------------------------------- + + nsAutoCString filter; + + // Anything is allowed when there are no filters set + printf("Testing empty filter: %s\n", filter.get()); + pps->LoadHostFilters(filter); + + CheckLoopbackURLs(true); // only time when loopbacks can be proxied. bug? + CheckLocalDomain(true); + CheckURLs(true); + CheckPortDomain(true); + + // -------------------------------------------------------------------------- + + filter = "example.com, 1.2.3.4/16, [2001::1], 10.0.0.0/8, 2.3.0.0/16:7777, [abcd::1]/64:123, *.test.com"; + printf("Testing filter: %s\n", filter.get()); + pps->LoadHostFilters(filter); + // Check URLs can no longer use filtered proxy + CheckURLs(false); + CheckLoopbackURLs(false); + CheckLocalDomain(true); + CheckPortDomain(true); + + // -------------------------------------------------------------------------- + + // This is space separated. See bug 1346711 comment 4. We check this to keep + // backwards compatibility. + filter = "<local> blabla.com:10"; + printf("Testing filter: %s\n", filter.get()); + pps->LoadHostFilters(filter); + CheckURLs(true); + CheckLoopbackURLs(false); + CheckLocalDomain(false); + CheckPortDomain(false); + + // Check that we don't crash on weird input + filter = "a b c abc:1x2, ,, * ** *.* *:10 :20 :40/12 */12:90"; + printf("Testing filter: %s\n", filter.get()); + pps->LoadHostFilters(filter); +} + +} // namespace net +} // namespace mozila diff --git a/netwerk/test/gtest/TestStandardURL.cpp b/netwerk/test/gtest/TestStandardURL.cpp new file mode 100644 index 0000000000..ccab556a96 --- /dev/null +++ b/netwerk/test/gtest/TestStandardURL.cpp @@ -0,0 +1,69 @@ +#include "gtest/gtest.h" +#include "gtest/MozGTestBench.h" // For MOZ_GTEST_BENCH + +#include "nsCOMPtr.h" +#include "nsNetCID.h" +#include "nsIURL.h" +#include "nsString.h" +#include "nsComponentManagerUtils.h" + +TEST(TestStandardURL, Simple) { + nsCOMPtr<nsIURL> url( do_CreateInstance(NS_STANDARDURL_CONTRACTID) ); + ASSERT_TRUE(url); + ASSERT_EQ(url->SetSpec(NS_LITERAL_CSTRING("http://example.com")), NS_OK); + + nsAutoCString out; + + ASSERT_EQ(url->GetSpec(out), NS_OK); + ASSERT_TRUE(out == NS_LITERAL_CSTRING("http://example.com/")); + + ASSERT_EQ(url->Resolve(NS_LITERAL_CSTRING("foo.html?q=45"), out), NS_OK); + ASSERT_TRUE(out == NS_LITERAL_CSTRING("http://example.com/foo.html?q=45")); + + ASSERT_EQ(url->SetScheme(NS_LITERAL_CSTRING("foo")), NS_OK); + + ASSERT_EQ(url->GetScheme(out), NS_OK); + ASSERT_TRUE(out == NS_LITERAL_CSTRING("foo")); + + ASSERT_EQ(url->GetHost(out), NS_OK); + ASSERT_TRUE(out == NS_LITERAL_CSTRING("example.com")); + ASSERT_EQ(url->SetHost(NS_LITERAL_CSTRING("www.yahoo.com")), NS_OK); + ASSERT_EQ(url->GetHost(out), NS_OK); + ASSERT_TRUE(out == NS_LITERAL_CSTRING("www.yahoo.com")); + + ASSERT_EQ(url->SetPath(NS_LITERAL_CSTRING("/some-path/one-the-net/about.html?with-a-query#for-you")), NS_OK); + ASSERT_EQ(url->GetPath(out), NS_OK); + ASSERT_TRUE(out == NS_LITERAL_CSTRING("/some-path/one-the-net/about.html?with-a-query#for-you")); + + ASSERT_EQ(url->SetQuery(NS_LITERAL_CSTRING("a=b&d=c&what-ever-you-want-to-be-called=45")), NS_OK); + ASSERT_EQ(url->GetQuery(out), NS_OK); + ASSERT_TRUE(out == NS_LITERAL_CSTRING("a=b&d=c&what-ever-you-want-to-be-called=45")); + + ASSERT_EQ(url->SetRef(NS_LITERAL_CSTRING("#some-book-mark")), NS_OK); + ASSERT_EQ(url->GetRef(out), NS_OK); + ASSERT_TRUE(out == NS_LITERAL_CSTRING("some-book-mark")); +} + +#define COUNT 10000 + +MOZ_GTEST_BENCH(TestStandardURL, Perf, [] { + nsCOMPtr<nsIURL> url( do_CreateInstance(NS_STANDARDURL_CONTRACTID) ); + ASSERT_TRUE(url); + nsAutoCString out; + + for (int i = COUNT; i; --i) { + ASSERT_EQ(url->SetSpec(NS_LITERAL_CSTRING("http://example.com")), NS_OK); + ASSERT_EQ(url->GetSpec(out), NS_OK); + url->Resolve(NS_LITERAL_CSTRING("foo.html?q=45"), out); + url->SetScheme(NS_LITERAL_CSTRING("foo")); + url->GetScheme(out); + url->SetHost(NS_LITERAL_CSTRING("www.yahoo.com")); + url->GetHost(out); + url->SetPath(NS_LITERAL_CSTRING("/some-path/one-the-net/about.html?with-a-query#for-you")); + url->GetPath(out); + url->SetQuery(NS_LITERAL_CSTRING("a=b&d=c&what-ever-you-want-to-be-called=45")); + url->GetQuery(out); + url->SetRef(NS_LITERAL_CSTRING("#some-book-mark")); + url->GetRef(out); + } +}); diff --git a/netwerk/test/gtest/moz.build b/netwerk/test/gtest/moz.build new file mode 100644 index 0000000000..6e6c801521 --- /dev/null +++ b/netwerk/test/gtest/moz.build @@ -0,0 +1,14 @@ +# -*- Mode: python; c-basic-offset: 4; 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/. + +UNIFIED_SOURCES += [ + 'TestProtocolProxyService.cpp', + 'TestStandardURL.cpp', +] + +include('/ipc/chromium/chromium-config.mozbuild') + +FINAL_LIBRARY = 'xul-gtest' diff --git a/netwerk/test/httpserver/README b/netwerk/test/httpserver/README new file mode 100644 index 0000000000..e253c6d471 --- /dev/null +++ b/netwerk/test/httpserver/README @@ -0,0 +1,101 @@ +httpd.js README +=============== + +httpd.js is a small cross-platform implementation of an HTTP/1.1 server in +JavaScript for the Mozilla platform. + +httpd.js may be used as an XPCOM component, as an inline script in a document +with XPCOM privileges, or from the XPCOM shell (xpcshell). Currently, its most- +supported method of use is from the XPCOM shell, where you can get all the +dynamicity of JS in adding request handlers and the like, but component-based +equivalent functionality is planned. + + +Using httpd.js as an XPCOM Component +------------------------------------ + +First, create an XPT file for nsIHttpServer.idl, using the xpidl tool included +in the Mozilla SDK for the environment in which you wish to run httpd.js. See +<http://developer.mozilla.org/en/docs/XPIDL:xpidl> for further details on how to +do this. + +Next, register httpd.js and nsIHttpServer.xpt in your Mozilla application. In +Firefox, these simply need to be added to the /components directory of your XPI. +Other applications may require use of regxpcom or other techniques; consult the +applicable documentation for further details. + +Finally, create an instance of the server using the following command: + + var server = Components.classes["@mozilla.org/server/jshttp;1"] + .createInstance(Components.interfaces.nsIHttpServer); + +At this point you'll want to initialize the server, since by default it doesn't +serve many useful paths. For more information on this, see the IDL docs for the +nsIHttpServer interface in nsIHttpServer.idl, particularly for +registerDirectory (useful for mapping the contents of directories onto request +paths), registerPathHandler (for setting a custom handler for a specific path on +the server, such as CGI functionality), and registerFile (for mapping a file to +a specific path). + +Finally, you'll want to start (and later stop) the server. Here's some example +code which does this: + + server.start(8080); // port on which server will operate + + // ...server now runs and serves requests... + + server.stop(); + +This server will only respond to requests on 127.0.0.1:8080 or localhost:8080. +If you want it to respond to requests at different hosts (say via a proxy +mechanism), you must use server.identity.add() or server.identity.setPrimary() +to add it. + + +Using httpd.js as an Inline Script or from xpcshell +--------------------------------------------------- + +Using httpd.js as a script or from xpcshell isn't very different from using it +as a component; the only real difference lies in how you create an instance of +the server. To create an instance, do the following: + + var server = new nsHttpServer(); + +You now can use |server| exactly as you would when |server| was created as an +XPCOM component. Note, however, that doing so will trample over the global +namespace, and global values defined in httpd.js will leak into your script. +This may typically be benign, but since some of the global values defined are +constants (specifically, Cc/Ci/Cr as abbreviations for the classes, interfaces, +and results properties of Components), it's possible this trampling could +break your script. In general you should use httpd.js as an XPCOM component +whenever possible. + + +Known Issues +------------ + +httpd.js makes no effort to time out requests, beyond any the socket itself +might or might not provide. I don't believe it provides any by default, but +I haven't verified this. + +Every incoming request is processed by the corresponding request handler +synchronously. In other words, once the first CRLFCRLF of a request is +received, the entire response is created before any new incoming requests can be +served. I anticipate adding asynchronous handler functionality in bug 396226, +but it may be some time before that happens. + +There is no way to access the body of an incoming request. This problem is +merely a symptom of the previous one, and they will probably both be addressed +at the same time. + + +Other Goodies +------------- + +A special testing function, |server|, is provided for use in xpcshell for quick +testing of the server; see the source code for details on its use. You don't +want to use this in a script, however, because doing so will block until the +server is shut down. It's also a good example of how to use the basic +functionality of httpd.js, if you need one. + +Have fun! diff --git a/netwerk/test/httpserver/TODO b/netwerk/test/httpserver/TODO new file mode 100644 index 0000000000..3a95466117 --- /dev/null +++ b/netwerk/test/httpserver/TODO @@ -0,0 +1,17 @@ +Bugs to fix: +- make content-length generation not rely on .available() returning the entire + size of the body stream's contents -- some sort of wrapper (but how does that + work for the unscriptable method WriteSegments, which is good to support from + a performance standpoint?) + +Ideas for future improvements: +- add API to disable response buffering which, when called, causes errors when + you try to do anything other than write to the body stream (i.e., modify + headers or status line) once you've written anything to it -- useful when + storing the entire response in memory is unfeasible (e.g., you're testing + >4GB download characteristics) +- add an API which performs asynchronous response processing (instead of + nsIHttpRequestHandler.handle, which must construct the response before control + returns; |void asyncHandle(request, response)|) -- useful, and can it be done + in JS? +- other awesomeness? diff --git a/netwerk/test/httpserver/httpd.js b/netwerk/test/httpserver/httpd.js new file mode 100644 index 0000000000..5542adfc2e --- /dev/null +++ b/netwerk/test/httpserver/httpd.js @@ -0,0 +1,5376 @@ +/* -*- 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/. */ + +/* + * An implementation of an HTTP server both as a loadable script and as an XPCOM + * component. See the accompanying README file for user documentation on + * httpd.js. + */ + +this.EXPORTED_SYMBOLS = [ + "HTTP_400", + "HTTP_401", + "HTTP_402", + "HTTP_403", + "HTTP_404", + "HTTP_405", + "HTTP_406", + "HTTP_407", + "HTTP_408", + "HTTP_409", + "HTTP_410", + "HTTP_411", + "HTTP_412", + "HTTP_413", + "HTTP_414", + "HTTP_415", + "HTTP_417", + "HTTP_500", + "HTTP_501", + "HTTP_502", + "HTTP_503", + "HTTP_504", + "HTTP_505", + "HttpError", + "HttpServer", +]; + +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cr = Components.results; +const Cu = Components.utils; +const CC = Components.Constructor; + +const PR_UINT32_MAX = Math.pow(2, 32) - 1; + +/** True if debugging output is enabled, false otherwise. */ +var DEBUG = false; // non-const *only* so tweakable in server tests + +/** True if debugging output should be timestamped. */ +var DEBUG_TIMESTAMP = false; // non-const so tweakable in server tests + +var gGlobalObject = this; + +/** + * Asserts that the given condition holds. If it doesn't, the given message is + * dumped, a stack trace is printed, and an exception is thrown to attempt to + * stop execution (which unfortunately must rely upon the exception not being + * accidentally swallowed by the code that uses it). + */ +function NS_ASSERT(cond, msg) +{ + if (DEBUG && !cond) + { + dumpn("###!!!"); + dumpn("###!!! ASSERTION" + (msg ? ": " + msg : "!")); + dumpn("###!!! Stack follows:"); + + var stack = new Error().stack.split(/\n/); + dumpn(stack.map(function(val) { return "###!!! " + val; }).join("\n")); + + throw Cr.NS_ERROR_ABORT; + } +} + +/** Constructs an HTTP error object. */ +this.HttpError = function HttpError(code, description) +{ + this.code = code; + this.description = description; +} +HttpError.prototype = +{ + toString: function() + { + return this.code + " " + this.description; + } +}; + +/** + * Errors thrown to trigger specific HTTP server responses. + */ +this.HTTP_400 = new HttpError(400, "Bad Request"); +this.HTTP_401 = new HttpError(401, "Unauthorized"); +this.HTTP_402 = new HttpError(402, "Payment Required"); +this.HTTP_403 = new HttpError(403, "Forbidden"); +this.HTTP_404 = new HttpError(404, "Not Found"); +this.HTTP_405 = new HttpError(405, "Method Not Allowed"); +this.HTTP_406 = new HttpError(406, "Not Acceptable"); +this.HTTP_407 = new HttpError(407, "Proxy Authentication Required"); +this.HTTP_408 = new HttpError(408, "Request Timeout"); +this.HTTP_409 = new HttpError(409, "Conflict"); +this.HTTP_410 = new HttpError(410, "Gone"); +this.HTTP_411 = new HttpError(411, "Length Required"); +this.HTTP_412 = new HttpError(412, "Precondition Failed"); +this.HTTP_413 = new HttpError(413, "Request Entity Too Large"); +this.HTTP_414 = new HttpError(414, "Request-URI Too Long"); +this.HTTP_415 = new HttpError(415, "Unsupported Media Type"); +this.HTTP_417 = new HttpError(417, "Expectation Failed"); + +this.HTTP_500 = new HttpError(500, "Internal Server Error"); +this.HTTP_501 = new HttpError(501, "Not Implemented"); +this.HTTP_502 = new HttpError(502, "Bad Gateway"); +this.HTTP_503 = new HttpError(503, "Service Unavailable"); +this.HTTP_504 = new HttpError(504, "Gateway Timeout"); +this.HTTP_505 = new HttpError(505, "HTTP Version Not Supported"); + +/** Creates a hash with fields corresponding to the values in arr. */ +function array2obj(arr) +{ + var obj = {}; + for (var i = 0; i < arr.length; i++) + obj[arr[i]] = arr[i]; + return obj; +} + +/** Returns an array of the integers x through y, inclusive. */ +function range(x, y) +{ + var arr = []; + for (var i = x; i <= y; i++) + arr.push(i); + return arr; +} + +/** An object (hash) whose fields are the numbers of all HTTP error codes. */ +const HTTP_ERROR_CODES = array2obj(range(400, 417).concat(range(500, 505))); + + +/** + * The character used to distinguish hidden files from non-hidden files, a la + * the leading dot in Apache. Since that mechanism also hides files from + * easy display in LXR, ls output, etc. however, we choose instead to use a + * suffix character. If a requested file ends with it, we append another + * when getting the file on the server. If it doesn't, we just look up that + * file. Therefore, any file whose name ends with exactly one of the character + * is "hidden" and available for use by the server. + */ +const HIDDEN_CHAR = "^"; + +/** + * The file name suffix indicating the file containing overridden headers for + * a requested file. + */ +const HEADERS_SUFFIX = HIDDEN_CHAR + "headers" + HIDDEN_CHAR; + +/** Type used to denote SJS scripts for CGI-like functionality. */ +const SJS_TYPE = "sjs"; + +/** Base for relative timestamps produced by dumpn(). */ +var firstStamp = 0; + +/** dump(str) with a trailing "\n" -- only outputs if DEBUG. */ +function dumpn(str) +{ + if (DEBUG) + { + var prefix = "HTTPD-INFO | "; + if (DEBUG_TIMESTAMP) + { + if (firstStamp === 0) + firstStamp = Date.now(); + + var elapsed = Date.now() - firstStamp; // milliseconds + var min = Math.floor(elapsed / 60000); + var sec = (elapsed % 60000) / 1000; + + if (sec < 10) + prefix += min + ":0" + sec.toFixed(3) + " | "; + else + prefix += min + ":" + sec.toFixed(3) + " | "; + } + + dump(prefix + str + "\n"); + } +} + +/** Dumps the current JS stack if DEBUG. */ +function dumpStack() +{ + // peel off the frames for dumpStack() and Error() + var stack = new Error().stack.split(/\n/).slice(2); + stack.forEach(dumpn); +} + + +/** The XPCOM thread manager. */ +var gThreadManager = null; + +/** The XPCOM prefs service. */ +var gRootPrefBranch = null; +function getRootPrefBranch() +{ + if (!gRootPrefBranch) + { + gRootPrefBranch = Cc["@mozilla.org/preferences-service;1"] + .getService(Ci.nsIPrefBranch); + } + return gRootPrefBranch; +} + +/** + * JavaScript constructors for commonly-used classes; precreating these is a + * speedup over doing the same from base principles. See the docs at + * http://developer.mozilla.org/en/docs/Components.Constructor for details. + */ +const ServerSocket = CC("@mozilla.org/network/server-socket;1", + "nsIServerSocket", + "init"); +const ScriptableInputStream = CC("@mozilla.org/scriptableinputstream;1", + "nsIScriptableInputStream", + "init"); +const Pipe = CC("@mozilla.org/pipe;1", + "nsIPipe", + "init"); +const FileInputStream = CC("@mozilla.org/network/file-input-stream;1", + "nsIFileInputStream", + "init"); +const ConverterInputStream = CC("@mozilla.org/intl/converter-input-stream;1", + "nsIConverterInputStream", + "init"); +const WritablePropertyBag = CC("@mozilla.org/hash-property-bag;1", + "nsIWritablePropertyBag2"); +const SupportsString = CC("@mozilla.org/supports-string;1", + "nsISupportsString"); + +/* These two are non-const only so a test can overwrite them. */ +var BinaryInputStream = CC("@mozilla.org/binaryinputstream;1", + "nsIBinaryInputStream", + "setInputStream"); +var BinaryOutputStream = CC("@mozilla.org/binaryoutputstream;1", + "nsIBinaryOutputStream", + "setOutputStream"); + +/** + * Returns the RFC 822/1123 representation of a date. + * + * @param date : Number + * the date, in milliseconds from midnight (00:00:00), January 1, 1970 GMT + * @returns string + * the representation of the given date + */ +function toDateString(date) +{ + // + // rfc1123-date = wkday "," SP date1 SP time SP "GMT" + // date1 = 2DIGIT SP month SP 4DIGIT + // ; day month year (e.g., 02 Jun 1982) + // time = 2DIGIT ":" 2DIGIT ":" 2DIGIT + // ; 00:00:00 - 23:59:59 + // wkday = "Mon" | "Tue" | "Wed" + // | "Thu" | "Fri" | "Sat" | "Sun" + // month = "Jan" | "Feb" | "Mar" | "Apr" + // | "May" | "Jun" | "Jul" | "Aug" + // | "Sep" | "Oct" | "Nov" | "Dec" + // + + const wkdayStrings = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; + const monthStrings = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", + "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; + + /** + * Processes a date and returns the encoded UTC time as a string according to + * the format specified in RFC 2616. + * + * @param date : Date + * the date to process + * @returns string + * a string of the form "HH:MM:SS", ranging from "00:00:00" to "23:59:59" + */ + function toTime(date) + { + var hrs = date.getUTCHours(); + var rv = (hrs < 10) ? "0" + hrs : hrs; + + var mins = date.getUTCMinutes(); + rv += ":"; + rv += (mins < 10) ? "0" + mins : mins; + + var secs = date.getUTCSeconds(); + rv += ":"; + rv += (secs < 10) ? "0" + secs : secs; + + return rv; + } + + /** + * Processes a date and returns the encoded UTC date as a string according to + * the date1 format specified in RFC 2616. + * + * @param date : Date + * the date to process + * @returns string + * a string of the form "HH:MM:SS", ranging from "00:00:00" to "23:59:59" + */ + function toDate1(date) + { + var day = date.getUTCDate(); + var month = date.getUTCMonth(); + var year = date.getUTCFullYear(); + + var rv = (day < 10) ? "0" + day : day; + rv += " " + monthStrings[month]; + rv += " " + year; + + return rv; + } + + date = new Date(date); + + const fmtString = "%wkday%, %date1% %time% GMT"; + var rv = fmtString.replace("%wkday%", wkdayStrings[date.getUTCDay()]); + rv = rv.replace("%time%", toTime(date)); + return rv.replace("%date1%", toDate1(date)); +} + +/** + * Prints out a human-readable representation of the object o and its fields, + * omitting those whose names begin with "_" if showMembers != true (to ignore + * "private" properties exposed via getters/setters). + */ +function printObj(o, showMembers) +{ + var s = "******************************\n"; + s += "o = {\n"; + for (var i in o) + { + if (typeof(i) != "string" || + (showMembers || (i.length > 0 && i[0] != "_"))) + s+= " " + i + ": " + o[i] + ",\n"; + } + s += " };\n"; + s += "******************************"; + dumpn(s); +} + +/** + * Instantiates a new HTTP server. + */ +function nsHttpServer() +{ + if (!gThreadManager) + gThreadManager = Cc["@mozilla.org/thread-manager;1"].getService(); + + /** The port on which this server listens. */ + this._port = undefined; + + /** The socket associated with this. */ + this._socket = null; + + /** The handler used to process requests to this server. */ + this._handler = new ServerHandler(this); + + /** Naming information for this server. */ + this._identity = new ServerIdentity(); + + /** + * Indicates when the server is to be shut down at the end of the request. + */ + this._doQuit = false; + + /** + * True if the socket in this is closed (and closure notifications have been + * sent and processed if the socket was ever opened), false otherwise. + */ + this._socketClosed = true; + + /** + * Used for tracking existing connections and ensuring that all connections + * are properly cleaned up before server shutdown; increases by 1 for every + * new incoming connection. + */ + this._connectionGen = 0; + + /** + * Hash of all open connections, indexed by connection number at time of + * creation. + */ + this._connections = {}; +} +nsHttpServer.prototype = +{ + classID: Components.ID("{54ef6f81-30af-4b1d-ac55-8ba811293e41}"), + + // NSISERVERSOCKETLISTENER + + /** + * Processes an incoming request coming in on the given socket and contained + * in the given transport. + * + * @param socket : nsIServerSocket + * the socket through which the request was served + * @param trans : nsISocketTransport + * the transport for the request/response + * @see nsIServerSocketListener.onSocketAccepted + */ + onSocketAccepted: function(socket, trans) + { + dumpn("*** onSocketAccepted(socket=" + socket + ", trans=" + trans + ")"); + + dumpn(">>> new connection on " + trans.host + ":" + trans.port); + + const SEGMENT_SIZE = 8192; + const SEGMENT_COUNT = 1024; + try + { + var input = trans.openInputStream(0, SEGMENT_SIZE, SEGMENT_COUNT) + .QueryInterface(Ci.nsIAsyncInputStream); + var output = trans.openOutputStream(0, 0, 0); + } + catch (e) + { + dumpn("*** error opening transport streams: " + e); + trans.close(Cr.NS_BINDING_ABORTED); + return; + } + + var connectionNumber = ++this._connectionGen; + + try + { + var conn = new Connection(input, output, this, socket.port, trans.port, + connectionNumber); + var reader = new RequestReader(conn); + + // XXX add request timeout functionality here! + + // Note: must use main thread here, or we might get a GC that will cause + // threadsafety assertions. We really need to fix XPConnect so that + // you can actually do things in multi-threaded JS. :-( + input.asyncWait(reader, 0, 0, gThreadManager.mainThread); + } + catch (e) + { + // Assume this connection can't be salvaged and bail on it completely; + // don't attempt to close it so that we can assert that any connection + // being closed is in this._connections. + dumpn("*** error in initial request-processing stages: " + e); + trans.close(Cr.NS_BINDING_ABORTED); + return; + } + + this._connections[connectionNumber] = conn; + dumpn("*** starting connection " + connectionNumber); + }, + + /** + * Called when the socket associated with this is closed. + * + * @param socket : nsIServerSocket + * the socket being closed + * @param status : nsresult + * the reason the socket stopped listening (NS_BINDING_ABORTED if the server + * was stopped using nsIHttpServer.stop) + * @see nsIServerSocketListener.onStopListening + */ + onStopListening: function(socket, status) + { + dumpn(">>> shutting down server on port " + socket.port); + for (var n in this._connections) { + if (!this._connections[n]._requestStarted) { + this._connections[n].close(); + } + } + this._socketClosed = true; + if (this._hasOpenConnections()) { + dumpn("*** open connections!!!"); + } + if (!this._hasOpenConnections()) + { + dumpn("*** no open connections, notifying async from onStopListening"); + + // Notify asynchronously so that any pending teardown in stop() has a + // chance to run first. + var self = this; + var stopEvent = + { + run: function() + { + dumpn("*** _notifyStopped async callback"); + self._notifyStopped(); + } + }; + gThreadManager.currentThread + .dispatch(stopEvent, Ci.nsIThread.DISPATCH_NORMAL); + } + }, + + // NSIHTTPSERVER + + // + // see nsIHttpServer.start + // + start: function(port) + { + this._start(port, "localhost") + }, + + _start: function(port, host) + { + if (this._socket) + throw Cr.NS_ERROR_ALREADY_INITIALIZED; + + this._port = port; + this._doQuit = this._socketClosed = false; + + this._host = host; + + // The listen queue needs to be long enough to handle + // network.http.max-persistent-connections-per-server or + // network.http.max-persistent-connections-per-proxy concurrent + // connections, plus a safety margin in case some other process is + // talking to the server as well. + var prefs = getRootPrefBranch(); + var maxConnections = 5 + Math.max( + prefs.getIntPref("network.http.max-persistent-connections-per-server"), + prefs.getIntPref("network.http.max-persistent-connections-per-proxy")); + + try + { + var loopback = true; + if (this._host != "127.0.0.1" && this._host != "localhost") { + var loopback = false; + } + + // When automatically selecting a port, sometimes the chosen port is + // "blocked" from clients. We don't want to use these ports because + // tests will intermittently fail. So, we simply keep trying to to + // get a server socket until a valid port is obtained. We limit + // ourselves to finite attempts just so we don't loop forever. + var ios = Cc["@mozilla.org/network/io-service;1"] + .getService(Ci.nsIIOService); + var socket; + for (var i = 100; i; i--) + { + var temp = new ServerSocket(this._port, + loopback, // true = localhost, false = everybody + maxConnections); + + var allowed = ios.allowPort(temp.port, "http"); + if (!allowed) + { + dumpn(">>>Warning: obtained ServerSocket listens on a blocked " + + "port: " + temp.port); + } + + if (!allowed && this._port == -1) + { + dumpn(">>>Throwing away ServerSocket with bad port."); + temp.close(); + continue; + } + + socket = temp; + break; + } + + if (!socket) { + throw new Error("No socket server available. Are there no available ports?"); + } + + dumpn(">>> listening on port " + socket.port + ", " + maxConnections + + " pending connections"); + socket.asyncListen(this); + this._port = socket.port; + this._identity._initialize(socket.port, host, true); + this._socket = socket; + } + catch (e) + { + dump("\n!!! could not start server on port " + port + ": " + e + "\n\n"); + throw Cr.NS_ERROR_NOT_AVAILABLE; + } + }, + + // + // see nsIHttpServer.stop + // + stop: function(callback) + { + if (!callback) + throw Cr.NS_ERROR_NULL_POINTER; + if (!this._socket) + throw Cr.NS_ERROR_UNEXPECTED; + + this._stopCallback = typeof callback === "function" + ? callback + : function() { callback.onStopped(); }; + + dumpn(">>> stopping listening on port " + this._socket.port); + this._socket.close(); + this._socket = null; + + // We can't have this identity any more, and the port on which we're running + // this server now could be meaningless the next time around. + this._identity._teardown(); + + this._doQuit = false; + + // socket-close notification and pending request completion happen async + }, + + // + // see nsIHttpServer.registerFile + // + registerFile: function(path, file) + { + if (file && (!file.exists() || file.isDirectory())) + throw Cr.NS_ERROR_INVALID_ARG; + + this._handler.registerFile(path, file); + }, + + // + // see nsIHttpServer.registerDirectory + // + registerDirectory: function(path, directory) + { + // XXX true path validation! + if (path.charAt(0) != "/" || + path.charAt(path.length - 1) != "/" || + (directory && + (!directory.exists() || !directory.isDirectory()))) + throw Cr.NS_ERROR_INVALID_ARG; + + // XXX determine behavior of nonexistent /foo/bar when a /foo/bar/ mapping + // exists! + + this._handler.registerDirectory(path, directory); + }, + + // + // see nsIHttpServer.registerPathHandler + // + registerPathHandler: function(path, handler) + { + this._handler.registerPathHandler(path, handler); + }, + + // + // see nsIHttpServer.registerPrefixHandler + // + registerPrefixHandler: function(prefix, handler) + { + this._handler.registerPrefixHandler(prefix, handler); + }, + + // + // see nsIHttpServer.registerErrorHandler + // + registerErrorHandler: function(code, handler) + { + this._handler.registerErrorHandler(code, handler); + }, + + // + // see nsIHttpServer.setIndexHandler + // + setIndexHandler: function(handler) + { + this._handler.setIndexHandler(handler); + }, + + // + // see nsIHttpServer.registerContentType + // + registerContentType: function(ext, type) + { + this._handler.registerContentType(ext, type); + }, + + // + // see nsIHttpServer.serverIdentity + // + get identity() + { + return this._identity; + }, + + // + // see nsIHttpServer.getState + // + getState: function(path, k) + { + return this._handler._getState(path, k); + }, + + // + // see nsIHttpServer.setState + // + setState: function(path, k, v) + { + return this._handler._setState(path, k, v); + }, + + // + // see nsIHttpServer.getSharedState + // + getSharedState: function(k) + { + return this._handler._getSharedState(k); + }, + + // + // see nsIHttpServer.setSharedState + // + setSharedState: function(k, v) + { + return this._handler._setSharedState(k, v); + }, + + // + // see nsIHttpServer.getObjectState + // + getObjectState: function(k) + { + return this._handler._getObjectState(k); + }, + + // + // see nsIHttpServer.setObjectState + // + setObjectState: function(k, v) + { + return this._handler._setObjectState(k, v); + }, + + + // NSISUPPORTS + + // + // see nsISupports.QueryInterface + // + QueryInterface: function(iid) + { + if (iid.equals(Ci.nsIHttpServer) || + iid.equals(Ci.nsIServerSocketListener) || + iid.equals(Ci.nsISupports)) + return this; + + throw Cr.NS_ERROR_NO_INTERFACE; + }, + + + // NON-XPCOM PUBLIC API + + /** + * Returns true iff this server is not running (and is not in the process of + * serving any requests still to be processed when the server was last + * stopped after being run). + */ + isStopped: function() + { + return this._socketClosed && !this._hasOpenConnections(); + }, + + // PRIVATE IMPLEMENTATION + + /** True if this server has any open connections to it, false otherwise. */ + _hasOpenConnections: function() + { + // + // If we have any open connections, they're tracked as numeric properties on + // |this._connections|. The non-standard __count__ property could be used + // to check whether there are any properties, but standard-wise, even + // looking forward to ES5, there's no less ugly yet still O(1) way to do + // this. + // + for (var n in this._connections) + return true; + return false; + }, + + /** Calls the server-stopped callback provided when stop() was called. */ + _notifyStopped: function() + { + NS_ASSERT(this._stopCallback !== null, "double-notifying?"); + NS_ASSERT(!this._hasOpenConnections(), "should be done serving by now"); + + // + // NB: We have to grab this now, null out the member, *then* call the + // callback here, or otherwise the callback could (indirectly) futz with + // this._stopCallback by starting and immediately stopping this, at + // which point we'd be nulling out a field we no longer have a right to + // modify. + // + var callback = this._stopCallback; + this._stopCallback = null; + try + { + callback(); + } + catch (e) + { + // not throwing because this is specified as being usually (but not + // always) asynchronous + dump("!!! error running onStopped callback: " + e + "\n"); + } + }, + + /** + * Notifies this server that the given connection has been closed. + * + * @param connection : Connection + * the connection that was closed + */ + _connectionClosed: function(connection) + { + NS_ASSERT(connection.number in this._connections, + "closing a connection " + this + " that we never added to the " + + "set of open connections?"); + NS_ASSERT(this._connections[connection.number] === connection, + "connection number mismatch? " + + this._connections[connection.number]); + delete this._connections[connection.number]; + + // Fire a pending server-stopped notification if it's our responsibility. + if (!this._hasOpenConnections() && this._socketClosed) + this._notifyStopped(); + // Bug 508125: Add a GC here else we'll use gigabytes of memory running + // mochitests. We can't rely on xpcshell doing an automated GC, as that + // would interfere with testing GC stuff... + Components.utils.forceGC(); + }, + + /** + * Requests that the server be shut down when possible. + */ + _requestQuit: function() + { + dumpn(">>> requesting a quit"); + dumpStack(); + this._doQuit = true; + } +}; + +this.HttpServer = nsHttpServer; + +// +// RFC 2396 section 3.2.2: +// +// host = hostname | IPv4address +// hostname = *( domainlabel "." ) toplabel [ "." ] +// domainlabel = alphanum | alphanum *( alphanum | "-" ) alphanum +// toplabel = alpha | alpha *( alphanum | "-" ) alphanum +// IPv4address = 1*digit "." 1*digit "." 1*digit "." 1*digit +// + +const HOST_REGEX = + new RegExp("^(?:" + + // *( domainlabel "." ) + "(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)*" + + // toplabel + "[a-z](?:[a-z0-9-]*[a-z0-9])?" + + "|" + + // IPv4 address + "\\d+\\.\\d+\\.\\d+\\.\\d+" + + ")$", + "i"); + + +/** + * Represents the identity of a server. An identity consists of a set of + * (scheme, host, port) tuples denoted as locations (allowing a single server to + * serve multiple sites or to be used behind both HTTP and HTTPS proxies for any + * host/port). Any incoming request must be to one of these locations, or it + * will be rejected with an HTTP 400 error. One location, denoted as the + * primary location, is the location assigned in contexts where a location + * cannot otherwise be endogenously derived, such as for HTTP/1.0 requests. + * + * A single identity may contain at most one location per unique host/port pair; + * other than that, no restrictions are placed upon what locations may + * constitute an identity. + */ +function ServerIdentity() +{ + /** The scheme of the primary location. */ + this._primaryScheme = "http"; + + /** The hostname of the primary location. */ + this._primaryHost = "127.0.0.1" + + /** The port number of the primary location. */ + this._primaryPort = -1; + + /** + * The current port number for the corresponding server, stored so that a new + * primary location can always be set if the current one is removed. + */ + this._defaultPort = -1; + + /** + * Maps hosts to maps of ports to schemes, e.g. the following would represent + * https://example.com:789/ and http://example.org/: + * + * { + * "xexample.com": { 789: "https" }, + * "xexample.org": { 80: "http" } + * } + * + * Note the "x" prefix on hostnames, which prevents collisions with special + * JS names like "prototype". + */ + this._locations = { "xlocalhost": {} }; +} +ServerIdentity.prototype = +{ + // NSIHTTPSERVERIDENTITY + + // + // see nsIHttpServerIdentity.primaryScheme + // + get primaryScheme() + { + if (this._primaryPort === -1) + throw Cr.NS_ERROR_NOT_INITIALIZED; + return this._primaryScheme; + }, + + // + // see nsIHttpServerIdentity.primaryHost + // + get primaryHost() + { + if (this._primaryPort === -1) + throw Cr.NS_ERROR_NOT_INITIALIZED; + return this._primaryHost; + }, + + // + // see nsIHttpServerIdentity.primaryPort + // + get primaryPort() + { + if (this._primaryPort === -1) + throw Cr.NS_ERROR_NOT_INITIALIZED; + return this._primaryPort; + }, + + // + // see nsIHttpServerIdentity.add + // + add: function(scheme, host, port) + { + this._validate(scheme, host, port); + + var entry = this._locations["x" + host]; + if (!entry) + this._locations["x" + host] = entry = {}; + + entry[port] = scheme; + }, + + // + // see nsIHttpServerIdentity.remove + // + remove: function(scheme, host, port) + { + this._validate(scheme, host, port); + + var entry = this._locations["x" + host]; + if (!entry) + return false; + + var present = port in entry; + delete entry[port]; + + if (this._primaryScheme == scheme && + this._primaryHost == host && + this._primaryPort == port && + this._defaultPort !== -1) + { + // Always keep at least one identity in existence at any time, unless + // we're in the process of shutting down (the last condition above). + this._primaryPort = -1; + this._initialize(this._defaultPort, host, false); + } + + return present; + }, + + // + // see nsIHttpServerIdentity.has + // + has: function(scheme, host, port) + { + this._validate(scheme, host, port); + + return "x" + host in this._locations && + scheme === this._locations["x" + host][port]; + }, + + // + // see nsIHttpServerIdentity.has + // + getScheme: function(host, port) + { + this._validate("http", host, port); + + var entry = this._locations["x" + host]; + if (!entry) + return ""; + + return entry[port] || ""; + }, + + // + // see nsIHttpServerIdentity.setPrimary + // + setPrimary: function(scheme, host, port) + { + this._validate(scheme, host, port); + + this.add(scheme, host, port); + + this._primaryScheme = scheme; + this._primaryHost = host; + this._primaryPort = port; + }, + + + // NSISUPPORTS + + // + // see nsISupports.QueryInterface + // + QueryInterface: function(iid) + { + if (iid.equals(Ci.nsIHttpServerIdentity) || iid.equals(Ci.nsISupports)) + return this; + + throw Cr.NS_ERROR_NO_INTERFACE; + }, + + + // PRIVATE IMPLEMENTATION + + /** + * Initializes the primary name for the corresponding server, based on the + * provided port number. + */ + _initialize: function(port, host, addSecondaryDefault) + { + this._host = host; + if (this._primaryPort !== -1) + this.add("http", host, port); + else + this.setPrimary("http", "localhost", port); + this._defaultPort = port; + + // Only add this if we're being called at server startup + if (addSecondaryDefault && host != "127.0.0.1") + this.add("http", "127.0.0.1", port); + }, + + /** + * Called at server shutdown time, unsets the primary location only if it was + * the default-assigned location and removes the default location from the + * set of locations used. + */ + _teardown: function() + { + if (this._host != "127.0.0.1") { + // Not the default primary location, nothing special to do here + this.remove("http", "127.0.0.1", this._defaultPort); + } + + // This is a *very* tricky bit of reasoning here; make absolutely sure the + // tests for this code pass before you commit changes to it. + if (this._primaryScheme == "http" && + this._primaryHost == this._host && + this._primaryPort == this._defaultPort) + { + // Make sure we don't trigger the readding logic in .remove(), then remove + // the default location. + var port = this._defaultPort; + this._defaultPort = -1; + this.remove("http", this._host, port); + + // Ensure a server start triggers the setPrimary() path in ._initialize() + this._primaryPort = -1; + } + else + { + // No reason not to remove directly as it's not our primary location + this.remove("http", this._host, this._defaultPort); + } + }, + + /** + * Ensures scheme, host, and port are all valid with respect to RFC 2396. + * + * @throws NS_ERROR_ILLEGAL_VALUE + * if any argument doesn't match the corresponding production + */ + _validate: function(scheme, host, port) + { + if (scheme !== "http" && scheme !== "https") + { + dumpn("*** server only supports http/https schemes: '" + scheme + "'"); + dumpStack(); + throw Cr.NS_ERROR_ILLEGAL_VALUE; + } + if (!HOST_REGEX.test(host)) + { + dumpn("*** unexpected host: '" + host + "'"); + throw Cr.NS_ERROR_ILLEGAL_VALUE; + } + if (port < 0 || port > 65535) + { + dumpn("*** unexpected port: '" + port + "'"); + throw Cr.NS_ERROR_ILLEGAL_VALUE; + } + } +}; + + +/** + * Represents a connection to the server (and possibly in the future the thread + * on which the connection is processed). + * + * @param input : nsIInputStream + * stream from which incoming data on the connection is read + * @param output : nsIOutputStream + * stream to write data out the connection + * @param server : nsHttpServer + * the server handling the connection + * @param port : int + * the port on which the server is running + * @param outgoingPort : int + * the outgoing port used by this connection + * @param number : uint + * a serial number used to uniquely identify this connection + */ +function Connection(input, output, server, port, outgoingPort, number) +{ + dumpn("*** opening new connection " + number + " on port " + outgoingPort); + + /** Stream of incoming data. */ + this.input = input; + + /** Stream for outgoing data. */ + this.output = output; + + /** The server associated with this request. */ + this.server = server; + + /** The port on which the server is running. */ + this.port = port; + + /** The outgoing poort used by this connection. */ + this._outgoingPort = outgoingPort; + + /** The serial number of this connection. */ + this.number = number; + + /** + * The request for which a response is being generated, null if the + * incoming request has not been fully received or if it had errors. + */ + this.request = null; + + /** This allows a connection to disambiguate between a peer initiating a + * close and the socket being forced closed on shutdown. + */ + this._closed = false; + + /** State variable for debugging. */ + this._processed = false; + + /** whether or not 1st line of request has been received */ + this._requestStarted = false; +} +Connection.prototype = +{ + /** Closes this connection's input/output streams. */ + close: function() + { + if (this._closed) + return; + + dumpn("*** closing connection " + this.number + + " on port " + this._outgoingPort); + + this.input.close(); + this.output.close(); + this._closed = true; + + var server = this.server; + server._connectionClosed(this); + + // If an error triggered a server shutdown, act on it now + if (server._doQuit) + server.stop(function() { /* not like we can do anything better */ }); + }, + + /** + * Initiates processing of this connection, using the data in the given + * request. + * + * @param request : Request + * the request which should be processed + */ + process: function(request) + { + NS_ASSERT(!this._closed && !this._processed); + + this._processed = true; + + this.request = request; + this.server._handler.handleResponse(this); + }, + + /** + * Initiates processing of this connection, generating a response with the + * given HTTP error code. + * + * @param code : uint + * an HTTP code, so in the range [0, 1000) + * @param request : Request + * incomplete data about the incoming request (since there were errors + * during its processing + */ + processError: function(code, request) + { + NS_ASSERT(!this._closed && !this._processed); + + this._processed = true; + this.request = request; + this.server._handler.handleError(code, this); + }, + + /** Converts this to a string for debugging purposes. */ + toString: function() + { + return "<Connection(" + this.number + + (this.request ? ", " + this.request.path : "") +"): " + + (this._closed ? "closed" : "open") + ">"; + }, + + requestStarted: function() + { + this._requestStarted = true; + } +}; + + + +/** Returns an array of count bytes from the given input stream. */ +function readBytes(inputStream, count) +{ + return new BinaryInputStream(inputStream).readByteArray(count); +} + + + +/** Request reader processing states; see RequestReader for details. */ +const READER_IN_REQUEST_LINE = 0; +const READER_IN_HEADERS = 1; +const READER_IN_BODY = 2; +const READER_FINISHED = 3; + + +/** + * Reads incoming request data asynchronously, does any necessary preprocessing, + * and forwards it to the request handler. Processing occurs in three states: + * + * READER_IN_REQUEST_LINE Reading the request's status line + * READER_IN_HEADERS Reading headers in the request + * READER_IN_BODY Reading the body of the request + * READER_FINISHED Entire request has been read and processed + * + * During the first two stages, initial metadata about the request is gathered + * into a Request object. Once the status line and headers have been processed, + * we start processing the body of the request into the Request. Finally, when + * the entire body has been read, we create a Response and hand it off to the + * ServerHandler to be given to the appropriate request handler. + * + * @param connection : Connection + * the connection for the request being read + */ +function RequestReader(connection) +{ + /** Connection metadata for this request. */ + this._connection = connection; + + /** + * A container providing line-by-line access to the raw bytes that make up the + * data which has been read from the connection but has not yet been acted + * upon (by passing it to the request handler or by extracting request + * metadata from it). + */ + this._data = new LineData(); + + /** + * The amount of data remaining to be read from the body of this request. + * After all headers in the request have been read this is the value in the + * Content-Length header, but as the body is read its value decreases to zero. + */ + this._contentLength = 0; + + /** The current state of parsing the incoming request. */ + this._state = READER_IN_REQUEST_LINE; + + /** Metadata constructed from the incoming request for the request handler. */ + this._metadata = new Request(connection.port); + + /** + * Used to preserve state if we run out of line data midway through a + * multi-line header. _lastHeaderName stores the name of the header, while + * _lastHeaderValue stores the value we've seen so far for the header. + * + * These fields are always either both undefined or both strings. + */ + this._lastHeaderName = this._lastHeaderValue = undefined; +} +RequestReader.prototype = +{ + // NSIINPUTSTREAMCALLBACK + + /** + * Called when more data from the incoming request is available. This method + * then reads the available data from input and deals with that data as + * necessary, depending upon the syntax of already-downloaded data. + * + * @param input : nsIAsyncInputStream + * the stream of incoming data from the connection + */ + onInputStreamReady: function(input) + { + dumpn("*** onInputStreamReady(input=" + input + ") on thread " + + gThreadManager.currentThread + " (main is " + + gThreadManager.mainThread + ")"); + dumpn("*** this._state == " + this._state); + + // Handle cases where we get more data after a request error has been + // discovered but *before* we can close the connection. + var data = this._data; + if (!data) + return; + + try + { + data.appendBytes(readBytes(input, input.available())); + } + catch (e) + { + if (streamClosed(e)) + { + dumpn("*** WARNING: unexpected error when reading from socket; will " + + "be treated as if the input stream had been closed"); + dumpn("*** WARNING: actual error was: " + e); + } + + // We've lost a race -- input has been closed, but we're still expecting + // to read more data. available() will throw in this case, and since + // we're dead in the water now, destroy the connection. + dumpn("*** onInputStreamReady called on a closed input, destroying " + + "connection"); + this._connection.close(); + return; + } + + switch (this._state) + { + default: + NS_ASSERT(false, "invalid state: " + this._state); + break; + + case READER_IN_REQUEST_LINE: + if (!this._processRequestLine()) + break; + /* fall through */ + + case READER_IN_HEADERS: + if (!this._processHeaders()) + break; + /* fall through */ + + case READER_IN_BODY: + this._processBody(); + } + + if (this._state != READER_FINISHED) + input.asyncWait(this, 0, 0, gThreadManager.currentThread); + }, + + // + // see nsISupports.QueryInterface + // + QueryInterface: function(aIID) + { + if (aIID.equals(Ci.nsIInputStreamCallback) || + aIID.equals(Ci.nsISupports)) + return this; + + throw Cr.NS_ERROR_NO_INTERFACE; + }, + + + // PRIVATE API + + /** + * Processes unprocessed, downloaded data as a request line. + * + * @returns boolean + * true iff the request line has been fully processed + */ + _processRequestLine: function() + { + NS_ASSERT(this._state == READER_IN_REQUEST_LINE); + + // Servers SHOULD ignore any empty line(s) received where a Request-Line + // is expected (section 4.1). + var data = this._data; + var line = {}; + var readSuccess; + while ((readSuccess = data.readLine(line)) && line.value == "") + dumpn("*** ignoring beginning blank line..."); + + // if we don't have a full line, wait until we do + if (!readSuccess) + return false; + + // we have the first non-blank line + try + { + this._parseRequestLine(line.value); + this._state = READER_IN_HEADERS; + this._connection.requestStarted(); + return true; + } + catch (e) + { + this._handleError(e); + return false; + } + }, + + /** + * Processes stored data, assuming it is either at the beginning or in + * the middle of processing request headers. + * + * @returns boolean + * true iff header data in the request has been fully processed + */ + _processHeaders: function() + { + NS_ASSERT(this._state == READER_IN_HEADERS); + + // XXX things to fix here: + // + // - need to support RFC 2047-encoded non-US-ASCII characters + + try + { + var done = this._parseHeaders(); + if (done) + { + var request = this._metadata; + + // XXX this is wrong for requests with transfer-encodings applied to + // them, particularly chunked (which by its nature can have no + // meaningful Content-Length header)! + this._contentLength = request.hasHeader("Content-Length") + ? parseInt(request.getHeader("Content-Length"), 10) + : 0; + dumpn("_processHeaders, Content-length=" + this._contentLength); + + this._state = READER_IN_BODY; + } + return done; + } + catch (e) + { + this._handleError(e); + return false; + } + }, + + /** + * Processes stored data, assuming it is either at the beginning or in + * the middle of processing the request body. + * + * @returns boolean + * true iff the request body has been fully processed + */ + _processBody: function() + { + NS_ASSERT(this._state == READER_IN_BODY); + + // XXX handle chunked transfer-coding request bodies! + + try + { + if (this._contentLength > 0) + { + var data = this._data.purge(); + var count = Math.min(data.length, this._contentLength); + dumpn("*** loading data=" + data + " len=" + data.length + + " excess=" + (data.length - count)); + + var bos = new BinaryOutputStream(this._metadata._bodyOutputStream); + bos.writeByteArray(data, count); + this._contentLength -= count; + } + + dumpn("*** remaining body data len=" + this._contentLength); + if (this._contentLength == 0) + { + this._validateRequest(); + this._state = READER_FINISHED; + this._handleResponse(); + return true; + } + + return false; + } + catch (e) + { + this._handleError(e); + return false; + } + }, + + /** + * Does various post-header checks on the data in this request. + * + * @throws : HttpError + * if the request was malformed in some way + */ + _validateRequest: function() + { + NS_ASSERT(this._state == READER_IN_BODY); + + dumpn("*** _validateRequest"); + + var metadata = this._metadata; + var headers = metadata._headers; + + // 19.6.1.1 -- servers MUST report 400 to HTTP/1.1 requests w/o Host header + var identity = this._connection.server.identity; + if (metadata._httpVersion.atLeast(nsHttpVersion.HTTP_1_1)) + { + if (!headers.hasHeader("Host")) + { + dumpn("*** malformed HTTP/1.1 or greater request with no Host header!"); + throw HTTP_400; + } + + // If the Request-URI wasn't absolute, then we need to determine our host. + // We have to determine what scheme was used to access us based on the + // server identity data at this point, because the request just doesn't + // contain enough data on its own to do this, sadly. + if (!metadata._host) + { + var host, port; + var hostPort = headers.getHeader("Host"); + var colon = hostPort.indexOf(":"); + if (colon < 0) + { + host = hostPort; + port = ""; + } + else + { + host = hostPort.substring(0, colon); + port = hostPort.substring(colon + 1); + } + + // NB: We allow an empty port here because, oddly, a colon may be + // present even without a port number, e.g. "example.com:"; in this + // case the default port applies. + if (!HOST_REGEX.test(host) || !/^\d*$/.test(port)) + { + dumpn("*** malformed hostname (" + hostPort + ") in Host " + + "header, 400 time"); + throw HTTP_400; + } + + // If we're not given a port, we're stuck, because we don't know what + // scheme to use to look up the correct port here, in general. Since + // the HTTPS case requires a tunnel/proxy and thus requires that the + // requested URI be absolute (and thus contain the necessary + // information), let's assume HTTP will prevail and use that. + port = +port || 80; + + var scheme = identity.getScheme(host, port); + if (!scheme) + { + dumpn("*** unrecognized hostname (" + hostPort + ") in Host " + + "header, 400 time"); + throw HTTP_400; + } + + metadata._scheme = scheme; + metadata._host = host; + metadata._port = port; + } + } + else + { + NS_ASSERT(metadata._host === undefined, + "HTTP/1.0 doesn't allow absolute paths in the request line!"); + + metadata._scheme = identity.primaryScheme; + metadata._host = identity.primaryHost; + metadata._port = identity.primaryPort; + } + + NS_ASSERT(identity.has(metadata._scheme, metadata._host, metadata._port), + "must have a location we recognize by now!"); + }, + + /** + * Handles responses in case of error, either in the server or in the request. + * + * @param e + * the specific error encountered, which is an HttpError in the case where + * the request is in some way invalid or cannot be fulfilled; if this isn't + * an HttpError we're going to be paranoid and shut down, because that + * shouldn't happen, ever + */ + _handleError: function(e) + { + // Don't fall back into normal processing! + this._state = READER_FINISHED; + + var server = this._connection.server; + if (e instanceof HttpError) + { + var code = e.code; + } + else + { + dumpn("!!! UNEXPECTED ERROR: " + e + + (e.lineNumber ? ", line " + e.lineNumber : "")); + + // no idea what happened -- be paranoid and shut down + code = 500; + server._requestQuit(); + } + + // make attempted reuse of data an error + this._data = null; + + this._connection.processError(code, this._metadata); + }, + + /** + * Now that we've read the request line and headers, we can actually hand off + * the request to be handled. + * + * This method is called once per request, after the request line and all + * headers and the body, if any, have been received. + */ + _handleResponse: function() + { + NS_ASSERT(this._state == READER_FINISHED); + + // We don't need the line-based data any more, so make attempted reuse an + // error. + this._data = null; + + this._connection.process(this._metadata); + }, + + + // PARSING + + /** + * Parses the request line for the HTTP request associated with this. + * + * @param line : string + * the request line + */ + _parseRequestLine: function(line) + { + NS_ASSERT(this._state == READER_IN_REQUEST_LINE); + + dumpn("*** _parseRequestLine('" + line + "')"); + + var metadata = this._metadata; + + // clients and servers SHOULD accept any amount of SP or HT characters + // between fields, even though only a single SP is required (section 19.3) + var request = line.split(/[ \t]+/); + if (!request || request.length != 3) + { + dumpn("*** No request in line"); + throw HTTP_400; + } + + metadata._method = request[0]; + + // get the HTTP version + var ver = request[2]; + var match = ver.match(/^HTTP\/(\d+\.\d+)$/); + if (!match) + { + dumpn("*** No HTTP version in line"); + throw HTTP_400; + } + + // determine HTTP version + try + { + metadata._httpVersion = new nsHttpVersion(match[1]); + if (!metadata._httpVersion.atLeast(nsHttpVersion.HTTP_1_0)) + throw "unsupported HTTP version"; + } + catch (e) + { + // we support HTTP/1.0 and HTTP/1.1 only + throw HTTP_501; + } + + + var fullPath = request[1]; + var serverIdentity = this._connection.server.identity; + + var scheme, host, port; + + if (fullPath.charAt(0) != "/") + { + // No absolute paths in the request line in HTTP prior to 1.1 + if (!metadata._httpVersion.atLeast(nsHttpVersion.HTTP_1_1)) + { + dumpn("*** Metadata version too low"); + throw HTTP_400; + } + + try + { + var uri = Cc["@mozilla.org/network/io-service;1"] + .getService(Ci.nsIIOService) + .newURI(fullPath, null, null); + fullPath = uri.path; + scheme = uri.scheme; + host = metadata._host = uri.asciiHost; + port = uri.port; + if (port === -1) + { + if (scheme === "http") + { + port = 80; + } + else if (scheme === "https") + { + port = 443; + } + else + { + dumpn("*** Unknown scheme: " + scheme); + throw HTTP_400; + } + } + } + catch (e) + { + // If the host is not a valid host on the server, the response MUST be a + // 400 (Bad Request) error message (section 5.2). Alternately, the URI + // is malformed. + dumpn("*** Threw when dealing with URI: " + e); + throw HTTP_400; + } + + if (!serverIdentity.has(scheme, host, port) || fullPath.charAt(0) != "/") + { + dumpn("*** serverIdentity unknown or path does not start with '/'"); + throw HTTP_400; + } + } + + var splitter = fullPath.indexOf("?"); + if (splitter < 0) + { + // _queryString already set in ctor + metadata._path = fullPath; + } + else + { + metadata._path = fullPath.substring(0, splitter); + metadata._queryString = fullPath.substring(splitter + 1); + } + + metadata._scheme = scheme; + metadata._host = host; + metadata._port = port; + }, + + /** + * Parses all available HTTP headers in this until the header-ending CRLFCRLF, + * adding them to the store of headers in the request. + * + * @throws + * HTTP_400 if the headers are malformed + * @returns boolean + * true if all headers have now been processed, false otherwise + */ + _parseHeaders: function() + { + NS_ASSERT(this._state == READER_IN_HEADERS); + + dumpn("*** _parseHeaders"); + + var data = this._data; + + var headers = this._metadata._headers; + var lastName = this._lastHeaderName; + var lastVal = this._lastHeaderValue; + + var line = {}; + while (true) + { + dumpn("*** Last name: '" + lastName + "'"); + dumpn("*** Last val: '" + lastVal + "'"); + NS_ASSERT(!((lastVal === undefined) ^ (lastName === undefined)), + lastName === undefined ? + "lastVal without lastName? lastVal: '" + lastVal + "'" : + "lastName without lastVal? lastName: '" + lastName + "'"); + + if (!data.readLine(line)) + { + // save any data we have from the header we might still be processing + this._lastHeaderName = lastName; + this._lastHeaderValue = lastVal; + return false; + } + + var lineText = line.value; + dumpn("*** Line text: '" + lineText + "'"); + var firstChar = lineText.charAt(0); + + // blank line means end of headers + if (lineText == "") + { + // we're finished with the previous header + if (lastName) + { + try + { + headers.setHeader(lastName, lastVal, true); + } + catch (e) + { + dumpn("*** setHeader threw on last header, e == " + e); + throw HTTP_400; + } + } + else + { + // no headers in request -- valid for HTTP/1.0 requests + } + + // either way, we're done processing headers + this._state = READER_IN_BODY; + return true; + } + else if (firstChar == " " || firstChar == "\t") + { + // multi-line header if we've already seen a header line + if (!lastName) + { + dumpn("We don't have a header to continue!"); + throw HTTP_400; + } + + // append this line's text to the value; starts with SP/HT, so no need + // for separating whitespace + lastVal += lineText; + } + else + { + // we have a new header, so set the old one (if one existed) + if (lastName) + { + try + { + headers.setHeader(lastName, lastVal, true); + } + catch (e) + { + dumpn("*** setHeader threw on a header, e == " + e); + throw HTTP_400; + } + } + + var colon = lineText.indexOf(":"); // first colon must be splitter + if (colon < 1) + { + dumpn("*** No colon or missing header field-name"); + throw HTTP_400; + } + + // set header name, value (to be set in the next loop, usually) + lastName = lineText.substring(0, colon); + lastVal = lineText.substring(colon + 1); + } // empty, continuation, start of header + } // while (true) + } +}; + + +/** The character codes for CR and LF. */ +const CR = 0x0D, LF = 0x0A; + +/** + * Calculates the number of characters before the first CRLF pair in array, or + * -1 if the array contains no CRLF pair. + * + * @param array : Array + * an array of numbers in the range [0, 256), each representing a single + * character; the first CRLF is the lowest index i where + * |array[i] == "\r".charCodeAt(0)| and |array[i+1] == "\n".charCodeAt(0)|, + * if such an |i| exists, and -1 otherwise + * @param start : uint + * start index from which to begin searching in array + * @returns int + * the index of the first CRLF if any were present, -1 otherwise + */ +function findCRLF(array, start) +{ + for (var i = array.indexOf(CR, start); i >= 0; i = array.indexOf(CR, i + 1)) + { + if (array[i + 1] == LF) + return i; + } + return -1; +} + + +/** + * A container which provides line-by-line access to the arrays of bytes with + * which it is seeded. + */ +function LineData() +{ + /** An array of queued bytes from which to get line-based characters. */ + this._data = []; + + /** Start index from which to search for CRLF. */ + this._start = 0; +} +LineData.prototype = +{ + /** + * Appends the bytes in the given array to the internal data cache maintained + * by this. + */ + appendBytes: function(bytes) + { + var count = bytes.length; + var quantum = 262144; // just above half SpiderMonkey's argument-count limit + if (count < quantum) + { + Array.prototype.push.apply(this._data, bytes); + return; + } + + // Large numbers of bytes may cause Array.prototype.push to be called with + // more arguments than the JavaScript engine supports. In that case append + // bytes in fixed-size amounts until all bytes are appended. + for (var start = 0; start < count; start += quantum) + { + var slice = bytes.slice(start, Math.min(start + quantum, count)); + Array.prototype.push.apply(this._data, slice); + } + }, + + /** + * Removes and returns a line of data, delimited by CRLF, from this. + * + * @param out + * an object whose "value" property will be set to the first line of text + * present in this, sans CRLF, if this contains a full CRLF-delimited line + * of text; if this doesn't contain enough data, the value of the property + * is undefined + * @returns boolean + * true if a full line of data could be read from the data in this, false + * otherwise + */ + readLine: function(out) + { + var data = this._data; + var length = findCRLF(data, this._start); + if (length < 0) + { + this._start = data.length; + + // But if our data ends in a CR, we have to back up one, because + // the first byte in the next packet might be an LF and if we + // start looking at data.length we won't find it. + if (data.length > 0 && data[data.length - 1] === CR) + --this._start; + + return false; + } + + // Reset for future lines. + this._start = 0; + + // + // We have the index of the CR, so remove all the characters, including + // CRLF, from the array with splice, and convert the removed array + // (excluding the trailing CRLF characters) into the corresponding string. + // + var leading = data.splice(0, length + 2); + var quantum = 262144; + var line = ""; + for (var start = 0; start < length; start += quantum) + { + var slice = leading.slice(start, Math.min(start + quantum, length)); + line += String.fromCharCode.apply(null, slice); + } + + out.value = line; + return true; + }, + + /** + * Removes the bytes currently within this and returns them in an array. + * + * @returns Array + * the bytes within this when this method is called + */ + purge: function() + { + var data = this._data; + this._data = []; + return data; + } +}; + + + +/** + * Creates a request-handling function for an nsIHttpRequestHandler object. + */ +function createHandlerFunc(handler) +{ + return function(metadata, response) { handler.handle(metadata, response); }; +} + + +/** + * The default handler for directories; writes an HTML response containing a + * slightly-formatted directory listing. + */ +function defaultIndexHandler(metadata, response) +{ + response.setHeader("Content-Type", "text/html;charset=utf-8", false); + + var path = htmlEscape(decodeURI(metadata.path)); + + // + // Just do a very basic bit of directory listings -- no need for too much + // fanciness, especially since we don't have a style sheet in which we can + // stick rules (don't want to pollute the default path-space). + // + + var body = '<html>\ + <head>\ + <title>' + path + '</title>\ + </head>\ + <body>\ + <h1>' + path + '</h1>\ + <ol style="list-style-type: none">'; + + var directory = metadata.getProperty("directory"); + NS_ASSERT(directory && directory.isDirectory()); + + var fileList = []; + var files = directory.directoryEntries; + while (files.hasMoreElements()) + { + var f = files.getNext().QueryInterface(Ci.nsIFile); + var name = f.leafName; + if (!f.isHidden() && + (name.charAt(name.length - 1) != HIDDEN_CHAR || + name.charAt(name.length - 2) == HIDDEN_CHAR)) + fileList.push(f); + } + + fileList.sort(fileSort); + + for (var i = 0; i < fileList.length; i++) + { + var file = fileList[i]; + try + { + var name = file.leafName; + if (name.charAt(name.length - 1) == HIDDEN_CHAR) + name = name.substring(0, name.length - 1); + var sep = file.isDirectory() ? "/" : ""; + + // Note: using " to delimit the attribute here because encodeURIComponent + // passes through '. + var item = '<li><a href="' + encodeURIComponent(name) + sep + '">' + + htmlEscape(name) + sep + + '</a></li>'; + + body += item; + } + catch (e) { /* some file system error, ignore the file */ } + } + + body += ' </ol>\ + </body>\ + </html>'; + + response.bodyOutputStream.write(body, body.length); +} + +/** + * Sorts a and b (nsIFile objects) into an aesthetically pleasing order. + */ +function fileSort(a, b) +{ + var dira = a.isDirectory(), dirb = b.isDirectory(); + + if (dira && !dirb) + return -1; + if (dirb && !dira) + return 1; + + var namea = a.leafName.toLowerCase(), nameb = b.leafName.toLowerCase(); + return nameb > namea ? -1 : 1; +} + + +/** + * Converts an externally-provided path into an internal path for use in + * determining file mappings. + * + * @param path + * the path to convert + * @param encoded + * true if the given path should be passed through decodeURI prior to + * conversion + * @throws URIError + * if path is incorrectly encoded + */ +function toInternalPath(path, encoded) +{ + if (encoded) + path = decodeURI(path); + + var comps = path.split("/"); + for (var i = 0, sz = comps.length; i < sz; i++) + { + var comp = comps[i]; + if (comp.charAt(comp.length - 1) == HIDDEN_CHAR) + comps[i] = comp + HIDDEN_CHAR; + } + return comps.join("/"); +} + +const PERMS_READONLY = (4 << 6) | (4 << 3) | 4; + +/** + * Adds custom-specified headers for the given file to the given response, if + * any such headers are specified. + * + * @param file + * the file on the disk which is to be written + * @param metadata + * metadata about the incoming request + * @param response + * the Response to which any specified headers/data should be written + * @throws HTTP_500 + * if an error occurred while processing custom-specified headers + */ +function maybeAddHeaders(file, metadata, response) +{ + var name = file.leafName; + if (name.charAt(name.length - 1) == HIDDEN_CHAR) + name = name.substring(0, name.length - 1); + + var headerFile = file.parent; + headerFile.append(name + HEADERS_SUFFIX); + + if (!headerFile.exists()) + return; + + const PR_RDONLY = 0x01; + var fis = new FileInputStream(headerFile, PR_RDONLY, PERMS_READONLY, + Ci.nsIFileInputStream.CLOSE_ON_EOF); + + try + { + var lis = new ConverterInputStream(fis, "UTF-8", 1024, 0x0); + lis.QueryInterface(Ci.nsIUnicharLineInputStream); + + var line = {value: ""}; + var more = lis.readLine(line); + + if (!more && line.value == "") + return; + + + // request line + + var status = line.value; + if (status.indexOf("HTTP ") == 0) + { + status = status.substring(5); + var space = status.indexOf(" "); + var code, description; + if (space < 0) + { + code = status; + description = ""; + } + else + { + code = status.substring(0, space); + description = status.substring(space + 1, status.length); + } + + response.setStatusLine(metadata.httpVersion, parseInt(code, 10), description); + + line.value = ""; + more = lis.readLine(line); + } + + // headers + while (more || line.value != "") + { + var header = line.value; + var colon = header.indexOf(":"); + + response.setHeader(header.substring(0, colon), + header.substring(colon + 1, header.length), + false); // allow overriding server-set headers + + line.value = ""; + more = lis.readLine(line); + } + } + catch (e) + { + dumpn("WARNING: error in headers for " + metadata.path + ": " + e); + throw HTTP_500; + } + finally + { + fis.close(); + } +} + + +/** + * An object which handles requests for a server, executing default and + * overridden behaviors as instructed by the code which uses and manipulates it. + * Default behavior includes the paths / and /trace (diagnostics), with some + * support for HTTP error pages for various codes and fallback to HTTP 500 if + * those codes fail for any reason. + * + * @param server : nsHttpServer + * the server in which this handler is being used + */ +function ServerHandler(server) +{ + // FIELDS + + /** + * The nsHttpServer instance associated with this handler. + */ + this._server = server; + + /** + * A FileMap object containing the set of path->nsILocalFile mappings for + * all directory mappings set in the server (e.g., "/" for /var/www/html/, + * "/foo/bar/" for /local/path/, and "/foo/bar/baz/" for /local/path2). + * + * Note carefully: the leading and trailing "/" in each path (not file) are + * removed before insertion to simplify the code which uses this. You have + * been warned! + */ + this._pathDirectoryMap = new FileMap(); + + /** + * Custom request handlers for the server in which this resides. Path-handler + * pairs are stored as property-value pairs in this property. + * + * @see ServerHandler.prototype._defaultPaths + */ + this._overridePaths = {}; + + /** + * Custom request handlers for the path prefixes on the server in which this + * resides. Path-handler pairs are stored as property-value pairs in this + * property. + * + * @see ServerHandler.prototype._defaultPaths + */ + this._overridePrefixes = {}; + + /** + * Custom request handlers for the error handlers in the server in which this + * resides. Path-handler pairs are stored as property-value pairs in this + * property. + * + * @see ServerHandler.prototype._defaultErrors + */ + this._overrideErrors = {}; + + /** + * Maps file extensions to their MIME types in the server, overriding any + * mapping that might or might not exist in the MIME service. + */ + this._mimeMappings = {}; + + /** + * The default handler for requests for directories, used to serve directories + * when no index file is present. + */ + this._indexHandler = defaultIndexHandler; + + /** Per-path state storage for the server. */ + this._state = {}; + + /** Entire-server state storage. */ + this._sharedState = {}; + + /** Entire-server state storage for nsISupports values. */ + this._objectState = {}; +} +ServerHandler.prototype = +{ + // PUBLIC API + + /** + * Handles a request to this server, responding to the request appropriately + * and initiating server shutdown if necessary. + * + * This method never throws an exception. + * + * @param connection : Connection + * the connection for this request + */ + handleResponse: function(connection) + { + var request = connection.request; + var response = new Response(connection); + + var path = request.path; + dumpn("*** path == " + path); + + try + { + try + { + if (path in this._overridePaths) + { + // explicit paths first, then files based on existing directory mappings, + // then (if the file doesn't exist) built-in server default paths + dumpn("calling override for " + path); + this._overridePaths[path](request, response); + } + else + { + var longestPrefix = ""; + for (let prefix in this._overridePrefixes) { + if (prefix.length > longestPrefix.length && + path.substr(0, prefix.length) == prefix) + { + longestPrefix = prefix; + } + } + if (longestPrefix.length > 0) + { + dumpn("calling prefix override for " + longestPrefix); + this._overridePrefixes[longestPrefix](request, response); + } + else + { + this._handleDefault(request, response); + } + } + } + catch (e) + { + if (response.partiallySent()) + { + response.abort(e); + return; + } + + if (!(e instanceof HttpError)) + { + dumpn("*** unexpected error: e == " + e); + throw HTTP_500; + } + if (e.code !== 404) + throw e; + + dumpn("*** default: " + (path in this._defaultPaths)); + + response = new Response(connection); + if (path in this._defaultPaths) + this._defaultPaths[path](request, response); + else + throw HTTP_404; + } + } + catch (e) + { + if (response.partiallySent()) + { + response.abort(e); + return; + } + + var errorCode = "internal"; + + try + { + if (!(e instanceof HttpError)) + throw e; + + errorCode = e.code; + dumpn("*** errorCode == " + errorCode); + + response = new Response(connection); + if (e.customErrorHandling) + e.customErrorHandling(response); + this._handleError(errorCode, request, response); + return; + } + catch (e2) + { + dumpn("*** error handling " + errorCode + " error: " + + "e2 == " + e2 + ", shutting down server"); + + connection.server._requestQuit(); + response.abort(e2); + return; + } + } + + response.complete(); + }, + + // + // see nsIHttpServer.registerFile + // + registerFile: function(path, file) + { + if (!file) + { + dumpn("*** unregistering '" + path + "' mapping"); + delete this._overridePaths[path]; + return; + } + + dumpn("*** registering '" + path + "' as mapping to " + file.path); + file = file.clone(); + + var self = this; + this._overridePaths[path] = + function(request, response) + { + if (!file.exists()) + throw HTTP_404; + + response.setStatusLine(request.httpVersion, 200, "OK"); + self._writeFileResponse(request, file, response, 0, file.fileSize); + }; + }, + + // + // see nsIHttpServer.registerPathHandler + // + registerPathHandler: function(path, handler) + { + // XXX true path validation! + if (path.charAt(0) != "/") + throw Cr.NS_ERROR_INVALID_ARG; + + this._handlerToField(handler, this._overridePaths, path); + }, + + // + // see nsIHttpServer.registerPrefixHandler + // + registerPrefixHandler: function(path, handler) + { + // XXX true path validation! + if (path.charAt(0) != "/" || path.charAt(path.length - 1) != "/") + throw Cr.NS_ERROR_INVALID_ARG; + + this._handlerToField(handler, this._overridePrefixes, path); + }, + + // + // see nsIHttpServer.registerDirectory + // + registerDirectory: function(path, directory) + { + // strip off leading and trailing '/' so that we can use lastIndexOf when + // determining exactly how a path maps onto a mapped directory -- + // conditional is required here to deal with "/".substring(1, 0) being + // converted to "/".substring(0, 1) per the JS specification + var key = path.length == 1 ? "" : path.substring(1, path.length - 1); + + // the path-to-directory mapping code requires that the first character not + // be "/", or it will go into an infinite loop + if (key.charAt(0) == "/") + throw Cr.NS_ERROR_INVALID_ARG; + + key = toInternalPath(key, false); + + if (directory) + { + dumpn("*** mapping '" + path + "' to the location " + directory.path); + this._pathDirectoryMap.put(key, directory); + } + else + { + dumpn("*** removing mapping for '" + path + "'"); + this._pathDirectoryMap.put(key, null); + } + }, + + // + // see nsIHttpServer.registerErrorHandler + // + registerErrorHandler: function(err, handler) + { + if (!(err in HTTP_ERROR_CODES)) + dumpn("*** WARNING: registering non-HTTP/1.1 error code " + + "(" + err + ") handler -- was this intentional?"); + + this._handlerToField(handler, this._overrideErrors, err); + }, + + // + // see nsIHttpServer.setIndexHandler + // + setIndexHandler: function(handler) + { + if (!handler) + handler = defaultIndexHandler; + else if (typeof(handler) != "function") + handler = createHandlerFunc(handler); + + this._indexHandler = handler; + }, + + // + // see nsIHttpServer.registerContentType + // + registerContentType: function(ext, type) + { + if (!type) + delete this._mimeMappings[ext]; + else + this._mimeMappings[ext] = headerUtils.normalizeFieldValue(type); + }, + + // PRIVATE API + + /** + * Sets or remove (if handler is null) a handler in an object with a key. + * + * @param handler + * a handler, either function or an nsIHttpRequestHandler + * @param dict + * The object to attach the handler to. + * @param key + * The field name of the handler. + */ + _handlerToField: function(handler, dict, key) + { + // for convenience, handler can be a function if this is run from xpcshell + if (typeof(handler) == "function") + dict[key] = handler; + else if (handler) + dict[key] = createHandlerFunc(handler); + else + delete dict[key]; + }, + + /** + * Handles a request which maps to a file in the local filesystem (if a base + * path has already been set; otherwise the 404 error is thrown). + * + * @param metadata : Request + * metadata for the incoming request + * @param response : Response + * an uninitialized Response to the given request, to be initialized by a + * request handler + * @throws HTTP_### + * if an HTTP error occurred (usually HTTP_404); note that in this case the + * calling code must handle post-processing of the response + */ + _handleDefault: function(metadata, response) + { + dumpn("*** _handleDefault()"); + + response.setStatusLine(metadata.httpVersion, 200, "OK"); + + var path = metadata.path; + NS_ASSERT(path.charAt(0) == "/", "invalid path: <" + path + ">"); + + // determine the actual on-disk file; this requires finding the deepest + // path-to-directory mapping in the requested URL + var file = this._getFileForPath(path); + + // the "file" might be a directory, in which case we either serve the + // contained index.html or make the index handler write the response + if (file.exists() && file.isDirectory()) + { + file.append("index.html"); // make configurable? + if (!file.exists() || file.isDirectory()) + { + metadata._ensurePropertyBag(); + metadata._bag.setPropertyAsInterface("directory", file.parent); + this._indexHandler(metadata, response); + return; + } + } + + // alternately, the file might not exist + if (!file.exists()) + throw HTTP_404; + + var start, end; + if (metadata._httpVersion.atLeast(nsHttpVersion.HTTP_1_1) && + metadata.hasHeader("Range") && + this._getTypeFromFile(file) !== SJS_TYPE) + { + var rangeMatch = metadata.getHeader("Range").match(/^bytes=(\d+)?-(\d+)?$/); + if (!rangeMatch) + { + dumpn("*** Range header bogosity: '" + metadata.getHeader("Range") + "'"); + throw HTTP_400; + } + + if (rangeMatch[1] !== undefined) + start = parseInt(rangeMatch[1], 10); + + if (rangeMatch[2] !== undefined) + end = parseInt(rangeMatch[2], 10); + + if (start === undefined && end === undefined) + { + dumpn("*** More Range header bogosity: '" + metadata.getHeader("Range") + "'"); + throw HTTP_400; + } + + // No start given, so the end is really the count of bytes from the + // end of the file. + if (start === undefined) + { + start = Math.max(0, file.fileSize - end); + end = file.fileSize - 1; + } + + // start and end are inclusive + if (end === undefined || end >= file.fileSize) + end = file.fileSize - 1; + + if (start !== undefined && start >= file.fileSize) { + var HTTP_416 = new HttpError(416, "Requested Range Not Satisfiable"); + HTTP_416.customErrorHandling = function(errorResponse) + { + maybeAddHeaders(file, metadata, errorResponse); + }; + throw HTTP_416; + } + + if (end < start) + { + response.setStatusLine(metadata.httpVersion, 200, "OK"); + start = 0; + end = file.fileSize - 1; + } + else + { + response.setStatusLine(metadata.httpVersion, 206, "Partial Content"); + var contentRange = "bytes " + start + "-" + end + "/" + file.fileSize; + response.setHeader("Content-Range", contentRange); + } + } + else + { + start = 0; + end = file.fileSize - 1; + } + + // finally... + dumpn("*** handling '" + path + "' as mapping to " + file.path + " from " + + start + " to " + end + " inclusive"); + this._writeFileResponse(metadata, file, response, start, end - start + 1); + }, + + /** + * Writes an HTTP response for the given file, including setting headers for + * file metadata. + * + * @param metadata : Request + * the Request for which a response is being generated + * @param file : nsILocalFile + * the file which is to be sent in the response + * @param response : Response + * the response to which the file should be written + * @param offset: uint + * the byte offset to skip to when writing + * @param count: uint + * the number of bytes to write + */ + _writeFileResponse: function(metadata, file, response, offset, count) + { + const PR_RDONLY = 0x01; + + var type = this._getTypeFromFile(file); + if (type === SJS_TYPE) + { + var fis = new FileInputStream(file, PR_RDONLY, PERMS_READONLY, + Ci.nsIFileInputStream.CLOSE_ON_EOF); + + try + { + var sis = new ScriptableInputStream(fis); + var s = Cu.Sandbox(gGlobalObject); + s.importFunction(dump, "dump"); + s.importFunction(atob, "atob"); + s.importFunction(btoa, "btoa"); + + // Define a basic key-value state-preservation API across requests, with + // keys initially corresponding to the empty string. + var self = this; + var path = metadata.path; + s.importFunction(function getState(k) + { + return self._getState(path, k); + }); + s.importFunction(function setState(k, v) + { + self._setState(path, k, v); + }); + s.importFunction(function getSharedState(k) + { + return self._getSharedState(k); + }); + s.importFunction(function setSharedState(k, v) + { + self._setSharedState(k, v); + }); + s.importFunction(function getObjectState(k, callback) + { + callback(self._getObjectState(k)); + }); + s.importFunction(function setObjectState(k, v) + { + self._setObjectState(k, v); + }); + s.importFunction(function registerPathHandler(p, h) + { + self.registerPathHandler(p, h); + }); + + // Make it possible for sjs files to access their location + this._setState(path, "__LOCATION__", file.path); + + try + { + // Alas, the line number in errors dumped to console when calling the + // request handler is simply an offset from where we load the SJS file. + // Work around this in a reasonably non-fragile way by dynamically + // getting the line number where we evaluate the SJS file. Don't + // separate these two lines! + var line = new Error().lineNumber; + Cu.evalInSandbox(sis.read(file.fileSize), s, "latest"); + } + catch (e) + { + dumpn("*** syntax error in SJS at " + file.path + ": " + e); + throw HTTP_500; + } + + try + { + s.handleRequest(metadata, response); + } + catch (e) + { + dump("*** error running SJS at " + file.path + ": " + + e + " on line " + + (e instanceof Error + ? e.lineNumber + " in httpd.js" + : (e.lineNumber - line)) + "\n"); + throw HTTP_500; + } + } + finally + { + fis.close(); + } + } + else + { + try + { + response.setHeader("Last-Modified", + toDateString(file.lastModifiedTime), + false); + } + catch (e) { /* lastModifiedTime threw, ignore */ } + + response.setHeader("Content-Type", type, false); + maybeAddHeaders(file, metadata, response); + response.setHeader("Content-Length", "" + count, false); + + var fis = new FileInputStream(file, PR_RDONLY, PERMS_READONLY, + Ci.nsIFileInputStream.CLOSE_ON_EOF); + + offset = offset || 0; + count = count || file.fileSize; + NS_ASSERT(offset === 0 || offset < file.fileSize, "bad offset"); + NS_ASSERT(count >= 0, "bad count"); + NS_ASSERT(offset + count <= file.fileSize, "bad total data size"); + + try + { + if (offset !== 0) + { + // Seek (or read, if seeking isn't supported) to the correct offset so + // the data sent to the client matches the requested range. + if (fis instanceof Ci.nsISeekableStream) + fis.seek(Ci.nsISeekableStream.NS_SEEK_SET, offset); + else + new ScriptableInputStream(fis).read(offset); + } + } + catch (e) + { + fis.close(); + throw e; + } + + let writeMore = function () { + gThreadManager.currentThread + .dispatch(writeData, Ci.nsIThread.DISPATCH_NORMAL); + } + + var input = new BinaryInputStream(fis); + var output = new BinaryOutputStream(response.bodyOutputStream); + var writeData = + { + run: function() + { + var chunkSize = Math.min(65536, count); + count -= chunkSize; + NS_ASSERT(count >= 0, "underflow"); + + try + { + var data = input.readByteArray(chunkSize); + NS_ASSERT(data.length === chunkSize, + "incorrect data returned? got " + data.length + + ", expected " + chunkSize); + output.writeByteArray(data, data.length); + if (count === 0) + { + fis.close(); + response.finish(); + } + else + { + writeMore(); + } + } + catch (e) + { + try + { + fis.close(); + } + finally + { + response.finish(); + } + throw e; + } + } + }; + + writeMore(); + + // Now that we know copying will start, flag the response as async. + response.processAsync(); + } + }, + + /** + * Get the value corresponding to a given key for the given path for SJS state + * preservation across requests. + * + * @param path : string + * the path from which the given state is to be retrieved + * @param k : string + * the key whose corresponding value is to be returned + * @returns string + * the corresponding value, which is initially the empty string + */ + _getState: function(path, k) + { + var state = this._state; + if (path in state && k in state[path]) + return state[path][k]; + return ""; + }, + + /** + * Set the value corresponding to a given key for the given path for SJS state + * preservation across requests. + * + * @param path : string + * the path from which the given state is to be retrieved + * @param k : string + * the key whose corresponding value is to be set + * @param v : string + * the value to be set + */ + _setState: function(path, k, v) + { + if (typeof v !== "string") + throw new Error("non-string value passed"); + var state = this._state; + if (!(path in state)) + state[path] = {}; + state[path][k] = v; + }, + + /** + * Get the value corresponding to a given key for SJS state preservation + * across requests. + * + * @param k : string + * the key whose corresponding value is to be returned + * @returns string + * the corresponding value, which is initially the empty string + */ + _getSharedState: function(k) + { + var state = this._sharedState; + if (k in state) + return state[k]; + return ""; + }, + + /** + * Set the value corresponding to a given key for SJS state preservation + * across requests. + * + * @param k : string + * the key whose corresponding value is to be set + * @param v : string + * the value to be set + */ + _setSharedState: function(k, v) + { + if (typeof v !== "string") + throw new Error("non-string value passed"); + this._sharedState[k] = v; + }, + + /** + * Returns the object associated with the given key in the server for SJS + * state preservation across requests. + * + * @param k : string + * the key whose corresponding object is to be returned + * @returns nsISupports + * the corresponding object, or null if none was present + */ + _getObjectState: function(k) + { + if (typeof k !== "string") + throw new Error("non-string key passed"); + return this._objectState[k] || null; + }, + + /** + * Sets the object associated with the given key in the server for SJS + * state preservation across requests. + * + * @param k : string + * the key whose corresponding object is to be set + * @param v : nsISupports + * the object to be associated with the given key; may be null + */ + _setObjectState: function(k, v) + { + if (typeof k !== "string") + throw new Error("non-string key passed"); + if (typeof v !== "object") + throw new Error("non-object value passed"); + if (v && !("QueryInterface" in v)) + { + throw new Error("must pass an nsISupports; use wrappedJSObject to ease " + + "pain when using the server from JS"); + } + + this._objectState[k] = v; + }, + + /** + * Gets a content-type for the given file, first by checking for any custom + * MIME-types registered with this handler for the file's extension, second by + * asking the global MIME service for a content-type, and finally by failing + * over to application/octet-stream. + * + * @param file : nsIFile + * the nsIFile for which to get a file type + * @returns string + * the best content-type which can be determined for the file + */ + _getTypeFromFile: function(file) + { + try + { + var name = file.leafName; + var dot = name.lastIndexOf("."); + if (dot > 0) + { + var ext = name.slice(dot + 1); + if (ext in this._mimeMappings) + return this._mimeMappings[ext]; + } + return Cc["@mozilla.org/uriloader/external-helper-app-service;1"] + .getService(Ci.nsIMIMEService) + .getTypeFromFile(file); + } + catch (e) + { + return "application/octet-stream"; + } + }, + + /** + * Returns the nsILocalFile which corresponds to the path, as determined using + * all registered path->directory mappings and any paths which are explicitly + * overridden. + * + * @param path : string + * the server path for which a file should be retrieved, e.g. "/foo/bar" + * @throws HttpError + * when the correct action is the corresponding HTTP error (i.e., because no + * mapping was found for a directory in path, the referenced file doesn't + * exist, etc.) + * @returns nsILocalFile + * the file to be sent as the response to a request for the path + */ + _getFileForPath: function(path) + { + // decode and add underscores as necessary + try + { + path = toInternalPath(path, true); + } + catch (e) + { + dumpn("*** toInternalPath threw " + e); + throw HTTP_400; // malformed path + } + + // next, get the directory which contains this path + var pathMap = this._pathDirectoryMap; + + // An example progression of tmp for a path "/foo/bar/baz/" might be: + // "foo/bar/baz/", "foo/bar/baz", "foo/bar", "foo", "" + var tmp = path.substring(1); + while (true) + { + // do we have a match for current head of the path? + var file = pathMap.get(tmp); + if (file) + { + // XXX hack; basically disable showing mapping for /foo/bar/ when the + // requested path was /foo/bar, because relative links on the page + // will all be incorrect -- we really need the ability to easily + // redirect here instead + if (tmp == path.substring(1) && + tmp.length != 0 && + tmp.charAt(tmp.length - 1) != "/") + file = null; + else + break; + } + + // if we've finished trying all prefixes, exit + if (tmp == "") + break; + + tmp = tmp.substring(0, tmp.lastIndexOf("/")); + } + + // no mapping applies, so 404 + if (!file) + throw HTTP_404; + + + // last, get the file for the path within the determined directory + var parentFolder = file.parent; + var dirIsRoot = (parentFolder == null); + + // Strategy here is to append components individually, making sure we + // never move above the given directory; this allows paths such as + // "<file>/foo/../bar" but prevents paths such as "<file>/../base-sibling"; + // this component-wise approach also means the code works even on platforms + // which don't use "/" as the directory separator, such as Windows + var leafPath = path.substring(tmp.length + 1); + var comps = leafPath.split("/"); + for (var i = 0, sz = comps.length; i < sz; i++) + { + var comp = comps[i]; + + if (comp == "..") + file = file.parent; + else if (comp == "." || comp == "") + continue; + else + file.append(comp); + + if (!dirIsRoot && file.equals(parentFolder)) + throw HTTP_403; + } + + return file; + }, + + /** + * Writes the error page for the given HTTP error code over the given + * connection. + * + * @param errorCode : uint + * the HTTP error code to be used + * @param connection : Connection + * the connection on which the error occurred + */ + handleError: function(errorCode, connection) + { + var response = new Response(connection); + + dumpn("*** error in request: " + errorCode); + + this._handleError(errorCode, new Request(connection.port), response); + }, + + /** + * Handles a request which generates the given error code, using the + * user-defined error handler if one has been set, gracefully falling back to + * the x00 status code if the code has no handler, and failing to status code + * 500 if all else fails. + * + * @param errorCode : uint + * the HTTP error which is to be returned + * @param metadata : Request + * metadata for the request, which will often be incomplete since this is an + * error + * @param response : Response + * an uninitialized Response should be initialized when this method + * completes with information which represents the desired error code in the + * ideal case or a fallback code in abnormal circumstances (i.e., 500 is a + * fallback for 505, per HTTP specs) + */ + _handleError: function(errorCode, metadata, response) + { + if (!metadata) + throw Cr.NS_ERROR_NULL_POINTER; + + var errorX00 = errorCode - (errorCode % 100); + + try + { + if (!(errorCode in HTTP_ERROR_CODES)) + dumpn("*** WARNING: requested invalid error: " + errorCode); + + // RFC 2616 says that we should try to handle an error by its class if we + // can't otherwise handle it -- if that fails, we revert to handling it as + // a 500 internal server error, and if that fails we throw and shut down + // the server + + // actually handle the error + try + { + if (errorCode in this._overrideErrors) + this._overrideErrors[errorCode](metadata, response); + else + this._defaultErrors[errorCode](metadata, response); + } + catch (e) + { + if (response.partiallySent()) + { + response.abort(e); + return; + } + + // don't retry the handler that threw + if (errorX00 == errorCode) + throw HTTP_500; + + dumpn("*** error in handling for error code " + errorCode + ", " + + "falling back to " + errorX00 + "..."); + response = new Response(response._connection); + if (errorX00 in this._overrideErrors) + this._overrideErrors[errorX00](metadata, response); + else if (errorX00 in this._defaultErrors) + this._defaultErrors[errorX00](metadata, response); + else + throw HTTP_500; + } + } + catch (e) + { + if (response.partiallySent()) + { + response.abort(); + return; + } + + // we've tried everything possible for a meaningful error -- now try 500 + dumpn("*** error in handling for error code " + errorX00 + ", falling " + + "back to 500..."); + + try + { + response = new Response(response._connection); + if (500 in this._overrideErrors) + this._overrideErrors[500](metadata, response); + else + this._defaultErrors[500](metadata, response); + } + catch (e2) + { + dumpn("*** multiple errors in default error handlers!"); + dumpn("*** e == " + e + ", e2 == " + e2); + response.abort(e2); + return; + } + } + + response.complete(); + }, + + // FIELDS + + /** + * This object contains the default handlers for the various HTTP error codes. + */ + _defaultErrors: + { + 400: function(metadata, response) + { + // none of the data in metadata is reliable, so hard-code everything here + response.setStatusLine("1.1", 400, "Bad Request"); + response.setHeader("Content-Type", "text/plain;charset=utf-8", false); + + var body = "Bad request\n"; + response.bodyOutputStream.write(body, body.length); + }, + 403: function(metadata, response) + { + response.setStatusLine(metadata.httpVersion, 403, "Forbidden"); + response.setHeader("Content-Type", "text/html;charset=utf-8", false); + + var body = "<html>\ + <head><title>403 Forbidden</title></head>\ + <body>\ + <h1>403 Forbidden</h1>\ + </body>\ + </html>"; + response.bodyOutputStream.write(body, body.length); + }, + 404: function(metadata, response) + { + response.setStatusLine(metadata.httpVersion, 404, "Not Found"); + response.setHeader("Content-Type", "text/html;charset=utf-8", false); + + var body = "<html>\ + <head><title>404 Not Found</title></head>\ + <body>\ + <h1>404 Not Found</h1>\ + <p>\ + <span style='font-family: monospace;'>" + + htmlEscape(metadata.path) + + "</span> was not found.\ + </p>\ + </body>\ + </html>"; + response.bodyOutputStream.write(body, body.length); + }, + 416: function(metadata, response) + { + response.setStatusLine(metadata.httpVersion, + 416, + "Requested Range Not Satisfiable"); + response.setHeader("Content-Type", "text/html;charset=utf-8", false); + + var body = "<html>\ + <head>\ + <title>416 Requested Range Not Satisfiable</title></head>\ + <body>\ + <h1>416 Requested Range Not Satisfiable</h1>\ + <p>The byte range was not valid for the\ + requested resource.\ + </p>\ + </body>\ + </html>"; + response.bodyOutputStream.write(body, body.length); + }, + 500: function(metadata, response) + { + response.setStatusLine(metadata.httpVersion, + 500, + "Internal Server Error"); + response.setHeader("Content-Type", "text/html;charset=utf-8", false); + + var body = "<html>\ + <head><title>500 Internal Server Error</title></head>\ + <body>\ + <h1>500 Internal Server Error</h1>\ + <p>Something's broken in this server and\ + needs to be fixed.</p>\ + </body>\ + </html>"; + response.bodyOutputStream.write(body, body.length); + }, + 501: function(metadata, response) + { + response.setStatusLine(metadata.httpVersion, 501, "Not Implemented"); + response.setHeader("Content-Type", "text/html;charset=utf-8", false); + + var body = "<html>\ + <head><title>501 Not Implemented</title></head>\ + <body>\ + <h1>501 Not Implemented</h1>\ + <p>This server is not (yet) Apache.</p>\ + </body>\ + </html>"; + response.bodyOutputStream.write(body, body.length); + }, + 505: function(metadata, response) + { + response.setStatusLine("1.1", 505, "HTTP Version Not Supported"); + response.setHeader("Content-Type", "text/html;charset=utf-8", false); + + var body = "<html>\ + <head><title>505 HTTP Version Not Supported</title></head>\ + <body>\ + <h1>505 HTTP Version Not Supported</h1>\ + <p>This server only supports HTTP/1.0 and HTTP/1.1\ + connections.</p>\ + </body>\ + </html>"; + response.bodyOutputStream.write(body, body.length); + } + }, + + /** + * Contains handlers for the default set of URIs contained in this server. + */ + _defaultPaths: + { + "/": function(metadata, response) + { + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/html;charset=utf-8", false); + + var body = "<html>\ + <head><title>httpd.js</title></head>\ + <body>\ + <h1>httpd.js</h1>\ + <p>If you're seeing this page, httpd.js is up and\ + serving requests! Now set a base path and serve some\ + files!</p>\ + </body>\ + </html>"; + + response.bodyOutputStream.write(body, body.length); + }, + + "/trace": function(metadata, response) + { + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/plain;charset=utf-8", false); + + var body = "Request-URI: " + + metadata.scheme + "://" + metadata.host + ":" + metadata.port + + metadata.path + "\n\n"; + body += "Request (semantically equivalent, slightly reformatted):\n\n"; + body += metadata.method + " " + metadata.path; + + if (metadata.queryString) + body += "?" + metadata.queryString; + + body += " HTTP/" + metadata.httpVersion + "\r\n"; + + var headEnum = metadata.headers; + while (headEnum.hasMoreElements()) + { + var fieldName = headEnum.getNext() + .QueryInterface(Ci.nsISupportsString) + .data; + body += fieldName + ": " + metadata.getHeader(fieldName) + "\r\n"; + } + + response.bodyOutputStream.write(body, body.length); + } + } +}; + + +/** + * Maps absolute paths to files on the local file system (as nsILocalFiles). + */ +function FileMap() +{ + /** Hash which will map paths to nsILocalFiles. */ + this._map = {}; +} +FileMap.prototype = +{ + // PUBLIC API + + /** + * Maps key to a clone of the nsILocalFile value if value is non-null; + * otherwise, removes any extant mapping for key. + * + * @param key : string + * string to which a clone of value is mapped + * @param value : nsILocalFile + * the file to map to key, or null to remove a mapping + */ + put: function(key, value) + { + if (value) + this._map[key] = value.clone(); + else + delete this._map[key]; + }, + + /** + * Returns a clone of the nsILocalFile mapped to key, or null if no such + * mapping exists. + * + * @param key : string + * key to which the returned file maps + * @returns nsILocalFile + * a clone of the mapped file, or null if no mapping exists + */ + get: function(key) + { + var val = this._map[key]; + return val ? val.clone() : null; + } +}; + + +// Response CONSTANTS + +// token = *<any CHAR except CTLs or separators> +// CHAR = <any US-ASCII character (0-127)> +// CTL = <any US-ASCII control character (0-31) and DEL (127)> +// separators = "(" | ")" | "<" | ">" | "@" +// | "," | ";" | ":" | "\" | <"> +// | "/" | "[" | "]" | "?" | "=" +// | "{" | "}" | SP | HT +const IS_TOKEN_ARRAY = + [0, 0, 0, 0, 0, 0, 0, 0, // 0 + 0, 0, 0, 0, 0, 0, 0, 0, // 8 + 0, 0, 0, 0, 0, 0, 0, 0, // 16 + 0, 0, 0, 0, 0, 0, 0, 0, // 24 + + 0, 1, 0, 1, 1, 1, 1, 1, // 32 + 0, 0, 1, 1, 0, 1, 1, 0, // 40 + 1, 1, 1, 1, 1, 1, 1, 1, // 48 + 1, 1, 0, 0, 0, 0, 0, 0, // 56 + + 0, 1, 1, 1, 1, 1, 1, 1, // 64 + 1, 1, 1, 1, 1, 1, 1, 1, // 72 + 1, 1, 1, 1, 1, 1, 1, 1, // 80 + 1, 1, 1, 0, 0, 0, 1, 1, // 88 + + 1, 1, 1, 1, 1, 1, 1, 1, // 96 + 1, 1, 1, 1, 1, 1, 1, 1, // 104 + 1, 1, 1, 1, 1, 1, 1, 1, // 112 + 1, 1, 1, 0, 1, 0, 1]; // 120 + + +/** + * Determines whether the given character code is a CTL. + * + * @param code : uint + * the character code + * @returns boolean + * true if code is a CTL, false otherwise + */ +function isCTL(code) +{ + return (code >= 0 && code <= 31) || (code == 127); +} + +/** + * Represents a response to an HTTP request, encapsulating all details of that + * response. This includes all headers, the HTTP version, status code and + * explanation, and the entity itself. + * + * @param connection : Connection + * the connection over which this response is to be written + */ +function Response(connection) +{ + /** The connection over which this response will be written. */ + this._connection = connection; + + /** + * The HTTP version of this response; defaults to 1.1 if not set by the + * handler. + */ + this._httpVersion = nsHttpVersion.HTTP_1_1; + + /** + * The HTTP code of this response; defaults to 200. + */ + this._httpCode = 200; + + /** + * The description of the HTTP code in this response; defaults to "OK". + */ + this._httpDescription = "OK"; + + /** + * An nsIHttpHeaders object in which the headers in this response should be + * stored. This property is null after the status line and headers have been + * written to the network, and it may be modified up until it is cleared, + * except if this._finished is set first (in which case headers are written + * asynchronously in response to a finish() call not preceded by + * flushHeaders()). + */ + this._headers = new nsHttpHeaders(); + + /** + * Set to true when this response is ended (completely constructed if possible + * and the connection closed); further actions on this will then fail. + */ + this._ended = false; + + /** + * A stream used to hold data written to the body of this response. + */ + this._bodyOutputStream = null; + + /** + * A stream containing all data that has been written to the body of this + * response so far. (Async handlers make the data contained in this + * unreliable as a way of determining content length in general, but auxiliary + * saved information can sometimes be used to guarantee reliability.) + */ + this._bodyInputStream = null; + + /** + * A stream copier which copies data to the network. It is initially null + * until replaced with a copier for response headers; when headers have been + * fully sent it is replaced with a copier for the response body, remaining + * so for the duration of response processing. + */ + this._asyncCopier = null; + + /** + * True if this response has been designated as being processed + * asynchronously rather than for the duration of a single call to + * nsIHttpRequestHandler.handle. + */ + this._processAsync = false; + + /** + * True iff finish() has been called on this, signaling that no more changes + * to this may be made. + */ + this._finished = false; + + /** + * True iff powerSeized() has been called on this, signaling that this + * response is to be handled manually by the response handler (which may then + * send arbitrary data in response, even non-HTTP responses). + */ + this._powerSeized = false; +} +Response.prototype = +{ + // PUBLIC CONSTRUCTION API + + // + // see nsIHttpResponse.bodyOutputStream + // + get bodyOutputStream() + { + if (this._finished) + throw Cr.NS_ERROR_NOT_AVAILABLE; + + if (!this._bodyOutputStream) + { + var pipe = new Pipe(true, false, Response.SEGMENT_SIZE, PR_UINT32_MAX, + null); + this._bodyOutputStream = pipe.outputStream; + this._bodyInputStream = pipe.inputStream; + if (this._processAsync || this._powerSeized) + this._startAsyncProcessor(); + } + + return this._bodyOutputStream; + }, + + // + // see nsIHttpResponse.write + // + write: function(data) + { + if (this._finished) + throw Cr.NS_ERROR_NOT_AVAILABLE; + + var dataAsString = String(data); + this.bodyOutputStream.write(dataAsString, dataAsString.length); + }, + + // + // see nsIHttpResponse.setStatusLine + // + setStatusLine: function(httpVersion, code, description) + { + if (!this._headers || this._finished || this._powerSeized) + throw Cr.NS_ERROR_NOT_AVAILABLE; + this._ensureAlive(); + + if (!(code >= 0 && code < 1000)) + throw Cr.NS_ERROR_INVALID_ARG; + + try + { + var httpVer; + // avoid version construction for the most common cases + if (!httpVersion || httpVersion == "1.1") + httpVer = nsHttpVersion.HTTP_1_1; + else if (httpVersion == "1.0") + httpVer = nsHttpVersion.HTTP_1_0; + else + httpVer = new nsHttpVersion(httpVersion); + } + catch (e) + { + throw Cr.NS_ERROR_INVALID_ARG; + } + + // Reason-Phrase = *<TEXT, excluding CR, LF> + // TEXT = <any OCTET except CTLs, but including LWS> + // + // XXX this ends up disallowing octets which aren't Unicode, I think -- not + // much to do if description is IDL'd as string + if (!description) + description = ""; + for (var i = 0; i < description.length; i++) + if (isCTL(description.charCodeAt(i)) && description.charAt(i) != "\t") + throw Cr.NS_ERROR_INVALID_ARG; + + // set the values only after validation to preserve atomicity + this._httpDescription = description; + this._httpCode = code; + this._httpVersion = httpVer; + }, + + // + // see nsIHttpResponse.setHeader + // + setHeader: function(name, value, merge) + { + if (!this._headers || this._finished || this._powerSeized) + throw Cr.NS_ERROR_NOT_AVAILABLE; + this._ensureAlive(); + + this._headers.setHeader(name, value, merge); + }, + + setHeaderNoCheck: function(name, value) + { + if (!this._headers || this._finished || this._powerSeized) + throw Cr.NS_ERROR_NOT_AVAILABLE; + this._ensureAlive(); + + this._headers.setHeaderNoCheck(name, value); + }, + + // + // see nsIHttpResponse.processAsync + // + processAsync: function() + { + if (this._finished) + throw Cr.NS_ERROR_UNEXPECTED; + if (this._powerSeized) + throw Cr.NS_ERROR_NOT_AVAILABLE; + if (this._processAsync) + return; + this._ensureAlive(); + + dumpn("*** processing connection " + this._connection.number + " async"); + this._processAsync = true; + + /* + * Either the bodyOutputStream getter or this method is responsible for + * starting the asynchronous processor and catching writes of data to the + * response body of async responses as they happen, for the purpose of + * forwarding those writes to the actual connection's output stream. + * If bodyOutputStream is accessed first, calling this method will create + * the processor (when it first is clear that body data is to be written + * immediately, not buffered). If this method is called first, accessing + * bodyOutputStream will create the processor. If only this method is + * called, we'll write nothing, neither headers nor the nonexistent body, + * until finish() is called. Since that delay is easily avoided by simply + * getting bodyOutputStream or calling write(""), we don't worry about it. + */ + if (this._bodyOutputStream && !this._asyncCopier) + this._startAsyncProcessor(); + }, + + // + // see nsIHttpResponse.seizePower + // + seizePower: function() + { + if (this._processAsync) + throw Cr.NS_ERROR_NOT_AVAILABLE; + if (this._finished) + throw Cr.NS_ERROR_UNEXPECTED; + if (this._powerSeized) + return; + this._ensureAlive(); + + dumpn("*** forcefully seizing power over connection " + + this._connection.number + "..."); + + // Purge any already-written data without sending it. We could as easily + // swap out the streams entirely, but that makes it possible to acquire and + // unknowingly use a stale reference, so we require there only be one of + // each stream ever for any response to avoid this complication. + if (this._asyncCopier) + this._asyncCopier.cancel(Cr.NS_BINDING_ABORTED); + this._asyncCopier = null; + if (this._bodyOutputStream) + { + var input = new BinaryInputStream(this._bodyInputStream); + var avail; + while ((avail = input.available()) > 0) + input.readByteArray(avail); + } + + this._powerSeized = true; + if (this._bodyOutputStream) + this._startAsyncProcessor(); + }, + + // + // see nsIHttpResponse.finish + // + finish: function() + { + if (!this._processAsync && !this._powerSeized) + throw Cr.NS_ERROR_UNEXPECTED; + if (this._finished) + return; + + dumpn("*** finishing connection " + this._connection.number); + this._startAsyncProcessor(); // in case bodyOutputStream was never accessed + if (this._bodyOutputStream) + this._bodyOutputStream.close(); + this._finished = true; + }, + + + // NSISUPPORTS + + // + // see nsISupports.QueryInterface + // + QueryInterface: function(iid) + { + if (iid.equals(Ci.nsIHttpResponse) || iid.equals(Ci.nsISupports)) + return this; + + throw Cr.NS_ERROR_NO_INTERFACE; + }, + + + // POST-CONSTRUCTION API (not exposed externally) + + /** + * The HTTP version number of this, as a string (e.g. "1.1"). + */ + get httpVersion() + { + this._ensureAlive(); + return this._httpVersion.toString(); + }, + + /** + * The HTTP status code of this response, as a string of three characters per + * RFC 2616. + */ + get httpCode() + { + this._ensureAlive(); + + var codeString = (this._httpCode < 10 ? "0" : "") + + (this._httpCode < 100 ? "0" : "") + + this._httpCode; + return codeString; + }, + + /** + * The description of the HTTP status code of this response, or "" if none is + * set. + */ + get httpDescription() + { + this._ensureAlive(); + + return this._httpDescription; + }, + + /** + * The headers in this response, as an nsHttpHeaders object. + */ + get headers() + { + this._ensureAlive(); + + return this._headers; + }, + + // + // see nsHttpHeaders.getHeader + // + getHeader: function(name) + { + this._ensureAlive(); + + return this._headers.getHeader(name); + }, + + /** + * Determines whether this response may be abandoned in favor of a newly + * constructed response. A response may be abandoned only if it is not being + * sent asynchronously and if raw control over it has not been taken from the + * server. + * + * @returns boolean + * true iff no data has been written to the network + */ + partiallySent: function() + { + dumpn("*** partiallySent()"); + return this._processAsync || this._powerSeized; + }, + + /** + * If necessary, kicks off the remaining request processing needed to be done + * after a request handler performs its initial work upon this response. + */ + complete: function() + { + dumpn("*** complete()"); + if (this._processAsync || this._powerSeized) + { + NS_ASSERT(this._processAsync ^ this._powerSeized, + "can't both send async and relinquish power"); + return; + } + + NS_ASSERT(!this.partiallySent(), "completing a partially-sent response?"); + + this._startAsyncProcessor(); + + // Now make sure we finish processing this request! + if (this._bodyOutputStream) + this._bodyOutputStream.close(); + }, + + /** + * Abruptly ends processing of this response, usually due to an error in an + * incoming request but potentially due to a bad error handler. Since we + * cannot handle the error in the usual way (giving an HTTP error page in + * response) because data may already have been sent (or because the response + * might be expected to have been generated asynchronously or completely from + * scratch by the handler), we stop processing this response and abruptly + * close the connection. + * + * @param e : Error + * the exception which precipitated this abort, or null if no such exception + * was generated + */ + abort: function(e) + { + dumpn("*** abort(<" + e + ">)"); + + // This response will be ended by the processor if one was created. + var copier = this._asyncCopier; + if (copier) + { + // We dispatch asynchronously here so that any pending writes of data to + // the connection will be deterministically written. This makes it easier + // to specify exact behavior, and it makes observable behavior more + // predictable for clients. Note that the correctness of this depends on + // callbacks in response to _waitToReadData in WriteThroughCopier + // happening asynchronously with respect to the actual writing of data to + // bodyOutputStream, as they currently do; if they happened synchronously, + // an event which ran before this one could write more data to the + // response body before we get around to canceling the copier. We have + // tests for this in test_seizepower.js, however, and I can't think of a + // way to handle both cases without removing bodyOutputStream access and + // moving its effective write(data, length) method onto Response, which + // would be slower and require more code than this anyway. + gThreadManager.currentThread.dispatch({ + run: function() + { + dumpn("*** canceling copy asynchronously..."); + copier.cancel(Cr.NS_ERROR_UNEXPECTED); + } + }, Ci.nsIThread.DISPATCH_NORMAL); + } + else + { + this.end(); + } + }, + + /** + * Closes this response's network connection, marks the response as finished, + * and notifies the server handler that the request is done being processed. + */ + end: function() + { + NS_ASSERT(!this._ended, "ending this response twice?!?!"); + + this._connection.close(); + if (this._bodyOutputStream) + this._bodyOutputStream.close(); + + this._finished = true; + this._ended = true; + }, + + // PRIVATE IMPLEMENTATION + + /** + * Sends the status line and headers of this response if they haven't been + * sent and initiates the process of copying data written to this response's + * body to the network. + */ + _startAsyncProcessor: function() + { + dumpn("*** _startAsyncProcessor()"); + + // Handle cases where we're being called a second time. The former case + // happens when this is triggered both by complete() and by processAsync(), + // while the latter happens when processAsync() in conjunction with sent + // data causes abort() to be called. + if (this._asyncCopier || this._ended) + { + dumpn("*** ignoring second call to _startAsyncProcessor"); + return; + } + + // Send headers if they haven't been sent already and should be sent, then + // asynchronously continue to send the body. + if (this._headers && !this._powerSeized) + { + this._sendHeaders(); + return; + } + + this._headers = null; + this._sendBody(); + }, + + /** + * Signals that all modifications to the response status line and headers are + * complete and then sends that data over the network to the client. Once + * this method completes, a different response to the request that resulted + * in this response cannot be sent -- the only possible action in case of + * error is to abort the response and close the connection. + */ + _sendHeaders: function() + { + dumpn("*** _sendHeaders()"); + + NS_ASSERT(this._headers); + NS_ASSERT(!this._powerSeized); + + // request-line + var statusLine = "HTTP/" + this.httpVersion + " " + + this.httpCode + " " + + this.httpDescription + "\r\n"; + + // header post-processing + + var headers = this._headers; + headers.setHeader("Connection", "close", false); + headers.setHeader("Server", "httpd.js", false); + if (!headers.hasHeader("Date")) + headers.setHeader("Date", toDateString(Date.now()), false); + + // Any response not being processed asynchronously must have an associated + // Content-Length header for reasons of backwards compatibility with the + // initial server, which fully buffered every response before sending it. + // Beyond that, however, it's good to do this anyway because otherwise it's + // impossible to test behaviors that depend on the presence or absence of a + // Content-Length header. + if (!this._processAsync) + { + dumpn("*** non-async response, set Content-Length"); + + var bodyStream = this._bodyInputStream; + var avail = bodyStream ? bodyStream.available() : 0; + + // XXX assumes stream will always report the full amount of data available + headers.setHeader("Content-Length", "" + avail, false); + } + + + // construct and send response + dumpn("*** header post-processing completed, sending response head..."); + + // request-line + var preambleData = [statusLine]; + + // headers + var headEnum = headers.enumerator; + while (headEnum.hasMoreElements()) + { + var fieldName = headEnum.getNext() + .QueryInterface(Ci.nsISupportsString) + .data; + var values = headers.getHeaderValues(fieldName); + for (var i = 0, sz = values.length; i < sz; i++) + preambleData.push(fieldName + ": " + values[i] + "\r\n"); + } + + // end request-line/headers + preambleData.push("\r\n"); + + var preamble = preambleData.join(""); + + var responseHeadPipe = new Pipe(true, false, 0, PR_UINT32_MAX, null); + responseHeadPipe.outputStream.write(preamble, preamble.length); + + var response = this; + var copyObserver = + { + onStartRequest: function(request, cx) + { + dumpn("*** preamble copying started"); + }, + + onStopRequest: function(request, cx, statusCode) + { + dumpn("*** preamble copying complete " + + "[status=0x" + statusCode.toString(16) + "]"); + + if (!Components.isSuccessCode(statusCode)) + { + dumpn("!!! header copying problems: non-success statusCode, " + + "ending response"); + + response.end(); + } + else + { + response._sendBody(); + } + }, + + QueryInterface: function(aIID) + { + if (aIID.equals(Ci.nsIRequestObserver) || aIID.equals(Ci.nsISupports)) + return this; + + throw Cr.NS_ERROR_NO_INTERFACE; + } + }; + + var headerCopier = this._asyncCopier = + new WriteThroughCopier(responseHeadPipe.inputStream, + this._connection.output, + copyObserver, null); + + responseHeadPipe.outputStream.close(); + + // Forbid setting any more headers or modifying the request line. + this._headers = null; + }, + + /** + * Asynchronously writes the body of the response (or the entire response, if + * seizePower() has been called) to the network. + */ + _sendBody: function() + { + dumpn("*** _sendBody"); + + NS_ASSERT(!this._headers, "still have headers around but sending body?"); + + // If no body data was written, we're done + if (!this._bodyInputStream) + { + dumpn("*** empty body, response finished"); + this.end(); + return; + } + + var response = this; + var copyObserver = + { + onStartRequest: function(request, context) + { + dumpn("*** onStartRequest"); + }, + + onStopRequest: function(request, cx, statusCode) + { + dumpn("*** onStopRequest [status=0x" + statusCode.toString(16) + "]"); + + if (statusCode === Cr.NS_BINDING_ABORTED) + { + dumpn("*** terminating copy observer without ending the response"); + } + else + { + if (!Components.isSuccessCode(statusCode)) + dumpn("*** WARNING: non-success statusCode in onStopRequest"); + + response.end(); + } + }, + + QueryInterface: function(aIID) + { + if (aIID.equals(Ci.nsIRequestObserver) || aIID.equals(Ci.nsISupports)) + return this; + + throw Cr.NS_ERROR_NO_INTERFACE; + } + }; + + dumpn("*** starting async copier of body data..."); + this._asyncCopier = + new WriteThroughCopier(this._bodyInputStream, this._connection.output, + copyObserver, null); + }, + + /** Ensures that this hasn't been ended. */ + _ensureAlive: function() + { + NS_ASSERT(!this._ended, "not handling response lifetime correctly"); + } +}; + +/** + * Size of the segments in the buffer used in storing response data and writing + * it to the socket. + */ +Response.SEGMENT_SIZE = 8192; + +/** Serves double duty in WriteThroughCopier implementation. */ +function notImplemented() +{ + throw Cr.NS_ERROR_NOT_IMPLEMENTED; +} + +/** Returns true iff the given exception represents stream closure. */ +function streamClosed(e) +{ + return e === Cr.NS_BASE_STREAM_CLOSED || + (typeof e === "object" && e.result === Cr.NS_BASE_STREAM_CLOSED); +} + +/** Returns true iff the given exception represents a blocked stream. */ +function wouldBlock(e) +{ + return e === Cr.NS_BASE_STREAM_WOULD_BLOCK || + (typeof e === "object" && e.result === Cr.NS_BASE_STREAM_WOULD_BLOCK); +} + +/** + * Copies data from source to sink as it becomes available, when that data can + * be written to sink without blocking. + * + * @param source : nsIAsyncInputStream + * the stream from which data is to be read + * @param sink : nsIAsyncOutputStream + * the stream to which data is to be copied + * @param observer : nsIRequestObserver + * an observer which will be notified when the copy starts and finishes + * @param context : nsISupports + * context passed to observer when notified of start/stop + * @throws NS_ERROR_NULL_POINTER + * if source, sink, or observer are null + */ +function WriteThroughCopier(source, sink, observer, context) +{ + if (!source || !sink || !observer) + throw Cr.NS_ERROR_NULL_POINTER; + + /** Stream from which data is being read. */ + this._source = source; + + /** Stream to which data is being written. */ + this._sink = sink; + + /** Observer watching this copy. */ + this._observer = observer; + + /** Context for the observer watching this. */ + this._context = context; + + /** + * True iff this is currently being canceled (cancel has been called, the + * callback may not yet have been made). + */ + this._canceled = false; + + /** + * False until all data has been read from input and written to output, at + * which point this copy is completed and cancel() is asynchronously called. + */ + this._completed = false; + + /** Required by nsIRequest, meaningless. */ + this.loadFlags = 0; + /** Required by nsIRequest, meaningless. */ + this.loadGroup = null; + /** Required by nsIRequest, meaningless. */ + this.name = "response-body-copy"; + + /** Status of this request. */ + this.status = Cr.NS_OK; + + /** Arrays of byte strings waiting to be written to output. */ + this._pendingData = []; + + // start copying + try + { + observer.onStartRequest(this, context); + this._waitToReadData(); + this._waitForSinkClosure(); + } + catch (e) + { + dumpn("!!! error starting copy: " + e + + ("lineNumber" in e ? ", line " + e.lineNumber : "")); + dumpn(e.stack); + this.cancel(Cr.NS_ERROR_UNEXPECTED); + } +} +WriteThroughCopier.prototype = +{ + /* nsISupports implementation */ + + QueryInterface: function(iid) + { + if (iid.equals(Ci.nsIInputStreamCallback) || + iid.equals(Ci.nsIOutputStreamCallback) || + iid.equals(Ci.nsIRequest) || + iid.equals(Ci.nsISupports)) + { + return this; + } + + throw Cr.NS_ERROR_NO_INTERFACE; + }, + + + // NSIINPUTSTREAMCALLBACK + + /** + * Receives a more-data-in-input notification and writes the corresponding + * data to the output. + * + * @param input : nsIAsyncInputStream + * the input stream on whose data we have been waiting + */ + onInputStreamReady: function(input) + { + if (this._source === null) + return; + + dumpn("*** onInputStreamReady"); + + // + // Ordinarily we'll read a non-zero amount of data from input, queue it up + // to be written and then wait for further callbacks. The complications in + // this method are the cases where we deviate from that behavior when errors + // occur or when copying is drawing to a finish. + // + // The edge cases when reading data are: + // + // Zero data is read + // If zero data was read, we're at the end of available data, so we can + // should stop reading and move on to writing out what we have (or, if + // we've already done that, onto notifying of completion). + // A stream-closed exception is thrown + // This is effectively a less kind version of zero data being read; the + // only difference is that we notify of completion with that result + // rather than with NS_OK. + // Some other exception is thrown + // This is the least kind result. We don't know what happened, so we + // act as though the stream closed except that we notify of completion + // with the result NS_ERROR_UNEXPECTED. + // + + var bytesWanted = 0, bytesConsumed = -1; + try + { + input = new BinaryInputStream(input); + + bytesWanted = Math.min(input.available(), Response.SEGMENT_SIZE); + dumpn("*** input wanted: " + bytesWanted); + + if (bytesWanted > 0) + { + var data = input.readByteArray(bytesWanted); + bytesConsumed = data.length; + this._pendingData.push(String.fromCharCode.apply(String, data)); + } + + dumpn("*** " + bytesConsumed + " bytes read"); + + // Handle the zero-data edge case in the same place as all other edge + // cases are handled. + if (bytesWanted === 0) + throw Cr.NS_BASE_STREAM_CLOSED; + } + catch (e) + { + if (streamClosed(e)) + { + dumpn("*** input stream closed"); + e = bytesWanted === 0 ? Cr.NS_OK : Cr.NS_ERROR_UNEXPECTED; + } + else + { + dumpn("!!! unexpected error reading from input, canceling: " + e); + e = Cr.NS_ERROR_UNEXPECTED; + } + + this._doneReadingSource(e); + return; + } + + var pendingData = this._pendingData; + + NS_ASSERT(bytesConsumed > 0); + NS_ASSERT(pendingData.length > 0, "no pending data somehow?"); + NS_ASSERT(pendingData[pendingData.length - 1].length > 0, + "buffered zero bytes of data?"); + + NS_ASSERT(this._source !== null); + + // Reading has gone great, and we've gotten data to write now. What if we + // don't have a place to write that data, because output went away just + // before this read? Drop everything on the floor, including new data, and + // cancel at this point. + if (this._sink === null) + { + pendingData.length = 0; + this._doneReadingSource(Cr.NS_ERROR_UNEXPECTED); + return; + } + + // Okay, we've read the data, and we know we have a place to write it. We + // need to queue up the data to be written, but *only* if none is queued + // already -- if data's already queued, the code that actually writes the + // data will make sure to wait on unconsumed pending data. + try + { + if (pendingData.length === 1) + this._waitToWriteData(); + } + catch (e) + { + dumpn("!!! error waiting to write data just read, swallowing and " + + "writing only what we already have: " + e); + this._doneWritingToSink(Cr.NS_ERROR_UNEXPECTED); + return; + } + + // Whee! We successfully read some data, and it's successfully queued up to + // be written. All that remains now is to wait for more data to read. + try + { + this._waitToReadData(); + } + catch (e) + { + dumpn("!!! error waiting to read more data: " + e); + this._doneReadingSource(Cr.NS_ERROR_UNEXPECTED); + } + }, + + + // NSIOUTPUTSTREAMCALLBACK + + /** + * Callback when data may be written to the output stream without blocking, or + * when the output stream has been closed. + * + * @param output : nsIAsyncOutputStream + * the output stream on whose writability we've been waiting, also known as + * this._sink + */ + onOutputStreamReady: function(output) + { + if (this._sink === null) + return; + + dumpn("*** onOutputStreamReady"); + + var pendingData = this._pendingData; + if (pendingData.length === 0) + { + // There's no pending data to write. The only way this can happen is if + // we're waiting on the output stream's closure, so we can respond to a + // copying failure as quickly as possible (rather than waiting for data to + // be available to read and then fail to be copied). Therefore, we must + // be done now -- don't bother to attempt to write anything and wrap + // things up. + dumpn("!!! output stream closed prematurely, ending copy"); + + this._doneWritingToSink(Cr.NS_ERROR_UNEXPECTED); + return; + } + + + NS_ASSERT(pendingData[0].length > 0, "queued up an empty quantum?"); + + // + // Write out the first pending quantum of data. The possible errors here + // are: + // + // The write might fail because we can't write that much data + // Okay, we've written what we can now, so re-queue what's left and + // finish writing it out later. + // The write failed because the stream was closed + // Discard pending data that we can no longer write, stop reading, and + // signal that copying finished. + // Some other error occurred. + // Same as if the stream were closed, but notify with the status + // NS_ERROR_UNEXPECTED so the observer knows something was wonky. + // + + try + { + var quantum = pendingData[0]; + + // XXX |quantum| isn't guaranteed to be ASCII, so we're relying on + // undefined behavior! We're only using this because writeByteArray + // is unusably broken for asynchronous output streams; see bug 532834 + // for details. + var bytesWritten = output.write(quantum, quantum.length); + if (bytesWritten === quantum.length) + pendingData.shift(); + else + pendingData[0] = quantum.substring(bytesWritten); + + dumpn("*** wrote " + bytesWritten + " bytes of data"); + } + catch (e) + { + if (wouldBlock(e)) + { + NS_ASSERT(pendingData.length > 0, + "stream-blocking exception with no data to write?"); + NS_ASSERT(pendingData[0].length > 0, + "stream-blocking exception with empty quantum?"); + this._waitToWriteData(); + return; + } + + if (streamClosed(e)) + dumpn("!!! output stream prematurely closed, signaling error..."); + else + dumpn("!!! unknown error: " + e + ", quantum=" + quantum); + + this._doneWritingToSink(Cr.NS_ERROR_UNEXPECTED); + return; + } + + // The day is ours! Quantum written, now let's see if we have more data + // still to write. + try + { + if (pendingData.length > 0) + { + this._waitToWriteData(); + return; + } + } + catch (e) + { + dumpn("!!! unexpected error waiting to write pending data: " + e); + this._doneWritingToSink(Cr.NS_ERROR_UNEXPECTED); + return; + } + + // Okay, we have no more pending data to write -- but might we get more in + // the future? + if (this._source !== null) + { + /* + * If we might, then wait for the output stream to be closed. (We wait + * only for closure because we have no data to write -- and if we waited + * for a specific amount of data, we would get repeatedly notified for no + * reason if over time the output stream permitted more and more data to + * be written to it without blocking.) + */ + this._waitForSinkClosure(); + } + else + { + /* + * On the other hand, if we can't have more data because the input + * stream's gone away, then it's time to notify of copy completion. + * Victory! + */ + this._sink = null; + this._cancelOrDispatchCancelCallback(Cr.NS_OK); + } + }, + + + // NSIREQUEST + + /** Returns true if the cancel observer hasn't been notified yet. */ + isPending: function() + { + return !this._completed; + }, + + /** Not implemented, don't use! */ + suspend: notImplemented, + /** Not implemented, don't use! */ + resume: notImplemented, + + /** + * Cancels data reading from input, asynchronously writes out any pending + * data, and causes the observer to be notified with the given error code when + * all writing has finished. + * + * @param status : nsresult + * the status to pass to the observer when data copying has been canceled + */ + cancel: function(status) + { + dumpn("*** cancel(" + status.toString(16) + ")"); + + if (this._canceled) + { + dumpn("*** suppressing a late cancel"); + return; + } + + this._canceled = true; + this.status = status; + + // We could be in the middle of absolutely anything at this point. Both + // input and output might still be around, we might have pending data to + // write, and in general we know nothing about the state of the world. We + // therefore must assume everything's in progress and take everything to its + // final steady state (or so far as it can go before we need to finish + // writing out remaining data). + + this._doneReadingSource(status); + }, + + + // PRIVATE IMPLEMENTATION + + /** + * Stop reading input if we haven't already done so, passing e as the status + * when closing the stream, and kick off a copy-completion notice if no more + * data remains to be written. + * + * @param e : nsresult + * the status to be used when closing the input stream + */ + _doneReadingSource: function(e) + { + dumpn("*** _doneReadingSource(0x" + e.toString(16) + ")"); + + this._finishSource(e); + if (this._pendingData.length === 0) + this._sink = null; + else + NS_ASSERT(this._sink !== null, "null output?"); + + // If we've written out all data read up to this point, then it's time to + // signal completion. + if (this._sink === null) + { + NS_ASSERT(this._pendingData.length === 0, "pending data still?"); + this._cancelOrDispatchCancelCallback(e); + } + }, + + /** + * Stop writing output if we haven't already done so, discard any data that + * remained to be sent, close off input if it wasn't already closed, and kick + * off a copy-completion notice. + * + * @param e : nsresult + * the status to be used when closing input if it wasn't already closed + */ + _doneWritingToSink: function(e) + { + dumpn("*** _doneWritingToSink(0x" + e.toString(16) + ")"); + + this._pendingData.length = 0; + this._sink = null; + this._doneReadingSource(e); + }, + + /** + * Completes processing of this copy: either by canceling the copy if it + * hasn't already been canceled using the provided status, or by dispatching + * the cancel callback event (with the originally provided status, of course) + * if it already has been canceled. + * + * @param status : nsresult + * the status code to use to cancel this, if this hasn't already been + * canceled + */ + _cancelOrDispatchCancelCallback: function(status) + { + dumpn("*** _cancelOrDispatchCancelCallback(" + status + ")"); + + NS_ASSERT(this._source === null, "should have finished input"); + NS_ASSERT(this._sink === null, "should have finished output"); + NS_ASSERT(this._pendingData.length === 0, "should have no pending data"); + + if (!this._canceled) + { + this.cancel(status); + return; + } + + var self = this; + var event = + { + run: function() + { + dumpn("*** onStopRequest async callback"); + + self._completed = true; + try + { + self._observer.onStopRequest(self, self._context, self.status); + } + catch (e) + { + NS_ASSERT(false, + "how are we throwing an exception here? we control " + + "all the callers! " + e); + } + } + }; + + gThreadManager.currentThread.dispatch(event, Ci.nsIThread.DISPATCH_NORMAL); + }, + + /** + * Kicks off another wait for more data to be available from the input stream. + */ + _waitToReadData: function() + { + dumpn("*** _waitToReadData"); + this._source.asyncWait(this, 0, Response.SEGMENT_SIZE, + gThreadManager.mainThread); + }, + + /** + * Kicks off another wait until data can be written to the output stream. + */ + _waitToWriteData: function() + { + dumpn("*** _waitToWriteData"); + + var pendingData = this._pendingData; + NS_ASSERT(pendingData.length > 0, "no pending data to write?"); + NS_ASSERT(pendingData[0].length > 0, "buffered an empty write?"); + + this._sink.asyncWait(this, 0, pendingData[0].length, + gThreadManager.mainThread); + }, + + /** + * Kicks off a wait for the sink to which data is being copied to be closed. + * We wait for stream closure when we don't have any data to be copied, rather + * than waiting to write a specific amount of data. We can't wait to write + * data because the sink might be infinitely writable, and if no data appears + * in the source for a long time we might have to spin quite a bit waiting to + * write, waiting to write again, &c. Waiting on stream closure instead means + * we'll get just one notification if the sink dies. Note that when data + * starts arriving from the sink we'll resume waiting for data to be written, + * dropping this closure-only callback entirely. + */ + _waitForSinkClosure: function() + { + dumpn("*** _waitForSinkClosure"); + + this._sink.asyncWait(this, Ci.nsIAsyncOutputStream.WAIT_CLOSURE_ONLY, 0, + gThreadManager.mainThread); + }, + + /** + * Closes input with the given status, if it hasn't already been closed; + * otherwise a no-op. + * + * @param status : nsresult + * status code use to close the source stream if necessary + */ + _finishSource: function(status) + { + dumpn("*** _finishSource(" + status.toString(16) + ")"); + + if (this._source !== null) + { + this._source.closeWithStatus(status); + this._source = null; + } + } +}; + + +/** + * A container for utility functions used with HTTP headers. + */ +const headerUtils = +{ + /** + * Normalizes fieldName (by converting it to lowercase) and ensures it is a + * valid header field name (although not necessarily one specified in RFC + * 2616). + * + * @throws NS_ERROR_INVALID_ARG + * if fieldName does not match the field-name production in RFC 2616 + * @returns string + * fieldName converted to lowercase if it is a valid header, for characters + * where case conversion is possible + */ + normalizeFieldName: function(fieldName) + { + if (fieldName == "") + { + dumpn("*** Empty fieldName"); + throw Cr.NS_ERROR_INVALID_ARG; + } + + for (var i = 0, sz = fieldName.length; i < sz; i++) + { + if (!IS_TOKEN_ARRAY[fieldName.charCodeAt(i)]) + { + dumpn(fieldName + " is not a valid header field name!"); + throw Cr.NS_ERROR_INVALID_ARG; + } + } + + return fieldName.toLowerCase(); + }, + + /** + * Ensures that fieldValue is a valid header field value (although not + * necessarily as specified in RFC 2616 if the corresponding field name is + * part of the HTTP protocol), normalizes the value if it is, and + * returns the normalized value. + * + * @param fieldValue : string + * a value to be normalized as an HTTP header field value + * @throws NS_ERROR_INVALID_ARG + * if fieldValue does not match the field-value production in RFC 2616 + * @returns string + * fieldValue as a normalized HTTP header field value + */ + normalizeFieldValue: function(fieldValue) + { + // field-value = *( field-content | LWS ) + // field-content = <the OCTETs making up the field-value + // and consisting of either *TEXT or combinations + // of token, separators, and quoted-string> + // TEXT = <any OCTET except CTLs, + // but including LWS> + // LWS = [CRLF] 1*( SP | HT ) + // + // quoted-string = ( <"> *(qdtext | quoted-pair ) <"> ) + // qdtext = <any TEXT except <">> + // quoted-pair = "\" CHAR + // CHAR = <any US-ASCII character (octets 0 - 127)> + + // Any LWS that occurs between field-content MAY be replaced with a single + // SP before interpreting the field value or forwarding the message + // downstream (section 4.2); we replace 1*LWS with a single SP + var val = fieldValue.replace(/(?:(?:\r\n)?[ \t]+)+/g, " "); + + // remove leading/trailing LWS (which has been converted to SP) + val = val.replace(/^ +/, "").replace(/ +$/, ""); + + // that should have taken care of all CTLs, so val should contain no CTLs + dumpn("*** Normalized value: '" + val + "'"); + for (var i = 0, len = val.length; i < len; i++) + if (isCTL(val.charCodeAt(i))) + { + dump("*** Char " + i + " has charcode " + val.charCodeAt(i)); + throw Cr.NS_ERROR_INVALID_ARG; + } + + // XXX disallows quoted-pair where CHAR is a CTL -- will not invalidly + // normalize, however, so this can be construed as a tightening of the + // spec and not entirely as a bug + return val; + } +}; + + + +/** + * Converts the given string into a string which is safe for use in an HTML + * context. + * + * @param str : string + * the string to make HTML-safe + * @returns string + * an HTML-safe version of str + */ +function htmlEscape(str) +{ + // this is naive, but it'll work + var s = ""; + for (var i = 0; i < str.length; i++) + s += "&#" + str.charCodeAt(i) + ";"; + return s; +} + + +/** + * Constructs an object representing an HTTP version (see section 3.1). + * + * @param versionString + * a string of the form "#.#", where # is an non-negative decimal integer with + * or without leading zeros + * @throws + * if versionString does not specify a valid HTTP version number + */ +function nsHttpVersion(versionString) +{ + var matches = /^(\d+)\.(\d+)$/.exec(versionString); + if (!matches) + throw "Not a valid HTTP version!"; + + /** The major version number of this, as a number. */ + this.major = parseInt(matches[1], 10); + + /** The minor version number of this, as a number. */ + this.minor = parseInt(matches[2], 10); + + if (isNaN(this.major) || isNaN(this.minor) || + this.major < 0 || this.minor < 0) + throw "Not a valid HTTP version!"; +} +nsHttpVersion.prototype = +{ + /** + * Returns the standard string representation of the HTTP version represented + * by this (e.g., "1.1"). + */ + toString: function () + { + return this.major + "." + this.minor; + }, + + /** + * Returns true if this represents the same HTTP version as otherVersion, + * false otherwise. + * + * @param otherVersion : nsHttpVersion + * the version to compare against this + */ + equals: function (otherVersion) + { + return this.major == otherVersion.major && + this.minor == otherVersion.minor; + }, + + /** True if this >= otherVersion, false otherwise. */ + atLeast: function(otherVersion) + { + return this.major > otherVersion.major || + (this.major == otherVersion.major && + this.minor >= otherVersion.minor); + } +}; + +nsHttpVersion.HTTP_1_0 = new nsHttpVersion("1.0"); +nsHttpVersion.HTTP_1_1 = new nsHttpVersion("1.1"); + + +/** + * An object which stores HTTP headers for a request or response. + * + * Note that since headers are case-insensitive, this object converts headers to + * lowercase before storing them. This allows the getHeader and hasHeader + * methods to work correctly for any case of a header, but it means that the + * values returned by .enumerator may not be equal case-sensitively to the + * values passed to setHeader when adding headers to this. + */ +function nsHttpHeaders() +{ + /** + * A hash of headers, with header field names as the keys and header field + * values as the values. Header field names are case-insensitive, but upon + * insertion here they are converted to lowercase. Header field values are + * normalized upon insertion to contain no leading or trailing whitespace. + * + * Note also that per RFC 2616, section 4.2, two headers with the same name in + * a message may be treated as one header with the same field name and a field + * value consisting of the separate field values joined together with a "," in + * their original order. This hash stores multiple headers with the same name + * in this manner. + */ + this._headers = {}; +} +nsHttpHeaders.prototype = +{ + /** + * Sets the header represented by name and value in this. + * + * @param name : string + * the header name + * @param value : string + * the header value + * @throws NS_ERROR_INVALID_ARG + * if name or value is not a valid header component + */ + setHeader: function(fieldName, fieldValue, merge) + { + var name = headerUtils.normalizeFieldName(fieldName); + var value = headerUtils.normalizeFieldValue(fieldValue); + + // The following three headers are stored as arrays because their real-world + // syntax prevents joining individual headers into a single header using + // ",". See also <http://hg.mozilla.org/mozilla-central/diff/9b2a99adc05e/netwerk/protocol/http/src/nsHttpHeaderArray.cpp#l77> + if (merge && name in this._headers) + { + if (name === "www-authenticate" || + name === "proxy-authenticate" || + name === "set-cookie") + { + this._headers[name].push(value); + } + else + { + this._headers[name][0] += "," + value; + NS_ASSERT(this._headers[name].length === 1, + "how'd a non-special header have multiple values?") + } + } + else + { + this._headers[name] = [value]; + } + }, + + setHeaderNoCheck: function(fieldName, fieldValue) + { + var name = headerUtils.normalizeFieldName(fieldName); + var value = headerUtils.normalizeFieldValue(fieldValue); + if (name in this._headers) { + this._headers[name].push(fieldValue); + } else { + this._headers[name] = [fieldValue]; + } + }, + + /** + * Returns the value for the header specified by this. + * + * @throws NS_ERROR_INVALID_ARG + * if fieldName does not constitute a valid header field name + * @throws NS_ERROR_NOT_AVAILABLE + * if the given header does not exist in this + * @returns string + * the field value for the given header, possibly with non-semantic changes + * (i.e., leading/trailing whitespace stripped, whitespace runs replaced + * with spaces, etc.) at the option of the implementation; multiple + * instances of the header will be combined with a comma, except for + * the three headers noted in the description of getHeaderValues + */ + getHeader: function(fieldName) + { + return this.getHeaderValues(fieldName).join("\n"); + }, + + /** + * Returns the value for the header specified by fieldName as an array. + * + * @throws NS_ERROR_INVALID_ARG + * if fieldName does not constitute a valid header field name + * @throws NS_ERROR_NOT_AVAILABLE + * if the given header does not exist in this + * @returns [string] + * an array of all the header values in this for the given + * header name. Header values will generally be collapsed + * into a single header by joining all header values together + * with commas, but certain headers (Proxy-Authenticate, + * WWW-Authenticate, and Set-Cookie) violate the HTTP spec + * and cannot be collapsed in this manner. For these headers + * only, the returned array may contain multiple elements if + * that header has been added more than once. + */ + getHeaderValues: function(fieldName) + { + var name = headerUtils.normalizeFieldName(fieldName); + + if (name in this._headers) + return this._headers[name]; + else + throw Cr.NS_ERROR_NOT_AVAILABLE; + }, + + /** + * Returns true if a header with the given field name exists in this, false + * otherwise. + * + * @param fieldName : string + * the field name whose existence is to be determined in this + * @throws NS_ERROR_INVALID_ARG + * if fieldName does not constitute a valid header field name + * @returns boolean + * true if the header's present, false otherwise + */ + hasHeader: function(fieldName) + { + var name = headerUtils.normalizeFieldName(fieldName); + return (name in this._headers); + }, + + /** + * Returns a new enumerator over the field names of the headers in this, as + * nsISupportsStrings. The names returned will be in lowercase, regardless of + * how they were input using setHeader (header names are case-insensitive per + * RFC 2616). + */ + get enumerator() + { + var headers = []; + for (var i in this._headers) + { + var supports = new SupportsString(); + supports.data = i; + headers.push(supports); + } + + return new nsSimpleEnumerator(headers); + } +}; + + +/** + * Constructs an nsISimpleEnumerator for the given array of items. + * + * @param items : Array + * the items, which must all implement nsISupports + */ +function nsSimpleEnumerator(items) +{ + this._items = items; + this._nextIndex = 0; +} +nsSimpleEnumerator.prototype = +{ + hasMoreElements: function() + { + return this._nextIndex < this._items.length; + }, + getNext: function() + { + if (!this.hasMoreElements()) + throw Cr.NS_ERROR_NOT_AVAILABLE; + + return this._items[this._nextIndex++]; + }, + QueryInterface: function(aIID) + { + if (Ci.nsISimpleEnumerator.equals(aIID) || + Ci.nsISupports.equals(aIID)) + return this; + + throw Cr.NS_ERROR_NO_INTERFACE; + } +}; + + +/** + * A representation of the data in an HTTP request. + * + * @param port : uint + * the port on which the server receiving this request runs + */ +function Request(port) +{ + /** Method of this request, e.g. GET or POST. */ + this._method = ""; + + /** Path of the requested resource; empty paths are converted to '/'. */ + this._path = ""; + + /** Query string, if any, associated with this request (not including '?'). */ + this._queryString = ""; + + /** Scheme of requested resource, usually http, always lowercase. */ + this._scheme = "http"; + + /** Hostname on which the requested resource resides. */ + this._host = undefined; + + /** Port number over which the request was received. */ + this._port = port; + + var bodyPipe = new Pipe(false, false, 0, PR_UINT32_MAX, null); + + /** Stream from which data in this request's body may be read. */ + this._bodyInputStream = bodyPipe.inputStream; + + /** Stream to which data in this request's body is written. */ + this._bodyOutputStream = bodyPipe.outputStream; + + /** + * The headers in this request. + */ + this._headers = new nsHttpHeaders(); + + /** + * For the addition of ad-hoc properties and new functionality without having + * to change nsIHttpRequest every time; currently lazily created, as its only + * use is in directory listings. + */ + this._bag = null; +} +Request.prototype = +{ + // SERVER METADATA + + // + // see nsIHttpRequest.scheme + // + get scheme() + { + return this._scheme; + }, + + // + // see nsIHttpRequest.host + // + get host() + { + return this._host; + }, + + // + // see nsIHttpRequest.port + // + get port() + { + return this._port; + }, + + // REQUEST LINE + + // + // see nsIHttpRequest.method + // + get method() + { + return this._method; + }, + + // + // see nsIHttpRequest.httpVersion + // + get httpVersion() + { + return this._httpVersion.toString(); + }, + + // + // see nsIHttpRequest.path + // + get path() + { + return this._path; + }, + + // + // see nsIHttpRequest.queryString + // + get queryString() + { + return this._queryString; + }, + + // HEADERS + + // + // see nsIHttpRequest.getHeader + // + getHeader: function(name) + { + return this._headers.getHeader(name); + }, + + // + // see nsIHttpRequest.hasHeader + // + hasHeader: function(name) + { + return this._headers.hasHeader(name); + }, + + // + // see nsIHttpRequest.headers + // + get headers() + { + return this._headers.enumerator; + }, + + // + // see nsIPropertyBag.enumerator + // + get enumerator() + { + this._ensurePropertyBag(); + return this._bag.enumerator; + }, + + // + // see nsIHttpRequest.headers + // + get bodyInputStream() + { + return this._bodyInputStream; + }, + + // + // see nsIPropertyBag.getProperty + // + getProperty: function(name) + { + this._ensurePropertyBag(); + return this._bag.getProperty(name); + }, + + + // NSISUPPORTS + + // + // see nsISupports.QueryInterface + // + QueryInterface: function(iid) + { + if (iid.equals(Ci.nsIHttpRequest) || iid.equals(Ci.nsISupports)) + return this; + + throw Cr.NS_ERROR_NO_INTERFACE; + }, + + + // PRIVATE IMPLEMENTATION + + /** Ensures a property bag has been created for ad-hoc behaviors. */ + _ensurePropertyBag: function() + { + if (!this._bag) + this._bag = new WritablePropertyBag(); + } +}; + + +// XPCOM trappings + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([nsHttpServer]); + +/** + * Creates a new HTTP server listening for loopback traffic on the given port, + * starts it, and runs the server until the server processes a shutdown request, + * spinning an event loop so that events posted by the server's socket are + * processed. + * + * This method is primarily intended for use in running this script from within + * xpcshell and running a functional HTTP server without having to deal with + * non-essential details. + * + * Note that running multiple servers using variants of this method probably + * doesn't work, simply due to how the internal event loop is spun and stopped. + * + * @note + * This method only works with Mozilla 1.9 (i.e., Firefox 3 or trunk code); + * you should use this server as a component in Mozilla 1.8. + * @param port + * the port on which the server will run, or -1 if there exists no preference + * for a specific port; note that attempting to use some values for this + * parameter (particularly those below 1024) may cause this method to throw or + * may result in the server being prematurely shut down + * @param basePath + * a local directory from which requests will be served (i.e., if this is + * "/home/jwalden/" then a request to /index.html will load + * /home/jwalden/index.html); if this is omitted, only the default URLs in + * this server implementation will be functional + */ +function server(port, basePath) +{ + if (basePath) + { + var lp = Cc["@mozilla.org/file/local;1"] + .createInstance(Ci.nsILocalFile); + lp.initWithPath(basePath); + } + + // if you're running this, you probably want to see debugging info + DEBUG = true; + + var srv = new nsHttpServer(); + if (lp) + srv.registerDirectory("/", lp); + srv.registerContentType("sjs", SJS_TYPE); + srv.identity.setPrimary("http", "localhost", port); + srv.start(port); + + var thread = gThreadManager.currentThread; + while (!srv.isStopped()) + thread.processNextEvent(true); + + // get rid of any pending requests + while (thread.hasPendingEvents()) + thread.processNextEvent(true); + + DEBUG = false; +} diff --git a/netwerk/test/httpserver/httpd.manifest b/netwerk/test/httpserver/httpd.manifest new file mode 100644 index 0000000000..745e5d3672 --- /dev/null +++ b/netwerk/test/httpserver/httpd.manifest @@ -0,0 +1,3 @@ +component {54ef6f81-30af-4b1d-ac55-8ba811293e41} httpd.js +contract @mozilla.org/server/jshttp;1 {54ef6f81-30af-4b1d-ac55-8ba811293e41} +interfaces test_necko.xpt diff --git a/netwerk/test/httpserver/moz.build b/netwerk/test/httpserver/moz.build new file mode 100644 index 0000000000..f8199d18ec --- /dev/null +++ b/netwerk/test/httpserver/moz.build @@ -0,0 +1,25 @@ +# -*- 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/. + +XPIDL_SOURCES += [ + 'nsIHttpServer.idl', +] + +XPIDL_MODULE = 'test_necko' + +# Don't add our test-only .xpt files to the normal manifests +XPIDL_NO_MANIFEST = True + +XPCSHELL_TESTS_MANIFESTS += ['test/xpcshell.ini'] + +EXTRA_COMPONENTS += [ + 'httpd.js', + 'httpd.manifest', +] + +TESTING_JS_MODULES += [ + 'httpd.js', +] diff --git a/netwerk/test/httpserver/nsIHttpServer.idl b/netwerk/test/httpserver/nsIHttpServer.idl new file mode 100644 index 0000000000..97192a2d60 --- /dev/null +++ b/netwerk/test/httpserver/nsIHttpServer.idl @@ -0,0 +1,620 @@ +/* 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/. */ + +#include "nsISupports.idl" + +interface nsIInputStream; +interface nsIFile; +interface nsIOutputStream; +interface nsISimpleEnumerator; + +interface nsIHttpServer; +interface nsIHttpServerStoppedCallback; +interface nsIHttpRequestHandler; +interface nsIHttpRequest; +interface nsIHttpResponse; +interface nsIHttpServerIdentity; + +/** + * An interface which represents an HTTP server. + */ +[scriptable, uuid(cea8812e-faa6-4013-9396-f9936cbb74ec)] +interface nsIHttpServer : nsISupports +{ + /** + * Starts up this server, listening upon the given port. + * + * @param port + * the port upon which listening should happen, or -1 if no specific port is + * desired + * @throws NS_ERROR_ALREADY_INITIALIZED + * if this server is already started + * @throws NS_ERROR_NOT_AVAILABLE + * if the server is not started and cannot be started on the desired port + * (perhaps because the port is already in use or because the process does + * not have privileges to do so) + * @note + * Behavior is undefined if this method is called after stop() has been + * called on this but before the provided callback function has been + * called. + */ + void start(in long port); + + /** + * Shuts down this server if it is running (including the period of time after + * stop() has been called but before the provided callback has been called). + * + * @param callback + * an asynchronous callback used to notify the user when this server is + * stopped and all pending requests have been fully served + * @throws NS_ERROR_NULL_POINTER + * if callback is null + * @throws NS_ERROR_UNEXPECTED + * if this server is not running + */ + void stop(in nsIHttpServerStoppedCallback callback); + + /** + * Associates the local file represented by the string file with all requests + * which match request. + * + * @param path + * the path which is to be mapped to the given file; must begin with "/" and + * be a valid URI path (i.e., no query string, hash reference, etc.) + * @param file + * the file to serve for the given path, or null to remove any mapping that + * might exist; this file must exist for the lifetime of the server + */ + void registerFile(in string path, in nsIFile file); + + /** + * Registers a custom path handler. + * + * @param path + * the path on the server (beginning with a "/") which is to be handled by + * handler; this path must not include a query string or hash component; it + * also should usually be canonicalized, since most browsers will do so + * before sending otherwise-matching requests + * @param handler + * an object which will handle any requests for the given path, or null to + * remove any existing handler; if while the server is running the handler + * throws an exception while responding to a request, an HTTP 500 response + * will be returned + * @throws NS_ERROR_INVALID_ARG + * if path does not begin with a "/" + */ + void registerPathHandler(in string path, in nsIHttpRequestHandler handler); + + /** + * Registers a custom prefix handler. + * + * @param prefix + * the path on the server (beginning and ending with "/") which is to be + * handled by handler; this path must not include a query string or hash + * component. All requests that start with this prefix will be directed to + * the given handler. + * @param handler + * an object which will handle any requests for the given path, or null to + * remove any existing handler; if while the server is running the handler + * throws an exception while responding to a request, an HTTP 500 response + * will be returned + * @throws NS_ERROR_INVALID_ARG + * if path does not begin with a "/" or does not end with a "/" + */ + void registerPrefixHandler(in string prefix, in nsIHttpRequestHandler handler); + + /** + * Registers a custom error page handler. + * + * @param code + * the error code which is to be handled by handler + * @param handler + * an object which will handle any requests which generate the given status + * code, or null to remove any existing handler. If the handler throws an + * exception during server operation, fallback is to the genericized error + * handler (the x00 version), then to 500, using a user-defined error + * handler if one exists or the server default handler otherwise. Fallback + * will never occur from a user-provided handler that throws to the same + * handler as provided by the server, e.g. a throwing user 404 falls back to + * 400, not a server-provided 404 that might not throw. + * @note + * If the error handler handles HTTP 500 and throws, behavior is undefined. + */ + void registerErrorHandler(in unsigned long code, in nsIHttpRequestHandler handler); + + /** + * Maps all requests to paths beneath path to the corresponding file beneath + * dir. + * + * @param path + * the absolute path on the server against which requests will be served + * from dir (e.g., "/", "/foo/", etc.); must begin and end with a forward + * slash + * @param dir + * the directory to be used to serve all requests for paths underneath path + * (except those further overridden by another, deeper path registered with + * another directory); if null, any current mapping for the given path is + * removed + * @throws NS_ERROR_INVALID_ARG + * if dir is non-null and does not exist or is not a directory, or if path + * does not begin with and end with a forward slash + */ + void registerDirectory(in string path, in nsIFile dir); + + /** + * Associates files with the given extension with the given Content-Type when + * served by this server, in the absence of any file-specific information + * about the desired Content-Type. If type is empty, removes any extant + * mapping, if one is present. + * + * @throws NS_ERROR_INVALID_ARG + * if the given type is not a valid header field value, i.e. if it doesn't + * match the field-value production in RFC 2616 + * @note + * No syntax checking is done of the given type, beyond ensuring that it is + * a valid header field value. Behavior when not given a string matching + * the media-type production in RFC 2616 section 3.7 is undefined. + * Implementations may choose to define specific behavior for types which do + * not match the production, such as for CGI functionality. + * @note + * Implementations MAY treat type as a trusted argument; users who fail to + * generate this string from trusted data risk security vulnerabilities. + */ + void registerContentType(in string extension, in string type); + + /** + * Sets the handler used to display the contents of a directory if + * the directory contains no index page. + * + * @param handler + * an object which will handle any requests for directories which + * do not contain index pages, or null to reset to the default + * index handler; if while the server is running the handler + * throws an exception while responding to a request, an HTTP 500 + * response will be returned. An nsIFile corresponding to the + * directory is available from the metadata object passed to the + * handler, under the key "directory". + */ + void setIndexHandler(in nsIHttpRequestHandler handler); + + /** Represents the locations at which this server is reachable. */ + readonly attribute nsIHttpServerIdentity identity; + + /** + * Retrieves the string associated with the given key in this, for the given + * path's saved state. All keys are initially associated with the empty + * string. + */ + AString getState(in AString path, in AString key); + + /** + * Sets the string associated with the given key in this, for the given path's + * saved state. + */ + void setState(in AString path, in AString key, in AString value); + + /** + * Retrieves the string associated with the given key in this, in + * entire-server saved state. All keys are initially associated with the + * empty string. + */ + AString getSharedState(in AString key); + + /** + * Sets the string associated with the given key in this, in entire-server + * saved state. + */ + void setSharedState(in AString key, in AString value); + + /** + * Retrieves the object associated with the given key in this in + * object-valued saved state. All keys are initially associated with null. + */ + nsISupports getObjectState(in AString key); + + /** + * Sets the object associated with the given key in this in object-valued + * saved state. The value may be null. + */ + void setObjectState(in AString key, in nsISupports value); +}; + +/** + * An interface through which a notification of the complete stopping (socket + * closure, in-flight requests all fully served and responded to) of an HTTP + * server may be received. + */ +[scriptable, function, uuid(925a6d33-9937-4c63-abe1-a1c56a986455)] +interface nsIHttpServerStoppedCallback : nsISupports +{ + /** Called when the corresponding server has been fully stopped. */ + void onStopped(); +}; + +/** + * Represents a set of names for a server, one of which is the primary name for + * the server and the rest of which are secondary. By default every server will + * contain ("http", "localhost", port) and ("http", "127.0.0.1", port) as names, + * where port is what was provided to the corresponding server when started; + * however, except for their being removed when the corresponding server stops + * they have no special importance. + */ +[scriptable, uuid(a89de175-ae8e-4c46-91a5-0dba99bbd284)] +interface nsIHttpServerIdentity : nsISupports +{ + /** + * The primary scheme at which the corresponding server is located, defaulting + * to 'http'. This name will be the value of nsIHttpRequest.scheme for + * HTTP/1.0 requests. + * + * This value is always set when the corresponding server is running. If the + * server is not running, this value is set only if it has been set to a + * non-default name using setPrimary. In this case reading this value will + * throw NS_ERROR_NOT_INITIALIZED. + */ + readonly attribute string primaryScheme; + + /** + * The primary name by which the corresponding server is known, defaulting to + * 'localhost'. This name will be the value of nsIHttpRequest.host for + * HTTP/1.0 requests. + * + * This value is always set when the corresponding server is running. If the + * server is not running, this value is set only if it has been set to a + * non-default name using setPrimary. In this case reading this value will + * throw NS_ERROR_NOT_INITIALIZED. + */ + readonly attribute string primaryHost; + + /** + * The primary port on which the corresponding server runs, defaulting to the + * associated server's port. This name will be the value of + * nsIHttpRequest.port for HTTP/1.0 requests. + * + * This value is always set when the corresponding server is running. If the + * server is not running, this value is set only if it has been set to a + * non-default name using setPrimary. In this case reading this value will + * throw NS_ERROR_NOT_INITIALIZED. + */ + readonly attribute long primaryPort; + + /** + * Adds a location at which this server may be accessed. + * + * @throws NS_ERROR_ILLEGAL_VALUE + * if scheme or host do not match the scheme or host productions imported + * into RFC 2616 from RFC 2396, or if port is not a valid port number + */ + void add(in string scheme, in string host, in long port); + + /** + * Removes this name from the list of names by which the corresponding server + * is known. If name is also the primary name for the server, the primary + * name reverts to 'http://127.0.0.1' with the associated server's port. + * + * @throws NS_ERROR_ILLEGAL_VALUE + * if scheme or host do not match the scheme or host productions imported + * into RFC 2616 from RFC 2396, or if port is not a valid port number + * @returns + * true if the given name was a name for this server, false otherwise + */ + boolean remove(in string scheme, in string host, in long port); + + /** + * Returns true if the given name is in this, false otherwise. + * + * @throws NS_ERROR_ILLEGAL_VALUE + * if scheme or host do not match the scheme or host productions imported + * into RFC 2616 from RFC 2396, or if port is not a valid port number + */ + boolean has(in string scheme, in string host, in long port); + + /** + * Returns the scheme for the name with the given host and port, if one is + * present; otherwise returns the empty string. + * + * @throws NS_ERROR_ILLEGAL_VALUE + * if host does not match the host production imported into RFC 2616 from + * RFC 2396, or if port is not a valid port number + */ + string getScheme(in string host, in long port); + + /** + * Designates the given name as the primary name in this and adds it to this + * if it is not already present. + * + * @throws NS_ERROR_ILLEGAL_VALUE + * if scheme or host do not match the scheme or host productions imported + * into RFC 2616 from RFC 2396, or if port is not a valid port number + */ + void setPrimary(in string scheme, in string host, in long port); +}; + +/** + * A representation of a handler for HTTP requests. The handler is used by + * calling its .handle method with data for an incoming request; it is the + * handler's job to use that data as it sees fit to make the desired response. + * + * @note + * This interface uses the [function] attribute, so you can pass a + * script-defined function with the functionality of handle() to any + * method which has a nsIHttpRequestHandler parameter, instead of wrapping + * it in an otherwise empty object. + */ +[scriptable, function, uuid(2bbb4db7-d285-42b3-a3ce-142b8cc7e139)] +interface nsIHttpRequestHandler : nsISupports +{ + /** + * Processes an HTTP request and initializes the passed-in response to reflect + * the correct HTTP response. + * + * If this method throws an exception, externally observable behavior depends + * upon whether is being processed asynchronously. If such is the case, the + * output is some prefix (perhaps all, perhaps none, perhaps only some) of the + * data which would have been sent if, instead, the response had been finished + * at that point. If no data has been written, the response has not had + * seizePower() called on it, and it is not being asynchronously created, an + * error handler will be invoked (usually 500 unless otherwise specified). + * + * Some uses of nsIHttpRequestHandler may require this method to never throw + * an exception; in the general case, however, this method may throw an + * exception (causing an HTTP 500 response to occur, if the above conditions + * are met). + * + * @param request + * data representing an HTTP request + * @param response + * an initially-empty response which must be modified to reflect the data + * which should be sent as the response to the request described by metadata + */ + void handle(in nsIHttpRequest request, in nsIHttpResponse response); +}; + + +/** + * A representation of the data included in an HTTP request. + */ +[scriptable, uuid(978cf30e-ad73-42ee-8f22-fe0aaf1bf5d2)] +interface nsIHttpRequest : nsISupports +{ + /** + * The request type for this request (see RFC 2616, section 5.1.1). + */ + readonly attribute string method; + + /** + * The scheme of the requested path, usually 'http' but might possibly be + * 'https' if some form of SSL tunneling is in use. Note that this value + * cannot be accurately determined unless the incoming request used the + * absolute-path form of the request line; it defaults to 'http', so only + * if it is something else can you be entirely certain it's correct. + */ + readonly attribute string scheme; + + /** + * The host of the data being requested (e.g. "localhost" for the + * http://localhost:8080/file resource). Note that the relevant port on the + * host is specified in this.port. This value is in the ASCII character + * encoding. + */ + readonly attribute string host; + + /** + * The port on the server on which the request was received. + */ + readonly attribute unsigned long port; + + /** + * The requested path, without any query string (e.g. "/dir/file.txt"). It is + * guaranteed to begin with a "/". The individual components in this string + * are URL-encoded. + */ + readonly attribute string path; + + /** + * The URL-encoded query string associated with this request, not including + * the initial "?", or "" if no query string was present. + */ + readonly attribute string queryString; + + /** + * A string containing the HTTP version of the request (i.e., "1.1"). Leading + * zeros for either component of the version will be omitted. (In other + * words, if the request contains the version "1.01", this attribute will be + * "1.1"; see RFC 2616, section 3.1.) + */ + readonly attribute string httpVersion; + + /** + * Returns the value for the header in this request specified by fieldName. + * + * @param fieldName + * the name of the field whose value is to be gotten; note that since HTTP + * header field names are case-insensitive, this method produces equivalent + * results for "HeAdER" and "hEADer" as fieldName + * @returns + * The result is a string containing the individual values of the header, + * usually separated with a comma. The headers WWW-Authenticate, + * Proxy-Authenticate, and Set-Cookie violate the HTTP specification, + * however, and for these headers only the separator string is '\n'. + * + * @throws NS_ERROR_INVALID_ARG + * if fieldName does not constitute a valid header field name + * @throws NS_ERROR_NOT_AVAILABLE + * if the given header does not exist in this + */ + string getHeader(in string fieldName); + + /** + * Returns true if a header with the given field name exists in this, false + * otherwise. + * + * @param fieldName + * the field name whose existence is to be determined in this; note that + * since HTTP header field names are case-insensitive, this method produces + * equivalent results for "HeAdER" and "hEADer" as fieldName + * @throws NS_ERROR_INVALID_ARG + * if fieldName does not constitute a valid header field name + */ + boolean hasHeader(in string fieldName); + + /** + * An nsISimpleEnumerator of nsISupportsStrings over the names of the headers + * in this request. The header field names in the enumerator may not + * necessarily have the same case as they do in the request itself. + */ + readonly attribute nsISimpleEnumerator headers; + + /** + * A stream from which data appearing in the body of this request can be read. + */ + readonly attribute nsIInputStream bodyInputStream; +}; + + +/** + * Represents an HTTP response, as described in RFC 2616, section 6. + */ +[scriptable, uuid(1acd16c2-dc59-42fa-9160-4f26c43c1c21)] +interface nsIHttpResponse : nsISupports +{ + /** + * Sets the status line for this. If this method is never called on this, the + * status line defaults to "HTTP/", followed by the server's default HTTP + * version (e.g. "1.1"), followed by " 200 OK". + * + * @param httpVersion + * the HTTP version of this, as a string (e.g. "1.1"); if null, the server + * default is used + * @param code + * the numeric HTTP status code for this + * @param description + * a human-readable description of code; may be null if no description is + * desired + * @throws NS_ERROR_INVALID_ARG + * if httpVersion is not a valid HTTP version string, statusCode is greater + * than 999, or description contains invalid characters + * @throws NS_ERROR_NOT_AVAILABLE + * if this response is being processed asynchronously and data has been + * written to this response's body, or if seizePower() has been called on + * this + */ + void setStatusLine(in string httpVersion, + in unsigned short statusCode, + in string description); + + /** + * Sets the specified header in this. + * + * @param name + * the name of the header; must match the field-name production per RFC 2616 + * @param value + * the value of the header; must match the field-value production per RFC + * 2616 + * @param merge + * when true, if the given header already exists in this, the values passed + * to this function will be merged into the existing header, per RFC 2616 + * header semantics (except for the Set-Cookie, WWW-Authenticate, and + * Proxy-Authenticate headers, which will treat each such merged header as + * an additional instance of the header, for real-world compatibility + * reasons); when false, replaces any existing header of the given name (if + * any exists) with a new header with the specified value + * @throws NS_ERROR_INVALID_ARG + * if name or value is not a valid header component + * @throws NS_ERROR_NOT_AVAILABLE + * if this response is being processed asynchronously and data has been + * written to this response's body, or if seizePower() has been called on + * this + */ + void setHeader(in string name, in string value, in boolean merge); + + /** + * This is used for testing our header handling, so header will be sent out + * without transformation. There can be multiple headers. + */ + void setHeaderNoCheck(in string name, in string value); + + /** + * A stream to which data appearing in the body of this response (or in the + * totality of the response if seizePower() is called) should be written. + * After this response has been designated as being processed asynchronously, + * or after seizePower() has been called on this, subsequent writes will no + * longer be buffered and will be written to the underlying transport without + * delaying until the entire response is constructed. Write-through may or + * may not be synchronous in the implementation, and in any case particular + * behavior may not be observable to the HTTP client as intermediate buffers + * both in the server socket and in the client may delay written data; be + * prepared for delays at any time. + * + * @throws NS_ERROR_NOT_AVAILABLE + * if accessed after this response is fully constructed + */ + readonly attribute nsIOutputStream bodyOutputStream; + + /** + * Writes a string to the response's output stream. This method is merely a + * convenient shorthand for writing the same data to bodyOutputStream + * directly. + * + * @note + * This method is only guaranteed to work with ASCII data. + * @throws NS_ERROR_NOT_AVAILABLE + * if called after this response has been fully constructed + */ + void write(in string data); + + /** + * Signals that this response is being constructed asynchronously. Requests + * are typically completely constructed during nsIHttpRequestHandler.handle; + * however, responses which require significant resources (time, memory, + * processing) to construct can be created and sent incrementally by calling + * this method during the call to nsIHttpRequestHandler.handle. This method + * only has this effect when called during nsIHttpRequestHandler.handle; + * behavior is undefined if it is called at a later time. It may be called + * multiple times with no ill effect, so long as each call occurs before + * finish() is called. + * + * @throws NS_ERROR_UNEXPECTED + * if not initially called within a nsIHttpRequestHandler.handle call or if + * called after this response has been finished + * @throws NS_ERROR_NOT_AVAILABLE + * if seizePower() has been called on this + */ + void processAsync(); + + /** + * Seizes complete control of this response (and its connection) from the + * server, allowing raw and unfettered access to data being sent in the HTTP + * response. Once this method has been called the only property which may be + * accessed without an exception being thrown is bodyOutputStream, and the + * only methods which may be accessed without an exception being thrown are + * write(), finish(), and seizePower() (which may be called multiple times + * without ill effect so long as all calls are otherwise allowed). + * + * After a successful call, all data subsequently written to the body of this + * response is written directly to the corresponding connection. (Previously- + * written data is silently discarded.) No status line or headers are sent + * before doing so; if the response handler wishes to write such data, it must + * do so manually. Data generation completes only when finish() is called; it + * is not enough to simply call close() on bodyOutputStream. + * + * @throws NS_ERROR_NOT_AVAILABLE + * if processAsync() has been called on this + * @throws NS_ERROR_UNEXPECTED + * if finish() has been called on this + */ + void seizePower(); + + /** + * Signals that construction of this response is complete and that it may be + * sent over the network to the client, or if seizePower() has been called + * signals that all data has been written and that the underlying connection + * may be closed. This method may only be called after processAsync() or + * seizePower() has been called. This method is idempotent. + * + * @throws NS_ERROR_UNEXPECTED + * if processAsync() or seizePower() has not already been properly called + */ + void finish(); +}; diff --git a/netwerk/test/httpserver/test/data/cern_meta/caret_test.txt^^ b/netwerk/test/httpserver/test/data/cern_meta/caret_test.txt^^ new file mode 100644 index 0000000000..b005a65fd2 --- /dev/null +++ b/netwerk/test/httpserver/test/data/cern_meta/caret_test.txt^^ @@ -0,0 +1 @@ +If this has goofy headers on it, it's a success. diff --git a/netwerk/test/httpserver/test/data/cern_meta/caret_test.txt^^headers^ b/netwerk/test/httpserver/test/data/cern_meta/caret_test.txt^^headers^ new file mode 100644 index 0000000000..66e1522317 --- /dev/null +++ b/netwerk/test/httpserver/test/data/cern_meta/caret_test.txt^^headers^ @@ -0,0 +1,3 @@ +HTTP 500 This Isn't A Server Error +Foo-RFC: 3092 +Shaving-Cream-Atom: Illudium Phosdex diff --git a/netwerk/test/httpserver/test/data/cern_meta/test_both.html b/netwerk/test/httpserver/test/data/cern_meta/test_both.html new file mode 100644 index 0000000000..db18ea5d7a --- /dev/null +++ b/netwerk/test/httpserver/test/data/cern_meta/test_both.html @@ -0,0 +1,2 @@ +This page is a text file served with status 501. (That's really a lie, tho, +because this is definitely Implemented.) diff --git a/netwerk/test/httpserver/test/data/cern_meta/test_both.html^headers^ b/netwerk/test/httpserver/test/data/cern_meta/test_both.html^headers^ new file mode 100644 index 0000000000..bb3c16a2e2 --- /dev/null +++ b/netwerk/test/httpserver/test/data/cern_meta/test_both.html^headers^ @@ -0,0 +1,2 @@ +HTTP 501 Unimplemented +Content-Type: text/plain diff --git a/netwerk/test/httpserver/test/data/cern_meta/test_ctype_override.txt b/netwerk/test/httpserver/test/data/cern_meta/test_ctype_override.txt new file mode 100644 index 0000000000..7235fa32a5 --- /dev/null +++ b/netwerk/test/httpserver/test/data/cern_meta/test_ctype_override.txt @@ -0,0 +1,9 @@ +<html> +<head> + <title>This is really HTML, not text</title> +</head> +<body> +<p>This file is really HTML; the test_ctype_override.txt^headers^ file sets a + new header that overwrites the default text/plain header.</p> +</body> +</html> diff --git a/netwerk/test/httpserver/test/data/cern_meta/test_ctype_override.txt^headers^ b/netwerk/test/httpserver/test/data/cern_meta/test_ctype_override.txt^headers^ new file mode 100644 index 0000000000..156209f9c8 --- /dev/null +++ b/netwerk/test/httpserver/test/data/cern_meta/test_ctype_override.txt^headers^ @@ -0,0 +1 @@ +Content-Type: text/html diff --git a/netwerk/test/httpserver/test/data/cern_meta/test_status_override.html b/netwerk/test/httpserver/test/data/cern_meta/test_status_override.html new file mode 100644 index 0000000000..fd243c640e --- /dev/null +++ b/netwerk/test/httpserver/test/data/cern_meta/test_status_override.html @@ -0,0 +1,9 @@ +<html> +<head> + <title>This is a 404 page</title> +</head> +<body> +<p>This page has a 404 HTTP status associated with it, via + <code>test_status_override.html^headers^</code>.</p> +</body> +</html> diff --git a/netwerk/test/httpserver/test/data/cern_meta/test_status_override.html^headers^ b/netwerk/test/httpserver/test/data/cern_meta/test_status_override.html^headers^ new file mode 100644 index 0000000000..f438a05746 --- /dev/null +++ b/netwerk/test/httpserver/test/data/cern_meta/test_status_override.html^headers^ @@ -0,0 +1 @@ +HTTP 404 Can't Find This diff --git a/netwerk/test/httpserver/test/data/cern_meta/test_status_override_nodesc.txt b/netwerk/test/httpserver/test/data/cern_meta/test_status_override_nodesc.txt new file mode 100644 index 0000000000..4718ec282f --- /dev/null +++ b/netwerk/test/httpserver/test/data/cern_meta/test_status_override_nodesc.txt @@ -0,0 +1 @@ +This page has an HTTP status override without a description (it defaults to ""). diff --git a/netwerk/test/httpserver/test/data/cern_meta/test_status_override_nodesc.txt^headers^ b/netwerk/test/httpserver/test/data/cern_meta/test_status_override_nodesc.txt^headers^ new file mode 100644 index 0000000000..32da7632f9 --- /dev/null +++ b/netwerk/test/httpserver/test/data/cern_meta/test_status_override_nodesc.txt^headers^ @@ -0,0 +1 @@ +HTTP 732 diff --git a/netwerk/test/httpserver/test/data/name-scheme/bar.html^^ b/netwerk/test/httpserver/test/data/name-scheme/bar.html^^ new file mode 100644 index 0000000000..bed1f34c9f --- /dev/null +++ b/netwerk/test/httpserver/test/data/name-scheme/bar.html^^ @@ -0,0 +1,10 @@ +<html> +<head> + <title>Welcome to bar.html^</title> +</head> +<body> +<p>This file is named with two trailing carets, so the last is stripped + away, producing bar.html^ as the final name.</p> +</body> +</html> + diff --git a/netwerk/test/httpserver/test/data/name-scheme/bar.html^^headers^ b/netwerk/test/httpserver/test/data/name-scheme/bar.html^^headers^ new file mode 100644 index 0000000000..04fbaa08fe --- /dev/null +++ b/netwerk/test/httpserver/test/data/name-scheme/bar.html^^headers^ @@ -0,0 +1,2 @@ +HTTP 200 OK +Content-Type: text/html diff --git a/netwerk/test/httpserver/test/data/name-scheme/folder^^/ERROR_IF_SEE_THIS.txt^ b/netwerk/test/httpserver/test/data/name-scheme/folder^^/ERROR_IF_SEE_THIS.txt^ new file mode 100644 index 0000000000..dccee48e34 --- /dev/null +++ b/netwerk/test/httpserver/test/data/name-scheme/folder^^/ERROR_IF_SEE_THIS.txt^ @@ -0,0 +1 @@ +This file shouldn't be shown in directory listings. diff --git a/netwerk/test/httpserver/test/data/name-scheme/folder^^/SHOULD_SEE_THIS.txt^^ b/netwerk/test/httpserver/test/data/name-scheme/folder^^/SHOULD_SEE_THIS.txt^^ new file mode 100644 index 0000000000..a8ee35a3b6 --- /dev/null +++ b/netwerk/test/httpserver/test/data/name-scheme/folder^^/SHOULD_SEE_THIS.txt^^ @@ -0,0 +1 @@ +This file should show up in directory listings as SHOULD_SEE_THIS.txt^. diff --git a/netwerk/test/httpserver/test/data/name-scheme/folder^^/file.txt b/netwerk/test/httpserver/test/data/name-scheme/folder^^/file.txt new file mode 100644 index 0000000000..2ceca8ca9e --- /dev/null +++ b/netwerk/test/httpserver/test/data/name-scheme/folder^^/file.txt @@ -0,0 +1,2 @@ +File in a directory named with a trailing caret (in the virtual FS; on disk it +actually ends with two carets). diff --git a/netwerk/test/httpserver/test/data/name-scheme/foo.html^ b/netwerk/test/httpserver/test/data/name-scheme/foo.html^ new file mode 100644 index 0000000000..a3efe8b5c3 --- /dev/null +++ b/netwerk/test/httpserver/test/data/name-scheme/foo.html^ @@ -0,0 +1,9 @@ +<html> +<head> + <title>ERROR</title> +</head> +<body> +<p>This file should never be served by the web server because its name ends + with a caret not followed by another caret.</p> +</body> +</html> diff --git a/netwerk/test/httpserver/test/data/name-scheme/normal-file.txt b/netwerk/test/httpserver/test/data/name-scheme/normal-file.txt new file mode 100644 index 0000000000..ab71eabaf0 --- /dev/null +++ b/netwerk/test/httpserver/test/data/name-scheme/normal-file.txt @@ -0,0 +1 @@ +This should be seen. diff --git a/netwerk/test/httpserver/test/data/ranges/empty.txt b/netwerk/test/httpserver/test/data/ranges/empty.txt new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/netwerk/test/httpserver/test/data/ranges/empty.txt diff --git a/netwerk/test/httpserver/test/data/ranges/headers.txt b/netwerk/test/httpserver/test/data/ranges/headers.txt new file mode 100644 index 0000000000..6cf83528c8 --- /dev/null +++ b/netwerk/test/httpserver/test/data/ranges/headers.txt @@ -0,0 +1 @@ +Hello Kitty diff --git a/netwerk/test/httpserver/test/data/ranges/headers.txt^headers^ b/netwerk/test/httpserver/test/data/ranges/headers.txt^headers^ new file mode 100644 index 0000000000..d0a633f042 --- /dev/null +++ b/netwerk/test/httpserver/test/data/ranges/headers.txt^headers^ @@ -0,0 +1 @@ +X-SJS-Header: customized diff --git a/netwerk/test/httpserver/test/data/ranges/range.txt b/netwerk/test/httpserver/test/data/ranges/range.txt new file mode 100644 index 0000000000..ab71eabaf0 --- /dev/null +++ b/netwerk/test/httpserver/test/data/ranges/range.txt @@ -0,0 +1 @@ +This should be seen. diff --git a/netwerk/test/httpserver/test/data/sjs/cgi.sjs b/netwerk/test/httpserver/test/data/sjs/cgi.sjs new file mode 100644 index 0000000000..b1554f2bc9 --- /dev/null +++ b/netwerk/test/httpserver/test/data/sjs/cgi.sjs @@ -0,0 +1,8 @@ +function handleRequest(request, response) +{ + if (request.queryString == "throw") + throw "monkey wrench!"; + + response.setHeader("Content-Type", "text/plain", false); + response.write("PASS"); +} diff --git a/netwerk/test/httpserver/test/data/sjs/cgi.sjs^headers^ b/netwerk/test/httpserver/test/data/sjs/cgi.sjs^headers^ new file mode 100644 index 0000000000..a83ff774ab --- /dev/null +++ b/netwerk/test/httpserver/test/data/sjs/cgi.sjs^headers^ @@ -0,0 +1,2 @@ +HTTP 500 Error +This-Header: SHOULD NOT APPEAR IN CGI.JSC RESPONSES! diff --git a/netwerk/test/httpserver/test/data/sjs/object-state.sjs b/netwerk/test/httpserver/test/data/sjs/object-state.sjs new file mode 100644 index 0000000000..1d9ea8b4e4 --- /dev/null +++ b/netwerk/test/httpserver/test/data/sjs/object-state.sjs @@ -0,0 +1,87 @@ +function parseQueryString(str) +{ + var paramArray = str.split("&"); + var regex = /^([^=]+)=(.*)$/; + var params = {}; + for (var i = 0, sz = paramArray.length; i < sz; i++) + { + var match = regex.exec(paramArray[i]); + if (!match) + throw "Bad parameter in queryString! '" + paramArray[i] + "'"; + params[decodeURIComponent(match[1])] = decodeURIComponent(match[2]); + } + + return params; +} + +/* + * We're relying somewhat dubiously on all data being sent as soon as it's + * available at numerous levels (in Necko in the server-side part of the + * connection, in the OS's outgoing socket buffer, in the OS's incoming socket + * buffer, and in Necko in the client-side part of the connection), but to the + * best of my knowledge there's no way to force data flow at all those levels, + * so this is the best we can do. + */ +function handleRequest(request, response) +{ + response.setHeader("Cache-Control", "no-cache", false); + + /* + * NB: A Content-Type header is *necessary* to avoid content-sniffing, which + * will delay onStartRequest past the the point where the entire head of + * the response has been received. + */ + response.setHeader("Content-Type", "text/plain", false); + + var params = parseQueryString(request.queryString); + + switch (params.state) + { + case "initial": + response.processAsync(); + response.write("do"); + var state = + { + QueryInterface: function(iid) + { + if (iid.equals(Components.interfaces.nsISupports)) + return this; + throw Components.results.NS_ERROR_NO_INTERFACE; + }, + end: function() + { + response.write("ne"); + response.finish(); + } + }; + state.wrappedJSObject = state; + setObjectState("object-state-test", state); + getObjectState("object-state-test", function(obj) + { + if (obj !== state) + { + response.write("FAIL bad state save"); + response.finish(); + } + }); + break; + + case "intermediate": + response.write("intermediate"); + break; + + case "trigger": + response.write("trigger"); + getObjectState("object-state-test", function(obj) + { + obj.wrappedJSObject.end(); + setObjectState("object-state-test", null); + }); + break; + + default: + response.setStatusLine(request.httpVersion, 500, "Unexpected State"); + response.write("Bad state: " + params.state); + break; + } +} diff --git a/netwerk/test/httpserver/test/data/sjs/qi.sjs b/netwerk/test/httpserver/test/data/sjs/qi.sjs new file mode 100644 index 0000000000..89c7089b5f --- /dev/null +++ b/netwerk/test/httpserver/test/data/sjs/qi.sjs @@ -0,0 +1,48 @@ +const Ci = Components.interfaces; + +function handleRequest(request, response) +{ + var exstr, qid; + + response.setStatusLine(request.httpVersion, 500, "FAIL"); + + var passed = false; + try + { + qid = request.QueryInterface(Ci.nsIHttpRequest); + passed = qid === request; + } + catch (e) + { + exstr = ("" + e).split(/[\x09\x20-\x7f\x81-\xff]+/)[0]; + response.setStatusLine(request.httpVersion, 500, + "request doesn't QI: " + exstr); + return; + } + if (!passed) + { + response.setStatusLine(request.httpVersion, 500, "request QI'd wrongly?"); + return; + } + + passed = false; + try + { + qid = response.QueryInterface(Ci.nsIHttpResponse); + passed = qid === response; + } + catch (e) + { + exstr = ("" + e).split(/[\x09\x20-\x7f\x81-\xff]+/)[0]; + response.setStatusLine(request.httpVersion, 500, + "response doesn't QI: " + exstr); + return; + } + if (!passed) + { + response.setStatusLine(request.httpVersion, 500, "response QI'd wrongly?"); + return; + } + + response.setStatusLine(request.httpVersion, 200, "SJS QI Tests Passed"); +} diff --git a/netwerk/test/httpserver/test/data/sjs/range-checker.sjs b/netwerk/test/httpserver/test/data/sjs/range-checker.sjs new file mode 100644 index 0000000000..39fcc2b88e --- /dev/null +++ b/netwerk/test/httpserver/test/data/sjs/range-checker.sjs @@ -0,0 +1,3 @@ +function handleRequest(request, response) +{ +} diff --git a/netwerk/test/httpserver/test/data/sjs/sjs b/netwerk/test/httpserver/test/data/sjs/sjs new file mode 100644 index 0000000000..374ca41674 --- /dev/null +++ b/netwerk/test/httpserver/test/data/sjs/sjs @@ -0,0 +1,4 @@ +function handleRequest(request, response) +{ + response.write("FAIL"); +} diff --git a/netwerk/test/httpserver/test/data/sjs/state1.sjs b/netwerk/test/httpserver/test/data/sjs/state1.sjs new file mode 100644 index 0000000000..da2862d1e9 --- /dev/null +++ b/netwerk/test/httpserver/test/data/sjs/state1.sjs @@ -0,0 +1,42 @@ +function parseQueryString(str) +{ + var paramArray = str.split("&"); + var regex = /^([^=]+)=(.*)$/; + var params = {}; + for (var i = 0, sz = paramArray.length; i < sz; i++) + { + var match = regex.exec(paramArray[i]); + if (!match) + throw "Bad parameter in queryString! '" + paramArray[i] + "'"; + params[decodeURIComponent(match[1])] = decodeURIComponent(match[2]); + } + + return params; +} + +function handleRequest(request, response) +{ + response.setHeader("Cache-Control", "no-cache", false); + + var params = parseQueryString(request.queryString); + + var oldShared = getSharedState("shared-value"); + response.setHeader("X-Old-Shared-Value", oldShared, false); + + var newShared = params.newShared; + if (newShared !== undefined) + { + setSharedState("shared-value", newShared); + response.setHeader("X-New-Shared-Value", newShared, false); + } + + var oldPrivate = getState("private-value"); + response.setHeader("X-Old-Private-Value", oldPrivate, false); + + var newPrivate = params.newPrivate; + if (newPrivate !== undefined) + { + setState("private-value", newPrivate); + response.setHeader("X-New-Private-Value", newPrivate, false); + } +} diff --git a/netwerk/test/httpserver/test/data/sjs/state2.sjs b/netwerk/test/httpserver/test/data/sjs/state2.sjs new file mode 100644 index 0000000000..da2862d1e9 --- /dev/null +++ b/netwerk/test/httpserver/test/data/sjs/state2.sjs @@ -0,0 +1,42 @@ +function parseQueryString(str) +{ + var paramArray = str.split("&"); + var regex = /^([^=]+)=(.*)$/; + var params = {}; + for (var i = 0, sz = paramArray.length; i < sz; i++) + { + var match = regex.exec(paramArray[i]); + if (!match) + throw "Bad parameter in queryString! '" + paramArray[i] + "'"; + params[decodeURIComponent(match[1])] = decodeURIComponent(match[2]); + } + + return params; +} + +function handleRequest(request, response) +{ + response.setHeader("Cache-Control", "no-cache", false); + + var params = parseQueryString(request.queryString); + + var oldShared = getSharedState("shared-value"); + response.setHeader("X-Old-Shared-Value", oldShared, false); + + var newShared = params.newShared; + if (newShared !== undefined) + { + setSharedState("shared-value", newShared); + response.setHeader("X-New-Shared-Value", newShared, false); + } + + var oldPrivate = getState("private-value"); + response.setHeader("X-Old-Private-Value", oldPrivate, false); + + var newPrivate = params.newPrivate; + if (newPrivate !== undefined) + { + setState("private-value", newPrivate); + response.setHeader("X-New-Private-Value", newPrivate, false); + } +} diff --git a/netwerk/test/httpserver/test/data/sjs/thrower.sjs b/netwerk/test/httpserver/test/data/sjs/thrower.sjs new file mode 100644 index 0000000000..1aaf1639a3 --- /dev/null +++ b/netwerk/test/httpserver/test/data/sjs/thrower.sjs @@ -0,0 +1,6 @@ +function handleRequest(request, response) +{ + if (request.queryString == "throw") + undefined[5]; + response.setHeader("X-Test-Status", "PASS", false); +} diff --git a/netwerk/test/httpserver/test/head_utils.js b/netwerk/test/httpserver/test/head_utils.js new file mode 100644 index 0000000000..21f6151174 --- /dev/null +++ b/netwerk/test/httpserver/test/head_utils.js @@ -0,0 +1,600 @@ +/* -*- 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/. */ + +var _HTTPD_JS_PATH = __LOCATION__.parent; +_HTTPD_JS_PATH.append("httpd.js"); +load(_HTTPD_JS_PATH.path); + +// if these tests fail, we'll want the debug output +DEBUG = true; + +Cu.import("resource://gre/modules/NetUtil.jsm"); + +/** + * Constructs a new nsHttpServer instance. This function is intended to + * encapsulate construction of a server so that at some point in the future it + * is possible to run these tests (with at most slight modifications) against + * the server when used as an XPCOM component (not as an inline script). + */ +function createServer() +{ + return new nsHttpServer(); +} + +/** + * Creates a new HTTP channel. + * + * @param url + * the URL of the channel to create + */ +function makeChannel(url) +{ + return NetUtil.newChannel({uri: url, loadUsingSystemPrincipal: true}) + .QueryInterface(Ci.nsIHttpChannel); +} + +/** + * Make a binary input stream wrapper for the given stream. + * + * @param stream + * the nsIInputStream to wrap + */ +function makeBIS(stream) +{ + return new BinaryInputStream(stream); +} + + +/** + * Returns the contents of the file as a string. + * + * @param file : nsILocalFile + * the file whose contents are to be read + * @returns string + * the contents of the file + */ +function fileContents(file) +{ + const PR_RDONLY = 0x01; + var fis = new FileInputStream(file, PR_RDONLY, 0o444, + Ci.nsIFileInputStream.CLOSE_ON_EOF); + var sis = new ScriptableInputStream(fis); + var contents = sis.read(file.fileSize); + sis.close(); + return contents; +} + +/** + * Iterates over the lines, delimited by CRLF, in data, returning each line + * without the trailing line separator. + * + * @param data : string + * a string consisting of lines of data separated by CRLFs + * @returns Iterator + * an Iterator which returns each line from data in turn; note that this + * includes a final empty line if data ended with a CRLF + */ +function LineIterator(data) +{ + var start = 0, index = 0; + do + { + index = data.indexOf("\r\n"); + if (index >= 0) + yield data.substring(0, index); + else + yield data; + + data = data.substring(index + 2); + } + while (index >= 0); +} + +/** + * Throws if iter does not contain exactly the CRLF-separated lines in the + * array expectedLines. + * + * @param iter : Iterator + * an Iterator which returns lines of text + * @param expectedLines : [string] + * an array of the expected lines of text + * @throws string + * an error message if iter doesn't agree with expectedLines + */ +function expectLines(iter, expectedLines) +{ + var index = 0; + for (var line in iter) + { + if (expectedLines.length == index) + throw "Error: got more than " + expectedLines.length + " expected lines!"; + + var expected = expectedLines[index++]; + if (expected !== line) + throw "Error on line " + index + "!\n" + + " actual: '" + line + "',\n" + + " expect: '" + expected + "'"; + } + + if (expectedLines.length !== index) + { + throw "Expected more lines! Got " + index + + ", expected " + expectedLines.length; + } +} + +/** + * Spew a bunch of HTTP metadata from request into the body of response. + * + * @param request : nsIHttpRequest + * the request whose metadata should be output + * @param response : nsIHttpResponse + * the response to which the metadata is written + */ +function writeDetails(request, response) +{ + response.write("Method: " + request.method + "\r\n"); + response.write("Path: " + request.path + "\r\n"); + response.write("Query: " + request.queryString + "\r\n"); + response.write("Version: " + request.httpVersion + "\r\n"); + response.write("Scheme: " + request.scheme + "\r\n"); + response.write("Host: " + request.host + "\r\n"); + response.write("Port: " + request.port); +} + +/** + * Advances iter past all non-blank lines and a single blank line, after which + * point the body of the response will be returned next from the iterator. + * + * @param iter : Iterator + * an iterator over the CRLF-delimited lines in an HTTP response, currently + * just after the Request-Line + */ +function skipHeaders(iter) +{ + var line = iter.next(); + while (line !== "") + line = iter.next(); +} + +/** + * Checks that the exception e (which may be an XPConnect-created exception + * object or a raw nsresult number) is the given nsresult. + * + * @param e : Exception or nsresult + * the actual exception + * @param code : nsresult + * the expected exception + */ +function isException(e, code) +{ + if (e !== code && e.result !== code) + do_throw("unexpected error: " + e); +} + +/** + * Calls the given function at least the specified number of milliseconds later. + * The callback will not undershoot the given time, but it might overshoot -- + * don't expect precision! + * + * @param milliseconds : uint + * the number of milliseconds to delay + * @param callback : function() : void + * the function to call + */ +function callLater(msecs, callback) +{ + do_timeout(msecs, callback); +} + + +/******************************************************* + * SIMPLE SUPPORT FOR LOADING/TESTING A SERIES OF URLS * + *******************************************************/ + +/** + * Create a completion callback which will stop the given server and end the + * test, assuming nothing else remains to be done at that point. + */ +function testComplete(srv) +{ + return function complete() + { + do_test_pending(); + srv.stop(function quit() { do_test_finished(); }); + }; +} + +/** + * Represents a path to load from the tested HTTP server, along with actions to + * take before, during, and after loading the associated page. + * + * @param path + * the URL to load from the server + * @param initChannel + * a function which takes as a single parameter a channel created for path and + * initializes its state, or null if no additional initialization is needed + * @param onStartRequest + * called during onStartRequest for the load of the URL, with the same + * parameters; the request parameter has been QI'd to nsIHttpChannel and + * nsIHttpChannelInternal for convenience; may be null if nothing needs to be + * done + * @param onStopRequest + * called during onStopRequest for the channel, with the same parameters plus + * a trailing parameter containing an array of the bytes of data downloaded in + * the body of the channel response; the request parameter has been QI'd to + * nsIHttpChannel and nsIHttpChannelInternal for convenience; may be null if + * nothing needs to be done + */ +function Test(path, initChannel, onStartRequest, onStopRequest) +{ + function nil() { } + + this.path = path; + this.initChannel = initChannel || nil; + this.onStartRequest = onStartRequest || nil; + this.onStopRequest = onStopRequest || nil; +} + +/** + * Runs all the tests in testArray. + * + * @param testArray + * a non-empty array of Tests to run, in order + * @param done + * function to call when all tests have run (e.g. to shut down the server) + */ +function runHttpTests(testArray, done) +{ + /** Kicks off running the next test in the array. */ + function performNextTest() + { + if (++testIndex == testArray.length) + { + try + { + done(); + } + catch (e) + { + do_report_unexpected_exception(e, "running test-completion callback"); + } + return; + } + + do_test_pending(); + + var test = testArray[testIndex]; + var ch = makeChannel(test.path); + try + { + test.initChannel(ch); + } + catch (e) + { + try + { + do_report_unexpected_exception(e, "testArray[" + testIndex + "].initChannel(ch)"); + } + catch (e) + { + /* swallow and let tests continue */ + } + } + + listener._channel = ch; + ch.asyncOpen2(listener); + } + + /** Index of the test being run. */ + var testIndex = -1; + + /** Stream listener for the channels. */ + var listener = + { + /** Current channel being observed by this. */ + _channel: null, + /** Array of bytes of data in body of response. */ + _data: [], + + onStartRequest: function(request, cx) + { + do_check_true(request === this._channel); + var ch = request.QueryInterface(Ci.nsIHttpChannel) + .QueryInterface(Ci.nsIHttpChannelInternal); + + this._data.length = 0; + try + { + try + { + testArray[testIndex].onStartRequest(ch, cx); + } + catch (e) + { + do_report_unexpected_exception(e, "testArray[" + testIndex + "].onStartRequest"); + } + } + catch (e) + { + do_note_exception(e, "!!! swallowing onStartRequest exception so onStopRequest is " + + "called..."); + } + }, + onDataAvailable: function(request, cx, inputStream, offset, count) + { + var quantum = 262144; // just above half the argument-count limit + var bis = makeBIS(inputStream); + for (var start = 0; start < count; start += quantum) + { + var newData = bis.readByteArray(Math.min(quantum, count - start)); + Array.prototype.push.apply(this._data, newData); + } + }, + onStopRequest: function(request, cx, status) + { + this._channel = null; + + var ch = request.QueryInterface(Ci.nsIHttpChannel) + .QueryInterface(Ci.nsIHttpChannelInternal); + + // NB: The onStopRequest callback must run before performNextTest here, + // because the latter runs the next test's initChannel callback, and + // we want one test to be sequentially processed before the next + // one. + try + { + testArray[testIndex].onStopRequest(ch, cx, status, this._data); + } + finally + { + try + { + performNextTest(); + } + finally + { + do_test_finished(); + } + } + }, + QueryInterface: function(aIID) + { + if (aIID.equals(Ci.nsIStreamListener) || + aIID.equals(Ci.nsIRequestObserver) || + aIID.equals(Ci.nsISupports)) + return this; + throw Cr.NS_ERROR_NO_INTERFACE; + } + }; + + performNextTest(); +} + + +/**************************************** + * RAW REQUEST FORMAT TESTING FUNCTIONS * + ****************************************/ + +/** + * Sends a raw string of bytes to the given host and port and checks that the + * response is acceptable. + * + * @param host : string + * the host to which a connection should be made + * @param port : PRUint16 + * the port to use for the connection + * @param data : string or [string...] + * either: + * - the raw data to send, as a string of characters with codes in the + * range 0-255, or + * - an array of such strings whose concatenation forms the raw data + * @param responseCheck : function(string) : void + * a function which is provided with the data sent by the remote host which + * conducts whatever tests it wants on that data; useful for tweaking the test + * environment between tests + */ +function RawTest(host, port, data, responseCheck) +{ + if (0 > port || 65535 < port || port % 1 !== 0) + throw "bad port"; + if (!(data instanceof Array)) + data = [data]; + if (data.length <= 0) + throw "bad data length"; + if (!data.every(function(v) { return /^[\x00-\xff]*$/.test(v); })) + throw "bad data contained non-byte-valued character"; + + this.host = host; + this.port = port; + this.data = data; + this.responseCheck = responseCheck; +} + +/** + * Runs all the tests in testArray, an array of RawTests. + * + * @param testArray : [RawTest] + * an array of RawTests to run, in order + * @param done + * function to call when all tests have run (e.g. to shut down the server) + */ +function runRawTests(testArray, done) +{ + do_test_pending(); + + var sts = Cc["@mozilla.org/network/socket-transport-service;1"] + .getService(Ci.nsISocketTransportService); + + var currentThread = Cc["@mozilla.org/thread-manager;1"] + .getService() + .currentThread; + + /** Kicks off running the next test in the array. */ + function performNextTest() + { + if (++testIndex == testArray.length) + { + do_test_finished(); + try + { + done(); + } + catch (e) + { + do_report_unexpected_exception(e, "running test-completion callback"); + } + return; + } + + + var rawTest = testArray[testIndex]; + + var transport = + sts.createTransport(null, 0, rawTest.host, rawTest.port, null); + + var inStream = transport.openInputStream(0, 0, 0); + var outStream = transport.openOutputStream(0, 0, 0); + + // reset + dataIndex = 0; + received = ""; + + waitForMoreInput(inStream); + waitToWriteOutput(outStream); + } + + function waitForMoreInput(stream) + { + reader.stream = stream; + stream = stream.QueryInterface(Ci.nsIAsyncInputStream); + stream.asyncWait(reader, 0, 0, currentThread); + } + + function waitToWriteOutput(stream) + { + // Do the QueryInterface here, not earlier, because there is no + // guarantee that 'stream' passed in here been QIed to nsIAsyncOutputStream + // since the last GC. + stream = stream.QueryInterface(Ci.nsIAsyncOutputStream); + stream.asyncWait(writer, 0, testArray[testIndex].data[dataIndex].length, + currentThread); + } + + /** Index of the test being run. */ + var testIndex = -1; + + /** + * Index of remaining data strings to be written to the socket in current + * test. + */ + var dataIndex = 0; + + /** Data received so far from the server. */ + var received = ""; + + /** Reads data from the socket. */ + var reader = + { + onInputStreamReady: function(stream) + { + do_check_true(stream === this.stream); + try + { + var bis = new BinaryInputStream(stream); + + var av = 0; + try + { + av = bis.available(); + } + catch (e) + { + /* default to 0 */ + do_note_exception(e); + } + + if (av > 0) + { + var quantum = 262144; + for (var start = 0; start < av; start += quantum) + { + var bytes = bis.readByteArray(Math.min(quantum, av - start)); + received += String.fromCharCode.apply(null, bytes); + } + waitForMoreInput(stream); + return; + } + } + catch(e) + { + do_report_unexpected_exception(e); + } + + var rawTest = testArray[testIndex]; + try + { + rawTest.responseCheck(received); + } + catch (e) + { + do_report_unexpected_exception(e); + } + finally + { + try + { + stream.close(); + performNextTest(); + } + catch (e) + { + do_report_unexpected_exception(e); + } + } + } + }; + + /** Writes data to the socket. */ + var writer = + { + onOutputStreamReady: function(stream) + { + var str = testArray[testIndex].data[dataIndex]; + + var written = 0; + try + { + written = stream.write(str, str.length); + if (written == str.length) + dataIndex++; + else + testArray[testIndex].data[dataIndex] = str.substring(written); + } + catch (e) + { + do_note_exception(e); + /* stream could have been closed, just ignore */ + } + + try + { + // Keep writing data while we can write and + // until there's no more data to read + if (written > 0 && dataIndex < testArray[testIndex].data.length) + waitToWriteOutput(stream); + else + stream.close(); + } + catch (e) + { + do_report_unexpected_exception(e); + } + } + }; + + performNextTest(); +} diff --git a/netwerk/test/httpserver/test/test_async_response_sending.js b/netwerk/test/httpserver/test/test_async_response_sending.js new file mode 100644 index 0000000000..84ec74daf4 --- /dev/null +++ b/netwerk/test/httpserver/test/test_async_response_sending.js @@ -0,0 +1,1683 @@ +/* -*- 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/. */ + +/* + * Ensures that data a request handler writes out in response is sent only as + * quickly as the client can receive it, without racing ahead and being forced + * to block while writing that data. + * + * NB: These tests are extremely tied to the current implementation, in terms of + * when and how stream-ready notifications occur, the amount of data which will + * be read or written at each notification, and so on. If the implementation + * changes in any way with respect to stream copying, this test will probably + * have to change a little at the edges as well. + */ + +gThreadManager = Cc["@mozilla.org/thread-manager;1"].createInstance(); + +function run_test() +{ + do_test_pending(); + tests.push(function testsComplete(_) + { + dumpn("******************\n" + + "* TESTS COMPLETE *\n" + + "******************"); + do_test_finished(); + }); + + runNextTest(); +} + +function runNextTest() +{ + testIndex++; + dumpn("*** runNextTest(), testIndex: " + testIndex); + + try + { + var test = tests[testIndex]; + test(runNextTest); + } + catch (e) + { + var msg = "exception running test " + testIndex + ": " + e; + if (e && "stack" in e) + msg += "\nstack follows:\n" + e.stack; + do_throw(msg); + } +} + + +/************* + * TEST DATA * + *************/ + +const NOTHING = []; + +const FIRST_SEGMENT = [1, 2, 3, 4]; +const SECOND_SEGMENT = [5, 6, 7, 8]; +const THIRD_SEGMENT = [9, 10, 11, 12]; + +const SEGMENT = FIRST_SEGMENT; +const TWO_SEGMENTS = [1, 2, 3, 4, 5, 6, 7, 8]; +const THREE_SEGMENTS = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]; + +const SEGMENT_AND_HALF = [1, 2, 3, 4, 5, 6]; + +const QUARTER_SEGMENT = [1]; +const HALF_SEGMENT = [1, 2]; +const SECOND_HALF_SEGMENT = [3, 4]; +const THREE_QUARTER_SEGMENT = [1, 2, 3]; +const EXTRA_HALF_SEGMENT = [5, 6]; +const MIDDLE_HALF_SEGMENT = [2, 3]; +const LAST_QUARTER_SEGMENT = [4]; +const FOURTH_HALF_SEGMENT = [7, 8]; +const HALF_THIRD_SEGMENT = [9, 10]; +const LATTER_HALF_THIRD_SEGMENT = [11, 12]; + +const TWO_HALF_SEGMENTS = [1, 2, 1, 2]; + + +/********* + * TESTS * + *********/ + +var tests = + [ + sourceClosedWithoutWrite, + writeOneSegmentThenClose, + simpleWriteThenRead, + writeLittleBeforeReading, + writeMultipleSegmentsThenRead, + writeLotsBeforeReading, + writeLotsBeforeReading2, + writeThenReadPartial, + manyPartialWrites, + partialRead, + partialWrite, + sinkClosedImmediately, + sinkClosedWithReadableData, + sinkClosedAfterWrite, + sourceAndSinkClosed, + sinkAndSourceClosed, + sourceAndSinkClosedWithPendingData, + sinkAndSourceClosedWithPendingData, + ]; +var testIndex = -1; + +function sourceClosedWithoutWrite(next) +{ + var t = new CopyTest("sourceClosedWithoutWrite", next); + + t.closeSource(Cr.NS_OK); + t.expect(Cr.NS_OK, [NOTHING]); +} + +function writeOneSegmentThenClose(next) +{ + var t = new CopyTest("writeLittleBeforeReading", next); + + t.addToSource(SEGMENT); + t.makeSourceReadable(SEGMENT.length); + t.closeSource(Cr.NS_OK); + t.makeSinkWritableAndWaitFor(SEGMENT.length, [SEGMENT]); + t.expect(Cr.NS_OK, [SEGMENT]); +} + +function simpleWriteThenRead(next) +{ + var t = new CopyTest("simpleWriteThenRead", next); + + t.addToSource(SEGMENT); + t.makeSourceReadable(SEGMENT.length); + t.makeSinkWritableAndWaitFor(SEGMENT.length, [SEGMENT]); + t.closeSource(Cr.NS_OK); + t.expect(Cr.NS_OK, [SEGMENT]); +} + +function writeLittleBeforeReading(next) +{ + var t = new CopyTest("writeLittleBeforeReading", next); + + t.addToSource(SEGMENT); + t.makeSourceReadable(SEGMENT.length); + t.addToSource(SEGMENT); + t.makeSourceReadable(SEGMENT.length); + t.closeSource(Cr.NS_OK); + t.makeSinkWritableAndWaitFor(SEGMENT.length, [SEGMENT]); + t.makeSinkWritableAndWaitFor(SEGMENT.length, [SEGMENT]); + t.expect(Cr.NS_OK, [SEGMENT, SEGMENT]); +} + +function writeMultipleSegmentsThenRead(next) +{ + var t = new CopyTest("writeMultipleSegmentsThenRead", next); + + t.addToSource(TWO_SEGMENTS); + t.makeSourceReadable(TWO_SEGMENTS.length); + t.makeSinkWritableAndWaitFor(TWO_SEGMENTS.length, + [FIRST_SEGMENT, SECOND_SEGMENT]); + t.closeSource(Cr.NS_OK); + t.expect(Cr.NS_OK, [TWO_SEGMENTS]); +} + +function writeLotsBeforeReading(next) +{ + var t = new CopyTest("writeLotsBeforeReading", next); + + t.addToSource(TWO_SEGMENTS); + t.makeSourceReadable(TWO_SEGMENTS.length); + t.makeSinkWritableAndWaitFor(FIRST_SEGMENT.length, [FIRST_SEGMENT]); + t.addToSource(SEGMENT); + t.makeSourceReadable(SEGMENT.length); + t.makeSinkWritableAndWaitFor(SECOND_SEGMENT.length, [SECOND_SEGMENT]); + t.addToSource(SEGMENT); + t.makeSourceReadable(SEGMENT.length); + t.closeSource(Cr.NS_OK); + t.makeSinkWritableAndWaitFor(2 * SEGMENT.length, [SEGMENT, SEGMENT]); + t.expect(Cr.NS_OK, [TWO_SEGMENTS, SEGMENT, SEGMENT]); +} + +function writeLotsBeforeReading2(next) +{ + var t = new CopyTest("writeLotsBeforeReading", next); + + t.addToSource(THREE_SEGMENTS); + t.makeSourceReadable(THREE_SEGMENTS.length); + t.makeSinkWritableAndWaitFor(FIRST_SEGMENT.length, [FIRST_SEGMENT]); + t.addToSource(SEGMENT); + t.makeSourceReadable(SEGMENT.length); + t.makeSinkWritableAndWaitFor(SECOND_SEGMENT.length, [SECOND_SEGMENT]); + t.addToSource(SEGMENT); + t.makeSourceReadable(SEGMENT.length); + t.makeSinkWritableAndWaitFor(THIRD_SEGMENT.length, [THIRD_SEGMENT]); + t.closeSource(Cr.NS_OK); + t.makeSinkWritableAndWaitFor(2 * SEGMENT.length, [SEGMENT, SEGMENT]); + t.expect(Cr.NS_OK, [THREE_SEGMENTS, SEGMENT, SEGMENT]); +} + +function writeThenReadPartial(next) +{ + var t = new CopyTest("writeThenReadPartial", next); + + t.addToSource(SEGMENT_AND_HALF); + t.makeSourceReadable(SEGMENT_AND_HALF.length); + t.makeSinkWritableAndWaitFor(SEGMENT.length, [SEGMENT]); + t.closeSource(Cr.NS_OK); + t.makeSinkWritableAndWaitFor(EXTRA_HALF_SEGMENT.length, [EXTRA_HALF_SEGMENT]); + t.expect(Cr.NS_OK, [SEGMENT_AND_HALF]); +} + +function manyPartialWrites(next) +{ + var t = new CopyTest("manyPartialWrites", next); + + t.addToSource(HALF_SEGMENT); + t.makeSourceReadable(HALF_SEGMENT.length); + + t.addToSource(HALF_SEGMENT); + t.makeSourceReadable(HALF_SEGMENT.length); + t.makeSinkWritableAndWaitFor(2 * HALF_SEGMENT.length, [TWO_HALF_SEGMENTS]); + t.closeSource(Cr.NS_OK); + t.expect(Cr.NS_OK, [TWO_HALF_SEGMENTS]); +} + +function partialRead(next) +{ + var t = new CopyTest("partialRead", next); + + t.addToSource(SEGMENT); + t.makeSourceReadable(SEGMENT.length); + t.addToSource(HALF_SEGMENT); + t.makeSourceReadable(HALF_SEGMENT.length); + t.makeSinkWritableAndWaitFor(SEGMENT.length, [SEGMENT]); + t.closeSourceAndWaitFor(Cr.NS_OK, HALF_SEGMENT.length, [HALF_SEGMENT]); + t.expect(Cr.NS_OK, [SEGMENT, HALF_SEGMENT]); +} + +function partialWrite(next) +{ + var t = new CopyTest("partialWrite", next); + + t.addToSource(SEGMENT); + t.makeSourceReadable(SEGMENT.length); + t.makeSinkWritableByIncrementsAndWaitFor(SEGMENT.length, + [QUARTER_SEGMENT, + MIDDLE_HALF_SEGMENT, + LAST_QUARTER_SEGMENT]); + + t.addToSource(SEGMENT); + t.makeSourceReadable(SEGMENT.length); + t.makeSinkWritableByIncrementsAndWaitFor(SEGMENT.length, + [HALF_SEGMENT, SECOND_HALF_SEGMENT]); + + t.addToSource(THREE_SEGMENTS); + t.makeSourceReadable(THREE_SEGMENTS.length); + t.makeSinkWritableByIncrementsAndWaitFor(THREE_SEGMENTS.length, + [HALF_SEGMENT, SECOND_HALF_SEGMENT, + SECOND_SEGMENT, + HALF_THIRD_SEGMENT, + LATTER_HALF_THIRD_SEGMENT]); + + t.closeSource(Cr.NS_OK); + t.expect(Cr.NS_OK, [SEGMENT, SEGMENT, THREE_SEGMENTS]); +} + +function sinkClosedImmediately(next) +{ + var t = new CopyTest("sinkClosedImmediately", next); + + t.closeSink(Cr.NS_OK); + t.expect(Cr.NS_ERROR_UNEXPECTED, [NOTHING]); +} + +function sinkClosedWithReadableData(next) +{ + var t = new CopyTest("sinkClosedWithReadableData", next); + + t.addToSource(SEGMENT); + t.makeSourceReadable(SEGMENT.length); + t.closeSink(Cr.NS_OK); + t.expect(Cr.NS_ERROR_UNEXPECTED, [NOTHING]); +} + +function sinkClosedAfterWrite(next) +{ + var t = new CopyTest("sinkClosedAfterWrite", next); + + t.addToSource(TWO_SEGMENTS); + t.makeSourceReadable(TWO_SEGMENTS.length); + t.makeSinkWritableAndWaitFor(FIRST_SEGMENT.length, [FIRST_SEGMENT]); + t.closeSink(Cr.NS_OK); + t.expect(Cr.NS_ERROR_UNEXPECTED, [FIRST_SEGMENT]); +} + +function sourceAndSinkClosed(next) +{ + var t = new CopyTest("sourceAndSinkClosed", next); + + t.closeSourceThenSink(Cr.NS_OK, Cr.NS_OK); + t.expect(Cr.NS_OK, []); +} + +function sinkAndSourceClosed(next) +{ + var t = new CopyTest("sinkAndSourceClosed", next); + + t.closeSinkThenSource(Cr.NS_OK, Cr.NS_OK); + + // sink notify received first, hence error + t.expect(Cr.NS_ERROR_UNEXPECTED, []); +} + +function sourceAndSinkClosedWithPendingData(next) +{ + var t = new CopyTest("sourceAndSinkClosedWithPendingData", next); + + t.addToSource(SEGMENT); + t.makeSourceReadable(SEGMENT.length); + + t.closeSourceThenSink(Cr.NS_OK, Cr.NS_OK); + + // not all data from source copied, so error + t.expect(Cr.NS_ERROR_UNEXPECTED, []); +} + +function sinkAndSourceClosedWithPendingData(next) +{ + var t = new CopyTest("sinkAndSourceClosedWithPendingData", next); + + t.addToSource(SEGMENT); + t.makeSourceReadable(SEGMENT.length); + + t.closeSinkThenSource(Cr.NS_OK, Cr.NS_OK); + + // not all data from source copied, plus sink notify received first, so error + t.expect(Cr.NS_ERROR_UNEXPECTED, []); +} + + +/************* + * UTILITIES * + *************/ + +/** Returns the sum of the elements in arr. */ +function sum(arr) +{ + var sum = 0; + for (var i = 0, sz = arr.length; i < sz; i++) + sum += arr[i]; + return sum; +} + +/** + * Returns a constructor for an input or output stream callback that will wrap + * the one provided to it as an argument. + * + * @param wrapperCallback : (nsIInputStreamCallback | nsIOutputStreamCallback) : void + * the original callback object (not a function!) being wrapped + * @param name : string + * either "onInputStreamReady" if we're wrapping an input stream callback or + * "onOutputStreamReady" if we're wrapping an output stream callback + * @returns function(nsIInputStreamCallback | nsIOutputStreamCallback) : (nsIInputStreamCallback | nsIOutputStreamCallback) + * a constructor function which constructs a callback object (not function!) + * which, when called, first calls the original callback provided to it and + * then calls wrapperCallback + */ +function createStreamReadyInterceptor(wrapperCallback, name) +{ + return function StreamReadyInterceptor(callback) + { + this.wrappedCallback = callback; + this[name] = function streamReadyInterceptor(stream) + { + dumpn("*** StreamReadyInterceptor." + name); + + try + { + dumpn("*** calling original " + name + "..."); + callback[name](stream); + } + catch (e) + { + dumpn("!!! error running inner callback: " + e); + throw e; + } + finally + { + dumpn("*** calling wrapper " + name + "..."); + wrapperCallback[name](stream); + } + } + }; +} + +/** + * Print out a banner with the given message, uppercased, for debugging + * purposes. + */ +function note(m) +{ + m = m.toUpperCase(); + var asterisks = Array(m.length + 1 + 4).join("*"); + dumpn(asterisks + "\n* " + m + " *\n" + asterisks); +} + + +/*********** + * MOCKERY * + ***********/ + +/* + * Blatantly violate abstractions in the name of testability. THIS IS NOT + * PUBLIC API! If you use any of these I will knowingly break your code by + * changing the names of variables and properties. + */ +var BinaryInputStream = function BIS(stream) { return stream; }; +var BinaryOutputStream = function BOS(stream) { return stream; }; +Response.SEGMENT_SIZE = SEGMENT.length; + +/** + * Roughly mocks an nsIPipe, presenting non-blocking input and output streams + * that appear to also be binary streams and whose readability and writability + * amounts are configurable. Only the methods used in this test have been + * implemented -- these aren't exact mocks (can't be, actually, because input + * streams have unscriptable methods). + * + * @param name : string + * a name for this pipe, used in debugging output + */ +function CustomPipe(name) +{ + var self = this; + + /** Data read from input that's buffered until it can be written to output. */ + this._data = []; + + /** + * The status of this pipe, which is to say the error result the ends of this + * pipe will return when attempts are made to use them. This value is always + * an error result when copying has finished, because success codes are + * converted to NS_BASE_STREAM_CLOSED. + */ + this._status = Cr.NS_OK; + + /** The input end of this pipe. */ + var input = this.inputStream = + { + /** A name for this stream, used in debugging output. */ + name: name + " input", + + /** + * The number of bytes of data available to be read from this pipe, or + * Infinity if any amount of data in this pipe is made readable as soon as + * it is written to the pipe output. + */ + _readable: 0, + + /** + * Data regarding a pending stream-ready callback on this, or null if no + * callback is currently waiting to be called. + */ + _waiter: null, + + /** + * The event currently dispatched to make a stream-ready callback, if any + * such callback is currently ready to be made and not already in + * progress, or null when no callback is waiting to happen. + */ + _event: null, + + /** + * A stream-ready constructor to wrap an existing callback to intercept + * stream-ready notifications, or null if notifications shouldn't be + * wrapped at all. + */ + _streamReadyInterceptCreator: null, + + /** + * Registers a stream-ready wrapper creator function so that a + * stream-ready callback made in the future can be wrapped. + */ + interceptStreamReadyCallbacks: function(streamReadyInterceptCreator) + { + dumpn("*** [" + this.name + "].interceptStreamReadyCallbacks"); + + do_check_true(this._streamReadyInterceptCreator === null, + "intercepting twice"); + this._streamReadyInterceptCreator = streamReadyInterceptCreator; + if (this._waiter) + { + this._waiter.callback = + new streamReadyInterceptCreator(this._waiter.callback); + } + }, + + /** + * Removes a previously-registered stream-ready wrapper creator function, + * also clearing any current wrapping. + */ + removeStreamReadyInterceptor: function() + { + dumpn("*** [" + this.name + "].removeStreamReadyInterceptor()"); + + do_check_true(this._streamReadyInterceptCreator !== null, + "removing interceptor when none present?"); + this._streamReadyInterceptCreator = null; + if (this._waiter) + this._waiter.callback = this._waiter.callback.wrappedCallback; + }, + + // + // see nsIAsyncInputStream.asyncWait + // + asyncWait: function asyncWait(callback, flags, requestedCount, target) + { + dumpn("*** [" + this.name + "].asyncWait"); + + do_check_true(callback && typeof callback !== "function"); + + var closureOnly = + (flags & Ci.nsIAsyncInputStream.WAIT_CLOSURE_ONLY) !== 0; + + do_check_true(this._waiter === null || + (this._waiter.closureOnly && !closureOnly), + "asyncWait already called with a non-closure-only " + + "callback? unexpected!"); + + this._waiter = + { + callback: + this._streamReadyInterceptCreator + ? new this._streamReadyInterceptCreator(callback) + : callback, + closureOnly: closureOnly, + requestedCount: requestedCount, + eventTarget: target + }; + + if (!Components.isSuccessCode(self._status) || + (!closureOnly && this._readable >= requestedCount && + self._data.length >= requestedCount)) + { + this._notify(); + } + }, + + // + // see nsIAsyncInputStream.closeWithStatus + // + closeWithStatus: function closeWithStatus(status) + { + dumpn("*** [" + this.name + "].closeWithStatus" + + "(" + status + ")"); + + if (!Components.isSuccessCode(self._status)) + { + dumpn("*** ignoring second closure of [input " + this.name + "] " + + "(status " + self._status + ")"); + return; + } + + if (Components.isSuccessCode(status)) + status = Cr.NS_BASE_STREAM_CLOSED; + + self._status = status; + + if (this._waiter) + this._notify(); + if (output._waiter) + output._notify(); + }, + + // + // see nsIBinaryInputStream.readByteArray + // + readByteArray: function readByteArray(count) + { + dumpn("*** [" + this.name + "].readByteArray(" + count + ")"); + + if (self._data.length === 0) + { + throw Components.isSuccessCode(self._status) + ? Cr.NS_BASE_STREAM_WOULD_BLOCK + : self._status; + } + + do_check_true(this._readable <= self._data.length || + this._readable === Infinity, + "consistency check"); + + if (this._readable < count || self._data.length < count) + throw Cr.NS_BASE_STREAM_WOULD_BLOCK; + this._readable -= count; + return self._data.splice(0, count); + }, + + /** + * Makes the given number of additional bytes of data previously written + * to the pipe's output stream available for reading, triggering future + * notifications when required. + * + * @param count : uint + * the number of bytes of additional data to make available; must not be + * greater than the number of bytes already buffered but not made + * available by previous makeReadable calls + */ + makeReadable: function makeReadable(count) + { + dumpn("*** [" + this.name + "].makeReadable(" + count + ")"); + + do_check_true(Components.isSuccessCode(self._status), "errant call"); + do_check_true(this._readable + count <= self._data.length || + this._readable === Infinity, + "increasing readable beyond written amount"); + + this._readable += count; + + dumpn("readable: " + this._readable + ", data: " + self._data); + + var waiter = this._waiter; + if (waiter !== null) + { + if (waiter.requestedCount <= this._readable && !waiter.closureOnly) + this._notify(); + } + }, + + /** + * Disables the readability limit on this stream, meaning that as soon as + * *any* amount of data is written to output it becomes available from + * this stream and a stream-ready event is dispatched (if any stream-ready + * callback is currently set). + */ + disableReadabilityLimit: function disableReadabilityLimit() + { + dumpn("*** [" + this.name + "].disableReadabilityLimit()"); + + this._readable = Infinity; + }, + + // + // see nsIInputStream.available + // + available: function available() + { + dumpn("*** [" + this.name + "].available()"); + + if (self._data.length === 0 && !Components.isSuccessCode(self._status)) + throw self._status; + + return Math.min(this._readable, self._data.length); + }, + + /** + * Dispatches a pending stream-ready event ahead of schedule, rather than + * waiting for it to be dispatched in response to normal writes. This is + * useful when writing to the output has completed, and we need to have + * read all data written to this stream. If the output isn't closed and + * the reading of data from this races ahead of the last write to output, + * we need a notification to know when everything that's been written has + * been read. This ordinarily might be supplied by closing output, but + * in some cases it's not desirable to close output, so this supplies an + * alternative method to get notified when the last write has occurred. + */ + maybeNotifyFinally: function maybeNotifyFinally() + { + dumpn("*** [" + this.name + "].maybeNotifyFinally()"); + + do_check_true(this._waiter !== null, "must be waiting now"); + + if (self._data.length > 0) + { + dumpn("*** data still pending, normal notifications will signal " + + "completion"); + return; + } + + // No data waiting to be written, so notify. We could just close the + // stream, but that's less faithful to the server's behavior (it doesn't + // close the stream, and we're pretending to impersonate the server as + // much as we can here), so instead we're going to notify when no data + // can be read. The CopyTest has already been flagged as complete, so + // the stream listener will detect that this is a wrap-it-up notify and + // invoke the next test. + this._notify(); + }, + + /** + * Dispatches an event to call a previously-registered stream-ready + * callback. + */ + _notify: function _notify() + { + dumpn("*** [" + this.name + "]._notify()"); + + var waiter = this._waiter; + do_check_true(waiter !== null, "no waiter?"); + + if (this._event === null) + { + var event = this._event = + { + run: function run() + { + input._waiter = null; + input._event = null; + try + { + do_check_true(!Components.isSuccessCode(self._status) || + input._readable >= waiter.requestedCount); + waiter.callback.onInputStreamReady(input); + } + catch (e) + { + do_throw("error calling onInputStreamReady: " + e); + } + } + }; + waiter.eventTarget.dispatch(event, Ci.nsIThread.DISPATCH_NORMAL); + } + }, + + QueryInterface: function QueryInterface(iid) + { + if (iid.equals(Ci.nsIAsyncInputStream) || + iid.equals(Ci.nsIInputStream) || + iid.equals(Ci.nsISupports)) + { + return this; + } + + throw Cr.NS_ERROR_NO_INTERFACE; + } + }; + + /** The output end of this pipe. */ + var output = this.outputStream = + { + /** A name for this stream, used in debugging output. */ + name: name + " output", + + /** + * The number of bytes of data which may be written to this pipe without + * blocking. + */ + _writable: 0, + + /** + * The increments in which pending data should be written, rather than + * simply defaulting to the amount requested (which, given that + * input.asyncWait precisely respects the requestedCount argument, will + * ordinarily always be writable in that amount), as an array whose + * elements from start to finish are the number of bytes to write each + * time write() or writeByteArray() is subsequently called. The sum of + * the values in this array, if this array is not empty, is always equal + * to this._writable. + */ + _writableAmounts: [], + + /** + * Data regarding a pending stream-ready callback on this, or null if no + * callback is currently waiting to be called. + */ + _waiter: null, + + /** + * The event currently dispatched to make a stream-ready callback, if any + * such callback is currently ready to be made and not already in + * progress, or null when no callback is waiting to happen. + */ + _event: null, + + /** + * A stream-ready constructor to wrap an existing callback to intercept + * stream-ready notifications, or null if notifications shouldn't be + * wrapped at all. + */ + _streamReadyInterceptCreator: null, + + /** + * Registers a stream-ready wrapper creator function so that a + * stream-ready callback made in the future can be wrapped. + */ + interceptStreamReadyCallbacks: function(streamReadyInterceptCreator) + { + dumpn("*** [" + this.name + "].interceptStreamReadyCallbacks"); + + do_check_true(this._streamReadyInterceptCreator !== null, + "intercepting onOutputStreamReady twice"); + this._streamReadyInterceptCreator = streamReadyInterceptCreator; + if (this._waiter) + { + this._waiter.callback = + new streamReadyInterceptCreator(this._waiter.callback); + } + }, + + /** + * Removes a previously-registered stream-ready wrapper creator function, + * also clearing any current wrapping. + */ + removeStreamReadyInterceptor: function() + { + dumpn("*** [" + this.name + "].removeStreamReadyInterceptor()"); + + do_check_true(this._streamReadyInterceptCreator !== null, + "removing interceptor when none present?"); + this._streamReadyInterceptCreator = null; + if (this._waiter) + this._waiter.callback = this._waiter.callback.wrappedCallback; + }, + + // + // see nsIAsyncOutputStream.asyncWait + // + asyncWait: function asyncWait(callback, flags, requestedCount, target) + { + dumpn("*** [" + this.name + "].asyncWait"); + + do_check_true(callback && typeof callback !== "function"); + + var closureOnly = + (flags & Ci.nsIAsyncInputStream.WAIT_CLOSURE_ONLY) !== 0; + + do_check_true(this._waiter === null || + (this._waiter.closureOnly && !closureOnly), + "asyncWait already called with a non-closure-only " + + "callback? unexpected!"); + + this._waiter = + { + callback: + this._streamReadyInterceptCreator + ? new this._streamReadyInterceptCreator(callback) + : callback, + closureOnly: closureOnly, + requestedCount: requestedCount, + eventTarget: target, + toString: function toString() + { + return "waiter(" + (closureOnly ? "closure only, " : "") + + "requestedCount: " + requestedCount + ", target: " + + target + ")"; + } + }; + + if ((!closureOnly && this._writable >= requestedCount) || + !Components.isSuccessCode(this.status)) + { + this._notify(); + } + }, + + // + // see nsIAsyncOutputStream.closeWithStatus + // + closeWithStatus: function closeWithStatus(status) + { + dumpn("*** [" + this.name + "].closeWithStatus(" + status + ")"); + + if (!Components.isSuccessCode(self._status)) + { + dumpn("*** ignoring redundant closure of [input " + this.name + "] " + + "because it's already closed (status " + self._status + ")"); + return; + } + + if (Components.isSuccessCode(status)) + status = Cr.NS_BASE_STREAM_CLOSED; + + self._status = status; + + if (input._waiter) + input._notify(); + if (this._waiter) + this._notify(); + }, + + // + // see nsIBinaryOutputStream.writeByteArray + // + writeByteArray: function writeByteArray(bytes, length) + { + dumpn("*** [" + this.name + "].writeByteArray" + + "([" + bytes + "], " + length + ")"); + + do_check_eq(bytes.length, length, "sanity"); + if (!Components.isSuccessCode(self._status)) + throw self._status; + + do_check_eq(this._writableAmounts.length, 0, + "writeByteArray can't support specified-length writes"); + + if (this._writable < length) + throw Cr.NS_BASE_STREAM_WOULD_BLOCK; + + self._data.push.apply(self._data, bytes); + this._writable -= length; + + if (input._readable === Infinity && input._waiter && + !input._waiter.closureOnly) + { + input._notify(); + } + }, + + // + // see nsIOutputStream.write + // + write: function write(str, length) + { + dumpn("*** [" + this.name + "].write"); + + do_check_eq(str.length, length, "sanity"); + if (!Components.isSuccessCode(self._status)) + throw self._status; + if (this._writable === 0) + throw Cr.NS_BASE_STREAM_WOULD_BLOCK; + + var actualWritten; + if (this._writableAmounts.length === 0) + { + actualWritten = Math.min(this._writable, length); + } + else + { + do_check_true(this._writable >= this._writableAmounts[0], + "writable amounts value greater than writable data?"); + do_check_eq(this._writable, sum(this._writableAmounts), + "total writable amount not equal to sum of writable " + + "increments"); + actualWritten = this._writableAmounts.shift(); + } + + var bytes = str.substring(0, actualWritten) + .split("") + .map(function(v) { return v.charCodeAt(0); }); + + self._data.push.apply(self._data, bytes); + this._writable -= actualWritten; + + if (input._readable === Infinity && input._waiter && + !input._waiter.closureOnly) + { + input._notify(); + } + + return actualWritten; + }, + + /** + * Increase the amount of data that can be written without blocking by the + * given number of bytes, triggering future notifications when required. + * + * @param count : uint + * the number of bytes of additional data to make writable + */ + makeWritable: function makeWritable(count) + { + dumpn("*** [" + this.name + "].makeWritable(" + count + ")"); + + do_check_true(Components.isSuccessCode(self._status)); + + this._writable += count; + + var waiter = this._waiter; + if (waiter && !waiter.closureOnly && + waiter.requestedCount <= this._writable) + { + this._notify(); + } + }, + + /** + * Increase the amount of data that can be written without blocking, but + * do so by specifying a number of bytes that will be written each time + * a write occurs, even as asyncWait notifications are initially triggered + * as usual. Thus, rather than writes eagerly writing everything possible + * at each step, attempts to write out data by segment devolve into a + * partial segment write, then another, and so on until the amount of data + * specified as permitted to be written, has been written. + * + * Note that the writeByteArray method is incompatible with the previous + * calling of this method, in that, until all increments provided to this + * method have been consumed, writeByteArray cannot be called. Once all + * increments have been consumed, writeByteArray may again be called. + * + * @param increments : [uint] + * an array whose elements are positive numbers of bytes to permit to be + * written each time write() is subsequently called on this, ignoring + * the total amount of writable space specified by the sum of all + * increments + */ + makeWritableByIncrements: function makeWritableByIncrements(increments) + { + dumpn("*** [" + this.name + "].makeWritableByIncrements" + + "([" + increments.join(", ") + "])"); + + do_check_true(increments.length > 0, "bad increments"); + do_check_true(increments.every(function(v) { return v > 0; }), + "zero increment?"); + + do_check_true(Components.isSuccessCode(self._status)); + + this._writable += sum(increments); + this._writableAmounts = increments; + + var waiter = this._waiter; + if (waiter && !waiter.closureOnly && + waiter.requestedCount <= this._writable) + { + this._notify(); + } + }, + + /** + * Dispatches an event to call a previously-registered stream-ready + * callback. + */ + _notify: function _notify() + { + dumpn("*** [" + this.name + "]._notify()"); + + var waiter = this._waiter; + do_check_true(waiter !== null, "no waiter?"); + + if (this._event === null) + { + var event = this._event = + { + run: function run() + { + output._waiter = null; + output._event = null; + + try + { + waiter.callback.onOutputStreamReady(output); + } + catch (e) + { + do_throw("error calling onOutputStreamReady: " + e); + } + } + }; + waiter.eventTarget.dispatch(event, Ci.nsIThread.DISPATCH_NORMAL); + } + }, + + QueryInterface: function QueryInterface(iid) + { + if (iid.equals(Ci.nsIAsyncOutputStream) || + iid.equals(Ci.nsIOutputStream) || + iid.equals(Ci.nsISupports)) + { + return this; + } + + throw Cr.NS_ERROR_NO_INTERFACE; + } + }; +} + +/** + * Represents a sequence of interactions to perform with a copier, in a given + * order and at the desired time intervals. + * + * @param name : string + * test name, used in debugging output + */ +function CopyTest(name, next) +{ + /** Name used in debugging output. */ + this.name = name; + + /** A function called when the test completes. */ + this._done = next; + + var sourcePipe = new CustomPipe(name + "-source"); + + /** The source of data for the copier to copy. */ + this._source = sourcePipe.inputStream; + + /** + * The sink to which to write data which will appear in the copier's source. + */ + this._copyableDataStream = sourcePipe.outputStream; + + var sinkPipe = new CustomPipe(name + "-sink"); + + /** The sink to which the copier copies data. */ + this._sink = sinkPipe.outputStream; + + /** Input stream from which to read data the copier's written to its sink. */ + this._copiedDataStream = sinkPipe.inputStream; + + this._copiedDataStream.disableReadabilityLimit(); + + /** + * True if there's a callback waiting to read data written by the copier to + * its output, from the input end of the pipe representing the copier's sink. + */ + this._waitingForData = false; + + /** + * An array of the bytes of data expected to be written to output by the + * copier when this test runs. + */ + this._expectedData = undefined; + + /** Array of bytes of data received so far. */ + this._receivedData = []; + + /** The expected final status returned by the copier. */ + this._expectedStatus = -1; + + /** The actual final status returned by the copier. */ + this._actualStatus = -1; + + /** The most recent sequence of bytes written to output by the copier. */ + this._lastQuantum = []; + + /** + * True iff we've received the last quantum of data written to the sink by the + * copier. + */ + this._allDataWritten = false; + + /** + * True iff the copier has notified its associated stream listener of + * completion. + */ + this._copyingFinished = false; + + /** Index of the next task to execute while driving the copier. */ + this._currentTask = 0; + + /** Array containing all tasks to run. */ + this._tasks = []; + + /** The copier used by this test. */ + this._copier = + new WriteThroughCopier(this._source, this._sink, this, null); + + // Start watching for data written by the copier to the sink. + this._waitForWrittenData(); +} +CopyTest.prototype = +{ + /** + * Adds the given array of bytes to data in the copier's source. + * + * @param bytes : [uint] + * array of bytes of data to add to the source for the copier + */ + addToSource: function addToSource(bytes) + { + var self = this; + this._addToTasks(function addToSourceTask() + { + note("addToSourceTask"); + + try + { + self._copyableDataStream.makeWritable(bytes.length); + self._copyableDataStream.writeByteArray(bytes, bytes.length); + } + finally + { + self._stageNextTask(); + } + }); + }, + + /** + * Makes bytes of data previously added to the source available to be read by + * the copier. + * + * @param count : uint + * number of bytes to make available for reading + */ + makeSourceReadable: function makeSourceReadable(count) + { + var self = this; + this._addToTasks(function makeSourceReadableTask() + { + note("makeSourceReadableTask"); + + self._source.makeReadable(count); + self._stageNextTask(); + }); + }, + + /** + * Increases available space in the sink by the given amount, waits for the + * given series of arrays of bytes to be written to sink by the copier, and + * causes execution to asynchronously continue to the next task when the last + * of those arrays of bytes is received. + * + * @param bytes : uint + * number of bytes of space to make available in the sink + * @param dataQuantums : [[uint]] + * array of byte arrays to expect to be written in sequence to the sink + */ + makeSinkWritableAndWaitFor: + function makeSinkWritableAndWaitFor(bytes, dataQuantums) + { + var self = this; + + do_check_eq(bytes, + dataQuantums.reduce(function(partial, current) + { + return partial + current.length; + }, 0), + "bytes/quantums mismatch"); + + function increaseSinkSpaceTask() + { + /* Now do the actual work to trigger the interceptor. */ + self._sink.makeWritable(bytes); + } + + this._waitForHelper("increaseSinkSpaceTask", + dataQuantums, increaseSinkSpaceTask); + }, + + /** + * Increases available space in the sink by the given amount, waits for the + * given series of arrays of bytes to be written to sink by the copier, and + * causes execution to asynchronously continue to the next task when the last + * of those arrays of bytes is received. + * + * @param bytes : uint + * number of bytes of space to make available in the sink + * @param dataQuantums : [[uint]] + * array of byte arrays to expect to be written in sequence to the sink + */ + makeSinkWritableByIncrementsAndWaitFor: + function makeSinkWritableByIncrementsAndWaitFor(bytes, dataQuantums) + { + var self = this; + + var desiredAmounts = dataQuantums.map(function(v) { return v.length; }); + do_check_eq(bytes, sum(desiredAmounts), "bytes/quantums mismatch"); + + function increaseSinkSpaceByIncrementsTask() + { + /* Now do the actual work to trigger the interceptor incrementally. */ + self._sink.makeWritableByIncrements(desiredAmounts); + } + + this._waitForHelper("increaseSinkSpaceByIncrementsTask", + dataQuantums, increaseSinkSpaceByIncrementsTask); + }, + + /** + * Close the copier's source stream, then asynchronously continue to the next + * task. + * + * @param status : nsresult + * the status to provide when closing the copier's source stream + */ + closeSource: function closeSource(status) + { + var self = this; + + this._addToTasks(function closeSourceTask() + { + note("closeSourceTask"); + + self._source.closeWithStatus(status); + self._stageNextTask(); + }); + }, + + /** + * Close the copier's source stream, then wait for the given number of bytes + * and for the given series of arrays of bytes to be written to the sink, then + * asynchronously continue to the next task. + * + * @param status : nsresult + * the status to provide when closing the copier's source stream + * @param bytes : uint + * number of bytes of space to make available in the sink + * @param dataQuantums : [[uint]] + * array of byte arrays to expect to be written in sequence to the sink + */ + closeSourceAndWaitFor: + function closeSourceAndWaitFor(status, bytes, dataQuantums) + { + var self = this; + + do_check_eq(bytes, sum(dataQuantums.map(function(v) { return v.length; })), + "bytes/quantums mismatch"); + + function closeSourceAndWaitForTask() + { + self._sink.makeWritable(bytes); + self._copyableDataStream.closeWithStatus(status); + } + + this._waitForHelper("closeSourceAndWaitForTask", + dataQuantums, closeSourceAndWaitForTask); + }, + + /** + * Closes the copier's sink stream, providing the given status, then + * asynchronously continue to the next task. + * + * @param status : nsresult + * the status to provide when closing the copier's sink stream + */ + closeSink: function closeSink(status) + { + var self = this; + this._addToTasks(function closeSinkTask() + { + note("closeSinkTask"); + + self._sink.closeWithStatus(status); + self._stageNextTask(); + }); + }, + + /** + * Closes the copier's source stream, then immediately closes the copier's + * sink stream, then asynchronously continues to the next task. + * + * @param sourceStatus : nsresult + * the status to provide when closing the copier's source stream + * @param sinkStatus : nsresult + * the status to provide when closing the copier's sink stream + */ + closeSourceThenSink: function closeSourceThenSink(sourceStatus, sinkStatus) + { + var self = this; + this._addToTasks(function closeSourceThenSinkTask() + { + note("closeSourceThenSinkTask"); + + self._source.closeWithStatus(sourceStatus); + self._sink.closeWithStatus(sinkStatus); + self._stageNextTask(); + }); + }, + + /** + * Closes the copier's sink stream, then immediately closes the copier's + * source stream, then asynchronously continues to the next task. + * + * @param sinkStatus : nsresult + * the status to provide when closing the copier's sink stream + * @param sourceStatus : nsresult + * the status to provide when closing the copier's source stream + */ + closeSinkThenSource: function closeSinkThenSource(sinkStatus, sourceStatus) + { + var self = this; + this._addToTasks(function closeSinkThenSourceTask() + { + note("closeSinkThenSource"); + + self._sink.closeWithStatus(sinkStatus); + self._source.closeWithStatus(sourceStatus); + self._stageNextTask(); + }); + }, + + /** + * Indicates that the given status is expected to be returned when the stream + * listener for the copy indicates completion, that the expected data copied + * by the copier to sink are the concatenation of the arrays of bytes in + * receivedData, and kicks off the tasks in this test. + * + * @param expectedStatus : nsresult + * the status expected to be returned by the copier at completion + * @param receivedData : [[uint]] + * an array containing arrays of bytes whose concatenation constitutes the + * expected copied data + */ + expect: function expect(expectedStatus, receivedData) + { + this._expectedStatus = expectedStatus; + this._expectedData = []; + for (var i = 0, sz = receivedData.length; i < sz; i++) + this._expectedData.push.apply(this._expectedData, receivedData[i]); + + this._stageNextTask(); + }, + + /** + * Sets up a stream interceptor that will verify that each piece of data + * written to the sink by the copier corresponds to the currently expected + * pieces of data, calls the trigger, then waits for those pieces of data to + * be received. Once all have been received, the interceptor is removed and + * the next task is asynchronously executed. + * + * @param name : string + * name of the task created by this, used in debugging output + * @param dataQuantums : [[uint]] + * array of expected arrays of bytes to be written to the sink by the copier + * @param trigger : function() : void + * function to call after setting up the interceptor to wait for + * notifications (which will be generated as a result of this function's + * actions) + */ + _waitForHelper: function _waitForHelper(name, dataQuantums, trigger) + { + var self = this; + this._addToTasks(function waitForHelperTask() + { + note(name); + + var quantumIndex = 0; + + /* + * Intercept all data-available notifications so we can continue when all + * the ones we expect have been received. + */ + var streamReadyCallback = + { + onInputStreamReady: function wrapperOnInputStreamReady(input) + { + dumpn("*** streamReadyCallback.onInputStreamReady" + + "(" + input.name + ")"); + + do_check_eq(this, streamReadyCallback, "sanity"); + + try + { + if (quantumIndex < dataQuantums.length) + { + var quantum = dataQuantums[quantumIndex++]; + var sz = quantum.length; + do_check_eq(self._lastQuantum.length, sz, + "different quantum lengths"); + for (var i = 0; i < sz; i++) + { + do_check_eq(self._lastQuantum[i], quantum[i], + "bad data at " + i); + } + + dumpn("*** waiting to check remaining " + + (dataQuantums.length - quantumIndex) + " quantums..."); + } + } + finally + { + if (quantumIndex === dataQuantums.length) + { + dumpn("*** data checks completed! next task..."); + self._copiedDataStream.removeStreamReadyInterceptor(); + self._stageNextTask(); + } + } + } + }; + + var interceptor = + createStreamReadyInterceptor(streamReadyCallback, "onInputStreamReady"); + self._copiedDataStream.interceptStreamReadyCallbacks(interceptor); + + /* Do the deed. */ + trigger(); + }); + }, + + /** + * Initiates asynchronous waiting for data written to the copier's sink to be + * available for reading from the input end of the sink's pipe. The callback + * stores the received data for comparison in the interceptor used in the + * callback added by _waitForHelper and signals test completion when it + * receives a zero-data-available notification (if the copier has notified + * that it is finished; otherwise allows execution to continue until that has + * occurred). + */ + _waitForWrittenData: function _waitForWrittenData() + { + dumpn("*** _waitForWrittenData (" + this.name + ")"); + + var self = this; + var outputWrittenWatcher = + { + onInputStreamReady: function onInputStreamReady(input) + { + dumpn("*** outputWrittenWatcher.onInputStreamReady" + + "(" + input.name + ")"); + + if (self._allDataWritten) + { + do_throw("ruh-roh! why are we getting notified of more data " + + "after we should have received all of it?"); + } + + self._waitingForData = false; + + try + { + var avail = input.available(); + } + catch (e) + { + dumpn("*** available() threw! error: " + e); + if (self._completed) + { + dumpn("*** NB: this isn't a problem, because we've copied " + + "completely now, and this notify may have been expedited " + + "by maybeNotifyFinally such that we're being called when " + + "we can *guarantee* nothing is available any more"); + } + avail = 0; + } + + if (avail > 0) + { + var data = input.readByteArray(avail); + do_check_eq(data.length, avail, + "readByteArray returned wrong number of bytes?"); + self._lastQuantum = data; + self._receivedData.push.apply(self._receivedData, data); + } + + if (avail === 0) + { + dumpn("*** all data received!"); + + self._allDataWritten = true; + + if (self._copyingFinished) + { + dumpn("*** copying already finished, continuing to next test"); + self._testComplete(); + } + else + { + dumpn("*** copying not finished, waiting for that to happen"); + } + + return; + } + + self._waitForWrittenData(); + } + }; + + this._copiedDataStream.asyncWait(outputWrittenWatcher, 0, 1, + gThreadManager.currentThread); + this._waitingForData = true; + }, + + /** + * Indicates this test is complete, does the final data-received and copy + * status comparisons, and calls the test-completion function provided when + * this test was first created. + */ + _testComplete: function _testComplete() + { + dumpn("*** CopyTest(" + this.name + ") complete! " + + "On to the next test..."); + + try + { + do_check_true(this._allDataWritten, "expect all data written now!"); + do_check_true(this._copyingFinished, "expect copying finished now!"); + + do_check_eq(this._actualStatus, this._expectedStatus, + "wrong final status"); + + var expected = this._expectedData, received = this._receivedData; + dumpn("received: [" + received + "], expected: [" + expected + "]"); + do_check_eq(received.length, expected.length, "wrong data"); + for (var i = 0, sz = expected.length; i < sz; i++) + do_check_eq(received[i], expected[i], "bad data at " + i); + } + catch (e) + { + dumpn("!!! ERROR PERFORMING FINAL " + this.name + " CHECKS! " + e); + throw e; + } + finally + { + dumpn("*** CopyTest(" + this.name + ") complete! " + + "Invoking test-completion callback..."); + this._done(); + } + }, + + /** Dispatches an event at this thread which will run the next task. */ + _stageNextTask: function _stageNextTask() + { + dumpn("*** CopyTest(" + this.name + ")._stageNextTask()"); + + if (this._currentTask === this._tasks.length) + { + dumpn("*** CopyTest(" + this.name + ") tasks complete!"); + return; + } + + var task = this._tasks[this._currentTask++]; + var self = this; + var event = + { + run: function run() + { + try + { + task(); + } + catch (e) + { + do_throw("exception thrown running task: " + e); + } + } + }; + gThreadManager.currentThread.dispatch(event, Ci.nsIThread.DISPATCH_NORMAL); + }, + + /** + * Adds the given function as a task to be run at a later time. + * + * @param task : function() : void + * the function to call as a task + */ + _addToTasks: function _addToTasks(task) + { + this._tasks.push(task); + }, + + // + // see nsIRequestObserver.onStartRequest + // + onStartRequest: function onStartRequest(self, _) + { + dumpn("*** CopyTest.onStartRequest (" + self.name + ")"); + + do_check_true(_ === null); + do_check_eq(this._receivedData.length, 0); + do_check_eq(this._lastQuantum.length, 0); + }, + + // + // see nsIRequestObserver.onStopRequest + // + onStopRequest: function onStopRequest(self, _, status) + { + dumpn("*** CopyTest.onStopRequest (" + self.name + ", " + status + ")"); + + do_check_true(_ === null); + this._actualStatus = status; + + this._copyingFinished = true; + + if (this._allDataWritten) + { + dumpn("*** all data written, continuing with remaining tests..."); + this._testComplete(); + } + else + { + /* + * Everything's copied as far as the copier is concerned. However, there + * may be a backup transferring from the output end of the copy sink to + * the input end where we can actually verify that the expected data was + * written as expected, because that transfer occurs asynchronously. If + * we do final data-received checks now, we'll miss still-pending data. + * Therefore, to wrap up this copy test we still need to asynchronously + * wait on the input end of the sink until we hit end-of-stream or some + * error condition. Then we know we're done and can continue with the + * next test. + */ + dumpn("*** not all data copied, waiting for that to happen..."); + + if (!this._waitingForData) + this._waitForWrittenData(); + + this._copiedDataStream.maybeNotifyFinally(); + } + } +}; diff --git a/netwerk/test/httpserver/test/test_basic_functionality.js b/netwerk/test/httpserver/test/test_basic_functionality.js new file mode 100644 index 0000000000..9151bed4bc --- /dev/null +++ b/netwerk/test/httpserver/test/test_basic_functionality.js @@ -0,0 +1,176 @@ +/* -*- 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/. */ + +/* + * Basic functionality test, from the client programmer's POV. + */ + +XPCOMUtils.defineLazyGetter(this, "port", function() { + return srv.identity.primaryPort; +}); + +XPCOMUtils.defineLazyGetter(this, "tests", function() { + return [ + new Test("http://localhost:" + port + "/objHandler", + null, start_objHandler, null), + new Test("http://localhost:" + port + "/functionHandler", + null, start_functionHandler, null), + new Test("http://localhost:" + port + "/nonexistent-path", + null, start_non_existent_path, null), + new Test("http://localhost:" + port + "/lotsOfHeaders", + null, start_lots_of_headers, null), + ]; +}); + +var srv; + +function run_test() +{ + srv = createServer(); + + // base path + // XXX should actually test this works with a file by comparing streams! + var dirServ = Cc["@mozilla.org/file/directory_service;1"] + .getService(Ci.nsIProperties); + var path = dirServ.get("CurProcD", Ci.nsILocalFile); + srv.registerDirectory("/", path); + + // register a few test paths + srv.registerPathHandler("/objHandler", objHandler); + srv.registerPathHandler("/functionHandler", functionHandler); + srv.registerPathHandler("/lotsOfHeaders", lotsOfHeadersHandler); + + srv.start(-1); + + runHttpTests(tests, testComplete(srv)); +} + +const HEADER_COUNT = 1000; + +// TEST DATA + +// common properties *always* appended by server +// or invariants for every URL in paths +function commonCheck(ch) +{ + do_check_true(ch.contentLength > -1); + do_check_eq(ch.getResponseHeader("connection"), "close"); + do_check_false(ch.isNoStoreResponse()); + do_check_false(ch.isPrivateResponse()); +} + +function start_objHandler(ch, cx) +{ + commonCheck(ch); + + do_check_eq(ch.responseStatus, 200); + do_check_true(ch.requestSucceeded); + do_check_eq(ch.getResponseHeader("content-type"), "text/plain"); + do_check_eq(ch.responseStatusText, "OK"); + + var reqMin = {}, reqMaj = {}, respMin = {}, respMaj = {}; + ch.getRequestVersion(reqMaj, reqMin); + ch.getResponseVersion(respMaj, respMin); + do_check_true(reqMaj.value == respMaj.value && + reqMin.value == respMin.value); +} + +function start_functionHandler(ch, cx) +{ + commonCheck(ch); + + do_check_eq(ch.responseStatus, 404); + do_check_false(ch.requestSucceeded); + do_check_eq(ch.getResponseHeader("foopy"), "quux-baz"); + do_check_eq(ch.responseStatusText, "Page Not Found"); + + var reqMin = {}, reqMaj = {}, respMin = {}, respMaj = {}; + ch.getRequestVersion(reqMaj, reqMin); + ch.getResponseVersion(respMaj, respMin); + do_check_true(reqMaj.value == 1 && reqMin.value == 1); + do_check_true(respMaj.value == 1 && respMin.value == 1); +} + +function start_non_existent_path(ch, cx) +{ + commonCheck(ch); + + do_check_eq(ch.responseStatus, 404); + do_check_false(ch.requestSucceeded); +} + +function start_lots_of_headers(ch, cx) +{ + commonCheck(ch); + + do_check_eq(ch.responseStatus, 200); + do_check_true(ch.requestSucceeded); + + for (var i = 0; i < HEADER_COUNT; i++) + do_check_eq(ch.getResponseHeader("X-Header-" + i), "value " + i); +} + +// PATH HANDLERS + +// /objHandler +var objHandler = + { + handle: function(metadata, response) + { + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/plain", false); + + var body = "Request (slightly reformatted):\n\n"; + body += metadata.method + " " + metadata.path; + + do_check_eq(metadata.port, port); + + if (metadata.queryString) + body += "?" + metadata.queryString; + + body += " HTTP/" + metadata.httpVersion + "\n"; + + var headEnum = metadata.headers; + while (headEnum.hasMoreElements()) + { + var fieldName = headEnum.getNext() + .QueryInterface(Ci.nsISupportsString) + .data; + body += fieldName + ": " + metadata.getHeader(fieldName) + "\n"; + } + + response.bodyOutputStream.write(body, body.length); + }, + QueryInterface: function(id) + { + if (id.equals(Ci.nsISupports) || id.equals(Ci.nsIHttpRequestHandler)) + return this; + throw Cr.NS_ERROR_NOINTERFACE; + } + }; + +// /functionHandler +function functionHandler(metadata, response) +{ + response.setStatusLine("1.1", 404, "Page Not Found"); + response.setHeader("foopy", "quux-baz", false); + + do_check_eq(metadata.port, port); + do_check_eq(metadata.host, "localhost"); + do_check_eq(metadata.path.charAt(0), "/"); + + var body = "this is text\n"; + response.bodyOutputStream.write(body, body.length); +} + +// /lotsOfHeaders +function lotsOfHeadersHandler(request, response) +{ + response.setHeader("Content-Type", "text/plain", false); + + for (var i = 0; i < HEADER_COUNT; i++) + response.setHeader("X-Header-" + i, "value " + i, false); +} diff --git a/netwerk/test/httpserver/test/test_body_length.js b/netwerk/test/httpserver/test/test_body_length.js new file mode 100644 index 0000000000..0fd2236d73 --- /dev/null +++ b/netwerk/test/httpserver/test/test_body_length.js @@ -0,0 +1,64 @@ +/* -*- 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/. */ + +/* + * Tests that the Content-Length header in incoming requests is interpreted as + * a decimal number, even if it has the form (including leading zero) of an + * octal number. + */ + +var srv; + +function run_test() +{ + srv = createServer(); + srv.registerPathHandler("/content-length", contentLength); + srv.start(-1); + + runHttpTests(tests, testComplete(srv)); +} + +const REQUEST_DATA = "12345678901234567"; + +function contentLength(request, response) +{ + do_check_eq(request.method, "POST"); + do_check_eq(request.getHeader("Content-Length"), "017"); + + var body = new ScriptableInputStream(request.bodyInputStream); + + var avail; + var data = ""; + while ((avail = body.available()) > 0) + data += body.read(avail); + + do_check_eq(data, REQUEST_DATA); +} + +/*************** + * BEGIN TESTS * + ***************/ + +XPCOMUtils.defineLazyGetter(this, 'tests', function() { + return [ + new Test("http://localhost:" + srv.identity.primaryPort + "/content-length", + init_content_length), + ]; +}); + +function init_content_length(ch) +{ + var content = Cc["@mozilla.org/io/string-input-stream;1"] + .createInstance(Ci.nsIStringInputStream); + content.data = REQUEST_DATA; + + ch.QueryInterface(Ci.nsIUploadChannel) + .setUploadStream(content, "text/plain", REQUEST_DATA.length); + + // Override the values implicitly set by setUploadStream above. + ch.requestMethod = "POST"; + ch.setRequestHeader("Content-Length", "017", false); // 17 bytes, not 15 +} diff --git a/netwerk/test/httpserver/test/test_byte_range.js b/netwerk/test/httpserver/test/test_byte_range.js new file mode 100644 index 0000000000..53d23e5226 --- /dev/null +++ b/netwerk/test/httpserver/test/test_byte_range.js @@ -0,0 +1,278 @@ +/* -*- 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/. */ + +// checks if a byte range request and non-byte range request retrieve the +// correct data. + +var srv; +XPCOMUtils.defineLazyGetter(this, "PREFIX", function() { + return "http://localhost:" + srv.identity.primaryPort; +}); + +XPCOMUtils.defineLazyGetter(this, "tests", function() { + return [ + new Test(PREFIX + "/range.txt", + init_byterange, start_byterange, stop_byterange), + new Test(PREFIX + "/range.txt", + init_byterange2, start_byterange2), + new Test(PREFIX + "/range.txt", + init_byterange3, start_byterange3, stop_byterange3), + new Test(PREFIX + "/range.txt", + init_byterange4, start_byterange4), + new Test(PREFIX + "/range.txt", + init_byterange5, start_byterange5, stop_byterange5), + new Test(PREFIX + "/range.txt", + init_byterange6, start_byterange6, stop_byterange6), + new Test(PREFIX + "/range.txt", + init_byterange7, start_byterange7, stop_byterange7), + new Test(PREFIX + "/range.txt", + init_byterange8, start_byterange8, stop_byterange8), + new Test(PREFIX + "/range.txt", + init_byterange9, start_byterange9, stop_byterange9), + new Test(PREFIX + "/range.txt", + init_byterange10, start_byterange10), + new Test(PREFIX + "/range.txt", + init_byterange11, start_byterange11, stop_byterange11), + new Test(PREFIX + "/empty.txt", + null, start_byterange12, stop_byterange12), + new Test(PREFIX + "/headers.txt", + init_byterange13, start_byterange13, null), + new Test(PREFIX + "/range.txt", + null, start_normal, stop_normal) + ]; +}); + +function run_test() +{ + srv = createServer(); + var dir = do_get_file("data/ranges/"); + srv.registerDirectory("/", dir); + + srv.start(-1); + + runHttpTests(tests, testComplete(srv)); +} + +function start_normal(ch, cx) +{ + do_check_eq(ch.responseStatus, 200); + do_check_eq(ch.getResponseHeader("Content-Length"), "21"); + do_check_eq(ch.getResponseHeader("Content-Type"), "text/plain"); +} + +function stop_normal(ch, cx, status, data) +{ + do_check_eq(data.length, 21); + do_check_eq(data[0], 0x54); + do_check_eq(data[20], 0x0a); +} + +function init_byterange(ch) +{ + ch.setRequestHeader("Range", "bytes=10-", false); +} + +function start_byterange(ch, cx) +{ + do_check_eq(ch.responseStatus, 206); + do_check_eq(ch.getResponseHeader("Content-Length"), "11"); + do_check_eq(ch.getResponseHeader("Content-Type"), "text/plain"); + do_check_eq(ch.getResponseHeader("Content-Range"), "bytes 10-20/21"); +} + +function stop_byterange(ch, cx, status, data) +{ + do_check_eq(data.length, 11); + do_check_eq(data[0], 0x64); + do_check_eq(data[10], 0x0a); +} + +function init_byterange2(ch) +{ + ch.setRequestHeader("Range", "bytes=21-", false); +} + +function start_byterange2(ch, cx) +{ + do_check_eq(ch.responseStatus, 416); +} + +function init_byterange3(ch) +{ + ch.setRequestHeader("Range", "bytes=10-15", false); +} + +function start_byterange3(ch, cx) +{ + do_check_eq(ch.responseStatus, 206); + do_check_eq(ch.getResponseHeader("Content-Length"), "6"); + do_check_eq(ch.getResponseHeader("Content-Type"), "text/plain"); + do_check_eq(ch.getResponseHeader("Content-Range"), "bytes 10-15/21"); +} + +function stop_byterange3(ch, cx, status, data) +{ + do_check_eq(data.length, 6); + do_check_eq(data[0], 0x64); + do_check_eq(data[1], 0x20); + do_check_eq(data[2], 0x62); + do_check_eq(data[3], 0x65); + do_check_eq(data[4], 0x20); + do_check_eq(data[5], 0x73); +} + +function init_byterange4(ch) +{ + ch.setRequestHeader("Range", "xbytes=21-", false); +} + +function start_byterange4(ch, cx) +{ + do_check_eq(ch.responseStatus, 400); +} + +function init_byterange5(ch) +{ + ch.setRequestHeader("Range", "bytes=-5", false); +} + +function start_byterange5(ch, cx) +{ + do_check_eq(ch.responseStatus, 206); +} + +function stop_byterange5(ch, cx, status, data) +{ + do_check_eq(data.length, 5); + do_check_eq(data[0], 0x65); + do_check_eq(data[1], 0x65); + do_check_eq(data[2], 0x6e); + do_check_eq(data[3], 0x2e); + do_check_eq(data[4], 0x0a); +} + +function init_byterange6(ch) +{ + ch.setRequestHeader("Range", "bytes=15-12", false); +} + +function start_byterange6(ch, cx) +{ + do_check_eq(ch.responseStatus, 200); +} + +function stop_byterange6(ch, cx, status, data) +{ + do_check_eq(data.length, 21); + do_check_eq(data[0], 0x54); + do_check_eq(data[20], 0x0a); +} + +function init_byterange7(ch) +{ + ch.setRequestHeader("Range", "bytes=0-5", false); +} + +function start_byterange7(ch, cx) +{ + do_check_eq(ch.responseStatus, 206); + do_check_eq(ch.getResponseHeader("Content-Length"), "6"); + do_check_eq(ch.getResponseHeader("Content-Type"), "text/plain"); + do_check_eq(ch.getResponseHeader("Content-Range"), "bytes 0-5/21"); +} + +function stop_byterange7(ch, cx, status, data) +{ + do_check_eq(data.length, 6); + do_check_eq(data[0], 0x54); + do_check_eq(data[1], 0x68); + do_check_eq(data[2], 0x69); + do_check_eq(data[3], 0x73); + do_check_eq(data[4], 0x20); + do_check_eq(data[5], 0x73); +} + +function init_byterange8(ch) +{ + ch.setRequestHeader("Range", "bytes=20-21", false); +} + +function start_byterange8(ch, cx) +{ + do_check_eq(ch.responseStatus, 206); + do_check_eq(ch.getResponseHeader("Content-Range"), "bytes 20-20/21"); +} + +function stop_byterange8(ch, cx, status, data) +{ + do_check_eq(data.length, 1); + do_check_eq(data[0], 0x0a); +} + +function init_byterange9(ch) +{ + ch.setRequestHeader("Range", "bytes=020-021", false); +} + +function start_byterange9(ch, cx) +{ + do_check_eq(ch.responseStatus, 206); +} + +function stop_byterange9(ch, cx, status, data) +{ + do_check_eq(data.length, 1); + do_check_eq(data[0], 0x0a); +} + +function init_byterange10(ch) +{ + ch.setRequestHeader("Range", "bytes=-", false); +} + +function start_byterange10(ch, cx) +{ + do_check_eq(ch.responseStatus, 400); +} + +function init_byterange11(ch) +{ + ch.setRequestHeader("Range", "bytes=-500", false); +} + +function start_byterange11(ch, cx) +{ + do_check_eq(ch.responseStatus, 206); +} + +function stop_byterange11(ch, cx, status, data) +{ + do_check_eq(data.length, 21); + do_check_eq(data[0], 0x54); + do_check_eq(data[20], 0x0a); +} + +function start_byterange12(ch, cx) +{ + do_check_eq(ch.responseStatus, 200); + do_check_eq(ch.getResponseHeader("Content-Length"), "0"); +} + +function stop_byterange12(ch, cx, status, data) +{ + do_check_eq(data.length, 0); +} + +function init_byterange13(ch) +{ + ch.setRequestHeader("Range", "bytes=9999999-", false); +} + +function start_byterange13(ch, cx) +{ + do_check_eq(ch.responseStatus, 416); + do_check_eq(ch.getResponseHeader("X-SJS-Header"), "customized"); +} diff --git a/netwerk/test/httpserver/test/test_cern_meta.js b/netwerk/test/httpserver/test/test_cern_meta.js new file mode 100644 index 0000000000..54062bc3ea --- /dev/null +++ b/netwerk/test/httpserver/test/test_cern_meta.js @@ -0,0 +1,76 @@ +/* -*- 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/. */ + +// exercises support for mod_cern_meta-style header/status line modification +var srv; + +XPCOMUtils.defineLazyGetter(this, 'PREFIX', function() { + return "http://localhost:" + srv.identity.primaryPort; +}); + +XPCOMUtils.defineLazyGetter(this, 'tests', function() { + return [ + new Test(PREFIX + "/test_both.html", + null, start_testBoth, null), + new Test(PREFIX + "/test_ctype_override.txt", + null, start_test_ctype_override_txt, null), + new Test(PREFIX + "/test_status_override.html", + null, start_test_status_override_html, null), + new Test(PREFIX + "/test_status_override_nodesc.txt", + null, start_test_status_override_nodesc_txt, null), + new Test(PREFIX + "/caret_test.txt^", + null, start_caret_test_txt_, null) + ]; +}); + +function run_test() +{ + srv = createServer(); + + var cernDir = do_get_file("data/cern_meta/"); + srv.registerDirectory("/", cernDir); + + srv.start(-1); + + runHttpTests(tests, testComplete(srv)); +} + + +// TEST DATA + +function start_testBoth(ch, cx) +{ + do_check_eq(ch.responseStatus, 501); + do_check_eq(ch.responseStatusText, "Unimplemented"); + + do_check_eq(ch.getResponseHeader("Content-Type"), "text/plain"); +} + +function start_test_ctype_override_txt(ch, cx) +{ + do_check_eq(ch.getResponseHeader("Content-Type"), "text/html"); +} + +function start_test_status_override_html(ch, cx) +{ + do_check_eq(ch.responseStatus, 404); + do_check_eq(ch.responseStatusText, "Can't Find This"); +} + +function start_test_status_override_nodesc_txt(ch, cx) +{ + do_check_eq(ch.responseStatus, 732); + do_check_eq(ch.responseStatusText, ""); +} + +function start_caret_test_txt_(ch, cx) +{ + do_check_eq(ch.responseStatus, 500); + do_check_eq(ch.responseStatusText, "This Isn't A Server Error"); + + do_check_eq(ch.getResponseHeader("Foo-RFC"), "3092"); + do_check_eq(ch.getResponseHeader("Shaving-Cream-Atom"), "Illudium Phosdex"); +} diff --git a/netwerk/test/httpserver/test/test_default_index_handler.js b/netwerk/test/httpserver/test/test_default_index_handler.js new file mode 100644 index 0000000000..c2c1b4e73e --- /dev/null +++ b/netwerk/test/httpserver/test/test_default_index_handler.js @@ -0,0 +1,290 @@ +/* -*- 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/. */ + +// checks for correct output with the default index handler, mostly to do +// escaping checks -- highly dependent on the default index handler output +// format + +var srv, dir, dirEntries; + +XPCOMUtils.defineLazyGetter(this, 'BASE_URL', function() { + return "http://localhost:" + srv.identity.primaryPort + "/"; +}); + +function run_test() +{ + createTestDirectory(); + + srv = createServer(); + srv.registerDirectory("/", dir); + + var nameDir = do_get_file("data/name-scheme/"); + srv.registerDirectory("/bar/", nameDir); + + srv.start(-1); + + function done() + { + do_test_pending(); + destroyTestDirectory(); + srv.stop(function() { do_test_finished(); }); + } + + runHttpTests(tests, done); +} + +function createTestDirectory() +{ + dir = Cc["@mozilla.org/file/directory_service;1"] + .getService(Ci.nsIProperties) + .get("TmpD", Ci.nsIFile); + dir.append("index_handler_test_" + Math.random()); + dir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, 0o744); + + // populate with test directories, files, etc. + // Files must be in expected order of display on the index page! + + var files = []; + + makeFile("aa_directory", true, dir, files); + makeFile("Ba_directory", true, dir, files); + makeFile("bb_directory", true, dir, files); + makeFile("foo", true, dir, files); + makeFile("a_file", false, dir, files); + makeFile("B_file", false, dir, files); + makeFile("za'z", false, dir, files); + makeFile("zb&z", false, dir, files); + makeFile("zc<q", false, dir, files); + makeFile('zd"q', false, dir, files); + makeFile("ze%g", false, dir, files); + makeFile("zf%200h", false, dir, files); + makeFile("zg>m", false, dir, files); + + dirEntries = [files]; + + var subdir = dir.clone(); + subdir.append("foo"); + + files = []; + + makeFile("aa_dir", true, subdir, files); + makeFile("b_dir", true, subdir, files); + makeFile("AA_file.txt", false, subdir, files); + makeFile("test.txt", false, subdir, files); + + dirEntries.push(files); +} + +function destroyTestDirectory() +{ + dir.remove(true); +} + + +/************* + * UTILITIES * + *************/ + +/** Verifies data in bytes for the trailing-caret path above. */ +function hiddenDataCheck(bytes, uri, path) +{ + var data = String.fromCharCode.apply(null, bytes); + + var parser = Cc["@mozilla.org/xmlextras/domparser;1"] + .createInstance(Ci.nsIDOMParser); + + // Note: the index format isn't XML -- it's actually HTML -- but we require + // the index format also be valid XML, albeit XML without namespaces, + // XML declarations, etc. Doing this simplifies output checking. + try + { + var doc = parser.parseFromString(data, "application/xml"); + } + catch (e) + { + do_throw("document failed to parse as XML"); + } + + // See all the .QueryInterface()s and .item()s happening here? That's because + // xpcshell sucks and doesn't have classinfo, so no automatic interface + // flattening or array-style access to items in NodeLists. Suck. + + var body = doc.documentElement.getElementsByTagName("body"); + do_check_eq(body.length, 1); + body = body.item(0); + + // header + var header = body.QueryInterface(Ci.nsIDOMElement) + .getElementsByTagName("h1"); + do_check_eq(header.length, 1); + + do_check_eq(header.item(0).QueryInterface(Ci.nsIDOMNode).textContent, path); + + // files + var lst = body.getElementsByTagName("ol"); + do_check_eq(lst.length, 1); + var items = lst.item(0).QueryInterface(Ci.nsIDOMElement) + .getElementsByTagName("li"); + + var ios = Cc["@mozilla.org/network/io-service;1"] + .getService(Ci.nsIIOService); + + var top = ios.newURI(uri, null, null); + + // N.B. No ERROR_IF_SEE_THIS.txt^ file! + var dirEntries = [{name: "file.txt", isDirectory: false}, + {name: "SHOULD_SEE_THIS.txt^", isDirectory: false}]; + + for (var i = 0; i < items.length; i++) + { + var link = items.item(i) + .childNodes + .item(0) + .QueryInterface(Ci.nsIDOMNode) + .QueryInterface(Ci.nsIDOMElement); + var f = dirEntries[i]; + + var sep = f.isDirectory ? "/" : ""; + + do_check_eq(link.textContent, f.name + sep); + + uri = ios.newURI(link.getAttribute("href"), null, top); + do_check_eq(decodeURIComponent(uri.path), path + f.name + sep); + } +} + +/** + * Verifies data in bytes (an array of bytes) represents an index page for the + * given URI and path, which should be a page listing the given directory + * entries, in order. + * + * @param bytes + * array of bytes representing the index page's contents + * @param uri + * string which is the URI of the index page + * @param path + * the path portion of uri + * @param dirEntries + * sorted (in the manner the directory entries should be sorted) array of + * objects, each of which has a name property (whose value is the file's name, + * without / if it's a directory) and an isDirectory property (with expected + * value) + */ +function dataCheck(bytes, uri, path, dirEntries) +{ + var data = String.fromCharCode.apply(null, bytes); + + var parser = Cc["@mozilla.org/xmlextras/domparser;1"] + .createInstance(Ci.nsIDOMParser); + + // Note: the index format isn't XML -- it's actually HTML -- but we require + // the index format also be valid XML, albeit XML without namespaces, + // XML declarations, etc. Doing this simplifies output checking. + try + { + var doc = parser.parseFromString(data, "application/xml"); + } + catch (e) + { + do_throw("document failed to parse as XML"); + } + + // See all the .QueryInterface()s and .item()s happening here? That's because + // xpcshell sucks and doesn't have classinfo, so no automatic interface + // flattening or array-style access to items in NodeLists. Suck. + + var body = doc.documentElement.getElementsByTagName("body"); + do_check_eq(body.length, 1); + body = body.item(0); + + // header + var header = body.QueryInterface(Ci.nsIDOMElement) + .getElementsByTagName("h1"); + do_check_eq(header.length, 1); + + do_check_eq(header.item(0).QueryInterface(Ci.nsIDOMNode).textContent, path); + + // files + var lst = body.getElementsByTagName("ol"); + do_check_eq(lst.length, 1); + var items = lst.item(0).QueryInterface(Ci.nsIDOMElement) + .getElementsByTagName("li"); + + var ios = Cc["@mozilla.org/network/io-service;1"] + .getService(Ci.nsIIOService); + + var dirURI = ios.newURI(uri, null, null); + + for (var i = 0; i < items.length; i++) + { + var link = items.item(i) + .childNodes + .item(0) + .QueryInterface(Ci.nsIDOMNode) + .QueryInterface(Ci.nsIDOMElement); + var f = dirEntries[i]; + + var sep = f.isDirectory ? "/" : ""; + + do_check_eq(link.textContent, f.name + sep); + + uri = ios.newURI(link.getAttribute("href"), null, top); + do_check_eq(decodeURIComponent(uri.path), path + f.name + sep); + } +} + +/** + * Create a file/directory with the given name underneath parentDir, and + * append an object with name/isDirectory properties to lst corresponding + * to it if the file/directory could be created. + */ +function makeFile(name, isDirectory, parentDir, lst) +{ + var type = Ci.nsIFile[isDirectory ? "DIRECTORY_TYPE" : "NORMAL_FILE_TYPE"]; + var file = parentDir.clone(); + + try + { + file.append(name); + file.create(type, 0o755); + lst.push({name: name, isDirectory: isDirectory}); + } + catch (e) { /* OS probably doesn't like file name, skip */ } +} + +/********* + * TESTS * + *********/ + +XPCOMUtils.defineLazyGetter(this, "tests", function() { + return [ + new Test(BASE_URL, null, start, stopRootDirectory), + new Test(BASE_URL + "foo/", null, start, stopFooDirectory), + new Test(BASE_URL + "bar/folder^/", null, start, stopTrailingCaretDirectory), + ]; +}); + +// check top-level directory listing +function start(ch) +{ + do_check_eq(ch.getResponseHeader("Content-Type"), "text/html;charset=utf-8"); +} +function stopRootDirectory(ch, cx, status, data) +{ + dataCheck(data, BASE_URL, "/", dirEntries[0]); +} + +// check non-top-level, too +function stopFooDirectory(ch, cx, status, data) +{ + dataCheck(data, BASE_URL + "foo/", "/foo/", dirEntries[1]); +} + +// trailing-caret leaf with hidden files +function stopTrailingCaretDirectory(ch, cx, status, data) +{ + hiddenDataCheck(data, BASE_URL + "bar/folder^/", "/bar/folder^/"); +} diff --git a/netwerk/test/httpserver/test/test_empty_body.js b/netwerk/test/httpserver/test/test_empty_body.js new file mode 100644 index 0000000000..85cc3d3453 --- /dev/null +++ b/netwerk/test/httpserver/test/test_empty_body.js @@ -0,0 +1,55 @@ +/* -*- 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/. */ + +// in its original incarnation, the server didn't like empty response-bodies; +// see the comment in _end for details + +var srv; + +XPCOMUtils.defineLazyGetter(this, "tests", function() { + return [ + new Test("http://localhost:" + srv.identity.primaryPort + "/empty-body-unwritten", + null, ensureEmpty, null), + new Test("http://localhost:" + srv.identity.primaryPort + "/empty-body-written", + null, ensureEmpty, null), + ]; +}); + +function run_test() +{ + srv = createServer(); + + // register a few test paths + srv.registerPathHandler("/empty-body-unwritten", emptyBodyUnwritten); + srv.registerPathHandler("/empty-body-written", emptyBodyWritten); + + srv.start(-1); + + runHttpTests(tests, testComplete(srv)); +} + +// TEST DATA + +function ensureEmpty(ch, cx) +{ + do_check_true(ch.contentLength == 0); +} + +// PATH HANDLERS + +// /empty-body-unwritten +function emptyBodyUnwritten(metadata, response) +{ + response.setStatusLine("1.1", 200, "OK"); +} + +// /empty-body-written +function emptyBodyWritten(metadata, response) +{ + response.setStatusLine("1.1", 200, "OK"); + var body = ""; + response.bodyOutputStream.write(body, body.length); +} diff --git a/netwerk/test/httpserver/test/test_errorhandler_exception.js b/netwerk/test/httpserver/test/test_errorhandler_exception.js new file mode 100644 index 0000000000..c70dd1f114 --- /dev/null +++ b/netwerk/test/httpserver/test/test_errorhandler_exception.js @@ -0,0 +1,84 @@ +/* -*- 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/. */ + +// Request handlers may throw exceptions, and those exception should be caught +// by the server and converted into the proper error codes. + +XPCOMUtils.defineLazyGetter(this, "tests", function() { + return [ + new Test("http://localhost:" + srv.identity.primaryPort + "/throws/exception", + null, start_throws_exception, succeeded), + new Test("http://localhost:" + srv.identity.primaryPort + + "/this/file/does/not/exist/and/404s", + null, start_nonexistent_404_fails_so_400, succeeded), + new Test("http://localhost:" + srv.identity.primaryPort + + "/attempts/404/fails/so/400/fails/so/500s", + register400Handler, start_multiple_exceptions_500, succeeded), + ]; +}); + +var srv; + +function run_test() +{ + srv = createServer(); + + srv.registerErrorHandler(404, throwsException); + srv.registerPathHandler("/throws/exception", throwsException); + + srv.start(-1); + + runHttpTests(tests, testComplete(srv)); +} + + +// TEST DATA + +function checkStatusLine(channel, httpMaxVer, httpMinVer, httpCode, statusText) +{ + do_check_eq(channel.responseStatus, httpCode); + do_check_eq(channel.responseStatusText, statusText); + + var respMaj = {}, respMin = {}; + channel.getResponseVersion(respMaj, respMin); + do_check_eq(respMaj.value, httpMaxVer); + do_check_eq(respMin.value, httpMinVer); +} + +function start_throws_exception(ch, cx) +{ + checkStatusLine(ch, 1, 1, 500, "Internal Server Error"); +} + +function start_nonexistent_404_fails_so_400(ch, cx) +{ + checkStatusLine(ch, 1, 1, 400, "Bad Request"); +} + +function start_multiple_exceptions_500(ch, cx) +{ + checkStatusLine(ch, 1, 1, 500, "Internal Server Error"); +} + +function succeeded(ch, cx, status, data) +{ + do_check_true(Components.isSuccessCode(status)); +} + +function register400Handler(ch) +{ + srv.registerErrorHandler(400, throwsException); +} + + +// PATH HANDLERS + +// /throws/exception (and also a 404 and 400 error handler) +function throwsException(metadata, response) +{ + throw "this shouldn't cause an exit..."; + do_throw("Not reached!"); +} diff --git a/netwerk/test/httpserver/test/test_header_array.js b/netwerk/test/httpserver/test/test_header_array.js new file mode 100644 index 0000000000..2933f9aa68 --- /dev/null +++ b/netwerk/test/httpserver/test/test_header_array.js @@ -0,0 +1,67 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// test that special headers are sent as an array of headers with the same name + +var srv; + +function run_test() +{ + srv; + + srv = createServer(); + srv.registerPathHandler("/path-handler", pathHandler); + srv.start(-1); + + runHttpTests(tests, testComplete(srv)); +} + + +/************ + * HANDLERS * + ************/ + +function pathHandler(request, response) +{ + response.setHeader("Cache-Control", "no-cache", false); + + response.setHeader("Proxy-Authenticate", "First line 1", true); + response.setHeader("Proxy-Authenticate", "Second line 1", true); + response.setHeader("Proxy-Authenticate", "Third line 1", true); + + response.setHeader("WWW-Authenticate", "Not merged line 1", false); + response.setHeader("WWW-Authenticate", "Not merged line 2", true); + + response.setHeader("WWW-Authenticate", "First line 2", false); + response.setHeader("WWW-Authenticate", "Second line 2", true); + response.setHeader("WWW-Authenticate", "Third line 2", true); + + response.setHeader("X-Single-Header-Merge", "Single 1", true); + response.setHeader("X-Single-Header-Merge", "Single 2", true); +} + +/*************** + * BEGIN TESTS * + ***************/ + +XPCOMUtils.defineLazyGetter(this, "tests", function() { + return [ + new Test("http://localhost:" + srv.identity.primaryPort + "/path-handler", + null, check) + ]; +}); + +function check(ch, cx) +{ + var headerValue; + + headerValue = ch.getResponseHeader("Proxy-Authenticate"); + do_check_eq(headerValue, "First line 1\nSecond line 1\nThird line 1"); + headerValue = ch.getResponseHeader("WWW-Authenticate"); + do_check_eq(headerValue, "First line 2\nSecond line 2\nThird line 2"); + headerValue = ch.getResponseHeader("X-Single-Header-Merge"); + do_check_eq(headerValue, "Single 1,Single 2"); +} diff --git a/netwerk/test/httpserver/test/test_headers.js b/netwerk/test/httpserver/test/test_headers.js new file mode 100644 index 0000000000..74314a9668 --- /dev/null +++ b/netwerk/test/httpserver/test/test_headers.js @@ -0,0 +1,189 @@ +/* -*- 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/. */ + +// tests for header storage in httpd.js; nsHttpHeaders is an *internal* data +// structure and is not to be used directly outside of httpd.js itself except +// for testing purposes + + +/** + * Ensures that a fieldname-fieldvalue combination is a valid header. + * + * @param fieldName + * the name of the header + * @param fieldValue + * the value of the header + * @param headers + * an nsHttpHeaders object to use to check validity + */ +function assertValidHeader(fieldName, fieldValue, headers) +{ + try + { + headers.setHeader(fieldName, fieldValue, false); + } + catch (e) + { + do_throw("Unexpected exception thrown: " + e); + } +} + +/** + * Ensures that a fieldname-fieldvalue combination is not a valid header. + * + * @param fieldName + * the name of the header + * @param fieldValue + * the value of the header + * @param headers + * an nsHttpHeaders object to use to check validity + */ +function assertInvalidHeader(fieldName, fieldValue, headers) +{ + try + { + headers.setHeader(fieldName, fieldValue, false); + throw "Setting (" + fieldName + ", " + + fieldValue + ") as header succeeded!"; + } + catch (e) + { + if (e !== Cr.NS_ERROR_INVALID_ARG) + do_throw("Unexpected exception thrown: " + e); + } +} + + +function run_test() +{ + testHeaderValidity(); + testGetHeader(); + testHeaderEnumerator(); + testHasHeader(); +} + +function testHeaderValidity() +{ + var headers = new nsHttpHeaders(); + + assertInvalidHeader("f o", "bar", headers); + assertInvalidHeader("f\0n", "bar", headers); + assertInvalidHeader("foo:", "bar", headers); + assertInvalidHeader("f\\o", "bar", headers); + assertInvalidHeader("@xml", "bar", headers); + assertInvalidHeader("fiz(", "bar", headers); + assertInvalidHeader("HTTP/1.1", "bar", headers); + assertInvalidHeader("b\"b", "bar", headers); + assertInvalidHeader("ascsd\t", "bar", headers); + assertInvalidHeader("{fds", "bar", headers); + assertInvalidHeader("baz?", "bar", headers); + assertInvalidHeader("a\\b\\c", "bar", headers); + assertInvalidHeader("\0x7F", "bar", headers); + assertInvalidHeader("\0x1F", "bar", headers); + assertInvalidHeader("f\n", "bar", headers); + assertInvalidHeader("foo", "b\nar", headers); + assertInvalidHeader("foo", "b\rar", headers); + assertInvalidHeader("foo", "b\0", headers); + + // request splitting, fwiw -- we're actually immune to this type of attack so + // long as we don't implement persistent connections + assertInvalidHeader("f\r\nGET /badness HTTP/1.1\r\nFoo", "bar", headers); + + assertValidHeader("f'", "baz", headers); + assertValidHeader("f`", "baz", headers); + assertValidHeader("f.", "baz", headers); + assertValidHeader("f---", "baz", headers); + assertValidHeader("---", "baz", headers); + assertValidHeader("~~~", "baz", headers); + assertValidHeader("~~~", "b\r\n bar", headers); + assertValidHeader("~~~", "b\r\n\tbar", headers); +} + +function testGetHeader() +{ + var headers = new nsHttpHeaders(); + + headers.setHeader("Content-Type", "text/html", false); + var c = headers.getHeader("content-type"); + do_check_eq(c, "text/html"); + + headers.setHeader("test", "FOO", false); + var c = headers.getHeader("test"); + do_check_eq(c, "FOO"); + + try + { + headers.getHeader(":"); + throw "Failed to throw for invalid header"; + } + catch (e) + { + if (e !== Cr.NS_ERROR_INVALID_ARG) + do_throw("headers.getHeader(':') must throw invalid arg"); + } + + try + { + headers.getHeader("valid"); + throw 'header doesn\'t exist'; + } + catch (e) + { + if (e !== Cr.NS_ERROR_NOT_AVAILABLE) + do_throw("shouldn't be a header named 'valid' in headers!"); + } +} + +function testHeaderEnumerator() +{ + var headers = new nsHttpHeaders(); + + var heads = + { + "foo": "17", + "baz": "two six niner", + "decaf": "class Program { int .7; int main(){ .7 = 5; return 7 - .7; } }" + }; + + for (var i in heads) + headers.setHeader(i, heads[i], false); + + var en = headers.enumerator; + while (en.hasMoreElements()) + { + var it = en.getNext().QueryInterface(Ci.nsISupportsString).data; + do_check_true(it.toLowerCase() in heads); + delete heads[it.toLowerCase()]; + } + + for (var i in heads) + do_throw("still have properties in heads!?!?"); + +} + +function testHasHeader() +{ + var headers = new nsHttpHeaders(); + + headers.setHeader("foo", "bar", false); + do_check_true(headers.hasHeader("foo")); + do_check_true(headers.hasHeader("fOo")); + do_check_false(headers.hasHeader("not-there")); + + headers.setHeader("f`'~", "bar", false); + do_check_true(headers.hasHeader("F`'~")); + + try + { + headers.hasHeader(":"); + throw "failed to throw"; + } + catch (e) + { + if (e !== Cr.NS_ERROR_INVALID_ARG) + do_throw(".hasHeader for an invalid name should throw"); + } +} diff --git a/netwerk/test/httpserver/test/test_host.js b/netwerk/test/httpserver/test/test_host.js new file mode 100644 index 0000000000..503a04fef0 --- /dev/null +++ b/netwerk/test/httpserver/test/test_host.js @@ -0,0 +1,666 @@ +/* -*- 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/. */ + +/** + * Tests that the scheme, host, and port of the server are correctly recorded + * and used in HTTP requests and responses. + */ + +const PORT = 4444; +const FAKE_PORT_ONE = 8888; +const FAKE_PORT_TWO = 8889; + +var srv, id; + +function run_test() +{ + dumpn("*** run_test"); + + srv = createServer(); + + srv.registerPathHandler("/http/1.0-request", http10Request); + srv.registerPathHandler("/http/1.1-good-host", http11goodHost); + srv.registerPathHandler("/http/1.1-good-host-wacky-port", + http11goodHostWackyPort); + srv.registerPathHandler("/http/1.1-ip-host", http11ipHost); + + srv.start(FAKE_PORT_ONE); + + id = srv.identity; + + // The default location is http://localhost:PORT, where PORT is whatever you + // provided when you started the server. http://127.0.0.1:PORT is also part + // of the default set of locations. + do_check_eq(id.primaryScheme, "http"); + do_check_eq(id.primaryHost, "localhost"); + do_check_eq(id.primaryPort, FAKE_PORT_ONE); + do_check_true(id.has("http", "localhost", FAKE_PORT_ONE)); + do_check_true(id.has("http", "127.0.0.1", FAKE_PORT_ONE)); + + // This should be a nop. + id.add("http", "localhost", FAKE_PORT_ONE); + do_check_eq(id.primaryScheme, "http"); + do_check_eq(id.primaryHost, "localhost"); + do_check_eq(id.primaryPort, FAKE_PORT_ONE); + do_check_true(id.has("http", "localhost", FAKE_PORT_ONE)); + do_check_true(id.has("http", "127.0.0.1", FAKE_PORT_ONE)); + + // Change the primary location and make sure all the getters work correctly. + id.setPrimary("http", "127.0.0.1", FAKE_PORT_ONE); + do_check_eq(id.primaryScheme, "http"); + do_check_eq(id.primaryHost, "127.0.0.1"); + do_check_eq(id.primaryPort, FAKE_PORT_ONE); + do_check_true(id.has("http", "localhost", FAKE_PORT_ONE)); + do_check_true(id.has("http", "127.0.0.1", FAKE_PORT_ONE)); + + // Okay, now remove the primary location -- we fall back to the original + // location. + id.remove("http", "127.0.0.1", FAKE_PORT_ONE); + do_check_eq(id.primaryScheme, "http"); + do_check_eq(id.primaryHost, "localhost"); + do_check_eq(id.primaryPort, FAKE_PORT_ONE); + do_check_true(id.has("http", "localhost", FAKE_PORT_ONE)); + do_check_false(id.has("http", "127.0.0.1", FAKE_PORT_ONE)); + + // You can't remove every location -- try this and the original default + // location will be silently readded. + id.remove("http", "localhost", FAKE_PORT_ONE); + do_check_eq(id.primaryScheme, "http"); + do_check_eq(id.primaryHost, "localhost"); + do_check_eq(id.primaryPort, FAKE_PORT_ONE); + do_check_true(id.has("http", "localhost", FAKE_PORT_ONE)); + do_check_false(id.has("http", "127.0.0.1", FAKE_PORT_ONE)); + + // Okay, now that we've exercised that behavior, shut down the server and + // restart it on the correct port, to exercise port-changing behaviors at + // server start and stop. + do_test_pending(); + srv.stop(function() + { + try + { + do_test_pending(); + run_test_2(); + } + finally + { + do_test_finished(); + } + }); +} + +function run_test_2() +{ + dumpn("*** run_test_2"); + + do_test_finished(); + + // Our primary location is gone because it was dependent on the port on which + // the server was running. + checkPrimariesThrow(id); + do_check_false(id.has("http", "127.0.0.1", FAKE_PORT_ONE)); + do_check_false(id.has("http", "localhost", FAKE_PORT_ONE)); + + srv.start(FAKE_PORT_TWO); + + // We should have picked up http://localhost:8889 as our primary location now + // that we've restarted. + do_check_eq(id.primaryScheme, "http"); + do_check_eq(id.primaryHost, "localhost", FAKE_PORT_TWO); + do_check_eq(id.primaryPort, FAKE_PORT_TWO); + do_check_false(id.has("http", "localhost", FAKE_PORT_ONE)); + do_check_false(id.has("http", "127.0.0.1", FAKE_PORT_ONE)); + do_check_true(id.has("http", "localhost", FAKE_PORT_TWO)); + do_check_true(id.has("http", "127.0.0.1", FAKE_PORT_TWO)); + + // Now we're going to see what happens when we shut down with a primary + // location that wasn't a default. That location should persist, and the + // default we remove should still not be present. + id.setPrimary("http", "example.com", FAKE_PORT_TWO); + do_check_true(id.has("http", "example.com", FAKE_PORT_TWO)); + do_check_true(id.has("http", "127.0.0.1", FAKE_PORT_TWO)); + do_check_true(id.has("http", "localhost", FAKE_PORT_TWO)); + do_check_false(id.has("http", "127.0.0.1", FAKE_PORT_ONE)); + do_check_false(id.has("http", "localhost", FAKE_PORT_ONE)); + + id.remove("http", "localhost", FAKE_PORT_TWO); + do_check_true(id.has("http", "example.com", FAKE_PORT_TWO)); + do_check_false(id.has("http", "localhost", FAKE_PORT_TWO)); + do_check_true(id.has("http", "127.0.0.1", FAKE_PORT_TWO)); + do_check_false(id.has("http", "localhost", FAKE_PORT_ONE)); + do_check_false(id.has("http", "127.0.0.1", FAKE_PORT_ONE)); + + id.remove("http", "127.0.0.1", FAKE_PORT_TWO); + do_check_true(id.has("http", "example.com", FAKE_PORT_TWO)); + do_check_false(id.has("http", "localhost", FAKE_PORT_TWO)); + do_check_false(id.has("http", "127.0.0.1", FAKE_PORT_TWO)); + do_check_false(id.has("http", "localhost", FAKE_PORT_ONE)); + do_check_false(id.has("http", "127.0.0.1", FAKE_PORT_ONE)); + + do_test_pending(); + srv.stop(function() + { + try + { + do_test_pending(); + run_test_3(); + } + finally + { + do_test_finished(); + } + }); +} + +function run_test_3() +{ + dumpn("*** run_test_3"); + + do_test_finished(); + + // Only the default added location disappears; any others stay around, + // possibly as the primary location. We may have removed the default primary + // location, but the one we set manually should persist here. + do_check_eq(id.primaryScheme, "http"); + do_check_eq(id.primaryHost, "example.com"); + do_check_eq(id.primaryPort, FAKE_PORT_TWO); + do_check_true(id.has("http", "example.com", FAKE_PORT_TWO)); + do_check_false(id.has("http", "localhost", FAKE_PORT_TWO)); + do_check_false(id.has("http", "127.0.0.1", FAKE_PORT_TWO)); + do_check_false(id.has("http", "localhost", FAKE_PORT_ONE)); + do_check_false(id.has("http", "127.0.0.1", FAKE_PORT_ONE)); + + srv.start(PORT); + + // Starting always adds HTTP entries for 127.0.0.1:port and localhost:port. + do_check_true(id.has("http", "example.com", FAKE_PORT_TWO)); + do_check_false(id.has("http", "localhost", FAKE_PORT_TWO)); + do_check_false(id.has("http", "127.0.0.1", FAKE_PORT_TWO)); + do_check_false(id.has("http", "localhost", FAKE_PORT_ONE)); + do_check_false(id.has("http", "127.0.0.1", FAKE_PORT_ONE)); + do_check_true(id.has("http", "localhost", PORT)); + do_check_true(id.has("http", "127.0.0.1", PORT)); + + // Remove the primary location we'd left set from last time. + id.remove("http", "example.com", FAKE_PORT_TWO); + + // Default-port behavior testing requires the server responds to requests + // claiming to be on one such port. + id.add("http", "localhost", 80); + + // Make sure we don't have anything lying around from running on either the + // first or the second port -- all we should have is our generated default, + // plus the additional port to test "portless" hostport variants. + do_check_true(id.has("http", "localhost", 80)); + do_check_eq(id.primaryScheme, "http"); + do_check_eq(id.primaryHost, "localhost"); + do_check_eq(id.primaryPort, PORT); + do_check_true(id.has("http", "localhost", PORT)); + do_check_true(id.has("http", "127.0.0.1", PORT)); + do_check_false(id.has("http", "localhost", FAKE_PORT_ONE)); + do_check_false(id.has("http", "127.0.0.1", FAKE_PORT_ONE)); + do_check_false(id.has("http", "example.com", FAKE_PORT_TWO)); + do_check_false(id.has("http", "localhost", FAKE_PORT_TWO)); + do_check_false(id.has("http", "127.0.0.1", FAKE_PORT_TWO)); + + // Okay, finally done with identity testing. Our primary location is the one + // we want it to be, so we're off! + runRawTests(tests, testComplete(srv)); +} + + +/********************* + * UTILITY FUNCTIONS * + *********************/ + +/** + * Verifies that all .primary* getters on a server identity correctly throw + * NS_ERROR_NOT_INITIALIZED. + * + * @param id : nsIHttpServerIdentity + * the server identity to test + */ +function checkPrimariesThrow(id) +{ + var threw = false; + try + { + id.primaryScheme; + } + catch (e) + { + threw = e === Cr.NS_ERROR_NOT_INITIALIZED; + } + do_check_true(threw); + + threw = false; + try + { + id.primaryHost; + } + catch (e) + { + threw = e === Cr.NS_ERROR_NOT_INITIALIZED; + } + do_check_true(threw); + + threw = false; + try + { + id.primaryPort; + } + catch (e) + { + threw = e === Cr.NS_ERROR_NOT_INITIALIZED; + } + do_check_true(threw); +} + +/** + * Utility function to check for a 400 response. + */ +function check400(data) +{ + var iter = LineIterator(data); + + // Status-Line + var firstLine = iter.next(); + do_check_eq(firstLine.substring(0, HTTP_400_LEADER_LENGTH), HTTP_400_LEADER); +} + + +/*************** + * BEGIN TESTS * + ***************/ + +const HTTP_400_LEADER = "HTTP/1.1 400 "; +const HTTP_400_LEADER_LENGTH = HTTP_400_LEADER.length; + +var test, data; +var tests = []; + +// HTTP/1.0 request, to ensure we see our default scheme/host/port + +function http10Request(request, response) +{ + writeDetails(request, response); + response.setStatusLine("1.0", 200, "TEST PASSED"); +} +data = "GET /http/1.0-request HTTP/1.0\r\n" + + "\r\n"; +function check10(data) +{ + var iter = LineIterator(data); + + // Status-Line + do_check_eq(iter.next(), "HTTP/1.0 200 TEST PASSED"); + + skipHeaders(iter); + + // Okay, next line must be the data we expected to be written + var body = + [ + "Method: GET", + "Path: /http/1.0-request", + "Query: ", + "Version: 1.0", + "Scheme: http", + "Host: localhost", + "Port: 4444", + ]; + + expectLines(iter, body); +} +test = new RawTest("localhost", PORT, data, check10), +tests.push(test); + + +// HTTP/1.1 request, no Host header, expect a 400 response + +data = "GET /http/1.1-request HTTP/1.1\r\n" + + "\r\n"; +test = new RawTest("localhost", PORT, data, check400), +tests.push(test); + + +// HTTP/1.1 request, wrong host, expect a 400 response + +data = "GET /http/1.1-request HTTP/1.1\r\n" + + "Host: not-localhost\r\n" + + "\r\n"; +test = new RawTest("localhost", PORT, data, check400), +tests.push(test); + + +// HTTP/1.1 request, wrong host/right port, expect a 400 response + +data = "GET /http/1.1-request HTTP/1.1\r\n" + + "Host: not-localhost:4444\r\n" + + "\r\n"; +test = new RawTest("localhost", PORT, data, check400), +tests.push(test); + + +// HTTP/1.1 request, Host header has host but no port, expect a 400 response + +data = "GET /http/1.1-request HTTP/1.1\r\n" + + "Host: 127.0.0.1\r\n" + + "\r\n"; +test = new RawTest("localhost", PORT, data, check400), +tests.push(test); + + +// HTTP/1.1 request, Request-URI has wrong port, expect a 400 response + +data = "GET http://127.0.0.1/http/1.1-request HTTP/1.1\r\n" + + "Host: 127.0.0.1\r\n" + + "\r\n"; +test = new RawTest("localhost", PORT, data, check400), +tests.push(test); + + +// HTTP/1.1 request, Request-URI has wrong port, expect a 400 response + +data = "GET http://localhost:31337/http/1.1-request HTTP/1.1\r\n" + + "Host: localhost:31337\r\n" + + "\r\n"; +test = new RawTest("localhost", PORT, data, check400), +tests.push(test); + + +// HTTP/1.1 request, Request-URI has wrong scheme, expect a 400 response + +data = "GET https://localhost:4444/http/1.1-request HTTP/1.1\r\n" + + "Host: localhost:4444\r\n" + + "\r\n"; +test = new RawTest("localhost", PORT, data, check400), +tests.push(test); + + +// HTTP/1.1 request, correct Host header, expect handler's response + +function http11goodHost(request, response) +{ + writeDetails(request, response); + response.setStatusLine("1.1", 200, "TEST PASSED"); +} +data = "GET /http/1.1-good-host HTTP/1.1\r\n" + + "Host: localhost:4444\r\n" + + "\r\n"; +function check11goodHost(data) +{ + var iter = LineIterator(data); + + // Status-Line + do_check_eq(iter.next(), "HTTP/1.1 200 TEST PASSED"); + + skipHeaders(iter); + + // Okay, next line must be the data we expected to be written + var body = + [ + "Method: GET", + "Path: /http/1.1-good-host", + "Query: ", + "Version: 1.1", + "Scheme: http", + "Host: localhost", + "Port: 4444", + ]; + + expectLines(iter, body); +} +test = new RawTest("localhost", PORT, data, check11goodHost), +tests.push(test); + + +// HTTP/1.1 request, Host header is secondary identity + +function http11ipHost(request, response) +{ + writeDetails(request, response); + response.setStatusLine("1.1", 200, "TEST PASSED"); +} +data = "GET /http/1.1-ip-host HTTP/1.1\r\n" + + "Host: 127.0.0.1:4444\r\n" + + "\r\n"; +function check11ipHost(data) +{ + var iter = LineIterator(data); + + // Status-Line + do_check_eq(iter.next(), "HTTP/1.1 200 TEST PASSED"); + + skipHeaders(iter); + + // Okay, next line must be the data we expected to be written + var body = + [ + "Method: GET", + "Path: /http/1.1-ip-host", + "Query: ", + "Version: 1.1", + "Scheme: http", + "Host: 127.0.0.1", + "Port: 4444", + ]; + + expectLines(iter, body); +} +test = new RawTest("localhost", PORT, data, check11ipHost), +tests.push(test); + + +// HTTP/1.1 request, absolute path, accurate Host header + +// reusing previous request handler so not defining a new one + +data = "GET http://localhost:4444/http/1.1-good-host HTTP/1.1\r\n" + + "Host: localhost:4444\r\n" + + "\r\n"; +test = new RawTest("localhost", PORT, data, check11goodHost), +tests.push(test); + + +// HTTP/1.1 request, absolute path, inaccurate Host header + +// reusing previous request handler so not defining a new one + +data = "GET http://localhost:4444/http/1.1-good-host HTTP/1.1\r\n" + + "Host: localhost:1234\r\n" + + "\r\n"; +test = new RawTest("localhost", PORT, data, check11goodHost), +tests.push(test); + + +// HTTP/1.1 request, absolute path, different inaccurate Host header + +// reusing previous request handler so not defining a new one + +data = "GET http://localhost:4444/http/1.1-good-host HTTP/1.1\r\n" + + "Host: not-localhost:4444\r\n" + + "\r\n"; +test = new RawTest("localhost", PORT, data, check11goodHost), +tests.push(test); + + +// HTTP/1.1 request, absolute path, yet another inaccurate Host header + +// reusing previous request handler so not defining a new one + +data = "GET http://localhost:4444/http/1.1-good-host HTTP/1.1\r\n" + + "Host: yippity-skippity\r\n" + + "\r\n"; +function checkInaccurate(data) +{ + check11goodHost(data); + + // dynamism setup + srv.identity.setPrimary("http", "127.0.0.1", 4444); +} +test = new RawTest("localhost", PORT, data, checkInaccurate), +tests.push(test); + + +// HTTP/1.0 request, absolute path, different inaccurate Host header + +// reusing previous request handler so not defining a new one + +data = "GET /http/1.0-request HTTP/1.0\r\n" + + "Host: not-localhost:4444\r\n" + + "\r\n"; +function check10ip(data) +{ + var iter = LineIterator(data); + + // Status-Line + do_check_eq(iter.next(), "HTTP/1.0 200 TEST PASSED"); + + skipHeaders(iter); + + // Okay, next line must be the data we expected to be written + var body = + [ + "Method: GET", + "Path: /http/1.0-request", + "Query: ", + "Version: 1.0", + "Scheme: http", + "Host: 127.0.0.1", + "Port: 4444", + ]; + + expectLines(iter, body); +} +test = new RawTest("localhost", PORT, data, check10ip), +tests.push(test); + + +// HTTP/1.1 request, Host header with implied port + +function http11goodHostWackyPort(request, response) +{ + writeDetails(request, response); + response.setStatusLine("1.1", 200, "TEST PASSED"); +} +data = "GET /http/1.1-good-host-wacky-port HTTP/1.1\r\n" + + "Host: localhost\r\n" + + "\r\n"; +function check11goodHostWackyPort(data) +{ + var iter = LineIterator(data); + + // Status-Line + do_check_eq(iter.next(), "HTTP/1.1 200 TEST PASSED"); + + skipHeaders(iter); + + // Okay, next line must be the data we expected to be written + var body = + [ + "Method: GET", + "Path: /http/1.1-good-host-wacky-port", + "Query: ", + "Version: 1.1", + "Scheme: http", + "Host: localhost", + "Port: 80", + ]; + + expectLines(iter, body); +} +test = new RawTest("localhost", PORT, data, check11goodHostWackyPort), +tests.push(test); + + +// HTTP/1.1 request, Host header with wacky implied port + +data = "GET /http/1.1-good-host-wacky-port HTTP/1.1\r\n" + + "Host: localhost:\r\n" + + "\r\n"; +test = new RawTest("localhost", PORT, data, check11goodHostWackyPort), +tests.push(test); + + +// HTTP/1.1 request, absolute URI with implied port + +data = "GET http://localhost/http/1.1-good-host-wacky-port HTTP/1.1\r\n" + + "Host: localhost\r\n" + + "\r\n"; +test = new RawTest("localhost", PORT, data, check11goodHostWackyPort), +tests.push(test); + + +// HTTP/1.1 request, absolute URI with wacky implied port + +data = "GET http://localhost:/http/1.1-good-host-wacky-port HTTP/1.1\r\n" + + "Host: localhost\r\n" + + "\r\n"; +test = new RawTest("localhost", PORT, data, check11goodHostWackyPort), +tests.push(test); + + +// HTTP/1.1 request, absolute URI with explicit implied port, ignored Host + +data = "GET http://localhost:80/http/1.1-good-host-wacky-port HTTP/1.1\r\n" + + "Host: who-cares\r\n" + + "\r\n"; +test = new RawTest("localhost", PORT, data, check11goodHostWackyPort), +tests.push(test); + + +// HTTP/1.1 request, a malformed Request-URI + +data = "GET is-this-the-real-life-is-this-just-fantasy HTTP/1.1\r\n" + + "Host: localhost:4444\r\n" + + "\r\n"; +test = new RawTest("localhost", PORT, data, check400), +tests.push(test); + + +// HTTP/1.1 request, a malformed Host header + +data = "GET /http/1.1-request HTTP/1.1\r\n" + + "Host: la la la\r\n" + + "\r\n"; +test = new RawTest("localhost", PORT, data, check400), +tests.push(test); + + +// HTTP/1.1 request, a malformed Host header but absolute URI, 5.2 sez fine + +data = "GET http://localhost:4444/http/1.1-good-host HTTP/1.1\r\n" + + "Host: la la la\r\n" + + "\r\n"; +test = new RawTest("localhost", PORT, data, check11goodHost), +tests.push(test); + + +// HTTP/1.0 request, absolute URI, but those aren't valid in HTTP/1.0 + +data = "GET http://localhost:4444/http/1.1-request HTTP/1.0\r\n" + + "Host: localhost:4444\r\n" + + "\r\n"; +test = new RawTest("localhost", PORT, data, check400), +tests.push(test); + + +// HTTP/1.1 request, absolute URI with unrecognized host + +data = "GET http://not-localhost:4444/http/1.1-request HTTP/1.1\r\n" + + "Host: not-localhost:4444\r\n" + + "\r\n"; +test = new RawTest("localhost", PORT, data, check400), +tests.push(test); + + +// HTTP/1.1 request, absolute URI with unrecognized host (but not in Host) + +data = "GET http://not-localhost:4444/http/1.1-request HTTP/1.1\r\n" + + "Host: localhost:4444\r\n" + + "\r\n"; +test = new RawTest("localhost", PORT, data, check400), +tests.push(test); diff --git a/netwerk/test/httpserver/test/test_linedata.js b/netwerk/test/httpserver/test/test_linedata.js new file mode 100644 index 0000000000..49f4f82587 --- /dev/null +++ b/netwerk/test/httpserver/test/test_linedata.js @@ -0,0 +1,20 @@ +/* -*- 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 that the LineData internal data structure works correctly + +function run_test() +{ + var data = new LineData(); + data.appendBytes(["a".charCodeAt(0), CR]); + + var out = { value: "" }; + do_check_false(data.readLine(out)); + + data.appendBytes([LF]); + do_check_true(data.readLine(out)); + do_check_eq(out.value, "a"); +} diff --git a/netwerk/test/httpserver/test/test_load_module.js b/netwerk/test/httpserver/test/test_load_module.js new file mode 100644 index 0000000000..8233833fa1 --- /dev/null +++ b/netwerk/test/httpserver/test/test_load_module.js @@ -0,0 +1,16 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Ensure httpd.js can be imported as a module and that a server starts. + */ +function run_test() { + Components.utils.import("resource://testing-common/httpd.js"); + + let server = new HttpServer(); + server.start(-1); + + do_test_pending(); + + server.stop(do_test_finished); +} diff --git a/netwerk/test/httpserver/test/test_name_scheme.js b/netwerk/test/httpserver/test/test_name_scheme.js new file mode 100644 index 0000000000..154f73d25d --- /dev/null +++ b/netwerk/test/httpserver/test/test_name_scheme.js @@ -0,0 +1,90 @@ +/* -*- 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/. */ + +// requests for files ending with a caret (^) are handled specially to enable +// htaccess-like functionality without the need to explicitly disable display +// of such files + +var srv; + +XPCOMUtils.defineLazyGetter(this, "PREFIX", function() { + return "http://localhost:" + srv.identity.primaryPort; +}); + +XPCOMUtils.defineLazyGetter(this, "tests", function() { + return [ + new Test(PREFIX + "/bar.html^", + null, start_bar_html_, null), + new Test(PREFIX + "/foo.html^", + null, start_foo_html_, null), + new Test(PREFIX + "/normal-file.txt", + null, start_normal_file_txt, null), + new Test(PREFIX + "/folder^/file.txt", + null, start_folder__file_txt, null), + + new Test(PREFIX + "/foo/bar.html^", + null, start_bar_html_, null), + new Test(PREFIX + "/foo/foo.html^", + null, start_foo_html_, null), + new Test(PREFIX + "/foo/normal-file.txt", + null, start_normal_file_txt, null), + new Test(PREFIX + "/foo/folder^/file.txt", + null, start_folder__file_txt, null), + + new Test(PREFIX + "/end-caret^/bar.html^", + null, start_bar_html_, null), + new Test(PREFIX + "/end-caret^/foo.html^", + null, start_foo_html_, null), + new Test(PREFIX + "/end-caret^/normal-file.txt", + null, start_normal_file_txt, null), + new Test(PREFIX + "/end-caret^/folder^/file.txt", + null, start_folder__file_txt, null) + ]; +}); + + +function run_test() +{ + srv = createServer(); + + // make sure underscores work in directories "mounted" in directories with + // folders starting with _ + var nameDir = do_get_file("data/name-scheme/"); + srv.registerDirectory("/", nameDir); + srv.registerDirectory("/foo/", nameDir); + srv.registerDirectory("/end-caret^/", nameDir); + + srv.start(-1); + + runHttpTests(tests, testComplete(srv)); +} + + +// TEST DATA + +function start_bar_html_(ch, cx) +{ + do_check_eq(ch.responseStatus, 200); + + do_check_eq(ch.getResponseHeader("Content-Type"), "text/html"); +} + +function start_foo_html_(ch, cx) +{ + do_check_eq(ch.responseStatus, 404); +} + +function start_normal_file_txt(ch, cx) +{ + do_check_eq(ch.responseStatus, 200); + do_check_eq(ch.getResponseHeader("Content-Type"), "text/plain"); +} + +function start_folder__file_txt(ch, cx) +{ + do_check_eq(ch.responseStatus, 200); + do_check_eq(ch.getResponseHeader("Content-Type"), "text/plain"); +} diff --git a/netwerk/test/httpserver/test/test_processasync.js b/netwerk/test/httpserver/test/test_processasync.js new file mode 100644 index 0000000000..21ded660d7 --- /dev/null +++ b/netwerk/test/httpserver/test/test_processasync.js @@ -0,0 +1,304 @@ +/* -*- 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/. */ + +/* + * Tests for correct behavior of asynchronous responses. + */ + +XPCOMUtils.defineLazyGetter(this, "PREPATH", function() { + return "http://localhost:" + srv.identity.primaryPort; +}); + +var srv; + +function run_test() +{ + srv = createServer(); + for (var path in handlers) + srv.registerPathHandler(path, handlers[path]); + srv.start(-1); + + runHttpTests(tests, testComplete(srv)); +} + + +/*************** + * BEGIN TESTS * + ***************/ + +XPCOMUtils.defineLazyGetter(this, "tests", function() { + return [ + new Test(PREPATH + "/handleSync", null, start_handleSync, null), + new Test(PREPATH + "/handleAsync1", null, start_handleAsync1, + stop_handleAsync1), + new Test(PREPATH + "/handleAsync2", init_handleAsync2, start_handleAsync2, + stop_handleAsync2), + new Test(PREPATH + "/handleAsyncOrdering", null, null, + stop_handleAsyncOrdering) + ]; +}); + +var handlers = {}; + +function handleSync(request, response) +{ + response.setStatusLine(request.httpVersion, 500, "handleSync fail"); + + try + { + response.finish(); + do_throw("finish called on sync response"); + } + catch (e) + { + isException(e, Cr.NS_ERROR_UNEXPECTED); + } + + response.setStatusLine(request.httpVersion, 200, "handleSync pass"); +} +handlers["/handleSync"] = handleSync; + +function start_handleSync(ch, cx) +{ + do_check_eq(ch.responseStatus, 200); + do_check_eq(ch.responseStatusText, "handleSync pass"); +} + +function handleAsync1(request, response) +{ + response.setStatusLine(request.httpVersion, 500, "Old status line!"); + response.setHeader("X-Foo", "old value", false); + + response.processAsync(); + + response.setStatusLine(request.httpVersion, 200, "New status line!"); + response.setHeader("X-Foo", "new value", false); + + response.finish(); + + try + { + response.setStatusLine(request.httpVersion, 500, "Too late!"); + do_throw("late setStatusLine didn't throw"); + } + catch (e) + { + isException(e, Cr.NS_ERROR_NOT_AVAILABLE); + } + + try + { + response.setHeader("X-Foo", "late value", false); + do_throw("late setHeader didn't throw"); + } + catch (e) + { + isException(e, Cr.NS_ERROR_NOT_AVAILABLE); + } + + try + { + response.bodyOutputStream; + do_throw("late bodyOutputStream get didn't throw"); + } + catch (e) + { + isException(e, Cr.NS_ERROR_NOT_AVAILABLE); + } + + try + { + response.write("fugly"); + do_throw("late write() didn't throw"); + } + catch (e) + { + isException(e, Cr.NS_ERROR_NOT_AVAILABLE); + } +} +handlers["/handleAsync1"] = handleAsync1; + +function start_handleAsync1(ch, cx) +{ + do_check_eq(ch.responseStatus, 200); + do_check_eq(ch.responseStatusText, "New status line!"); + do_check_eq(ch.getResponseHeader("X-Foo"), "new value"); +} + +function stop_handleAsync1(ch, cx, status, data) +{ + do_check_eq(data.length, 0); +} + +const startToHeaderDelay = 500; +const startToFinishedDelay = 750; + +function handleAsync2(request, response) +{ + response.processAsync(); + + response.setStatusLine(request.httpVersion, 200, "Status line"); + response.setHeader("X-Custom-Header", "value", false); + + callLater(startToHeaderDelay, function() + { + var body = "BO"; + response.bodyOutputStream.write(body, body.length); + + try + { + response.setStatusLine(request.httpVersion, 500, "after body write"); + do_throw("setStatusLine succeeded"); + } + catch (e) + { + isException(e, Cr.NS_ERROR_NOT_AVAILABLE); + } + + try + { + response.setHeader("X-Custom-Header", "new 1", false); + } + catch (e) + { + isException(e, Cr.NS_ERROR_NOT_AVAILABLE); + } + + callLater(startToFinishedDelay - startToHeaderDelay, function() + { + var body = "DY"; + response.bodyOutputStream.write(body, body.length); + + response.finish(); + response.finish(); // idempotency + + try + { + response.setStatusLine(request.httpVersion, 500, "after finish"); + } + catch (e) + { + isException(e, Cr.NS_ERROR_NOT_AVAILABLE); + } + + try + { + response.setHeader("X-Custom-Header", "new 2", false); + } + catch (e) + { + isException(e, Cr.NS_ERROR_NOT_AVAILABLE); + } + + try + { + response.write("EVIL"); + } + catch (e) + { + isException(e, Cr.NS_ERROR_NOT_AVAILABLE); + } + }); + }); +} +handlers["/handleAsync2"] = handleAsync2; + +var startTime_handleAsync2; + +function init_handleAsync2(ch) +{ + var now = startTime_handleAsync2 = Date.now(); + dumpn("*** init_HandleAsync2: start time " + now); +} + +function start_handleAsync2(ch, cx) +{ + var now = Date.now(); + dumpn("*** start_handleAsync2: onStartRequest time " + now + ", " + + (now - startTime_handleAsync2) + "ms after start time"); + do_check_true(now >= startTime_handleAsync2 + startToHeaderDelay); + + do_check_eq(ch.responseStatus, 200); + do_check_eq(ch.responseStatusText, "Status line"); + do_check_eq(ch.getResponseHeader("X-Custom-Header"), "value"); +} + +function stop_handleAsync2(ch, cx, status, data) +{ + var now = Date.now(); + dumpn("*** stop_handleAsync2: onStopRequest time " + now + ", " + + (now - startTime_handleAsync2) + "ms after header time"); + do_check_true(now >= startTime_handleAsync2 + startToFinishedDelay); + + do_check_eq(String.fromCharCode.apply(null, data), "BODY"); +} + +/* + * Tests that accessing output stream *before* calling processAsync() works + * correctly, sending written data immediately as it is written, not buffering + * until finish() is called -- which for this much data would mean we would all + * but certainly deadlock, since we're trying to read/write all this data in one + * process on a single thread. + */ +function handleAsyncOrdering(request, response) +{ + var out = new BinaryOutputStream(response.bodyOutputStream); + + var data = []; + for (var i = 0; i < 65536; i++) + data[i] = 0; + var count = 20; + + var writeData = + { + run: function() + { + if (count-- === 0) + { + response.finish(); + return; + } + + try + { + out.writeByteArray(data, data.length); + step(); + } + catch (e) + { + try + { + do_throw("error writing data: " + e); + } + finally + { + response.finish(); + } + } + } + }; + function step() + { + // Use gThreadManager here because it's expedient, *not* because it's + // intended for public use! If you do this in client code, expect me to + // knowingly break your code by changing the variable name. :-P + gThreadManager.currentThread + .dispatch(writeData, Ci.nsIThread.DISPATCH_NORMAL); + } + step(); + response.processAsync(); +} +handlers["/handleAsyncOrdering"] = handleAsyncOrdering; + +function stop_handleAsyncOrdering(ch, cx, status, data) +{ + do_check_eq(data.length, 20 * 65536); + data.forEach(function(v, index) + { + if (v !== 0) + do_throw("value " + v + " at index " + index + " should be zero"); + }); +} diff --git a/netwerk/test/httpserver/test/test_qi.js b/netwerk/test/httpserver/test/test_qi.js new file mode 100644 index 0000000000..aeac79d891 --- /dev/null +++ b/netwerk/test/httpserver/test/test_qi.js @@ -0,0 +1,110 @@ +/* -*- 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/. */ + +/* + * Verify the presence of explicit QueryInterface methods on XPCOM objects + * exposed by httpd.js, rather than allowing QueryInterface to be implicitly + * created by XPConnect. + */ + +XPCOMUtils.defineLazyGetter(this, "tests", function() { + return [ + new Test("http://localhost:" + srv.identity.primaryPort + "/test", + null, start_test, null), + new Test("http://localhost:" + srv.identity.primaryPort + "/sjs/qi.sjs", + null, start_sjs_qi, null), + ]; +}); + +var srv; + +function run_test() +{ + srv = createServer(); + + var qi; + try + { + qi = srv.identity.QueryInterface(Ci.nsIHttpServerIdentity); + } + catch (e) + { + var exstr = ("" + e).split(/[\x09\x20-\x7f\x81-\xff]+/)[0]; + do_throw("server identity didn't QI: " + exstr); + return; + } + + srv.registerPathHandler("/test", testHandler); + srv.registerDirectory("/", do_get_file("data/")); + srv.registerContentType("sjs", "sjs"); + srv.start(-1); + + runHttpTests(tests, testComplete(srv)); +} + + +// TEST DATA + +function start_test(ch, cx) +{ + do_check_eq(ch.responseStatusText, "QI Tests Passed"); + do_check_eq(ch.responseStatus, 200); +} + +function start_sjs_qi(ch, cx) +{ + do_check_eq(ch.responseStatusText, "SJS QI Tests Passed"); + do_check_eq(ch.responseStatus, 200); +} + + +function testHandler(request, response) +{ + var exstr; + var qid; + + response.setStatusLine(request.httpVersion, 500, "FAIL"); + + var passed = false; + try + { + qid = request.QueryInterface(Ci.nsIHttpRequest); + passed = qid === request; + } + catch (e) + { + exstr = ("" + e).split(/[\x09\x20-\x7f\x81-\xff]+/)[0]; + response.setStatusLine(request.httpVersion, 500, + "request doesn't QI: " + exstr); + return; + } + if (!passed) + { + response.setStatusLine(request.httpVersion, 500, "request QI'd wrongly?"); + return; + } + + passed = false; + try + { + qid = response.QueryInterface(Ci.nsIHttpResponse); + passed = qid === response; + } + catch (e) + { + exstr = ("" + e).split(/[\x09\x20-\x7f\x81-\xff]+/)[0]; + response.setStatusLine(request.httpVersion, 500, + "response doesn't QI: " + exstr); + return; + } + if (!passed) + { + response.setStatusLine(request.httpVersion, 500, "response QI'd wrongly?"); + return; + } + + response.setStatusLine(request.httpVersion, 200, "QI Tests Passed"); +} diff --git a/netwerk/test/httpserver/test/test_registerdirectory.js b/netwerk/test/httpserver/test/test_registerdirectory.js new file mode 100644 index 0000000000..fbb41293e9 --- /dev/null +++ b/netwerk/test/httpserver/test/test_registerdirectory.js @@ -0,0 +1,263 @@ +/* -*- 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/. */ + +// tests the registerDirectory API + +XPCOMUtils.defineLazyGetter(this, "BASE", function() { + return "http://localhost:" + srv.identity.primaryPort; +}); + + +function nocache(ch) +{ + ch.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE; // important! +} + +function notFound(ch) +{ + do_check_eq(ch.responseStatus, 404); + do_check_false(ch.requestSucceeded); +} + +function checkOverride(ch) +{ + do_check_eq(ch.responseStatus, 200); + do_check_eq(ch.responseStatusText, "OK"); + do_check_true(ch.requestSucceeded); + do_check_eq(ch.getResponseHeader("Override-Succeeded"), "yes"); +} + +function check200(ch) +{ + do_check_eq(ch.responseStatus, 200); + do_check_eq(ch.responseStatusText, "OK"); +} + +function checkFile(ch, cx, status, data) +{ + do_check_eq(ch.responseStatus, 200); + do_check_true(ch.requestSucceeded); + + var actualFile = serverBasePath.clone(); + actualFile.append("test_registerdirectory.js"); + do_check_eq(ch.getResponseHeader("Content-Length"), + actualFile.fileSize.toString()); + do_check_eq(data.map(v => String.fromCharCode(v)).join(""), + fileContents(actualFile)); +} + +XPCOMUtils.defineLazyGetter(this, "tests", function() { + return [ + +/*********************** + * without a base path * + ***********************/ + new Test(BASE + "/test_registerdirectory.js", + nocache, notFound, null), + +/******************** + * with a base path * + ********************/ + new Test(BASE + "/test_registerdirectory.js", + function(ch) + { + nocache(ch); + serverBasePath = testsDirectory.clone(); + srv.registerDirectory("/", serverBasePath); + }, + null, + checkFile), + +/***************************** + * without a base path again * + *****************************/ + new Test(BASE + "/test_registerdirectory.js", + function(ch) + { + nocache(ch); + serverBasePath = null; + srv.registerDirectory("/", serverBasePath); + }, + notFound, + null), + +/*************************** + * registered path handler * + ***************************/ + new Test(BASE + "/test_registerdirectory.js", + function(ch) + { + nocache(ch); + srv.registerPathHandler("/test_registerdirectory.js", + override_test_registerdirectory); + }, + checkOverride, + null), + +/************************ + * removed path handler * + ************************/ + new Test(BASE + "/test_registerdirectory.js", + function init_registerDirectory6(ch) + { + nocache(ch); + srv.registerPathHandler("/test_registerdirectory.js", null); + }, + notFound, + null), + +/******************** + * with a base path * + ********************/ + new Test(BASE + "/test_registerdirectory.js", + function(ch) + { + nocache(ch); + + // set the base path again + serverBasePath = testsDirectory.clone(); + srv.registerDirectory("/", serverBasePath); + }, + null, + checkFile), + +/************************* + * ...and a path handler * + *************************/ + new Test(BASE + "/test_registerdirectory.js", + function(ch) + { + nocache(ch); + srv.registerPathHandler("/test_registerdirectory.js", + override_test_registerdirectory); + }, + checkOverride, + null), + +/************************ + * removed base handler * + ************************/ + new Test(BASE + "/test_registerdirectory.js", + function(ch) + { + nocache(ch); + serverBasePath = null; + srv.registerDirectory("/", serverBasePath); + }, + checkOverride, + null), + +/************************ + * removed path handler * + ************************/ + new Test(BASE + "/test_registerdirectory.js", + function(ch) + { + nocache(ch); + srv.registerPathHandler("/test_registerdirectory.js", null); + }, + notFound, + null), + +/************************* + * mapping set up, works * + *************************/ + new Test(BASE + "/foo/test_registerdirectory.js", + function(ch) + { + nocache(ch); + serverBasePath = testsDirectory.clone(); + srv.registerDirectory("/foo/", serverBasePath); + }, + check200, + null), + +/********************* + * no mapping, fails * + *********************/ + new Test(BASE + "/foo/test_registerdirectory.js/test_registerdirectory.js", + nocache, + notFound, + null), + +/****************** + * mapping, works * + ******************/ + new Test(BASE + "/foo/test_registerdirectory.js/test_registerdirectory.js", + function(ch) + { + nocache(ch); + srv.registerDirectory("/foo/test_registerdirectory.js/", + serverBasePath); + }, + null, + checkFile), + +/************************************ + * two mappings set up, still works * + ************************************/ + new Test(BASE + "/foo/test_registerdirectory.js", + nocache, null, checkFile), + +/************************** + * remove topmost mapping * + **************************/ + new Test(BASE + "/foo/test_registerdirectory.js", + function(ch) + { + nocache(ch); + srv.registerDirectory("/foo/", null); + }, + notFound, + null), + +/************************************** + * lower mapping still present, works * + **************************************/ + new Test(BASE + "/foo/test_registerdirectory.js/test_registerdirectory.js", + nocache, null, checkFile), + +/******************* + * mapping removed * + *******************/ + new Test(BASE + "/foo/test_registerdirectory.js/test_registerdirectory.js", + function(ch) + { + nocache(ch); + srv.registerDirectory("/foo/test_registerdirectory.js/", null); + }, + notFound, + null) + ]; +}); + + +var srv; +var serverBasePath; +var testsDirectory; + +function run_test() +{ + testsDirectory = do_get_cwd(); + + srv = createServer(); + srv.start(-1); + + runHttpTests(tests, testComplete(srv)); +} + + +// PATH HANDLERS + +// override of /test_registerdirectory.js +function override_test_registerdirectory(metadata, response) +{ + response.setStatusLine("1.1", 200, "OK"); + response.setHeader("Override-Succeeded", "yes", false); + + var body = "success!"; + response.bodyOutputStream.write(body, body.length); +} diff --git a/netwerk/test/httpserver/test/test_registerfile.js b/netwerk/test/httpserver/test/test_registerfile.js new file mode 100644 index 0000000000..16a1270f51 --- /dev/null +++ b/netwerk/test/httpserver/test/test_registerfile.js @@ -0,0 +1,50 @@ +/* -*- 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/. */ + +// tests the registerFile API + +XPCOMUtils.defineLazyGetter(this, "BASE", function() { + return "http://localhost:" + srv.identity.primaryPort; +}); + +var file = do_get_file("test_registerfile.js"); + +function onStart(ch, cx) +{ + do_check_eq(ch.responseStatus, 200); +} + +function onStop(ch, cx, status, data) +{ + // not sufficient for equality, but not likely to be wrong! + do_check_eq(data.length, file.fileSize); +} + +XPCOMUtils.defineLazyGetter(this, "test", function() { + return new Test(BASE + "/foo", null, onStart, onStop); +}); + +var srv; + +function run_test() +{ + srv = createServer(); + + try + { + srv.registerFile("/foo", do_get_profile()); + throw "registerFile succeeded!"; + } + catch (e) + { + isException(e, Cr.NS_ERROR_INVALID_ARG); + } + + srv.registerFile("/foo", file); + srv.start(-1); + + runHttpTests([test], testComplete(srv)); +} diff --git a/netwerk/test/httpserver/test/test_registerprefix.js b/netwerk/test/httpserver/test/test_registerprefix.js new file mode 100644 index 0000000000..fa3c3390a2 --- /dev/null +++ b/netwerk/test/httpserver/test/test_registerprefix.js @@ -0,0 +1,127 @@ +/* -*- 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/. */ + +// tests the registerPrefixHandler API + +XPCOMUtils.defineLazyGetter(this, "BASE", function() { + return "http://localhost:" + srv.identity.primaryPort; +}); + +function nocache(ch) +{ + ch.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE; // important! +} + +function notFound(ch) +{ + do_check_eq(ch.responseStatus, 404); + do_check_false(ch.requestSucceeded); +} + +function makeCheckOverride(magic) +{ + return (function checkOverride(ch) + { + do_check_eq(ch.responseStatus, 200); + do_check_eq(ch.responseStatusText, "OK"); + do_check_true(ch.requestSucceeded); + do_check_eq(ch.getResponseHeader("Override-Succeeded"), magic); + }); +} + +XPCOMUtils.defineLazyGetter(this, "tests", function() { + return [ + new Test(BASE + "/prefix/dummy", prefixHandler, null, + makeCheckOverride("prefix")), + new Test(BASE + "/prefix/dummy", pathHandler, null, + makeCheckOverride("path")), + new Test(BASE + "/prefix/subpath/dummy", longerPrefixHandler, null, + makeCheckOverride("subpath")), + new Test(BASE + "/prefix/dummy", removeHandlers, null, notFound), + new Test(BASE + "/prefix/subpath/dummy", newPrefixHandler, null, + makeCheckOverride("subpath")) + ]; +}); + +/*************************** + * registered prefix handler * + ***************************/ + +function prefixHandler(channel) +{ + nocache(channel); + srv.registerPrefixHandler("/prefix/", makeOverride("prefix")); +} + +/******************************** + * registered path handler on top * + ********************************/ + +function pathHandler(channel) +{ + nocache(channel); + srv.registerPathHandler("/prefix/dummy", makeOverride("path")); +} + +/********************************** + * registered longer prefix handler * + **********************************/ + +function longerPrefixHandler(channel) +{ + nocache(channel); + srv.registerPrefixHandler("/prefix/subpath/", makeOverride("subpath")); +} + +/************************ + * removed prefix handler * + ************************/ + +function removeHandlers(channel) +{ + nocache(channel); + srv.registerPrefixHandler("/prefix/", null); + srv.registerPathHandler("/prefix/dummy", null); +} + +/***************************** + * re-register shorter handler * + *****************************/ + +function newPrefixHandler(channel) +{ + nocache(channel); + srv.registerPrefixHandler("/prefix/", makeOverride("prefix")); +} + +var srv; +var serverBasePath; +var testsDirectory; + +function run_test() +{ + testsDirectory = do_get_profile(); + + srv = createServer(); + srv.start(-1); + + runHttpTests(tests, testComplete(srv)); +} + +// PATH HANDLERS + +// generate an override +function makeOverride(magic) +{ + return (function override(metadata, response) + { + response.setStatusLine("1.1", 200, "OK"); + response.setHeader("Override-Succeeded", magic, false); + + var body = "success!"; + response.bodyOutputStream.write(body, body.length); + }); +} diff --git a/netwerk/test/httpserver/test/test_request_line_split_in_two_packets.js b/netwerk/test/httpserver/test/test_request_line_split_in_two_packets.js new file mode 100644 index 0000000000..b1a863f485 --- /dev/null +++ b/netwerk/test/httpserver/test/test_request_line_split_in_two_packets.js @@ -0,0 +1,135 @@ +/* -*- 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/. */ + +/** + * Tests that even when an incoming request's data for the Request-Line doesn't + * all fit in a single onInputStreamReady notification, the request is handled + * properly. + */ + +var srv = createServer(); +srv.start(-1); +const PORT = srv.identity.primaryPort; + +function run_test() +{ + srv.registerPathHandler("/lots-of-leading-blank-lines", + lotsOfLeadingBlankLines); + srv.registerPathHandler("/very-long-request-line", + veryLongRequestLine); + + runRawTests(tests, testComplete(srv)); +} + + +/*************** + * BEGIN TESTS * + ***************/ + +var test, data, str; +var tests = []; + + +function veryLongRequestLine(request, response) +{ + writeDetails(request, response); + response.setStatusLine(request.httpVersion, 200, "TEST PASSED"); +} + +var path = "/very-long-request-line?"; +var reallyLong = "0123456789ABCDEF0123456789ABCDEF"; // 32 +reallyLong = reallyLong + reallyLong + reallyLong + reallyLong; // 128 +reallyLong = reallyLong + reallyLong + reallyLong + reallyLong; // 512 +reallyLong = reallyLong + reallyLong + reallyLong + reallyLong; // 2048 +reallyLong = reallyLong + reallyLong + reallyLong + reallyLong; // 8192 +reallyLong = reallyLong + reallyLong + reallyLong + reallyLong; // 32768 +reallyLong = reallyLong + reallyLong + reallyLong + reallyLong; // 131072 +reallyLong = reallyLong + reallyLong + reallyLong + reallyLong; // 524288 +if (reallyLong.length !== 524288) + throw new TypeError("generated length not as long as expected"); +str = "GET /very-long-request-line?" + reallyLong + " HTTP/1.1\r\n" + + "Host: localhost:" + PORT + "\r\n" + + "\r\n"; +data = []; +for (var i = 0; i < str.length; i += 16384) + data.push(str.substr(i, 16384)); + +function checkVeryLongRequestLine(data) +{ + var iter = LineIterator(data); + + print("data length: " + data.length); + print("iter object: " + iter); + + // Status-Line + do_check_eq(iter.next(), "HTTP/1.1 200 TEST PASSED"); + + skipHeaders(iter); + + // Okay, next line must be the data we expected to be written + var body = + [ + "Method: GET", + "Path: /very-long-request-line", + "Query: " + reallyLong, + "Version: 1.1", + "Scheme: http", + "Host: localhost", + "Port: " + PORT, + ]; + + expectLines(iter, body); +} +test = new RawTest("localhost", PORT, data, checkVeryLongRequestLine), +tests.push(test); + + +function lotsOfLeadingBlankLines(request, response) +{ + writeDetails(request, response); + response.setStatusLine(request.httpVersion, 200, "TEST PASSED"); +} + +var blankLines = "\r\n"; +for (var i = 0; i < 14; i++) + blankLines += blankLines; +str = blankLines + + "GET /lots-of-leading-blank-lines HTTP/1.1\r\n" + + "Host: localhost:" + PORT + "\r\n" + + "\r\n"; +data = []; +for (var i = 0; i < str.length; i += 100) + data.push(str.substr(i, 100)); + +function checkLotsOfLeadingBlankLines(data) +{ + var iter = LineIterator(data); + + // Status-Line + print("data length: " + data.length); + print("iter object: " + iter); + + do_check_eq(iter.next(), "HTTP/1.1 200 TEST PASSED"); + + skipHeaders(iter); + + // Okay, next line must be the data we expected to be written + var body = + [ + "Method: GET", + "Path: /lots-of-leading-blank-lines", + "Query: ", + "Version: 1.1", + "Scheme: http", + "Host: localhost", + "Port: " + PORT, + ]; + + expectLines(iter, body); +} + +test = new RawTest("localhost", PORT, data, checkLotsOfLeadingBlankLines), +tests.push(test); diff --git a/netwerk/test/httpserver/test/test_response_write.js b/netwerk/test/httpserver/test/test_response_write.js new file mode 100644 index 0000000000..0a37ee44b3 --- /dev/null +++ b/netwerk/test/httpserver/test/test_response_write.js @@ -0,0 +1,55 @@ +/* -*- 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/. */ + +// make sure response.write works for strings, and coerces other args to strings + +XPCOMUtils.defineLazyGetter(this, "tests", function() { + return [ + new Test("http://localhost:" + srv.identity.primaryPort + "/writeString", + null, check_1234, succeeded), + new Test("http://localhost:" + srv.identity.primaryPort + "/writeInt", + null, check_1234, succeeded), + ]; +}); + +var srv; + +function run_test() +{ + srv = createServer(); + + srv.registerPathHandler("/writeString", writeString); + srv.registerPathHandler("/writeInt", writeInt); + srv.start(-1); + + runHttpTests(tests, testComplete(srv)); +} + + +// TEST DATA + +function succeeded(ch, cx, status, data) +{ + do_check_true(Components.isSuccessCode(status)); + do_check_eq(data.map(v => String.fromCharCode(v)).join(""), "1234"); +} + +function check_1234(ch, cx) +{ + do_check_eq(ch.getResponseHeader("Content-Length"), "4"); +} + +// PATH HANDLERS + +function writeString(metadata, response) +{ + response.write("1234"); +} + +function writeInt(metadata, response) +{ + response.write(1234); +} diff --git a/netwerk/test/httpserver/test/test_seizepower.js b/netwerk/test/httpserver/test/test_seizepower.js new file mode 100644 index 0000000000..f2d9e32c1c --- /dev/null +++ b/netwerk/test/httpserver/test/test_seizepower.js @@ -0,0 +1,182 @@ +/* -*- 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/. */ + +/* + * Tests that the seizePower API works correctly. + */ + +XPCOMUtils.defineLazyGetter(this, "PORT", function() { + return srv.identity.primaryPort; +}); + +var srv; + +function run_test() +{ + srv = createServer(); + + srv.registerPathHandler("/raw-data", handleRawData); + srv.registerPathHandler("/called-too-late", handleTooLate); + srv.registerPathHandler("/exceptions", handleExceptions); + srv.registerPathHandler("/async-seizure", handleAsyncSeizure); + srv.registerPathHandler("/seize-after-async", handleSeizeAfterAsync); + + srv.start(-1); + + runRawTests(tests, testComplete(srv)); +} + + +function checkException(fun, err, msg) +{ + try + { + fun(); + } + catch (e) + { + if (e !== err && e.result !== err) + do_throw(msg); + return; + } + do_throw(msg); +} + +function callASAPLater(fun) +{ + gThreadManager.currentThread.dispatch({ + run: function() + { + fun(); + } + }, Ci.nsIThread.DISPATCH_NORMAL); +} + + +/***************** + * PATH HANDLERS * + *****************/ + +function handleRawData(request, response) +{ + response.seizePower(); + response.write("Raw data!"); + response.finish(); +} + +function handleTooLate(request, response) +{ + response.write("DO NOT WANT"); + var output = response.bodyOutputStream; + + response.seizePower(); + + if (response.bodyOutputStream !== output) + response.write("bodyOutputStream changed!"); + else + response.write("too-late passed"); + response.finish(); +} + +function handleExceptions(request, response) +{ + response.seizePower(); + checkException(function() { response.setStatusLine("1.0", 500, "ISE"); }, + Cr.NS_ERROR_NOT_AVAILABLE, + "setStatusLine should throw not-available after seizePower"); + checkException(function() { response.setHeader("X-Fail", "FAIL", false); }, + Cr.NS_ERROR_NOT_AVAILABLE, + "setHeader should throw not-available after seizePower"); + checkException(function() { response.processAsync(); }, + Cr.NS_ERROR_NOT_AVAILABLE, + "processAsync should throw not-available after seizePower"); + var out = response.bodyOutputStream; + var data = "exceptions test passed"; + out.write(data, data.length); + response.seizePower(); // idempotency test of seizePower + response.finish(); + response.finish(); // idempotency test of finish after seizePower + checkException(function() { response.seizePower(); }, + Cr.NS_ERROR_UNEXPECTED, + "seizePower should throw unexpected after finish"); +} + +function handleAsyncSeizure(request, response) +{ + response.seizePower(); + callLater(1, function() + { + response.write("async seizure passed"); + response.bodyOutputStream.close(); + callLater(1, function() + { + response.finish(); + }); + }); +} + +function handleSeizeAfterAsync(request, response) +{ + response.setStatusLine(request.httpVersion, 200, "async seizure pass"); + response.processAsync(); + checkException(function() { response.seizePower(); }, + Cr.NS_ERROR_NOT_AVAILABLE, + "seizePower should throw not-available after processAsync"); + callLater(1, function() + { + response.finish(); + }); +} + + +/*************** + * BEGIN TESTS * + ***************/ + +XPCOMUtils.defineLazyGetter(this, "tests", function() { + return [ + new RawTest("localhost", PORT, data0, checkRawData), + new RawTest("localhost", PORT, data1, checkTooLate), + new RawTest("localhost", PORT, data2, checkExceptions), + new RawTest("localhost", PORT, data3, checkAsyncSeizure), + new RawTest("localhost", PORT, data4, checkSeizeAfterAsync), + ]; +}); + +var data0 = "GET /raw-data HTTP/1.0\r\n" + + "\r\n"; +function checkRawData(data) +{ + do_check_eq(data, "Raw data!"); +} + +var data1 = "GET /called-too-late HTTP/1.0\r\n" + + "\r\n"; +function checkTooLate(data) +{ + do_check_eq(LineIterator(data).next(), "too-late passed"); +} + +var data2 = "GET /exceptions HTTP/1.0\r\n" + + "\r\n"; +function checkExceptions(data) +{ + do_check_eq("exceptions test passed", data); +} + +var data3 = "GET /async-seizure HTTP/1.0\r\n" + + "\r\n"; +function checkAsyncSeizure(data) +{ + do_check_eq(data, "async seizure passed"); +} + +var data4 = "GET /seize-after-async HTTP/1.0\r\n" + + "\r\n"; +function checkSeizeAfterAsync(data) +{ + do_check_eq(LineIterator(data).next(), "HTTP/1.0 200 async seizure pass"); +} diff --git a/netwerk/test/httpserver/test/test_setindexhandler.js b/netwerk/test/httpserver/test/test_setindexhandler.js new file mode 100644 index 0000000000..6e733f4dbb --- /dev/null +++ b/netwerk/test/httpserver/test/test_setindexhandler.js @@ -0,0 +1,67 @@ +/* -*- 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/. */ + +// Make sure setIndexHandler works as expected + +var srv, serverBasePath; + +function run_test() +{ + srv = createServer(); + serverBasePath = do_get_profile(); + srv.registerDirectory("/", serverBasePath); + srv.setIndexHandler(myIndexHandler); + srv.start(-1); + + runHttpTests(tests, testComplete(srv)); +} + +XPCOMUtils.defineLazyGetter(this, "URL", function() { + return "http://localhost:" + srv.identity.primaryPort + "/"; +}); + +XPCOMUtils.defineLazyGetter(this, "tests", function() { + return [ + new Test(URL, init, startCustomIndexHandler, stopCustomIndexHandler), + new Test(URL, init, startDefaultIndexHandler, stopDefaultIndexHandler) + ]; +}); + +function init(ch) +{ + ch.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE; // important! +} +function startCustomIndexHandler(ch, cx) +{ + do_check_eq(ch.getResponseHeader("Content-Length"), "10"); + srv.setIndexHandler(null); +} +function stopCustomIndexHandler(ch, cx, status, data) +{ + do_check_true(Components.isSuccessCode(status)); + do_check_eq(String.fromCharCode.apply(null, data), "directory!"); +} + +function startDefaultIndexHandler(ch, cx) +{ + do_check_eq(ch.responseStatus, 200); +} +function stopDefaultIndexHandler(ch, cx, status, data) +{ + do_check_true(Components.isSuccessCode(status)); +} + +// PATH HANDLERS + +function myIndexHandler(metadata, response) +{ + var dir = metadata.getProperty("directory"); + do_check_true(dir != null); + do_check_true(dir instanceof Ci.nsIFile); + do_check_true(dir.equals(serverBasePath)); + + response.write("directory!"); +} diff --git a/netwerk/test/httpserver/test/test_setstatusline.js b/netwerk/test/httpserver/test/test_setstatusline.js new file mode 100644 index 0000000000..f6f6514887 --- /dev/null +++ b/netwerk/test/httpserver/test/test_setstatusline.js @@ -0,0 +1,172 @@ +/* -*- 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/. */ + +// exercise nsIHttpResponse.setStatusLine, ensure its atomicity, and ensure the +// specified behavior occurs if it's not called + +XPCOMUtils.defineLazyGetter(this, "URL", function() { + return "http://localhost:" + srv.identity.primaryPort; +}); + +var srv; + +function run_test() +{ + srv = createServer(); + + srv.registerPathHandler("/no/setstatusline", noSetstatusline); + srv.registerPathHandler("/http1_0", http1_0); + srv.registerPathHandler("/http1_1", http1_1); + srv.registerPathHandler("/invalidVersion", invalidVersion); + srv.registerPathHandler("/invalidStatus", invalidStatus); + srv.registerPathHandler("/invalidDescription", invalidDescription); + srv.registerPathHandler("/crazyCode", crazyCode); + srv.registerPathHandler("/nullVersion", nullVersion); + + srv.start(-1); + + runHttpTests(tests, testComplete(srv)); +} + + +/************* + * UTILITIES * + *************/ + +function checkStatusLine(channel, httpMaxVer, httpMinVer, httpCode, statusText) +{ + do_check_eq(channel.responseStatus, httpCode); + do_check_eq(channel.responseStatusText, statusText); + + var respMaj = {}, respMin = {}; + channel.getResponseVersion(respMaj, respMin); + do_check_eq(respMaj.value, httpMaxVer); + do_check_eq(respMin.value, httpMinVer); +} + + +/********* + * TESTS * + *********/ + +XPCOMUtils.defineLazyGetter(this, "tests", function() { + return [ + new Test(URL + "/no/setstatusline", null, startNoSetStatusLine, stop), + new Test(URL + "/http1_0", null, startHttp1_0, stop), + new Test(URL + "/http1_1", null, startHttp1_1, stop), + new Test(URL + "/invalidVersion", null, startPassedTrue, stop), + new Test(URL + "/invalidStatus", null, startPassedTrue, stop), + new Test(URL + "/invalidDescription", null, startPassedTrue, stop), + new Test(URL + "/crazyCode", null, startCrazy, stop), + new Test(URL + "/nullVersion", null, startNullVersion, stop) + ]; +}); + + +// /no/setstatusline +function noSetstatusline(metadata, response) +{ +} +function startNoSetStatusLine(ch, cx) +{ + checkStatusLine(ch, 1, 1, 200, "OK"); +} +function stop(ch, cx, status, data) +{ + do_check_true(Components.isSuccessCode(status)); +} + + +// /http1_0 +function http1_0(metadata, response) +{ + response.setStatusLine("1.0", 200, "OK"); +} +function startHttp1_0(ch, cx) +{ + checkStatusLine(ch, 1, 0, 200, "OK"); +} + + +// /http1_1 +function http1_1(metadata, response) +{ + response.setStatusLine("1.1", 200, "OK"); +} +function startHttp1_1(ch, cx) +{ + checkStatusLine(ch, 1, 1, 200, "OK"); +} + + +// /invalidVersion +function invalidVersion(metadata, response) +{ + try + { + response.setStatusLine(" 1.0", 200, "FAILED"); + } + catch (e) + { + response.setHeader("Passed", "true", false); + } +} +function startPassedTrue(ch, cx) +{ + checkStatusLine(ch, 1, 1, 200, "OK"); + do_check_eq(ch.getResponseHeader("Passed"), "true"); +} + + +// /invalidStatus +function invalidStatus(metadata, response) +{ + try + { + response.setStatusLine("1.0", 1000, "FAILED"); + } + catch (e) + { + response.setHeader("Passed", "true", false); + } +} + + +// /invalidDescription +function invalidDescription(metadata, response) +{ + try + { + response.setStatusLine("1.0", 200, "FAILED\x01"); + } + catch (e) + { + response.setHeader("Passed", "true", false); + } +} + + +// /crazyCode +function crazyCode(metadata, response) +{ + response.setStatusLine("1.1", 617, "Crazy"); +} +function startCrazy(ch, cx) +{ + checkStatusLine(ch, 1, 1, 617, "Crazy"); +} + + +// /nullVersion +function nullVersion(metadata, response) +{ + response.setStatusLine(null, 255, "NULL"); +} +function startNullVersion(ch, cx) +{ + // currently, this server implementation defaults to 1.1 + checkStatusLine(ch, 1, 1, 255, "NULL"); +} diff --git a/netwerk/test/httpserver/test/test_sjs.js b/netwerk/test/httpserver/test/test_sjs.js new file mode 100644 index 0000000000..2a9814196c --- /dev/null +++ b/netwerk/test/httpserver/test/test_sjs.js @@ -0,0 +1,251 @@ +/* -*- 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/. */ + +// tests support for server JS-generated pages + +var srv = createServer(); + +var sjs = do_get_file("data/sjs/cgi.sjs"); +// NB: The server has no state at this point -- all state is set up and torn +// down in the tests, because we run the same tests twice with only a +// different query string on the requests, followed by the oddball +// test that doesn't care about throwing or not. +srv.start(-1); +const PORT = srv.identity.primaryPort; + +const BASE = "http://localhost:" + PORT; +var test; +var tests = []; + + +/********************* + * UTILITY FUNCTIONS * + *********************/ + +function bytesToString(bytes) +{ + return bytes.map(function(v) { return String.fromCharCode(v); }).join(""); +} + +function skipCache(ch) +{ + ch.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE; +} + + +/******************** + * DEFINE THE TESTS * + ********************/ + +/** + * Adds the set of tests defined in here, differentiating between tests with a + * SJS which throws an exception and creates a server error and tests with a + * normal, successful SJS. + */ +function setupTests(throwing) +{ + const TEST_URL = BASE + "/cgi.sjs" + (throwing ? "?throw" : ""); + + // registerFile with SJS => raw text + + function setupFile(ch) + { + srv.registerFile("/cgi.sjs", sjs); + skipCache(ch); + } + + function verifyRawText(channel, cx, status, bytes) + { + dumpn(channel.originalURI.spec); + do_check_eq(bytesToString(bytes), fileContents(sjs)); + } + + test = new Test(TEST_URL, setupFile, null, verifyRawText); + tests.push(test); + + + // add mapping, => interpreted + + function addTypeMapping(ch) + { + srv.registerContentType("sjs", "sjs"); + skipCache(ch); + } + + function checkType(ch, cx) + { + if (throwing) + { + do_check_false(ch.requestSucceeded); + do_check_eq(ch.responseStatus, 500); + } + else + { + do_check_eq(ch.contentType, "text/plain"); + } + } + + function checkContents(ch, cx, status, data) + { + if (!throwing) + do_check_eq("PASS", bytesToString(data)); + } + + test = new Test(TEST_URL, addTypeMapping, checkType, checkContents); + tests.push(test); + + + // remove file/type mapping, map containing directory => raw text + + function setupDirectoryAndRemoveType(ch) + { + dumpn("removing type mapping"); + srv.registerContentType("sjs", null); + srv.registerFile("/cgi.sjs", null); + srv.registerDirectory("/", sjs.parent); + skipCache(ch); + } + + test = new Test(TEST_URL, setupDirectoryAndRemoveType, null, verifyRawText); + tests.push(test); + + + // add mapping, => interpreted + + function contentAndCleanup(ch, cx, status, data) + { + checkContents(ch, cx, status, data); + + // clean up state we've set up + srv.registerDirectory("/", null); + srv.registerContentType("sjs", null); + } + + test = new Test(TEST_URL, addTypeMapping, checkType, contentAndCleanup); + tests.push(test); + + // NB: No remaining state in the server right now! If we have any here, + // either the second run of tests (without ?throw) or the tests added + // after the two sets will almost certainly fail. +} + + +/***************** + * ADD THE TESTS * + *****************/ + +setupTests(true); +setupTests(false); + +// Test that when extension-mappings are used, the entire filename cannot be +// treated as an extension -- there must be at least one dot for a filename to +// match an extension. + +function init(ch) +{ + // clean up state we've set up + srv.registerDirectory("/", sjs.parent); + srv.registerContentType("sjs", "sjs"); + skipCache(ch); +} + +function checkNotSJS(ch, cx, status, data) +{ + do_check_neq("FAIL", bytesToString(data)); +} + +test = new Test(BASE + "/sjs", init, null, checkNotSJS); +tests.push(test); + +// Test that Range requests are passed through to the SJS file without +// bounds checking. + +function rangeInit(expectedRangeHeader) +{ + return function setupRangeRequest(ch) + { + ch.setRequestHeader("Range", expectedRangeHeader, false); + }; +} + +function checkRangeResult(ch, cx) +{ + try + { + var val = ch.getResponseHeader("Content-Range"); + } + catch (e) { /* IDL doesn't specify a particular exception to require */ } + if (val !== undefined) + { + do_throw("should not have gotten a Content-Range header, but got one " + + "with this value: " + val); + } + do_check_eq(200, ch.responseStatus); + do_check_eq("OK", ch.responseStatusText); +} + +test = new Test(BASE + "/range-checker.sjs", + rangeInit("not-a-bytes-equals-specifier"), + checkRangeResult, null); +tests.push(test); +test = new Test(BASE + "/range-checker.sjs", + rangeInit("bytes=-"), + checkRangeResult, null); +tests.push(test); +test = new Test(BASE + "/range-checker.sjs", + rangeInit("bytes=1000000-"), + checkRangeResult, null); +tests.push(test); +test = new Test(BASE + "/range-checker.sjs", + rangeInit("bytes=1-4"), + checkRangeResult, null); +tests.push(test); +test = new Test(BASE + "/range-checker.sjs", + rangeInit("bytes=-4"), + checkRangeResult, null); +tests.push(test); + +// One last test: for file mappings, the content-type is determined by the +// extension of the file on the server, not by the extension of the requested +// path. + +function setupFileMapping(ch) +{ + srv.registerFile("/script.html", sjs); +} + +function onStart(ch, cx) +{ + do_check_eq(ch.contentType, "text/plain"); +} + +function onStop(ch, cx, status, data) +{ + do_check_eq("PASS", bytesToString(data)); +} + +test = new Test(BASE + "/script.html", setupFileMapping, onStart, onStop); +tests.push(test); + + +/***************** + * RUN THE TESTS * + *****************/ + +function run_test() +{ + // Test for a content-type which isn't a field-value + try + { + srv.registerContentType("foo", "bar\nbaz"); + throw "this server throws on content-types which aren't field-values"; + } + catch (e) + { + isException(e, Cr.NS_ERROR_INVALID_ARG); + } + runHttpTests(tests, testComplete(srv)); +} diff --git a/netwerk/test/httpserver/test/test_sjs_object_state.js b/netwerk/test/httpserver/test/test_sjs_object_state.js new file mode 100644 index 0000000000..b0f4e546d9 --- /dev/null +++ b/netwerk/test/httpserver/test/test_sjs_object_state.js @@ -0,0 +1,290 @@ +/* -*- 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/. */ + +/* + * Tests that the object-state-preservation mechanism works correctly. + */ + + +XPCOMUtils.defineLazyGetter(this, "PATH", function() { + return "http://localhost:" + srv.identity.primaryPort + "/object-state.sjs"; +}); + +var srv; + +function run_test() +{ + srv = createServer(); + var sjsDir = do_get_file("data/sjs/"); + srv.registerDirectory("/", sjsDir); + srv.registerContentType("sjs", "sjs"); + srv.start(-1); + + do_test_pending(); + + new HTTPTestLoader(PATH + "?state=initial", initialStart, initialStop); +} + +/******************** + * OBSERVER METHODS * + ********************/ + +/* + * In practice the current implementation will guarantee an exact ordering of + * all start and stop callbacks. However, in the interests of robustness, this + * test will pass given any valid ordering of callbacks. Any ordering of calls + * which obeys the partial ordering shown by this directed acyclic graph will be + * handled correctly: + * + * initialStart + * | + * V + * intermediateStart + * | + * V + * intermediateStop + * | \ + * | V + * | initialStop + * V + * triggerStart + * | + * V + * triggerStop + * + */ + +var initialStarted = false; +function initialStart(ch, cx) +{ + dumpn("*** initialStart"); + + if (initialStarted) + do_throw("initialStart: initialStarted is true?!?!"); + + initialStarted = true; + + new HTTPTestLoader(PATH + "?state=intermediate", + intermediateStart, intermediateStop); +} + +var initialStopped = false; +function initialStop(ch, cx, status, data) +{ + dumpn("*** initialStop"); + + do_check_eq(data.map(function(v) { return String.fromCharCode(v); }).join(""), + "done"); + + do_check_eq(srv.getObjectState("object-state-test"), null); + + if (!initialStarted) + do_throw("initialStop: initialStarted is false?!?!"); + if (initialStopped) + do_throw("initialStop: initialStopped is true?!?!"); + if (!intermediateStarted) + do_throw("initialStop: intermediateStarted is false?!?!"); + if (!intermediateStopped) + do_throw("initialStop: intermediateStopped is false?!?!"); + + initialStopped = true; + + checkForFinish(); +} + +var intermediateStarted = false; +function intermediateStart(ch, cx) +{ + dumpn("*** intermediateStart"); + + do_check_neq(srv.getObjectState("object-state-test"), null); + + if (!initialStarted) + do_throw("intermediateStart: initialStarted is false?!?!"); + if (intermediateStarted) + do_throw("intermediateStart: intermediateStarted is true?!?!"); + + intermediateStarted = true; +} + +var intermediateStopped = false; +function intermediateStop(ch, cx, status, data) +{ + dumpn("*** intermediateStop"); + + do_check_eq(data.map(function(v) { return String.fromCharCode(v); }).join(""), + "intermediate"); + + do_check_neq(srv.getObjectState("object-state-test"), null); + + if (!initialStarted) + do_throw("intermediateStop: initialStarted is false?!?!"); + if (!intermediateStarted) + do_throw("intermediateStop: intermediateStarted is false?!?!"); + if (intermediateStopped) + do_throw("intermediateStop: intermediateStopped is true?!?!"); + + intermediateStopped = true; + + new HTTPTestLoader(PATH + "?state=trigger", triggerStart, + triggerStop); +} + +var triggerStarted = false; +function triggerStart(ch, cx) +{ + dumpn("*** triggerStart"); + + if (!initialStarted) + do_throw("triggerStart: initialStarted is false?!?!"); + if (!intermediateStarted) + do_throw("triggerStart: intermediateStarted is false?!?!"); + if (!intermediateStopped) + do_throw("triggerStart: intermediateStopped is false?!?!"); + if (triggerStarted) + do_throw("triggerStart: triggerStarted is true?!?!"); + + triggerStarted = true; +} + +var triggerStopped = false; +function triggerStop(ch, cx, status, data) +{ + dumpn("*** triggerStop"); + + do_check_eq(data.map(function(v) { return String.fromCharCode(v); }).join(""), + "trigger"); + + if (!initialStarted) + do_throw("triggerStop: initialStarted is false?!?!"); + if (!intermediateStarted) + do_throw("triggerStop: intermediateStarted is false?!?!"); + if (!intermediateStopped) + do_throw("triggerStop: intermediateStopped is false?!?!"); + if (!triggerStarted) + do_throw("triggerStop: triggerStarted is false?!?!"); + if (triggerStopped) + do_throw("triggerStop: triggerStopped is false?!?!"); + + triggerStopped = true; + + checkForFinish(); +} + +var finished = false; +function checkForFinish() +{ + if (finished) + { + try + { + do_throw("uh-oh, how are we being finished twice?!?!"); + } + finally + { + quit(1); + } + } + + if (triggerStopped && initialStopped) + { + finished = true; + try + { + do_check_eq(srv.getObjectState("object-state-test"), null); + + if (!initialStarted) + do_throw("checkForFinish: initialStarted is false?!?!"); + if (!intermediateStarted) + do_throw("checkForFinish: intermediateStarted is false?!?!"); + if (!intermediateStopped) + do_throw("checkForFinish: intermediateStopped is false?!?!"); + if (!triggerStarted) + do_throw("checkForFinish: triggerStarted is false?!?!"); + } + finally + { + srv.stop(do_test_finished); + } + } +} + + +/********************************* + * UTILITY OBSERVABLE URL LOADER * + *********************************/ + +/** Stream listener for the channels. */ +function HTTPTestLoader(path, start, stop) +{ + /** Path to load. */ + this._path = path; + + /** Array of bytes of data in body of response. */ + this._data = []; + + /** onStartRequest callback. */ + this._start = start; + + /** onStopRequest callback. */ + this._stop = stop; + + var channel = makeChannel(path); + channel.asyncOpen2(this); +} +HTTPTestLoader.prototype = + { + onStartRequest: function(request, cx) + { + dumpn("*** HTTPTestLoader.onStartRequest for " + this._path); + + var ch = request.QueryInterface(Ci.nsIHttpChannel) + .QueryInterface(Ci.nsIHttpChannelInternal); + + try + { + try + { + this._start(ch, cx); + } + catch (e) + { + do_throw(this._path + ": error in onStartRequest: " + e); + } + } + catch (e) + { + dumpn("!!! swallowing onStartRequest exception so onStopRequest is " + + "called..."); + } + }, + onDataAvailable: function(request, cx, inputStream, offset, count) + { + dumpn("*** HTTPTestLoader.onDataAvailable for " + this._path); + + Array.prototype.push.apply(this._data, + makeBIS(inputStream).readByteArray(count)); + }, + onStopRequest: function(request, cx, status) + { + dumpn("*** HTTPTestLoader.onStopRequest for " + this._path); + + var ch = request.QueryInterface(Ci.nsIHttpChannel) + .QueryInterface(Ci.nsIHttpChannelInternal); + + this._stop(ch, cx, status, this._data); + }, + QueryInterface: function(aIID) + { + dumpn("*** QueryInterface: " + aIID); + + if (aIID.equals(Ci.nsIStreamListener) || + aIID.equals(Ci.nsIRequestObserver) || + aIID.equals(Ci.nsISupports)) + return this; + throw Cr.NS_ERROR_NO_INTERFACE; + } + }; diff --git a/netwerk/test/httpserver/test/test_sjs_state.js b/netwerk/test/httpserver/test/test_sjs_state.js new file mode 100644 index 0000000000..ccf5c4b031 --- /dev/null +++ b/netwerk/test/httpserver/test/test_sjs_state.js @@ -0,0 +1,186 @@ +/* -*- 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/. */ + +// exercises the server's state-preservation API + +XPCOMUtils.defineLazyGetter(this, "URL", function() { + return "http://localhost:" + srv.identity.primaryPort; +}); + +var srv; + +function run_test() +{ + srv = createServer(); + var sjsDir = do_get_file("data/sjs/"); + srv.registerDirectory("/", sjsDir); + srv.registerContentType("sjs", "sjs"); + srv.registerPathHandler("/path-handler", pathHandler); + srv.start(-1); + + function done() + { + do_check_eq(srv.getSharedState("shared-value"), "done!"); + do_check_eq(srv.getState("/path-handler", "private-value"), + "pathHandlerPrivate2"); + do_check_eq(srv.getState("/state1.sjs", "private-value"), + ""); + do_check_eq(srv.getState("/state2.sjs", "private-value"), + "newPrivate5"); + do_test_pending(); + srv.stop(function() { do_test_finished(); }); + } + + runHttpTests(tests, done); +} + + +/************ + * HANDLERS * + ************/ + +var firstTime = true; + +function pathHandler(request, response) +{ + response.setHeader("Cache-Control", "no-cache", false); + + response.setHeader("X-Old-Shared-Value", srv.getSharedState("shared-value"), + false); + response.setHeader("X-Old-Private-Value", srv.getState("/path-handler", "private-value"), + false); + + var privateValue, sharedValue; + if (firstTime) + { + firstTime = false; + privateValue = "pathHandlerPrivate"; + sharedValue = "pathHandlerShared"; + } + else + { + privateValue = "pathHandlerPrivate2"; + sharedValue = ""; + } + + srv.setState("/path-handler", "private-value", privateValue); + srv.setSharedState("shared-value", sharedValue); + + response.setHeader("X-New-Private-Value", privateValue, false); + response.setHeader("X-New-Shared-Value", sharedValue, false); +} + + +/*************** + * BEGIN TESTS * + ***************/ + +XPCOMUtils.defineLazyGetter(this, "tests", function() { + return [ + new Test(URL + "/state1.sjs?" + + "newShared=newShared&newPrivate=newPrivate", + null, start_initial, null), + new Test(URL + "/state1.sjs?" + + "newShared=newShared2&newPrivate=newPrivate2", + null, start_overwrite, null), + new Test(URL + "/state1.sjs?" + + "newShared=&newPrivate=newPrivate3", + null, start_remove, null), + new Test(URL + "/path-handler", + null, start_handler, null), + new Test(URL + "/path-handler", + null, start_handler_again, null), + new Test(URL + "/state2.sjs?" + + "newShared=newShared4&newPrivate=newPrivate4", + null, start_other_initial, null), + new Test(URL + "/state2.sjs?" + + "newShared=", + null, start_other_remove_ignore, null), + new Test(URL + "/state2.sjs?" + + "newShared=newShared5&newPrivate=newPrivate5", + null, start_other_set_new, null), + new Test(URL + "/state1.sjs?" + + "newShared=done!&newPrivate=", + null, start_set_remove_original, null) + ]; +}); + +/* Hack around bug 474845 for now. */ +function getHeaderFunction(ch) +{ + function getHeader(name) + { + try + { + return ch.getResponseHeader(name); + } + catch (e) + { + if (e.result !== Cr.NS_ERROR_NOT_AVAILABLE) + throw e; + } + return ""; + } + return getHeader; +} + +function expectValues(ch, oldShared, newShared, oldPrivate, newPrivate) +{ + var getHeader = getHeaderFunction(ch); + + do_check_eq(ch.responseStatus, 200); + do_check_eq(getHeader("X-Old-Shared-Value"), oldShared); + do_check_eq(getHeader("X-New-Shared-Value"), newShared); + do_check_eq(getHeader("X-Old-Private-Value"), oldPrivate); + do_check_eq(getHeader("X-New-Private-Value"), newPrivate); +} + +function start_initial(ch, cx) +{ +dumpn("XXX start_initial"); + expectValues(ch, "", "newShared", "", "newPrivate"); +} + +function start_overwrite(ch, cx) +{ + expectValues(ch, "newShared", "newShared2", "newPrivate", "newPrivate2"); +} + +function start_remove(ch, cx) +{ + expectValues(ch, "newShared2", "", "newPrivate2", "newPrivate3"); +} + +function start_handler(ch, cx) +{ + expectValues(ch, "", "pathHandlerShared", "", "pathHandlerPrivate"); +} + +function start_handler_again(ch, cx) +{ + expectValues(ch, "pathHandlerShared", "", + "pathHandlerPrivate", "pathHandlerPrivate2"); +} + +function start_other_initial(ch, cx) +{ + expectValues(ch, "", "newShared4", "", "newPrivate4"); +} + +function start_other_remove_ignore(ch, cx) +{ + expectValues(ch, "newShared4", "", "newPrivate4", ""); +} + +function start_other_set_new(ch, cx) +{ + expectValues(ch, "", "newShared5", "newPrivate4", "newPrivate5"); +} + +function start_set_remove_original(ch, cx) +{ + expectValues(ch, "newShared5", "done!", "newPrivate3", ""); +} diff --git a/netwerk/test/httpserver/test/test_sjs_throwing_exceptions.js b/netwerk/test/httpserver/test/test_sjs_throwing_exceptions.js new file mode 100644 index 0000000000..2e4855b01f --- /dev/null +++ b/netwerk/test/httpserver/test/test_sjs_throwing_exceptions.js @@ -0,0 +1,74 @@ +/* -*- 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/. */ + +/* + * Tests that running an SJS a whole lot of times doesn't have any ill effects + * (like exceeding open-file limits due to not closing the SJS file each time, + * then preventing any file from being opened). + */ + +XPCOMUtils.defineLazyGetter(this, "URL", function() { + return "http://localhost:" + srv.identity.primaryPort; +}); + +var srv; + +function run_test() +{ + srv = createServer(); + var sjsDir = do_get_file("data/sjs/"); + srv.registerDirectory("/", sjsDir); + srv.registerContentType("sjs", "sjs"); + srv.start(-1); + + function done() + { + do_test_pending(); + srv.stop(function() { do_test_finished(); }); + do_check_eq(gStartCount, TEST_RUNS); + do_check_true(lastPassed); + } + + runHttpTests(tests, done); +} + +/*************** + * BEGIN TESTS * + ***************/ + +var gStartCount = 0; +var lastPassed = false; + +// This hits the open-file limit for me on OS X; your mileage may vary. +const TEST_RUNS = 250; + +XPCOMUtils.defineLazyGetter(this, "tests", function() { + var _tests = new Array(TEST_RUNS + 1); + var _test = new Test(URL + "/thrower.sjs?throw", null, start_thrower); + for (var i = 0; i < TEST_RUNS; i++) + _tests[i] = _test; + // ...and don't forget to stop! + _tests[TEST_RUNS] = new Test(URL + "/thrower.sjs", null, start_last); + return _tests; +}); + +function start_thrower(ch, cx) +{ + do_check_eq(ch.responseStatus, 500); + do_check_false(ch.requestSucceeded); + + gStartCount++; +} + +function start_last(ch, cx) +{ + do_check_eq(ch.responseStatus, 200); + do_check_true(ch.requestSucceeded); + + do_check_eq(ch.getResponseHeader("X-Test-Status"), "PASS"); + + lastPassed = true; +} diff --git a/netwerk/test/httpserver/test/test_start_stop.js b/netwerk/test/httpserver/test/test_start_stop.js new file mode 100644 index 0000000000..e9f42bddd8 --- /dev/null +++ b/netwerk/test/httpserver/test/test_start_stop.js @@ -0,0 +1,189 @@ +/* -*- 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/. */ + +/* + * Tests for correct behavior of the server start() and stop() methods. + */ + +XPCOMUtils.defineLazyGetter(this, "PORT", function() { + return srv.identity.primaryPort; +}); + +XPCOMUtils.defineLazyGetter(this, "PREPATH", function() { + return "http://localhost:" + PORT; +}); + +var srv, srv2; + +function run_test() +{ + if (mozinfo.os == "win") + { + dumpn("*** not running test_start_stop.js on Windows for now, because " + + "Windows is dumb"); + return; + } + + dumpn("*** run_test"); + + srv = createServer(); + srv.start(-1); + + try + { + srv.start(PORT); + do_throw("starting a started server"); + } + catch (e) + { + isException(e, Cr.NS_ERROR_ALREADY_INITIALIZED); + } + + try + { + srv.stop(); + do_throw("missing argument to stop"); + } + catch (e) + { + isException(e, Cr.NS_ERROR_NULL_POINTER); + } + + try + { + srv.stop(null); + do_throw("null argument to stop"); + } + catch (e) + { + isException(e, Cr.NS_ERROR_NULL_POINTER); + } + + do_test_pending(); + srv.stop(function() + { + try + { + do_test_pending(); + run_test_2(); + } + finally + { + do_test_finished(); + } + }); +} + +function run_test_2() +{ + dumpn("*** run_test_2"); + + do_test_finished(); + + srv.start(PORT); + srv2 = createServer(); + + try + { + srv2.start(PORT); + do_throw("two servers on one port?"); + } + catch (e) + { + isException(e, Cr.NS_ERROR_NOT_AVAILABLE); + } + + do_test_pending(); + try + { + srv.stop({onStopped: function() + { + try + { + do_test_pending(); + run_test_3(); + } + finally + { + do_test_finished(); + } + } + }); + } + catch (e) + { + do_throw("error stopping with an object: " + e); + } +} + +function run_test_3() +{ + dumpn("*** run_test_3"); + + do_test_finished(); + + srv.registerPathHandler("/handle", handle); + srv.start(PORT); + + // Don't rely on the exact (but implementation-constant) sequence of events + // as it currently exists by making either run_test_4 or serverStopped handle + // the final shutdown. + do_test_pending(); + + runHttpTests([new Test(PREPATH + "/handle")], run_test_4); +} + +var testsComplete = false; + +function run_test_4() +{ + dumpn("*** run_test_4"); + + testsComplete = true; + if (stopped) + do_test_finished(); +} + + +const INTERVAL = 500; + +function handle(request, response) +{ + response.processAsync(); + + dumpn("*** stopping server..."); + srv.stop(serverStopped); + + callLater(INTERVAL, function() + { + do_check_false(stopped); + + callLater(INTERVAL, function() + { + do_check_false(stopped); + response.finish(); + + try + { + response.processAsync(); + do_throw("late processAsync didn't throw?"); + } + catch (e) + { + isException(e, Cr.NS_ERROR_UNEXPECTED); + } + }); + }); +} + +var stopped = false; +function serverStopped() +{ + dumpn("*** server really, fully shut down now"); + stopped = true; + if (testsComplete) + do_test_finished(); +} diff --git a/netwerk/test/httpserver/test/xpcshell.ini b/netwerk/test/httpserver/test/xpcshell.ini new file mode 100644 index 0000000000..7890917a6b --- /dev/null +++ b/netwerk/test/httpserver/test/xpcshell.ini @@ -0,0 +1,37 @@ +[DEFAULT] +head = head_utils.js +tail = +support-files = data/** ../httpd.js + +[test_async_response_sending.js] +[test_basic_functionality.js] +[test_body_length.js] +[test_byte_range.js] +[test_cern_meta.js] +[test_default_index_handler.js] +[test_empty_body.js] +[test_errorhandler_exception.js] +[test_header_array.js] +[test_headers.js] +[test_host.js] +run-sequentially = Reusing same server on different specific ports. +[test_linedata.js] +[test_load_module.js] +[test_name_scheme.js] +[test_processasync.js] +[test_qi.js] +[test_registerdirectory.js] +[test_registerfile.js] +[test_registerprefix.js] +[test_request_line_split_in_two_packets.js] +[test_response_write.js] +[test_seizepower.js] +[test_setindexhandler.js] +[test_setstatusline.js] +[test_sjs.js] +[test_sjs_object_state.js] +[test_sjs_state.js] +[test_sjs_throwing_exceptions.js] +# Bug 1063533: frequent timeouts on Android 4.3 +skip-if = os == "android" +[test_start_stop.js] diff --git a/netwerk/test/mochitests/empty.html b/netwerk/test/mochitests/empty.html new file mode 100644 index 0000000000..e60f5abdf4 --- /dev/null +++ b/netwerk/test/mochitests/empty.html @@ -0,0 +1,16 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> + +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + This page does nothing. If the loading page managed to load this, the test + probably succeeded. +</body> +</html> diff --git a/netwerk/test/mochitests/file_loadinfo_redirectchain.sjs b/netwerk/test/mochitests/file_loadinfo_redirectchain.sjs new file mode 100644 index 0000000000..88cacbdb97 --- /dev/null +++ b/netwerk/test/mochitests/file_loadinfo_redirectchain.sjs @@ -0,0 +1,106 @@ +/* + * Redirect handler specifically for the needs of: + * Bug 1194052 - Append Principal to RedirectChain within LoadInfo before the channel is succesfully openend + */ + +function createIframeContent(aQuery) { + + var content =` + <!DOCTYPE HTML> + <html> + <head><meta charset="utf-8"> + <title>Bug 1194052 - LoadInfo redirect chain subtest</title> + </head> + <body> + <script type="text/javascript"> + var myXHR = new XMLHttpRequest(); + myXHR.open("GET", "http://example.com/tests/netwerk/test/mochitests/file_loadinfo_redirectchain.sjs?` + aQuery + `"); + myXHR.onload = function() { + var loadinfo = SpecialPowers.wrap(myXHR).channel.loadInfo; + var redirectChain = loadinfo.redirectChain; + var redirectChainIncludingInternalRedirects = loadinfo.redirectChainIncludingInternalRedirects; + var resultOBJ = { redirectChain : [], redirectChainIncludingInternalRedirects : [] }; + for (var i = 0; i < redirectChain.length; i++) { + resultOBJ.redirectChain.push(redirectChain[i].URI.spec); + } + for (var i = 0; i < redirectChainIncludingInternalRedirects.length; i++) { + resultOBJ.redirectChainIncludingInternalRedirects.push(redirectChainIncludingInternalRedirects[i].URI.spec); + } + var loadinfoJSON = JSON.stringify(resultOBJ); + window.parent.postMessage({ loadinfo: loadinfoJSON }, "*"); + } + myXHR.onerror = function() { + var resultOBJ = { redirectChain : [], redirectChainIncludingInternalRedirects : [] }; + var loadinfoJSON = JSON.stringify(resultOBJ); + window.parent.postMessage({ loadinfo: loadinfoJSON }, "*"); + } + myXHR.send(); + </script> + </body> + </html>`; + + return content; +} + +function handleRequest(request, response) +{ + response.setHeader("Cache-Control", "no-cache", false); + var queryString = request.queryString; + + if (queryString == "iframe-redir-https-2" || + queryString == "iframe-redir-err-2") { + var query = queryString.replace("iframe-", ""); + // send upgrade-insecure-requests CSP header + response.setHeader("Content-Type", "text/html", false); + response.setHeader("Content-Security-Policy", "upgrade-insecure-requests", false); + response.write(createIframeContent(query)); + return; + } + + // at the end of the redirectchain we return some text + // for sanity checking + if (queryString == "redir-0" || + queryString == "redir-https-0") { + response.setHeader("Content-Type", "text/html", false); + response.write("checking redirectchain"); + return; + } + + // special case redir-err-1 and return an error to trigger the fallback + if (queryString == "redir-err-1") { + response.setStatusLine("1.1", 404, "Bad request"); + return; + } + + // must be a redirect + var newLoaction = ""; + switch (queryString) { + case "redir-err-2": + newLocation = + "http://example.com/tests/netwerk/test/mochitests/file_loadinfo_redirectchain.sjs?redir-err-1"; + break; + + case "redir-https-2": + newLocation = + "http://example.com/tests/netwerk/test/mochitests/file_loadinfo_redirectchain.sjs?redir-https-1"; + break; + + case "redir-https-1": + newLocation = + "http://example.com/tests/netwerk/test/mochitests/file_loadinfo_redirectchain.sjs?redir-https-0"; + break; + + case "redir-2": + newLocation = + "http://mochi.test:8888/tests/netwerk/test/mochitests/file_loadinfo_redirectchain.sjs?redir-1"; + break; + + case "redir-1": + newLocation = + "http://mochi.test:8888/tests/netwerk/test/mochitests/file_loadinfo_redirectchain.sjs?redir-0"; + break; + } + + response.setStatusLine("1.1", 302, "Found"); + response.setHeader("Location", newLocation, false); +} diff --git a/netwerk/test/mochitests/method.sjs b/netwerk/test/mochitests/method.sjs new file mode 100644 index 0000000000..386c15d794 --- /dev/null +++ b/netwerk/test/mochitests/method.sjs @@ -0,0 +1,12 @@ + +function handleRequest(request, response) +{ + // avoid confusing cache behaviors + response.setHeader("Cache-Control", "no-cache", false); + response.setHeader("Content-Type", "text/plain", false); + response.setHeader("Access-Control-Allow-Origin", "*", false); + + // echo method + response.write(request.method); +} + diff --git a/netwerk/test/mochitests/mochitest.ini b/netwerk/test/mochitests/mochitest.ini new file mode 100644 index 0000000000..98d406c803 --- /dev/null +++ b/netwerk/test/mochitests/mochitest.ini @@ -0,0 +1,26 @@ +[DEFAULT] +support-files = + method.sjs + partial_content.sjs + rel_preconnect.sjs + user_agent.sjs + user_agent_update.sjs + web_packaged_app.sjs + file_loadinfo_redirectchain.sjs + redirect_idn.html^headers^ + redirect_idn.html + empty.html + redirect.sjs + +[test_arraybufferinputstream.html] +[test_idn_redirect.html] +[test_loadinfo_redirectchain.html] +[test_partially_cached_content.html] +[test_rel_preconnect.html] +[test_redirect_ref.html] +[test_uri_scheme.html] +[test_user_agent_overrides.html] +[test_user_agent_updates.html] +[test_user_agent_updates_reset.html] +[test_viewsource_unlinkable.html] +[test_xhr_method_case.html] diff --git a/netwerk/test/mochitests/partial_content.sjs b/netwerk/test/mochitests/partial_content.sjs new file mode 100644 index 0000000000..5fd3443f10 --- /dev/null +++ b/netwerk/test/mochitests/partial_content.sjs @@ -0,0 +1,151 @@ +/* -*- Mode: C++; 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/. */ + +/* Debug and Error wrapper functions for dump(). + */ +function ERR(response, responseCode, responseCodeStr, msg) +{ + // Reset state var. + setState("expectedRequestType", ""); + // Dump to console log and send to client in response. + dump("SERVER ERROR: " + msg + "\n"); + response.write("HTTP/1.1 " + responseCode + " " + responseCodeStr + "\r\n"); + response.write("Content-Type: text/html; charset=UTF-8\r\n"); + response.write("Content-Length: " + msg.length + "\r\n"); + response.write("\r\n"); + response.write(msg); +} + +function DBG(msg) +{ + // Dump to console only. + dump("SERVER DEBUG: " + msg + "\n"); +} + +/* Delivers content in parts to test partially cached content: requires two + * requests for the same resource. + * + * First call will respond with partial content, but a 200 header and + * Content-Length equal to the full content length. No Range or If-Range + * headers are allowed in the request. + * + * Second call will require Range and If-Range in the request headers, and + * will respond with the range requested. + */ +function handleRequest(request, response) +{ + DBG("Trying to seize power"); + response.seizePower(); + + DBG("About to check state vars"); + // Get state var to determine if this is the first or second request. + var expectedRequestType; + var lastModified; + if (getState("expectedRequestType") === "") { + DBG("First call: Should be requesting full content."); + expectedRequestType = "fullRequest"; + // Set state var for second request. + setState("expectedRequestType", "partialRequest"); + // Create lastModified variable for responses. + lastModified = (new Date()).toUTCString(); + setState("lastModified", lastModified); + } else if (getState("expectedRequestType") === "partialRequest") { + DBG("Second call: Should be requesting undelivered content."); + expectedRequestType = "partialRequest"; + // Reset state var for first request. + setState("expectedRequestType", ""); + // Get last modified date and reset state var. + lastModified = getState("lastModified"); + } else { + ERR(response, 500, "Internal Server Error", + "Invalid expectedRequestType \"" + expectedRequestType + "\"in " + + "server state db."); + return; + } + + // Look for Range and If-Range + var range = request.hasHeader("Range") ? request.getHeader("Range") : ""; + var ifRange = request.hasHeader("If-Range") ? request.getHeader("If-Range") : ""; + + if (expectedRequestType === "fullRequest") { + // Should not have Range or If-Range in first request. + if (range && range.length > 0) { + ERR(response, 400, "Bad Request", + "Should not receive \"Range: " + range + "\" for first, full request."); + return; + } + if (ifRange && ifRange.length > 0) { + ERR(response, 400, "Bad Request", + "Should not receive \"Range: " + range + "\" for first, full request."); + return; + } + } else if (expectedRequestType === "partialRequest") { + // Range AND If-Range should both be present in second request. + if (!range) { + ERR(response, 400, "Bad Request", + "Should receive \"Range: \" for second, partial request."); + return; + } + if (!ifRange) { + ERR(response, 400, "Bad Request", + "Should receive \"If-Range: \" for second, partial request."); + return; + } + } else { + // Somewhat redundant, but a check for errors in this test code. + ERR(response, 500, "Internal Server Error", + "expectedRequestType not set correctly: \"" + expectedRequestType + "\""); + return; + } + + // Prepare content in two parts for responses. + var partialContent = "<html><head></head><body><p id=\"firstResponse\">" + + "First response</p>"; + var remainderContent = "<p id=\"secondResponse\">Second response</p>" + + "</body></html>"; + var totalLength = partialContent.length + remainderContent.length; + + DBG("totalLength: " + totalLength); + + // Prepare common headers for the two responses. + date = new Date(); + DBG("Date: " + date.toUTCString() + ", Last-Modified: " + lastModified); + var commonHeaders = "Date: " + date.toUTCString() + "\r\n" + + "Last-Modified: " + lastModified + "\r\n" + + "Content-Type: text/html; charset=UTF-8\r\n" + + "ETag: abcd0123\r\n" + + "Accept-Ranges: bytes\r\n"; + + + // Prepare specific headers and content for first and second responses. + if (expectedRequestType === "fullRequest") { + DBG("First response: Sending partial content with a full header"); + response.write("HTTP/1.1 200 OK\r\n"); + response.write(commonHeaders); + // Set Content-Length to full length of resource. + response.write("Content-Length: " + totalLength + "\r\n"); + response.write("\r\n"); + response.write(partialContent); + } else if (expectedRequestType === "partialRequest") { + DBG("Second response: Sending remaining content with a range header"); + response.write("HTTP/1.1 206 Partial Content\r\n"); + response.write(commonHeaders); + // Set Content-Length to length of bytes transmitted. + response.write("Content-Length: " + remainderContent.length + "\r\n"); + response.write("Content-Range: bytes " + partialContent.length + "-" + + (totalLength - 1) + "/" + totalLength + "\r\n"); + response.write("\r\n"); + response.write(remainderContent); + } else { + // Somewhat redundant, but a check for errors in this test code. + ERR(response, 500, "Internal Server Error", + "Something very bad happened here: expectedRequestType is invalid " + + "towards the end of handleRequest! - \"" + expectedRequestType + "\""); + return; + } + + response.finish(); +} diff --git a/netwerk/test/mochitests/redirect.sjs b/netwerk/test/mochitests/redirect.sjs new file mode 100644 index 0000000000..e36f7c99ae --- /dev/null +++ b/netwerk/test/mochitests/redirect.sjs @@ -0,0 +1,8 @@ +var Cc = Components.classes; +var Ci = Components.interfaces; +var Cu = Components.utils; + +function handleRequest(request, response) { + response.setStatusLine(request.httpVersion, 301, "Moved Permanently"); + response.setHeader("Location", "empty.html#"); +} diff --git a/netwerk/test/mochitests/redirect_idn.html b/netwerk/test/mochitests/redirect_idn.html new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/netwerk/test/mochitests/redirect_idn.html diff --git a/netwerk/test/mochitests/redirect_idn.html^headers^ b/netwerk/test/mochitests/redirect_idn.html^headers^ new file mode 100644 index 0000000000..753f65db87 --- /dev/null +++ b/netwerk/test/mochitests/redirect_idn.html^headers^ @@ -0,0 +1,3 @@ +HTTP 301 Moved Permanently +Location: http://exämple.test/tests/netwerk/test/mochitests/empty.html +X-Comment: Bug 1142083 - This is a redirect to http://exämple.test diff --git a/netwerk/test/mochitests/rel_preconnect.sjs b/netwerk/test/mochitests/rel_preconnect.sjs new file mode 100644 index 0000000000..9b5f5a69ce --- /dev/null +++ b/netwerk/test/mochitests/rel_preconnect.sjs @@ -0,0 +1,15 @@ +// Generate response header "Link: <HREF>; rel=preconnect" +// HREF is provided by the request header X-Link + +function handleRequest(request, response) +{ + response.setHeader("Cache-Control", "no-cache", false); + response.setHeader("Link", "<" + + request.queryString + + ">; rel=preconnect" + ", " + + "<" + + request.queryString + + ">; rel=preconnect; crossOrigin=anonymous"); + response.write("check that header"); +} + diff --git a/netwerk/test/mochitests/signed_web_packaged_app.sjs b/netwerk/test/mochitests/signed_web_packaged_app.sjs new file mode 100644 index 0000000000..6bcac6ffd4 --- /dev/null +++ b/netwerk/test/mochitests/signed_web_packaged_app.sjs @@ -0,0 +1,78 @@ +var Cc = Components.classes; +var Ci = Components.interfaces; +var Cu = Components.utils; + +function handleRequest(request, response) +{ + response.setHeader("Content-Type", "application/package", false); + response.write(signedPackage); +} + +// The package content +// getData formats it as described at http://www.w3.org/TR/web-packaging/#streamable-package-format +var signedPackage = `manifest-signature: MIIF1AYJKoZIhvcNAQcCoIIFxTCCBcECAQExCzAJBgUrDgMCGgUAMAsGCSqGSIb3DQEHAaCCA54wggOaMIICgqADAgECAgEEMA0GCSqGSIb3DQEBCwUAMHMxCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJDQTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEkMCIGA1UEChMbRXhhbXBsZSBUcnVzdGVkIENvcnBvcmF0aW9uMRkwFwYDVQQDExBUcnVzdGVkIFZhbGlkIENBMB4XDTE1MTExOTAzMDEwNVoXDTM1MTExOTAzMDEwNVowdDELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAkNBMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MSQwIgYDVQQKExtFeGFtcGxlIFRydXN0ZWQgQ29ycG9yYXRpb24xGjAYBgNVBAMTEVRydXN0ZWQgQ29ycCBDZXJ0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzPback9X7RRxKTc3/5o2vm9Ro6XNiSM9NPsN3djjCIVz50bY0rJkP98zsqpFjnLwqHeJigxyYoqFexRhRLgKrG10CxNl4rxP6CEPENjvj5FfbX/HUZpT/DelNR18F498yD95vSHcSrCc3JrjV3bKA+wgt11E4a0Ba95S1RuwtehZw1+Y4hO8nHpbSGfjD0BpluFY2nDoYAm+aWSrsmLuJsKLO8Xn2I1brZFJUynR3q1ujuDE9EJk1niDLfOeVgXM4AavJS5C0ZBHhAhR2W+K9NN97jpkpmHFqecTwDXB7rEhsyB3185cI7anaaQfHHfH5+4SD+cMDNtYIOSgLO06ZwIDAQABozgwNjAMBgNVHRMBAf8EAjAAMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMDMA4GA1UdDwEB/wQEAwIHgDANBgkqhkiG9w0BAQsFAAOCAQEAlnVyLz5dPhS0ZhZD6qJOUzSo6nFwMxNX1m0oS37mevtuh0b0o1gmEuMw3mVxiAVkC2vPsoxBL2wLlAkcEdBPxGEqhBmtiBY3F3DgvEkf+/sOY1rnr6O1qLZuBAnPzA1Vnco8Jwf0DYF0PxaRd8yT5XSl5qGpM2DItEldZwuKKaL94UEgIeC2c+Uv/IOyrv+EyftX96vcmRwr8ghPFLQ+36H5nuAKEpDD170EvfWl1zs0dUPiqSI6l+hy5V14gl63Woi34L727+FKx8oatbyZtdvbeeOmenfTLifLomnZdx+3WMLkp3TLlHa5xDLwifvZtBP2d3c6zHp7gdrGU1u2WTGCAf4wggH6AgEBMHgwczELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAkNBMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MSQwIgYDVQQKExtFeGFtcGxlIFRydXN0ZWQgQ29ycG9yYXRpb24xGTAXBgNVBAMTEFRydXN0ZWQgVmFsaWQgQ0ECAQQwCQYFKw4DAhoFAKBdMBgGCSqGSIb3DQEJAzELBgkqhkiG9w0BBwEwHAYJKoZIhvcNAQkFMQ8XDTE1MTEyNTAzMDQzMFowIwYJKoZIhvcNAQkEMRYEFD4ut4oKoYdcGzyfQE6ROeazv+uNMA0GCSqGSIb3DQEBAQUABIIBAFG99dKBSOzQmYVn6lHKWERVDtYXbDTIVF957ID8YH9B5unlX/PdludTNbP5dzn8GWQV08tNRgoXQ5sgxjifHunrpaR1WiR6XqvwOCBeA5NB688jxGNxth6zg6fCGFaynsYMX3FlglfIW+AYwyQUclbv+C4UORJpBjvuknOnK+UDBLVSoP9ivL6KhylYna3oFcs0SMsumc/jf/oQW51LzFHpn61TRUqdDgvGhwcjgphMhKj23KwkjwRspU2oIWNRAuhZgqDD5BJlNniCr9X5Hx1dW6tIVISO91CLAryYkGZKRJYekXctCpIvldUkIDeh2tAw5owr0jtsVd6ovFF3bV4=\r +--NKWXJUAFXB\r +Content-Location: manifest.webapp\r +Content-Type: application/x-web-app-manifest+json\r +\r +{ + "moz-package-origin": "http://mochi.test:8888", + "name": "My App", + "moz-resources": [ + { + "src": "page2.html", + "integrity": "JREF3JbXGvZ+I1KHtoz3f46ZkeIPrvXtG4VyFQrJ7II=" + }, + { + "src": "index.html", + "integrity": "Jkvco7U8WOY9s0YREsPouX+DWK7FWlgZwA0iYYSrb7Q=" + }, + { + "src": "scripts/script.js", + "integrity": "6TqtNArQKrrsXEQWu3D9ZD8xvDRIkhyV6zVdTcmsT5Q=" + }, + { + "src": "scripts/library.js", + "integrity": "TN2ByXZiaBiBCvS4MeZ02UyNi44vED+KjdjLInUl4o8=" + } + ], + "moz-permissions": [ + { + "systemXHR": { + "description": "Needed to download stuff" + } + } + ], + "package-identifier": "09bc9714-7ab6-4320-9d20-fde4c237522c", + "description": "A great app!" +}\r +--NKWXJUAFXB\r +Content-Location: page2.html\r +Content-Type: text/html\r +\r +<html> + page2.html +</html> +\r +--NKWXJUAFXB\r +Content-Location: index.html\r +Content-Type: text/html\r +\r +<html> + Last updated: 2015/10/28 + <iframe id="innerFrame" src="page2.html"></iframe> +</html> +\r +--NKWXJUAFXB\r +Content-Location: scripts/script.js\r +Content-Type: text/javascript\r +\r +// script.js +\r +--NKWXJUAFXB\r +Content-Location: scripts/library.js\r +Content-Type: text/javascript\r +\r +// library.js +\r +--NKWXJUAFXB--`; diff --git a/netwerk/test/mochitests/test_arraybufferinputstream.html b/netwerk/test/mochitests/test_arraybufferinputstream.html new file mode 100644 index 0000000000..eb7796ef2d --- /dev/null +++ b/netwerk/test/mochitests/test_arraybufferinputstream.html @@ -0,0 +1,89 @@ +<!DOCTYPE HTML> +<html> +<!-- +--> +<head> + <title>ArrayBuffer stream test</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + +<script type="text/javascript"> +function detachArrayBuffer(ab) +{ + var w = new Worker("data:application/javascript,"); + w.postMessage(ab, [ab]); +} + +function test() +{ + var ab = new ArrayBuffer(4000); + var ta = new Uint8Array(ab); + ta[0] = 'a'.charCodeAt(0); + ta[1] = 'b'.charCodeAt(0); + + const Cc = SpecialPowers.Cc, Ci = SpecialPowers.Ci, Cr = SpecialPowers.Cr; + var abis = Cc["@mozilla.org/io/arraybuffer-input-stream;1"] + .createInstance(Ci.nsIArrayBufferInputStream); + + var sis = Cc["@mozilla.org/scriptableinputstream;1"] + .createInstance(Ci.nsIScriptableInputStream); + sis.init(abis); + + is(sis.read(1), "", "should read no data from an uninitialized ABIS"); + + abis.setData(ab, 0, 256 * 1024); + + is(sis.read(1), "a", "should read 'a' after init"); + + detachArrayBuffer(ab); + + SpecialPowers.forceGC(); + SpecialPowers.forceGC(); + + try + { + is(sis.read(1), "b", "should read 'b' after detaching buffer"); + } + catch (e) + { + ok(false, "reading from stream should have worked"); + } + + // A regression test for bug 1265076. Previously, overflowing + // the internal buffer from readSegments would cause incorrect + // copying. The constant mirrors the value in + // ArrayBufferInputStream::readSegments. + var size = 8192; + ab = new ArrayBuffer(2 * size); + ta = new Uint8Array(ab); + + var i; + for (i = 0; i < size; ++i) { + ta[i] = 'x'.charCodeAt(0); + } + for (i = 0; i < size; ++i) { + ta[size + i] = 'y'.charCodeAt(0); + } + + abis = Cc["@mozilla.org/io/arraybuffer-input-stream;1"] + .createInstance(Ci.nsIArrayBufferInputStream); + abis.setData(ab, 0, 2 * size); + + sis = Cc["@mozilla.org/scriptableinputstream;1"] + .createInstance(Ci.nsIScriptableInputStream); + sis.init(abis); + + var result = sis.read(2 * size); + is(result, "x".repeat(size) + "y".repeat(size), "correctly read the data"); +} + +test(); +</script> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/netwerk/test/mochitests/test_idn_redirect.html b/netwerk/test/mochitests/test_idn_redirect.html new file mode 100644 index 0000000000..b9a594cc56 --- /dev/null +++ b/netwerk/test/mochitests/test_idn_redirect.html @@ -0,0 +1,36 @@ +<!DOCTYPE HTML> +<html> +<!-- + Bug 1142083 - IDN Unicode domain redirect is broken + This test loads redirectme.html which is redirected simple_test.html, on a different IDN domain. + A message is posted to that page, with responds with another. + Upon receiving that message, we consider that the IDN redirect has functioned properly, since the intended page was loaded. +--> +<head> + <title>Test for URI Manipulation</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + +<pre id="test"> +<script type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); + +var iframe = document.createElement("iframe"); +iframe.src = "about:blank"; +iframe.addEventListener("load", finishTest); +document.body.appendChild(iframe); +iframe.src = "http://mochi.test:8888/tests/netwerk/test/mochitests/redirect_idn.html"; + +function finishTest(e) { + ok(true); + SimpleTest.finish(); +} + +</script> + +</body> +</html> + diff --git a/netwerk/test/mochitests/test_loadinfo_redirectchain.html b/netwerk/test/mochitests/test_loadinfo_redirectchain.html new file mode 100644 index 0000000000..4568db0c2b --- /dev/null +++ b/netwerk/test/mochitests/test_loadinfo_redirectchain.html @@ -0,0 +1,213 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1194052 - Append Principal to RedirectChain within LoadInfo before the channel is succesfully openend</title> + <!-- Including SimpleTest.js so we can use waitForExplicitFinish !--> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<iframe style="width:100%;" id="testframe"></iframe> + +<script class="testbody" type="text/javascript"> + +/* + * We perform the following tests on the redirectchain of the loadinfo: + * (1) checkLoadInfoWithoutRedirects: + * checks the length of the redirectchain and tries to pop an element + * which should result in an exception and not a crash. + * (2) checkLoadInfoWithTwoRedirects: + * perform two redirects and confirm that both redirect chains + * contain the redirected URIs. + * (3) checkLoadInfoWithInternalRedirects: + * perform two redirects including CSPs upgrade-insecure-requests + * so that the redirectchain which includes internal redirects differs. + * (4) checkLoadInfoWithInternalRedirectsAndFallback + * perform two redirects including CSPs upgrade-insecure-requests + * including a 404 repsonse and hence a fallback. + */ + +SimpleTest.waitForExplicitFinish(); + +// *************** TEST 1 *************** + +function checkLoadInfoWithoutRedirects() { + var myXHR = new XMLHttpRequest(); + myXHR.open("GET", "http://mochi.test:8888/tests/netwerk/test/mochitests/file_loadinfo_redirectchain.sjs?redir-0"); + + myXHR.onload = function() { + var loadinfo = SpecialPowers.wrap(myXHR).channel.loadInfo; + var redirectChain = loadinfo.redirectChain; + var redirectChainIncludingInternalRedirects = loadinfo.redirectChainIncludingInternalRedirects; + + is(redirectChain.length, 0, "no redirect, length should be 0"); + is(redirectChainIncludingInternalRedirects.length, 0, "no redirect, length should be 0"); + is(myXHR.responseText, "checking redirectchain", "sanity check to make sure redirects succeeded"); + + // try to pop an element from redirectChain + try { + loadInfo.popRedirectedPrincipal(false); + ok(false, "should not be possible to pop from redirectChain"); + } + catch(e) { + ok(true, "popping element from empty redirectChain should throw"); + } + + // try to pop an element from redirectChainIncludingInternalRedirects + try { + loadInfo.popRedirectedPrincipal(true); + ok(false, "should not be possible to pop from redirectChainIncludingInternalRedirects"); + } + catch(e) { + ok(true, "popping element from empty redirectChainIncludingInternalRedirects should throw"); + } + // move on to the next test + checkLoadInfoWithTwoRedirects(); + } + myXHR.onerror = function() { + ok(false, "xhr problem within checkLoadInfoWithoutRedirect()"); + } + myXHR.send(); +} + +// *************** TEST 2 *************** + +function checkLoadInfoWithTwoRedirects() { + var myXHR = new XMLHttpRequest(); + myXHR.open("GET", "http://mochi.test:8888/tests/netwerk/test/mochitests/file_loadinfo_redirectchain.sjs?redir-2"); + + const EXPECTED_REDIRECT_CHAIN = [ + "http://mochi.test:8888/tests/netwerk/test/mochitests/file_loadinfo_redirectchain.sjs?redir-2", + "http://mochi.test:8888/tests/netwerk/test/mochitests/file_loadinfo_redirectchain.sjs?redir-1" + ]; + + myXHR.onload = function() { + is(myXHR.responseText, "checking redirectchain", "sanity check to make sure redirects succeeded"); + + var loadinfo = SpecialPowers.wrap(myXHR).channel.loadInfo; + var redirectChain = loadinfo.redirectChain; + var redirectChainIncludingInternalRedirects = loadinfo.redirectChainIncludingInternalRedirects; + + is(redirectChain.length, + EXPECTED_REDIRECT_CHAIN.length, + "two redirects, chain should have length 2"); + is(redirectChainIncludingInternalRedirects.length, + EXPECTED_REDIRECT_CHAIN.length, + "two redirect, chainInternal should have length 2"); + + for (var i = 0; i < redirectChain.length; i++) { + is(redirectChain[i].URI.spec, + EXPECTED_REDIRECT_CHAIN[i], + "redirectChain at index [" + i + "] should match"); + is(redirectChainIncludingInternalRedirects[i].URI.spec, + EXPECTED_REDIRECT_CHAIN[i], + "redirectChainIncludingInternalRedirects at index [" + i + "] should match"); + } + + // move on to the next test + checkLoadInfoWithInternalRedirects(); + } + myXHR.onerror = function() { + ok(false, "xhr problem within checkLoadInfoWithTwoRedirects()"); + } + myXHR.send(); +} + +// ************** HELPERS *************** + +function compareChains(aLoadInfo, aExpectedRedirectChain, aExpectedRedirectChainIncludingInternalRedirects) { + + var redirectChain = aLoadInfo.redirectChain; + var redirectChainIncludingInternalRedirects = aLoadInfo.redirectChainIncludingInternalRedirects; + + is(redirectChain.length, + aExpectedRedirectChain.length, + "confirming length of redirectChain"); + + is(redirectChainIncludingInternalRedirects.length, + aExpectedRedirectChainIncludingInternalRedirects.length, + "confirming length of redirectChainIncludingInternalRedirects"); + + for (var i = 0; i < redirectChain.length; i++) { + is(redirectChain[i], + aExpectedRedirectChain[i], + "redirectChain at index [" + i + "] should match"); + } + + for (var i = 0; i < redirectChainIncludingInternalRedirects.length; i++) { + is(redirectChainIncludingInternalRedirects[i], + aExpectedRedirectChainIncludingInternalRedirects[i], + "redirectChainIncludingInternalRedirects at index [" + i + "] should match"); + } +} + +// *************** TEST 3 *************** + +function confirmCheckLoadInfoWithInternalRedirects(event) { + + const EXPECTED_REDIRECT_CHAIN = [ + "https://example.com/tests/netwerk/test/mochitests/file_loadinfo_redirectchain.sjs?redir-https-2", + "https://example.com/tests/netwerk/test/mochitests/file_loadinfo_redirectchain.sjs?redir-https-1" + ]; + + const EXPECTED_REDIRECT_CHAIN_INCLUDING_INTERNAL_REDIRECTS = [ + "http://example.com/tests/netwerk/test/mochitests/file_loadinfo_redirectchain.sjs?redir-https-2", + "https://example.com/tests/netwerk/test/mochitests/file_loadinfo_redirectchain.sjs?redir-https-2", + "http://example.com/tests/netwerk/test/mochitests/file_loadinfo_redirectchain.sjs?redir-https-1", + "https://example.com/tests/netwerk/test/mochitests/file_loadinfo_redirectchain.sjs?redir-https-1", + "http://example.com/tests/netwerk/test/mochitests/file_loadinfo_redirectchain.sjs?redir-https-0", + ]; + + var loadinfo = JSON.parse(event.data.loadinfo); + compareChains(loadinfo, EXPECTED_REDIRECT_CHAIN, EXPECTED_REDIRECT_CHAIN_INCLUDING_INTERNAL_REDIRECTS); + + // remove the postMessage listener and move on to the next test + window.removeEventListener("message", confirmCheckLoadInfoWithInternalRedirects, false); + checkLoadInfoWithInternalRedirectsAndFallback(); +} + +function checkLoadInfoWithInternalRedirects() { + // load the XHR request into an iframe so we can apply a CSP to the iframe + // a postMessage returns the result back to the main page. + window.addEventListener("message", confirmCheckLoadInfoWithInternalRedirects, false); + document.getElementById("testframe").src = + "https://example.com/tests/netwerk/test/mochitests/file_loadinfo_redirectchain.sjs?iframe-redir-https-2"; +} + +// *************** TEST 4 *************** + +function confirmCheckLoadInfoWithInternalRedirectsAndFallback(event) { + + var EXPECTED_REDIRECT_CHAIN = [ + "https://example.com/tests/netwerk/test/mochitests/file_loadinfo_redirectchain.sjs?redir-err-2", + ]; + + var EXPECTED_REDIRECT_CHAIN_INCLUDING_INTERNAL_REDIRECTS = [ + "http://example.com/tests/netwerk/test/mochitests/file_loadinfo_redirectchain.sjs?redir-err-2", + "https://example.com/tests/netwerk/test/mochitests/file_loadinfo_redirectchain.sjs?redir-err-2", + "http://example.com/tests/netwerk/test/mochitests/file_loadinfo_redirectchain.sjs?redir-err-1", + ]; + + var loadinfo = JSON.parse(event.data.loadinfo); + compareChains(loadinfo, EXPECTED_REDIRECT_CHAIN, EXPECTED_REDIRECT_CHAIN_INCLUDING_INTERNAL_REDIRECTS); + + // remove the postMessage listener and finish test + window.removeEventListener("message", confirmCheckLoadInfoWithInternalRedirectsAndFallback, false); + SimpleTest.finish(); +} + +function checkLoadInfoWithInternalRedirectsAndFallback() { + // load the XHR request into an iframe so we can apply a CSP to the iframe + // a postMessage returns the result back to the main page. + window.addEventListener("message", confirmCheckLoadInfoWithInternalRedirectsAndFallback, false); + document.getElementById("testframe").src = + "https://example.com/tests/netwerk/test/mochitests/file_loadinfo_redirectchain.sjs?iframe-redir-err-2"; +} + +// *************** START TESTS *************** + +checkLoadInfoWithoutRedirects(); + +</script> +</body> +</html> diff --git a/netwerk/test/mochitests/test_partially_cached_content.html b/netwerk/test/mochitests/test_partially_cached_content.html new file mode 100644 index 0000000000..65a56dfe5b --- /dev/null +++ b/netwerk/test/mochitests/test_partially_cached_content.html @@ -0,0 +1,92 @@ +<!DOCTYPE HTML> +<html> +<!-- + https://bugzilla.mozilla.org/show_bug.cgi?id=497003 + + This test verifies that partially cached content is read from the cache first + and then from the network. It is written in the mochitest framework to take + thread retargeting into consideration of nsIStreamListener callbacks (inc. + nsIRequestObserver). E.g. HTML5 Stream Parser requesting retargeting of + nsIStreamListener callbacks to the parser thread. +--> +<head> + <meta charset="UTF-8"> + <title>Test for Bug 497003: support sending OnDataAvailable() to other threads</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + <p><a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=497003">Mozilla Bug 497003: support sending OnDataAvailable() to other threads</a></p> + <p><iframe id="contentFrame" src="partial_content.sjs"></iframe></p> + +<pre id="test"> +<script> + + + +/* Check that the iframe has initial content only after the first load. + */ +function expectInitialContent(e) { + info("expectInitialContent", + "First response received: should have partial content"); + var frameElement = document.getElementById('contentFrame'); + var frameWindow = frameElement.contentWindow; + + // Expect "First response" in received HTML. + var firstResponse = frameWindow.document.getElementById('firstResponse'); + ok(firstResponse, "First response should exist"); + if (firstResponse) { + is(firstResponse.innerHTML, "First response", + "First response should be correct"); + } + + // Expect NOT to get any second response element. + var secondResponse = frameWindow.document.getElementById('secondResponse'); + ok(!secondResponse, "Should not get text for second response in first."); + + // Set up listener for second load. + removeEventListener("load", expectInitialContent, false); + frameElement.addEventListener("load", expectFullContent, false); + + // Reload. + frameElement.src="partial_content.sjs"; +} + +/* Check that the iframe has all the content after the second load. + */ +function expectFullContent(e) +{ + info("expectFullContent", + "Second response received: should complete content from first load"); + var frameWindow = document.getElementById('contentFrame').contentWindow; + + // Expect "First response" to still be there + var firstResponse = frameWindow.document.getElementById('firstResponse'); + ok(firstResponse, "First response should exist"); + if (firstResponse) { + is(firstResponse.innerHTML, "First response", + "First response should be correct"); + } + + // Expect "Second response" to be there also. + var secondResponse = frameWindow.document.getElementById('secondResponse'); + ok(secondResponse, "Second response should exist"); + if (secondResponse) { + is(secondResponse.innerHTML, "Second response", + "Second response should be correct"); + } + + SimpleTest.finish(); +} + +// Set listener for first load to expect partial content. +// Note: Set listener on the global object/window since 'load' should not fire +// for partially loaded content in an iframe. +addEventListener("load", expectInitialContent, false); + +SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> diff --git a/netwerk/test/mochitests/test_redirect_ref.html b/netwerk/test/mochitests/test_redirect_ref.html new file mode 100644 index 0000000000..40ea166589 --- /dev/null +++ b/netwerk/test/mochitests/test_redirect_ref.html @@ -0,0 +1,30 @@ +<!DOCTYPE HTML> +<html> +<head> + <title> Bug 1234575 - Test redirect ref</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + +<pre id="test"> +<script type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); + +var iframe = document.createElement("iframe"); +iframe.src = "about:blank"; +iframe.addEventListener("load", finishTest); +document.body.appendChild(iframe); +iframe.src = "redirect.sjs#start"; + +function finishTest(e) { + is(iframe.contentWindow.location.href, "http://mochi.test:8888/tests/netwerk/test/mochitests/empty.html#"); + SimpleTest.finish(); +} + +</script> + +</body> +</html> + diff --git a/netwerk/test/mochitests/test_rel_preconnect.html b/netwerk/test/mochitests/test_rel_preconnect.html new file mode 100644 index 0000000000..4e94fb4cee --- /dev/null +++ b/netwerk/test/mochitests/test_rel_preconnect.html @@ -0,0 +1,62 @@ +<!DOCTYPE HTML> +<html> +<!-- +--> +<head> + <title>Test for link rel=preconnect</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + +<script type="text/javascript"> +SimpleTest.waitForExplicitFinish(); + +const Cc = SpecialPowers.Cc, Ci = SpecialPowers.Ci, Cr = SpecialPowers.Cr; + +var remainder = 4; +var observer; + +function doTest() +{ + SpecialPowers.setBoolPref("network.http.debug-observations", true); + + observer = SpecialPowers.wrapCallback(function(subject, topic, data) { + remainder--; + ok(true, "observed remainder = " + remainder); + if (!remainder) { + SpecialPowers.removeObserver(observer, "speculative-connect-request"); + SpecialPowers.setBoolPref("network.http.debug-observations", false); + SimpleTest.finish(); + } + }); + SpecialPowers.addObserver(observer, "speculative-connect-request", false); + + // test the link rel=preconnect element in the head for both normal + // and crossOrigin=anonymous + var link = document.createElement("link"); + link.rel = "preconnect"; + link.href = "//localhost:8888"; + document.head.appendChild(link); + link = document.createElement("link"); + link.rel = "preconnect"; + link.href = "//localhost:8888"; + link.crossOrigin = "anonymous"; + document.head.appendChild(link); + + // test the http link response header - the test contains both a + // normal and anonymous preconnect link header + var iframe = document.createElement('iframe'); + iframe.src = 'rel_preconnect.sjs?//localhost:8888'; + + document.body.appendChild(iframe); +} + +</script> +</head> +<body onload="doTest();"> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"> +</pre> +</body> +</html> + diff --git a/netwerk/test/mochitests/test_uri_scheme.html b/netwerk/test/mochitests/test_uri_scheme.html new file mode 100644 index 0000000000..1f01a4d0a1 --- /dev/null +++ b/netwerk/test/mochitests/test_uri_scheme.html @@ -0,0 +1,51 @@ +<!DOCTYPE HTML> +<html> +<!-- +--> +<head> + <title>Test for URI Manipulation</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + +<script type="text/javascript"> +function dotest1() +{ + SimpleTest.waitForExplicitFinish(); + var o = new URL("http://localhost/"); + try { o.href = "foopy:bar:baz"; } catch(e) { } + o.protocol = "http:"; + o.hostname; + try { o.href = "http://localhost/"; } catch(e) { } + ok(o.protocol, "http:"); + dotest2(); +} + +function dotest2() +{ + var o = new URL("http://www.mozilla.org/"); + try { + o.href ="aaaaaaaaaaa:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"; + } catch(e) { } + o.hash = "#"; + o.pathname = "/"; + o.protocol = "http:"; + try { o.href = "http://localhost/"; } catch(e) { } + ok(o.protocol, "http:"); + dotest3(); +} + +function dotest3() +{ + is(new URL("resource://123/").href, "resource://123/"); + SimpleTest.finish(); +} +</script> +</head> +<body onload="dotest1();"> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"> +</pre> +</body> +</html> + diff --git a/netwerk/test/mochitests/test_user_agent_overrides.html b/netwerk/test/mochitests/test_user_agent_overrides.html new file mode 100644 index 0000000000..7396c35e13 --- /dev/null +++ b/netwerk/test/mochitests/test_user_agent_overrides.html @@ -0,0 +1,240 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=782453 +--> +<head> + <title>Test for User Agent Overrides</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=782453">Mozilla Bug 782453</a> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +const PREF_OVERRIDES_ENABLED = "general.useragent.site_specific_overrides"; +const PREF_OVERRIDES_BRANCH = "general.useragent.override."; + +const DEFAULT_UA = navigator.userAgent; + +const UA_WHOLE_OVERRIDE = "DummyUserAgent"; +const UA_WHOLE_EXPECTED = UA_WHOLE_OVERRIDE; + +const UA_PARTIAL_FROM = "\\wozilla"; // /\wozilla +const UA_PARTIAL_SEP = "#"; +const UA_PARTIAL_TO = UA_WHOLE_OVERRIDE; +const UA_PARTIAL_OVERRIDE = UA_PARTIAL_FROM + UA_PARTIAL_SEP + UA_PARTIAL_TO; +const UA_PARTIAL_EXPECTED = DEFAULT_UA.replace(new RegExp(UA_PARTIAL_FROM, 'g'), UA_PARTIAL_TO); + +function testUAIFrame(host, expected, sameQ, message, testNavQ, navSameQ, navMessage, callback) { + let url = location.pathname; + url = host + url.slice(0, url.lastIndexOf('/')) + '/user_agent.sjs'; + let ifr = document.createElement('IFRAME'); + + ifr.src = url; + + document.getElementById('content').appendChild(ifr); + + window.addEventListener("message", function recv(e) { + ok(sameQ == (e.data.header.indexOf(expected) != -1), message); + if (testNavQ) { + ok(navSameQ == (e.data.nav.indexOf(expected) != -1), navMessage); + } + window.removeEventListener("message", recv, false); + callback(); + }, false); + +} + +function testUAIFrameNoNav(host, expected, sameQ, message, callback) { + testUAIFrame(host, expected, sameQ, message, false, true, '', callback); +} + +function testUA(options, callback) { + var [domain, override, test_hosts, expected] = + [options.domain, options.override, options.test_hosts, options.expected]; + + (function nextTest() { + let test_host = test_hosts.shift(); + + info("Overriding " + domain + " with " + override + " for " + test_host); + + function is_subdomain(host) { + var [test_domain] = host.slice(host.lastIndexOf('/') + 1).split(':', 1); + return test_domain === domain || test_domain.endsWith('.' + domain); + } + + var localhost = location.origin; + var overrideNavigator = is_subdomain(localhost); + var navigator_ua, test_ua; + + if (overrideNavigator) { + navigator_ua = navigator.userAgent; + } + + let url = location.pathname; + url = test_host + url.slice(0, url.lastIndexOf('/')) + '/user_agent.sjs'; + let ifr = document.createElement('IFRAME'); + ifr.src = url; + + document.getElementById('content').appendChild(ifr); + + window.addEventListener("message", function recv(e) { + test_ua = e.data.header; + SpecialPowers.pushPrefEnv({ + set: [[PREF_OVERRIDES_BRANCH + domain, override]], + }, function () { + testUAIFrame(test_host, expected, true, 'Header UA not overridden at step ' + (++step), true, + true, 'Navigator UA not overridden at step ' + (++step), function () { + // clear the override pref to undo overriding the UA + SpecialPowers.pushPrefEnv({ + clear: [[PREF_OVERRIDES_BRANCH + domain]], + }, function () { + testUAIFrameNoNav(test_host, test_ua, true, 'Header UA not restored at step ' + (++step), function() { + test_hosts.length ? nextTest() : callback(); + }); + }); + }); + }); + window.removeEventListener("message", recv, false); + }, false); + })(); +} + +var step = 0; // for logging +var tests = [ + // should override both header and navigator.userAgent + { + domain: location.hostname, + override: UA_WHOLE_OVERRIDE, + test_hosts: [location.origin], + expected: UA_WHOLE_EXPECTED + }, + + // should support partial overrides + { + domain: location.hostname, + override: UA_PARTIAL_OVERRIDE, + test_hosts: [location.origin], + expected: UA_PARTIAL_EXPECTED + }, + + // should match domain and subdomains + { + domain: 'example.org', + override: UA_WHOLE_OVERRIDE, + test_hosts: ['http://example.org', + 'http://test1.example.org', + 'http://sub1.test1.example.org'], + expected: UA_WHOLE_EXPECTED + }, + + // should not match superdomains + { + domain: 'sub1.test1.example.org', + override: UA_WHOLE_OVERRIDE, + test_hosts: ['http://example.org', + 'http://test1.example.org'], + expected: DEFAULT_UA + }, + + // should work with https + { + domain: 'example.com', + override: UA_WHOLE_OVERRIDE, + test_hosts: ['https://example.com', + 'https://test1.example.com', + 'https://sub1.test1.example.com'], + expected: UA_WHOLE_EXPECTED + }, +]; + +// test that UA is not overridden when the 'site_specific_overrides' pref is off +function testInactive(callback) { + SpecialPowers.pushPrefEnv({ + set: [ + [PREF_OVERRIDES_ENABLED, false], + [PREF_OVERRIDES_BRANCH + location.hostname, UA_WHOLE_OVERRIDE] + ] + }, function () { + testUAIFrame(location.origin, UA_WHOLE_OVERRIDE, false, 'Failed to disabled header UA override at step ' + (++step), + true, false, 'Failed to disable navigator UA override at step + ' + (++step), function () { + SpecialPowers.pushPrefEnv({ + clear: [ + [PREF_OVERRIDES_ENABLED], + [PREF_OVERRIDES_BRANCH + location.hostname] + ] + }, callback); + }); + }); +} + +function testPriority(callback) { + // foo.bar.com override should have priority over bar.com override + var tests = [ + ['example.org', 'test1.example.org', 'sub1.test1.example.org'], + ['example.org', 'test1.example.org', 'sub2.test1.example.org'], + ['example.org', 'test2.example.org', 'sub1.test2.example.org'], + ['example.org', 'test2.example.org', 'sub2.test2.example.org'], + ]; + (function nextTest() { + var [level0, level1, level2] = tests.shift(); + var host = 'http://' + level2; + SpecialPowers.pushPrefEnv({ + set: [ + [PREF_OVERRIDES_ENABLED, true], + [PREF_OVERRIDES_BRANCH + level1, UA_WHOLE_OVERRIDE] + ] + }, function () { + // should use first override at this point + testUAIFrameNoNav(host, UA_WHOLE_EXPECTED, true, 'UA not overridden at step ' + (++step), function() { + // add a second override that should be used + testUA({ + domain: level2, + override: UA_PARTIAL_OVERRIDE, + test_hosts: [host], + expected: UA_PARTIAL_EXPECTED + }, function () { + // add a third override that should not be used + testUA({ + domain: level0, + override: UA_PARTIAL_OVERRIDE, + test_hosts: [host], + expected: UA_WHOLE_EXPECTED + }, tests.length ? nextTest : callback); + }); + }); + }); + })(); +} + +function testOverrides(callback) { + SpecialPowers.pushPrefEnv({ + set: [[PREF_OVERRIDES_ENABLED, true]] + }, function nextTest() { + testUA(tests.shift(), function() { tests.length ? nextTest() : callback() }); + }); +} + +SpecialPowers.loadChromeScript(_ => { + Components.utils.import("resource://gre/modules/UserAgentOverrides.jsm"); + UserAgentOverrides.init(); +}); + +SimpleTest.waitForExplicitFinish(); +SimpleTest.requestCompleteLog(); +SimpleTest.requestLongerTimeout(5); + +testOverrides(function() { + testInactive(function() { + testPriority(SimpleTest.finish) + }); +}); + +</script> +</pre> +</body> +</html> diff --git a/netwerk/test/mochitests/test_user_agent_updates.html b/netwerk/test/mochitests/test_user_agent_updates.html new file mode 100644 index 0000000000..839f9e0005 --- /dev/null +++ b/netwerk/test/mochitests/test_user_agent_updates.html @@ -0,0 +1,369 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=897221 +--> +<head> + <title>Test for User Agent Updates</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=897221">Mozilla Bug 897221</a> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +const PREF_APP_UPDATE_TIMERMINIMUMDELAY = "app.update.timerMinimumDelay"; +const PREF_UPDATES = "general.useragent.updates."; +const PREF_UPDATES_ENABLED = PREF_UPDATES + "enabled"; +const PREF_UPDATES_URL = PREF_UPDATES + "url"; +const PREF_UPDATES_INTERVAL = PREF_UPDATES + "interval"; +const PREF_UPDATES_TIMEOUT = PREF_UPDATES + "timeout"; + +const DEFAULT_UA = navigator.userAgent; +const UA_OVERRIDE = "DummyUserAgent"; +const UA_ALT_OVERRIDE = "AltUserAgent"; + +const UA_PARTIAL_FROM = "\\wozilla"; // /\wozilla +const UA_PARTIAL_SEP = "#"; +const UA_PARTIAL_TO = UA_OVERRIDE; +const UA_PARTIAL_OVERRIDE = UA_PARTIAL_FROM + UA_PARTIAL_SEP + UA_PARTIAL_TO; +const UA_PARTIAL_EXPECTED = DEFAULT_UA.replace(new RegExp(UA_PARTIAL_FROM, 'g'), UA_PARTIAL_TO); + +function getUA(host) { + var url = location.pathname; + url = host + url.slice(0, url.lastIndexOf('/')) + '/user_agent.sjs'; + + var xhr = new XMLHttpRequest(); + xhr.open('GET', url, false); // sync request + xhr.send(); + is(xhr.status, 200, 'request failed'); + is(typeof xhr.response, 'string', 'invalid response'); + return xhr.response; +} + +function testUAIFrame(host, expected, sameQ, message, testNavQ, navSameQ, navMessage, callback) { + let url = location.pathname; + url = host + url.slice(0, url.lastIndexOf('/')) + '/user_agent.sjs'; + let ifr = document.createElement('IFRAME'); + + ifr.src = url; + + document.getElementById('content').appendChild(ifr); + + window.addEventListener("message", function recv(e) { + ok(sameQ == (e.data.header.indexOf(expected) != -1), message); + if (testNavQ) { + ok(navSameQ == (e.data.nav.indexOf(expected) != -1), navMessage); + } + window.removeEventListener("message", recv, false); + callback(); + }, false); +} + +function testUAIFrameNoNav(host, expected, sameQ, message, callback) { + testUAIFrame(host, expected, sameQ, message, false, true, '', callback); +} + +const OVERRIDES = [ + { + domain: 'example.org', + override: '%DATE%', + host: 'http://example.org' + }, + { + domain: 'test1.example.org', + override: '%PRODUCT%', + expected: SpecialPowers.Services.appinfo.name, + host: 'http://test1.example.org' + }, + { + domain: 'test2.example.org', + override: '%APP_ID%', + expected: SpecialPowers.Services.appinfo.ID, + host: 'http://test2.example.org' + }, + { + domain: 'sub1.test1.example.org', + override: '%APP_VERSION%', + expected: SpecialPowers.Services.appinfo.version, + host: 'http://sub1.test1.example.org' + }, + { + domain: 'sub2.test1.example.org', + override: '%BUILD_ID%', + expected: SpecialPowers.Services.appinfo.appBuildID, + host: 'http://sub2.test1.example.org' + }, + { + domain: 'sub1.test2.example.org', + override: '%OS%', + expected: SpecialPowers.Services.appinfo.OS, + host: 'http://sub1.test2.example.org' + }, + { + domain: 'sub2.test2.example.org', + override: UA_PARTIAL_OVERRIDE, + expected: UA_PARTIAL_EXPECTED, + host: 'http://sub2.test2.example.org' + }, +]; + +function getServerURL() { + var url = location.pathname; + return location.origin + url.slice(0, url.lastIndexOf('/')) + '/user_agent_update.sjs?'; +} + +function getUpdateURL() { + var url = getServerURL(); + var overrides = {}; + overrides[location.hostname] = UA_OVERRIDE; + OVERRIDES.forEach(function (val) { + overrides[val.domain] = val.override; + }); + url = url + encodeURIComponent(JSON.stringify(overrides)).replace(/%25/g, '%'); + return url; +} + +function testDownload(callback) { + var startTime = Date.now(); + var url = getUpdateURL(); + isnot(navigator.userAgent, UA_OVERRIDE, 'UA already overridden'); + info('Waiting for UA update: ' + url); + + chromeScript.sendAsyncMessage("notify-on-update"); + SpecialPowers.pushPrefEnv({ + set: [ + [PREF_UPDATES_ENABLED, true], + [PREF_UPDATES_URL, url], + [PREF_UPDATES_TIMEOUT, 10000], + [PREF_UPDATES_INTERVAL, 1] // 1 second interval + ] + }); + + function waitForUpdate() { + info("Update Happened"); + testUAIFrameNoNav(location.origin, UA_OVERRIDE, true, 'Header UA not overridden', function() { + var updateTime = parseInt(getUA('http://example.org')); + todo(startTime <= updateTime, 'Update was before start time'); + todo(updateTime <= Date.now(), 'Update was after present time'); + + let overs = OVERRIDES; + (function nextOverride() { + val = overs.shift(); + if (val.expected) { + testUAIFrameNoNav(val.host, val.expected, true, 'Incorrect URL parameter: ' + val.override, function() { + overs.length ? nextOverride() : callback(); + }); + } else { + nextOverride(); + } + })(); + }); + } + + chromeScript.addMessageListener("useragent-update-complete", waitForUpdate); +} + +function testBadUpdate(callback) { + var url = getServerURL() + 'invalid-json'; + var prevOverride = navigator.userAgent; + SpecialPowers.pushPrefEnv({ + set: [ + [PREF_UPDATES_URL, url], + [PREF_UPDATES_INTERVAL, 1] // 1 second interval + ] + }, function () { setTimeout(function () { + var ifr = document.createElement('IFRAME'); + ifr.src = "about:blank"; + + ifr.addEventListener('load', function() { + // We want to make sure a bad update doesn't cancel out previous + // overrides. We do this by waiting for 5 seconds (assuming the update + // occurs within 5 seconds), and check that the previous override hasn't + // changed. + is(navigator.userAgent, prevOverride, + 'Invalid update deleted previous override'); + callback(); + }, false); + document.getElementById('content').appendChild(ifr); + }, 5000); }); +} + +SimpleTest.waitForExplicitFinish(); +SimpleTest.requestFlakyTimeout("Test sets timeouts to wait for updates to happen."); + +SpecialPowers.pushPrefEnv({ + set: [ + [PREF_APP_UPDATE_TIMERMINIMUMDELAY, 0] + ] +}, function () { + chromeScript.sendSyncMessage("UAO-uninit"); + + // Sets the OVERRIDES var in the chrome script. + // We do this to avoid code duplication. + chromeScript.sendSyncMessage("set-overrides", OVERRIDES); + + // testProfileLoad, testDownload, and testProfileSave must run in this order + // because testDownload depends on testProfileLoad to call UAO.init() + // and testProfileSave depends on testDownload to save overrides to the profile + chromeScript.sendAsyncMessage("testProfileLoad", location.hostname); +}); + + +const chromeScript = SpecialPowers.loadChromeScript(_ => { + // Enter update timer manager test mode + Components.classes["@mozilla.org/updates/timer-manager;1"].getService( + Components.interfaces.nsIObserver).observe(null, "utm-test-init", ""); + + Components.utils.import("resource://gre/modules/UserAgentOverrides.jsm"); + + var _notifyOnUpdate = false; + + var UAO = UserAgentOverrides; + UAO.uninit(); + + Components.utils.import("resource://gre/modules/FileUtils.jsm"); + var FU = FileUtils; + + const { TextDecoder, TextEncoder, OS } = Components.utils.import("resource://gre/modules/osfile.jsm"); + var OSF = OS.File; + + const KEY_PREFDIR = "PrefD"; + const KEY_APPDIR = "XCurProcD"; + const FILE_UPDATES = "ua-update.json"; + + const UA_OVERRIDE = "DummyUserAgent"; + const UA_ALT_OVERRIDE = "AltUserAgent"; + + const PREF_UPDATES = "general.useragent.updates."; + const PREF_UPDATES_ENABLED = PREF_UPDATES + "enabled"; + const PREF_UPDATES_LASTUPDATED = PREF_UPDATES + "lastupdated"; + + Components.utils.import("resource://gre/modules/Services.jsm"); + Services.prefs.addObserver(PREF_UPDATES_LASTUPDATED, () => { + if (_notifyOnUpdate) { + _notifyOnUpdate = false; // Only notify once, for the first update. + sendAsyncMessage("useragent-update-complete"); + } + } , false); + + var OVERRIDES = null; + + function is(value, expected, message) { + sendAsyncMessage("is-message", {value, expected, message}); + } + + function info(message) { + sendAsyncMessage("info-message", message); + } + + function testProfileSave(hostname) { + info('Waiting for saving to profile'); + var file = FU.getFile(KEY_PREFDIR, [FILE_UPDATES]).path; + (function waitForSave() { + OSF.exists(file).then( + (exists) => { + if (!exists) { + setTimeout(waitForSave, 100); + return; + } + return OSF.read(file).then( + (bytes) => { + info('Saved new overrides'); + var decoder = new TextDecoder(); + var overrides = JSON.parse(decoder.decode(bytes)); + is(overrides[hostname], UA_OVERRIDE, 'Incorrect saved override'); + OVERRIDES.forEach(function (val) { + val.expected && is(overrides[val.domain], val.expected, + 'Incorrect saved override: ' + val.override); + }); + sendAsyncMessage("testProfileSaveDone"); + } + ); + } + ).then(null, + (reason) => { + throw reason + } + ); + })(); + } + + function testProfileLoad(hostname) { + var file = FU.getFile(KEY_APPDIR, [FILE_UPDATES]).path; + var encoder = new TextEncoder(); + var overrides = {}; + overrides[hostname] = UA_ALT_OVERRIDE; + var bytes = encoder.encode(JSON.stringify(overrides)); + + var badfile = FU.getFile(KEY_PREFDIR, [FILE_UPDATES]).path; + var badbytes = encoder.encode("null"); + + OSF.writeAtomic(file, bytes, {tmpPath: file + ".tmp"}).then( + () => OSF.writeAtomic(badfile, badbytes, {tmpPath: badfile + ".tmp"}) + ).then( + () => { + sendAsyncMessage("testProfileLoadDone"); + }, + (reason) => { + throw reason + } + ); + } + + + addMessageListener("testProfileSave", testProfileSave); + addMessageListener("testProfileLoad", testProfileLoad); + addMessageListener("set-overrides", function(overrides) { OVERRIDES = overrides}); + addMessageListener("UAO-init", function() { UAO.init(); }); + addMessageListener("UAO-uninit", function() { UAO.uninit(); }); + addMessageListener("notify-on-update", () => { _notifyOnUpdate = true }); +}); + +chromeScript.addMessageListener("testProfileSaveDone", SimpleTest.finish); +chromeScript.addMessageListener("testProfileLoadDone", function() { + SpecialPowers.pushPrefEnv({ + set: [[PREF_UPDATES_ENABLED, true]] + }, function () { + // initialize UserAgentOverrides.jsm and + // UserAgentUpdates.jsm and load saved file + chromeScript.sendSyncMessage("UAO-init"); + (function waitForLoad() { + var ifr = document.createElement('IFRAME'); + ifr.src = "about:blank"; + + ifr.addEventListener('load', function() { + var nav = ifr.contentWindow.navigator; + if (nav.userAgent !== UA_ALT_OVERRIDE) { + setTimeout(waitForLoad, 100); + return; + } + testUAIFrameNoNav(location.origin, UA_ALT_OVERRIDE, true, 'Did not apply saved override', function () { + testDownload(function() { + testBadUpdate(function() { + chromeScript.sendAsyncMessage("testProfileSave", location.hostname); + }) + }) + }); + }, true); + + document.getElementById('content').appendChild(ifr); + })(); + }); +}); + +chromeScript.addMessageListener("is-message", function(params) { + let {value, expected, message} = params; + is(value, expected, message); +}); +chromeScript.addMessageListener("info-message", function(message) { + info(message); +}); + +</script> +</pre> +</body> +</html> diff --git a/netwerk/test/mochitests/test_user_agent_updates_reset.html b/netwerk/test/mochitests/test_user_agent_updates_reset.html new file mode 100644 index 0000000000..5b51fc40d5 --- /dev/null +++ b/netwerk/test/mochitests/test_user_agent_updates_reset.html @@ -0,0 +1,44 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=942470 +--> +<head> + <title>Test for Bug 942470</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=942470">Mozilla Bug 942470</a> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 942470 **/ + +function getUA(host) { + var url = location.pathname; + url = host + url.slice(0, url.lastIndexOf('/')) + '/user_agent.sjs'; + + var xhr = new XMLHttpRequest(); + xhr.open('GET', url, false); // sync request + xhr.send(); + is(xhr.status, 200, 'request failed'); + is(typeof xhr.response, 'string', 'invalid response'); + return xhr.response; +} + +const UA_OVERRIDE = "DummyUserAgent"; + +info("User agent is " + navigator.userAgent); +isnot(navigator.userAgent, UA_OVERRIDE, + "navigator.userAgent is not reverted"); +isnot(getUA(location.origin), UA_OVERRIDE, + "User-Agent is not reverted"); + +</script> +</pre> +</body> +</html> diff --git a/netwerk/test/mochitests/test_viewsource_unlinkable.html b/netwerk/test/mochitests/test_viewsource_unlinkable.html new file mode 100644 index 0000000000..9a1a35d682 --- /dev/null +++ b/netwerk/test/mochitests/test_viewsource_unlinkable.html @@ -0,0 +1,27 @@ +<!DOCTYPE HTML> +<html> +<!-- +--> +<head> + <title>Test for view-source linkability</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + +<script type="text/javascript"> +function runTest() { + SimpleTest.doesThrow(function() { + window.open('view-source:' + location.href, "_blank"); + }, "Trying to access view-source URL from unprivileged code should throw."); + SimpleTest.finish(); +} +</script> +</head> +<body onload="runTest();"> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"> +</pre> +</body> +</html> + + diff --git a/netwerk/test/mochitests/test_xhr_method_case.html b/netwerk/test/mochitests/test_xhr_method_case.html new file mode 100644 index 0000000000..236ab210b1 --- /dev/null +++ b/netwerk/test/mochitests/test_xhr_method_case.html @@ -0,0 +1,66 @@ +<!DOCTYPE HTML> +<html> +<!-- +XHR uppercases certain method names, but not others +--> +<head> + <title>Test for XHR Method casing</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + +<script type="text/javascript"> + +const testMethods = [ +// these methods should be normalized + ["get", "GET"], + ["GET", "GET"], + ["GeT", "GET"], + ["geT", "GET"], + ["GEt", "GET"], + ["post", "POST"], + ["POST", "POST"], + ["delete", "DELETE"], + ["DELETE", "DELETE"], + ["options", "OPTIONS"], + ["OPTIONS", "OPTIONS"], + ["put", "PUT"], + ["PUT", "PUT"], +// HEAD is not tested because we use the resposne body as part of the test +// ["head", "HEAD"], +// ["HEAD", "HEAD"], + +// other custom methods should not be normalized + ["Foo", "Foo"], + ["bAR", "bAR"], + ["foobar", "foobar"], + ["FOOBAR", "FOOBAR"] +] + +function doIter(index) +{ + var xhr = new XMLHttpRequest(); + xhr.open(testMethods[index][0], 'method.sjs', false); // sync request + xhr.send(); + is(xhr.status, 200, 'transaction failed'); + is(xhr.response, testMethods[index][1], 'unexpected method'); +} + +function dotest() +{ + SimpleTest.waitForExplicitFinish(); + for (var i = 0; i < testMethods.length; i++) { + doIter(i); + } + SimpleTest.finish(); +} + +</script> +</head> +<body onload="dotest();"> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"> +</pre> +</body> +</html> + diff --git a/netwerk/test/mochitests/user_agent.sjs b/netwerk/test/mochitests/user_agent.sjs new file mode 100644 index 0000000000..dea299a204 --- /dev/null +++ b/netwerk/test/mochitests/user_agent.sjs @@ -0,0 +1,21 @@ + +function handleRequest(request, response) +{ + // avoid confusing cache behaviors + response.setHeader("Cache-Control", "no-cache", false); + response.setHeader("Content-Type", "text/html", false); + response.setHeader("Access-Control-Allow-Origin", "*", false); + + // used by test_user_agent tests + response.write( + "<html><body>\ + <script type='text/javascript'>\ + var msg = {\ + header: '" + request.getHeader('User-Agent') + "',\ + nav: navigator.userAgent\ + };\ + self.parent.postMessage(msg, '*');\ + </script>\ + </body></html>" + ); +} diff --git a/netwerk/test/mochitests/user_agent_update.sjs b/netwerk/test/mochitests/user_agent_update.sjs new file mode 100644 index 0000000000..649ceb9036 --- /dev/null +++ b/netwerk/test/mochitests/user_agent_update.sjs @@ -0,0 +1,10 @@ + +function handleRequest(request, response) +{ + // avoid confusing cache behaviors + response.setHeader("Cache-Control", "no-cache", false); + response.setHeader("Content-Type", "application/json", false); + + // used by test_user_agent_updates test + response.write(decodeURIComponent(request.queryString)); +} diff --git a/netwerk/test/mochitests/web_packaged_app.sjs b/netwerk/test/mochitests/web_packaged_app.sjs new file mode 100644 index 0000000000..d5587d8d76 --- /dev/null +++ b/netwerk/test/mochitests/web_packaged_app.sjs @@ -0,0 +1,35 @@ +var Cc = Components.classes; +var Ci = Components.interfaces; +var Cu = Components.utils; + +function handleRequest(request, response) +{ + response.setHeader("Content-Type", "application/package", false); + response.write(octetStreamData.getData()); + return; +} + +// The package content +// getData formats it as described at http://www.w3.org/TR/web-packaging/#streamable-package-format +var octetStreamData = { + content: [ + { headers: ["Content-Location: /index.html", "Content-Type: text/html"], data: "<html>\r\n <head>\r\n <script> alert('OK: hello'); alert('DONE'); </script>\r\n</head>\r\n Web Packaged App Index\r\n</html>\r\n", type: "text/html" }, + { headers: ["Content-Location: /scripts/app.js", "Content-Type: text/javascript"], data: "module Math from '/scripts/helpers/math.js';\r\n...\r\n", type: "text/javascript" }, + { headers: ["Content-Location: /scripts/helpers/math.js", "Content-Type: text/javascript"], data: "export function sum(nums) { ... }\r\n...\r\n", type: "text/javascript" } + ], + token : "gc0pJq0M:08jU534c0p", + getData: function() { + var str = ""; + for (var i in this.content) { + str += "--" + this.token + "\r\n"; + for (var j in this.content[i].headers) { + str += this.content[i].headers[j] + "\r\n"; + } + str += "\r\n"; + str += this.content[i].data + "\r\n"; + } + + str += "--" + this.token + "--"; + return str; + } +} diff --git a/netwerk/test/moz.build b/netwerk/test/moz.build new file mode 100644 index 0000000000..3df865c3a8 --- /dev/null +++ b/netwerk/test/moz.build @@ -0,0 +1,70 @@ +# -*- 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/. + +TEST_DIRS += ['httpserver', 'gtest'] + +BROWSER_CHROME_MANIFESTS += ['browser/browser.ini'] +MOCHITEST_MANIFESTS += ['mochitests/mochitest.ini'] + +XPCSHELL_TESTS_MANIFESTS += [ + 'unit/xpcshell.ini', + 'unit_ipc/xpcshell.ini', +] + +GeckoSimplePrograms([ + 'PropertiesTest', + 'ReadNTLM', + 'TestBlockingSocket', + 'TestDNS', + 'TestIncrementalDownload', + 'TestOpen', + 'TestProtocols', + 'TestServ', + 'TestStreamLoader', + 'TestUpload', + 'TestURLParser', + 'urltest', +]) + +# XXX Make this work in libxul builds. +#SIMPLE_PROGRAMS += [ +# TestIDN', +# TestIOThreads', +# TestSocketTransport', +# TestStreamPump', +# TestStreamTransport', +# TestUDPSocketProvider', +#] + +CppUnitTests([ + 'TestBind', + 'TestCookie', + 'TestUDPSocket', +]) + +if CONFIG['OS_TARGET'] == 'WINNT': + CppUnitTests([ + 'TestNamedPipeService' + ]) + +RESOURCE_FILES += [ + 'urlparse.dat', + 'urlparse_unx.dat', +] + +USE_LIBS += ['static:js'] + +if CONFIG['ENABLE_INTL_API'] and CONFIG['MOZ_ICU_DATA_ARCHIVE']: + # The ICU libraries linked into libmozjs will not include the ICU data, + # so link it directly. + USE_LIBS += ['icudata'] + +CXXFLAGS += CONFIG['TK_CFLAGS'] + +include('/ipc/chromium/chromium-config.mozbuild') + +if CONFIG['GNU_CXX']: + CXXFLAGS += ['-Wno-shadow'] diff --git a/netwerk/test/reftest/658949-1-ref.html b/netwerk/test/reftest/658949-1-ref.html new file mode 100644 index 0000000000..6e6d7e25f6 --- /dev/null +++ b/netwerk/test/reftest/658949-1-ref.html @@ -0,0 +1 @@ +<iframe src="data:text/html,ABC"></iframe> diff --git a/netwerk/test/reftest/658949-1.html b/netwerk/test/reftest/658949-1.html new file mode 100644 index 0000000000..f61c03a525 --- /dev/null +++ b/netwerk/test/reftest/658949-1.html @@ -0,0 +1 @@ +<iframe src="data:text/html,ABC#myRef"></iframe> diff --git a/netwerk/test/reftest/bug565432-1-ref.html b/netwerk/test/reftest/bug565432-1-ref.html new file mode 100644 index 0000000000..05ea512a3d --- /dev/null +++ b/netwerk/test/reftest/bug565432-1-ref.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<title>Test newlines in href</title> +<ul> +<li><a href="about:blank">Link</a> +<li><a href="data:,test">Link</a> +<li><a href="file:///tmp/test">Link</a> +<li><a href="ftp://test.invalid/">Link</a> +<li><a href="gopher://test.invalid/">Link</a> +<li><a href="http://test.invalid/">Link</a> +<li><a href="ftp://test.invalid/%0a">Not Link</a> +</ul> + diff --git a/netwerk/test/reftest/bug565432-1.html b/netwerk/test/reftest/bug565432-1.html new file mode 100644 index 0000000000..69980a5346 --- /dev/null +++ b/netwerk/test/reftest/bug565432-1.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<title>Test newlines in href</title> +<ul> +<li><a href=" +about:blank">Link</a> +<li><a href=" +data:,test">Link</a> +<li><a href=" +file:///tmp/test">Link</a> +<li><a href=" +ftp://test.invalid/">Link</a> +<li><a href=" +gopher://test.invalid/">Link</a> +<li><a href=" +http://test.invalid/">Link</a> +<li><a href="ftp://test.invalid/%0a">Not Link</a> +</ul> + diff --git a/netwerk/test/reftest/reftest-stylo.list b/netwerk/test/reftest/reftest-stylo.list new file mode 100644 index 0000000000..d96477f0d5 --- /dev/null +++ b/netwerk/test/reftest/reftest-stylo.list @@ -0,0 +1,3 @@ +# DO NOT EDIT! This is a auto-generated temporary list for Stylo testing +== bug565432-1.html bug565432-1.html +== 658949-1.html 658949-1.html diff --git a/netwerk/test/reftest/reftest.list b/netwerk/test/reftest/reftest.list new file mode 100644 index 0000000000..98b5d4fb9a --- /dev/null +++ b/netwerk/test/reftest/reftest.list @@ -0,0 +1,2 @@ +== bug565432-1.html bug565432-1-ref.html +== 658949-1.html 658949-1-ref.html diff --git a/netwerk/test/sites.txt b/netwerk/test/sites.txt new file mode 100644 index 0000000000..e3089595aa --- /dev/null +++ b/netwerk/test/sites.txt @@ -0,0 +1,257 @@ +http://www.yahoo.com/ +http://www.netscape.com/ +http://www.microsoft.com/ +http://www.excite.com/ +http://www.mckinley.com/ +http://www.city.net/ +http://www.webcrawler.com/ +http://www.mirabilis.com/ +http://www.infoseek.com/ +http://www.pathfinder.com/ +http://www.warnerbros.com/ +http://www.cnn.com/ +http://www.altavista.digital.com/ +http://www.altavista.com/ +http://www.usatoday.com/ +http://www.disney.com/ +http://www.starwave.com/ +http://www.hotwired.com/ +http://www.hotbot.com/ +http://www.lycos.com/ +http://www.pointcom.com/ +http://www.cnet.com/ +http://www.search.com/ +http://www.news.com/ +http://www.download.com/ +http://www.geocities.com/ +http://www.aol.com/ +http://members.aol.com/ +http://www.imdb.com/ +http://uk.imdb.com/ +http://www.macromedia.com/ +http://www.infobeat.com/ +http://www.fxweb.com/ +http://www.whowhere.com/ +http://www.real.com/ +http://www.sportsline.com/ +http://www.dejanews.com/ +http://www.the-park.com/ +http://www.cmpnet.com/ +http://www.go2net.com/ +http://www.metacrawler.com/ +http://www.playsite.com/ +http://www.stocksite.com/ +http://www.sony.com/ +http://www.music.sony.com/ +http://www.station.sony.com/ +http://www.scea.sony.com/ +http://www.infospace.com/ +http://www.zdnet.com/ +http://www.hotfiles.com/ +http://www.chathouse.com/ +http://www.looksmart.com/ +http://www.iamginegames.com/ +http://www.macaddict.com/ +http://www.rsac.org/ +http://www.apple.com/ +http://www.beseen.com/ +http://www.dogpile.com/ +http://www.xoom.com/ +http://www.tucows.com/ +http://www.freethemes.com/ +http://www.winfiles.com/ +http://www.vservers.com/ +http://www.mtv.com/ +http://www.the-xfiles.com/ +http://www.datek.com/ +http://www.cyberthrill.com/ +http://www.surplusdirect.com/ +http://www.tomshardware.com/ +http://www.bigyellow.com/ +http://www.100hot.com/ +http://www.messagemates.com/ +http://www.onelist.com/ +http://www.bluemountain.com/ +http://www.ea.com/ +http://www.bullfrog.co.uk/ +http://www.travelocity.com/ +http://www.ibm.com/ +http://www.bigcharts.com/ +http://www.davesclassics.com/ +http://www.goto.com/ +http://www.weather.com/ +http://www.gamespot.com/ +http://www.bloomberg.com/ +http://www.winzip.com/ +http://www.filez.com/ +http://www.westwood.com/ +http://www.internet.com/ +http://www.cardmaster.com/ +http://www.creaf.com/ +http://netaddress.usa.net/ +http://www.occ.com/ +http://www.as.org/ +http://www.amazon.com/ +http://www.drudgereport.com/ +http://www.hardradio.com/ +http://www.intel.com/ +http://www.mp3.com/ +http://www.ebay.com/ +http://www.msn.com/ +http://www.fifa.com/ +http://www.attitude.com/ +http://www.happypuppy.com/ +http://www.gamesdomain.com/ +http://www.onsale.com/ +http://www.tm.com/ +http://www.xlnc1.com/ +http://www.greatsports.com/ +http://www.discovery.com/ +http://www.nai.com/ +http://www.nasa.gov/ +http://www.ogr.com/ +http://www.warzone.com/ +http://www.gamestats.com/ +http://www.winamp.com/ +http://java.sun.com/ +http://www.hp.com/ +http://www.cdnow.com/ +http://www.nytimes.com/ +http://www.majorleaguebaseball.com/ +http://www.washingtonpost.com/ +http://www.planetquake.com/ +http://www.wsj.com/ +http://www.slashdot.org/ +http://www.adobe.com/ +http://www.quicken.com/ +http://www.talkcity.com/ +http://www.developer.com/ +http://www.mapquest.com/ +http://www.yahoo.com/ +http://www.pathfinder.com/ +http://www.msn.com/ +http://www.fifa.com/ +http://www.attitude.com/ +http://www.happypuppy.com/ +http://www.gamesdomain.com/ +http://www.onsale.com/ +http://www.tm.com/ +http://www.xlnc1.com/ +http://www.greatsports.com/ +http://www.discovery.com/ +http://www.warnerbros.com/ +http://www.nai.com/ +http://www.nasa.gov/ +http://www.ogr.com/ +http://www.warzone.com/ +http://www.gamestats.com/ +http://www.winamp.com/ +http://java.sun.com/ +http://www.hp.com/ +http://www.cdnow.com/ +http://www.nytimes.com/ +http://www.majorleaguebaseball.com/ +http://www.planetquake.com/ +http://www.wsj.com/ +http://www.slashdot.org/ +http://www.adobe.com/ +http://www.quicken.com/ +http://www.talkcity.com/ +http://www.developer.com/ +http://www.mapquest.com/ +http://www.altavista.digital.com/ +http://www.altavista.com/ +http://www.usatoday.com/ +http://www.disney.com/ +http://www.starwave.com/ +http://www.hotwired.com/ +http://www.hotbot.com/ +http://www.netscape.com/ +http://www.lycos.com/ +http://www.pointcom.com/ +http://www.cnet.com/ +http://www.search.com/ +http://www.news.com/ +http://www.download.com/ +http://www.geocities.com/ +http://www.imdb.com/ +http://www.microsoft.com/ +http://uk.imdb.com/ +http://www.macromedia.com/ +http://www.infobeat.com/ +http://www.fxweb.com/ +http://www.whowhere.com/ +http://www.real.com/ +http://www.sportsline.com/ +http://www.dejanews.com/ +http://www.the-park.com/ +http://www.cmpnet.com/ +http://www.excite.com/ +http://www.go2net.com/ +http://www.metacrawler.com/ +http://www.playsite.com/ +http://www.stocksite.com/ +http://www.infospace.com/ +http://www.zdnet.com/ +http://www.mckinley.com/ +http://www.hotfiles.com/ +http://www.chathouse.com/ +http://www.looksmart.com/ +http://www.iamginegames.com/ +http://www.macaddict.com/ +http://www.rsac.org/ +http://www.apple.com/ +http://www.beseen.com/ +http://www.dogpile.com/ +http://www.xoom.com/ +http://www.city.net/ +http://www.tucows.com/ +http://www.freethemes.com/ +http://www.winfiles.com/ +http://www.vservers.com/ +http://www.mtv.com/ +http://www.the-xfiles.com/ +http://www.datek.com/ +http://www.cyberthrill.com/ +http://www.surplusdirect.com/ +http://www.tomshardware.com/ +http://www.webcrawler.com/ +http://www.bigyellow.com/ +http://www.100hot.com/ +http://www.messagemates.com/ +http://www.onelist.com/ +http://www.bluemountain.com/ +http://www.ea.com/ +http://www.bullfrog.co.uk/ +http://www.travelocity.com/ +http://www.ibm.com/ +http://www.bigcharts.com/ +http://www.mirabilis.com/ +http://www.davesclassics.com/ +http://www.goto.com/ +http://www.weather.com/ +http://www.gamespot.com/ +http://www.bloomberg.com/ +http://www.winzip.com/ +http://www.filez.com/ +http://www.westwood.com/ +http://www.internet.com/ +http://www.cardmaster.com/ +http://www.infoseek.com/ +http://www.creaf.com/ +http://netaddress.usa.net/ +http://www.occ.com/ +http://www.as.org/ +http://www.amazon.com/ +http://www.drudgereport.com/ +http://www.hardradio.com/ +http://www.intel.com/ +http://www.mp3.com/ +http://www.ebay.com/ +http://www.aol.com/ +http://www.cnn.com/ +http://www.music.sony.com/ +http://www.scea.sony.com/ +http://www.sony.com/ +http://www.station.sony.com/ +http://www.washingtonpost.com/ diff --git a/netwerk/test/unit/CA.cert.der b/netwerk/test/unit/CA.cert.der Binary files differnew file mode 100644 index 0000000000..67157cabd0 --- /dev/null +++ b/netwerk/test/unit/CA.cert.der 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..610e8a1cf5 --- /dev/null +++ b/netwerk/test/unit/client_cert_chooser.js @@ -0,0 +1,26 @@ +// -*- 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/. + +var { utils: Cu, interfaces: Ci } = Components; +const { XPCOMUtils } = Cu.import("resource://gre/modules/XPCOMUtils.jsm", {}); + +var Prompter = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIPrompt]), + alert: function() {} // Do nothing when asked to show an alert +}; + +function WindowWatcherService() {} +WindowWatcherService.prototype = { + classID: Components.ID("{01ae923c-81bb-45db-b860-d423b0fc4fe1}"), + QueryInterface: XPCOMUtils.generateQI([Ci.nsIWindowWatcher]), + + getNewPrompter: function() { + return Prompter; + } +}; + +this.NSGetFactory = XPCOMUtils.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/image.png b/netwerk/test/unit/data/image.png Binary files differnew file mode 100644 index 0000000000..e0c5d3d6a1 --- /dev/null +++ b/netwerk/test/unit/data/image.png diff --git a/netwerk/test/unit/data/signed_win.exe b/netwerk/test/unit/data/signed_win.exe Binary files differnew file mode 100644 index 0000000000..de3bb40e84 --- /dev/null +++ b/netwerk/test/unit/data/signed_win.exe diff --git a/netwerk/test/unit/data/system_root.lnk b/netwerk/test/unit/data/system_root.lnk Binary files differnew file mode 100644 index 0000000000..e5885ce9a5 --- /dev/null +++ b/netwerk/test/unit/data/system_root.lnk diff --git a/netwerk/test/unit/data/test_psl.txt b/netwerk/test/unit/data/test_psl.txt new file mode 100644 index 0000000000..da6a637adb --- /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('il', null); +checkPublicSuffix('c.il', null); +checkPublicSuffix('b.c.il', 'b.c.il'); +checkPublicSuffix('a.b.c.il', 'b.c.il'); +// 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..6c8cf0d4a8 --- /dev/null +++ b/netwerk/test/unit/head_cache.js @@ -0,0 +1,147 @@ +Components.utils.import('resource://gre/modules/XPCOMUtils.jsm'); +Components.utils.import('resource://gre/modules/LoadContextInfo.jsm'); + +var _CSvc; +function get_cache_service() { + if (_CSvc) + return _CSvc; + + return _CSvc = Components.classes["@mozilla.org/netwerk/cache-storage-service;1"] + .getService(Components.interfaces.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(LoadContextInfo.default); + storage.asyncEvictStorage(null); + } + + if (clearDisk) { + storage = svc.diskCacheStorage(LoadContextInfo.default, false); + storage.asyncEvictStorage(null); + } + + if (clearAppCache) { + storage = svc.appCacheStorage(LoadContextInfo.default, null); + storage.asyncEvictStorage(null); + } +} + +function createURI(urispec) +{ + var ioServ = Components.classes["@mozilla.org/network/io-service;1"] + .getService(Components.interfaces.nsIIOService); + return ioServ.newURI(urispec, null, null); +} + +function getCacheStorage(where, lci, appcache) +{ + if (!lci) lci = 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: function (iid) { + if (iid.equals(Components.interfaces.nsICacheEntryOpenCallback) || + iid.equals(Components.interfaces.nsISupports)) + return this; + throw Components.results.NS_ERROR_NO_INTERFACE; + }, + + onCacheEntryCheck: function(entry, appCache) { + if (typeof callback === "object") + return callback.onCacheEntryCheck(entry, appCache); + return Ci.nsICacheEntryOpenCallback.ENTRY_WANTED; + }, + + onCacheEntryAvailable: function (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: function () { + var storage = getCacheStorage(where, lci, this._appCache); + storage.asyncOpenURI(key, "", flags, this); + } + }; + + (new CacheListener()).run(); +} + +function syncWithCacheIOThread(callback, force) +{ + if (!newCacheBackEndUsed() || force) { + asyncOpenCacheEntry( + "http://nonexistententry/", "disk", Ci.nsICacheStorage.OPEN_READONLY, null, + function(status, entry) { + do_check_eq(status, Components.results.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: function (entryCount, consumption) { + do_execute_soon(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); + do_check_eq(status, Cr.NS_OK); + do_check_true(!!entry); + } else { + dump("TEST-INFO | checking cache key " + key + " doesn't exist @ " + where); + do_check_eq(status, Cr.NS_ERROR_CACHE_KEY_NOT_FOUND); + do_check_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..decf04f90b --- /dev/null +++ b/netwerk/test/unit/head_cache2.js @@ -0,0 +1,429 @@ +var Cc = Components.classes; +var Ci = Components.interfaces; +var Cu = Components.utils; +var Cr = Components.results; + +function newCacheBackEndUsed() +{ + var cache1srv = Components.classes["@mozilla.org/network/cache-service;1"] + .getService(Components.interfaces.nsICacheService); + var cache2srv = Components.classes["@mozilla.org/netwerk/cache-storage-service;1"] + .getService(Components.interfaces.nsICacheStorageService); + + return cache1srv.cacheIOTarget != cache2srv.ioTarget; +} + +var callbacks = new Array(); + +// 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, -1, -1, 0, 0, true); + var data = ""; + pump.asyncRead({ + onStartRequest: function (aRequest, aContext) { }, + onDataAvailable: function (aRequest, aContext, 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: function (aRequest, aContext, aStatusCode) + { + LOG_C2("done reading data: " + aStatusCode); + do_check_eq(aStatusCode, Cr.NS_OK); + goon(data); + }, + }, null); + } + else { + // blocking stream + var data = read_stream(inputStream, inputStream.available()); + goon(data); + } +} + +OpenCallback.prototype = +{ + QueryInterface: function listener_qi(iid) { + if (iid.equals(Ci.nsISupports) || + iid.equals(Ci.nsICacheEntryOpenCallback)) { + return this; + } + throw Components.results.NS_ERROR_NO_INTERFACE; + }, + onCacheEntryCheck: function(entry, appCache) + { + LOG_C2(this, "onCacheEntryCheck"); + do_check_true(!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; + } + + do_check_eq(entry.getMetaDataElement("meto"), this.workingMetadata); + + // check for sane flag combination + do_check_neq(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"); + if (newCacheBackEndUsed()) { + // 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: function(entry, isnew, appCache, status) + { + if ((this.behavior & MAYBE_NEW) && isnew) { + this.behavior |= NEW; + } + + LOG_C2(this, "onCacheEntryAvailable, " + this.behavior); + do_check_true(!this.onAvailPassed); + this.onAvailPassed = true; + + do_check_eq(isnew, !!(this.behavior & NEW)); + + if (this.behavior & (NOTFOUND|NOTWANTED)) { + do_check_eq(status, Cr.NS_ERROR_CACHE_KEY_NOT_FOUND); + do_check_false(!!entry); + if (this.behavior & THROWAVAIL) + this.throwAndNotify(entry); + this.goon(entry); + } + else if (this.behavior & (NEW|RECREATE)) { + do_check_true(!!entry); + + if (this.behavior & RECREATE) { + entry = entry.recreate(); + do_check_true(!!entry); + } + + if (this.behavior & THROWAVAIL) + this.throwAndNotify(entry); + + if (!(this.behavior & WAITFORWRITE)) + this.goon(entry); + + if (!(this.behavior & PARTIAL)) { + try { + entry.getMetaDataElement("meto"); + do_check_true(false); + } + catch (ex) {} + } + + if (this.behavior & DONTFILL) { + do_check_false(this.behavior & WAITFORWRITE); + return; + } + + var self = this; + do_execute_soon(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; + } + do_execute_soon(function() { // emulate more network latency + if (self.behavior & DOOMED) { + LOG_C2(self, "checking doom state"); + try { + var os = entry.openOutputStream(0); + // Unfortunately, in the undetermined state we cannot even check whether the entry + // is actually doomed or not. + os.close(); + do_check_true(!!(self.behavior & MAYBE_NEW)); + } catch (ex) { + do_check_true(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); + LOG_C2(self, "writing data"); + var wrt = os.write(self.workingData, self.workingData.length); + do_check_eq(wrt, self.workingData.length); + os.close(); + if (self.behavior & WAITFORWRITE) + self.goon(entry); + + entry.close(); + }) + }) + } + else { // NORMAL + do_check_true(!!entry); + do_check_eq(entry.getMetaDataElement("meto"), this.workingMetadata); + if (this.behavior & THROWAVAIL) + this.throwAndNotify(entry); + if (this.behavior & NOTIFYBEFOREREAD) + this.goon(entry, true); + + var wrapper = Cc["@mozilla.org/scriptableinputstream;1"]. + createInstance(Ci.nsIScriptableInputStream); + var self = this; + pumpReadStream(entry.openInputStream(0), function(data) { + do_check_eq(data, self.workingData); + self.onDataCheckPassed = true; + LOG_C2(self, "entry read done"); + self.goon(entry); + entry.close(); + }); + } + }, + selfCheck: function() + { + LOG_C2(this, "selfCheck"); + + do_check_true(this.onCheckPassed || (this.behavior & MAYBE_NEW)); + do_check_true(this.onAvailPassed); + do_check_true(this.onDataCheckPassed || (this.behavior & MAYBE_NEW)); + }, + throwAndNotify: function(entry) + { + LOG_C2(this, "Throwing"); + var self = this; + do_execute_soon(function() { + LOG_C2(self, "Notifying"); + self.goon(entry); + }); + throw 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: function listener_qi(iid) { + if (iid.equals(Ci.nsISupports) || + iid.equals(Ci.nsICacheStorageVisitor)) { + return this; + } + throw Components.results.NS_ERROR_NO_INTERFACE; + }, + onCacheStorageInfo: function(num, consumption) + { + LOG_C2(this, "onCacheStorageInfo: num=" + num + ", size=" + consumption); + do_check_eq(this.num, num); + if (newCacheBackEndUsed()) { + // Consumption is as expected only in the new backend + do_check_eq(this.consumption, consumption); + } + if (!this.entries) + this.notify(); + }, + onCacheEntryInfo: function(aURI, aIdEnhance, aDataSize, aFetchCount, aLastModifiedTime, aExpirationTime) + { + var key = (aIdEnhance ? (aIdEnhance + ":") : "") + aURI.asciiSpec; + LOG_C2(this, "onCacheEntryInfo: key=" + key); + + do_check_true(!!this.entries); + + var index = this.entries.indexOf(key); + do_check_true(index > -1); + + this.entries.splice(index, 1); + }, + onCacheEntryVisitCompleted: function() + { + LOG_C2(this, "onCacheEntryVisitCompleted"); + if (this.entries) + do_check_eq(this.entries.length, 0); + this.notify(); + }, + notify: function() + { + do_check_true(!!this.goon); + var goon = this.goon; + this.goon = null; + do_execute_soon(goon); + }, + selfCheck: function() + { + do_check_true(!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: function listener_qi(iid) { + if (iid.equals(Ci.nsISupports) || + iid.equals(Ci.nsICacheEntryDoomCallback)) { + return this; + } + throw Components.results.NS_ERROR_NO_INTERFACE; + }, + onCacheEntryDoomed: function(result) + { + do_check_eq(this.expectedSuccess, result == Cr.NS_OK); + this.goon(); + }, + selfCheck: function() {} +} + +function EvictionCallback(success, goon) +{ + this.expectedSuccess = success; + this.goon = goon; + callbacks.push(this); + this.order = callbacks.length; +} + +MultipleCallbacks.prototype = +{ + fired: function() + { + if (--this.pending == 0) + { + var self = this; + if (this.delayed) + do_execute_soon(function() { self.goon(); }); + else + this.goon(); + } + }, + add: function() + { + ++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: function() { continue_func(); }, + 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..5d71716683 --- /dev/null +++ b/netwerk/test/unit/head_channels.js @@ -0,0 +1,218 @@ +/** + * Read count bytes from stream and return as a String object + */ +function read_stream(stream, count) { + /* assume stream has non-ASCII data */ + var wrapper = + Components.classes["@mozilla.org/binaryinputstream;1"] + .createInstance(Components.interfaces.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; +} +ChannelListener.prototype = { + _closure: null, + _closurectx: null, + _buffer: "", + _got_onstartrequest: false, + _got_onstoprequest: false, + _contentLen: -1, + _lastEvent: 0, + + QueryInterface: function(iid) { + if (iid.equals(Components.interfaces.nsIStreamListener) || + iid.equals(Components.interfaces.nsIRequestObserver) || + iid.equals(Components.interfaces.nsISupports)) + return this; + throw Components.results.NS_ERROR_NO_INTERFACE; + }, + + onStartRequest: function(request, context) { + try { + if (this._got_onstartrequest) + do_throw("Got second onStartRequest event!"); + this._got_onstartrequest = true; + this._lastEvent = Date.now(); + + request.QueryInterface(Components.interfaces.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: function(request, context, 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: function(request, context, 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) + do_check_eq(this._buffer.length, this._contentLen) + } catch (ex) { + do_throw("Error in onStopRequest: " + ex); + } + try { + this._closure(request, this._buffer, this._closurectx); + } catch (ex) { + do_throw("Error in closure function: " + ex); + } + } +}; + +var ES_ABORT_REDIRECT = 0x01; + +function ChannelEventSink(flags) +{ + this._flags = flags; +} + +ChannelEventSink.prototype = { + QueryInterface: function(iid) { + if (iid.equals(Ci.nsIInterfaceRequestor) || + iid.equals(Ci.nsISupports)) + return this; + throw Cr.NS_ERROR_NO_INTERFACE; + }, + + getInterface: function(iid) { + if (iid.equals(Ci.nsIChannelEventSink)) + return this; + throw Cr.NS_ERROR_NO_INTERFACE; + }, + + asyncOnChannelRedirect: function(oldChannel, newChannel, flags, callback) { + if (this._flags & ES_ABORT_REDIRECT) + throw Cr.NS_BINDING_ABORTED; + + callback.onRedirectVerifyCallback(Cr.NS_OK); + } +}; + +/** + * A helper class to construct origin attributes. + */ +function OriginAttributes(appId, inIsolatedMozBrowser, privateId) { + this.appId = appId; + this.inIsolatedMozBrowser = inIsolatedMozBrowser; + this.privateBrowsingId = privateId; +} +OriginAttributes.prototype = { + appId: 0, + inIsolatedMozBrowser: false, + privateBrowsingId: 0 +}; diff --git a/netwerk/test/unit/socks_client_subprocess.js b/netwerk/test/unit/socks_client_subprocess.js new file mode 100644 index 0000000000..144bc8757e --- /dev/null +++ b/netwerk/test/unit/socks_client_subprocess.js @@ -0,0 +1,42 @@ +var CC = Components.Constructor; +var Cc = Components.classes; +var Ci = Components.interfaces; + +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 launchConnection(socks_vers, socks_port, dest_host, dest_port, dns) +{ + var pi_flags = 0; + if (dns == 'remote') + pi_flags = Ci.nsIProxyInfo.TRANSPARENT_PROXY_RESOLVES_HOST; + + var pps = new ProtocolProxyService(); + var pi = pps.newProxyInfo(socks_vers, 'localhost', socks_port, + pi_flags, -1, null); + var trans = sts.createTransport(null, 0, dest_host, dest_port, pi); + var input = trans.openInputStream(Ci.nsITransport.OPEN_BLOCKING,0,0); + var output = trans.openOutputStream(Ci.nsITransport.OPEN_BLOCKING,0,0); + var bin = new BinaryInputStream(input); + var data = bin.readBytes(5); + if (data == 'PING!') { + print('client: got ping, sending pong.'); + output.write('PONG!', 5); + } else { + print('client: wrong data from server:', data); + output.write('Error: wrong data received.', 27); + } + output.close(); +} + +for (var arg of arguments) { + print('client: running test', arg); + test = arg.split('|'); + launchConnection(test[0], parseInt(test[1]), test[2], + parseInt(test[3]), test[4]); +} diff --git a/netwerk/test/unit/test_1073747.js b/netwerk/test/unit/test_1073747.js new file mode 100644 index 0000000000..c930514e7e --- /dev/null +++ b/netwerk/test/unit/test_1073747.js @@ -0,0 +1,30 @@ +// Test based on submitted one from Peter B Shalimoff + +var test = function(s, funcName){ + function Arg(){}; + Arg.prototype.toString = function(){ + do_print("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 = Components.classes["@mozilla.org/network/url-parser;1?auth=maybe"].getService(Components.interfaces.nsIURLParser); + try { + up[funcName].apply(up, args); + return args; + } catch (x) { + do_check_true(true); // make sure it throws an exception instead of crashing + return x; + } + // should always have an exception to catch + do_check_true(false); +}; +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..033c337b7f --- /dev/null +++ b/netwerk/test/unit/test_304_responses.js @@ -0,0 +1,95 @@ +"use strict"; +// https://bugzilla.mozilla.org/show_bug.cgi?id=761228 + +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +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, null, null); +} + +function make_channel(url) { + return NetUtil.newChannel({uri: url, loadUsingSystemPrincipal: true}) + .QueryInterface(Ci.nsIHttpChannel); +} + +function clearCache() { + var service = Components.classes["@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(Components.interfaces.nsIHttpChannel); + do_check_eq(request.responseStatus, 304); + do_check_eq(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.asyncOpen2(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", + "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(Components.interfaces.nsIHttpChannel); + chan.setRequestHeader("If-None-Match", '"foo"', false); + + chan.asyncOpen2(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..fbbcdf000d --- /dev/null +++ b/netwerk/test/unit/test_307_redirect.js @@ -0,0 +1,91 @@ +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +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); + return; +} + +function contentHandler(metadata, response) +{ + response.setHeader("Content-Type", "text/plain"); + response.bodyOutputStream.writeFrom(metadata.bodyInputStream, + metadata.bodyInputStream.available()); +} + +function noRedirectStreamObserver(request, buffer) +{ + do_check_eq(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.asyncOpen2(new ChannelListener(noHeaderStreamObserver, null)); +} + +function noHeaderStreamObserver(request, buffer) +{ + do_check_eq(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.asyncOpen2(new ChannelListener(headerStreamObserver, null)); +} + +function headerStreamObserver(request, buffer) +{ + do_check_eq(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(Components.interfaces.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.asyncOpen2(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..7a9e070291 --- /dev/null +++ b/netwerk/test/unit/test_421.js @@ -0,0 +1,60 @@ +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +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.asyncOpen2(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) { + do_check_eq(channel.responseStatus, 200); + do_check_eq(channel.responseStatusText, "OK"); + do_check_true(channel.requestSucceeded); + + do_check_eq(channel.contentType, "text/plain"); + do_check_eq(channel.contentLength, httpbody.length); + do_check_eq(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..2c46a061c2 --- /dev/null +++ b/netwerk/test/unit/test_MIME_params.js @@ -0,0 +1,560 @@ +/** + * 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 + */ + +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 + ["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], + + ["attachment; filename=foo extension=bla", + "attachment", "foo"], + + ["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\\"], +]; + +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 = Components.classes["@mozilla.org/network/mime-hdrparam;1"] + .getService(Components.interfaces.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); + + do_check_eq(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 { + do_check_eq(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); + + do_check_eq(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 { + do_check_eq(e.result, expectedFn); + } catch(e) {} + } + continue; + } + } +} + +function test_decode5987Param() { + var mhp = Components.classes["@mozilla.org/network/mime-hdrparam;1"] + .getService(Components.interfaces.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) { + do_check_eq(rfc5987paramtests[i][1], decoded); + do_check_eq(rfc5987paramtests[i][2], lang.value); + } + else { + do_check_eq(rfc5987paramtests[i][3], "instead got: " + decoded); + } + } + catch (e) { + do_check_eq(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..7f446ecc46 --- /dev/null +++ b/netwerk/test/unit/test_NetUtil.js @@ -0,0 +1,867 @@ +/* -*- 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. + */ + +Cu.import("resource://testing-common/httpd.js"); + +Cu.import("resource://gre/modules/NetUtil.jsm"); +Cu.import("resource://gre/modules/Task.jsm"); +Cu.import("resource://gre/modules/Promise.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); + +// 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! + do_check_true(Components.isSuccessCode(aResult)); + + // Check the file contents. + do_check_eq(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: file, sink: ostream}; + } + + let bstream = Cc["@mozilla.org/network/buffered-output-stream;1"]. + createInstance(Ci.nsIBufferedOutputStream); + bstream.init(ostream, 256); + return {file: file, sink: bstream}; + } + Task.spawn(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"); + do_print(text); + let TEST_DATA = "[" + make_sample(text) + "]"; + let source = make_input(bufferedInput, TEST_DATA); + let {file, sink} = make_output(bufferedOutput); + let deferred = Promise.defer(); + NetUtil.asyncCopy(source, sink, deferred.resolve); + let result = yield deferred.promise; + + // Make sure the copy was successful! + if (!Components.isSuccessCode(result)) { + do_throw(new Components.Exception("asyncCopy error", result)); + } + + // Check the file contents. + do_check_eq(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) { + do_check_eq(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, null, null); + let NetUtilURI = NetUtil.newURI(TEST_URI); + do_check_true(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); + do_check_true(iosURI.equals(NetUtilURI)); + + run_next_test(); +} + +function test_ioService() +{ + do_check_true(NetUtil.ioService instanceof Ci.nsIIOService); + run_next_test(); +} + +function test_asyncFetch_no_channel() +{ + try { + NetUtil.asyncFetch(null, function() { }); + do_throw("should throw!"); + } + catch (e) { + do_check_eq(e.result, Cr.NS_ERROR_INVALID_ARG); + } + + run_next_test(); +} + +function test_asyncFetch_no_callback() +{ + try { + NetUtil.asyncFetch({ }); + do_throw("should throw!"); + } + catch (e) { + do_check_eq(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. + do_check_true(Components.isSuccessCode(aResult)); + + // Check that we got the right data. + do_check_eq(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); + do_check_eq(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. + do_check_true(Components.isSuccessCode(aResult)); + + // Check that we got the right data. + do_check_eq(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); + do_check_eq(TEST_DATA, result); + + server.stop(run_next_test); + }, + null, // aLoadingNode + Services.scriptSecurityManager.getSystemPrincipal(), + null, // aTriggeringPrincipal + Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_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. + do_check_true(Components.isSuccessCode(aResult)); + + // Check that we got the right data. + do_check_eq(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); + do_check_eq(TEST_DATA, result); + + server.stop(run_next_test); + }, + null, // aLoadingNode + Services.scriptSecurityManager.getSystemPrincipal(), + null, // aTriggeringPrincipal + Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_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. + do_check_eq(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. + do_check_true(Components.isSuccessCode(aResult)); + + // Check that we got the right data. + do_check_eq(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); + do_check_eq(TEST_DATA, result); + + run_next_test(); + }, + null, // aLoadingNode + Services.scriptSecurityManager.getSystemPrincipal(), + null, // aTriggeringPrincipal + Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_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. + do_check_true(Components.isSuccessCode(aResult)); + + // Check that we got the right data. + do_check_eq(aInputStream.available(), TEST_DATA.length); + do_check_eq(NetUtil.readInputStreamToString(aInputStream, TEST_DATA.length), + TEST_DATA); + + run_next_test(); + }, + null, // aLoadingNode + Services.scriptSecurityManager.getSystemPrincipal(), + null, // aTriggeringPrincipal + Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_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. + do_check_true(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) { + do_check_eq(e.result, Cr.NS_BASE_STREAM_CLOSED); + } + + run_next_test(); + }); +} + +function test_newChannel_no_specifier() +{ + try { + NetUtil.newChannel(); + do_throw("should throw!"); + } + catch (e) { + do_check_eq(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 = NetUtil.ioService; + let iosChannel = ios.newChannel2(TEST_SPEC, + null, + null, + null, // aLoadingNode + Services.scriptSecurityManager.getSystemPrincipal(), + null, // aTriggeringPrincipal + Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL, + Ci.nsIContentPolicy.TYPE_OTHER); + let NetUtilChannel = NetUtil.newChannel({ + uri: TEST_SPEC, + loadUsingSystemPrincipal: true + }); + do_check_true(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 = NetUtil.ioService.newChannelFromURI2(uri, + null, // aLoadingNode + Services.scriptSecurityManager.getSystemPrincipal(), + null, // aTriggeringPrincipal + Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL, + Ci.nsIContentPolicy.TYPE_OTHER); + let NetUtilChannel = NetUtil.newChannel({ + uri: uri, + loadUsingSystemPrincipal: true + }); + do_check_true(iosChannel.URI.equals(NetUtilChannel.URI)); + + run_next_test(); +} + +function test_newChannel_with_options() +{ + let uri = "data:text/plain,"; + + let iosChannel = NetUtil.ioService.newChannelFromURI2(NetUtil.newURI(uri), + null, // aLoadingNode + Services.scriptSecurityManager.getSystemPrincipal(), + null, // aTriggeringPrincipal + Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL, + Ci.nsIContentPolicy.TYPE_OTHER); + + function checkEqualToIOSChannel(channel) { + do_check_true(iosChannel.URI.equals(channel.URI)); + } + + checkEqualToIOSChannel(NetUtil.newChannel({ + uri, + loadingPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), + 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({}); + }, /requires the 'uri' property/); + + Assert.throws(() => { + NetUtil.newChannel({ uri }); + }, /requires at least one of the 'loadingNode'/); + + Assert.throws(() => { + NetUtil.newChannel({ + uri, + loadingPrincipal: systemPrincipal, + }); + }, /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_deprecated_newChannel_API_with_string() { + const TEST_SPEC = "http://mozilla.org"; + let uri = NetUtil.newURI(TEST_SPEC); + let oneArgChannel = NetUtil.newChannel(TEST_SPEC); + let threeArgChannel = NetUtil.newChannel(TEST_SPEC, null, null); + do_check_true(uri.equals(oneArgChannel.URI)); + do_check_true(uri.equals(threeArgChannel.URI)); + + run_next_test(); +} + +function test_deprecated_newChannel_API_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-deprecated-newchannel-api-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. + do_check_eq(TEST_DATA, getFileContents(file)); + + // create a channel using the file + let channel = NetUtil.newChannel(file); + + // Create a pipe that will create our output stream that we can use once + // we have gotten all the data. + let pipe = Cc["@mozilla.org/pipe;1"].createInstance(Ci.nsIPipe); + pipe.init(true, true, 0, 0, null); + + let listener = Cc["@mozilla.org/network/simple-stream-listener;1"]. + createInstance(Ci.nsISimpleStreamListener); + listener.init(pipe.outputStream, { + onStartRequest: function(aRequest, aContext) {}, + onStopRequest: function(aRequest, aContext, aStatusCode) { + pipe.outputStream.close(); + do_check_true(Components.isSuccessCode(aContext)); + + // Check that we got the right data. + do_check_eq(pipe.inputStream.available(), TEST_DATA.length); + let is = Cc["@mozilla.org/scriptableinputstream;1"]. + createInstance(Ci.nsIScriptableInputStream); + is.init(pipe.inputStream); + let result = is.read(TEST_DATA.length); + do_check_eq(TEST_DATA, result); + run_next_test(); + } + }); + channel.asyncOpen2(listener); +} + +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; + + do_check_eq(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) { + do_check_eq(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) { + do_check_eq(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) { + do_check_eq(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) { + do_check_eq(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); + do_check_eq(NetUtil.readInputStreamToString(istream, + TEST_DATA_UTF8.length, + { charset: "UTF-8"}), + TEST_DATA); + + istream.setData(TEST_DATA_SJIS, TEST_DATA_SJIS.length); + do_check_eq(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) { + do_check_eq(e.result, Cr.NS_ERROR_ILLEGAL_INPUT); + } + + istream.setData(TEST_DATA_UTF8, TEST_DATA_UTF8.length); + do_check_eq(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_ioService, + 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_deprecated_newChannel_API_with_string, + test_deprecated_newChannel_API_with_nsIFile, + 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(add_test); +var index = 0; + +function run_test() +{ + run_next_test(); +} + diff --git a/netwerk/test/unit/test_URIs.js b/netwerk/test/unit/test_URIs.js new file mode 100644 index 0000000000..b68c4f7874 --- /dev/null +++ b/netwerk/test/unit/test_URIs.js @@ -0,0 +1,608 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +Components.utils.import("resource://gre/modules/NetUtil.jsm"); + +var gIoService = Components.classes["@mozilla.org/network/io-service;1"] + .getService(Components.interfaces.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:", + path: "blank", + ref: "", + nsIURL: false, nsINestedURI: true, immutable: true }, + { spec: "about:foobar", + scheme: "about", + prePath: "about:", + path: "foobar", + ref: "", + nsIURL: false, nsINestedURI: false, immutable: true }, + { spec: "chrome://foobar/somedir/somefile.xml", + scheme: "chrome", + prePath: "chrome://foobar", + path: "/somedir/somefile.xml", + ref: "", + nsIURL: true, nsINestedURI: false, immutable: true }, + { spec: "data:text/html;charset=utf-8,<html></html>", + scheme: "data", + prePath: "data:", + path: "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:", + path: "text/html;charset=utf-8,<html></html>", + ref: "", + nsIURL: false, nsINestedURI: false }, + { spec: "data:text/plain,hello world", + scheme: "data", + prePath: "data:", + path: "text/plain,hello%20world", + ref: "", + nsIURL: false, nsINestedURI: false }, + { spec: "file:///dir/afile", + scheme: "data", + prePath: "data:", + path: "text/plain,2", + ref: "", + relativeURI: "data:te\nxt/plain,2", + nsIURL: false, nsINestedURI: false }, + { spec: "file://", + scheme: "file", + prePath: "file://", + path: "/", + ref: "", + nsIURL: true, nsINestedURI: false }, + { spec: "file:///", + scheme: "file", + prePath: "file://", + path: "/", + ref: "", + nsIURL: true, nsINestedURI: false }, + { spec: "file:///myFile.html", + scheme: "file", + prePath: "file://", + path: "/myFile.html", + ref: "", + nsIURL: true, nsINestedURI: false }, + { spec: "file:///dir/afile", + scheme: "file", + prePath: "file://", + path: "/dir/data/text/plain,2", + ref: "", + relativeURI: "data/text/plain,2", + nsIURL: true, nsINestedURI: false }, + { spec: "file:///dir/dir2/", + scheme: "file", + prePath: "file://", + path: "/dir/dir2/data/text/plain,2", + ref: "", + relativeURI: "data/text/plain,2", + nsIURL: true, nsINestedURI: false }, + { spec: "ftp://", + scheme: "ftp", + prePath: "ftp://", + path: "/", + ref: "", + nsIURL: true, nsINestedURI: false }, + { spec: "ftp:///", + scheme: "ftp", + prePath: "ftp://", + path: "/", + ref: "", + nsIURL: true, nsINestedURI: false }, + { spec: "ftp://ftp.mozilla.org/pub/mozilla.org/README", + scheme: "ftp", + prePath: "ftp://ftp.mozilla.org", + path: "/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", + path: "/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: "", + path: "/pub/mozilla.org/README", + ref: "", + nsIURL: true, nsINestedURI: false }, + //Bug 706249 + { spec: "gopher://mozilla.org/", + scheme: "gopher", + prePath: "gopher:", + path: "//mozilla.org/", + ref: "", + nsIURL: false, nsINestedURI: false }, + { spec: "http://", + scheme: "http", + prePath: "http://", + path: "/", + ref: "", + nsIURL: true, nsINestedURI: false }, + { spec: "http:///", + scheme: "http", + prePath: "http://", + path: "/", + ref: "", + nsIURL: true, nsINestedURI: false }, + { spec: "http://www.example.com/", + scheme: "http", + prePath: "http://www.example.com", + path: "/", + ref: "", + nsIURL: true, nsINestedURI: false }, + { spec: "http://www.exa\nmple.com/", + scheme: "http", + prePath: "http://www.example.com", + path: "/", + ref: "", + nsIURL: true, nsINestedURI: false }, + { spec: "http://10.32.4.239/", + scheme: "http", + prePath: "http://10.32.4.239", + host: "10.32.4.239", + path: "/", + ref: "", + nsIURL: true, nsINestedURI: false }, + { spec: "http://[::192.9.5.5]/ipng", + scheme: "http", + prePath: "http://[::192.9.5.5]", + host: "::192.9.5.5", + path: "/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, + path: "/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", + path: "/pub/mozilla.org/README.html", + ref: "", + nsIURL: true, nsINestedURI: false }, + { spec: "jar:resource://!/", + scheme: "jar", + prePath: "jar:", + path: "resource:///!/", + ref: "", + nsIURL: true, nsINestedURI: true }, + { spec: "jar:resource://gre/chrome.toolkit.jar!/", + scheme: "jar", + prePath: "jar:", + path: "resource://gre/chrome.toolkit.jar!/", + ref: "", + nsIURL: true, nsINestedURI: true }, + { spec: "mailto:webmaster@mozilla.com", + scheme: "mailto", + prePath: "mailto:", + path: "webmaster@mozilla.com", + ref: "", + nsIURL: false, nsINestedURI: false }, + { spec: "javascript:new Date()", + scheme: "javascript", + prePath: "javascript:", + path: "new%20Date()", + ref: "", + nsIURL: false, nsINestedURI: false }, + { spec: "blob:123456", + scheme: "blob", + prePath: "blob:", + path: "123456", + ref: "", + nsIURL: false, nsINestedURI: false, immutable: true }, + { spec: "place:sort=8&maxResults=10", + scheme: "place", + prePath: "place:", + path: "sort=8&maxResults=10", + ref: "", + nsIURL: false, nsINestedURI: false }, + { spec: "resource://gre/", + scheme: "resource", + prePath: "resource://gre", + path: "/", + ref: "", + nsIURL: true, nsINestedURI: false }, + { spec: "resource://gre/components/", + scheme: "resource", + prePath: "resource://gre", + path: "/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 do_check_true. +function do_check_uri_eq(aURI1, aURI2, aCheckTrueFunc) { + if (!aCheckTrueFunc) { + aCheckTrueFunc = do_check_true; + } + + 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 == do_check_true) { + 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 do_check_true. +function do_check_uri_eqExceptRef(aURI1, aURI2, aCheckTrueFunc) { + if (!aCheckTrueFunc) { + aCheckTrueFunc = do_check_true; + } + + 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 + "'"); + do_check_eq(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) { + do_check_eq(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) { + do_check_eq(e.result, aTest.relativeFail); + return; + } + do_throw(e.result); + } + do_info("relURI.path = " + relURI.path + ", was " + URI.path); + URI = relURI; + do_info("URI.path now = " + URI.path); + } + + // Sanity-check + do_info("testing " + aTest.spec + " equals a clone of itself"); + do_check_uri_eq(URI, URI.clone()); + do_check_uri_eqExceptRef(URI, URI.cloneIgnoringRef()); + do_info("testing " + aTest.spec + " instanceof nsIURL"); + do_check_eq(URI instanceof Ci.nsIURL, aTest.nsIURL); + do_info("testing " + aTest.spec + " instanceof nsINestedURI"); + do_check_eq(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; + } + do_check_true(threw || !isEqualToNull); + + + // Check the various components + do_check_property(aTest, URI, "scheme"); + do_check_property(aTest, URI, "prePath"); + do_check_property(aTest, URI, "path"); + 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); + do_check_eq(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 '#'"); + do_check_eq(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.clone()); + + do_info("testing " + aTest.spec + + " doesn't equal self with '" + aSuffix + "' appended"); + + do_check_false(origURI.equals(testURI)); + + do_info("testing " + aTest.spec + + " is equalExceptRef to self with '" + aSuffix + "' appended"); + do_check_uri_eqExceptRef(origURI, testURI); + + do_check_eq(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.cloneIgnoringRef(); + do_check_uri_eq(cloneNoRef, origURI); + do_check_false(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.cloneWithNewRef(""); + do_check_uri_eq(cloneNewRef, origURI); + do_check_uri_eq(cloneNewRef, cloneNoRef); + do_check_false(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.cloneWithNewRef(aSuffix); + do_check_uri_eq(cloneNewRef, testURI); + do_check_true(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, "path", + 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 '#'"); + do_check_eq(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.ref = aSuffix; + 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.ref = suffixLackingHash; + 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.ref = ""; + 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.spec = specWithSuffix; + testURI.ref = ""; + 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 .path directly (including suffix) and then clearing .ref + // (same as above, but with now with .path instead of .spec) + testURI = NetUtil.newURI(aTest.spec); + + var pathWithSuffix = aTest.path + aSuffix; + do_info("testing that setting path to " + + pathWithSuffix + " and then clearing ref does what we expect"); + testURI.path = pathWithSuffix; + testURI.ref = ""; + do_check_uri_eq(testURI, refURIWithoutSuffix); + do_check_uri_eqExceptRef(testURI, refURIWithSuffix); + + // Also: make sure that clearing .path also clears .ref + testURI.path = pathWithSuffix; + do_info("testing that clearing path from " + + pathWithSuffix + " also clears .ref"); + testURI.path = ""; + do_check_eq(testURI.ref, ""); + } + } +} + +// Tests that normally-mutable properties can't be modified on +// special URIs that are known to be immutable. +function do_test_immutable(aTest) { + do_check_true(aTest.immutable); + + var URI = NetUtil.newURI(aTest.spec); + // All the non-readonly attributes on nsIURI.idl: + var propertiesToCheck = ["spec", "scheme", "userPass", "username", "password", + "hostPort", "host", "port", "path", "ref"]; + + propertiesToCheck.forEach(function(aProperty) { + var threw = false; + try { + URI[aProperty] = "anothervalue"; + } catch(e) { + threw = true; + } + + do_info("testing that setting '" + aProperty + + "' on immutable URI '" + aTest.spec + "' will throw"); + do_check_true(threw); + }); +} + + +// TEST MAIN FUNCTION +// ------------------ +function run_test() +{ + // UTF-8 check - From bug 622981 + // ASCII + let base = gIoService.newURI("http://example.org/xenia?", null, null); + let resolved = gIoService.newURI("?x", null, base); + let expected = gIoService.newURI("http://example.org/xenia?x", + null, null); + do_info("Bug 662981: ACSII - comparing " + resolved.spec + " and " + expected.spec); + do_check_true(resolved.equals(expected)); + + // UTF-8 character "è" + // Bug 622981 was triggered by an empty query string + base = gIoService.newURI("http://example.org/xènia?", null, null); + resolved = gIoService.newURI("?x", null, base); + expected = gIoService.newURI("http://example.org/xènia?x", + null, null); + do_info("Bug 662981: UTF8 - comparing " + resolved.spec + " and " + expected.spec); + do_check_true(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) { + do_test_immutable(aTest); + } + } + }); +} diff --git a/netwerk/test/unit/test_URIs2.js b/netwerk/test/unit/test_URIs2.js new file mode 100644 index 0000000000..201473f58f --- /dev/null +++ b/netwerk/test/unit/test_URIs2.js @@ -0,0 +1,693 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +Components.utils.import("resource://gre/modules/NetUtil.jsm"); + +var gIoService = Components.classes["@mozilla.org/network/io-service;1"] + .getService(Components.interfaces.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:", + path: "about:blank", + ref: "", + nsIURL: false, nsINestedURI: true, immutable: true }, + { spec: "view-source:http://www.mozilla.org/", + scheme: "view-source", + prePath: "view-source:", + path: "http://www.mozilla.org/", + ref: "", + nsIURL: false, nsINestedURI: true, immutable: true }, + { spec: "x-external:", + scheme: "x-external", + prePath: "x-external:", + path: "", + ref: "", + nsIURL: false, nsINestedURI: false }, + { spec: "x-external:abc", + scheme: "x-external", + prePath: "x-external:", + path: "abc", + ref: "", + nsIURL: false, nsINestedURI: false }, + { spec: "http://www2.example.com/", + relativeURI: "a/b/c/d", + scheme: "http", + prePath: "http://www2.example.com", + path: "/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:", + path: "h", + ref: "", + nsIURL: false, nsINestedURI: false }, + { spec: "http://a/b/c/d;p?q", + relativeURI: "g", + scheme: "http", + prePath: "http://a", + path: "/b/c/g", + ref: "", + nsIURL: true, nsINestedURI: false }, + { spec: "http://a/b/c/d;p?q", + relativeURI: "./g", + scheme: "http", + prePath: "http://a", + path: "/b/c/g", + ref: "", + nsIURL: true, nsINestedURI: false }, + { spec: "http://a/b/c/d;p?q", + relativeURI: "g/", + scheme: "http", + prePath: "http://a", + path: "/b/c/g/", + ref: "", + nsIURL: true, nsINestedURI: false }, + { spec: "http://a/b/c/d;p?q", + relativeURI: "/g", + scheme: "http", + prePath: "http://a", + path: "/g", + ref: "", + nsIURL: true, nsINestedURI: false }, + { spec: "http://a/b/c/d;p?q", + relativeURI: "?y", + scheme: "http", + prePath: "http://a", + path: "/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", + path: "/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", + path: "/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", + path: "/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", + path: "/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", + path: "/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", + path: "/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", + path: "/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", + path: "/b/c/d", + ref: "", + nsIURL: true, nsINestedURI: false }, + */ + { spec: "http://a/b/c/d;p?q", + relativeURI: ".", + scheme: "http", + prePath: "http://a", + path: "/b/c/", + ref: "", + nsIURL: true, nsINestedURI: false }, + { spec: "http://a/b/c/d;p?q", + relativeURI: "./", + scheme: "http", + prePath: "http://a", + path: "/b/c/", + ref: "", + nsIURL: true, nsINestedURI: false }, + { spec: "http://a/b/c/d;p?q", + relativeURI: "..", + scheme: "http", + prePath: "http://a", + path: "/b/", + ref: "", + nsIURL: true, nsINestedURI: false }, + { spec: "http://a/b/c/d;p?q", + relativeURI: "../", + scheme: "http", + prePath: "http://a", + path: "/b/", + ref: "", + nsIURL: true, nsINestedURI: false }, + { spec: "http://a/b/c/d;p?q", + relativeURI: "../g", + scheme: "http", + prePath: "http://a", + path: "/b/g", + ref: "", + nsIURL: true, nsINestedURI: false }, + { spec: "http://a/b/c/d;p?q", + relativeURI: "../..", + scheme: "http", + prePath: "http://a", + path: "/", + ref: "", + nsIURL: true, nsINestedURI: false }, + { spec: "http://a/b/c/d;p?q", + relativeURI: "../../", + scheme: "http", + prePath: "http://a", + path: "/", + ref: "", + nsIURL: true, nsINestedURI: false }, + { spec: "http://a/b/c/d;p?q", + relativeURI: "../../g", + scheme: "http", + prePath: "http://a", + path: "/g", + ref: "", + nsIURL: true, nsINestedURI: false }, + + // abnormal examples + { spec: "http://a/b/c/d;p?q", + relativeURI: "../../../g", + scheme: "http", + prePath: "http://a", + path: "/g", + ref: "", + nsIURL: true, nsINestedURI: false }, + { spec: "http://a/b/c/d;p?q", + relativeURI: "../../../../g", + scheme: "http", + prePath: "http://a", + path: "/g", + ref: "", + nsIURL: true, nsINestedURI: false }, + + // coalesce + { spec: "http://a/b/c/d;p?q", + relativeURI: "/./g", + scheme: "http", + prePath: "http://a", + path: "/g", + ref: "", + nsIURL: true, nsINestedURI: false }, + { spec: "http://a/b/c/d;p?q", + relativeURI: "/../g", + scheme: "http", + prePath: "http://a", + path: "/g", + ref: "", + nsIURL: true, nsINestedURI: false }, + { spec: "http://a/b/c/d;p?q", + relativeURI: "g.", + scheme: "http", + prePath: "http://a", + path: "/b/c/g.", + ref: "", + nsIURL: true, nsINestedURI: false }, + { spec: "http://a/b/c/d;p?q", + relativeURI: ".g", + scheme: "http", + prePath: "http://a", + path: "/b/c/.g", + ref: "", + nsIURL: true, nsINestedURI: false }, + { spec: "http://a/b/c/d;p?q", + relativeURI: "g..", + scheme: "http", + prePath: "http://a", + path: "/b/c/g..", + ref: "", + nsIURL: true, nsINestedURI: false }, + { spec: "http://a/b/c/d;p?q", + relativeURI: "..g", + scheme: "http", + prePath: "http://a", + path: "/b/c/..g", + ref: "", + nsIURL: true, nsINestedURI: false }, + { spec: "http://a/b/c/d;p?q", + relativeURI: ".", + scheme: "http", + prePath: "http://a", + path: "/b/c/", + ref: "", + nsIURL: true, nsINestedURI: false }, + { spec: "http://a/b/c/d;p?q", + relativeURI: "./../g", + scheme: "http", + prePath: "http://a", + path: "/b/g", + ref: "", + nsIURL: true, nsINestedURI: false }, + { spec: "http://a/b/c/d;p?q", + relativeURI: "./g/.", + scheme: "http", + prePath: "http://a", + path: "/b/c/g/", + ref: "", + nsIURL: true, nsINestedURI: false }, + { spec: "http://a/b/c/d;p?q", + relativeURI: "g/./h", + scheme: "http", + prePath: "http://a", + path: "/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", + path: "/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", + path: "/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", + path: "/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", + path: "/bar", + ref: "", + nsIURL: true, nsINestedURI: false }, + { spec: "https://www2.example.com/", + relativeURI: "//www3.example2.com/bar", + scheme: "https", + prePath: "https://www3.example2.com", + path: "/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 do_check_true. +function do_check_uri_eq(aURI1, aURI2, aCheckTrueFunc) { + if (!aCheckTrueFunc) { + aCheckTrueFunc = do_check_true; + } + + 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 == do_check_true) { + 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 do_check_true. +function do_check_uri_eqExceptRef(aURI1, aURI2, aCheckTrueFunc) { + if (!aCheckTrueFunc) { + aCheckTrueFunc = do_check_true; + } + + 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 + "'"); + do_check_eq(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) { + do_check_eq(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) { + do_check_eq(e.result, aTest.relativeFail); + return; + } + do_throw(e.result); + } + do_info("relURI.path = " + relURI.path + ", was " + URI.path); + URI = relURI; + do_info("URI.path now = " + URI.path); + } + + // Sanity-check + do_info("testing " + aTest.spec + " equals a clone of itself"); + do_check_uri_eq(URI, URI.clone()); + do_check_uri_eqExceptRef(URI, URI.cloneIgnoringRef()); + do_info("testing " + aTest.spec + " instanceof nsIURL"); + do_check_eq(URI instanceof Ci.nsIURL, aTest.nsIURL); + do_info("testing " + aTest.spec + " instanceof nsINestedURI"); + do_check_eq(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; + } + do_check_true(threw || !isEqualToNull); + + + // Check the various components + do_check_property(aTest, URI, "scheme"); + do_check_property(aTest, URI, "prePath"); + do_check_property(aTest, URI, "path"); + 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); + do_check_eq(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 '#'"); + do_check_eq(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.clone()); + + do_info("testing " + aTest.spec + + " doesn't equal self with '" + aSuffix + "' appended"); + + do_check_false(origURI.equals(testURI)); + + do_info("testing " + aTest.spec + + " is equalExceptRef to self with '" + aSuffix + "' appended"); + do_check_uri_eqExceptRef(origURI, testURI); + + do_check_eq(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.cloneIgnoringRef(); + do_check_uri_eq(cloneNoRef, origURI); + do_check_false(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, "path", + 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 '#'"); + do_check_eq(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.ref = aSuffix; + 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.ref = suffixLackingHash; + 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.ref = ""; + 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.spec = specWithSuffix; + testURI.ref = ""; + 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 .path directly (including suffix) and then clearing .ref + // (same as above, but with now with .path instead of .spec) + testURI = NetUtil.newURI(aTest.spec); + + var pathWithSuffix = aTest.path + aSuffix; + do_info("testing that setting path to " + + pathWithSuffix + " and then clearing ref does what we expect"); + testURI.path = pathWithSuffix; + testURI.ref = ""; + do_check_uri_eq(testURI, refURIWithoutSuffix); + do_check_uri_eqExceptRef(testURI, refURIWithSuffix); + + // Also: make sure that clearing .path also clears .ref + testURI.path = pathWithSuffix; + do_info("testing that clearing path from " + + pathWithSuffix + " also clears .ref"); + testURI.path = ""; + do_check_eq(testURI.ref, ""); + } + } +} + +// Tests that normally-mutable properties can't be modified on +// special URIs that are known to be immutable. +function do_test_immutable(aTest) { + do_check_true(aTest.immutable); + + var URI = NetUtil.newURI(aTest.spec); + // All the non-readonly attributes on nsIURI.idl: + var propertiesToCheck = ["spec", "scheme", "userPass", "username", "password", + "hostPort", "host", "port", "path", "ref"]; + + propertiesToCheck.forEach(function(aProperty) { + var threw = false; + try { + URI[aProperty] = "anothervalue"; + } catch(e) { + threw = true; + } + + do_info("testing that setting '" + aProperty + + "' on immutable URI '" + aTest.spec + "' will throw"); + do_check_true(threw); + }); +} + + +// TEST MAIN FUNCTION +// ------------------ +function run_test() +{ + // UTF-8 check - From bug 622981 + // ASCII + let base = gIoService.newURI("http://example.org/xenia?", null, null); + let resolved = gIoService.newURI("?x", null, base); + let expected = gIoService.newURI("http://example.org/xenia?x", + null, null); + do_info("Bug 662981: ACSII - comparing " + resolved.spec + " and " + expected.spec); + do_check_true(resolved.equals(expected)); + + // UTF-8 character "è" + // Bug 622981 was triggered by an empty query string + base = gIoService.newURI("http://example.org/xènia?", null, null); + resolved = gIoService.newURI("?x", null, base); + expected = gIoService.newURI("http://example.org/xènia?x", + null, null); + do_info("Bug 662981: UTF8 - comparing " + resolved.spec + " and " + expected.spec); + do_check_true(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) { + do_test_immutable(aTest); + } + } + }); +} diff --git a/netwerk/test/unit/test_XHR_redirects.js b/netwerk/test/unit/test_XHR_redirects.js new file mode 100644 index 0000000000..8c9b42437b --- /dev/null +++ b/netwerk/test/unit/test_XHR_redirects.js @@ -0,0 +1,235 @@ +// 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. + +Cu.import("resource://testing-common/httpd.js"); +Cu.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 = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"] + .createInstance(Ci.nsIXMLHttpRequest); + 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; + do_check_eq(xhr.status, status); + + if (status == 200) { + // if followed then check for echoed method name + do_check_eq(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..efcd5910ea --- /dev/null +++ b/netwerk/test/unit/test_about_networking.js @@ -0,0 +1,96 @@ +/* -*- 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/. */ + +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +const gDashboard = Cc['@mozilla.org/network/dashboard;1'] + .getService(Ci.nsIDashboard); + +const gServerSocket = Components.classes["@mozilla.org/network/server-socket;1"] + .createInstance(Components.interfaces.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; + } + } + do_check_eq(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; + } + } + do_check_eq(found, true); + + do_test_pending(); + gHttpServer.stop(do_test_finished); + + run_next_test(); + }); +}); + +add_test(function test_sockets() { + 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(null, 0, "127.0.0.1", + gServerSocket.port, null); + let listener = { + onTransportStatus: function(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; + } + } + do_check_eq(found, true); + + run_next_test(); + }); + } + } + }; + transport.setEventSink(listener, threadManager.currentThread); + + transport.openOutputStream(Ci.nsITransport.OPEN_BLOCKING, 0, 0); +}); + +function run_test() { + let ioService = Cc["@mozilla.org/network/io-service;1"] + .getService(Ci.nsIIOService); + + gHttpServer.start(-1); + + let uri = ioService.newURI("http://localhost:" + gHttpServer.identity.primaryPort, + null, null); + let channel = NetUtil.newChannel({uri: uri, loadUsingSystemPrincipal: true}); + + channel.open2(); + + gServerSocket.init(-1, true, -1); + + 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..8f45d1c18d --- /dev/null +++ b/netwerk/test/unit/test_about_protocol.js @@ -0,0 +1,50 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var Ci = Components.interfaces; +var Cc = Components.classes; + +Components.utils.import("resource://gre/modules/Services.jsm"); +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); +Components.utils.import("resource://gre/modules/NetUtil.jsm"); + +var unsafeAboutModule = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIAboutModule]), + newChannel: function (aURI, aLoadInfo) { + var uri = Services.io.newURI("about:blank", null, null); + let chan = Services.io.newChannelFromURIWithLoadInfo(uri, aLoadInfo); + chan.owner = Services.scriptSecurityManager.getSystemPrincipal(); + return chan; + }, + getURIFlags: function (aURI) { + return Ci.nsIAboutModule.URI_SAFE_FOR_UNTRUSTED_CONTENT; + } +}; + +var factory = { + createInstance: function(aOuter, aIID) { + if (aOuter) + throw Components.results.NS_ERROR_NO_AGGREGATION; + return unsafeAboutModule.QueryInterface(aIID); + }, + lockFactory: function(aLock) { + throw Components.results.NS_ERROR_NOT_IMPLEMENTED; + }, + QueryInterface: XPCOMUtils.generateQI([Ci.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 + }); + + do_check_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..2abe3c5768 --- /dev/null +++ b/netwerk/test/unit/test_aboutblank.js @@ -0,0 +1,32 @@ +Cu.import("resource://gre/modules/NetUtil.jsm"); + +function run_test() { + var base = NetUtil.newURI("http://www.example.com", null, null); + var about1 = NetUtil.newURI("about:blank", null, null); + var about2 = NetUtil.newURI("about:blank", null, base); + + var chan1 = NetUtil.newChannel({ + uri: about1, + loadUsingSystemPrincipal: true + }).QueryInterface(Components.interfaces.nsIPropertyBag2); + + var chan2 = NetUtil.newChannel({ + uri: about2, + loadUsingSystemPrincipal: true + }).QueryInterface(Components.interfaces.nsIPropertyBag2); + + var haveProp = false; + var propVal = null; + try { + propVal = chan1.getPropertyAsInterface("baseURI", + Components.interfaces.nsIURI); + haveProp = true; + } catch (e if e.result == Components.results.NS_ERROR_NOT_AVAILABLE) { + // Property shouldn't be there. + } + do_check_eq(propVal, null); + do_check_eq(haveProp, false); + do_check_eq(chan2.getPropertyAsInterface("baseURI", + Components.interfaces.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..736282a118 --- /dev/null +++ b/netwerk/test/unit/test_addr_in_use_error.js @@ -0,0 +1,32 @@ +// 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. + +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); + do_check_true(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_simple.js b/netwerk/test/unit/test_alt-data_simple.js new file mode 100644 index 0000000000..a140809231 --- /dev/null +++ b/netwerk/test/unit/test_alt-data_simple.js @@ -0,0 +1,111 @@ +/** + * 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 + */ + +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); + +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}); +} + +const responseContent = "response body"; +const altContent = "!@#$%^&*()"; +const altContentType = "text/binary"; + +var servedNotModified = false; + +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") { + response.setStatusLine(metadata.httpVersion, 304, "Not Modified"); + servedNotModified = true; + } else { + 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); + + chan.asyncOpen2(new ChannelListener(readServerContent, null)); + do_test_pending(); +} + +function readServerContent(request, buffer) +{ + var cc = request.QueryInterface(Ci.nsICacheInfoChannel); + + do_check_eq(buffer, responseContent); + do_check_eq(cc.alternativeDataType, ""); + + do_execute_soon(() => { + var os = cc.openAlternativeOutputStream(altContentType); + os.write(altContent, altContent.length); + os.close(); + + do_execute_soon(flushAndOpenAltChannel); + }); +} + +// needs to be rooted +var cacheFlushObserver = cacheFlushObserver = { observe: function() { + cacheFlushObserver = null; + + var chan = make_channel(URL); + var cc = chan.QueryInterface(Ci.nsICacheInfoChannel); + cc.preferAlternativeDataType(altContentType); + + chan.asyncOpen2(new ChannelListener(readAltContent, null)); +}}; + +function flushAndOpenAltChannel() +{ + // We need to do a GC pass to ensure the cache entry has been freed. + gc(); + Services.cache2.QueryInterface(Ci.nsICacheTesting).flush(cacheFlushObserver); +} + +function readAltContent(request, buffer) +{ + var cc = request.QueryInterface(Ci.nsICacheInfoChannel); + + do_check_eq(servedNotModified, true); + do_check_eq(cc.alternativeDataType, altContentType); + do_check_eq(buffer, altContent); + + 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..da3794dd06 --- /dev/null +++ b/netwerk/test/unit/test_alt-data_stream.js @@ -0,0 +1,120 @@ +/** + * 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. + * + */ + +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); + +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}); +} + +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); + + chan.asyncOpen2(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); + + do_check_eq(buffer, responseContent); + do_check_eq(cc.alternativeDataType, ""); + + do_execute_soon(() => { + os = cc.openAlternativeOutputStream(altContentType); + // Write a quarter of the alt data content + os.write(altContent, firstChunkSize); + + do_execute_soon(openAltChannel); + }); +} + +function openAltChannel() +{ + var chan = make_channel(URL); + var cc = chan.QueryInterface(Ci.nsICacheInfoChannel); + cc.preferAlternativeDataType(altContentType); + + chan.asyncOpen2(listener); +} + +var listener = { + buffer: "", + onStartRequest: function(request, context) { }, + onDataAvailable: function(request, context, 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: function(request, context, status) { + var cc = request.QueryInterface(Ci.nsICacheInfoChannel); + do_check_eq(cc.alternativeDataType, altContentType); + do_check_eq(this.buffer.length, altContent.length); + do_check_eq(this.buffer, altContent); + httpServer.stop(do_test_finished); + }, +}; diff --git a/netwerk/test/unit/test_altsvc.js b/netwerk/test/unit/test_altsvc.js new file mode 100644 index 0000000000..7dafca0285 --- /dev/null +++ b/netwerk/test/unit/test_altsvc.js @@ -0,0 +1,378 @@ +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +var h2Port; +var prefs; +var spdypref; +var http2pref; +var tlspref; +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 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"); + do_check_neq(h2Port, null); + do_check_neq(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"); + tlspref = prefs.getBoolPref("network.http.spdy.enforce-tls-profile"); + 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.spdy.enforce-tls-profile", false); + 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 CA.cert.der + // 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, "CA.cert.der", "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 + '": { "tls-ports": [' + h2Port + '] }}'; + 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.spdy.enforce-tls-profile", tlspref); + prefs.setBoolPref("network.http.altsvc.enabled", altsvcpref1); + prefs.setBoolPref("network.http.altsvc.oe", altsvcpref2); + prefs.clearUserPref("network.dns.localDomains"); +} + +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 der = readFile(certFile); + certdb.addCert(der, trustString, null); +} + +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 nextTest; +var expectPass = true; +var waitFor = 0; + +var Listener = function() {}; +Listener.prototype = { + onStartRequest: function testOnStartRequest(request, ctx) { + do_check_true(request instanceof Components.interfaces.nsIHttpChannel); + + if (expectPass) { + if (!Components.isSuccessCode(request.status)) { + do_throw("Channel should have a success code! (" + request.status + ")"); + } + do_check_eq(request.responseStatus, 200); + } else { + do_check_eq(Components.isSuccessCode(request.status), false); + } + }, + + onDataAvailable: function testOnDataAvailable(request, ctx, stream, off, cnt) { + read_stream(stream, cnt); + }, + + onStopRequest: function testOnStopRequest(request, ctx, status) { + var routed = ""; + try { + routed = request.getRequestHeader("Alt-Used"); + } catch (e) {} + dump("routed is " + routed + "\n"); + do_check_eq(Components.isSuccessCode(status), expectPass); + + if (waitFor != 0) { + do_check_eq(routed, ""); + do_test_pending(); + loadWithoutClearingMappings = true; + do_timeout(waitFor, doTest); + waitFor = 0; + xaltsvc = "NA"; + } else if (xaltsvc == "NA") { + do_check_eq(routed, ""); + nextTest(); + } else if (routed == xaltsvc) { + do_check_eq(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(); + 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; + } + loadWithoutClearingMappings = false; + chan.asyncOpen2(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 = testsDone; + do_test_pending(); + doTest(); +} + diff --git a/netwerk/test/unit/test_assoc.js b/netwerk/test/unit/test_assoc.js new file mode 100644 index 0000000000..ded2e3d5ab --- /dev/null +++ b/netwerk/test/unit/test_assoc.js @@ -0,0 +1,102 @@ +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +var httpserver = new HttpServer(); +var currentTestIndex = 0; + +XPCOMUtils.defineLazyGetter(this, "port", function() { + return httpserver.identity.primaryPort; +}); + +XPCOMUtils.defineLazyGetter(this, "tests", function() { + return [ + // this is valid + {url: "/assoc/assoctest?valid", + responseheader: ["Assoc-Req: GET http://localhost:" + port + + "/assoc/assoctest?valid", + "Pragma: X-Verify-Assoc-Req"], + flags: 0}, + + // this is invalid because the method is wrong + {url: "/assoc/assoctest?invalid", + responseheader: ["Assoc-Req: POST http://localhost:" + port + + "/assoc/assoctest?invalid", + "Pragma: X-Verify-Assoc-Req"], + flags: CL_EXPECT_LATE_FAILURE}, + + // this is invalid because the url is wrong + {url: "/assoc/assoctest?notvalid", + responseheader: ["Assoc-Req: GET http://localhost:" + port + + "/wrongpath/assoc/assoctest?notvalid", + "Pragma: X-Verify-Assoc-Req"], + flags: CL_EXPECT_LATE_FAILURE}, + + // this is invalid because the space between method and URL is missing + {url: "/assoc/assoctest?invalid2", + responseheader: ["Assoc-Req: GEThttp://localhost:" + port + + "/assoc/assoctest?invalid2", + "Pragma: X-Verify-Assoc-Req"], + flags: CL_EXPECT_LATE_FAILURE}, + ]; +}); + +var oldPrefVal; +var domBranch; + +function setupChannel(url) +{ + return NetUtil.newChannel({ + uri: "http://localhost:" + port + url, + loadUsingSystemPrincipal: true + }); +} + +function startIter() +{ + var channel = setupChannel(tests[currentTestIndex].url); + channel.asyncOpen2(new ChannelListener(completeIter, + channel, tests[currentTestIndex].flags)); +} + +function completeIter(request, data, ctx) +{ + if (++currentTestIndex < tests.length ) { + startIter(); + } else { + domBranch.setBoolPref("enforce", oldPrefVal); + httpserver.stop(do_test_finished); + } +} + +function run_test() +{ + var prefService = + Components.classes["@mozilla.org/preferences-service;1"] + .getService(Components.interfaces.nsIPrefService); + domBranch = prefService.getBranch("network.http.assoc-req."); + oldPrefVal = domBranch.getBoolPref("enforce"); + domBranch.setBoolPref("enforce", true); + + httpserver.registerPathHandler("/assoc/assoctest", 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[currentTestIndex].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, 200, "OK"); + response.bodyOutputStream.write(body, body.length); +} 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..a45ef7532e --- /dev/null +++ b/netwerk/test/unit/test_auth_dialog_permission.js @@ -0,0 +1,255 @@ +// 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. + +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +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: function authprompt_qi(iid) { + if (iid.equals(Components.interfaces.nsISupports) || + iid.equals(Components.interfaces.nsIAuthPrompt)) + return this; + throw Components.results.NS_ERROR_NO_INTERFACE; + }, + + prompt: function(title, text, realm, save, defaultText, result) { + do_throw("unexpected prompt call"); + }, + + promptUsernameAndPassword: function(title, text, realm, savePW, user, pw) { + do_check_true(this.promptExpected, + "Not expected the authentication prompt."); + + user.value = this.user; + pw.value = this.pass; + return true; + }, + + promptPassword: function(title, text, realm, save, pwd) { + do_throw("unexpected promptPassword call"); + } + +}; + +function Requestor(promptExpected) { + this.promptExpected = promptExpected; +} + +Requestor.prototype = { + QueryInterface: function(iid) { + if (iid.equals(Components.interfaces.nsISupports) || + iid.equals(Components.interfaces.nsIInterfaceRequestor)) + return this; + throw Components.results.NS_ERROR_NO_INTERFACE; + }, + + getInterface: function(iid) { + if (iid.equals(Components.interfaces.nsIAuthPrompt)) { + this.prompter = new AuthPrompt(this.promptExpected); + return this.prompter; + } + + throw Components.results.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, null, null); +} + +function makeChan(loadingUrl, url, contentPolicy) { + var ssm = Cc["@mozilla.org/scriptsecuritymanager;1"] + .getService(Ci.nsIScriptSecurityManager); + var uri = make_uri(loadingUrl); + var principal = ssm.createCodebasePrincipal(uri, {}); + + return NetUtil.newChannel({ + uri: url, + loadingPrincipal: principal, + securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_INHERITS, + contentPolicyType: contentPolicy + }).QueryInterface(Components.interfaces.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: function(request, ctx) { + try { + if (!Components.isSuccessCode(request.status)) { + do_throw("Channel should have a success code!"); + } + + if (!(request instanceof Components.interfaces.nsIHttpChannel)) { + do_throw("Expecting an HTTP channel"); + } + + do_check_eq(request.responseStatus, this._expectedCode); + // The request should be succeeded iff we expect 200 + do_check_eq(request.requestSucceeded, this._expectedCode == 200); + + } catch (e) { + do_throw("Unexpected exception: " + e); + } + + throw Components.results.NS_ERROR_ABORT; + }, + + onDataAvailable: function(request, context, stream, offset, count) { + do_throw("Should not get any data!"); + }, + + onStopRequest: function(request, ctx, status) { + do_check_eq(status, Components.results.NS_ERROR_ABORT); + + // Clear the auth cache. + Components.classes["@mozilla.org/network/http-auth-manager;1"] + .getService(Components.interfaces.nsIHttpAuthManager) + .clearAll(); + + do_timeout(0, run_next_test); + }, + + run: function() { + 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.asyncOpen2(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..e3050105e7 --- /dev/null +++ b/netwerk/test/unit/test_auth_jar.js @@ -0,0 +1,49 @@ +Components.utils.import("resource://gre/modules/Services.jsm"); +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); + +function createURI(s) { + let service = Components.classes["@mozilla.org/network/io-service;1"] + .getService(Components.interfaces.nsIIOService); + return service.newURI(s, null, null); +} + +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 app1 = secMan.createCodebasePrincipal(createURI(kURI1), {appId: 1}); + var app10 = secMan.createCodebasePrincipal(createURI(kURI1),{appId: 10}); + var app1browser = secMan.createCodebasePrincipal(createURI(kURI1), {appId: 1, 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, app1); + am.setAuthIdentity("http", "a.example.com", -1, "basic", "realm", "", "example.com", "user3", "pass3", false, app1browser); + am.setAuthIdentity("http", "a.example.com", -1, "basic", "realm", "", "example.com", "user2", "pass2", false, app10); + + let attrs_inBrowser = JSON.stringify({ appId:1, inIsolatedMozBrowser:true }); + Services.obs.notifyObservers(null, "clear-origin-attributes-data", attrs_inBrowser); + + var domain = {value: ""}, user = {value: ""}, pass = {value: ""}; + try { + am.getAuthIdentity("http", "a.example.com", -1, "basic", "realm", "", domain, user, pass, false, app1browser); + do_check_false(true); // no identity should be present + } catch (x) { + do_check_eq(domain.value, ""); + do_check_eq(user.value, ""); + do_check_eq(pass.value, ""); + } + + am.getAuthIdentity("http", "a.example.com", -1, "basic", "realm", "", domain, user, pass, false, app1); + do_check_eq(domain.value, "example.com"); + do_check_eq(user.value, "user"); + do_check_eq(pass.value, "pass"); + + + am.getAuthIdentity("http", "a.example.com", -1, "basic", "realm", "", domain, user, pass, false, app10); + do_check_eq(domain.value, "example.com"); + do_check_eq(user.value, "user2"); + do_check_eq(pass.value, "pass2"); +} diff --git a/netwerk/test/unit/test_auth_proxy.js b/netwerk/test/unit/test_auth_proxy.js new file mode 100644 index 0000000000..ae5260ade8 --- /dev/null +++ b/netwerk/test/unit/test_auth_proxy.js @@ -0,0 +1,399 @@ +/* -*- 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"/> + */ + +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +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: function authprompt2_qi(iid) { + if (iid.equals(Ci.nsISupports) || + iid.equals(Ci.nsIAuthPrompt2)) + return this; + throw Cr.NS_ERROR_NO_INTERFACE; + }, + + promptAuth: + function ap2_promptAuth(channel, encryptionLevel, authInfo) + { + try { + + // never HOST and PROXY set at the same time in prompt + do_check_eq((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) + do_check_eq(cred.realmExpected, authInfo.realm); + + // PREVIOUS_FAILED properly set by necko + do_check_eq((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; + do_execute_soon(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: function cancelable_qi(iid) { + if (iid.equals(Ci.nsISupports) || + iid.equals(Ci.nsICancelable)) + return this; + throw Cr.NS_ERROR_NO_INTERFACE; + }, + cancel: function cancel() { + try { + this.onCancelFunc(); + } catch (e) { do_throw(e); } + } +}; + +function Requestor(proxyFlags, hostFlags) { + this.proxyFlags = proxyFlags; + this.hostFlags = hostFlags; +} +Requestor.prototype = { + QueryInterface: function requestor_qi(iid) { + if (iid.equals(Ci.nsISupports) || + iid.equals(Ci.nsIInterfaceRequestor)) + return this; + throw Cr.NS_ERROR_NO_INTERFACE; + }, + + getInterface: function requestor_gi(iid) { + if (iid.equals(Ci.nsIAuthPrompt)) { + dump("authprompt1 not implemented\n"); + throw 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 Cr.NS_ERROR_NO_INTERFACE; + }, + + prompt2: null +}; + +var listener = { + expectedCode: -1, // uninitialized + + onStartRequest: function test_onStartR(request, ctx) { + 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"); + + do_check_eq(this.expectedCode, request.responseStatus); + // If we expect 200, the request should have succeeded + do_check_eq(this.expectedCode == 200, request.requestSucceeded); + + var cookie = ""; + try { + cookie = request.getRequestHeader("Cookie"); + } catch (e) { } + do_check_eq(cookie, ""); + + } catch (e) { + do_throw("Unexpected exception: " + e); + } + + throw Cr.NS_ERROR_ABORT; + }, + + onDataAvailable: function test_ODA() { + do_throw("Should not get any data!"); + }, + + onStopRequest: function test_onStopR(request, ctx, status) { + do_check_eq(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); + + 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.setCharPref("network.proxy.no_proxies_on", ""); + prefs.setIntPref("network.proxy.type", 1); + + // Turn off the authentication dialog blocking for this test. + prefs.setIntPref("network.auth.subresource-http-auth-allow", 2); + + 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.asyncOpen2(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.asyncOpen2(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.asyncOpen2(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.asyncOpen2(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.asyncOpen2(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.asyncOpen2(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.asyncOpen2(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.asyncOpen2(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.asyncOpen2(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..a7e059a2b7 --- /dev/null +++ b/netwerk/test/unit/test_authentication.js @@ -0,0 +1,2074 @@ +// This file tests authentication prompt callbacks +// TODO NIT use do_check_eq(expected, actual) consistently, not sometimes eq(actual, expected) + +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +// 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 nsIAuthPrompt2 = Components.interfaces.nsIAuthPrompt2; +const nsIAuthInformation = Components.interfaces.nsIAuthInformation; + + +function AuthPrompt1(flags) { + this.flags = flags; +} + +AuthPrompt1.prototype = { + user: "guest", + pass: "guest", + + expectedRealm: "secret", + + QueryInterface: function authprompt_qi(iid) { + if (iid.equals(Components.interfaces.nsISupports) || + iid.equals(Components.interfaces.nsIAuthPrompt)) + return this; + throw Components.results.NS_ERROR_NO_INTERFACE; + }, + + 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) + { + // Note that the realm here isn't actually the realm. it's a pw mgr key. + do_check_eq(URL + " (" + this.expectedRealm + ")", realm); + if (!(this.flags & CROSS_ORIGIN)) { + if (text.indexOf(this.expectedRealm) == -1) { + do_throw("Text must indicate the realm"); + } + } else { + if (text.indexOf(this.expectedRealm) != -1) { + do_throw("There should not be realm for cross origin"); + } + } + if (text.indexOf("localhost") == -1) + do_throw("Text must indicate the hostname"); + if (text.indexOf(String(PORT)) == -1) + do_throw("Text must indicate the port"); + if (text.indexOf("-1") != -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"; + + 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 { + 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: function authprompt2_qi(iid) { + if (iid.equals(Components.interfaces.nsISupports) || + iid.equals(Components.interfaces.nsIAuthPrompt2)) + return this; + throw Components.results.NS_ERROR_NO_INTERFACE; + }, + + promptAuth: + function ap2_promptAuth(channel, level, authInfo) + { + var isNTLM = channel.URI.path.indexOf("ntlm") != -1; + var isDigest = channel.URI.path.indexOf("digest") != -1; + + if (isNTLM) + this.expectedRealm = ""; // NTLM knows no realms + + do_check_eq(this.expectedRealm, authInfo.realm); + + var expectedLevel = (isNTLM || isDigest) ? + nsIAuthPrompt2.LEVEL_PW_ENCRYPTED : + nsIAuthPrompt2.LEVEL_NONE; + do_check_eq(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 = 63; // Don't fail test for newly added flags + do_check_eq(expectedFlags, authInfo.flags & kAllKnownFlags); + + var expectedScheme = isNTLM ? "ntlm" : isDigest ? "digest" : "basic"; + do_check_eq(expectedScheme, authInfo.authenticationScheme); + + // No passwords in the URL -> nothing should be prefilled + do_check_eq(authInfo.username, ""); + do_check_eq(authInfo.password, ""); + do_check_eq(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"; + + 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 { + authInfo.password = this.pass; + this.flags &= ~FLAG_PREVIOUS_FAILED; + } + return true; + }, + + asyncPromptAuth: function ap2_async(chan, cb, ctx, lvl, info) { + throw Components.results.NS_ERROR_NOT_IMPLEMENTED; + } +}; + +function Requestor(flags, versions) { + this.flags = flags; + this.versions = versions; +} + +Requestor.prototype = { + QueryInterface: function requestor_qi(iid) { + if (iid.equals(Components.interfaces.nsISupports) || + iid.equals(Components.interfaces.nsIInterfaceRequestor)) + return this; + throw Components.results.NS_ERROR_NO_INTERFACE; + }, + + getInterface: function requestor_gi(iid) { + if (this.versions & 1 && + iid.equals(Components.interfaces.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(Components.interfaces.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.results.NS_ERROR_NO_INTERFACE; + }, + + prompt1: null, + prompt2: null +}; + +function RealmTestRequestor() {} + +RealmTestRequestor.prototype = { + QueryInterface: function realmtest_qi(iid) { + if (iid.equals(Components.interfaces.nsISupports) || + iid.equals(Components.interfaces.nsIInterfaceRequestor) || + iid.equals(Components.interfaces.nsIAuthPrompt2)) + return this; + throw Components.results.NS_ERROR_NO_INTERFACE; + }, + + getInterface: function realmtest_interface(iid) { + if (iid.equals(Components.interfaces.nsIAuthPrompt2)) + return this; + + throw Components.results.NS_ERROR_NO_INTERFACE; + }, + + promptAuth: function realmtest_checkAuth(channel, level, authInfo) { + do_check_eq(authInfo.realm, '\"foo_bar'); + + return false; + }, + + asyncPromptAuth: function realmtest_async(chan, cb, ctx, lvl, info) { + throw Components.results.NS_ERROR_NOT_IMPLEMENTED; + } +}; + +var listener = { + expectedCode: -1, // Uninitialized + + onStartRequest: function test_onStartR(request, ctx) { + try { + if (!Components.isSuccessCode(request.status)) + do_throw("Channel should have a success code!"); + + if (!(request instanceof Components.interfaces.nsIHttpChannel)) + do_throw("Expecting an HTTP channel"); + + do_check_eq(request.responseStatus, this.expectedCode); + // The request should be succeeded iff we expect 200 + do_check_eq(request.requestSucceeded, this.expectedCode == 200); + + } catch (e) { + do_throw("Unexpected exception: " + e); + } + + throw Components.results.NS_ERROR_ABORT; + }, + + onDataAvailable: function test_ODA() { + do_throw("Should not get any data!"); + }, + + onStopRequest: function test_onStopR(request, ctx, status) { + do_check_eq(status, Components.results.NS_ERROR_ABORT); + + if (current_test < (tests.length - 1)) { + // First, gotta clear the auth cache + Components.classes["@mozilla.org/network/http-auth-manager;1"] + .getService(Components.interfaces.nsIHttpAuthManager) + .clearAll(); + + current_test++; + tests[current_test](); + } else { + do_test_pending(); + httpserv.stop(do_test_finished); + } + + do_test_finished(); + } +}; + +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.createCodebasePrincipal(ios.newURI(loadingUrl, null, null), {}); + return NetUtil.newChannel( + { uri: url, loadingPrincipal: principal, + securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL, + contentPolicyType: Components.interfaces.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_digest_noauth, test_digest, + test_digest_bogus_user, test_large_realm, test_large_domain]; + +var current_test = 0; + +var httpserv = null; + +function run_test() { + httpserv = new HttpServer(); + + httpserv.registerPathHandler("/auth", authHandler); + httpserv.registerPathHandler("/auth/ntlm/simple", authNtlmSimple); + httpserv.registerPathHandler("/auth/realm", authRealm); + httpserv.registerPathHandler("/auth/digest", authDigest); + 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.asyncOpen2(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.asyncOpen2(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.asyncOpen2(listener); + + do_test_pending(); +} + +function test_prompt1() { + var chan = makeChan(URL + "/auth", URL); + + chan.notificationCallbacks = new Requestor(0, 1); + listener.expectedCode = 200; // OK + chan.asyncOpen2(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.asyncOpen2(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.asyncOpen2(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.asyncOpen2(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.asyncOpen2(listener); + + do_test_pending(); +} + +function test_prompt2() { + var chan = makeChan(URL + "/auth", URL); + + chan.notificationCallbacks = new Requestor(0, 2); + listener.expectedCode = 200; // OK + chan.asyncOpen2(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.asyncOpen2(listener); + + do_test_pending(); +} + +function test_basicrealm() { + var chan = makeChan(URL + "/auth/realm", URL); + + chan.notificationCallbacks = new RealmTestRequestor(); + listener.expectedCode = 401; // Unauthorized + chan.asyncOpen2(listener); + + 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.asyncOpen2(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.asyncOpen2(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.asyncOpen2(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); +} + +// +// Digest functions +// +function bytesFromString(str) { + var converter = + Components.classes["@mozilla.org/intl/scriptableunicodeconverter"] + .createInstance(Components.interfaces.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 = Components.classes["@mozilla.org/security/hash;1"] + .createInstance(Components.interfaces.nsICryptoHash); + ch.init(Components.interfaces.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 largeRealm(metadata, response) { + // test > 32KB realm tokens + var body; + + response.setStatusLine(metadata.httpVersion, 401, "Unauthorized"); + response.setHeader("WWW-Authenticate", + 'Digest realm="' + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + '", 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="' + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + '"'); + + 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.asyncOpen2(listener); + + do_test_pending(); +} + +function test_large_domain() { + var chan = makeChan(URL + "/largeDomain", URL); + + listener.expectedCode = 401; // Unauthorized + chan.asyncOpen2(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..d8a1dc40cf --- /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 + +Cu.import("resource://gre/modules/NetUtil.jsm"); +const nsIAuthInformation = Components.interfaces.nsIAuthInformation; +const nsIAuthPromptAdapterFactory = Components.interfaces.nsIAuthPromptAdapterFactory; + +function run_test() { + const contractID = "@mozilla.org/network/authprompt-adapter-factory;1"; + if (!(contractID in Components.classes)) { + print("No adapter factory found, skipping testing"); + return; + } + var adapter = Components.classes[contractID].getService(); + do_check_eq(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 + do_check_neq(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: function authprompt_qi(iid) { + if (iid.equals(Components.interfaces.nsISupports) || + iid.equals(Components.interfaces.nsIAuthPrompt)) + return this; + throw Components.results.NS_ERROR_NO_INTERFACE; + }, + + 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) { + do_check_eq(this.scheme + "://" + host + " (" + info.realm + ")", realm); + + do_check_neq(text.indexOf(host), -1); + if (info.flags & nsIAuthInformation.ONLY_PASSWORD) { + // Should have the username in the text + do_check_neq(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 != "") + do_check_neq(text.indexOf(info.realm), -1); + else + do_check_eq(text.indexOf('""'), -1); + // No explicit port in the URL; message should not contain -1 + // for those cases + do_check_eq(text.indexOf("-1"), -1); + } + } + }; + + + // Also have to make up a channel + var uri = NetUtil.newURI("http://" + host, "", null) + var chan = NetUtil.newChannel({ + uri: 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); + do_check_eq(rv, prompt1.rv); + do_check_eq(prompt1.called, CALLED_PROMPTUP); + + if (rv) { + do_check_eq(info.domain, ""); + do_check_eq(info.username, prompt1.user); + do_check_eq(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); + do_check_eq(rv, prompt1.rv); + do_check_eq(prompt1.called, CALLED_PROMPTP); + + if (rv) { + do_check_eq(info.domain, ""); + do_check_eq(info.username, prompt1.user); // we initialized this + do_check_eq(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); + do_check_eq(rv, prompt1.rv); + do_check_eq(prompt1.called, CALLED_PROMPTUP); + + if (rv) { + do_check_eq(info.domain, "foo"); + do_check_eq(info.username, "bar"); + do_check_eq(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); + do_check_eq(rv, prompt1.rv); + do_check_eq(prompt1.called, CALLED_PROMPTUP); + + if (rv) { + do_check_eq(info.domain, ""); + do_check_eq(info.username, prompt1.user); + do_check_eq(info.password, prompt1.pw); + } + + info.flags &= ~nsIAuthInformation.NEED_DOMAIN; + + info.domain = ""; + info.username = ""; + info.password = ""; + + // 5: FTP + var uri2 = NetUtil.newURI("ftp://" + host, "", null); + 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); + do_check_eq(rv, prompt1.rv); + do_check_eq(prompt1.called, CALLED_PROMPTUP); + + if (rv) { + do_check_eq(info.domain, ""); + do_check_eq(info.username, prompt1.user); + do_check_eq(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..a545f596fd --- /dev/null +++ b/netwerk/test/unit/test_backgroundfilesaver.js @@ -0,0 +1,731 @@ +/* -*- 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 + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "FileUtils", + "resource://gre/modules/FileUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", + "resource://gre/modules/NetUtil.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Promise", + "resource://gre/modules/Promise.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Task", + "resource://gre/modules/Task.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Services", + "resource://gre/modules/Services.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 + 8388608 : "e3611a47714c42bdf326acfb2eb6ed9fa4cca65cb7d7be55217770a5bf5e7ff0", + // TEST_DATA_LONG + TEST_DATA_LONG + 16777216 : "03a0db69a30140f307587ee746a539247c181bafd85b85c8516a3533c7d9ea1d" +}; + +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 * 2; +const TEST_DATA_LONG = new Array(1 + DESIRED_LENGTH / 256).join(TEST_256_CHARS); +do_check_eq(TEST_DATA_LONG.length, DESIRED_LENGTH); + +/** + * Returns a reference to a temporary file. If the file is then created, it + * will be removed when tests in this file finish. + */ +function getTempFile(aLeafName) { + let file = FileUtils.getFile("TmpD", [aLeafName]); + do_register_cleanup(function GTF_cleanup() { + if (file.exists()) { + file.remove(false); + } + }); + return file; +} + +/** + * 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) { + let deferred = Promise.defer(); + NetUtil.asyncFetch({ + uri: NetUtil.newURI(aFile), + loadUsingSystemPrincipal: true + }, function(aInputStream, aStatus) { + do_check_true(Components.isSuccessCode(aStatus)); + let contents = NetUtil.readInputStreamToString(aInputStream, + aInputStream.available()); + if (contents.length <= TEST_DATA_SHORT.length * 2) { + do_check_eq(contents, aExpectedContents); + } else { + // Do not print the entire content string to the test log. + do_check_eq(contents.length, aExpectedContents.length); + do_check_true(contents == aExpectedContents); + } + deferred.resolve(); + }); + + return deferred.promise; +} + +/** + * 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) { + let deferred = Promise.defer(); + aSaver.observer = { + onTargetChange: function BFSO_onSaveComplete(aSaver, aTarget) + { + if (aOnTargetChangeFn) { + aOnTargetChangeFn(aTarget); + } + }, + onSaveComplete: function BFSO_onSaveComplete(aSaver, aStatus) + { + if (Components.isSuccessCode(aStatus)) { + deferred.resolve(); + } else { + deferred.reject(new Components.Exception("Saver failed.", aStatus)); + } + }, + }; + return deferred.promise; +} + +/** + * 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) { + let deferred = Promise.defer(); + 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: function () { }, + onStopRequest: function (aRequest, aContext, aStatusCode) + { + if (Components.isSuccessCode(aStatusCode)) { + deferred.resolve(); + } else { + deferred.reject(new Components.Exception(aResult)); + } + }, + }, null); + return deferred.promise; +} + +/** + * 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) { + let deferred = Promise.defer(); + 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, -1, -1, 0, 0, true); + pump.asyncRead({ + onStartRequest: function PPTS_onStartRequest(aRequest, aContext) + { + aSaverStreamListener.onStartRequest(aRequest, aContext); + }, + onStopRequest: function PPTS_onStopRequest(aRequest, aContext, aStatusCode) + { + aSaverStreamListener.onStopRequest(aRequest, aContext, aStatusCode); + if (Components.isSuccessCode(aStatusCode)) { + deferred.resolve(); + } else { + deferred.reject(new Components.Exception(aResult)); + } + }, + onDataAvailable: function PPTS_onDataAvailable(aRequest, aContext, + aInputStream, aOffset, + aCount) + { + aSaverStreamListener.onDataAvailable(aRequest, aContext, aInputStream, + aOffset, aCount); + }, + }, null); + return deferred.promise; +} + +var gStillRunning = true; + +//////////////////////////////////////////////////////////////////////////////// +//// Tests + +function run_test() +{ + run_next_test(); +} + +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(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) { + do_check_true(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. + yield promiseCopyToSaver(TEST_DATA_SHORT, saver, true); + + // Indicate that we are ready to finish, and wait for a successful callback. + saver.finish(Cr.NS_OK); + yield 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. + do_check_true(receivedOnTargetChange); + + // Clean up. + destFile.remove(false); +}); + +add_task(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; + do_print("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(); + do_print("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. + yield 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) { + yield promiseCopyToSaver(testData, saver, true); + } + + // Wait for completion, and ensure we succeeded or failed as expected. + if (!cancelAtSomePoint) { + saver.finish(Cr.NS_OK); + } + try { + yield completionPromise; + if (cancelAtSomePoint) { + do_throw("Failure expected."); + } + } catch (ex if cancelAtSomePoint && ex.result == Cr.NS_ERROR_FAILURE) { } + + if (!cancelAtSomePoint) { + // In this case, the file must exist. + do_check_true(currentFile.exists()); + let expectedContents = testData + testData; + yield promiseVerifyContents(currentFile, expectedContents); + do_check_eq(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)) { + do_check_false(initialFile.exists()); + } + } else if (!keepPartialOnFailure) { + // In this case, the file must not exist. + do_check_false(initialFile.exists()); + do_check_false(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); + } + } + + do_print("Test case completed in " + (Date.now() - startTime) + " ms."); + } +}); + +add_task(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. + yield 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); + yield completionPromise; + + // Verify results. + yield promiseVerifyContents(destFile, TEST_DATA_SHORT); + do_check_eq(EXPECTED_HASHES[TEST_DATA_SHORT.length], + toHex(saver.sha256Hash)); + } + + // Clean up. + destFile.remove(false); +}); + +add_task(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. + yield 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); + yield completionPromise; + + // Verify results and clean up. + do_check_false(destFile1.exists()); + yield promiseVerifyContents(destFile2, TEST_DATA_SHORT); + destFile2.remove(false); +}); + +add_task(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); + yield 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); + yield completionPromise; + + // Verify results and clean up. + do_check_false(getTempFile(TEST_FILE_NAME_2).exists()); + do_check_false(getTempFile(TEST_FILE_NAME_3).exists()); + yield promiseVerifyContents(destFile, TEST_DATA_SHORT); + destFile.remove(false); +}); + +add_task(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); + yield promiseCopyToSaver(TEST_DATA_LONG, saver, true); + + saver.finish(Cr.NS_OK); + yield completionPromise; + + // Verify results. + let expectedContents = (i == 0 ? TEST_DATA_LONG + : TEST_DATA_LONG + TEST_DATA_LONG); + yield promiseVerifyContents(destFile, expectedContents); + } + + // Clean up. + destFile.remove(false); +}); + +add_task(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); + + yield 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); + yield completionPromise; + + // Verify results. + do_check_false(firstFile.exists()); + let expectedContents = (i == 0 ? TEST_DATA_SHORT + : TEST_DATA_SHORT + TEST_DATA_SHORT); + yield promiseVerifyContents(secondFile, expectedContents); + } + + // Clean up. + destFile1.remove(false); +}); + +add_task(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); + yield promiseCopyToSaver(TEST_DATA_LONG, saver, true); + + saver.finish(Cr.NS_OK); + yield completionPromise; + + // Verify results. + let expectedContents = (i == 0 ? TEST_DATA_LONG + : TEST_DATA_LONG + TEST_DATA_LONG); + yield promiseVerifyContents(destFile, expectedContents); + do_check_eq(EXPECTED_HASHES[expectedContents.length], + toHex(saver.sha256Hash)); + } + + // Clean up. + destFile.remove(false); +}); + +add_task(function test_finish_only() +{ + // This test checks creating the object and doing nothing. + let destFile = getTempFile(TEST_FILE_NAME_1); + 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); + yield completionPromise; +}); + +add_task(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); + yield promiseCopyToSaver("", saver, true); + + saver.finish(Cr.NS_OK); + yield completionPromise; + + // Verify results. + do_check_true(destFile.exists()); + do_check_eq(destFile.fileSize, 0); + + // Clean up. + destFile.remove(false); +}); + +add_task(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); + yield promiseCopyToSaver("", saver, true); + + saver.finish(Cr.NS_OK); + yield completionPromise; + + // Verify results. + do_check_eq(destFile.fileSize, 0); + do_check_eq(EXPECTED_HASHES[0], toHex(saver.sha256Hash)); + } + + // Clean up. + destFile.remove(false); +}); + +add_task(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 { + let hash = 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) { } + // 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 { + let hash = 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) { } + // Wait for completion so that the worker thread finishes dealing with the + // target file. We expect it to fail. + try { + yield completionPromise; + do_throw("completionPromise should throw"); + } catch (ex if ex.result == Cr.NS_ERROR_FAILURE) { } +}); + +add_task(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 { + let signatureInfo = saver.signatureInfo; + do_throw("Can't get signature if saver is not complete"); + } catch (ex if ex.result == Cr.NS_ERROR_NOT_AVAILABLE) { } + + saver.enableSignatureInfo(); + saver.setTarget(destFile, false); + yield promiseCopyToSaver(TEST_DATA_SHORT, saver, true); + + saver.finish(Cr.NS_OK); + yield completionPromise; + yield promiseVerifyContents(destFile, TEST_DATA_SHORT); + + // signatureInfo is an empty nsIArray + do_check_eq(0, saver.signatureInfo.length); + + // Clean up. + destFile.remove(false); +}); + +add_task(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); + yield promiseCopyToSaver(TEST_DATA_SHORT, saver, true); + + saver.finish(Cr.NS_OK); + yield completionPromise; + try { + let signatureInfo = saver.signatureInfo; + do_throw("Can't get signature if not enabled"); + } catch (ex if ex.result == Cr.NS_ERROR_NOT_AVAILABLE) { } + + // 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..2c6ac46ad5 --- /dev/null +++ b/netwerk/test/unit/test_be_conservative.js @@ -0,0 +1,213 @@ +// -*- 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. + +const { Services } = Cu.import("resource://gre/modules/Services.jsm", {}); +const { NetUtil } = Cu.import("resource://gre/modules/NetUtil.jsm", {}); +const { Promise: promise } = + Cu.import("resource://gre/modules/Promise.jsm", {}); + +// Get a profile directory and ensure PSM initializes NSS. +do_get_profile(); +Cc["@mozilla.org/psm;1"].getService(Ci.nsISupports); + +function getCert() { + let deferred = promise.defer(); + let certService = Cc["@mozilla.org/security/local-cert-service;1"] + .getService(Ci.nsILocalCertService); + certService.getOrCreateCert("beConservative-test", { + handleCert: function(c, rv) { + if (rv) { + deferred.reject(rv); + return; + } + deferred.resolve(c); + } + }); + return deferred.promise; +} + +class InputStreamCallback { + constructor(output) { + this.output = output; + this.stopped = false; + } + + onInputStreamReady(stream) { + do_print("input stream ready"); + if (this.stopped) { + do_print("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(); + do_print("done with input stream ready"); + } + + stop() { + this.stopped = true; + } +} + +class TLSServerSecurityObserver { + constructor(input, output) { + this.input = input; + this.output = output; + this.callbacks = []; + this.stopped = false; + } + + onHandshakeDone(socket, status) { + do_print("TLS handshake done"); + do_print(`TLS version used: ${status.tlsVersionUsed}`); + + if (this.stopped) { + do_print("handshake done callback stopped - closing streams and bailing"); + this.input.close(); + this.output.close(); + 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.callbacks.forEach((callback) => { + callback.stop(); + }); + } +} + +class ServerSocketListener { + constructor() { + this.securityObservers = []; + } + + onSocketAccepted(socket, transport) { + do_print("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() { + do_print("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.setSessionCache(false); + 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; + let deferred = promise.defer(); + req.onload = () => { + ok(expectSuccess, + `should ${expectSuccess ? "" : "not "}have gotten load event`); + equal(req.responseText, "OK", "response text should be 'OK'"); + deferred.resolve(); + }; + req.onerror = () => { + ok(!expectSuccess, + `should ${!expectSuccess ? "" : "not "}have gotten an error`); + deferred.resolve(); + }; + + req.send(); + return deferred.promise; +} + +add_task(function*() { + Services.prefs.setIntPref("security.tls.version.max", 4); + Services.prefs.setCharPref("network.dns.localDomains", hostname); + let cert = yield 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); + yield 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); + yield startClient(server.port, true /*be conservative*/, + false /*should fail*/); + + // However, a non-conservative client should succeed. + yield startClient(server.port, false /*don't be conservative*/, + true /*should succeed*/); + server.close(); +}); + +do_register_cleanup(function() { + Services.prefs.clearUserPref("security.tls.version.max"); + Services.prefs.clearUserPref("network.dns.localDomains"); +}); diff --git a/netwerk/test/unit/test_bug1064258.js b/netwerk/test/unit/test_bug1064258.js new file mode 100644 index 0000000000..3f76837ae9 --- /dev/null +++ b/netwerk/test/unit/test_bug1064258.js @@ -0,0 +1,153 @@ +/** + * 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 + */ + +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +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: + do_check_true(metadata.hasHeader("If-None-Match")); + do_check_eq(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"); + caching = chan.QueryInterface(Ci.nsICachingChannel); + caching.cacheOnlyMetadata = true; + chan.asyncOpen2(new ChannelListener(contentListener1a, null)); +} + +function contentListener1a(request, buffer) +{ + do_check_eq(buffer, responseBody1); + + asyncOpenCacheEntry(URL + "/content1", "disk", 0, null, cacheCheck1) +} + +function cacheCheck1(status, entry) +{ + do_check_eq(status, 0); + do_check_eq(entry.dataSize, 0); + try { + do_check_neq(entry.getMetaDataElement("response-head"), null); + } + catch (ex) { + do_throw("Missing response head"); + } + + var chan = make_channel(URL + "/content1"); + caching = chan.QueryInterface(Ci.nsICachingChannel); + caching.cacheOnlyMetadata = true; + chan.asyncOpen2(new ChannelListener(contentListener1b, null, CL_IGNORE_CL)); +} + +function contentListener1b(request, buffer) +{ + request.QueryInterface(Ci.nsIHttpChannel); + do_check_eq(request.requestMethod, "GET"); + do_check_eq(request.responseStatus, 200); + do_check_eq(request.getResponseHeader("Cache-control"), "max-age=999999"); + + do_check_eq(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"); + caching = chan.QueryInterface(Ci.nsICachingChannel); + caching.cacheOnlyMetadata = true; + chan.asyncOpen2(new ChannelListener(contentListener2a, null)); +} + +function contentListener2a(request, buffer) +{ + do_check_eq(buffer, responseBody2a); + + asyncOpenCacheEntry(URL + "/content2", "disk", 0, null, cacheCheck2) +} + +function cacheCheck2(status, entry) +{ + do_check_eq(status, 0); + do_check_eq(entry.dataSize, 0); + try { + do_check_neq(entry.getMetaDataElement("response-head"), null); + do_check_true(entry.getMetaDataElement("response-head").match('Etag: testetag')); + } + catch (ex) { + do_throw("Missing response head"); + } + + var chan = make_channel(URL + "/content2"); + caching = chan.QueryInterface(Ci.nsICachingChannel); + caching.cacheOnlyMetadata = true; + chan.asyncOpen2(new ChannelListener(contentListener2b, null)); +} + +function contentListener2b(request, buffer) +{ + do_check_eq(buffer, responseBody2b); + + httpServer.stop(do_test_finished); +} diff --git a/netwerk/test/unit/test_bug1195415.js b/netwerk/test/unit/test_bug1195415.js new file mode 100644 index 0000000000..b97d209d3f --- /dev/null +++ b/netwerk/test/unit/test_bug1195415.js @@ -0,0 +1,116 @@ +// Test for bug 1195415 + +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", null, null); + do_check_eq(uri.asciiHostPort, "foo.com"); + uri.port = 90; + var prin = ssm.createCodebasePrincipal(uri, {}); + do_check_eq(uri.asciiHostPort, "foo.com:90"); + do_check_eq(prin.origin, "http://foo.com:90"); + + uri = ios.newURI("http://foo.com:10/file.txt", null, null); + do_check_eq(uri.asciiHostPort, "foo.com:10"); + uri.port = 500; + prin = ssm.createCodebasePrincipal(uri, {}); + do_check_eq(uri.asciiHostPort, "foo.com:500"); + do_check_eq(prin.origin, "http://foo.com:500"); + + uri = ios.newURI("http://foo.com:5000/file.txt", null, null); + do_check_eq(uri.asciiHostPort, "foo.com:5000"); + uri.port = 20; + prin = ssm.createCodebasePrincipal(uri, {}); + do_check_eq(uri.asciiHostPort, "foo.com:20"); + do_check_eq(prin.origin, "http://foo.com:20"); + + uri = ios.newURI("http://foo.com:5000/file.txt", null, null); + do_check_eq(uri.asciiHostPort, "foo.com:5000"); + uri.port = -1; + prin = ssm.createCodebasePrincipal(uri, {}); + do_check_eq(uri.asciiHostPort, "foo.com"); + do_check_eq(prin.origin, "http://foo.com"); + + uri = ios.newURI("http://foo.com:5000/file.txt", null, null); + do_check_eq(uri.asciiHostPort, "foo.com:5000"); + uri.port = 80; + prin = ssm.createCodebasePrincipal(uri, {}); + do_check_eq(uri.asciiHostPort, "foo.com"); + do_check_eq(prin.origin, "http://foo.com"); + + // UNICODE + uri = ios.newURI("http://jos\u00e9.example.net.ch/file.txt", null, null); + do_check_eq(uri.asciiHostPort, "xn--jos-dma.example.net.ch"); + uri.port = 90; + prin = ssm.createCodebasePrincipal(uri, {}); + do_check_eq(uri.asciiHostPort, "xn--jos-dma.example.net.ch:90"); + do_check_eq(prin.origin, "http://xn--jos-dma.example.net.ch:90"); + + uri = ios.newURI("http://jos\u00e9.example.net.ch:10/file.txt", null, null); + do_check_eq(uri.asciiHostPort, "xn--jos-dma.example.net.ch:10"); + uri.port = 500; + prin = ssm.createCodebasePrincipal(uri, {}); + do_check_eq(uri.asciiHostPort, "xn--jos-dma.example.net.ch:500"); + do_check_eq(prin.origin, "http://xn--jos-dma.example.net.ch:500"); + + uri = ios.newURI("http://jos\u00e9.example.net.ch:5000/file.txt", null, null); + do_check_eq(uri.asciiHostPort, "xn--jos-dma.example.net.ch:5000"); + uri.port = 20; + prin = ssm.createCodebasePrincipal(uri, {}); + do_check_eq(uri.asciiHostPort, "xn--jos-dma.example.net.ch:20"); + do_check_eq(prin.origin, "http://xn--jos-dma.example.net.ch:20"); + + uri = ios.newURI("http://jos\u00e9.example.net.ch:5000/file.txt", null, null); + do_check_eq(uri.asciiHostPort, "xn--jos-dma.example.net.ch:5000"); + uri.port = -1; + prin = ssm.createCodebasePrincipal(uri, {}); + do_check_eq(uri.asciiHostPort, "xn--jos-dma.example.net.ch"); + do_check_eq(prin.origin, "http://xn--jos-dma.example.net.ch"); + + uri = ios.newURI("http://jos\u00e9.example.net.ch:5000/file.txt", null, null); + do_check_eq(uri.asciiHostPort, "xn--jos-dma.example.net.ch:5000"); + uri.port = 80; + prin = ssm.createCodebasePrincipal(uri, {}); + do_check_eq(uri.asciiHostPort, "xn--jos-dma.example.net.ch"); + do_check_eq(prin.origin, "http://xn--jos-dma.example.net.ch"); + + // ipv6 + uri = ios.newURI("http://[123:45::678]/file.txt", null, null); + do_check_eq(uri.asciiHostPort, "[123:45::678]"); + uri.port = 90; + prin = ssm.createCodebasePrincipal(uri, {}); + do_check_eq(uri.asciiHostPort, "[123:45::678]:90"); + do_check_eq(prin.origin, "http://[123:45::678]:90"); + + uri = ios.newURI("http://[123:45::678]:10/file.txt", null, null); + do_check_eq(uri.asciiHostPort, "[123:45::678]:10"); + uri.port = 500; + prin = ssm.createCodebasePrincipal(uri, {}); + do_check_eq(uri.asciiHostPort, "[123:45::678]:500"); + do_check_eq(prin.origin, "http://[123:45::678]:500"); + + uri = ios.newURI("http://[123:45::678]:5000/file.txt", null, null); + do_check_eq(uri.asciiHostPort, "[123:45::678]:5000"); + uri.port = 20; + prin = ssm.createCodebasePrincipal(uri, {}); + do_check_eq(uri.asciiHostPort, "[123:45::678]:20"); + do_check_eq(prin.origin, "http://[123:45::678]:20"); + + uri = ios.newURI("http://[123:45::678]:5000/file.txt", null, null); + do_check_eq(uri.asciiHostPort, "[123:45::678]:5000"); + uri.port = -1; + prin = ssm.createCodebasePrincipal(uri, {}); + do_check_eq(uri.asciiHostPort, "[123:45::678]"); + do_check_eq(prin.origin, "http://[123:45::678]"); + + uri = ios.newURI("http://[123:45::678]:5000/file.txt", null, null); + do_check_eq(uri.asciiHostPort, "[123:45::678]:5000"); + uri.port = 80; + prin = ssm.createCodebasePrincipal(uri, {}); + do_check_eq(uri.asciiHostPort, "[123:45::678]"); + do_check_eq(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..cbab52797e --- /dev/null +++ b/netwerk/test/unit/test_bug1218029.js @@ -0,0 +1,82 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +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: XPCOMUtils.generateQI([Ci.nsIChannel]) + }; + + let chunkIndex = 0; + + let observer = { + onStreamComplete: function(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: function (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: + XPCOMUtils.generateQI([Ci.nsIIncrementalStreamLoaderObserver]) + }; + + let listener = Cc["@mozilla.org/network/incremental-stream-loader;1"] + .createInstance(Ci.nsIIncrementalStreamLoader); + listener.init(observer); + + listener.onStartRequest(channel, null); + var offset = 0; + test.chunks.forEach(function (chunkLength) { + listener.onDataAvailable(channel, null, stream, offset, chunkLength); + offset += chunkLength; + }); + listener.onStopRequest(channel, null, 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..8111a6c4bc --- /dev/null +++ b/netwerk/test/unit/test_bug1279246.js @@ -0,0 +1,97 @@ +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +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: function(iid) { + if (iid.equals(Components.interfaces.nsIStreamListener) || + iid.equals(Components.interfaces.nsIRequestObserver) || + iid.equals(Components.interfaces.nsISupports)) + return this; + throw Components.results.NS_ERROR_NO_INTERFACE; + }, + + onStartRequest: function(request, ctx) { + do_check_eq(request.status, Cr.NS_OK); + this._buffer = ""; + }, + + onDataAvailable: function(request, cx, 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: function(request, ctx, status) { + if (pass == 0) { + do_check_eq(this._buffer.length, responseLen); + pass++; + + var channel = setupChannel(); + channel.loadFlags = Ci.nsIRequest.VALIDATE_NEVER; + channel.asyncOpen2(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"); + + httpserver.registerPathHandler(testUrl, handler); + httpserver.start(-1); + + var channel = setupChannel(); + channel.asyncOpen2(new Listener()); + + do_test_pending(); +} + +function handler(metadata, response) { + do_check_eq(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 = Components.classes["@mozilla.org/binaryoutputstream;1"] + .createInstance(Components.interfaces.nsIBinaryOutputStream); + bos.setOutputStream(response.bodyOutputStream); + + response.processAsync(); + bos.writeByteArray(responseBody, responseBody.length); + response.finish(); +} diff --git a/netwerk/test/unit/test_bug203271.js b/netwerk/test/unit/test_bug203271.js new file mode 100644 index 0000000000..34463811ef --- /dev/null +++ b/netwerk/test/unit/test_bug203271.js @@ -0,0 +1,177 @@ +// +// 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 + +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); +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: function(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(Components.interfaces.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.asyncOpen2(new ChannelListener(checkValueAndTrigger, channel)); +} + +function checkValueAndTrigger(request, data, ctx) { + logit(index, data, ctx); + do_check_eq(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..ee6930c609 --- /dev/null +++ b/netwerk/test/unit/test_bug248970_cache.js @@ -0,0 +1,151 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// 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) { + do_execute_soon(store_cb); + return; + } + + asyncOpenCacheEntry(entries[store_idx][0], + entries[store_idx][2], + Ci.nsICacheStorage.OPEN_TRUNCATE, + LoadContextInfo.custom(false, + {privateBrowsingId : entries[store_idx][3] ? 0 : 1}), + store_data, + appCache); +} + +var store_data = function(status, entry) { + do_check_eq(status, Cr.NS_OK); + var os = entry.openOutputStream(0); + + 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++; + do_execute_soon(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) { + do_execute_soon(check_cb); + return; + } + + asyncOpenCacheEntry(entries[check_idx][0], + entries[check_idx][2], + Ci.nsICacheStorage.OPEN_READONLY, + LoadContextInfo.custom(false, + {privateBrowsingId : entries[check_idx][3] ? 0 : 1}), + check_data, + appCache); +} + +var check_data = function (status, entry) { + var cont = function() { + check_idx++; + do_execute_soon(check_entries); + } + + if (!check_pb_exited || entries[check_idx][3]) { + do_check_eq(status, Cr.NS_OK); + var is = entry.openInputStream(0); + pumpReadStream(is, function(read) { + entry.close(); + do_check_eq(read, entries[check_idx][1]); + cont(); + }); + } else { + do_check_eq(status, Cr.NS_ERROR_CACHE_KEY_NOT_FOUND); + cont(); + } +}; + +function run_test() { + // Simulate a profile dir for xpcshell + do_get_profile(); + + 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", null); + + // Make sure the memory device is not empty + get_device_entry_count(kMemoryDevice, null, function(count) { + do_check_eq(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..41c45e1059 --- /dev/null +++ b/netwerk/test/unit/test_bug248970_cookie.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/. */ + +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +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.asyncOpen2(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) { + var chan = setup_chan('present?cookie=' + value.replace('=','|'), isPrivate, function(req) { + req.QueryInterface(Ci.nsIHttpChannel); + do_check_eq(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").indexOf(match[1].replace("|","=")) != -1; + } 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); + + 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); + do_check_eq(req.responseStatus, 200); + try { + do_check_true(req.getResponseHeader("Set-Cookie") != "", "expected a Set-Cookie header"); + } catch (x) { + do_throw("missing Set-Cookie header"); + } + + runNextTest(); + } + + let tests = []; + + function runNextTest() { + do_execute_soon(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", null); + // 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..4f4de6037d --- /dev/null +++ b/netwerk/test/unit/test_bug261425.js @@ -0,0 +1,26 @@ +function run_test() { + var ios = Cc["@mozilla.org/network/io-service;1"]. + getService(Ci.nsIIOService); + + var newURI = ios.newURI("http://foo.com", null, null); + + var success = false; + try { + newURI.spec = "http: //foo.com"; + } + 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.host = " foo.com"; + } + 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..8262c07e84 --- /dev/null +++ b/netwerk/test/unit/test_bug263127.js @@ -0,0 +1,61 @@ +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +var server; +const BUGID = "263127"; + +var listener = { + QueryInterface: function(iid) { + if (!iid.equals(nsIDownloadObserver) && + !iid.equals(nsISupports)) + throw Components.results.NS_ERROR_NO_INTERFACE; + + return this; + }, + + onDownloadComplete: function(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); + } + + do_check_false(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.asyncOpen2(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..f8da54356a --- /dev/null +++ b/netwerk/test/unit/test_bug282432.js @@ -0,0 +1,42 @@ +Cu.import("resource://gre/modules/NetUtil.jsm"); + +function run_test() { + do_test_pending(); + + function StreamListener() {} + + StreamListener.prototype = { + QueryInterface: function(aIID) { + if (aIID.equals(Components.interfaces.nsIStreamListener) || + aIID.equals(Components.interfaces.nsIRequestObserver) || + aIID.equals(Components.interfaces.nsISupports)) + return this; + throw Components.results.NS_NOINTERFACE; + }, + + onStartRequest: function(aRequest, aContext) {}, + + onStopRequest: function(aRequest, aContext, aStatusCode) { + // Make sure we can catch the error NS_ERROR_FILE_NOT_FOUND here. + do_check_eq(aStatusCode, Components.results.NS_ERROR_FILE_NOT_FOUND); + do_test_finished(); + }, + + onDataAvailable: function(aRequest, aContext, aStream, aOffset, aCount) { + do_throw("The channel must not call onDataAvailable()."); + } + }; + + let listener = new StreamListener(); + let ios = Components.classes["@mozilla.org/network/io-service;1"] + .getService(Components.interfaces.nsIIOService); + + // This file does not exist. + let file = do_get_file("_NOT_EXIST_.txt", true); + do_check_false(file.exists()); + let channel = NetUtil.newChannel({ + uri: ios.newFileURI(file), + loadUsingSystemPrincipal: true + }); + channel.asyncOpen2(listener); +} diff --git a/netwerk/test/unit/test_bug321706.js b/netwerk/test/unit/test_bug321706.js new file mode 100644 index 0000000000..8ddbce6e75 --- /dev/null +++ b/netwerk/test/unit/test_bug321706.js @@ -0,0 +1,11 @@ +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, null, null); + do_check_eq(newURI.spec, url); + do_check_eq(newURI.path, "/folder/file?/."); + do_check_eq(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..0533e9bc5f --- /dev/null +++ b/netwerk/test/unit/test_bug331825.js @@ -0,0 +1,42 @@ +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +var server; +const BUGID = "331825"; + +function TestListener() { +} +TestListener.prototype.onStartRequest = function(request, context) { +} +TestListener.prototype.onStopRequest = function(request, context, status) { + var channel = request.QueryInterface(Components.interfaces.nsIHttpChannel); + do_check_eq(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(Components.interfaces.nsIHttpChannel); + channel.setRequestHeader("If-None-Match", "foobar", false); + channel.asyncOpen2(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..c27aff5e9b --- /dev/null +++ b/netwerk/test/unit/test_bug336501.js @@ -0,0 +1,27 @@ +var Cc = Components.classes; +var Ci = Components.interfaces; + +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); + do_check_eq(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..bcf8cdb749 --- /dev/null +++ b/netwerk/test/unit/test_bug337744.js @@ -0,0 +1,114 @@ +/* verify that certain invalid URIs are not parsed by the resource + protocol handler */ + +Cu.import("resource://gre/modules/NetUtil.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); + +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", null, null); +var principal = Services.scriptSecurityManager.createCodebasePrincipal(uri, {}); + +function get_channel(spec) +{ + var channelURI = NetUtil.newURI(spec, null, null); + + var channel = NetUtil.newChannel({ + uri: NetUtil.newURI(spec, null, null), + loadingPrincipal: principal, + securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL, + contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER + }); + + try { + channel.asyncOpen2(null); + ok(false, "asyncOpen2() of URI: " + spec + "should throw"); + } + catch (e) { + // make sure we get the right error code in the exception + // ERROR code for NS_ERROR_DOM_BAD_URI is 1012 + equal(e.code, 1012); + } + + try { + channel.open2(); + ok(false, "Open2() of uri: " + spec + "should throw"); + } + catch (e) { + // make sure we get the right error code in the exception + // ERROR code for NS_ERROR_DOM_BAD_URI is 1012 + equal(e.code, 1012); + } + + return channel; +} + +function check_safe_resolution(spec, rootURI) +{ + do_print(`Testing URL "${spec}"`); + + let channel = get_channel(spec); + + ok(channel.name.startsWith(rootURI), `URL resolved safely to ${channel.name}`); + ok(!/%2f/i.test(channel.name), `URL contains no escaped / characters`); +} + +function check_resolution_error(spec) +{ + try { + get_channel(spec); + ok(false, "Expected an error"); + } catch (e) { + equal(e.result, Components.results.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); + + resProto.setSubstitution("res-test", rootURI); + do_register_cleanup(() => { + resProto.setSubstitution("res-test", null); + }); + + let baseRoot = resProto.resolveURI(Services.io.newURI("resource:///", null, null)); + let greRoot = resProto.resolveURI(Services.io.newURI("resource://gre/", null, null)); + + for (var spec of specs) { + check_safe_resolution(spec, rootURI.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..d905f8ae4d --- /dev/null +++ b/netwerk/test/unit/test_bug365133.js @@ -0,0 +1,111 @@ +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 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 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 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 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 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 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 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 FILE \n" + ] +] + +function checkData(request, data, ctx) { + do_check_eq(tests[0][1], data); + tests.shift(); + do_execute_soon(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 = { + password: "", + asciiSpec: URL, + QueryInterface: XPCOMUtils.generateQI([Ci.nsIURI]) + }; + + var channel = { + URI: url, + contentLength: -1, + pending: true, + isPending: function() { + return this.pending; + }, + QueryInterface: XPCOMUtils.generateQI([Ci.nsIChannel]) + }; + + converter.onStartRequest(channel, null); + converter.onDataAvailable(channel, null, 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() { + do_execute_soon(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..8f511e5595 --- /dev/null +++ b/netwerk/test/unit/test_bug368702.js @@ -0,0 +1,150 @@ +function run_test() { + var tld = + Cc["@mozilla.org/network/effective-tld-service;1"]. + getService(Ci.nsIEffectiveTLDService); + + var etld; + + do_check_eq(tld.getPublicSuffixFromHost("localhost"), "localhost"); + do_check_eq(tld.getPublicSuffixFromHost("localhost."), "localhost."); + do_check_eq(tld.getPublicSuffixFromHost("domain.com"), "com"); + do_check_eq(tld.getPublicSuffixFromHost("domain.com."), "com."); + do_check_eq(tld.getPublicSuffixFromHost("domain.co.uk"), "co.uk"); + do_check_eq(tld.getPublicSuffixFromHost("domain.co.uk."), "co.uk."); + do_check_eq(tld.getPublicSuffixFromHost("co.uk"), "co.uk"); + do_check_eq(tld.getBaseDomainFromHost("domain.co.uk"), "domain.co.uk"); + do_check_eq(tld.getBaseDomainFromHost("domain.co.uk."), "domain.co.uk."); + + try { + etld = tld.getPublicSuffixFromHost(""); + do_throw("this should fail"); + } catch(e) { + do_check_eq(e.result, Cr.NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS); + } + + try { + etld = tld.getBaseDomainFromHost("domain.co.uk", 1); + do_throw("this should fail"); + } catch(e) { + do_check_eq(e.result, Cr.NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS); + } + + try { + etld = tld.getBaseDomainFromHost("co.uk"); + do_throw("this should fail"); + } catch(e) { + do_check_eq(e.result, Cr.NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS); + } + + try { + etld = tld.getBaseDomainFromHost(""); + do_throw("this should fail"); + } catch(e) { + do_check_eq(e.result, Cr.NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS); + } + + try { + etld = tld.getPublicSuffixFromHost("1.2.3.4"); + do_throw("this should fail"); + } catch(e) { + do_check_eq(e.result, Cr.NS_ERROR_HOST_IS_IP_ADDRESS); + } + + try { + etld = tld.getPublicSuffixFromHost("2010:836B:4179::836B:4179"); + do_throw("this should fail"); + } catch(e) { + do_check_eq(e.result, Cr.NS_ERROR_HOST_IS_IP_ADDRESS); + } + + try { + etld = tld.getPublicSuffixFromHost("3232235878"); + do_throw("this should fail"); + } catch(e) { + do_check_eq(e.result, Cr.NS_ERROR_HOST_IS_IP_ADDRESS); + } + + try { + etld = tld.getPublicSuffixFromHost("::ffff:192.9.5.5"); + do_throw("this should fail"); + } catch(e) { + do_check_eq(e.result, Cr.NS_ERROR_HOST_IS_IP_ADDRESS); + } + + try { + etld = tld.getPublicSuffixFromHost("::1"); + do_throw("this should fail"); + } catch(e) { + do_check_eq(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 { + etld = tld.getPublicSuffixFromHost("127.0.0.1."); + do_throw("this should fail"); + } catch(e) { + do_check_eq(e.result, Cr.NS_ERROR_HOST_IS_IP_ADDRESS); + } + + try { + etld = tld.getPublicSuffixFromHost("::ffff:127.0.0.1."); + do_throw("this should fail"); + } catch(e) { + do_check_eq(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 = Components.classes["@mozilla.org/network/io-service;1"] + .getService(Components.interfaces.nsIIOService); + + var uri = ioService.newURI("http://b\u00FCcher.co.uk", null, null); + do_check_eq(tld.getBaseDomain(uri), "xn--bcher-kva.co.uk"); + do_check_eq(tld.getBaseDomainFromHost("b\u00FCcher.co.uk"), "xn--bcher-kva.co.uk"); + do_check_eq(tld.getPublicSuffix(uri), "co.uk"); + do_check_eq(tld.getPublicSuffixFromHost("b\u00FCcher.co.uk"), "co.uk"); + + // check that malformed hosts are rejected as invalid args + try { + etld = tld.getBaseDomainFromHost("domain.co.uk.."); + do_throw("this should fail"); + } catch(e) { + do_check_eq(e.result, Cr.NS_ERROR_ILLEGAL_VALUE); + } + + try { + etld = tld.getBaseDomainFromHost("domain.co..uk"); + do_throw("this should fail"); + } catch(e) { + do_check_eq(e.result, Cr.NS_ERROR_ILLEGAL_VALUE); + } + + try { + etld = tld.getBaseDomainFromHost(".domain.co.uk"); + do_throw("this should fail"); + } catch(e) { + do_check_eq(e.result, Cr.NS_ERROR_ILLEGAL_VALUE); + } + + try { + etld = tld.getBaseDomainFromHost(".domain.co.uk"); + do_throw("this should fail"); + } catch(e) { + do_check_eq(e.result, Cr.NS_ERROR_ILLEGAL_VALUE); + } + + try { + etld = tld.getBaseDomainFromHost("."); + do_throw("this should fail"); + } catch(e) { + do_check_eq(e.result, Cr.NS_ERROR_ILLEGAL_VALUE); + } + + try { + etld = tld.getBaseDomainFromHost(".."); + do_throw("this should fail"); + } catch(e) { + do_check_eq(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..d59bef005c --- /dev/null +++ b/netwerk/test/unit/test_bug369787.js @@ -0,0 +1,71 @@ +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +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; + do_check_eq(channel.contentType, newType); + channel.contentType = origType; + do_check_eq(channel.contentType, origType); +} + +function TestListener() { +} +TestListener.prototype.onStartRequest = function(request, context) { + try { + // request might be different from channel + channel = request.QueryInterface(Components.interfaces.nsIChannel); + + change_content_type(); + } catch (ex) { + print(ex); + throw ex; + } +} +TestListener.prototype.onStopRequest = function(request, context, 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(Components.interfaces.nsIHttpChannel); + channel.asyncOpen2(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..75752d3836 --- /dev/null +++ b/netwerk/test/unit/test_bug371473.js @@ -0,0 +1,44 @@ +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 { + var newURI = ios.newURI(spec, null, null); + } + 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. + var newURI = ios.newURI(spec, null, null); + } + catch (e) { + return; + } +} + +function run_test() { + test_not_too_long(); + test_too_long(); +} diff --git a/netwerk/test/unit/test_bug376660.js b/netwerk/test/unit/test_bug376660.js new file mode 100644 index 0000000000..8208eef6a6 --- /dev/null +++ b/netwerk/test/unit/test_bug376660.js @@ -0,0 +1,72 @@ +Cu.import("resource://gre/modules/NetUtil.jsm"); + +var Cc = Components.classes; +var Ci = Components.interfaces; + +var listener = { + expect_failure: false, + QueryInterface: function listener_qi(iid) { + if (iid.equals(Ci.nsISupports) || + iid.equals(Ci.nsIUnicharStreamLoaderObserver)) { + return this; + } + throw Components.results.NS_ERROR_NO_INTERFACE; + }, + onDetermineCharset : function onDetermineCharset(loader, context, data) + { + return "us-ascii"; + }, + onStreamComplete : function onStreamComplete (loader, context, status, data) + { + try { + if (this.expect_failure) + do_check_false(Components.isSuccessCode(status)); + else + do_check_eq(status, Components.results.NS_OK); + do_check_eq(data, ""); + do_check_neq(loader.channel, null); + tests[current_test++](); + } finally { + do_test_finished(); + } + } +}; + +var current_test = 0; +var tests = [test1, test2, done]; + +function run_test() { + tests[current_test++](); +} + +function test1() { + var f = + Cc["@mozilla.org/network/unichar-stream-loader;1"]. + createInstance(Ci.nsIUnicharStreamLoader); + f.init(listener); + + var chan = NetUtil.newChannel({ + uri: "data:text/plain,", + loadUsingSystemPrincipal: true + }); + chan.asyncOpen2(f); + do_test_pending(); +} + +function test2() { + var f = + Cc["@mozilla.org/network/unichar-stream-loader;1"]. + createInstance(Ci.nsIUnicharStreamLoader); + f.init(listener); + + var chan = NetUtil.newChannel({ + uri: "http://localhost:0/", + loadUsingSystemPrincipal: true + }); + listener.expect_failure = true; + chan.asyncOpen2(f); + do_test_pending(); +} + +function done() { +} diff --git a/netwerk/test/unit/test_bug376844.js b/netwerk/test/unit/test_bug376844.js new file mode 100644 index 0000000000..9a21b0171e --- /dev/null +++ b/netwerk/test/unit/test_bug376844.js @@ -0,0 +1,21 @@ +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], null, null); + do_check_eq(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..a068c555a3 --- /dev/null +++ b/netwerk/test/unit/test_bug376865.js @@ -0,0 +1,20 @@ +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, -1, -1, 0, 0, false); + + // When we pass a null listener argument too asyncRead we expect it to throw + // instead of crashing. + try { + pump.asyncRead(null, 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..0177e904c5 --- /dev/null +++ b/netwerk/test/unit/test_bug379034.js @@ -0,0 +1,18 @@ +function run_test() { + const ios = Components.classes["@mozilla.org/network/io-service;1"] + .getService(Components.interfaces.nsIIOService); + + var base = ios.newURI("http://localhost/bug379034/index.html", null, null); + + var uri = ios.newURI("http:a.html", null, base); + do_check_eq(uri.spec, "http://localhost/bug379034/a.html"); + + uri = ios.newURI("HtTp:b.html", null, base); + do_check_eq(uri.spec, "http://localhost/bug379034/b.html"); + + uri = ios.newURI("https:c.html", null, base); + do_check_eq(uri.spec, "https://c.html/"); + + uri = ios.newURI("./https:d.html", null, base); + do_check_eq(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..b9b9d5bd60 --- /dev/null +++ b/netwerk/test/unit/test_bug380994.js @@ -0,0 +1,22 @@ +/* check resource: protocol for traversal problems */ + +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, null, null); + if (uri.spec.indexOf("..") != -1) + 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..112678d00d --- /dev/null +++ b/netwerk/test/unit/test_bug388281.js @@ -0,0 +1,24 @@ +function run_test() { + const ios = Cc["@mozilla.org/network/io-service;1"]. + getService(Ci.nsIIOService); + + var uri = ios.newURI("http://foo.com/file.txt", null, null); + uri.port = 90; + do_check_eq(uri.hostPort, "foo.com:90"); + + uri = ios.newURI("http://foo.com:10/file.txt", null, null); + uri.port = 500; + do_check_eq(uri.hostPort, "foo.com:500"); + + uri = ios.newURI("http://foo.com:5000/file.txt", null, null); + uri.port = 20; + do_check_eq(uri.hostPort, "foo.com:20"); + + uri = ios.newURI("http://foo.com:5000/file.txt", null, null); + uri.port = -1; + do_check_eq(uri.hostPort, "foo.com"); + + uri = ios.newURI("http://foo.com:5000/file.txt", null, null); + uri.port = 80; + do_check_eq(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..0bcfa83624 --- /dev/null +++ b/netwerk/test/unit/test_bug396389.js @@ -0,0 +1,71 @@ +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:///", null, null); + do_check_true(uri1 instanceof Ci.nsIFileURL); + + var uri2 = uri1.clone(); + do_check_true(uri2 instanceof Ci.nsIFileURL); + do_check_true(uri1.equals(uri2)); + + var uri3 = round_trip(uri1); + do_check_true(uri3 instanceof Ci.nsIFileURL); + do_check_true(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) { + try { + pref.oldVal = prefs.getBoolPref(pref.name); + } catch(e) { + } + 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/", null, null); + do_check_eq(uri4.asciiHost, "xn--jos-dma.example.net.ch"); + do_check_eq(uri4.host, "jos\u00e9.example.net.ch"); + + var uri5 = round_trip(uri4); + do_check_true(uri4.equals(uri5)); + do_check_eq(uri4.host, uri5.host); + do_check_eq(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..e7643fa9de --- /dev/null +++ b/netwerk/test/unit/test_bug401564.js @@ -0,0 +1,48 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +"use strict"; +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +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) +{ + do_check_eq(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(Components.interfaces.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.asyncOpen2(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..9ac9d1d745 --- /dev/null +++ b/netwerk/test/unit/test_bug411952.js @@ -0,0 +1,35 @@ +function run_test() { + try { + var cm = Cc["@mozilla.org/cookiemanager;1"]. + getService(Ci.nsICookieManager2); + do_check_neq(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, {}); + const now = Math.floor((new Date()).getTime() / 1000); + + var enumerator = cm.enumerator, found = false; + while (enumerator.hasMoreElements()) { + var cookie = enumerator.getNext().QueryInterface(Ci.nsICookie2); + if (cookie.host == "example.com" && + cookie.path == "/" && + cookie.name == "C") { + do_check_true("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. + do_check_true(Math.abs(creationTime - now) <= 1, + "Cookie's creationTime is set incorrectly"); + found = true; + break; + } + } + + do_check_true(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..dd4358413e --- /dev/null +++ b/netwerk/test/unit/test_bug412457.js @@ -0,0 +1,44 @@ +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/", null, null); + do_check_eq(newURI.asciiHost, "xn--kkr.com"); + + // escaped UTF8 + newURI.spec = "http://%e5%8d%81.com"; + do_check_eq(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.spec = "http://%80.com"; }, "illegal UTF character"); + + // test parsing URL with all possible host terminators + newURI.spec = "http://example.com?foo"; + do_check_eq(newURI.asciiHost, "example.com"); + + newURI.spec = "http://example.com#foo"; + do_check_eq(newURI.asciiHost, "example.com"); + + newURI.spec = "http://example.com:80"; + do_check_eq(newURI.asciiHost, "example.com"); + + newURI.spec = "http://example.com/foo"; + do_check_eq(newURI.asciiHost, "example.com"); + + // Characters that are invalid in the host, shouldn't be decoded. + newURI.spec = "http://example.com%3ffoo"; + do_check_eq(newURI.asciiHost, "example.com%3ffoo"); + newURI.spec = "http://example.com%23foo"; + do_check_eq(newURI.asciiHost, "example.com%23foo"); + newURI.spec = "http://example.com%3bfoo"; + do_check_eq(newURI.asciiHost, "example.com%3bfoo"); + newURI.spec = "http://example.com%3a80"; + do_check_eq(newURI.asciiHost, "example.com%3a80"); + newURI.spec = "http://example.com%2ffoo"; + do_check_eq(newURI.asciiHost, "example.com%2ffoo"); + newURI.spec = "http://example.com%00"; + do_check_eq(newURI.asciiHost, "example.com%00"); +}
\ No newline at end of file diff --git a/netwerk/test/unit/test_bug412945.js b/netwerk/test/unit/test_bug412945.js new file mode 100644 index 0000000000..e8b39774b3 --- /dev/null +++ b/netwerk/test/unit/test_bug412945.js @@ -0,0 +1,42 @@ +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +var httpserv; + +function TestListener() { +} + +TestListener.prototype.onStartRequest = function(request, context) { +} + +TestListener.prototype.onStopRequest = function(request, context, 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(Components.interfaces.nsIHttpChannel); + channel.requestMethod = "POST"; + channel.asyncOpen2(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..7e5a418d01 --- /dev/null +++ b/netwerk/test/unit/test_bug414122.js @@ -0,0 +1,58 @@ +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); + do_check_eq(actualSuffix, expectedSuffix); +} diff --git a/netwerk/test/unit/test_bug427957.js b/netwerk/test/unit/test_bug427957.js new file mode 100644 index 0000000000..e869725a40 --- /dev/null +++ b/netwerk/test/unit/test_bug427957.js @@ -0,0 +1,106 @@ +/** + * Test for Bidi restrictions on IDNs from RFC 3454 + */ + +var Cc = Components.classes; +var Ci = Components.interfaces; +var idnService; + +function expected_pass(inputIDN) +{ + var isASCII = {}; + var displayIDN = idnService.convertToDisplayIDN(inputIDN, isASCII); + do_check_eq(displayIDN, inputIDN); +} + +function expected_fail(inputIDN) +{ + var isASCII = {}; + var displayIDN = ""; + + try { + displayIDN = idnService.convertToDisplayIDN(inputIDN, isASCII); + } + catch(e) {} + + do_check_neq(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\200ebar.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..a6f1452c25 --- /dev/null +++ b/netwerk/test/unit/test_bug429347.js @@ -0,0 +1,38 @@ +function run_test() { + var ios = Cc["@mozilla.org/network/io-service;1"]. + getService(Ci.nsIIOService); + + var uri1 = ios.newURI("http://example.com#bar", null, null); + var uri2 = ios.newURI("http://example.com/#bar", null, null); + do_check_true(uri1.equals(uri2)); + + uri1.spec = "http://example.com?bar"; + uri2.spec = "http://example.com/?bar"; + do_check_true(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.spec = "http://example.com;bar"; + uri2.spec = "http://example.com/;bar"; + do_check_false(uri1.equals(uri2)); + + uri1.spec = "http://example.com#"; + uri2.spec = "http://example.com/#"; + do_check_true(uri1.equals(uri2)); + + uri1.spec = "http://example.com?"; + uri2.spec = "http://example.com/?"; + do_check_true(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.spec = "http://example.com;"; + uri2.spec = "http://example.com/;"; + do_check_false(uri1.equals(uri2)); + + uri1.spec = "http://example.com"; + uri2.spec = "http://example.com/"; + do_check_true(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..564831df3b --- /dev/null +++ b/netwerk/test/unit/test_bug455311.js @@ -0,0 +1,128 @@ +Cu.import("resource://gre/modules/NetUtil.jsm"); + +function getLinkFile() +{ + 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); +var link; +var linkURI; +const newURI = ios.newURI("http://www.mozilla.org/", null, null); + +function NotificationCallbacks(origURI, newURI) +{ + this._origURI = origURI; + this._newURI = newURI; +} +NotificationCallbacks.prototype = { + QueryInterface: function(iid) + { + if (iid.equals(Ci.nsISupports) || + iid.equals(Ci.nsIInterfaceRequestor) || + iid.equals(Ci.nsIChannelEventSink)) { + return this; + } + throw Cr.NS_ERROR_NO_INTERFACE; + }, + getInterface: function (iid) + { + return this.QueryInterface(iid); + }, + asyncOnChannelRedirect: function(oldChan, newChan, flags, callback) + { + do_check_eq(oldChan.URI.spec, this._origURI.spec); + do_check_eq(oldChan.URI, this._origURI); + do_check_eq(oldChan.originalURI.spec, this._origURI.spec); + do_check_eq(oldChan.originalURI, this._origURI); + do_check_eq(newChan.originalURI.spec, this._newURI.spec); + do_check_eq(newChan.originalURI, newChan.URI); + do_check_eq(newChan.URI.spec, this._newURI.spec); + throw Cr.NS_ERROR_ABORT; + } +}; + +function RequestObserver(origURI, newURI, nextTest) +{ + this._origURI = origURI; + this._newURI = newURI; + this._nextTest = nextTest; +} +RequestObserver.prototype = { + QueryInterface: function(iid) + { + if (iid.equals(Ci.nsISupports) || + iid.equals(Ci.nsIRequestObserver) || + iid.equals(Ci.nsIStreamListener)) { + return this; + } + throw Cr.NS_ERROR_NO_INTERFACE; + }, + onStartRequest: function (req, ctx) + { + var chan = req.QueryInterface(Ci.nsIChannel); + do_check_eq(chan.URI.spec, this._origURI.spec); + do_check_eq(chan.URI, this._origURI); + do_check_eq(chan.originalURI.spec, this._origURI.spec); + do_check_eq(chan.originalURI, this._origURI); + }, + onDataAvailable: function(req, ctx, stream, offset, count) + { + do_throw("Unexpected call to onDataAvailable"); + }, + onStopRequest: function (req, ctx, status) + { + var chan = req.QueryInterface(Ci.nsIChannel); + try { + do_check_eq(chan.URI.spec, this._origURI.spec); + do_check_eq(chan.URI, this._origURI); + do_check_eq(chan.originalURI.spec, this._origURI.spec); + do_check_eq(chan.originalURI, this._origURI); + do_check_eq(status, Cr.NS_ERROR_ABORT); + do_check_false(chan.isPending()); + } catch(e) {} + this._nextTest(); + } +}; + +function test_cancel() +{ + var chan = NetUtil.newChannel({ + uri: linkURI, + loadUsingSystemPrincipal: true + }); + do_check_eq(chan.URI, linkURI); + do_check_eq(chan.originalURI, linkURI); + chan.asyncOpen2(new RequestObserver(linkURI, newURI, do_test_finished)); + do_check_true(chan.isPending()); + chan.cancel(Cr.NS_ERROR_ABORT); + do_check_true(chan.isPending()); +} + +function run_test() +{ + if (mozinfo.os != "win" && mozinfo.os != "linux") { + return; + } + + link = getLinkFile(); + linkURI = ios.newFileURI(link); + + do_test_pending(); + var chan = NetUtil.newChannel({ + uri: linkURI, + loadUsingSystemPrincipal: true + }); + do_check_eq(chan.URI, linkURI); + do_check_eq(chan.originalURI, linkURI); + chan.notificationCallbacks = new NotificationCallbacks(linkURI, newURI); + chan.asyncOpen2(new RequestObserver(linkURI, newURI, test_cancel)); + do_check_true(chan.isPending()); +} diff --git a/netwerk/test/unit/test_bug455598.js b/netwerk/test/unit/test_bug455598.js new file mode 100644 index 0000000000..d0f9e459ec --- /dev/null +++ b/netwerk/test/unit/test_bug455598.js @@ -0,0 +1,35 @@ +/* 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/. */ + +function run_test() { + const time = (new Date("Jan 1, 2030")).getTime() / 1000; + var cookie = { + name: "foo", + value: "bar", + isDomain: true, + host: "example.com", + path: "/baz", + isSecure: false, + expires: time, + status: 0, + policy: 0, + isSession: false, + expiry: time, + isHttpOnly: true, + QueryInterface: function(iid) { + const validIIDs = [Components.interfaces.nsISupports, + Components.interfaces.nsICookie, + Components.interfaces.nsICookie2]; + for (var i = 0; i < validIIDs.length; ++i) + if (iid == validIIDs[i]) + return this; + throw Components.results.NS_ERROR_NO_INTERFACE; + } + }; + var cm = Components.classes["@mozilla.org/cookiemanager;1"]. + getService(Components.interfaces.nsICookieManager2); + do_check_false(cm.cookieExists(cookie)); + // if the above line does not crash, the test was successful + do_test_finished(); +} diff --git a/netwerk/test/unit/test_bug464591.js b/netwerk/test/unit/test_bug464591.js new file mode 100644 index 0000000000..34d7c46b52 --- /dev/null +++ b/netwerk/test/unit/test_bug464591.js @@ -0,0 +1,81 @@ + +const StandardURL = Components.Constructor("@mozilla.org/network/standard-url;1", + "nsIStandardURL", + "init"); + + // 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 +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"], + ["www.mozill%61%2f.org", "www.mozilla%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", + "www.e%00xample.xn--comwww-re3c.xn--mozill-8nf.xn--comwww-rq0c.mozilla.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.blacklist_chars", + minimumList: "\u2215\u0430\u2044", + }; + +function stringToURL(str) { + return (new StandardURL(Ci.nsIStandardURL.URLTYPE_AUTHORITY, 80, + str, "UTF-8", null)) + .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) { + } + + do_register_cleanup(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]); + } + } +} diff --git a/netwerk/test/unit/test_bug468426.js b/netwerk/test/unit/test_bug468426.js new file mode 100644 index 0000000000..95ce32674d --- /dev/null +++ b/netwerk/test/unit/test_bug468426.js @@ -0,0 +1,100 @@ +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +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(Components.interfaces.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.asyncOpen2(new ChannelListener(checkValueAndTrigger, null)); +} + +function checkValueAndTrigger(request, data, ctx) { + do_check_eq(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..66d463190e --- /dev/null +++ b/netwerk/test/unit/test_bug468594.js @@ -0,0 +1,127 @@ +// +// 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. + +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +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(Components.interfaces.nsIHttpChannel); + httpChan.requestMethod = "GET"; + httpChan.setRequestHeader("x-request", value, false); + return httpChan; +} + +function triggerNextTest() { + var channel = setupChannel(tests[index].url, tests[index].server); + channel.asyncOpen2(new ChannelListener(checkValueAndTrigger, null)); +} + +function checkValueAndTrigger(request, data, ctx) { + logit(index, data); + do_check_eq(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..7d2ab4bc2b --- /dev/null +++ b/netwerk/test/unit/test_bug470716.js @@ -0,0 +1,174 @@ +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: function(request, context) { }, + + onStopRequest: function(request, cx, statusCode) + { + // check status code + do_check_eq(statusCode, test_result); + + // check number of copied bytes + do_check_eq(pipe2.inputStream.available(), test_content.length); + + // check content + var scinp = new ScriptableInputStream(pipe2.inputStream); + var content = scinp.read(scinp.available()); + do_check_eq(content, test_content); + + // check closed sink + try { + pipe2.outputStream.write("closedSinkTest", 14); + do_check_false(test_sink_closed); + } + catch (ex) { + do_check_true(test_sink_closed); + } + + // check closed source + try { + pipe1.outputStream.write("closedSourceTest", 16); + do_check_false(test_source_closed); + } + catch (ex) { + do_check_true(test_source_closed); + } + + do_timeout(0, do_test); + }, + + QueryInterface: function(aIID) + { + if (aIID.equals(Ci.nsIRequestObserver) || + aIID.equals(Ci.nsISupports)) + return this; + + throw Cr.NS_ERROR_NO_INTERFACE; + } +}; + +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..942ab1e005 --- /dev/null +++ b/netwerk/test/unit/test_bug477578.js @@ -0,0 +1,50 @@ +// test that methods are not normalized +Cu.import("resource://gre/modules/NetUtil.jsm"); + +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(Components.interfaces.nsIHttpChannel); + + for (var i = 0; i < testMethods.length; i++) { + chan.requestMethod = testMethods[i]; + do_check_eq(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..1a5e3335be --- /dev/null +++ b/netwerk/test/unit/test_bug479413.js @@ -0,0 +1,59 @@ +/** + * Test for unassigned code points in IDNs (RFC 3454 section 7) + */ + +var idnService; + +function expected_pass(inputIDN) +{ + var isASCII = {}; + var displayIDN = idnService.convertToDisplayIDN(inputIDN, isASCII); + do_check_eq(displayIDN, inputIDN); +} + +function expected_fail(inputIDN) +{ + var isASCII = {}; + var displayIDN = ""; + + try { + displayIDN = idnService.convertToDisplayIDN(inputIDN, isASCII); + } + catch(e) {} + + do_check_neq(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..05f0bc4f0b --- /dev/null +++ b/netwerk/test/unit/test_bug479485.js @@ -0,0 +1,47 @@ +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, null, null); + } + 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"); + do_check_eq(exception_threw, exception_expected); + + exception_threw = false; + newURI = ios.newURI("http://foo.com", null, null); + try { + newURI.spec = "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 setting a spec of a URI with "+port+" as a port"); + do_check_eq(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..fde70f005e --- /dev/null +++ b/netwerk/test/unit/test_bug482601.js @@ -0,0 +1,233 @@ +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +var httpserv = null; +var test_nr = 0; +var observers_called = ""; +var handlers_called = ""; +var buffer = ""; + +var observer = { + QueryInterface: function (aIID) { + if (aIID.equals(Ci.nsISupports) || + aIID.equals(Ci.nsIObserver)) + return this; + throw Cr.NS_ERROR_NO_INTERFACE; + }, + + observe: function(subject, topic, data) { + if (observers_called.length) + observers_called += ","; + + observers_called += topic; + } +}; + +var listener = { + onStartRequest: function (request, ctx) { + buffer = ""; + }, + + onDataAvailable: function (request, ctx, stream, offset, count) { + buffer = buffer.concat(read_stream(stream, count)); + }, + + onStopRequest: function (request, ctx, status) { + do_check_eq(status, Cr.NS_OK); + do_check_eq(buffer, "0123456789"); + do_check_eq(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", false); + obs.addObserver(observer, "http-on-examine-merged-response", false); + obs.addObserver(observer, "http-on-examine-cached-response", false); + + do_timeout(0, do_test); + do_test_pending(); +} + +function do_test() { + if (test_nr < tests.length) { + tests[test_nr](); + } + else { + do_check_eq(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); + 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.asyncOpen2(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) { + do_check_eq(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.asyncOpen2(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) { + do_check_eq(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.asyncOpen2(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) { + do_check_eq(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.asyncOpen2(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) { + do_check_true(metadata.hasHeader("If-Range")); + do_check_eq(metadata.getHeader("If-Range"), + "Thu, 1 Jan 2009 00:00:00 GMT"); + do_check_true(metadata.hasHeader("Range")); + do_check_eq(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) { + do_check_true(metadata.hasHeader("If-Modified-Since")); + do_check_eq(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_bug484684.js b/netwerk/test/unit/test_bug484684.js new file mode 100644 index 0000000000..259d8cec1b --- /dev/null +++ b/netwerk/test/unit/test_bug484684.js @@ -0,0 +1,115 @@ +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 FILE \n" + + "201: \"%20file2\" 0 Sat%2C%2001%20Jan%202000%2000%3A00%3A00 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 FILE \n" + + "201: \"file2\" 214059 Sat%2C%2001%20Jan%202000%2000%3A00%3A00 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 FILE \n" + + "201: \"%20file2\" 192 Sat%2C%2001%20Jan%202000%2000%3A00%3A00 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 DIRECTORY \n" + + "201: \"junction1\" Sat%2C%2001%20Jan%202000%2001%3A00%3A00 SYMBOLIC-LINK \n" + + "201: \"file1\" 95077 Sat%2C%2001%20Jan%202000%2001%3A00%3A00 FILE \n" + + "201: \"%20dir2\" 0 Sat%2C%2001%20Jan%202000%2001%3A00%3A00 DIRECTORY \n" + + "201: \"%20junction2\" Sat%2C%2001%20Jan%202000%2001%3A00%3A00 SYMBOLIC-LINK \n" + + "201: \"%20file2\" 95077 Sat%2C%2001%20Jan%202000%2001%3A00%3A00 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 DIRECTORY \n" + + "201: \"dir2\" 0 Sat%2C%2001%20Jan%202000%2001%3A00%3A00 DIRECTORY \n" + + "201: \"dir3\" 0 Sat%2C%2001%20Jan%202000%2001%3A00%3A00 DIRECTORY \n" + + "201: \"junction1\" Sat%2C%2001%20Jan%202000%2001%3A00%3A00 SYMBOLIC-LINK \n" + + "201: \"junction2\" Sat%2C%2001%20Jan%202000%2001%3A00%3A00 SYMBOLIC-LINK \n" + + "201: \"junction3\" Sat%2C%2001%20Jan%202000%2001%3A00%3A00 SYMBOLIC-LINK \n" + + "201: \"file1\" 95077 Sat%2C%2001%20Jan%202000%2001%3A00%3A00 FILE \n" + + "201: \"file2\" 95077 Sat%2C%2001%20Jan%202000%2001%3A00%3A00 FILE \n"] +] + +function checkData(request, data, ctx) { + do_check_eq(tests[0][1], data); + tests.shift(); + do_execute_soon(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 = { + password: "", + asciiSpec: URL, + QueryInterface: XPCOMUtils.generateQI([Ci.nsIURI]) + }; + + var channel = { + URI: url, + contentLength: -1, + pending: true, + isPending: function() { + return this.pending; + }, + QueryInterface: XPCOMUtils.generateQI([Ci.nsIChannel]) + }; + + converter.onStartRequest(channel, null); + converter.onDataAvailable(channel, null, 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() { + do_execute_soon(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..8b588b1a8a --- /dev/null +++ b/netwerk/test/unit/test_bug490095.js @@ -0,0 +1,116 @@ +// +// Verify that the VALIDATE_NEVER and LOAD_FROM_CACHE flags override +// heuristic query freshness as defined in RFC 2616 section 13.9 +// + +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +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: Components.interfaces.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: Components.interfaces.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(Components.interfaces.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.asyncOpen2(new ChannelListener(checkValueAndTrigger, null)); +} + +function checkValueAndTrigger(request, data, ctx) { + logit(index, data); + do_check_eq(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..5c28dfa326 --- /dev/null +++ b/netwerk/test/unit/test_bug504014.js @@ -0,0 +1,69 @@ +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 { + var uri = ios.newURI(valid_URIs[i], null, null); + } catch (e) { + do_throw("cannot create URI:" + valid_URIs[i]); + } + } + + for (var i=0 ; i<invalid_URIs.length ; i++) { + try { + var uri = ios.newURI(invalid_URIs[i], null, null); + do_throw("should throw: " + invalid_URIs[i]); + } catch (e) { + do_check_eq(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..176b5ed378 --- /dev/null +++ b/netwerk/test/unit/test_bug510359.js @@ -0,0 +1,77 @@ +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +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(Components.interfaces.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.asyncOpen2(new ChannelListener(checkValueAndTrigger, null)); +} + +function checkValueAndTrigger(request, data, ctx) { + do_check_eq(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 { + var IMS = 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..28e43a3de9 --- /dev/null +++ b/netwerk/test/unit/test_bug515583.js @@ -0,0 +1,73 @@ +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 FILE \n" + + "201: \"E\" 2048 Mon%2C%2008%20Mar%201993%2018%3A09%3A01 FILE \n"] + , + ["\r\r\r\n" + , + "300: " + URL + "\n" + + "200: filename content-length last-modified file-type\n"] +] + +function checkData(request, data, ctx) { + do_check_eq(tests[0][1], data); + tests.shift(); + do_execute_soon(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 = { + password: "", + asciiSpec: URL, + QueryInterface: XPCOMUtils.generateQI([Ci.nsIURI]) + }; + + var channel = { + URI: url, + contentLength: -1, + pending: true, + isPending: function() { + return this.pending; + }, + QueryInterface: XPCOMUtils.generateQI([Ci.nsIChannel]) + }; + + converter.onStartRequest(channel, null); + converter.onDataAvailable(channel, null, 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() { + do_execute_soon(next_test); + do_test_pending(); +} diff --git a/netwerk/test/unit/test_bug528292.js b/netwerk/test/unit/test_bug528292.js new file mode 100644 index 0000000000..ef030baeeb --- /dev/null +++ b/netwerk/test/unit/test_bug528292.js @@ -0,0 +1,90 @@ +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +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); + return; +} + +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; +} + +function run_test() +{ + // 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); + } + + 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 (since nsICookieService::setCookieString + // requires such a channel in order to successfully 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, "", null); + Cc["@mozilla.org/cookieService;1"].getService(Ci.nsICookieService). + setCookieString(postRedirectURI, null, sentCookieVal, chan); + + // Load the pre-redirect URI. + chan.asyncOpen2(new ChannelListener(finish_test, null)); + do_test_pending(); +} + +function finish_test(event) +{ + do_check_eq(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..1dcb475be7 --- /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 */ + +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +// 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: function (req, ctx) { + }, + + onDataAvailable: function (req, ctx, stream, off, count) { + do_check_eq(req.getResponseHeader("Content-Length"), CONTENT_LENGTH); + + // We're done here, cancel the channel + req.cancel(NS_BINDING_ABORT); + }, + + onStopRequest: function (req, ctx, 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.asyncOpen2(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..e44fa9c170 --- /dev/null +++ b/netwerk/test/unit/test_bug540566.js @@ -0,0 +1,18 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +function continue_test(status, entry) { + do_check_eq(status, Components.results.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..ed25f3ebed --- /dev/null +++ b/netwerk/test/unit/test_bug543805.js @@ -0,0 +1,93 @@ +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 FILE \n" + + "201: \"%20test.blankfile\" 22 " + day + "%2C%2001%20Jan%20" + year + "%2020%3A19%3A00 FILE \n" + + "201: \"%20test2.blankfile\" 33 Tue%2C%2001%20Apr%202008%2000%3A00%3A00 FILE \n" + + "201: \"nodup.file\" 44 " + day + "%2C%2001%20Jan%20" + year + "%2020%3A19%3A00 FILE \n" + + "201: \"test.file\" 55 " + day + "%2C%2001%20Jan%20" + year + "%2020%3A19%3A00 FILE \n" + + "201: \"test2.file\" 66 Tue%2C%2001%20Apr%202008%2000%3A00%3A00 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 FILE \n" + + "201: \"%20test.blankfile\" 22 " + day + "%2C%2001%20Jan%20" + year + "%2020%3A19%3A00 FILE \n" + + "201: \"%20test2.blankfile\" 33 Tue%2C%2001%20Apr%202008%2000%3A00%3A00 FILE \n" + + "201: \"nodup.file\" 44 " + day + "%2C%2001%20Jan%20" + year + "%2020%3A19%3A00 FILE \n" + + "201: \"test.file\" 55 " + day + "%2C%2001%20Jan%20" + year + "%2020%3A19%3A00 FILE \n" + + "201: \"test2.file\" 66 Tue%2C%2001%20Apr%202008%2000%3A00%3A00 FILE \n"] +] + +function checkData(request, data, ctx) { + do_check_eq(tests[0][1], data); + tests.shift(); + do_execute_soon(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 = { + password: "", + asciiSpec: URL, + QueryInterface: XPCOMUtils.generateQI([Ci.nsIURI]) + }; + + var channel = { + URI: url, + contentLength: -1, + pending: true, + isPending: function() { + return this.pending; + }, + QueryInterface: XPCOMUtils.generateQI([Ci.nsIChannel]) + }; + + converter.onStartRequest(channel, null); + converter.onDataAvailable(channel, null, 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() { + do_execute_soon(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..00e222a2c0 --- /dev/null +++ b/netwerk/test/unit/test_bug553970.js @@ -0,0 +1,44 @@ +function makeURL(spec) { + return Cc["@mozilla.org/network/io-service;1"]. + getService(Components.interfaces.nsIIOService). + newURI(spec, null, null). + QueryInterface(Components.interfaces.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]; + do_check_eq(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..d794aeaf6d --- /dev/null +++ b/netwerk/test/unit/test_bug561042.js @@ -0,0 +1,38 @@ +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +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: function (request, ctx) { + }, + + onDataAvailable: function (request, ctx, stream) { + }, + + onStopRequest: function (request, ctx, status) { + do_check_eq(status, Components.results.NS_OK); + do_test_finished(); + }, + +}; + +function run_test() { + var server = new HttpServer(); + 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(Components.interfaces.nsIHttpChannel); + chan.asyncOpen2(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..fd67d24dc6 --- /dev/null +++ b/netwerk/test/unit/test_bug561276.js @@ -0,0 +1,68 @@ +// +// Verify that we hit the net if we discover a cycle of redirects +// coming from cache. +// + +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +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(Components.interfaces.nsIHttpChannel); + httpChan.requestMethod = "GET"; + return httpChan; +} + +function checkValueAndTrigger(request, data, ctx) +{ + do_check_eq("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.asyncOpen2(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..3e2c495e0e --- /dev/null +++ b/netwerk/test/unit/test_bug580508.js @@ -0,0 +1,26 @@ +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/", null, null); + 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"); + do_check_eq(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", + null, null); + let myNetUtilURI = ioService.newURI("resource://my-gre-modules/NetUtil.jsm", + null, null); + do_check_eq(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..008577da06 --- /dev/null +++ b/netwerk/test/unit/test_bug586908.js @@ -0,0 +1,92 @@ +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); +Cu.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: function (iid) { + if (iid.equals(Components.interfaces.nsISupports) || + iid.equals(Components.interfaces.nsISystemProxySettings)) + return this; + throw Components.results.NS_ERROR_NO_INTERFACE; + }, + + mainThreadOnly: true, + PACURI: "http://localhost:" + httpserv.identity.primaryPort + "/redirect", + getProxyForURI: function(aURI) { + throw Components.results.NS_ERROR_NOT_IMPLEMENTED; + } + }; +}); + +function checkValue(request, data, ctx) { + do_check_true(called); + do_check_eq("ok", data); + httpserv.stop(do_test_finished); +} + +function makeChan(url) { + return NetUtil.newChannel({uri: url, loadUsingSystemPrincipal: true}) + .QueryInterface(Components.interfaces.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", + Components.interfaces.nsIProtocolProxyService.PROXYCONFIG_SYSTEM); + + // clear cache + evict_cache_entries(); + + var chan = makeChan("http://localhost:" + httpserv.identity.primaryPort + + "/target"); + chan.asyncOpen2(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..e86bdf817b --- /dev/null +++ b/netwerk/test/unit/test_bug596443.js @@ -0,0 +1,97 @@ +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +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(Components.interfaces.nsIHttpChannel); + httpChan.setRequestHeader("x-request", xRequest, false); + + return httpChan; +} + +function Listener(response) { + this._response = response; +} +Listener.prototype = { + _response: null, + _buffer: null, + + QueryInterface: function(iid) { + if (iid.equals(Components.interfaces.nsIStreamListener) || + iid.equals(Components.interfaces.nsIRequestObserver) || + iid.equals(Components.interfaces.nsISupports)) + return this; + throw Components.results.NS_ERROR_NO_INTERFACE; + }, + + onStartRequest: function (request, ctx) { + this._buffer = ""; + }, + onDataAvailable: function (request, ctx, stream, offset, count) { + this._buffer = this._buffer.concat(read_stream(stream, count)); + }, + onStopRequest: function (request, ctx, status) { + do_check_eq(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); + + // make sure we have a profile so we can use the disk-cache + do_get_profile(); + + // clear cache + evict_cache_entries(); + + var ch0 = setupChannel("/bug596443", "Response0", Ci.nsIRequest.LOAD_BYPASS_CACHE); + ch0.asyncOpen2(new Listener("Response0")); + + var ch1 = setupChannel("/bug596443", "Response1", Ci.nsIRequest.LOAD_BYPASS_CACHE); + ch1.asyncOpen2(new Listener("Response1")); + + var ch2 = setupChannel("/bug596443", "Should not be used"); + ch2.asyncOpen2(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..811608a619 --- /dev/null +++ b/netwerk/test/unit/test_bug618835.js @@ -0,0 +1,115 @@ +// +// 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. +// + +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +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: function(request, context) { }, + onStopRequest: function(request, context, status) { + do_check_eq(1, numberOfCLHandlerCalls); + do_execute_soon(function() { + var channel = setupChannel("http://localhost:" + + httpserv.identity.primaryPort + "/post"); + channel.requestMethod = "POST"; + channel.asyncOpen2(new RedirectingListener()); + }); + } +}; + +// Verify that Location-URI has been loaded once, reload post_target +function RedirectingListener() { } +RedirectingListener.prototype = { + onStartRequest: function(request, context) { }, + onStopRequest: function(request, context, status) { + do_check_eq(1, numberOfHandlerCalls); + do_execute_soon(function() { + var channel = setupChannel("http://localhost:" + + httpserv.identity.primaryPort + "/post"); + channel.requestMethod = "POST"; + channel.asyncOpen2(new VerifyingListener()); + }); + } +}; + +// Verify that Location-URI has been loaded twice (cached entry invalidated), +// reload Content-Location-URI +function VerifyingListener() { } +VerifyingListener.prototype = { + onStartRequest: function(request, context) { }, + onStopRequest: function(request, context, status) { + do_check_eq(2, numberOfHandlerCalls); + var channel = setupChannel("http://localhost:" + + httpserv.identity.primaryPort + "/cl"); + channel.asyncOpen2(new FinalListener()); + } +}; + +// Verify that Location-URI has been loaded twice (cached entry invalidated), +// stop test +function FinalListener() { } +FinalListener.prototype = { + onStartRequest: function(request, context) { }, + onStopRequest: function(request, context, status) { + do_check_eq(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.asyncOpen2(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..e5ac078b4c --- /dev/null +++ b/netwerk/test/unit/test_bug633743.js @@ -0,0 +1,186 @@ +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +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(Components.interfaces.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: function(iid) { + if (iid.equals(Ci.nsIStreamListener) || + iid.equals(Ci.nsIRequestObserver) || + iid.equals(Ci.nsISupports)) + return this; + throw Cr.NS_ERROR_NO_INTERFACE; + }, + + onStartRequest: function(request, context) { }, + + onDataAvailable: function(request, context, stream, offset, count) { + this._buffer = this._buffer.concat(read_stream(stream, count)); + }, + + onStopRequest: function(request, context, status) { + do_check_eq(this._buffer, this._expectVal); + do_timeout(0, run_next_test); + }, + + run: function() { + 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.asyncOpen2(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 + do_check_false(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_bug650995.js b/netwerk/test/unit/test_bug650995.js new file mode 100644 index 0000000000..3c1ea8d67e --- /dev/null +++ b/netwerk/test/unit/test_bug650995.js @@ -0,0 +1,159 @@ +// +// Test that "max_entry_size" prefs for disk- and memory-cache prevents +// caching resources with size out of bounds +// + +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +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(Components.interfaces.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; + } + do_execute_soon(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) { + try { + cap = prefService.getIntPref("browser.cache.memory.capacity"); + } + catch(ex) { + cap = 0; + } + if (cap == 0) { + prefService.setIntPref("browser.cache.memory.capacity", 1024); + } + } + prefService.setBoolPref("browser.cache.disk.enable", diskDevice); + if (diskDevice) { + try { + cap = prefService.getIntPref("browser.cache.disk.capacity"); + } + catch(ex) { + cap = 0; + } + if (cap == 0) { + prefService.setIntPref("browser.cache.disk.capacity", 1024); + } + } + var channel = setupChannel("/bug650995", "Initial value"); + channel.asyncOpen2(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.asyncOpen2(new ChannelListener(this.initialLoad, this)); + }, + + this.initialLoad = function(request, data, ctx) { + do_check_eq(firstRequest, data); + var channel = setupChannel("/bug650995", secondRequest); + do_execute_soon(function() { + channel.asyncOpen2(new ChannelListener(ctx.testAndTriggerNext, ctx)); + }); + }, + + this.testAndTriggerNext = function(request, data, ctx) { + do_check_eq(secondExpectedReply, data); + do_execute_soon(nextTest); + } +} + +function run_test() +{ + httpserver.registerPathHandler("/bug650995", handler); + httpserver.start(-1); + + prefService.setBoolPref("browser.cache.offline.enable", 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..e2b781da87 --- /dev/null +++ b/netwerk/test/unit/test_bug652761.js @@ -0,0 +1,17 @@ +// This is just a crashtest for a url that is rejected at parse time (port 80,000) + +Cu.import("resource://gre/modules/NetUtil.jsm"); + +function run_test() +{ + // Bug 1301621 makes invalid ports throw + Assert.throws(() => { + var chan = NetUtil.newChannel({ + uri: "http://localhost:80000/", + loadUsingSystemPrincipal: true + }); + }, "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..83fd7286f6 --- /dev/null +++ b/netwerk/test/unit/test_bug654926.js @@ -0,0 +1,88 @@ +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) +{ + do_check_eq(status, Cr.NS_OK); + var os = entry.openOutputStream(0); + var data = gen_1MiB(); + + // 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) +{ + do_check_eq(status, Cr.NS_OK); + var os = entry.openOutputStream(entry.dataSize); + 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..74c9f66c0e --- /dev/null +++ b/netwerk/test/unit/test_bug654926_doom_and_read.js @@ -0,0 +1,77 @@ +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) +{ + do_check_eq(status, Cr.NS_OK); + var os = entry.openOutputStream(0); + var data = gen_1MiB(); + + 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) +{ + do_check_eq(status, Cr.NS_OK); + var os = entry.openOutputStream(entry.dataSize); + var data = gen_1MiB(); + + entry.asyncDoom(null); + write_and_check(os, data, data.length); + + os.close(); + + var is = entry.openInputStream(0); + pumpReadStream(is, function(read) { + do_check_eq(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..2916b03805 --- /dev/null +++ b/netwerk/test/unit/test_bug654926_test_seek.js @@ -0,0 +1,63 @@ +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) +{ + do_check_eq(status, Cr.NS_OK); + var os = entry.openOutputStream(0); + var data = gen_1MiB(); + + 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) +{ + do_check_eq(status, Cr.NS_OK); + var os = entry.openOutputStream(entry.dataSize); + + // 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..b9b7d105f7 --- /dev/null +++ b/netwerk/test/unit/test_bug659569.js @@ -0,0 +1,57 @@ +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +var httpserver = new HttpServer(); + +function setupChannel(suffix) +{ + return NetUtil.newChannel({ + uri: "http://localhost:" + httpserver.identity.primaryPort + suffix, + loadUsingSystemPrincipal: true + }); +} + +function checkValueAndTrigger(request, data, ctx) +{ + do_check_eq("Ok", data); + httpserver.stop(do_test_finished); +} + +function run_test() +{ + // 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.asyncOpen2(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..99c01c40ce --- /dev/null +++ b/netwerk/test/unit/test_bug660066.js @@ -0,0 +1,42 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +Components.utils.import("resource://gre/modules/NetUtil.jsm"); +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..."); + do_check_false(uri1.equals(uri2)); + do_check_false(uri1.equalsExceptRef(uri2)); + + do_info("Checking equality in reverse direction..."); + do_check_false(uri2.equals(uri1)); + do_check_false(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.spec = BLOBURI_SPEC; + + do_info("Verifying that .spec matches"); + do_check_eq(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_bug667818.js b/netwerk/test/unit/test_bug667818.js new file mode 100644 index 0000000000..e730e74c1b --- /dev/null +++ b/netwerk/test/unit/test_bug667818.js @@ -0,0 +1,21 @@ +Cu.import("resource://gre/modules/Services.jsm"); + +function makeURI(str) { + return Components.classes["@mozilla.org/network/io-service;1"] + .getService(Components.interfaces.nsIIOService) + .newURI(str, null, null); +} + +function run_test() { + // Allow all cookies. + Services.prefs.setIntPref("network.cookie.cookieBehavior", 0); + var serv = Components.classes["@mozilla.org/cookieService;1"] + .getService(Components.interfaces.nsICookieService); + var uri = makeURI("http://example.com/"); + // Try an expiration time before the epoch + serv.setCookieString(uri, null, "test=test; path=/; domain=example.com; expires=Sun, 31-Dec-1899 16:00:00 GMT;", null); + do_check_eq(serv.getCookieString(uri, null), null); + // Now sanity check + serv.setCookieString(uri, null, "test2=test2; path=/; domain=example.com;", null); + do_check_eq(serv.getCookieString(uri, null), "test2=test2"); +} diff --git a/netwerk/test/unit/test_bug667907.js b/netwerk/test/unit/test_bug667907.js new file mode 100644 index 0000000000..8e33422740 --- /dev/null +++ b/netwerk/test/unit/test_bug667907.js @@ -0,0 +1,84 @@ +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +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: function(iid) { + if (iid.equals(Components.interfaces.nsIStreamListener) || + iid.equals(Components.interfaces.nsIRequestObserver) || + iid.equals(Components.interfaces.nsISupports)) + return this; + throw Components.results.NS_ERROR_NO_INTERFACE; + }, + + onStartRequest: function(request, context) { + do_check_eq(request.QueryInterface(Ci.nsIChannel).contentType, + this.contentType); + request.cancel(Cr.NS_BINDING_ABORTED); + }, + + onDataAvailable: function(request, context, stream, offset, count) { + do_throw("Unexpected onDataAvailable"); + }, + + onStopRequest: function(request, context, status) { + do_check_eq(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.asyncOpen2(new listener("text/plain", function() { run_test2();})); + + do_test_pending(); +} + +function run_test2() +{ + var channel = make_channel(uri2); + channel.asyncOpen2(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..bbb376f8f6 --- /dev/null +++ b/netwerk/test/unit/test_bug669001.js @@ -0,0 +1,160 @@ +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +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: function() { }, + test: function(response) { + do_check_true(fetched); + } +}, +{ + prepare: function() { }, + test: function(response) { + do_check_false(fetched); + } +}, +{ + prepare: function() { + setUA("A different User Agent"); + }, + test: function(response) { + do_check_true(fetched); + } +}, +{ + prepare: function() { }, + test: function(response) { + do_check_false(fetched); + } +}, +{ + prepare: function() { + setUA("And another User Agent"); + }, + test: function(response) { + do_check_true(fetched); + } +}, +{ + prepare: function() { }, + test: function(response) { + do_check_false(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... + do_execute_soon(function() { + chan.asyncOpen2(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..e10976559e --- /dev/null +++ b/netwerk/test/unit/test_bug767025.js @@ -0,0 +1,275 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ + +Cu.import("resource://testing-common/httpd.js"); + +/** + * This is testcase do following steps to make sure bug767025 removing + * files as expection. + * + * 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. + */ + +Cu.import("resource://gre/modules/Services.jsm"); + +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) { + do_print("manifest\n"); + response.setHeader("content-type", "text/cache-manifest"); + + response.write(kManifest); +} + +function datafile_handler(metadata, response) { + do_print("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) { + do_print("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.setComplexValue("browser.cache.offline.parent_directory", + Ci.nsILocalFile, do_get_profile()); + do_print("profile " + do_get_profile()); +} + +function init_http_server() { + httpServer = new HttpServer(); + httpServer.registerPathHandler("/app.appcache", manifest_handler); + httpServer.registerPathHandler("/app", app_handler); + for (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(LoadContextInfo.default, null); + storage.asyncEvictStorage(null); +} + +function do_app_cache(manifestURL, pageURL) { + let update_service = Cc[kNS_OFFLINECACHEUPDATESERVICE_CONTRACTID]. + getService(Ci.nsIOfflineCacheUpdateService); + + Services.perms.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: function QueryInterface(iftype) { + return this; + }, + + updateStateChanged: stateChangeHandler, + applicationCacheAvailable: cacheAvailHandler + };~ + update.addObserver(observer, false); + + 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, null, null), + ioService.newURI(pageURL, null, null)); + 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() { + do_print("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) { + do_print("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(LoadContextInfo.default, appcache); + + // Doom foo1 & foo2 + storage.asyncDoomURI(createURI(kHttpLocation + "pages/foo1"), "", { onCacheEntryDoomed: function() { + storage.asyncDoomURI(createURI(kHttpLocation + "pages/foo2"), "", { onCacheEntryDoomed: function() { + 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"); + do_check_eq(file.exists(), true); + + file = do_get_profile().clone(); + file.append("OfflineCache"); + file.append("C"); + file.append("2"); + file.append("5F356A168B5E3B-0"); + do_check_eq(file.exists(), false); + + // activate foo3 + asyncOpenCacheEntry( + kHttpLocation + "pages/foo3", + "appcache", Ci.nsICacheStorage.OPEN_READONLY, null, + function(status, entry, appcache) { + var hold_entry_foo3 = entry; + + // evict all documents. + let storage = get_cache_service().appCacheStorage(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"); + do_check_eq(file.exists(), true); + + file = do_get_profile().clone(); + file.append("OfflineCache"); + file.append("0"); + file.append("0"); + file.append("61FEE819921D39-0"); + do_check_eq(file.exists(), false); + + file = do_get_profile().clone(); + file.append("OfflineCache"); + file.append("3"); + file.append("9"); + file.append("0D8759F1DE5452-0"); + do_check_eq(file.exists(), false); + + file = do_get_profile().clone(); + file.append("OfflineCache"); + file.append("C"); + file.append("2"); + file.append("5F356A168B5E3B-0"); + do_check_eq(file.exists(), false); + + // foo3 + file = do_get_profile().clone(); + file.append("OfflineCache"); + file.append("D"); + file.append("C"); + file.append("1ADCCC843B5C00-0"); + do_check_eq(file.exists(), true); + + file = do_get_profile().clone(); + file.append("OfflineCache"); + file.append("F"); + file.append("0"); + file.append("FC3E6D6C1164E9-0"); + do_check_eq(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..8fd7abcea4 --- /dev/null +++ b/netwerk/test/unit/test_bug770243.js @@ -0,0 +1,207 @@ +/* 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 +*/ + +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +var httpserv; + +function addCreds(scheme, host) +{ + var authMgr = Components.classes['@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 = Components.classes['@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) { + do_check_eq(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) { + do_check_eq(metadata.hasHeader("Authorization"), false); + do_check_eq(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) { + do_check_eq(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) { + do_check_eq(metadata.hasHeader("Authorization"), false); + do_check_eq(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) { + do_check_eq(metadata.hasHeader("Authorization"), true); + do_check_eq(metadata.getHeader("If-None-Match"), '"two"'); + response.setStatusLine(metadata.httpVersion, 304, "OK"); + response.setHeader("ETag", '"two"', false); + clearCreds(); + }, + + // Test 4 + function(metadata, response) { + do_check_eq(metadata.hasHeader("Authorization"), false); + do_check_eq(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) { + do_check_eq(metadata.hasHeader("Proxy-Authorization"), true); + do_check_eq(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) { + do_check_eq(metadata.hasHeader("Proxy-Authorization"), false); + do_check_eq(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) { + do_check_eq(metadata.hasHeader("Proxy-Authorization"), true); + do_check_eq(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.asyncOpen2(new ChannelListener(function(req, body) { + do_check_eq(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.asyncOpen2(new ChannelListener(function(req, body) { + do_check_eq(body, "Response body 2"); + sync_and_run_next_test(); + }, null, CL_NOT_FROM_CACHE)); + }, + + // Test 3: 401 and 304 + function() { + var ch = makeChan(); + ch.asyncOpen2(new ChannelListener(function(req, body) { + do_check_eq(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.asyncOpen2(new ChannelListener(function(req, body) { + do_check_eq(body, "Response body 3"); + sync_and_run_next_test(); + }, null, CL_NOT_FROM_CACHE)); + }, + + // Test 5: 407 and 304 + function() { + var ch = makeChan(); + ch.asyncOpen2(new ChannelListener(function(req, body) { + do_check_eq(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.setCharPref("network.proxy.no_proxies_on", ""); + prefs.setIntPref("network.proxy.type", 1); + + 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..ecda0780cd --- /dev/null +++ b/netwerk/test/unit/test_bug812167.js @@ -0,0 +1,127 @@ +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +/* +- 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; + return; +} + +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; + return; +} + +function contentHandler(metadata, response) +{ + response.setHeader("Content-Type", "text/plain"); + response.bodyOutputStream.write(responseBody, responseBody.length); +} + +function check_response(path, request, buffer, expectedExpiration, continuation) +{ + do_check_eq(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) { + do_check_eq(status, 0); + + // Expired entry is on disk, no-store entry is in memory + do_check_eq(entry.persistent, expectedExpiration); + + // Do the request again and check the server handler is called appropriately + var chan = make_channel(path); + chan.asyncOpen2(new ChannelListener(function(request, buffer) { + do_check_eq(buffer, responseBody); + + if (expectedExpiration) { + // Handler had to be called second time + do_check_eq(redirectHandler_ExpiresInPast_calls, 2); + } + else { + // Handler had to be called second time (no-store forces validate), + // and we are just in memory + do_check_eq(redirectHandler_NoStore_calls, 2); + do_check_true(!entry.persistent); + } + + continuation(); + }, null)); + }); +} + +function run_test_no_store() +{ + var chan = make_channel(randomURI1); + chan.asyncOpen2(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.asyncOpen2(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..233e13a9e3 --- /dev/null +++ b/netwerk/test/unit/test_bug826063.js @@ -0,0 +1,107 @@ +/* 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 + */ + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + + +var URIs = [ + "http://example.org", + "https://example.org", + "ftp://example.org" + ]; + +function LoadContext(usePrivateBrowsing) { + this.usePrivateBrowsing = usePrivateBrowsing; +} +LoadContext.prototype = { + originAttributes: {}, + QueryInterface: XPCOMUtils.generateQI([Ci.nsILoadContext, Ci.nsIInterfaceRequestor]), + getInterface: XPCOMUtils.generateQI([Ci.nsILoadContext]) +}; + +function getChannels() { + for (let u of URIs) { + yield NetUtil.newChannel({ + uri: u, + loadUsingSystemPrincipal: true + }); + } +} + +function checkPrivate(channel, shouldBePrivate) { + do_check_eq(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 = new LoadContext(true); + 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 = new LoadContext(false); + 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. + + +function run_test() { + run_next_test(); +} diff --git a/netwerk/test/unit/test_bug856978.js b/netwerk/test/unit/test_bug856978.js new file mode 100644 index 0000000000..9624ef64e1 --- /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. + +Components.utils.import("resource://testing-common/httpd.js"); +Components.utils.import("resource://gre/modules/NetUtil.jsm"); + +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; + } + do_check_true(noAuthHeader); + } else { + // Not our test request yet. + 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: function() { + do_print("Registering " + notification); + Cc["@mozilla.org/observer-service;1"].getService(Ci.nsIObserverService). + addObserver(this, notification, true); + }, + + QueryInterface: function(iid) { + if (iid.equals(Ci.nsIObserver) || iid.equals(Ci.nsISupportsWeakReference) || + iid.equals(Ci.nsISupports)) { + return this; + } + throw Cr.NS_ERROR_NO_INTERFACE; + }, + + observe: function(subject, topic, data) { + if (topic == notification) { + if (!(subject instanceof Ci.nsIHttpChannel)) { + do_throw(notification + " observed a non-HTTP channel."); + } + try { + let authHeader = 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, ctx) {}, + + onDataAvailable: function test_ODA() { + do_throw("Should not get any data!"); + }, + + onStopRequest: function test_onStopR(request, ctx, 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.asyncOpen2(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.asyncOpen2(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..97b9ee20f6 --- /dev/null +++ b/netwerk/test/unit/test_bug894586.js @@ -0,0 +1,158 @@ +/* + * Tests for bug 894586: nsSyncLoadService::PushSyncStreamToListener + * should not fail for channels of unknown size + */ + +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +var contentSecManager = Cc["@mozilla.org/contentsecuritymanager;1"] + .getService(Ci.nsIContentSecurityManager); + +function ProtocolHandler() { + this.uri = Cc["@mozilla.org/network/simple-uri;1"]. + createInstance(Ci.nsIURI); + this.uri.spec = this.scheme + ":dummy"; + this.uri.QueryInterface(Ci.nsIMutable).mutable = false; +} + +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; + }, + newURI: function(aSpec, aOriginCharset, aBaseURI) { + return this.uri; + }, + newChannel2: function(aURI, aLoadInfo) { + this.loadInfo = aLoadInfo; + return this; + }, + newChannel: function(aURI) { + return this; + }, + allowPort: function(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", NS_ERROR_NOT_IMPLEMENTED); + }, + open: function() { + var file = do_get_file("test_bug894586.js", false); + do_check_true(file.exists()); + var url = Services.io.newFileURI(file); + return NetUtil.newChannel({uri: url, loadUsingSystemPrincipal: true}).open2(); + }, + open2: function() { + // throws an error if security checks fail + contentSecManager.performSecurityCheck(this, null); + return this.open(); + }, + asyncOpen: function(aListener, aContext) { + throw Components.Exception("Not implemented", + Cr.NS_ERROR_NOT_IMPLEMENTED); + }, + asyncOpen2: function(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: function(status) {}, + loadGroup: null, + loadFlags: Ci.nsIRequest.LOAD_NORMAL | + Ci.nsIRequest.INHIBIT_CACHING | + Ci.nsIRequest.LOAD_BYPASS_CACHE, + + /** nsIFactory */ + createInstance: function(aOuter, aIID) { + if (aOuter) { + throw Components.Exception("createInstance no aggregation", + Cr.NS_ERROR_NO_AGGREGATION); + } + return this.QueryInterface(aIID); + }, + lockFactory: function() {}, + + /** nsISupports */ + QueryInterface: XPCOMUtils.generateQI([Ci.nsIProtocolHandler, + Ci.nsIRequest, + Ci.nsIChannel, + Ci.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); + do_check_true(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..5e2ba65693 --- /dev/null +++ b/netwerk/test/unit/test_bug935499.js @@ -0,0 +1,7 @@ +function run_test() { + var idnService = Cc["@mozilla.org/network/idn-service;1"] + .getService(Ci.nsIIDNService); + + var isASCII = {}; + do_check_eq(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..bed26de0e6 --- /dev/null +++ b/netwerk/test/unit/test_cache-control_request.js @@ -0,0 +1,385 @@ +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +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, null, null); +} + +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(); + + if (!newCacheBackEndUsed()) { + do_check_true(true, "This test doesn't run when the old cache back end is used since it depends on the new APIs."); + return; + } + + do_test_pending(); + + 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.asyncOpen2(new ChannelListener(function(request, data) { + do_check_true(hit_server); + do_check_false(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.asyncOpen2(new ChannelListener(function(request, data) { + do_check_true(hit_server); + do_check_true(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.asyncOpen2(new ChannelListener(function(request, data) { + do_check_false(hit_server); + do_check_true(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.asyncOpen2(new ChannelListener(function(request, data) { + do_check_true(hit_server); + do_check_true(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.asyncOpen2(new ChannelListener(function(request, data) { + do_check_false(hit_server); + do_check_true(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.asyncOpen2(new ChannelListener(function(request, data) { + do_check_true(hit_server); + do_check_true(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.asyncOpen2(new ChannelListener(function(request, data) { + do_check_false(hit_server); + do_check_true(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.asyncOpen2(new ChannelListener(function(request, data) { + do_check_true(hit_server); + do_check_true(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.asyncOpen2(new ChannelListener(function(request, data) { + do_check_false(hit_server); + do_check_true(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.asyncOpen2(new ChannelListener(function(request, data) { + do_check_false(hit_server); + do_check_true(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.asyncOpen2(new ChannelListener(function(request, data) { + do_check_true(hit_server); + do_check_true(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.asyncOpen2(new ChannelListener(function(request, data) { + do_check_true(hit_server); + do_check_true(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.asyncOpen2(new ChannelListener(function(request, data) { + do_check_false(hit_server); + do_check_true(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.asyncOpen2(new ChannelListener(function(request, data) { + do_check_false(hit_server); + do_check_true(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.asyncOpen2(new ChannelListener(function(request, data) { + do_check_true(hit_server); + do_check_true(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.asyncOpen2(new ChannelListener(function(request, data) { + do_check_true(hit_server); + do_check_true(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.asyncOpen2(new ChannelListener(function(request, data) { + do_check_false(hit_server); + do_check_true(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.asyncOpen2(new ChannelListener(function(request, data) { + do_check_false(hit_server); + do_check_true(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.asyncOpen2(new ChannelListener(function(request, data) { + do_check_true(hit_server); + do_check_true(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.asyncOpen2(new ChannelListener(function(request, data) { + do_check_true(hit_server); + do_check_true(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.asyncOpen2(new ChannelListener(function(request, data) { + do_check_true(hit_server); + do_check_true(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_cache2-00-service-get.js b/netwerk/test/unit/test_cache2-00-service-get.js new file mode 100644 index 0000000000..6a8b2e10c9 --- /dev/null +++ b/netwerk/test/unit/test_cache2-00-service-get.js @@ -0,0 +1,16 @@ +function run_test() +{ + // Just check the contract ID alias works well. + try { + var serviceA = Components.classes["@mozilla.org/netwerk/cache-storage-service;1"] + .getService(Components.interfaces.nsICacheStorageService); + do_check_true(serviceA); + var serviceB = Components.classes["@mozilla.org/network/cache-storage-service;1"] + .getService(Components.interfaces.nsICacheStorageService); + do_check_true(serviceB); + + do_check_eq(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..dd8c340871 --- /dev/null +++ b/netwerk/test/unit/test_cache2-01-basic.js @@ -0,0 +1,28 @@ +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..bf1d313171 --- /dev/null +++ b/netwerk/test/unit/test_cache2-01a-basic-readonly.js @@ -0,0 +1,28 @@ +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..e46d6ab5f0 --- /dev/null +++ b/netwerk/test/unit/test_cache2-01b-basic-datasize.js @@ -0,0 +1,32 @@ +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 + do_check_eq(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 + do_check_eq(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 + do_check_eq(entry.dataSize, 3); + asyncOpenCacheEntry("http://a/", "disk", Ci.nsICacheStorage.OPEN_NORMALLY, null, + new OpenCallback(NORMAL, "a2m", "a2d", function(entry) { + do_check_eq(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..0467656f09 --- /dev/null +++ b/netwerk/test/unit/test_cache2-01c-basic-hasmeta-only.js @@ -0,0 +1,28 @@ +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..b07831d7b0 --- /dev/null +++ b/netwerk/test/unit/test_cache2-01d-basic-not-wanted.js @@ -0,0 +1,28 @@ +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..156add50e1 --- /dev/null +++ b/netwerk/test/unit/test_cache2-01e-basic-bypass-if-busy.js @@ -0,0 +1,35 @@ +function run_test() +{ + do_get_profile(); + + if (!newCacheBackEndUsed()) { + do_check_true(true, "This test doesn't run when the old cache back end is used since the behavior is different"); + return; + } + + // 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) { + do_check_false(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 + do_execute_soon(function() { + do_check_true(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..d1ef54b5fc --- /dev/null +++ b/netwerk/test/unit/test_cache2-01f-basic-openTruncate.js @@ -0,0 +1,24 @@ +function run_test() +{ + do_get_profile(); + + if (!newCacheBackEndUsed()) { + do_check_true(true, "This test doesn't run when the old cache back end is used since the behavior is different"); + return; + } + + var storage = getCacheStorage("disk"); + var entry = storage.openTruncate(createURI("http://new1/"), ""); + do_check_true(!!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..584ff7dfa7 --- /dev/null +++ b/netwerk/test/unit/test_cache2-02-open-non-existing.js @@ -0,0 +1,28 @@ +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..79c367410d --- /dev/null +++ b/netwerk/test/unit/test_cache2-03-oncacheentryavail-throws.js @@ -0,0 +1,24 @@ +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..f4e59bd360 --- /dev/null +++ b/netwerk/test/unit/test_cache2-04-oncacheentryavail-throws2x.js @@ -0,0 +1,28 @@ +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..5501622710 --- /dev/null +++ b/netwerk/test/unit/test_cache2-05-visit.js @@ -0,0 +1,78 @@ +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. + do_execute_soon(function() { + syncWithCacheIOThread(function() { + + var expectedConsumption = newCacheBackEndUsed() + ? 4096 + : 48; + + 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 + ); + }); + }); + }, !newCacheBackEndUsed()); + + 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..616499df99 --- /dev/null +++ b/netwerk/test/unit/test_cache2-06-pb-mode.js @@ -0,0 +1,41 @@ +function exitPB() +{ + var obsvc = Cc["@mozilla.org/observer-service;1"]. + getService(Ci.nsIObserverService); + obsvc.notifyObservers(null, "last-pb-context-exited", null); +} + +function run_test() +{ + do_get_profile(); + + // Store PB entry + asyncOpenCacheEntry("http://p1/", "disk", Ci.nsICacheStorage.OPEN_NORMALLY, LoadContextInfo.private, + new OpenCallback(NEW, "p1m", "p1d", function(entry) { + asyncOpenCacheEntry("http://p1/", "disk", Ci.nsICacheStorage.OPEN_NORMALLY, LoadContextInfo.private, + new OpenCallback(NORMAL, "p1m", "p1d", function(entry) { + // Check it's there + syncWithCacheIOThread(function() { + var storage = getCacheStorage("disk", 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..03b5aba80a --- /dev/null +++ b/netwerk/test/unit/test_cache2-07-visit-memory.js @@ -0,0 +1,82 @@ +function run_test() +{ + do_get_profile(); + + if (!newCacheBackEndUsed()) { + do_check_true(true, "This test doesn't run when the old cache back end is used since the behavior is different"); + return; + } + + // 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..1018a4cbaa --- /dev/null +++ b/netwerk/test/unit/test_cache2-07a-open-memory.js @@ -0,0 +1,53 @@ +function run_test() +{ + do_get_profile(); + + if (!newCacheBackEndUsed()) { + do_check_true(true, "This test doesn't run when the old cache back end is used since the behavior is different"); + return; + } + + // 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) { + do_check_false(entryM1.persistent); + asyncOpenCacheEntry("http://mem-first/", "disk", Ci.nsICacheStorage.OPEN_NORMALLY, null, + new OpenCallback(NORMAL, "mem1-meta", "mem1-data", function(entryM2) { + do_check_false(entryM1.persistent); + do_check_false(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) { + do_check_true(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) { + do_check_true(entryD1.persistent); + do_check_false(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) { + do_check_true(entryD1.persistent); + do_check_false(entryD2.persistent); + do_check_false(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..246cb789c1 --- /dev/null +++ b/netwerk/test/unit/test_cache2-08-evict-disk-by-memory-storage.js @@ -0,0 +1,18 @@ +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..24c736fe20 --- /dev/null +++ b/netwerk/test/unit/test_cache2-09-evict-disk-by-uri.js @@ -0,0 +1,21 @@ +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..edeeee4167 --- /dev/null +++ b/netwerk/test/unit/test_cache2-10-evict-direct.js @@ -0,0 +1,20 @@ +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..0f9048db1b --- /dev/null +++ b/netwerk/test/unit/test_cache2-10b-evict-direct-immediate.js @@ -0,0 +1,21 @@ +function run_test() +{ + do_get_profile(); + + if (!newCacheBackEndUsed()) { + do_check_true(true, "This test doesn't run when the old cache back end is used since the behavior is different"); + return; + } + + 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..90eced7de1 --- /dev/null +++ b/netwerk/test/unit/test_cache2-11-evict-memory.js @@ -0,0 +1,61 @@ +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 = newCacheBackEndUsed() + ? 2048 + : 24; + + storage.asyncVisitStorage( + new VisitCallback(2, expectedConsumption, ["http://a/", "http://b/"], function() { + finish_cache2_test(); + }), + true + ); + }), + true + ); + }) + ); + }, !newCacheBackEndUsed()); + + 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..0ce7ec0844 --- /dev/null +++ b/netwerk/test/unit/test_cache2-12-evict-disk.js @@ -0,0 +1,61 @@ +function run_test() +{ + do_get_profile(); + + if (!newCacheBackEndUsed()) { + do_check_true(true, "This test doesn't run when the old cache back end is used since the behavior is different"); + return; + } + + 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 + ); + }) + ); + }, !newCacheBackEndUsed()); + + 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..1aa1072953 --- /dev/null +++ b/netwerk/test/unit/test_cache2-13-evict-non-existing.js @@ -0,0 +1,13 @@ +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..7355a2a9e8 --- /dev/null +++ b/netwerk/test/unit/test_cache2-14-concurent-readers.js @@ -0,0 +1,31 @@ +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..c5ceb99a09 --- /dev/null +++ b/netwerk/test/unit/test_cache2-14b-concurent-readers-complete.js @@ -0,0 +1,48 @@ +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; + do_check_eq(order, newCacheBackEndUsed() ? 3 : 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; + do_check_eq(order, newCacheBackEndUsed() ? 1 : 2); + } else { + mc.fired(); + } + }) + ); + asyncOpenCacheEntry("http://x/", "disk", Ci.nsICacheStorage.OPEN_NORMALLY, null, + new OpenCallback(NORMAL|NOTIFYBEFOREREAD, "x1m", "x1d", function(entry, beforeReading) { + if (beforeReading) { + ++order; + do_check_eq(order, newCacheBackEndUsed() ? 2 : 3); + } 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..9375cdbe8d --- /dev/null +++ b/netwerk/test/unit/test_cache2-15-conditional-304.js @@ -0,0 +1,39 @@ +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 + do_execute_soon(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..601cca97e9 --- /dev/null +++ b/netwerk/test/unit/test_cache2-16-conditional-200.js @@ -0,0 +1,52 @@ +function run_test() +{ + do_get_profile(); + + if (!newCacheBackEndUsed()) { + do_check_true(true, "This test doesn't run when the old cache back end is used since the behavior is different"); + return; + } + + // 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) + do_execute_soon(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..3033e42d6b --- /dev/null +++ b/netwerk/test/unit/test_cache2-17-evict-all.js @@ -0,0 +1,17 @@ +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..c83a78c7af --- /dev/null +++ b/netwerk/test/unit/test_cache2-18-not-valid.js @@ -0,0 +1,30 @@ +function run_test() +{ + do_get_profile(); + + if (!newCacheBackEndUsed()) { + do_check_true(true, "This test doesn't run when the old cache back end is used since the behavior is different"); + return; + } + + // 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..ceb782d4e7 --- /dev/null +++ b/netwerk/test/unit/test_cache2-19-range-206.js @@ -0,0 +1,44 @@ +function run_test() +{ + do_get_profile(); + + if (!newCacheBackEndUsed()) { + do_check_true(true, "This test doesn't run when the old cache back end is used since the behavior is different"); + return; + } + + // 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..349dd343ee --- /dev/null +++ b/netwerk/test/unit/test_cache2-20-range-200.js @@ -0,0 +1,45 @@ +function run_test() +{ + do_get_profile(); + + if (!newCacheBackEndUsed()) { + do_check_true(true, "This test doesn't run when the old cache back end is used since the behavior is different"); + return; + } + + // 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..6f325077d8 --- /dev/null +++ b/netwerk/test/unit/test_cache2-21-anon-storage.js @@ -0,0 +1,38 @@ +Components.utils.import('resource://gre/modules/LoadContextInfo.jsm'); + +function run_test() +{ + do_get_profile(); + + if (!newCacheBackEndUsed()) { + do_check_true(true, "This test doesn't run when the old cache back end is used since the behavior is different"); + return; + } + + // Create and check an entry anon disk storage + asyncOpenCacheEntry("http://anon1/", "disk", Ci.nsICacheStorage.OPEN_NORMALLY, LoadContextInfo.anonymous, + new OpenCallback(NEW, "an1", "an1", function(entry) { + asyncOpenCacheEntry("http://anon1/", "disk", Ci.nsICacheStorage.OPEN_NORMALLY, 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, LoadContextInfo.default, + new OpenCallback(NEW, "na1", "na1", function(entry) { + asyncOpenCacheEntry("http://anon1/", "disk", Ci.nsICacheStorage.OPEN_NORMALLY, 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, 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..bc8b15822b --- /dev/null +++ b/netwerk/test/unit/test_cache2-22-anon-visit.js @@ -0,0 +1,58 @@ +Components.utils.import('resource://gre/modules/LoadContextInfo.jsm'); + +function run_test() +{ + do_get_profile(); + + function checkNewBackEnd() + { + var storage = getCacheStorage("disk", LoadContextInfo.default); + storage.asyncVisitStorage( + new VisitCallback(1, 1024, ["http://an2/"], function() { + storage = getCacheStorage("disk", LoadContextInfo.anonymous); + storage.asyncVisitStorage( + new VisitCallback(1, 1024, ["http://an2/"], function() { + finish_cache2_test(); + }), + true + ); + }), + true + ); + } + + function checkOldBackEnd() + { + syncWithCacheIOThread(function() { + var storage = getCacheStorage("disk", LoadContextInfo.default); + storage.asyncVisitStorage( + new VisitCallback(2, 24, ["http://an2/"], function() { + storage = getCacheStorage("disk", LoadContextInfo.anonymous); + storage.asyncVisitStorage( + new VisitCallback(0, 0, ["http://an2/"], function() { + finish_cache2_test(); + }), + true + ); + }), + true + ); + }); + } + + var mc = new MultipleCallbacks(2, newCacheBackEndUsed() ? checkNewBackEnd : checkOldBackEnd, !newCacheBackEndUsed()); + + asyncOpenCacheEntry("http://an2/", "disk", Ci.nsICacheStorage.OPEN_NORMALLY, LoadContextInfo.default, + new OpenCallback(NEW|WAITFORWRITE, "an2", "an2", function(entry) { + mc.fired(); + }) + ); + + asyncOpenCacheEntry("http://an2/", "disk", Ci.nsICacheStorage.OPEN_NORMALLY, 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..92959645dd --- /dev/null +++ b/netwerk/test/unit/test_cache2-23-read-over-chunk.js @@ -0,0 +1,35 @@ +Components.utils.import('resource://gre/modules/LoadContextInfo.jsm'); + +function run_test() +{ + do_get_profile(); + + if (!newCacheBackEndUsed()) { + do_check_true(true, "This test checks only cache2 specific behavior."); + return; + } + + 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, LoadContextInfo.default, + new OpenCallback(NEW|WAITFORWRITE, "", payload, function(entry) { + var is = entry.openInputStream(0); + pumpReadStream(is, function(read) { + do_check_eq(read.length, kChunkSize + 10); + is.close(); + do_check_true(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..fee8a2ee75 --- /dev/null +++ b/netwerk/test/unit/test_cache2-24-exists.js @@ -0,0 +1,38 @@ +Components.utils.import('resource://gre/modules/LoadContextInfo.jsm'); + +function run_test() +{ + do_get_profile(); + + if (!newCacheBackEndUsed()) { + do_check_true(true, "This test checks only cache2 specific behavior."); + return; + } + + var mc = new MultipleCallbacks(2, function() { + var mem = getCacheStorage("memory"); + var disk = getCacheStorage("disk"); + + do_check_true(disk.exists(createURI("http://m1/"), "")); + do_check_true(mem.exists(createURI("http://m1/"), "")); + do_check_false(mem.exists(createURI("http://m2/"), "")); + do_check_true(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, LoadContextInfo.default, + new OpenCallback(NEW | WAITFORWRITE, "meta", "data", function(entry) { + mc.fired(); + }) + ); + + asyncOpenCacheEntry("http://m1/", "memory", Ci.nsICacheStorage.OPEN_NORMALLY, 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..0999dc8d2f --- /dev/null +++ b/netwerk/test/unit/test_cache2-25-chunk-memory-limit.js @@ -0,0 +1,51 @@ +Components.utils.import('resource://gre/modules/LoadContextInfo.jsm'); + +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(); + + if (!newCacheBackEndUsed()) { + do_check_true(true, "This test doesn't run when the old cache back end is used since the behavior is different"); + return; + } + + 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) { + do_check_eq(status, Cr.NS_OK); + oStr = entry.openOutputStream(0); + var data = gen_200k(); + do_check_eq(data.length, oStr.write(data, data.length)); + + asyncOpenCacheEntry("http://b/", "disk", Ci.nsICacheStorage.OPEN_NORMALLY, null, + function(status, entry) { + do_check_eq(status, Cr.NS_OK); + var oStr2 = entry.openOutputStream(0); + 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..f54bc19a5c --- /dev/null +++ b/netwerk/test/unit/test_cache2-26-no-outputstream-open.js @@ -0,0 +1,27 @@ +function run_test() +{ + do_get_profile(); + + if (!newCacheBackEndUsed()) { + do_check_true(true, "This test doesn't run when the old cache back end is used since the behavior is different"); + return; + } + + // 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 + do_execute_soon(() => { + 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..c3663751df --- /dev/null +++ b/netwerk/test/unit/test_cache2-27-force-valid-for.js @@ -0,0 +1,37 @@ +Components.utils.import('resource://gre/modules/LoadContextInfo.jsm'); + +function run_test() +{ + do_get_profile(); + + if (!newCacheBackEndUsed()) { + do_check_true(true, "This test checks only cache2 specific behavior."); + return; + } + + var mc = new MultipleCallbacks(2, function() { + finish_cache2_test(); + }); + + asyncOpenCacheEntry("http://m1/", "memory", Ci.nsICacheStorage.OPEN_NORMALLY, 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..b8d93dc44b --- /dev/null +++ b/netwerk/test/unit/test_cache2-28-last-access-attrs.js @@ -0,0 +1,39 @@ +function run_test() +{ + do_get_profile(); + function NowSeconds() { + return parseInt((new Date()).getTime() / 1000); + } + function do_check_time(t, min, max) { + do_check_true(t >= min); + do_check_true(t <= max); + } + + var timeStart = NowSeconds(); + + asyncOpenCacheEntry("http://t/", "disk", Ci.nsICacheStorage.OPEN_NORMALLY, null, + new OpenCallback(NEW, "m", "d", function(entry) { + + var firstOpen = NowSeconds(); + do_check_eq(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(); + do_check_eq(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..fdc66e10fb --- /dev/null +++ b/netwerk/test/unit/test_cache2-28a-OPEN_SECRETLY.js @@ -0,0 +1,34 @@ +function run_test() +{ + do_get_profile(); + function NowSeconds() { + return parseInt((new Date()).getTime() / 1000); + } + function do_check_time(a, b) { + do_check_true(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(); + do_check_eq(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) { + do_check_eq(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..d291b5f665 --- /dev/null +++ b/netwerk/test/unit/test_cache2-29a-concurrent_read_resumable_entry_size_zero.js @@ -0,0 +1,72 @@ +/* + +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 + +*/ + +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + + +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); + + httpServer = new HttpServer(); + httpServer.registerPathHandler("/content", contentHandler); + httpServer.start(-1); + + var chan1 = make_channel(URL + "/content"); + chan1.asyncOpen2(new ChannelListener(firstTimeThrough, null)); + var chan2 = make_channel(URL + "/content"); + chan2.asyncOpen2(new ChannelListener(secondTimeThrough, null)); + + do_test_pending(); +} + +function firstTimeThrough(request, buffer) +{ + do_check_eq(buffer, responseBody); +} + +function secondTimeThrough(request, buffer) +{ + do_check_eq(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..67f6467bb2 --- /dev/null +++ b/netwerk/test/unit/test_cache2-29b-concurrent_read_non-resumable_entry_size_zero.js @@ -0,0 +1,71 @@ +/* + +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 + +*/ + +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +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); + + httpServer = new HttpServer(); + httpServer.registerPathHandler("/content", contentHandler); + httpServer.start(-1); + + var chan1 = make_channel(URL + "/content"); + chan1.asyncOpen2(new ChannelListener(firstTimeThrough, null, CL_ALLOW_UNKNOWN_CL)); + var chan2 = make_channel(URL + "/content"); + chan2.asyncOpen2(new ChannelListener(secondTimeThrough, null, CL_ALLOW_UNKNOWN_CL)); + + do_test_pending(); +} + +function firstTimeThrough(request, buffer) +{ + do_check_eq(buffer, responseBodyDecoded); +} + +function secondTimeThrough(request, buffer) +{ + do_check_eq(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..f82d685e1e --- /dev/null +++ b/netwerk/test/unit/test_cache2-29c-concurrent_read_half-interrupted.js @@ -0,0 +1,91 @@ +/* + +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 + +*/ + +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + + +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 + do_check_true(responseBody.length > 1024); + + do_get_profile(); + + Services.prefs.setIntPref("browser.cache.disk.max_entry_size", 1); + + httpServer = new HttpServer(); + httpServer.registerPathHandler("/content", contentHandler); + httpServer.start(-1); + + var chan1 = make_channel(URL + "/content"); + chan1.asyncOpen2(new ChannelListener(firstTimeThrough, null)); + var chan2 = make_channel(URL + "/content"); + chan2.asyncOpen2(new ChannelListener(secondTimeThrough, null)); + + do_test_pending(); +} + +function firstTimeThrough(request, buffer) +{ + do_check_eq(buffer, responseBody); +} + +function secondTimeThrough(request, buffer) +{ + do_check_eq(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..a5461d8545 --- /dev/null +++ b/netwerk/test/unit/test_cache2-29d-concurrent_read_half-corrupted-206.js @@ -0,0 +1,95 @@ +/* + +Checkes if the concurrent cache read/write works when the write is interrupted because of max-entry-size limits. +This is enhancement of 29c test, this test checks that a corrupted 206 response is correctly handled (no crashes or asserion failures) +This test is using a resumable response. +- with a profile, set max-entry-size to 1 (=1024 bytes) +- first channel makes a request for a resumable response +- second channel makes a request for the same resource, concurrent read happens +- first channel sets predicted data size on the entry with every chunk, it's doomed on 1024 +- second channel now must engage interrupted concurrent write algorithm and read the rest of the content from the network +- the response to the range request is broken (bad Content-Range header) +- the first must deliver full content w/o errors +- the second channel must correctly fail + +*/ + +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + + +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 + do_check_true(responseBody.length > 1024); + + do_get_profile(); + + if (!newCacheBackEndUsed()) { + do_check_true(true, "This test doesn't run when the old cache back end is used since the behavior is different"); + return; + } + + Services.prefs.setIntPref("browser.cache.disk.max_entry_size", 1); + + httpServer = new HttpServer(); + httpServer.registerPathHandler("/content", contentHandler); + httpServer.start(-1); + + var chan1 = make_channel(URL + "/content"); + chan1.asyncOpen2(new ChannelListener(firstTimeThrough, null)); + var chan2 = make_channel(URL + "/content"); + chan2.asyncOpen2(new ChannelListener(secondTimeThrough, null, CL_EXPECT_FAILURE)); + + do_test_pending(); +} + +function firstTimeThrough(request, buffer) +{ + do_check_eq(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..dc0190eca1 --- /dev/null +++ b/netwerk/test/unit/test_cache2-29e-concurrent_read_half-non-206-response.js @@ -0,0 +1,90 @@ +/* + +Checkes if the concurrent cache read/write works when the write is interrupted because of max-entry-size limits. +This is enhancement of 29c test, this test checks that a corrupted 206 response is correctly handled (no crashes or asserion failures) +This test is using a resumable response. +- with a profile, set max-entry-size to 1 (=1024 bytes) +- first channel makes a request for a resumable response +- second channel makes a request for the same resource, concurrent read happens +- first channel sets predicted data size on the entry with every chunk, it's doomed on 1024 +- second channel now must engage interrupted concurrent write algorithm and read the rest of the content from the network +- the response to the range request is plain 200 +- the first must deliver full content w/o errors +- the second channel must correctly fail + +*/ + +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + + +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 + do_check_true(responseBody.length > 1024); + + do_get_profile(); + + if (!newCacheBackEndUsed()) { + do_check_true(true, "This test doesn't run when the old cache back end is used since the behavior is different"); + return; + } + + Services.prefs.setIntPref("browser.cache.disk.max_entry_size", 1); + + httpServer = new HttpServer(); + httpServer.registerPathHandler("/content", contentHandler); + httpServer.start(-1); + + var chan1 = make_channel(URL + "/content"); + chan1.asyncOpen2(new ChannelListener(firstTimeThrough, null)); + var chan2 = make_channel(URL + "/content"); + chan2.asyncOpen2(new ChannelListener(secondTimeThrough, null, CL_EXPECT_FAILURE)); + + do_test_pending(); +} + +function firstTimeThrough(request, buffer) +{ + do_check_eq(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..3a5db421aa --- /dev/null +++ b/netwerk/test/unit/test_cache2-30a-entry-pinning.js @@ -0,0 +1,32 @@ +function run_test() +{ + do_get_profile(); + if (!newCacheBackEndUsed()) { + do_check_true(true, "This test checks only cache2 specific behavior."); + return; + } + + // Open for write, write + asyncOpenCacheEntry("http://a/", "pin", Ci.nsICacheStorage.OPEN_TRUNCATE, LoadContextInfo.default, + new OpenCallback(NEW|WAITFORWRITE, "a1m", "a1d", function(entry) { + // Open for read and check + asyncOpenCacheEntry("http://a/", "disk", Ci.nsICacheStorage.OPEN_NORMALLY, 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, 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..a9410ff757 --- /dev/null +++ b/netwerk/test/unit/test_cache2-30b-pinning-storage-clear.js @@ -0,0 +1,38 @@ +function run_test() +{ + do_get_profile(); + if (!newCacheBackEndUsed()) { + do_check_true(true, "This test checks only cache2 specific behavior."); + return; + } + var lci = 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..91c621ce59 --- /dev/null +++ b/netwerk/test/unit/test_cache2-30c-pinning-deferred-doom.js @@ -0,0 +1,134 @@ +/* + +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.) + +*/ + +const kENTRYCOUNT = 10; + +function log_(msg) { if (true) dump(">>>>>>>>>>>>> " + msg + "\n"); } + +function run_test() +{ + do_get_profile(); + if (!newCacheBackEndUsed()) { + do_check_true(true, "This test checks only cache2 specific behavior."); + return; + } + + var lci = LoadContextInfo.default; + var testingInterface = get_cache_service().QueryInterface(Ci.nsICacheTesting); + do_check_true(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(); + do_execute_soon(() => { + 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: function(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", false); + + + 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..07105d535f --- /dev/null +++ b/netwerk/test/unit/test_cache2-30d-pinning-WasEvicted-API.js @@ -0,0 +1,113 @@ +/* + +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. + +*/ + +const kENTRYCOUNT = 10; + +function log_(msg) { if (true) dump(">>>>>>>>>>>>> " + msg + "\n"); } + +function run_test() +{ + do_get_profile(); + if (!newCacheBackEndUsed()) { + do_check_true(true, "This test checks only cache2 specific behavior."); + return; + } + var lci = LoadContextInfo.default; + var testingInterface = get_cache_service().QueryInterface(Ci.nsICacheTesting); + do_check_true(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(); + do_execute_soon(() => { + 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: function(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", false); + + + 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..8a49242eee --- /dev/null +++ b/netwerk/test/unit/test_cacheForOfflineUse_no-store.js @@ -0,0 +1,93 @@ +"use strict"; +// https://bugzilla.mozilla.org/show_bug.cgi?id=760955 + +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +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 = Components.classes["@mozilla.org/network/application-cache-service;1"]. + getService(Components.interfaces.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, null, null); +} + +const responseBody = "response body"; + +// A HTTP channel for updating the offline cache should normally succeed. +function normalHandler(metadata, response) +{ + do_print("normalHandler"); + response.setHeader("Content-Type", "text/plain"); + response.bodyOutputStream.write(responseBody, responseBody.length); +} +function checkNormal(request, buffer) +{ + do_check_eq(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.asyncOpen2(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) +{ + do_print("noStoreHandler"); + response.setHeader("Content-Type", "text/plain"); + response.setHeader("Cache-Control", "no-store"); + response.bodyOutputStream.write(responseBody, responseBody.length); +} +function checkNoStore(request, buffer) +{ + do_check_eq(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.asyncOpen2(new ChannelListener(checkNoStore, chan, CL_EXPECT_FAILURE)); +}); + +function run_test() +{ + do_get_profile(); + + 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_jar.js b/netwerk/test/unit/test_cache_jar.js new file mode 100644 index 0000000000..126e811f86 --- /dev/null +++ b/netwerk/test/unit/test_cache_jar.js @@ -0,0 +1,126 @@ +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/NetUtil.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +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, appId, inIsolatedMozBrowser, userContextId) { + var chan = NetUtil.newChannel({uri: url, loadUsingSystemPrincipal: true}) + .QueryInterface(Ci.nsIHttpChannel); + chan.loadInfo.originAttributes = { appId: appId, + inIsolatedMozBrowser: inIsolatedMozBrowser, + userContextId: userContextId, + }; + return chan; +} + +// [appId, inIsolatedMozBrowser, userContextId, expected_handlers_called] +var firstTests = [ + [0, false, 0, 1], [0, true, 0, 1], [1, false, 0, 1], [1, true, 0, 1], + [0, false, 1, 1], [0, true, 1, 1], [1, false, 1, 1], [1, true, 1, 1] +]; +var secondTests = [ + [0, false, 0, 0], [0, true, 0, 0], [1, false, 0, 0], [1, true, 0, 1], + [0, false, 1, 0], [0, true, 1, 0], [1, false, 1, 0], [1, true, 1, 0] +]; +var thirdTests = [ + [0, false, 0, 0], [0, true, 0, 0], [1, false, 0, 1], [1, true, 0, 1], + [0, false, 1, 0], [0, true, 1, 0], [1, false, 1, 0], [1, true, 1, 0] +]; +var fourthTests = [ + [0, false, 0, 0], [0, true, 0, 0], [1, false, 0, 0], [1, true, 0, 0], + [0, false, 1, 1], [0, true, 1, 0], [1, false, 1, 0], [1, true, 1, 0] +]; + +function run_all_tests() { + for (let test of firstTests) { + handlers_called = 0; + var chan = makeChan(URL, test[0], test[1], test[2]); + chan.asyncOpen2(new ChannelListener(doneFirstLoad, test[3])); + yield undefined; + } + + // 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; + + let attrs_inBrowser = JSON.stringify({ appId:1, inIsolatedMozBrowser:true }); + let attrs_notInBrowser = JSON.stringify({ appId:1 }); + + Services.obs.notifyObservers(null, "clear-origin-attributes-data", attrs_inBrowser); + + for (let test of secondTests) { + handlers_called = 0; + var chan = makeChan(URL, test[0], test[1], test[2]); + chan.asyncOpen2(new ChannelListener(doneFirstLoad, test[3])); + yield undefined; + } + + Services.obs.notifyObservers(null, "clear-origin-attributes-data", attrs_notInBrowser); + Services.obs.notifyObservers(null, "clear-origin-attributes-data", attrs_inBrowser); + + for (let test of thirdTests) { + handlers_called = 0; + var chan = makeChan(URL, test[0], test[1], test[2]); + chan.asyncOpen2(new ChannelListener(doneFirstLoad, test[3])); + yield undefined; + } + + let attrs_userContextId = JSON.stringify({ userContextId: 1 }); + Services.obs.notifyObservers(null, "clear-origin-attributes-data", attrs_userContextId); + + for (let test of fourthTests) { + handlers_called = 0; + var chan = makeChan(URL, test[0], test[1], test[2]); + chan.asyncOpen2(new ChannelListener(doneFirstLoad, test[3])); + yield undefined; + } +} + +var gTests; +function run_test() { + do_get_profile(); + if (!newCacheBackEndUsed()) { + do_check_true(true, "This test checks only cache2 specific behavior."); + return; + } + do_test_pending(); + httpserv = new HttpServer(); + httpserv.registerPathHandler("/cached", cached_handler); + httpserv.start(-1); + gTests = run_all_tests(); + gTests.next(); +} + +function doneFirstLoad(req, buffer, expected) { + // Load it again, make sure it hits the cache + var oa = req.loadInfo.originAttributes; + var chan = makeChan(URL, oa.appId, oa.isInIsolatedMozBrowserElement, oa.userContextId); + chan.asyncOpen2(new ChannelListener(doneSecondLoad, expected)); +} + +function doneSecondLoad(req, buffer, expected) { + do_check_eq(handlers_called, expected); + try { + gTests.next(); + } catch (x) { + do_test_finished(); + } +} diff --git a/netwerk/test/unit/test_cacheflags.js b/netwerk/test/unit/test_cacheflags.js new file mode 100644 index 0000000000..28c24b14c4 --- /dev/null +++ b/netwerk/test/unit/test_cacheflags.js @@ -0,0 +1,370 @@ +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); +Cu.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; + +// We attach this to channel when we want to test Private Browsing mode +function LoadContext(usePrivateBrowsing) { + this.usePrivateBrowsing = usePrivateBrowsing; + this.originAttributes.privateBrowsingId = usePrivateBrowsing ? 1 : 0; +} + +LoadContext.prototype = { + originAttributes: { + privateBrowsingId : 0 + }, + usePrivateBrowsing: false, + // don't bother defining rest of nsILoadContext fields: don't need 'em + + QueryInterface: function(iid) { + if (iid.equals(Ci.nsILoadContext)) + return this; + throw Cr.NS_ERROR_NO_INTERFACE; + }, + + getInterface: function(iid) { + if (iid.equals(Ci.nsILoadContext)) + return this; + throw Cr.NS_ERROR_NO_INTERFACE; + }, +}; + +var PrivateBrowsingLoadContext = new LoadContext(true); + +function make_channel(url, flags, usePrivateBrowsing) { + var securityFlags = Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL; + + var uri = Services.io.newURI(url, null, null); + var principal = Services.scriptSecurityManager.createCodebasePrincipal(uri, + { privateBrowsingId : usePrivateBrowsing ? 1 : 0 }); + + var req = NetUtil.newChannel({uri: uri, + loadingPrincipal: principal, + securityFlags: 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: function(iid) { + if (iid.equals(Components.interfaces.nsIStreamListener) || + iid.equals(Components.interfaces.nsIRequestObserver) || + iid.equals(Components.interfaces.nsISupports)) + return this; + throw Components.results.NS_ERROR_NO_INTERFACE; + }, + + onStartRequest: function(request, context) { + var cachingChannel = request.QueryInterface(Ci.nsICacheInfoChannel); + this._isFromCache = request.isPending() && cachingChannel.isFromCache(); + }, + + onDataAvailable: function(request, context, stream, offset, count) { + this._buffer = this._buffer.concat(read_stream(stream, count)); + }, + + onStopRequest: function(request, context, status) { + do_check_eq(Components.isSuccessCode(status), this.expectSuccess); + do_check_eq(this._isFromCache, this.readFromCache); + do_check_eq(gHitServer, this.hitServer); + + do_timeout(0, run_next_test); + }, + + run: function() { + 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.asyncOpen2(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_channel_close.js b/netwerk/test/unit/test_channel_close.js new file mode 100644 index 0000000000..a7a90e04fd --- /dev/null +++ b/netwerk/test/unit/test_channel_close.js @@ -0,0 +1,59 @@ +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +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); + + var local_channel; + + // Opened channel that has no remaining references on shutdown + local_channel = setupChannel(testpath); + local_channel.asyncOpen2(new ChannelListener(checkRequest, local_channel)); + + // Opened channel that has no remaining references after being opened + setupChannel(testpath).asyncOpen2(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].asyncOpen2(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) { + do_check_eq(data, httpbody); +} + +function checkRequestFinish(request, data, context) { + checkRequest(request, data, context); + httpserver.stop(do_test_finished); +} diff --git a/netwerk/test/unit/test_chunked_responses.js b/netwerk/test/unit/test_chunked_responses.js new file mode 100644 index 0000000000..396e266144 --- /dev/null +++ b/netwerk/test/unit/test_chunked_responses.js @@ -0,0 +1,175 @@ +/* + * Test Chunked-Encoded response parsing. + */ + +//////////////////////////////////////////////////////////////////////////////// +// Test infrastructure + +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +XPCOMUtils.defineLazyGetter(this, "URL", function() { + return "http://localhost:" + httpserver.identity.primaryPort; +}); + +var httpserver = new HttpServer(); +var index = 0; +var test_flags = new Array(); +var testPathBase = "/chunked_hdrs"; + +function run_test() +{ + httpserver.start(-1); + + do_test_pending(); + run_test_number(1); +} + +function run_test_number(num) +{ + testPath = testPathBase + num; + httpserver.registerPathHandler(testPath, eval("handler" + num)); + + var channel = setupChannel(testPath); + flags = test_flags[num]; // OK if flags undefined for test + channel.asyncOpen2(new ChannelListener(eval("completeTest" + num), + channel, flags)); +} + +function setupChannel(url) +{ + var chan = NetUtil.newChannel({ + uri: URL + url, + loadUsingSystemPrincipal: true + }); + var httpChan = chan.QueryInterface(Components.interfaces.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) +{ + do_check_eq(request.status, Components.results.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) +{ + do_check_eq(request.status, Components.results.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) +{ + do_check_eq(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) +{ + do_check_eq(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) +{ + do_check_eq(request.status, Components.results.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..8e68fc6a4c --- /dev/null +++ b/netwerk/test/unit/test_compareURIs.js @@ -0,0 +1,49 @@ +Components.utils.import("resource://gre/modules/NetUtil.jsm"); + +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 = Components.classes["@mozilla.org/scriptsecuritymanager;1"].getService(Components.interfaces.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); + equal = true; + } catch (e) { + equal = false + } + do_check_eq(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..275c944330 --- /dev/null +++ b/netwerk/test/unit/test_compressappend.js @@ -0,0 +1,80 @@ +// +// Test that data can be appended to a cache entry even when the data is +// compressed by the cache compression feature - bug 648429. +// + +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: function() { + evict_cache_entries(); + asyncOpenCacheEntry("http://data/", + "disk", Ci.nsICacheStorage.OPEN_NORMALLY, null, + this.writeData.bind(this)); + }, + + writeData: function(status, entry) { + do_check_eq(status, Cr.NS_OK); + if (this._compress) + entry.setMetaDataElement("uncompressed-len", "0"); + var os = entry.openOutputStream(0); + write_and_check(os, "12345", 5); + os.close(); + entry.close(); + asyncOpenCacheEntry("http://data/", + "disk", Ci.nsICacheStorage.OPEN_NORMALLY, null, + this.appendData.bind(this)); + }, + + appendData: function(status, entry) { + do_check_eq(status, Cr.NS_OK); + var os = entry.openOutputStream(entry.storageDataSize); + write_and_check(os, "abcde", 5); + os.close(); + entry.close(); + + asyncOpenCacheEntry("http://data/", + "disk", Ci.nsICacheStorage.OPEN_READONLY, null, + this.checkData.bind(this)); + }, + + checkData: function(status, entry) { + do_check_eq(status, Cr.NS_OK); + var self = this; + pumpReadStream(entry.openInputStream(0), function(str) { + do_check_eq(str.length, 10); + do_check_eq(str, "12345abcde"); + entry.close(); + + do_execute_soon(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..165080b434 --- /dev/null +++ b/netwerk/test/unit/test_content_encoding_gzip.js @@ -0,0 +1,114 @@ +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +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.asyncOpen2(new ChannelListener(completeIter, channel, tests[index].flags)); +} + +function completeIter(request, data, ctx) { + if (!(tests[index].flags & CL_EXPECT_FAILURE)) { + do_check_eq(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 = Components.classes["@mozilla.org/binaryoutputstream;1"] + .createInstance(Components.interfaces.nsIBinaryOutputStream); + bos.setOutputStream(response.bodyOutputStream); + + response.processAsync(); + bos.writeByteArray(tests[index].body, tests[index].body.length); + 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..f9891a7d3d --- /dev/null +++ b/netwerk/test/unit/test_content_length_underrun.js @@ -0,0 +1,278 @@ +/* + * Test Content-Length underrun behavior + */ + +//////////////////////////////////////////////////////////////////////////////// +// Test infrastructure + +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +XPCOMUtils.defineLazyGetter(this, "URL", function() { + return "http://localhost:" + httpserver.identity.primaryPort; +}); + +var httpserver = new HttpServer(); +var index = 0; +var test_flags = new Array(); +var testPathBase = "/cl_hdrs"; + +var prefs; +var enforcePrefStrict; +var enforcePrefSoft; + +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"); + prefs.setBoolPref("network.http.enforce-framing.http1", true); + + httpserver.start(-1); + + do_test_pending(); + run_test_number(1); +} + +function run_test_number(num) +{ + testPath = testPathBase + num; + httpserver.registerPathHandler(testPath, eval("handler" + num)); + + var channel = setupChannel(testPath); + flags = test_flags[num]; // OK if flags undefined for test + channel.asyncOpen2(new ChannelListener(eval("completeTest" + num), + channel, flags)); +} + +function run_gzip_test(num) +{ + testPath = testPathBase + num; + httpserver.registerPathHandler(testPath, eval("handler" + num)); + + var channel = setupChannel(testPath); + + function StreamListener() {} + + StreamListener.prototype = { + QueryInterface: function(aIID) { + if (aIID.equals(Components.interfaces.nsIStreamListener) || + aIID.equals(Components.interfaces.nsIRequestObserver) || + aIID.equals(Components.interfaces.nsISupports)) + return this; + throw Components.results.NS_ERROR_NO_INTERFACE; + }, + + onStartRequest: function(aRequest, aContext) {}, + + onStopRequest: function(aRequest, aContext, aStatusCode) { + // Make sure we catch the error NS_ERROR_NET_PARTIAL_TRANSFER here. + do_check_eq(aStatusCode, Components.results.NS_ERROR_NET_PARTIAL_TRANSFER); + // do_test_finished(); + endTests(); + }, + + onDataAvailable: function(request, context, stream, offset, count) {} + }; + + let listener = new StreamListener(); + + channel.asyncOpen2(listener); + +} + +function setupChannel(url) +{ + var chan = NetUtil.newChannel({ + uri: URL + url, + loadUsingSystemPrincipal: true + }); + var httpChan = chan.QueryInterface(Components.interfaces.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); + 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) +{ + do_check_eq(request.status, Components.results.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) +{ + do_check_eq(request.status, Components.results.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) +{ + do_check_eq(request.status, Components.results.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); + 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) +{ + do_check_eq(request.status, Components.results.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) +{ + do_check_eq(request.status, Components.results.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..c422e0497c --- /dev/null +++ b/netwerk/test/unit/test_content_sniffer.js @@ -0,0 +1,131 @@ +// This file tests nsIContentSniffer, introduced in bug 324985 + +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +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; + +/** + * This object is both a factory and an nsIContentSniffer implementation (so, it + * is de-facto a service) + */ +var sniffer = { + QueryInterface: function sniffer_qi(iid) { + if (iid.equals(Components.interfaces.nsISupports) || + iid.equals(Components.interfaces.nsIFactory) || + iid.equals(Components.interfaces.nsIContentSniffer)) + return this; + throw Components.results.NS_ERROR_NO_INTERFACE; + }, + createInstance: function sniffer_ci(outer, iid) { + if (outer) + throw Components.results.NS_ERROR_NO_AGGREGATION; + return this.QueryInterface(iid); + }, + lockFactory: function sniffer_lockf(lock) { + throw Components.results.NS_ERROR_NOT_IMPLEMENTED; + }, + + getMIMETypeFromContent: function (request, data, length) { + return sniffedType; + } +}; + +var listener = { + onStartRequest: function test_onStartR(request, ctx) { + try { + var chan = request.QueryInterface(Components.interfaces.nsIChannel); + if (chan.contentType == unknownType) + do_throw("Type should not be unknown!"); + 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.results.NS_ERROR_ABORT; + }, + + onDataAvailable: function test_ODA() { + throw Components.results.NS_ERROR_UNEXPECTED; + }, + + onStopRequest: function test_onStopR(request, ctx, 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 |= Components.interfaces.nsIChannel.LOAD_CALL_CONTENT_SNIFFERS; + + return chan; +} + +var httpserv = null; +var urls = null; + +function run_test() { + httpserv = new HttpServer(); + 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 +]; + + Components.manager.nsIComponentRegistrar.registerFactory(snifferCID, + "Unit test content sniffer", snifferContract, sniffer); + + run_test_iteration(1); +} + +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 = Components.classes["@mozilla.org/categorymanager;1"] + .getService(Components.interfaces.nsICategoryManager); + catMan.nsICategoryManager.addCategoryEntry(categoryName, "unit test", + snifferContract, false, true); + } + + var chan = makeChan(urls[index - 1]); + + listener._iteration++; + chan.asyncOpen2(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..d9ef2922aa --- /dev/null +++ b/netwerk/test/unit/test_cookie_blacklist.js @@ -0,0 +1,19 @@ +const GOOD_COOKIE = "GoodCookie=OMNOMNOM"; +const SPACEY_COOKIE = "Spacey Cookie=Major Tom"; + +function run_test() { + var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService); + var cookieURI = ios.newURI("http://mozilla.org/test_cookie_blacklist.js", + null, null); + + var cookieService = Cc["@mozilla.org/cookieService;1"] + .getService(Ci.nsICookieService); + cookieService.setCookieStringFromHttp(cookieURI, cookieURI, null, "BadCookie1=\x01", null, null); + cookieService.setCookieStringFromHttp(cookieURI, cookieURI, null, "BadCookie2=\v", null, null); + cookieService.setCookieStringFromHttp(cookieURI, cookieURI, null, "Bad\x07Name=illegal", null, null); + cookieService.setCookieStringFromHttp(cookieURI, cookieURI, null, GOOD_COOKIE, null, null); + cookieService.setCookieStringFromHttp(cookieURI, cookieURI, null, SPACEY_COOKIE, null, null); + + var storedCookie = cookieService.getCookieString(cookieURI, null); + do_check_eq(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..e3ac76d93c --- /dev/null +++ b/netwerk/test/unit/test_cookie_header.js @@ -0,0 +1,100 @@ +// This file tests bug 250375 + +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +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"); + do_check_eq(chanValue, value); +} + +var cookieVal = "C1=V1"; + +var listener = { + onStartRequest: function test_onStartR(request, ctx) { + try { + var chan = request.QueryInterface(Components.interfaces.nsIHttpChannel); + check_request_header(chan, "Cookie", cookieVal); + } catch (e) { + do_throw("Unexpected exception: " + e); + } + + throw Components.results.NS_ERROR_ABORT; + }, + + onDataAvailable: function test_ODA() { + throw Components.results.NS_ERROR_UNEXPECTED; + }, + + onStopRequest: function test_onStopR(request, ctx, status) { + if (this._iteration == 1) { + 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(Components.interfaces.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); + + httpserv = new HttpServer(); + httpserv.start(-1); + + var chan = makeChan(); + + chan.setRequestHeader("Cookie", cookieVal, false); + + chan.asyncOpen2(listener); + + do_test_pending(); +} + +function run_test_continued() { + var chan = makeChan(); + + var cookServ = Components.classes["@mozilla.org/cookieService;1"] + .getService(Components.interfaces.nsICookieService); + var cookie2 = "C2=V2"; + cookServ.setCookieString(chan.URI, null, cookie2, chan); + 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.asyncOpen2(listener); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_cookiejars.js b/netwerk/test/unit/test_cookiejars.js new file mode 100644 index 0000000000..2e0fae8f4b --- /dev/null +++ b/netwerk/test/unit/test_cookiejars.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/. */ + +/* + * Test that channels with different LoadInfo + * are stored in separate namespaces ("cookie jars") + */ + +XPCOMUtils.defineLazyGetter(this, "URL", function() { + return "http://localhost:" + httpserver.identity.primaryPort; +}); + +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +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.asyncOpen2(new ChannelListener(setNextCookie, null)); +} + +function setNextCookie(request, data, context) +{ + if (++i == tests.length) { + // all cookies set: switch to checking them + i = 0; + checkCookie(); + } else { + do_print("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.asyncOpen2(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; + found = (cookiesSeen.indexOf(cookieToCheck) != -1); + 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. + do_print("Saw only correct cookie '" + expectedCookie + "'"); + do_check_true(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); + + 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..c4e12aff06 --- /dev/null +++ b/netwerk/test/unit/test_cookiejars_safebrowsing.js @@ -0,0 +1,178 @@ +/* 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 reserved AppId (UINT_32_MAX - 1). Setting this + * custom OriginAttributes on the loadInfo of the channel allows us to query the + * AppId and therefore separate the safebrowing 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). + */ + +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + + +XPCOMUtils.defineLazyGetter(this, "URL", function() { + return "http://localhost:" + httpserver.identity.primaryPort; +}); + +XPCOMUtils.defineLazyModuleGetter(this, "SafeBrowsing", + "resource://gre/modules/SafeBrowsing.jsm"); + +var setCookiePath = "/setcookie"; +var checkCookiePath = "/checkcookie"; +var safebrowsingUpdatePath = "/safebrowsingUpdate"; +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 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); + + httpserver = new HttpServer(); + httpserver.registerPathHandler(setCookiePath, cookieSetHandler); + httpserver.registerPathHandler(checkCookiePath, cookieCheckHandler); + httpserver.registerPathHandler(safebrowsingUpdatePath, safebrowsingUpdateHandler); + + 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 dbservice = Cc["@mozilla.org/url-classifier/dbservice;1"] + .getService(Ci.nsIUrlClassifierDBService); + 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_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.asyncOpen2(new ChannelListener(checkNonSafeBrowsingCookie, null)); + } + + function checkNonSafeBrowsingCookie() { + var channel = setupChannel(checkCookiePath, originAttributes); + channel.asyncOpen2(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"); + do_check_eq(cookiesSeen, expectedCookie); + run_next_test(); + } + + setNonSafeBrowsingCookie(); +}); + +add_test(function test_safebrowsing_cookie() { + + var cookieName = 'sbCookie_id4294967294'; + var originAttributes = new OriginAttributes(Ci.nsIScriptSecurityManager.SAFEBROWSING_APP_ID, false, 0); + + function setSafeBrowsingCookie() { + var channel = setupChannel(setCookiePath, originAttributes); + channel.setRequestHeader("set-cookie", cookieName, false); + channel.asyncOpen2(new ChannelListener(checkSafeBrowsingCookie, null)); + } + + function checkSafeBrowsingCookie() { + var channel = setupChannel(checkCookiePath, originAttributes); + channel.asyncOpen2(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 regular cookie with custom loadcontext defined in this test. + var expectedCookies = "sb-update-cookie=1; "; + expectedCookies += cookieName + "=1"; + request.QueryInterface(Ci.nsIHttpChannel); + var cookiesSeen = request.getResponseHeader("saw-cookies"); + + do_check_eq(cookiesSeen, expectedCookies); + httpserver.stop(do_test_finished); + } + + setSafeBrowsingCookie(); +}); diff --git a/netwerk/test/unit/test_data_protocol.js b/netwerk/test/unit/test_data_protocol.js new file mode 100644 index 0000000000..a7f02025de --- /dev/null +++ b/netwerk/test/unit/test_data_protocol.js @@ -0,0 +1,58 @@ +/* run some tests on the data: protocol handler */ +Cu.import("resource://gre/modules/NetUtil.jsm"); + +// The behaviour wrt spaces is: +// - Textual content keeps all spaces +// - Other content strips unescaped spaces +// - Base64 content strips escaped and unescaped spaces +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", ""] +]; + +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] + ">"); + + /* 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(); + } + + var ios = Cc["@mozilla.org/network/io-service;1"]. + getService(Ci.nsIIOService); + 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.asyncOpen2(new ChannelListener(on_read_complete, i)); + } +} + diff --git a/netwerk/test/unit/test_dns_cancel.js b/netwerk/test/unit/test_dns_cancel.js new file mode 100644 index 0000000000..c2bdd04f73 --- /dev/null +++ b/netwerk/test/unit/test_dns_cancel.js @@ -0,0 +1,83 @@ +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: function(inRequest, inRecord, inStatus) { + // One request should be resolved and two request should be canceled. + if (inRequest == requestList1NotCanceled) { + // This request should not be canceled. + do_check_neq(inStatus, Cr.NS_ERROR_ABORT); + + do_test_finished(); + } + }, + QueryInterface: function(aIID) { + if (aIID.equals(Ci.nsIDNSListener) || + aIID.equals(Ci.nsISupports)) { + return this; + } + throw Cr.NS_ERROR_NO_INTERFACE; + } +}; + +var listener2 = { + onLookupComplete: function(inRequest, inRecord, inStatus) { + // One request should be resolved and the other canceled. + if (inRequest == requestList2NotCanceled) { + // The request should not be canceled. + do_check_neq(inStatus, Cr.NS_ERROR_ABORT); + + do_test_finished(); + } + }, + QueryInterface: function(aIID) { + if (aIID.equals(Ci.nsIDNSListener) || + aIID.equals(Ci.nsISupports)) { + return this; + } + throw Cr.NS_ERROR_NO_INTERFACE; + } +}; + +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, flags, listener1, mainThread); + dns.cancelAsyncResolve(hostname2, flags, listener1, Cr.NS_ERROR_ABORT); + + // This one will not be canceled. + requestList1NotCanceled = dns.asyncResolve(hostname1, flags, listener1, mainThread); + + // This one will be canceled with cancel(Cr.NS_ERROR_ABORT). + requestList1Canceled2 = dns.asyncResolve(hostname1, flags, listener1, mainThread); + requestList1Canceled2.cancel(Cr.NS_ERROR_ABORT); + + // This one will not be canceled. + requestList2NotCanceled = dns.asyncResolve(hostname1, flags, listener2, mainThread); + + // This one will be canceled with cancel(Cr.NS_ERROR_ABORT). + requestList2Canceled = dns.asyncResolve(hostname2, flags, listener2, mainThread); + 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..ec334b1f6d --- /dev/null +++ b/netwerk/test/unit/test_dns_disable_ipv4.js @@ -0,0 +1,40 @@ +// +// Tests that calling asyncResolve with the RESOLVE_DISABLE_IPV4 flag doesn't +// return any IPv4 addresses. +// + +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: function(inRequest, inRecord, inStatus) { + if (inStatus != Cr.NS_OK) { + do_check_eq(inStatus, Cr.NS_ERROR_UNKNOWN_HOST); + do_test_finished(); + return; + } + + while (true) { + try { + var answer = inRecord.getNextAddrAsString(); + // If there is an answer it should be an IPv6 address + dump(answer); + do_check_true(answer.indexOf(':') != -1); + } catch (e) { + break; + } + } + do_test_finished(); + } +}; + +function run_test() { + do_test_pending(); + try { + dns.asyncResolve("example.org", Ci.nsIDNSService.RESOLVE_DISABLE_IPV4, listener, null); + } catch (e) { + dump(e); + do_check_true(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..af5558d53e --- /dev/null +++ b/netwerk/test/unit/test_dns_disable_ipv6.js @@ -0,0 +1,41 @@ +// +// Tests that calling asyncResolve with the RESOLVE_DISABLE_IPV6 flag doesn't +// return any IPv6 addresses. +// + +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: function(inRequest, inRecord, inStatus) { + if (inStatus != Cr.NS_OK) { + do_check_eq(inStatus, Cr.NS_ERROR_UNKNOWN_HOST); + do_test_finished(); + return; + } + + while (true) { + try { + var answer = inRecord.getNextAddrAsString(); + // If there is an answer it should be an IPv4 address + dump(answer); + do_check_true(answer.indexOf(':') == -1); + do_check_true(answer.indexOf('.') != -1); + } catch (e) { + break; + } + } + do_test_finished(); + } +}; + +function run_test() { + do_test_pending(); + try { + dns.asyncResolve("example.com", Ci.nsIDNSService.RESOLVE_DISABLE_IPV6, listener, null); + } catch (e) { + dump(e); + do_check_true(false); + do_test_finished(); + } +} diff --git a/netwerk/test/unit/test_dns_localredirect.js b/netwerk/test/unit/test_dns_localredirect.js new file mode 100644 index 0000000000..71e312ebc7 --- /dev/null +++ b/netwerk/test/unit/test_dns_localredirect.js @@ -0,0 +1,31 @@ +var dns = Cc["@mozilla.org/network/dns-service;1"].getService(Ci.nsIDNSService); +var prefs = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch); + +var listener = { + onLookupComplete: function(inRequest, inRecord, inStatus) { + var answer = inRecord.getNextAddrAsString(); + do_check_true(answer == "127.0.0.1" || answer == "::1"); + + prefs.clearUserPref("network.dns.localDomains"); + + do_test_finished(); + }, + QueryInterface: function(aIID) { + if (aIID.equals(Ci.nsIDNSListener) || + aIID.equals(Ci.nsISupports)) { + return this; + } + throw Cr.NS_ERROR_NO_INTERFACE; + } +}; + +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; + dns.asyncResolve("local.vingtetun.org", 0, listener, mainThread); + + do_test_pending(); +} + diff --git a/netwerk/test/unit/test_dns_offline.js b/netwerk/test/unit/test_dns_offline.js new file mode 100644 index 0000000000..87a9ad8b1e --- /dev/null +++ b/netwerk/test/unit/test_dns_offline.js @@ -0,0 +1,74 @@ +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: function(inRequest, inRecord, inStatus) { + do_check_eq(inStatus, Cr.NS_ERROR_OFFLINE); + test2(); + do_test_finished(); + } +}; + +var listener2 = { + onLookupComplete: function(inRequest, inRecord, inStatus) { + do_check_eq(inStatus, Cr.NS_OK); + var answer = inRecord.getNextAddrAsString(); + do_check_true(answer == "127.0.0.1" || answer == "::1"); + test3(); + do_test_finished(); + } +}; + +var listener3 = { + onLookupComplete: function(inRequest, inRecord, inStatus) { + do_check_eq(inStatus, Cr.NS_OK); + var answer = inRecord.getNextAddrAsString(); + do_check_true(answer == "127.0.0.1" || answer == "::1"); + cleanup(); + do_test_finished(); + } +}; + +function run_test() { + do_test_pending(); + prefs.setBoolPref("network.dns.offline-localhost", false); + ioService.offline = true; + try { + dns.asyncResolve("localhost", 0, listener1, mainThread); + } catch (e) { + do_check_eq(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", 0, listener2, mainThread); +} + +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", 0, listener3, mainThread); +} + +function cleanup() { + prefs.clearUserPref("network.dns.offline-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..02647b9647 --- /dev/null +++ b/netwerk/test/unit/test_dns_onion.js @@ -0,0 +1,70 @@ +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: function(inRequest, inRecord, inStatus) { + do_check_false(Components.isSuccessCode(inStatus)); + do_test_dontBlock(); + }, + QueryInterface: function(aIID) { + if (aIID.equals(Ci.nsIDNSListener) || + aIID.equals(Ci.nsISupports)) { + return this; + } + throw Cr.NS_ERROR_NO_INTERFACE; + } +}; + +// check that we do lookup .onion (via pref) +var listenerDontBlock = { + onLookupComplete: function(inRequest, inRecord, inStatus) { + var answer = inRecord.getNextAddrAsString(); + do_check_true(answer == "127.0.0.1" || answer == "::1"); + all_done(); + }, + QueryInterface: function(aIID) { + if (aIID.equals(Ci.nsIDNSListener) || + aIID.equals(Ci.nsISupports)) { + return this; + } + throw Cr.NS_ERROR_NO_INTERFACE; + } +}; + +function do_test_dontBlock() { + prefs.setBoolPref("network.dns.blockDotOnion", false); + dns.asyncResolve("private.onion", 0, listenerDontBlock, mainThread); +} + +function do_test_block() { + prefs.setBoolPref("network.dns.blockDotOnion", true); + try { + dns.asyncResolve("private.onion", 0, listenerBlock, mainThread); + } catch (e) { + // it is ok for this negative test to fail fast + do_check_true(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_per_interface.js b/netwerk/test/unit/test_dns_per_interface.js new file mode 100644 index 0000000000..b4c69f8c36 --- /dev/null +++ b/netwerk/test/unit/test_dns_per_interface.js @@ -0,0 +1,79 @@ +var dns = Cc["@mozilla.org/network/dns-service;1"].getService(Ci.nsIDNSService); + +// This test checks DNSService host resolver when a network interface is supplied +// as well. In the test 3 request are sent: two with a network interface set +// and one without a network interface. +// All requests have the same host to be resolved and the same flags. +// One of the request with the network interface will be canceled. +// The request with and without a network interface should not be mixed during +// the requests lifetime. + +var netInterface1 = "interface1"; +var netInterface2 = "interface2"; + +// We are not using localhost because on e10s a host resolve callback is almost +// always faster than a cancel request, therefore cancel operation would not be +// tested. +var hostname = "thisshouldnotexist.mozilla.com"; + +// 3 requests. +var requestWithInterfaceCanceled; +var requestWithoutInterfaceNotCanceled; +var requestWithInterfaceNotCanceled; + +var listener = { + onLookupComplete: function(inRequest, inRecord, inStatus) { + // Two requests should be resolved and one request should be canceled. + // Since cancalation of a request is racy we will check only for not + // canceled request - they should not be canceled. + if ((inRequest == requestWithoutInterfaceNotCanceled) || + (inRequest == requestWithInterfaceNotCanceled)) { + // This request should not be canceled. + do_check_neq(inStatus, Cr.NS_ERROR_ABORT); + + do_test_finished(); + } else if (inRequest == requestWithInterfaceCanceled) { + // We do not check the outcome for this one because it is racy - + // whether the request cancelation is faster than resolving the request. + do_test_finished(); + } + }, + QueryInterface: function(aIID) { + if (aIID.equals(Ci.nsIDNSListener) || + aIID.equals(Ci.nsISupports)) { + return this; + } + throw Cr.NS_ERROR_NO_INTERFACE; + } +}; + +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. + requestWithInterfaceCanceled = dns.asyncResolveExtended(hostname, flags, + netInterface1, + listener, + mainThread); + requestWithInterfaceCanceled.cancel(Cr.NS_ERROR_ABORT); + + // This one will not be canceled. This is the request without a network + // interface. + requestWithoutInterfaceNotCanceled = dns.asyncResolve(hostname, flags, + listener, mainThread); + + // This one will not be canceled. + requestWithInterfaceNotCanceled = dns.asyncResolveExtended(hostname, flags, + netInterface2, + listener, + mainThread); + // We wait for notifications for the requests. + // For each request onLookupComplete will be called. + do_test_pending(); + do_test_pending(); + do_test_pending(); +} 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..6443f7ade0 --- /dev/null +++ b/netwerk/test/unit/test_dns_proxy_bypass.js @@ -0,0 +1,77 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +Cu.import("resource://gre/modules/Services.jsm"); + +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: function() { + this.obs = Cc["@mozilla.org/observer-service;1"]. + getService(Ci.nsIObserverService); + this.obs.addObserver(this, "dns-resolution-request", false); + }, + + unregister: function() { + if (this.obs) { + this.obs.removeObserver(this, "dns-resolution-request"); + } + }, + + observe: function(subject, topic, data) { + if (topic == "dns-resolution-request") { + do_print(data); + if (data.indexOf("dnsleak.example.com") > -1) { + try { + do_check_true(false); + } catch (e) {} + } + } + } +}; + +var listener = { + onAcknowledge: function(aContext, aSize) {}, + onBinaryMessageAvailable: function(aContext, aMsg) {}, + onMessageAvailable: function(aContext, aMsg) {}, + onServerClose: function(aContext, aCode, aReason) {}, + onStart: function(aContext) {}, + onStop: function(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(Components.interfaces.nsIWebSocketChannel); + + chan.initLoadInfo(null, // aLoadingNode + Services.scriptSecurityManager.getSystemPrincipal(), + null, // aTriggeringPrincipal + Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL, + Ci.nsIContentPolicy.TYPE_WEBSOCKET); + + var uri = ioService.newURI(url, null, null); + 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..04c1faeefe --- /dev/null +++ b/netwerk/test/unit/test_dns_service.js @@ -0,0 +1,26 @@ +var dns = Cc["@mozilla.org/network/dns-service;1"].getService(Ci.nsIDNSService); + +var listener = { + onLookupComplete: function(inRequest, inRecord, inStatus) { + var answer = inRecord.getNextAddrAsString(); + do_check_true(answer == "127.0.0.1" || answer == "::1"); + + do_test_finished(); + }, + QueryInterface: function(aIID) { + if (aIID.equals(Ci.nsIDNSListener) || + aIID.equals(Ci.nsISupports)) { + return this; + } + throw Cr.NS_ERROR_NO_INTERFACE; + } +}; + +function run_test() { + var threadManager = Cc["@mozilla.org/thread-manager;1"].getService(Ci.nsIThreadManager); + var mainThread = threadManager.currentThread; + dns.asyncResolve("localhost", 0, listener, mainThread); + + do_test_pending(); +} + diff --git a/netwerk/test/unit/test_doomentry.js b/netwerk/test/unit/test_doomentry.js new file mode 100644 index 0000000000..5929523068 --- /dev/null +++ b/netwerk/test/unit/test_doomentry.js @@ -0,0 +1,97 @@ +/** + * Test for nsICacheStorage.asyncDoomURI(). + * It tests dooming + * - an existent inactive entry + * - a non-existent inactive entry + * - an existent active entry + */ + +function doom(url, callback) +{ + get_cache_service() + .diskCacheStorage(LoadContextInfo.default, false) + .asyncDoomURI(createURI(url), "", { + onCacheEntryDoomed: function(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)); + }); +} + +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) +{ + do_check_eq(status, Cr.NS_OK); + doom("http://nonexistententry/", check_doom2); +} + +function check_doom2(status) +{ + do_check_eq(status, Cr.NS_ERROR_NOT_AVAILABLE); + asyncOpenCacheEntry("http://testentry/", "disk", Ci.nsICacheStorage.OPEN_TRUNCATE, null, function(status, entry) { + write_entry2(entry, entry.openOutputStream(0)); + }); +} + +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) +{ + do_check_eq(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) +{ + do_check_eq(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..80c1708871 --- /dev/null +++ b/netwerk/test/unit/test_duplicate_headers.js @@ -0,0 +1,605 @@ +/* + * Tests bugs 597706, 655389: prevent duplicate headers with differing values + * for some headers like Content-Length, Location, etc. + */ + +//////////////////////////////////////////////////////////////////////////////// +// Test infrastructure + +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +XPCOMUtils.defineLazyGetter(this, "URL", function() { + return "http://localhost:" + httpserver.identity.primaryPort; +}); + +var httpserver = new HttpServer(); +var index = 0; +var test_flags = new Array(); +var testPathBase = "/dupe_hdrs"; + +function run_test() +{ + httpserver.start(-1); + + do_test_pending(); + run_test_number(1); +} + +function run_test_number(num) +{ + testPath = testPathBase + num; + httpserver.registerPathHandler(testPath, eval("handler" + num)); + + var channel = setupChannel(testPath); + flags = test_flags[num]; // OK if flags undefined for test + channel.asyncOpen2(new ChannelListener(eval("completeTest" + num), + channel, flags)); +} + +function setupChannel(url) +{ + var chan = NetUtil.newChannel({ + uri: URL + url, + loadUsingSystemPrincipal: true + }); + var httpChan = chan.QueryInterface(Components.interfaces.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) +{ + do_check_eq(request.status, Components.results.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) +{ + do_check_eq(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) +{ + do_check_eq(request.status, Components.results.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) +{ + do_check_eq(request.status, Components.results.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 { + referer = request.getResponseHeader("Referer"); + do_check_eq(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) +{ + do_check_eq(request.status, Components.results.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(Components.interfaces.nsIHttpChannel); + + try { + referer = request.getResponseHeader("Referer"); + do_check_eq(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) +{ + do_check_eq(request.status, Components.results.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: + do_check_eq(request.status, Components.results.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) +{ + do_check_eq(request.status, Components.results.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) +{ + do_check_eq(request.status, 0); + + try { + var chan = request.QueryInterface(Ci.nsIChannel); + do_check_eq(chan.contentDisposition, chan.DISPOSITION_ATTACHMENT); + do_check_eq(chan.contentDispositionFilename, "foo"); + do_check_eq(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) +{ + do_check_eq(request.status, Components.results.NS_OK); + do_check_eq(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) +{ + do_check_eq(request.status, Components.results.NS_OK); + do_check_eq(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) +{ + do_check_eq(request.status, Components.results.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) +{ + do_check_eq(request.status, Components.results.NS_ERROR_CORRUPTED_CONTENT); + + run_test_number(16); +} + +//////////////////////////////////////////////////////////////////////////////// +// empty content length is ok +test_flags[16] = CL_ALLOW_UNKNOWN_CL; +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) +{ + do_check_eq(request.status, Components.results.NS_OK); + do_check_eq(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) +{ + do_check_eq(request.status, Components.results.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) +{ + do_check_eq(request.status, Components.results.NS_OK); + do_check_eq(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) +{ + do_check_eq(request.status, Components.results.NS_OK); + do_check_eq(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) +{ + do_check_eq(request.status, Components.results.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..45c01683a7 --- /dev/null +++ b/netwerk/test/unit/test_event_sink.js @@ -0,0 +1,170 @@ +// This file tests channel event sinks (bug 315598 et al) + +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +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: function eventsink_qi(iid) { + if (iid.equals(Components.interfaces.nsISupports) || + iid.equals(Components.interfaces.nsIFactory) || + iid.equals(Components.interfaces.nsIChannelEventSink)) + return this; + throw Components.results.NS_ERROR_NO_INTERFACE; + }, + createInstance: function eventsink_ci(outer, iid) { + if (outer) + throw Components.results.NS_ERROR_NO_AGGREGATION; + return this.QueryInterface(iid); + }, + lockFactory: function eventsink_lockf(lock) { + throw Components.results.NS_ERROR_NOT_IMPLEMENTED; + }, + + asyncOnChannelRedirect: function eventsink_onredir(oldChan, newChan, flags, callback) { + // veto + this.called = true; + throw Components.results.NS_BINDING_ABORTED; + }, + + getInterface: function eventsink_gi(iid) { + if (iid.equals(Components.interfaces.nsIChannelEventSink)) + return this; + throw Components.results.NS_ERROR_NO_INTERFACE; + }, + + called: false +}; + +var listener = { + expectSinkCall: true, + + onStartRequest: function test_onStartR(request, ctx) { + 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 Components.interfaces.nsIChannel) || + !request.URI.equals(request.originalURI)) + do_throw("Wrong URI: Is <" + request.URI.spec + ">, should be <" + + request.originalURI.spec + ">"); + + if (request instanceof Components.interfaces.nsIHttpChannel) { + // As we expect a blocked redirect, verify that we have a 3xx status + do_check_eq(Math.floor(request.responseStatus / 100), 3); + do_check_eq(request.requestSucceeded, false); + } + + do_check_eq(eventsink.called, this.expectSinkCall); + } catch (e) { + do_throw("Unexpected exception: " + e); + } + + throw Components.results.NS_ERROR_ABORT; + }, + + onDataAvailable: function test_ODA() { + do_throw("Should not get any data!"); + }, + + onStopRequest: function test_onStopR(request, ctx, 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.asyncOpen2(listener); + + do_test_pending(); +} + +function run_test_continued() { + eventsink.called = false; + + var catMan = Components.classes["@mozilla.org/categorymanager;1"] + .getService(Components.interfaces.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.asyncOpen2(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_extract_charset_from_content_type.js b/netwerk/test/unit/test_extract_charset_from_content_type.js new file mode 100644 index 0000000000..224be7052e --- /dev/null +++ b/netwerk/test/unit/test_extract_charset_from_content_type.js @@ -0,0 +1,163 @@ +/* -*- 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/. */ + +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) { + do_check_eq(aHadCharset, hadCharset); + do_check_eq(aCharset, charset.value); + do_check_eq(aCharsetStart, charsetStart.value); + do_check_eq(aCharsetEnd, charsetEnd.value); +} + +function run_test() { + var netutil = Components.classes["@mozilla.org/network/util;1"] + .getService(Components.interfaces.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..5f06b29608 --- /dev/null +++ b/netwerk/test/unit/test_fallback_no-cache-entry_canceled.js @@ -0,0 +1,112 @@ +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + + +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, null, null); +} + +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) +{ + do_check_eq(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.createCodebasePrincipal(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.setComplexValue("browser.cache.offline.parent_directory", Ci.nsILocalFile, do_get_profile()); + + cacheUpdateObserver = {observe: function() { + 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.asyncOpen2(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", false); + + 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..cd389421b8 --- /dev/null +++ b/netwerk/test/unit/test_fallback_no-cache-entry_passing.js @@ -0,0 +1,110 @@ +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +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, null, null); +} + +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) +{ + do_check_eq(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.createCodebasePrincipal(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.setComplexValue("browser.cache.offline.parent_directory", Ci.nsILocalFile, do_get_profile()); + + cacheUpdateObserver = {observe: function() { + 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.asyncOpen2(new ChannelListener(finish_test)); + }} + + var os = Cc["@mozilla.org/observer-service;1"]. + getService(Ci.nsIObserverService); + os.addObserver(cacheUpdateObserver, "offline-cache-update-completed", false); + + 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..eddfcbec0f --- /dev/null +++ b/netwerk/test/unit/test_fallback_redirect-to-different-origin_canceled.js @@ -0,0 +1,115 @@ +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +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, null, null); +} + +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) +{ + do_check_eq(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.createCodebasePrincipal(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.setComplexValue("browser.cache.offline.parent_directory", Ci.nsILocalFile, do_get_profile()); + + cacheUpdateObserver = {observe: function() { + 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.asyncOpen2(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", false); + + 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..a725161efc --- /dev/null +++ b/netwerk/test/unit/test_fallback_redirect-to-different-origin_passing.js @@ -0,0 +1,114 @@ +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +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, null, null); +} + +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) +{ + do_check_eq(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.createCodebasePrincipal(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.setComplexValue("browser.cache.offline.parent_directory", Ci.nsILocalFile, do_get_profile()); + + cacheUpdateObserver = {observe: function() { + 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.asyncOpen2(new ChannelListener(finish_test)); + }} + + var os = Cc["@mozilla.org/observer-service;1"]. + getService(Ci.nsIObserverService); + os.addObserver(cacheUpdateObserver, "offline-cache-update-completed", false); + + 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..02129e6b4e --- /dev/null +++ b/netwerk/test/unit/test_fallback_request-error_canceled.js @@ -0,0 +1,121 @@ +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +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, null, null); +} + +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) +{ + do_check_eq(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.createCodebasePrincipal(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.setComplexValue("browser.cache.offline.parent_directory", Ci.nsILocalFile, do_get_profile()); + + cacheUpdateObserver = {observe: function() { + 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.asyncOpen2(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", false); + + 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..8dc935273b --- /dev/null +++ b/netwerk/test/unit/test_fallback_request-error_passing.js @@ -0,0 +1,119 @@ +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +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, null, null); +} + +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) +{ + do_check_eq(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.createCodebasePrincipal(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.setComplexValue("browser.cache.offline.parent_directory", Ci.nsILocalFile, do_get_profile()); + + cacheUpdateObserver = {observe: function() { + dump("got offline-cache-update-completed\n"); + // offline cache update completed. + var _x = 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.asyncOpen2(new ChannelListener(finish_test)); + }); + }} + + + var os = Cc["@mozilla.org/observer-service;1"]. + getService(Ci.nsIObserverService); + os.addObserver(cacheUpdateObserver, "offline-cache-update-completed", false); + + 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..e025f29678 --- /dev/null +++ b/netwerk/test/unit/test_fallback_response-error_canceled.js @@ -0,0 +1,116 @@ +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + + +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, null, null); +} + +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) +{ + do_check_eq(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.createCodebasePrincipal(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.setComplexValue("browser.cache.offline.parent_directory", Ci.nsILocalFile, do_get_profile()); + + cacheUpdateObserver = {observe: function() { + 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.asyncOpen2(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", false); + + 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..e59fad7d09 --- /dev/null +++ b/netwerk/test/unit/test_fallback_response-error_passing.js @@ -0,0 +1,114 @@ +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +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, null, null); +} + +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) +{ + do_check_eq(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.createCodebasePrincipal(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.setComplexValue("browser.cache.offline.parent_directory", Ci.nsILocalFile, do_get_profile()); + + cacheUpdateObserver = {observe: function() { + 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.asyncOpen2(new ChannelListener(finish_test)); + }} + + var os = Cc["@mozilla.org/observer-service;1"]. + getService(Ci.nsIObserverService); + os.addObserver(cacheUpdateObserver, "offline-cache-update-completed", false); + + 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_partial_inputstream.js b/netwerk/test/unit/test_file_partial_inputstream.js new file mode 100644 index 0000000000..6eb4a3ac8b --- /dev/null +++ b/netwerk/test/unit/test_file_partial_inputstream.js @@ -0,0 +1,512 @@ +/* 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 nsIPartialFileInputStream +// NOTE! These tests often use do_check_true(a == b) rather than +// do_check_eq(a, b) to avoid outputting characters which confuse +// the console + +var CC = Components.Constructor; +const BinaryInputStream = CC("@mozilla.org/binaryinputstream;1", + "nsIBinaryInputStream", + "setInputStream"); +const PR_RDONLY = 0x1; // see prio.h + +// We need the profile directory so the test harness will clean up our test +// files. +do_get_profile(); + +var binary_test_file_name = "data/image.png"; +var text_test_file_name = "test_file_partial_inputstream.js"; +// This is a global variable since if it's passed as an argument stack traces +// become unreadable. +var test_file_data; + +function run_test() +{ + // Binary tests + let binaryFile = do_get_file(binary_test_file_name); + let size = binaryFile.fileSize; + // Want to make sure we're working with a large enough file + dump("**** binary file size is: " + size + " ****\n"); + do_check_true(size > 65536); + + let binaryStream = new BinaryInputStream(new_file_input_stream(binaryFile)); + test_file_data = ""; + while ((avail = binaryStream.available()) > 0) { + test_file_data += binaryStream.readBytes(avail); + } + do_check_eq(test_file_data.length, size); + binaryStream.close(); + + test_binary_portion(0, 10); + test_binary_portion(0, 20000); + test_binary_portion(0, size); + test_binary_portion(20000, 10); + test_binary_portion(20000, 20000); + test_binary_portion(20000, size-20000); + test_binary_portion(size-10, 10); + test_binary_portion(size-20000, 20000); + test_binary_portion(0, 0); + test_binary_portion(20000, 0); + test_binary_portion(size-1, 1); + + + // Text-file tests + let textFile = do_get_file(binary_test_file_name); + size = textFile.fileSize; + // Want to make sure we're working with a large enough file + dump("**** text file size is: " + size + " ****\n"); + do_check_true(size > 7000); + + let textStream = new BinaryInputStream(new_file_input_stream(textFile)); + test_file_data = ""; + while ((avail = textStream.available()) > 0) + test_file_data += textStream.readBytes(avail); + do_check_eq(test_file_data.length, size); + textStream.close(); + + test_text_portion(0, 100); + test_text_portion(0, size); + test_text_portion(5000, 1000); + test_text_portion(size-10, 10); + test_text_portion(size-5000, 5000); + test_text_portion(10, 0); + test_text_portion(size-1, 1); + + // Test auto-closing files + // Test behavior when *not* autoclosing + let tempFile = create_temp_file("01234567890123456789"); + let tempInputStream = new_partial_file_input_stream(tempFile, 5, 10); + tempInputStream.QueryInterface(Ci.nsILineInputStream); + do_check_eq(read_line_stream(tempInputStream)[1], "5678901234"); + try { + // This fails on some platforms + tempFile.remove(false); + } + catch (ex) { + } + tempInputStream.QueryInterface(Ci.nsISeekableStream); + tempInputStream.seek(SET, 1); + do_check_eq(read_line_stream(tempInputStream)[1], "678901234"); + + // Test removing the file when autoclosing + tempFile = create_temp_file("01234567890123456789"); + tempInputStream = new_partial_file_input_stream(tempFile, 5, 10, + Ci.nsIFileInputStream.CLOSE_ON_EOF | + Ci.nsIFileInputStream.REOPEN_ON_REWIND); + tempInputStream.QueryInterface(Ci.nsILineInputStream); + do_check_eq(read_line_stream(tempInputStream)[1], "5678901234"); + tempFile.remove(false); + tempInputStream.QueryInterface(Ci.nsISeekableStream); + try { + // The seek should reopen the file, which should fail. + tempInputStream.seek(SET, 1); + do_check_true(false); + } + catch (ex) { + } + + // Test editing the file when autoclosing + tempFile = create_temp_file("01234567890123456789"); + tempInputStream = new_partial_file_input_stream(tempFile, 5, 10, + Ci.nsIFileInputStream.CLOSE_ON_EOF | + Ci.nsIFileInputStream.REOPEN_ON_REWIND); + tempInputStream.QueryInterface(Ci.nsILineInputStream); + do_check_eq(read_line_stream(tempInputStream)[1], "5678901234"); + let ostream = Cc["@mozilla.org/network/file-output-stream;1"]. + createInstance(Ci.nsIFileOutputStream); + ostream.init(tempFile, 0x02 | 0x08 | 0x20, // write, create, truncate + 0o666, 0); + let newData = "abcdefghijklmnopqrstuvwxyz"; + ostream.write(newData, newData.length); + ostream.close(); + tempInputStream.QueryInterface(Ci.nsISeekableStream); + tempInputStream.seek(SET, 1); + do_check_eq(read_line_stream(tempInputStream)[1], newData.substr(6,9)); + + // Test auto-delete and auto-close together + tempFile = create_temp_file("01234567890123456789"); + tempInputStream = new_partial_file_input_stream(tempFile, 5, 10, + Ci.nsIFileInputStream.CLOSE_ON_EOF | + Ci.nsIFileInputStream.DELETE_ON_CLOSE); + tempInputStream.QueryInterface(Ci.nsILineInputStream); + do_check_eq(read_line_stream(tempInputStream)[1], "5678901234"); + do_check_false(tempFile.exists()); +} + +function test_binary_portion(start, length) { + let subFile = create_temp_file(test_file_data.substr(start, length)); + + let streamTests = [ + test_4k_read, + test_max_read, + test_seek, + test_seek_then_read, + ]; + + for (var test of streamTests) { + let fileStream = new_file_input_stream(subFile); + let partialStream = new_partial_file_input_stream(do_get_file(binary_test_file_name), + start, length); + test(fileStream, partialStream, length); + fileStream.close(); + partialStream.close(); + } +} + +function test_4k_read(fileStreamA, fileStreamB) { + fileStreamA.QueryInterface(Ci.nsISeekableStream); + fileStreamB.QueryInterface(Ci.nsISeekableStream); + let streamA = new BinaryInputStream(fileStreamA); + let streamB = new BinaryInputStream(fileStreamB); + + while(1) { + do_check_eq(fileStreamA.tell(), fileStreamB.tell()); + + let availA = streamA.available(); + let availB = streamB.available(); + do_check_eq(availA, availB); + if (availA == 0) + return; + + let readSize = availA > 4096 ? 4096 : availA; + + do_check_true(streamA.readBytes(readSize) == + streamB.readBytes(readSize)); + } +} + +function test_max_read(fileStreamA, fileStreamB) { + fileStreamA.QueryInterface(Ci.nsISeekableStream); + fileStreamB.QueryInterface(Ci.nsISeekableStream); + let streamA = new BinaryInputStream(fileStreamA); + let streamB = new BinaryInputStream(fileStreamB); + + while(1) { + do_check_eq(fileStreamA.tell(), fileStreamB.tell()); + + let availA = streamA.available(); + let availB = streamB.available(); + do_check_eq(availA, availB); + if (availA == 0) + return; + + do_check_true(streamA.readBytes(availA) == + streamB.readBytes(availB)); + } +} + +const SET = Ci.nsISeekableStream.NS_SEEK_SET; +const CUR = Ci.nsISeekableStream.NS_SEEK_CUR; +const END = Ci.nsISeekableStream.NS_SEEK_END; +function test_seek(dummy, partialFileStream, size) { + // We can't test the "real" filestream here as our existing file streams + // are very broken and allows searching past the end of the file. + + partialFileStream.QueryInterface(Ci.nsISeekableStream); + + var tests = [ + [SET, 0], + [SET, 5], + [SET, 1000], + [SET, size-10], + [SET, size-5], + [SET, size-1], + [SET, size], + [SET, size+10], + [SET, 0], + [CUR, 5], + [CUR, -5], + [SET, 5000], + [CUR, -100], + [CUR, 200], + [CUR, -5000], + [CUR, 5000], + [CUR, size * 2], + [SET, 1], + [CUR, -1], + [CUR, -1], + [CUR, -1], + [CUR, -1], + [CUR, -1], + [SET, size-1], + [CUR, 1], + [CUR, 1], + [CUR, 1], + [CUR, 1], + [CUR, 1], + [END, 0], + [END, -1], + [END, -5], + [END, -1000], + [END, -size+10], + [END, -size+5], + [END, -size+1], + [END, -size], + [END, -size-10], + [END, 10], + [CUR, 10], + [CUR, 10], + [CUR, 100], + [CUR, 1000], + [END, -1000], + [CUR, 100], + [CUR, 900], + [CUR, 100], + [CUR, 100], + ]; + + let pos = 0; + for (var test of tests) { + let didThrow = false; + try { + partialFileStream.seek(test[0], test[1]); + } + catch (ex) { + didThrow = true; + } + + let newPos = test[0] == SET ? test[1] : + test[0] == CUR ? pos + test[1] : + size + test[1]; + if (newPos > size || newPos < 0) { + do_check_true(didThrow); + } + else { + do_check_false(didThrow); + pos = newPos; + } + + do_check_eq(partialFileStream.tell(), pos); + do_check_eq(partialFileStream.available(), size - pos); + } +} + +function test_seek_then_read(fileStreamA, fileStreamB, size) { + // For now we only test seeking inside the file since our existing file + // streams behave very strange when seeking to past the end of the file. + if (size < 20000) { + return; + } + + fileStreamA.QueryInterface(Ci.nsISeekableStream); + fileStreamB.QueryInterface(Ci.nsISeekableStream); + let streamA = new BinaryInputStream(fileStreamA); + let streamB = new BinaryInputStream(fileStreamB); + + let read = {}; + + var tests = [ + [SET, 0], + [read, 1000], + [read, 1000], + [SET, 5], + [read, 1000], + [read, 5000], + [CUR, 100], + [read, 1000], + [read, 5000], + [CUR, -100], + [read, 1000], + [CUR, -100], + [read, 5000], + [END, -10], + [read, 10], + [END, -100], + [read, 101], + [CUR, -100], + [read, 10], + [SET, 0], + [read, 20000], + [read, 1], + [read, 100], + ]; + + for (var test of tests) { + if (test[0] === read) { + + let didThrowA = false; + let didThrowB = false; + + let bytesA, bytesB; + try { + bytesA = streamA.readBytes(test[1]); + } + catch (ex) { + didThrowA = true; + } + try { + bytesB = streamB.readBytes(test[1]); + } + catch (ex) { + didThrowB = true; + } + + do_check_eq(didThrowA, didThrowB); + do_check_true(bytesA == bytesB); + } + else { + fileStreamA.seek(test[0], test[1]); + fileStreamB.seek(test[0], test[1]); + } + do_check_eq(fileStreamA.tell(), fileStreamB.tell()); + do_check_eq(fileStreamA.available(), fileStreamB.available()); + } +} + +function test_text_portion(start, length) { + let subFile = create_temp_file(test_file_data.substr(start, length)); + + let streamTests = [ + test_readline, + test_seek_then_readline, + ]; + + for (var test of streamTests) { + let fileStream = new_file_input_stream(subFile) + .QueryInterface(Ci.nsILineInputStream); + let partialStream = new_partial_file_input_stream(do_get_file(binary_test_file_name), + start, length) + .QueryInterface(Ci.nsILineInputStream); + test(fileStream, partialStream, length); + fileStream.close(); + partialStream.close(); + } +} + +function test_readline(fileStreamA, fileStreamB) +{ + let moreA = true, moreB; + while(moreA) { + let lineA, lineB; + [moreA, lineA] = read_line_stream(fileStreamA); + [moreB, lineB] = read_line_stream(fileStreamB); + do_check_eq(moreA, moreB); + do_check_true(lineA.value == lineB.value); + } +} + +function test_seek_then_readline(fileStreamA, fileStreamB, size) { + // For now we only test seeking inside the file since our existing file + // streams behave very strange when seeking to past the end of the file. + if (size < 100) { + return; + } + + fileStreamA.QueryInterface(Ci.nsISeekableStream); + fileStreamB.QueryInterface(Ci.nsISeekableStream); + + let read = {}; + + var tests = [ + [SET, 0], + [read, 5], + [read, 5], + [SET, 5], + [read, 5], + [read, 15], + [CUR, 100], + [read, 5], + [read, 15], + [CUR, -100], + [read, 5], + [CUR, -100], + [read, 25], + [END, -10], + [read, 1], + [END, -50], + [read, 30], + [read, 1], + [read, 1], + [CUR, -100], + [read, 1], + [SET, 0], + [read, 10000], + [read, 1], + [read, 1], + [SET, 0], + [read, 1], + ]; + + for (var test of tests) { + if (test[0] === read) { + + for (let i = 0; i < test[1]; ++i) { + let didThrowA = false; + let didThrowB = false; + + let lineA, lineB, moreA, moreB; + try { + [moreA, lineA] = read_line_stream(fileStreamA); + } + catch (ex) { + didThrowA = true; + } + try { + [moreB, lineB] = read_line_stream(fileStreamB); + } + catch (ex) { + didThrowB = true; + } + + do_check_eq(didThrowA, didThrowB); + do_check_eq(moreA, moreB); + do_check_true(lineA == lineB); + do_check_eq(fileStreamA.tell(), fileStreamB.tell()); + do_check_eq(fileStreamA.available(), fileStreamB.available()); + if (!moreA) + break; + } + } + else { + if (!(test[0] == CUR && (test[1] > fileStreamA.available() || + test[1] < -fileStreamA.tell()))) { + fileStreamA.seek(test[0], test[1]); + fileStreamB.seek(test[0], test[1]); + do_check_eq(fileStreamA.tell(), fileStreamB.tell()); + do_check_eq(fileStreamA.available(), fileStreamB.available()); + } + } + } +} + +function read_line_stream(stream) { + let line = {}; + let more = stream.readLine(line); + return [more, line.value]; +} + +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.QueryInterface(Ci.nsIInputStream); +} + +function new_partial_file_input_stream(file, start, length, flags) { + var stream = + Cc["@mozilla.org/network/partial-file-input-stream;1"] + .createInstance(Ci.nsIPartialFileInputStream); + stream.init(file, start, length, PR_RDONLY, 0, flags || 0); + return stream.QueryInterface(Ci.nsIInputStream); +} + +function create_temp_file(data) { + let file = Cc["@mozilla.org/file/directory_service;1"]. + getService(Ci.nsIProperties). + get("ProfD", Ci.nsIFile); + file.append("fileinputstream-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, 0x02 | 0x08 | 0x20, // write, create, truncate + 0o666, 0); + do_check_eq(ostream.write(data, data.length), data.length); + ostream.close(); + + return file; +} diff --git a/netwerk/test/unit/test_file_protocol.js b/netwerk/test/unit/test_file_protocol.js new file mode 100644 index 0000000000..2f1c5eb7f1 --- /dev/null +++ b/netwerk/test/unit/test_file_protocol.js @@ -0,0 +1,251 @@ +/* run some tests on the file:// protocol handler */ + +Cu.import("resource://gre/modules/NetUtil.jsm"); + +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_replace, + do_test_finished +].forEach(add_test); + +function getFile(key) { + var dirSvc = Components.classes["@mozilla.org/file/directory_service;1"] + .getService(Components.interfaces.nsIProperties); + return dirSvc.get(key, Components.interfaces.nsILocalFile); +} + +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: function(request) { + request.QueryInterface(Ci.nsIFileChannel); + return request.file.isDirectory(); + }, + + QueryInterface: function(iid) { + if (iid.equals(Ci.nsIStreamListener) || + iid.equals(Ci.nsIRequestObserver) || + iid.equals(Ci.nsISupports)) + return this; + throw Cr.NS_ERROR_NO_INTERFACE; + }, + + onStartRequest: function(request, context) { + 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: function(request, context, 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: function(request, context, 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.asyncOpen2(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.asyncOpen2(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.asyncOpen2(new FileStreamListener(on_upload_complete)); +} + +function test_load_replace() { + // lnk files should resolve to their targets + if (mozinfo.os == "win") { + dump("*** test_load_replace\n"); + file = do_get_file("data/system_root.lnk", false); + var chan = new_file_channel(file); + + // The LOAD_REPLACE flag should be set + do_check_eq(chan.loadFlags & chan.LOAD_REPLACE, chan.LOAD_REPLACE); + + // The original URI path should differ from the URI path + do_check_neq(chan.URI.path, chan.originalURI.path); + + // 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); + do_check_eq(chan.originalURI.path, ios.newFileURI(file).path); + } + run_next_test(); +} + +function run_test() { + 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..2736527a53 --- /dev/null +++ b/netwerk/test/unit/test_filestreams.js @@ -0,0 +1,298 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +var Cc = Components.classes; +var Ci = Components.interfaces; + +// 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); + do_check_eq(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 + do_check_true(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(aFile, -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). + do_check_true( (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(); + + do_check_eq(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); + + do_check_eq(buffered.available(), 0); + + // Now try reading from this stream + let string = {}; + do_check_eq(cstream.readString(BUFFERSIZE, string), 0); + do_check_eq(string.value, ""); + + // Now check that available() throws + var exceptionThrown = false; + try { + do_check_eq(buffered.available(), 0); + } catch (e) { + exceptionThrown = true; + } + do_check_true(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 { + do_check_eq(buffered.available(), 0); + } catch (e) { + exceptionThrown = true; + } + do_check_false(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..c1403f517e --- /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. +Cu.import("resource://gre/modules/NetUtil.jsm"); + +var listener = { + onStartRequest: function test_onStartR(request, ctx) { + }, + + onDataAvailable: function test_ODA() { + do_throw("Should not get any data!"); + }, + + onStopRequest: function test_onStopR(request, ctx, 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.asyncOpen2(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..78e84a8710 --- /dev/null +++ b/netwerk/test/unit/test_getHost.js @@ -0,0 +1,68 @@ +// Test getLocalHost/getLocalPort and getRemoteHost/getRemotePort. + +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +var httpserver = new HttpServer(); +httpserver.start(-1); +const PORT = httpserver.identity.primaryPort; + +var gotOnStartRequest = false; + +function CheckGetHostListener() {} + +CheckGetHostListener.prototype = { + onStartRequest: function(request, context) { + dump("*** listener onStartRequest\n"); + + gotOnStartRequest = true; + + request.QueryInterface(Components.interfaces.nsIHttpChannelInternal); + try { + do_check_eq(request.localAddress, "127.0.0.1"); + do_check_eq(request.localPort > 0, true); + do_check_neq(request.localPort, PORT); + do_check_eq(request.remoteAddress, "127.0.0.1"); + do_check_eq(request.remotePort, PORT); + } catch (e) { + do_check_true(0, "Get local/remote host/port throws an error!"); + } + }, + + onStopRequest: function(request, context, statusCode) { + dump("*** listener onStopRequest\n"); + + do_check_eq(gotOnStartRequest, true); + httpserver.stop(do_test_finished); + }, + + QueryInterface: function(iid) { + if (iid.equals(Components.interfaces.nsIRequestObserver) || + iid.equals(Components.interfaces.nsISupports) + ) + return this; + throw Components.results.NS_NOINTERFACE; + }, +} + +function make_channel(url) { + return NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true + }).QueryInterface(Components.interfaces.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.asyncOpen2(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..3287fae664 --- /dev/null +++ b/netwerk/test/unit/test_gre_resources.js @@ -0,0 +1,31 @@ +// test that things that are expected to be in gre-resources are still there +Cu.import("resource://gre/modules/NetUtil.jsm"); + +var ios = Cc["@mozilla.org/network/io-service;1"]. getService(Ci.nsIIOService); + +function wrapInputStream(input) +{ + var nsIScriptableInputStream = Components.interfaces.nsIScriptableInputStream; + var factory = Components.classes["@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.open2()); + do_check_true(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..8e4eaa4c76 --- /dev/null +++ b/netwerk/test/unit/test_gzipped_206.js @@ -0,0 +1,94 @@ +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + + +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) { + do_check_true(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, body.length); + response.finish(); +} + +function continue_test(request, data) { + do_check_eq(17, data.length); + var chan = make_channel("http://localhost:" + + httpserver.identity.primaryPort + "/cached/test.gz"); + chan.asyncOpen2(new ChannelListener(finish_test, null, CL_EXPECT_GZIP)); +} + +var enforcePref; + +function finish_test(request, data, ctx) { + do_check_eq(request.status, 0); + do_check_eq(data.length, responseBody.length); + for (var i = 0; i < data.length; ++i) { + do_check_eq(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.asyncOpen2(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..058f47bcbf --- /dev/null +++ b/netwerk/test/unit/test_head.js @@ -0,0 +1,150 @@ +// +// HTTP headers test +// + +// Note: sets Cc and Ci variables + +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +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"); + do_check_eq(setOK, "initial value"); + channel.setRequestHeader("ReplaceMe", "replaced", false); + setOK = channel.getRequestHeader("ReplaceMe"); + do_check_eq(setOK, "replaced"); + + channel.setRequestHeader("MergeMe", "foo1", true); + channel.setRequestHeader("MergeMe", "foo2", true); + channel.setRequestHeader("MergeMe", "foo3", true); + setOK = channel.getRequestHeader("MergeMe"); + do_check_eq(setOK, "foo1, foo2, foo3"); + + channel.setEmptyRequestHeader("Empty"); + setOK = channel.getRequestHeader("Empty"); + do_check_eq(setOK, ""); + + channel.setRequestHeader("ReplaceWithEmpty", "initial value", true); + setOK = channel.getRequestHeader("ReplaceWithEmpty"); + do_check_eq(setOK, "initial value"); + channel.setEmptyRequestHeader("ReplaceWithEmpty"); + setOK = channel.getRequestHeader("ReplaceWithEmpty"); + do_check_eq(setOK, ""); + + channel.setEmptyRequestHeader("MergeWithEmpty"); + setOK = channel.getRequestHeader("MergeWithEmpty"); + do_check_eq(setOK, ""); + channel.setRequestHeader("MergeWithEmpty", "foo", true); + setOK = channel.getRequestHeader("MergeWithEmpty"); + do_check_eq(setOK, "foo"); + + var uri = NetUtil.newURI("http://foo1.invalid:80"); + channel.referrer = uri; + do_check_true(channel.referrer.equals(uri)); + setOK = channel.getRequestHeader("Referer"); + do_check_eq(setOK, "http://foo1.invalid/"); + + uri = NetUtil.newURI("http://foo2.invalid:90/bar"); + channel.referrer = uri; + setOK = channel.getRequestHeader("Referer"); + do_check_eq(setOK, "http://foo2.invalid:90/bar"); + + // ChannelListener defined in head_channels.js + channel.asyncOpen2(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"); + do_check_eq(setOK, "replaced"); + setOK = metadata.getHeader("MergeMe"); + do_check_eq(setOK, "foo1, foo2, foo3"); + setOK = metadata.getHeader("Empty"); + do_check_eq(setOK, ""); + setOK = metadata.getHeader("ReplaceWithEmpty"); + do_check_eq(setOK, ""); + setOK = metadata.getHeader("MergeWithEmpty"); + do_check_eq(setOK, "foo"); + setOK = metadata.getHeader("Referer"); + do_check_eq(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"); } + + do_check_eq(channel.responseStatus, 200); + do_check_eq(channel.responseStatusText, "OK"); + do_check_true(channel.requestSucceeded); + + var response = channel.getResponseHeader("httpdMerge"); + do_check_eq(response, "bar1,bar2,bar3"); + channel.setResponseHeader("httpdMerge", "bar", true); + do_check_eq(channel.getResponseHeader("httpdMerge"), "bar1,bar2,bar3, bar"); + + response = channel.getResponseHeader("Proxy-Authenticate"); + do_check_eq(response, "line 1\nline 2\nline 3"); + + channel.contentCharset = "UTF-8"; + do_check_eq(channel.contentCharset, "UTF-8"); + do_check_eq(channel.contentType, "text/plain"); + do_check_eq(channel.contentLength, httpbody.length); + do_check_eq(data, httpbody); + + httpserver.stop(do_test_finished); + if (dbg) { print("============== checkRequestResponse: out"); } +} 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..63ea2552a3 --- /dev/null +++ b/netwerk/test/unit/test_header_Accept-Language.js @@ -0,0 +1,91 @@ +// +// HTTP Accept-Language header test +// + +Cu.import("resource://gre/modules/NetUtil.jsm"); + +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 acceptedLanguage, qualityValue; + + try { + // The q-value must conform to the definition in HTTP/1.1 Section 3.9. + [_, acceptedLanguage, 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. + do_check_eq(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) { + do_check_true(qualityValue.length == 3); + + decimalPlaces = 1; + } else { + do_check_true(qualityValue.length >= 3); + do_check_true(qualityValue.length <= 4); + + decimalPlaces = 2; + } + + // All the other languages should have an evenly-spaced quality value. + do_check_eq(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..3ed901ab56 --- /dev/null +++ b/netwerk/test/unit/test_header_Accept-Language_case.js @@ -0,0 +1,47 @@ +Cu.import("resource://gre/modules/NetUtil.jsm"); + +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"], + ]; + + 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, "", null); + let chan = NetUtil.newChannel({ + uri: uri, + loadUsingSystemPrincipal: true + }); + chan.QueryInterface(Ci.nsIHttpChannel); + return chan; +} diff --git a/netwerk/test/unit/test_headers.js b/netwerk/test/unit/test_headers.js new file mode 100644 index 0000000000..d9fecc11e9 --- /dev/null +++ b/netwerk/test/unit/test_headers.js @@ -0,0 +1,186 @@ +// +// 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 + +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 + +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +XPCOMUtils.defineLazyGetter(this, "URL", function() { + return "http://localhost:" + httpserver.identity.primaryPort; +}); + +var httpserver = new HttpServer(); +var index = 0; +var nextTest = firstTest; +var test_flags = new Array(); +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 (eval("handler" + nextTest) == undefined) + do_throw("handler" + nextTest + " undefined!"); + if (eval("completeTest" + nextTest) == undefined) + do_throw("completeTest" + nextTest + " undefined!"); + + run_test_number(nextTest); +} + +function run_test_number(num) +{ + testPath = testPathBase + num; + httpserver.registerPathHandler(testPath, eval("handler" + num)); + + var channel = setupChannel(testPath); + flags = test_flags[num]; // OK if flags undefined for test + channel.asyncOpen2(new ChannelListener(eval("completeTest" + num), + channel, flags)); +} + +function setupChannel(url) +{ + var chan = NetUtil.newChannel({ + uri: URL + url, + loadUsingSystemPrincipal: true + }); + var httpChan = chan.QueryInterface(Components.interfaces.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); + var body = "foo"; +} + +function completeTest1(request, data, ctx) +{ + try { + var chan = request.QueryInterface(Ci.nsIChannel); + do_check_eq(chan.contentDisposition, chan.DISPOSITION_ATTACHMENT); + do_check_eq(chan.contentDispositionFilename, "foo"); + do_check_eq(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); + do_check_eq(chan.contentDisposition, chan.DISPOSITION_ATTACHMENT); + do_check_eq(chan.contentDispositionHeader, "attachment"); + + filename = chan.contentDispositionFilename; // should barf + do_throw("Should have failed getting Content-Disposition filename"); + } catch (ex) { + do_print("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); + do_check_eq(chan.contentDisposition, chan.DISPOSITION_ATTACHMENT); + do_check_eq(chan.contentDispositionHeader, "attachment; filename="); + + filename = chan.contentDispositionFilename; // should barf + do_throw("Should have failed getting Content-Disposition filename"); + } catch (ex) { + do_print("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); + do_check_eq(chan.contentDisposition, chan.DISPOSITION_INLINE); + do_check_eq(chan.contentDispositionHeader, "inline"); + + filename = chan.contentDispositionFilename; // should barf + do_throw("Should have failed getting Content-Disposition filename"); + } catch (ex) { + do_print("correctly ate exception"); + } + runNextTest(); +} diff --git a/netwerk/test/unit/test_http2.js b/netwerk/test/unit/test_http2.js new file mode 100644 index 0000000000..3fefe707e7 --- /dev/null +++ b/netwerk/test/unit/test_http2.js @@ -0,0 +1,1119 @@ +// test HTTP/2 + +var Ci = Components.interfaces; +var Cc = Components.classes; + +// 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, ctx) { + 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!"); + } + + do_check_true(request instanceof Components.interfaces.nsIHttpChannel); + do_check_eq(request.requestSucceeded, this.shouldSucceed); + if (this.shouldSucceed) { + do_check_eq(request.responseStatus, 200); + } + }, + + onDataAvailable: function testOnDataAvailable(request, ctx, stream, off, cnt) { + this.onDataAvailableFired = true; + this.isHttp2Connection = checkIsHttp2(request); + this.accum += cnt; + read_stream(stream, cnt); + }, + + onStopRequest: function testOnStopRequest(request, ctx, status) { + do_check_true(this.onStartRequestFired); + if (this.expected != -1) { + do_check_eq(this.accum, this.expected); + } + if (this.shouldSucceed) { + do_check_true(Components.isSuccessCode(status)); + do_check_true(this.onDataAvailableFired); + do_check_true(this.isHttp2Connection == this.shouldBeHttp2); + } else { + do_check_false(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) { + do_check_neq(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, ctx, 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, ctx, status) { + do_check_true(this.onStartRequestFired); + do_check_true(this.onDataAvailableFired); + do_check_true(this.isHttp2Connection); + do_check_true(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, ctx, stream, off, cnt) { + this.onDataAvailableFired = true; + this.isHttp2Connection = checkIsHttp2(request); + var hvalue = request.getResponseHeader(this.name); + do_check_neq(hvalue, ""); + this.callback(hvalue); + read_stream(stream, cnt); +}; + +var Http2PushListener = function() {}; + +Http2PushListener.prototype = new Http2CheckListener(); + +Http2PushListener.prototype.onDataAvailable = function(request, ctx, 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") { + do_check_eq(request.getResponseHeader("pushed"), "yes"); + } + 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++) { + do_check_eq(getHeader(headerPrefix + 1), headerText); + } +} + +var Http2ContinuedHeaderListener = function() {}; + +Http2ContinuedHeaderListener.prototype = new Http2CheckListener(); + +Http2ContinuedHeaderListener.prototype.onStopsLeft = 2; + +Http2ContinuedHeaderListener.prototype.QueryInterface = function (aIID) { + if (aIID.equals(Ci.nsIHttpPushListener) || + aIID.equals(Ci.nsIStreamListener)) + return this; + throw Components.results.NS_ERROR_NO_INTERFACE; +}; + +Http2ContinuedHeaderListener.prototype.getInterface = function(aIID) { + return this.QueryInterface(aIID); +}; + +Http2ContinuedHeaderListener.prototype.onDataAvailable = function (request, ctx, 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, ctx, status) { + do_check_true(this.onStartRequestFired); + do_check_true(Components.isSuccessCode(status)); + do_check_true(this.onDataAvailableFired); + do_check_true(this.isHttp2Connection); + + --this.onStopsLeft; + if (this.onStopsLeft === 0) { + run_next_test(); + do_test_finished(); + } +}; + +Http2ContinuedHeaderListener.prototype.onPush = function(associatedChannel, pushChannel) { + do_check_eq(associatedChannel.originalURI.spec, "https://localhost:" + serverPort + "/continuedheaders"); + do_check_eq(pushChannel.getRequestHeader("x-pushed-request"), "true"); + checkContinuedHeaders(pushChannel.getRequestHeader, "X-Push-Test-Header-", pushHdrTxt); + + pushChannel.asyncOpen2(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, ctx, 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 + do_check_eq(bigListenerMD5, request.getResponseHeader("X-Expected-MD5")); +}; + +Http2BigListener.prototype.onStopRequest = function(request, ctx, status) { + do_check_true(this.onStartRequestFired); + do_check_true(this.onDataAvailableFired); + do_check_true(this.isHttp2Connection); + + // Don't want to flood output, so don't use do_check_eq + do_check_true(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, ctx, stream, off, cnt) { + this.onDataAvailableFired = true; + this.isHttp2Connection = checkIsHttp2(request); + this.count += cnt; + read_stream(stream, cnt); +}; + +Http2HugeSuspendedListener.prototype.onStopRequest = function(request, ctx, status) { + do_check_true(this.onStartRequestFired); + do_check_true(this.onDataAvailableFired); + do_check_true(this.isHttp2Connection); + do_check_eq(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, ctx, stream, off, cnt) { + this.onDataAvailableFired = true; + this.isHttp2Connection = checkIsHttp2(request); + read_stream(stream, cnt); + do_check_eq(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, ctx) { + this.onStartRequestFired = true; + if (!Components.isSuccessCode(request.status)) + do_throw("Channel should have a success code! (" + request.status + ")"); + + do_check_true(request instanceof Components.interfaces.nsIHttpChannel); + do_check_eq(request.responseStatus, 200); + do_check_eq(request.requestSucceeded, true); + }, + + onDataAvailable: function testOnDataAvailable(request, ctx, stream, off, cnt) { + this.onDataAvailableFired = true; + this.isHttp2Connection = checkIsHttp2(request); + read_stream(stream, cnt); + }, + + onStopRequest: function testOnStopRequest(request, ctx, status) { + do_check_true(this.onStartRequestFired); + do_check_true(Components.isSuccessCode(status)); + do_check_true(this.onDataAvailableFired); + do_check_true(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.asyncOpen2(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.asyncOpen2(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.asyncOpen2(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.asyncOpen2(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.asyncOpen2(listener); +} + +// Support for making sure XHR works over SPDY +function checkXhr(xhr) { + if (xhr.readyState != 4) { + return; + } + + do_check_eq(xhr.status, 200); + do_check_eq(checkIsHttp2(xhr), true); + run_next_test(); + do_test_finished(); +} + +// Fires off an XHR request over h2 +function test_http2_xhr() { + var req = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"] + .createInstance(Ci.nsIXMLHttpRequest); + req.open("GET", "https://localhost:" + serverPort + "/", true); + req.addEventListener("readystatechange", function (evt) { checkXhr(req); }, + false); + 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, ctx, status) { + this.count++; + do_check_true(this.isHttp2Connection); + if (this.recvdHdr > 0) { + do_check_eq(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].asyncOpen2(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].asyncOpen2(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.asyncOpen2(listener1); + chan2.asyncOpen2(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) { + do_check_eq(received_hvalue, hvalue); + }); + chan.asyncOpen2(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(); + do_check_eq(cookiesReceived.length, cookiesSent.length); + cookiesReceived.forEach(function(cookieReceived, index) { + do_check_eq(cookiesSent[index], cookieReceived) + }); + }); + chan.asyncOpen2(listener); +} + +function test_http2_push1() { + var chan = makeChan("https://localhost:" + serverPort + "/push"); + chan.loadGroup = loadGroup; + var listener = new Http2PushListener(); + chan.asyncOpen2(listener); +} + +function test_http2_push2() { + var chan = makeChan("https://localhost:" + serverPort + "/push.js"); + chan.loadGroup = loadGroup; + var listener = new Http2PushListener(); + chan.asyncOpen2(listener); +} + +function test_http2_push3() { + var chan = makeChan("https://localhost:" + serverPort + "/push2"); + chan.loadGroup = loadGroup; + var listener = new Http2PushListener(); + chan.asyncOpen2(listener); +} + +function test_http2_push4() { + var chan = makeChan("https://localhost:" + serverPort + "/push2.js"); + chan.loadGroup = loadGroup; + var listener = new Http2PushListener(); + chan.asyncOpen2(listener); +} + +function test_http2_push5() { + var chan = makeChan("https://localhost:" + serverPort + "/push5"); + chan.loadGroup = loadGroup; + var listener = new Http2PushListener(); + chan.asyncOpen2(listener); +} + +function test_http2_push6() { + var chan = makeChan("https://localhost:" + serverPort + "/push5.js"); + chan.loadGroup = loadGroup; + var listener = new Http2PushListener(); + chan.asyncOpen2(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.asyncOpen2(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.asyncOpen2(listener); +} + +function test_http2_huge_suspended() { + var chan = makeChan("https://localhost:" + serverPort + "/huge"); + var listener = new Http2HugeSuspendedListener(); + chan.asyncOpen2(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.asyncOpen2(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"); +} + +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +var httpserv = null; +var httpserv2 = null; +var ios = Components.classes["@mozilla.org/network/io-service;1"] + .getService(Components.interfaces.nsIIOService); + +var altsvcClientListener = { + onStartRequest: function test_onStartR(request, ctx) { + do_check_eq(request.status, Components.results.NS_OK); + }, + + onDataAvailable: function test_ODA(request, cx, stream, offset, cnt) { + read_stream(stream, cnt); + }, + + onStopRequest: function test_onStopR(request, ctx, status) { + var isHttp2Connection = checkIsHttp2(request.QueryInterface(Components.interfaces.nsIHttpChannel)); + if (!isHttp2Connection) { + dump("/altsvc1 not over h2 yet - retry\n"); + var chan = makeChan("http://foo.example.com:" + httpserv.identity.primaryPort + "/altsvc1") + .QueryInterface(Components.interfaces.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.asyncOpen2(altsvcClientListener); + } else { + do_check_true(isHttp2Connection); + var chan = makeChan("http://foo.example.com:" + httpserv2.identity.primaryPort + "/altsvc2") + .QueryInterface(Components.interfaces.nsIHttpChannel); + chan.loadFlags = Ci.nsIRequest.LOAD_BYPASS_CACHE; + chan.asyncOpen2(altsvcClientListener2); + } + } +}; + +var altsvcClientListener2 = { + onStartRequest: function test_onStartR(request, ctx) { + do_check_eq(request.status, Components.results.NS_OK); + }, + + onDataAvailable: function test_ODA(request, cx, stream, offset, cnt) { + read_stream(stream, cnt); + }, + + onStopRequest: function test_onStopR(request, ctx, status) { + var isHttp2Connection = checkIsHttp2(request.QueryInterface(Components.interfaces.nsIHttpChannel)); + if (!isHttp2Connection) { + dump("/altsvc2 not over h2 yet - retry\n"); + var chan = makeChan("http://foo.example.com:" + httpserv2.identity.primaryPort + "/altsvc2") + .QueryInterface(Components.interfaces.nsIHttpChannel); + chan.loadFlags = Ci.nsIRequest.LOAD_BYPASS_CACHE; + chan.asyncOpen2(altsvcClientListener2); + } else { + do_check_true(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 + '": { "tls-ports": [' + serverPort + '] }}'; + 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 + '": { "tls-ports": [' + serverPort + '] }}'; + response.bodyOutputStream.write(body, body.length); +} +function test_http2_altsvc() { + var chan = makeChan("http://foo.example.com:" + httpserv.identity.primaryPort + "/altsvc1") + .QueryInterface(Components.interfaces.nsIHttpChannel); + chan.asyncOpen2(altsvcClientListener); +} + +var Http2PushApiListener = function() {}; + +Http2PushApiListener.prototype = { + checksPending: 9, // 4 onDataAvailable and 5 onStop + + getInterface: function(aIID) { + return this.QueryInterface(aIID); + }, + + QueryInterface: function(aIID) { + if (aIID.equals(Ci.nsIHttpPushListener) || + aIID.equals(Ci.nsIStreamListener)) + return this; + throw Components.results.NS_ERROR_NO_INTERFACE; + }, + + // nsIHttpPushListener + onPush: function onPush(associatedChannel, pushChannel) { + do_check_eq(associatedChannel.originalURI.spec, "https://localhost:" + serverPort + "/pushapi1"); + do_check_eq (pushChannel.getRequestHeader("x-pushed-request"), "true"); + + pushChannel.asyncOpen2(this); + if (pushChannel.originalURI.spec == "https://localhost:" + serverPort + "/pushapi1/2") { + pushChannel.cancel(Components.results.NS_ERROR_ABORT); + } + }, + + // normal Channel listeners + onStartRequest: function pushAPIOnStart(request, ctx) { + }, + + onDataAvailable: function pushAPIOnDataAvailable(request, ctx, stream, offset, cnt) { + do_check_neq(request.originalURI.spec, "https://localhost:" + serverPort + "/pushapi1/2"); + + var data = read_stream(stream, cnt); + + if (request.originalURI.spec == "https://localhost:" + serverPort + "/pushapi1") { + do_check_eq(data[0], '0'); + --this.checksPending; + } else if (request.originalURI.spec == "https://localhost:" + serverPort + "/pushapi1/1") { + do_check_eq(data[0], '1'); + --this.checksPending; // twice + } else if (request.originalURI.spec == "https://localhost:" + serverPort + "/pushapi1/3") { + do_check_eq(data[0], '3'); + --this.checksPending; + } else { + do_check_eq(true, false); + } + }, + + onStopRequest: function test_onStopR(request, ctx, status) { + if (request.originalURI.spec == "https://localhost:" + serverPort + "/pushapi1/2") { + do_check_eq(request.status, Components.results.NS_ERROR_ABORT); + } else { + do_check_eq(request.status, Components.results.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 + +function test_http2_pushapi_1() { + var chan = makeChan("https://localhost:" + serverPort + "/pushapi1"); + chan.loadGroup = loadGroup; + var listener = new Http2PushApiListener(); + chan.notificationCallbacks = listener; + chan.asyncOpen2(listener); +} + +var WrongSuiteListener = function() {}; +WrongSuiteListener.prototype = new Http2CheckListener(); +WrongSuiteListener.prototype.shouldBeHttp2 = false; +WrongSuiteListener.prototype.onStopRequest = function(request, ctx, status) { + prefs.setBoolPref("security.ssl3.ecdhe_rsa_aes_128_gcm_sha256", true); + Http2CheckListener.prototype.onStopRequest.call(this); +}; + +// test that we use h1 without the mandatory cipher suite available +function test_http2_wrongsuite() { + 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(); + chan.asyncOpen2(listener); +} + +function test_http2_h11required_stream() { + var chan = makeChan("https://localhost:" + serverPort + "/h11required_stream"); + var listener = new Http2CheckListener(); + listener.shouldBeHttp2 = false; + chan.asyncOpen2(listener); +} + +function H11RequiredSessionListener () { } +H11RequiredSessionListener.prototype = new Http2CheckListener(); + +H11RequiredSessionListener.prototype.onStopRequest = function (request, ctx, status) { + var streamReused = request.getResponseHeader("X-H11Required-Stream-Ok"); + do_check_eq(streamReused, "yes"); + + do_check_true(this.onStartRequestFired); + do_check_true(this.onDataAvailableFired); + do_check_true(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.asyncOpen2(listener); +} + +function test_http2_retry_rst() { + var chan = makeChan("https://localhost:" + serverPort + "/rstonce"); + var listener = new Http2CheckListener(); + chan.asyncOpen2(listener); +} + +function test_http2_continuations() { + var chan = makeChan("https://localhost:" + serverPort + "/continuedheaders"); + chan.loadGroup = loadGroup; + var listener = new Http2ContinuedHeaderListener(); + chan.notificationCallbacks = listener; + chan.asyncOpen2(listener); +} + +function Http2IllegalHpackValidationListener() { } +Http2IllegalHpackValidationListener.prototype = new Http2CheckListener(); +Http2IllegalHpackValidationListener.prototype.shouldGoAway = false; + +Http2IllegalHpackValidationListener.prototype.onStopRequest = function (request, ctx, status) { + var wentAway = (request.getResponseHeader('X-Did-Goaway') === 'yes'); + do_check_eq(wentAway, this.shouldGoAway); + + do_check_true(this.onStartRequestFired); + do_check_true(this.onDataAvailableFired); + do_check_true(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, ctx, status) { + var chan = makeChan("https://localhost:" + serverPort + "/illegalhpack_validate"); + var listener = new Http2IllegalHpackValidationListener(); + listener.shouldGoAway = this.shouldGoAway; + chan.asyncOpen2(listener); +}; + +function test_http2_illegalhpacksoft() { + var chan = makeChan("https://localhost:" + serverPort + "/illegalhpacksoft"); + var listener = new Http2IllegalHpackListener(); + listener.shouldGoAway = false; + listener.shouldSucceed = false; + chan.asyncOpen2(listener); +} + +function test_http2_illegalhpackhard() { + var chan = makeChan("https://localhost:" + serverPort + "/illegalhpackhard"); + var listener = new Http2IllegalHpackListener(); + listener.shouldGoAway = true; + listener.shouldSucceed = false; + chan.asyncOpen2(listener); +} + +function test_http2_folded_header() { + var chan = makeChan("https://localhost:" + serverPort + "/foldedheader"); + chan.loadGroup = loadGroup; + var listener = new Http2CheckListener(); + listener.shouldSucceed = false; + chan.asyncOpen2(listener); +} + +function test_http2_empty_data() { + var chan = makeChan("https://localhost:" + serverPort + "/emptydata"); + var listener = new Http2CheckListener(); + chan.asyncOpen2(listener); +} + +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); +} + +// 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 + // 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 + + // 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(); + } +} + +// Support for making sure we can talk to the invalid cert the server presents +var CertOverrideListener = function(host, port, bits) { + this.host = host; + if (port) { + this.port = port; + } + this.bits = bits; +}; + +CertOverrideListener.prototype = { + host: null, + port: -1, + bits: null, + + getInterface: function(aIID) { + return this.QueryInterface(aIID); + }, + + QueryInterface: function(aIID) { + if (aIID.equals(Ci.nsIBadCertListener2) || + aIID.equals(Ci.nsIInterfaceRequestor) || + aIID.equals(Ci.nsISupports)) + return this; + throw Components.results.NS_ERROR_NO_INTERFACE; + }, + + notifyCertProblem: function(socketInfo, sslStatus, targetHost) { + var cert = sslStatus.QueryInterface(Ci.nsISSLStatus).serverCert; + var cos = Cc["@mozilla.org/security/certoverride;1"]. + getService(Ci.nsICertOverrideService); + cos.rememberValidityOverride(this.host, this.port, cert, this.bits, false); + dump("Certificate Override in place\n"); + return true; + }, +}; + +function addCertOverride(host, port, bits) { + var req = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"] + .createInstance(Ci.nsIXMLHttpRequest); + try { + var url; + if (port) { + url = "https://" + host + ":" + port + "/"; + } else { + url = "https://" + host + "/"; + } + req.open("GET", url, false); + req.channel.notificationCallbacks = new CertOverrideListener(host, port, bits); + req.send(null); + } catch (e) { + // This will fail since the server is not trusted yet + } +} + +var prefs; +var spdypref; +var spdypush; +var http2pref; +var tlspref; +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.spdy.enforce-tls-profile", tlspref); + prefs.setBoolPref("network.http.altsvc.enabled", altsvcpref1); + prefs.setBoolPref("network.http.altsvc.oe", altsvcpref2); + prefs.clearUserPref("network.dns.localDomains"); +} + +function run_test() { + var env = Cc["@mozilla.org/process/environment;1"].getService(Ci.nsIEnvironment); + serverPort = env.get("MOZHTTP2_PORT"); + do_check_neq(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 CA.cert.der + // 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, "CA.cert.der", "CTu,u,u"); + + addCertOverride("localhost", serverPort, + Ci.nsICertOverrideService.ERROR_UNTRUSTED | + Ci.nsICertOverrideService.ERROR_MISMATCH | + Ci.nsICertOverrideService.ERROR_TIME); + + // 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"); + tlspref = prefs.getBoolPref("network.http.spdy.enforce-tls-profile"); + 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.v3-1", true); + prefs.setBoolPref("network.http.spdy.allow-push", true); + prefs.setBoolPref("network.http.spdy.enabled.http2", true); + prefs.setBoolPref("network.http.spdy.enforce-tls-profile", false); + 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"); + + 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(); +} + +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 der = readFile(certFile); + certdb.addCert(der, trustString, null); +} diff --git a/netwerk/test/unit/test_httpResponseTimeout.js b/netwerk/test/unit/test_httpResponseTimeout.js new file mode 100644 index 0000000000..3a40b0a089 --- /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"; + +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +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: function (request, ctx) { + }, + + onDataAvailable: function (request, ctx, stream) { + }, + + onStopRequest: function (request, ctx, status) { + if (this.expectResponse) { + do_check_eq(status, Cr.NS_OK); + } else { + do_check_eq(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.asyncOpen2(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); + do_register_cleanup(function() { + prefService.setBoolPref(kShortLivedKeepalivePref, true); + }); + } + if (prefService.getBoolPref(kLongLivedKeepalivePref)) { + prefService.setBoolPref(kLongLivedKeepalivePref, false); + do_register_cleanup(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 + "/"; + do_print("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(); + }); + }); + do_register_cleanup(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..586e064aac --- /dev/null +++ b/netwerk/test/unit/test_http_headers.js @@ -0,0 +1,70 @@ +Cu.import("resource://gre/modules/NetUtil.jsm"); + +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"); + } + do_check_eq(chanValue, value); +} + +function run_test() { + + var chan = NetUtil.newChannel ({ + uri: "http://www.mozilla.org/", + loadUsingSystemPrincipal: true + }).QueryInterface(Components.interfaces.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_httpauth.js b/netwerk/test/unit/test_httpauth.js new file mode 100644 index 0000000000..65846710e1 --- /dev/null +++ b/netwerk/test/unit/test_httpauth.js @@ -0,0 +1,99 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// This test makes sure the HTTP authenticated sessions are correctly cleared +// when entering and leaving the private browsing mode. + +Components.utils.import("resource://gre/modules/Services.jsm"); + +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); + do_check_eq(domain.value, kDomain); + do_check_eq(user.value, kUser); + do_check_eq(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) { + do_check_eq(domain.value, kEmpty); + do_check_eq(user.value, kEmpty); + do_check_eq(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); + do_check_eq(domain.value, kDomain); + do_check_eq(user.value, kUser2); + do_check_eq(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) { + do_check_eq(domain.value, kEmpty); + do_check_eq(user.value, kEmpty); + do_check_eq(pass.value, kEmpty); + } + + // simulate leaving private browsing mode + Services.obs.notifyObservers(null, "last-pb-context-exited", null); + + // 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) { + do_check_eq(domain.value, kEmpty); + do_check_eq(user.value, kEmpty); + do_check_eq(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) { + do_check_eq(domain.value, kEmpty); + do_check_eq(user.value, kEmpty); + do_check_eq(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..49188ca1ee --- /dev/null +++ b/netwerk/test/unit/test_httpcancel.js @@ -0,0 +1,114 @@ +// 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: + +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +var ios = Components.classes["@mozilla.org/network/io-service;1"] + .getService(Components.interfaces.nsIIOService); +var observer = { + QueryInterface: function eventsink_qi(iid) { + if (iid.equals(Components.interfaces.nsISupports) || + iid.equals(Components.interfaces.nsIObserver)) + return this; + throw Components.results.NS_ERROR_NO_INTERFACE; + }, + + observe: function(subject, topic, data) { + subject = subject.QueryInterface(Components.interfaces.nsIRequest); + subject.cancel(Components.results.NS_BINDING_ABORTED); + + // ENSURE_CALLED_BEFORE_CONNECT: setting values should still work + try { + subject.QueryInterface(Components.interfaces.nsIHttpChannel); + currentReferrer = subject.getRequestHeader("Referer"); + do_check_eq(currentReferrer, "http://site1.com/"); + var uri = ios.newURI("http://site2.com", null, null); + subject.referrer = uri; + } catch (ex) { + do_throw("Exception: " + ex); + } + + var obs = Components.classes["@mozilla.org/observer-service;1"].getService(); + obs = obs.QueryInterface(Components.interfaces.nsIObserverService); + obs.removeObserver(observer, "http-on-modify-request"); + } +}; + +var listener = { + onStartRequest: function test_onStartR(request, ctx) { + do_check_eq(request.status, Components.results.NS_BINDING_ABORTED); + + // ENSURE_CALLED_BEFORE_CONNECT: setting referrer should now fail + try { + request.QueryInterface(Components.interfaces.nsIHttpChannel); + currentReferrer = request.getRequestHeader("Referer"); + do_check_eq(currentReferrer, "http://site2.com/"); + var uri = ios.newURI("http://site3.com/", null, null); + + // Need to set NECKO_ERRORS_ARE_FATAL=0 else we'll abort process + var env = Components.classes["@mozilla.org/process/environment;1"]. + getService(Components.interfaces.nsIEnvironment); + env.set("NECKO_ERRORS_ARE_FATAL", "0"); + // we expect setting referrer to fail + try { + request.referrer = 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, ctx, status) { + httpserv.stop(do_test_finished); + } +}; + +function makeChan(url) { + var chan = NetUtil.newChannel({uri: url, loadUsingSystemPrincipal: true}) + .QueryInterface(Components.interfaces.nsIHttpChannel); + + // ENSURE_CALLED_BEFORE_CONNECT: set original value + var uri = ios.newURI("http://site1.com", null, null); + chan.referrer = uri; + + return chan; +} + +var httpserv = null; + +function execute_test() { + var chan = makeChan("http://localhost:" + + httpserv.identity.primaryPort + "/failtest"); + + var obs = Components.classes["@mozilla.org/observer-service;1"].getService(); + obs = obs.QueryInterface(Components.interfaces.nsIObserverService); + obs.addObserver(observer, "http-on-modify-request", false); + + chan.asyncOpen2(listener); +} + +function run_test() { + httpserv = new HttpServer(); + httpserv.registerPathHandler("/failtest", failtest); + httpserv.start(-1); + + execute_test(); + + do_test_pending(); +} + +// PATHS + +// /failtest +function failtest(metadata, response) { + do_throw("This should not be reached"); +} diff --git a/netwerk/test/unit/test_httpsuspend.js b/netwerk/test/unit/test_httpsuspend.js new file mode 100644 index 0000000000..7d0b1326f6 --- /dev/null +++ b/netwerk/test/unit/test_httpsuspend.js @@ -0,0 +1,80 @@ +// This file ensures that suspending a channel directly after opening it +// suspends future notifications correctly. + +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +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: function(iid) { + if (iid.equals(Components.interfaces.nsIStreamListener) || + iid.equals(Components.interfaces.nsIRequestObserver) || + iid.equals(Components.interfaces.nsISupports)) + return this; + throw Components.results.NS_ERROR_NO_INTERFACE; + }, + + onStartRequest: function(request, ctx) { + 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: function(request, context, stream, offset, count) { + do_check_true(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: function(request, ctx, status) { + do_check_true(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.asyncOpen2(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..5ca0173bb2 --- /dev/null +++ b/netwerk/test/unit/test_idn_blacklist.js @@ -0,0 +1,170 @@ +// Test that URLs containing characters in the IDN blacklist are +// always displayed as punycode +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; + try { + oldWhitelistCom = pbi.getBoolPref("network.IDN.whitelist.com"); + } catch(e) { + oldWhitelistCom = 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..06a53032b4 --- /dev/null +++ b/netwerk/test/unit/test_idn_urls.js @@ -0,0 +1,345 @@ +// Test algorithm for unicode display of IDNA URL (bug 722299) +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] +]; + + +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; + try { + oldWhitelistCom = pbi.getBoolPref("network.IDN.whitelist.com"); + } catch(e) { + oldWhitelistCom = 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--") { + // test convertToDisplayIDN with a Unicode URL and with a + // Punycode URL if we have one + do_check_eq(escape(result), + expectedUnicode ? escape(URL) : escape(punycodeURL)); + + result = idnService.convertToDisplayIDN(punycodeURL, isASCII); + do_check_eq(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 + do_check_eq(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..9221a0e605 --- /dev/null +++ b/netwerk/test/unit/test_idna2008.js @@ -0,0 +1,60 @@ +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\200cn", + // U+200D ZERO WIDTH JOINER in Latin script + "p\200dq", +]; + +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 = Components.classes["@mozilla.org/network/idn-service;1"] + .getService(Components.interfaces.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..e6d659857b --- /dev/null +++ b/netwerk/test/unit/test_idnservice.js @@ -0,0 +1,25 @@ +// Tests nsIIDNService + +var reference = [ + // The 3rd element indicates whether the second element + // is ACE-encoded + ["asciihost", "asciihost", false], + ["b\u00FCcher", "xn--bcher-kva", true] + ]; + +function run_test() { + var idnService = Components.classes["@mozilla.org/network/idn-service;1"] + .getService(Components.interfaces.nsIIDNService); + + 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) + do_check_eq(idnService.convertUTF8toACE(reference[i][0]), reference[i][1]); + do_check_eq(idnService.convertUTF8toACE(reference[i][1]), reference[i][1]); + do_check_eq(idnService.convertACEtoUTF8(reference[i][1]), reference[i][0]); + do_check_eq(idnService.isACE(reference[i][1]), reference[i][2]); + } +} diff --git a/netwerk/test/unit/test_immutable.js b/netwerk/test/unit/test_immutable.js new file mode 100644 index 0000000000..d3438bbe11 --- /dev/null +++ b/netwerk/test/unit/test_immutable.js @@ -0,0 +1,180 @@ +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +var prefs; +var spdypref; +var http2pref; +var tlspref; +var origin; + +function run_test() { + var env = Cc["@mozilla.org/process/environment;1"].getService(Ci.nsIEnvironment); + var h2Port = env.get("MOZHTTP2_PORT"); + do_check_neq(h2Port, null); + do_check_neq(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"); + tlspref = prefs.getBoolPref("network.http.spdy.enforce-tls-profile"); + + prefs.setBoolPref("network.http.spdy.enabled", true); + prefs.setBoolPref("network.http.spdy.enabled.http2", true); + prefs.setBoolPref("network.http.spdy.enforce-tls-profile", false); + prefs.setCharPref("network.dns.localDomains", "foo.example.com, bar.example.com"); + + // The moz-http2 cert is for foo.example.com and is signed by CA.cert.der + // 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, "CA.cert.der", "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.spdy.enforce-tls-profile", tlspref); + prefs.clearUserPref("network.dns.localDomains"); +} + +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 der = readFile(certFile); + certdb.addCert(der, trustString, null); +} + +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, ctx) { + do_check_true(request instanceof Components.interfaces.nsIHttpChannel); + + if (expectPass) { + if (!Components.isSuccessCode(request.status)) { + do_throw("Channel should have a success code! (" + request.status + ")"); + } + do_check_eq(request.responseStatus, 200); + } else { + do_check_eq(Components.isSuccessCode(request.status), false); + } + }, + + onDataAvailable: function testOnDataAvailable(request, ctx, stream, off, cnt) { + read_stream(stream, cnt); + }, + + onStopRequest: function testOnStopRequest(request, ctx, status) { + if (expectConditional) { + do_check_eq(request.getResponseHeader("x-conditional"), "true"); + } else { + try { do_check_neq(request.getResponseHeader("x-conditional"), "true"); } + catch (e) { do_check_true(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.asyncOpen2(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.asyncOpen2(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.asyncOpen2(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.asyncOpen2(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.asyncOpen2(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.asyncOpen2(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..7e81eb6968 --- /dev/null +++ b/netwerk/test/unit/test_inhibit_caching.js @@ -0,0 +1,76 @@ +Cu.import('resource://gre/modules/LoadContextInfo.jsm'); +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +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.asyncOpen2(new ChannelListener(check_first_response, null)); +} + +// Checks that we got the appropriate response +function check_first_response(request, buffer) { + request.QueryInterface(Ci.nsIHttpChannel); + do_check_eq(request.responseStatus, 200); + do_check_eq(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.asyncOpen2(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); + do_check_eq(request.responseStatus, 200); + do_check_eq(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_large_port.js b/netwerk/test/unit/test_large_port.js new file mode 100644 index 0000000000..d2480582f2 --- /dev/null +++ b/netwerk/test/unit/test_large_port.js @@ -0,0 +1,36 @@ +/* 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"; + +var Cc = Components.classes; +var Ci = Components.interfaces; + +const StandardURL = Components.Constructor("@mozilla.org/network/standard-url;1", + "nsIStandardURL", + "init"); +function run_test() +{ + // Bug 1301621 makes invalid ports throw + Assert.throws(() => { + new StandardURL(Ci.nsIStandardURL.URLTYPE_AUTHORITY, 65536, + "http://localhost", "UTF-8", null) + }, "invalid port during creation"); + let url = new StandardURL(Ci.nsIStandardURL.URLTYPE_AUTHORITY, 65535, + "http://localhost", "UTF-8", null) + .QueryInterface(Ci.nsIStandardURL) + + Assert.throws(() => { + url.setDefaultPort(65536); + }, "invalid port in setDefaultPort"); + Assert.throws(() => { + url.port = 65536; + }, "invalid port in port setter"); + + do_check_eq(url.QueryInterface(Ci.nsIURI).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.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_localstreams.js b/netwerk/test/unit/test_localstreams.js new file mode 100644 index 0000000000..8e926f5712 --- /dev/null +++ b/netwerk/test/unit/test_localstreams.js @@ -0,0 +1,87 @@ +// Tests bug 304414 +Cu.import("resource://gre/modules/NetUtil.jsm"); + +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. + do_check_eq(stream.isNonBlocking(), false); + + // Check that the stream is not buffered + do_check_eq(Components.classes["@mozilla.org/io-util;1"] + .getService(Components.interfaces.nsIIOUtil) + .inputStreamIsBuffered(stream), + false); + + // Wrap it in a binary stream (to avoid wrong results that + // scriptablestream would produce with binary content) + var binstream = Components.classes['@mozilla.org/binaryinputstream;1'] + .createInstance(Components.interfaces.nsIBinaryInputStream); + binstream.setInputStream(stream); + + var numread = 0; + for (;;) { + do_check_eq(stream.available(), binstream.available()); + var avail = stream.available(); + do_check_neq(avail, -1); + + // PR_UINT32_MAX and PR_INT32_MAX; the files we're testing with aren't that + // large. + do_check_neq(avail, Math.pow(2, 32) - 1); + do_check_neq(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 + var data = binstream.readByteArray(avail); + + numread += avail; + } + return numread; +} + +function stream_for_file(file) { + var str = Components.classes["@mozilla.org/network/file-input-stream;1"] + .createInstance(Components.interfaces.nsIFileInputStream); + str.init(file, PR_RDONLY, 0, 0); + return str; +} + +function stream_from_channel(file) { + var ios = Components.classes["@mozilla.org/network/io-service;1"] + .getService(Components.interfaces.nsIIOService); + var uri = ios.newFileURI(file); + return NetUtil.newChannel({ + uri: uri, + loadUsingSystemPrincipal: true + }).open2(); +} + +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; + do_check_eq(test_stream(stream_for_file(file)), len); + do_check_eq(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..f675a123fb --- /dev/null +++ b/netwerk/test/unit/test_mismatch_last-modified.js @@ -0,0 +1,154 @@ +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); +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: function(iid) { + if (iid.equals(Components.interfaces.nsIStreamListener) || + iid.equals(Components.interfaces.nsIRequestObserver) || + iid.equals(Components.interfaces.nsISupports)) + return this; + throw Components.results.NS_ERROR_NO_INTERFACE; + }, + + onStartRequest: function test_onStartR(request, ctx) {}, + + onDataAvailable: function test_ODA(request, cx, inputStream, + offset, count) { + var data = new BinaryInputStream(inputStream).readByteArray(count); + + do_check_eq(data[0], "B".charCodeAt(0)); + }, + + onStopRequest: function test_onStopR(request, ctx, 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: function(iid) { + if (iid.equals(Components.interfaces.nsIStreamListener) || + iid.equals(Components.interfaces.nsIRequestObserver) || + iid.equals(Components.interfaces.nsISupports)) + return this; + throw Components.results.NS_ERROR_NO_INTERFACE; + }, + + onStartRequest: function test_onStartR(request, ctx) {}, + + onDataAvailable: function test_ODA(request, cx, 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 + + do_check_eq(data[0], "A".charCodeAt(0)); + }, + + onStopRequest: function test_onStopR(request, ctx, status) { + var channel = request.QueryInterface(Ci.nsIHttpChannel); + var chan = NetUtil.newChannel({ + uri: "http://localhost:" + httpserver.identity.primaryPort + "/test1", + loadUsingSystemPrincipal: true + }); + chan.asyncOpen2(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: function(iid) { + if (iid.equals(Components.interfaces.nsIStreamListener) || + iid.equals(Components.interfaces.nsIRequestObserver) || + iid.equals(Components.interfaces.nsISupports)) + return this; + throw Components.results.NS_ERROR_NO_INTERFACE; + }, + + onStartRequest: function test_onStartR(request, ctx) {}, + + onDataAvailable: function test_ODA(request, cx, inputStream, + offset, count) { + var data = new BinaryInputStream(inputStream).readByteArray(count); + do_check_eq(data[0], "A".charCodeAt(0)); + }, + + onStopRequest: function test_onStopR(request, ctx, status) { + var channel = request.QueryInterface(Ci.nsIHttpChannel); + var chan = NetUtil.newChannel({ + uri: "http://localhost:" + httpserver.identity.primaryPort + "/test1", + loadUsingSystemPrincipal: true + }); + chan.asyncOpen2(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.asyncOpen2(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..075323d7ca --- /dev/null +++ b/netwerk/test/unit/test_mozTXTToHTMLConv.js @@ -0,0 +1,199 @@ +/* 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. + */ + +var Cc = Components.classes; +var Ci = Components.interfaces; + +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" + }, + // -- RFC2396E + { + input: "RFC2396E: <http://mozilla.org/> then", + url: "http://mozilla.org/" + }, + // -- abbreviated + { + input: "see www.mozilla.org maybe", + url: "http://www.mozilla.org" + }, + // -- freetext + { + input:"I mean http://www.mozilla.org/.", + url: "http://www.mozilla.org/" + }, + { + input:"you mean http://mozilla.org:80, right?", + url: "http://mozilla.org:80" + }, + { + input:"go to http://mozilla.org; then go home", + url: "http://mozilla.org" + }, + { + input:"http://mozilla.org! yay!", + url: "http://mozilla.org" + }, + { + input:"er, http://mozilla.com?", + url: "http://mozilla.com" + }, + { + input:"http://example.org- where things happen", + url: "http://example.org" + }, + { + input:"see http://mozilla.org: front page", + url: "http://mozilla.org" + }, + { + input:"'http://mozilla.org/': that's the url", + url: "http://mozilla.org/" + }, + { + input:"some special http://mozilla.org/?x=.,;!-:x", + url: "http://mozilla.org/?x=.,;!-:x" + }, + { + // escape & when producing html + input:"'http://example.org/?test=true&success=true': ok", + url: "http://example.org/?test=true&success=true" + }, + { + input: "bracket: http://localhost/[1] etc.", + url: "http://localhost/" + }, + { + input: "parenthesis: (http://localhost/) etc.", + url: "http://localhost/" + }, + { + input: "(thunderbird)http://mozilla.org/thunderbird", + url: "http://mozilla.org/thunderbird" + }, + { + input: "()http://mozilla.org", + url: "http://mozilla.org" + }, + { + input: "parenthesis included: http://kb.mozillazine.org/Performance_(Thunderbird) etc.", + url: "http://kb.mozillazine.org/Performance_(Thunderbird)" + }, + { + input: "parenthesis slash bracket: (http://localhost/)[1] etc.", + url: "http://localhost/" + }, + { + input: "parenthesis bracket: (http://example.org[1]) etc.", + url: "http://example.org" + }, + { + input: "ipv6 1: https://[1080::8:800:200C:417A]/foo?bar=x test", + url: "https://[1080::8:800:200C:417A]/foo?bar=x" + }, + { + input: "ipv6 2: http://[::ffff:127.0.0.1]/#yay test", + url: "http://[::ffff:127.0.0.1]/#yay" + }, + { + input: "ipv6 parenthesis port: (http://[2001:db8::1]:80/) test", + url: "http://[2001:db8::1]:80/" + }, + { + input: "test http://www.map.com/map.php?t=Nova_Scotia&markers=//Not_a_survey||description=plm2 test", + url: "http://www.map.com/map.php?t=Nova_Scotia&markers=//Not_a_survey||description=plm2" + } + ]; + + 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 + '"'; + } + + 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); + if (output.indexOf(link) == -1) + do_throw("Unexpected conversion by scanTXT: input=" + t.input + + ", output=" + output + ", link=" + link); + } + + 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..367615fff9 --- /dev/null +++ b/netwerk/test/unit/test_multipart_byteranges.js @@ -0,0 +1,113 @@ +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +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) +{ + do_check_eq(buffer, testData[testNum].data); + do_check_eq(request.QueryInterface(Ci.nsIChannel).contentType, + testData[testNum].type); + do_check_eq(request.QueryInterface(Ci.nsIByteRangeRequest).isByteRangeRequest, + testData[testNum].isByteRangeRequest); + do_check_eq(request.QueryInterface(Ci.nsIByteRangeRequest).startRange, + testData[testNum].startRange); + do_check_eq(request.QueryInterface(Ci.nsIByteRangeRequest).endRange, + testData[testNum].endRange); + if (++testNum == numTests) + httpserver.stop(do_test_finished); +} + +var multipartListener = { + _buffer: "", + + QueryInterface: function(iid) { + if (iid.equals(Components.interfaces.nsIStreamListener) || + iid.equals(Components.interfaces.nsIRequestObserver) || + iid.equals(Components.interfaces.nsISupports)) + return this; + throw Components.results.NS_ERROR_NO_INTERFACE; + }, + + onStartRequest: function(request, context) { + this._buffer = ""; + }, + + onDataAvailable: function(request, context, 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: function(request, context, 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.asyncOpen2(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..bab964ee8e --- /dev/null +++ b/netwerk/test/unit/test_multipart_streamconv-byte-by-byte.js @@ -0,0 +1,109 @@ +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +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\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.0'?><root/>", type: "text/xml" } + ]; + +function responseHandler(request, buffer) +{ + do_check_eq(buffer, testData[testNum].data); + do_check_eq(request.QueryInterface(Ci.nsIChannel).contentType, + testData[testNum].type); + if (++testNum == numTests) + httpserver.stop(do_test_finished); +} + +var multipartListener = { + _buffer: "", + _index: 0, + + QueryInterface: function(iid) { + if (iid.equals(Components.interfaces.nsIStreamListener) || + iid.equals(Components.interfaces.nsIRequestObserver) || + iid.equals(Components.interfaces.nsISupports)) + return this; + throw Components.results.NS_ERROR_NO_INTERFACE; + }, + + onStartRequest: function(request, context) { + this._buffer = ""; + }, + + onDataAvailable: function(request, context, 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: function(request, context, status) { + this._index++; + // Second part should be last part + do_check_eq(request.QueryInterface(Ci.nsIMultiPartChannel).isLastPart, this._index == 2); + 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.asyncOpen2(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..c35628faa4 --- /dev/null +++ b/netwerk/test/unit/test_multipart_streamconv.js @@ -0,0 +1,93 @@ +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +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\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) +{ + do_check_eq(buffer, testData[testNum].data); + do_check_eq(request.QueryInterface(Ci.nsIChannel).contentType, + testData[testNum].type); + if (++testNum == numTests) + httpserver.stop(do_test_finished); +} + +var multipartListener = { + _buffer: "", + _index: 0, + + QueryInterface: function(iid) { + if (iid.equals(Components.interfaces.nsIStreamListener) || + iid.equals(Components.interfaces.nsIRequestObserver) || + iid.equals(Components.interfaces.nsISupports)) + return this; + throw Components.results.NS_ERROR_NO_INTERFACE; + }, + + onStartRequest: function(request, context) { + this._buffer = ""; + }, + + onDataAvailable: function(request, context, 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: function(request, context, status) { + this._index++; + // Second part should be last part + do_check_eq(request.QueryInterface(Ci.nsIMultiPartChannel).isLastPart, this._index == 2); + 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.asyncOpen2(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..97924ccb16 --- /dev/null +++ b/netwerk/test/unit/test_multipart_streamconv_missing_lead_boundary.js @@ -0,0 +1,89 @@ +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +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\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) +{ + do_check_eq(buffer, testData[testNum].data); + do_check_eq(request.QueryInterface(Ci.nsIChannel).contentType, + testData[testNum].type); + if (++testNum == numTests) + httpserver.stop(do_test_finished); +} + +var multipartListener = { + _buffer: "", + + QueryInterface: function(iid) { + if (iid.equals(Components.interfaces.nsIStreamListener) || + iid.equals(Components.interfaces.nsIRequestObserver) || + iid.equals(Components.interfaces.nsISupports)) + return this; + throw Components.results.NS_ERROR_NO_INTERFACE; + }, + + onStartRequest: function(request, context) { + this._buffer = ""; + }, + + onDataAvailable: function(request, context, 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: function(request, context, 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.asyncOpen2(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..58fff91c4f --- /dev/null +++ b/netwerk/test/unit/test_nestedabout_serialize.js @@ -0,0 +1,35 @@ +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", null); + + // 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); + + do_check_eq(copy.asciiSpec, aboutLicense.asciiSpec); + do_check_true(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..732ecd42fc --- /dev/null +++ b/netwerk/test/unit/test_net_addr.js @@ -0,0 +1,199 @@ +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; + do_print('server: listening on ' + this.port); + this.listener.asyncListen(this); +} + +TestServer.prototype = { + onSocketAccepted: function(socket, trans) { + do_print('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: function(socket) {} , + + /** + * Called to close a connection and clean up properties. + */ + reset: function() { + 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: function() { + this.reset(); + try { this.listener.close(); } catch(ignore) {} + } +}; + + +/** + * Helper function. + * Compares two nsINetAddr objects and ensures they are logically equivalent. + */ +function checkAddrEqual(lhs, rhs) { + do_check_eq(lhs.family, rhs.family); + + if (lhs.family === Ci.nsINetAddr.FAMILY_INET) { + do_check_eq(lhs.address, rhs.address); + do_check_eq(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 + do_check_eq(peerAddr.family, Ci.nsINetAddr.FAMILY_INET); + do_check_eq(peerAddr.port, testDataStore.transport.port); + do_check_eq(peerAddr.port, serv.port); + do_check_eq(peerAddr.address, "127.0.0.1"); + + // check selfAddr against expected values + do_check_eq(selfAddr.family, Ci.nsINetAddr.FAMILY_INET); + do_check_eq(selfAddr.address, "127.0.0.1"); + + // check that selfAddr = server.peerAddr and vice versa. + checkAddrEqual(selfAddr, serv.peerAddr); + checkAddrEqual(peerAddr, serv.selfAddr); + + testDataStore = null; + do_execute_soon(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(null, 0, '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(); + + do_register_cleanup(function(){ serv.stop(); }); + + add_test(testIpv4); + /* TODO: testIpv6 */ + /* TODO: testLocal */ + + run_next_test(); +} diff --git a/netwerk/test/unit/test_nojsredir.js b/netwerk/test/unit/test_nojsredir.js new file mode 100644 index 0000000000..d61c41a183 --- /dev/null +++ b/netwerk/test/unit/test_nojsredir.js @@ -0,0 +1,62 @@ +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +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.asyncOpen2(new ChannelListener(completeIter, channel, tests[index].flags)); +} + +function completeIter(request, data, ctx) { + do_check_true(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..72169cc243 --- /dev/null +++ b/netwerk/test/unit/test_nsIBufferedOutputStream_writeFrom_block.js @@ -0,0 +1,179 @@ +function run_test() { run_next_test(); } + +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. + do_check_eq(pipe.outputStream.write('xy', 2), 1); + do_check_eq(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. + do_check_eq(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); + do_check_eq(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); + do_check_eq(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); + do_check_eq(inPipe.outputStream.write('y', 1), 1); + + do_check_eq(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. + do_check_eq(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. + do_check_eq(inPipe.outputStream.write('0123456789abcde', 15), 15); + do_check_eq(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. + do_check_eq(buffered.writeFrom(inPipe.inputStream, 11), 7); + do_check_eq(outPipe.inputStream.available(), 5); + do_check_eq(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. + do_check_eq(buffered.writeFrom(inPipe.inputStream, 5), 5); + do_check_eq(outPipe.inputStream.available(), 5); + do_check_eq(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. + do_check_eq(inPipe.inputStream.available(), 3); + + // Push the rest of the data through, checking that it all came through. + do_check_eq(outPipeReadable.available(), 5); + do_check_eq(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'); + do_check_eq(outPipeReadable.available(), 5); + do_check_eq(outPipeReadable.read(5), '56789'); + buffered.flush(); + do_check_eq(outPipeReadable.available(), 2); + do_check_eq(outPipeReadable.read(2), 'ab'); + do_check_eq(buffered.writeFrom(inPipe.inputStream, 3), 3); + buffered.flush(); + do_check_eq(outPipeReadable.available(), 3); + do_check_eq(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. + do_check_eq(inPipe.outputStream.write('0123456789abcde', 15), 15); + do_check_eq(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. + do_check_eq(buffered.writeFrom(inPipe.inputStream, 5), 5); + buffered.flush(); + do_check_eq(outPipe.inputStream.available(), 5); + do_check_eq(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. + do_check_eq(buffered.writeFrom(inPipe.inputStream, 10), 7); + do_check_eq(outPipe.inputStream.available(), 5); + do_check_eq(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. + do_check_eq(inPipe.inputStream.available(), 3); + + // Push the rest of the data through, checking that it all came through. + do_check_eq(outPipeReadable.available(), 5); + do_check_eq(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'); + do_check_eq(outPipeReadable.available(), 5); + do_check_eq(outPipeReadable.read(5), '56789'); + do_check_eq(buffered.writeFrom(inPipe.inputStream, 3), 3); + buffered.flush(); + do_check_eq(outPipeReadable.available(), 5); + do_check_eq(outPipeReadable.read(5), 'abcde'); + + run_next_test(); +}); diff --git a/netwerk/test/unit/test_offline_status.js b/netwerk/test/unit/test_offline_status.js new file mode 100644 index 0000000000..ac462a540c --- /dev/null +++ b/netwerk/test/unit/test_offline_status.js @@ -0,0 +1,15 @@ +function run_test() { + var ioService = Components.classes["@mozilla.org/network/io-service;1"] + .getService(Components.interfaces.nsIIOService); + + try { + var linkService = Components.classes["@mozilla.org/network/network-link-service;1"] + .getService(Components.interfaces.nsINetworkLinkService); + + // The offline status should depends on the link status + do_check_neq(ioService.offline, linkService.isLinkUp); + } catch (e) { + // The network link service might not be available + do_check_eq(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..44e7957dc7 --- /dev/null +++ b/netwerk/test/unit/test_offlinecache_custom-directory.js @@ -0,0 +1,151 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * 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 + */ + +Cu.import("resource://testing-common/httpd.js"); +Cu.import('resource://gre/modules/Services.jsm'); +Cu.import("resource://gre/modules/NetUtil.jsm"); + + +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, null, null); +} + +// 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'); + do_check_eq(indexSqlFile.exists(), true); + + var file1 = offlineCacheDir.clone(); + file1.append("2"); + file1.append("E"); + file1.append("2C99DE6E7289A5-0"); + do_check_eq(file1.exists(), true); + + var file2 = offlineCacheDir.clone(); + file2.append("8"); + file2.append("6"); + file2.append("0B457F75198B29-0"); + do_check_eq(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); + do_check_true(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.createCodebasePrincipal(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); + // Set this pref to mimic the default browser behavior. + ps.setComplexValue("browser.cache.offline.parent_directory", Ci.nsILocalFile, 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: function(update, state) + { + do_check_eq(state, expectedStates.shift()); + + if (state == Ci.nsIOfflineCacheUpdateObserver.STATE_FINISHED) + finish_test(customDir); + }, + + applicationCacheAvailable: function(appCache) + { + } + }, false); + + do_test_pending(); +} 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..c4d02d5d22 --- /dev/null +++ b/netwerk/test/unit/test_original_sent_received_head.js @@ -0,0 +1,220 @@ +// +// 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 + +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +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 = Components.interfaces.nsIRequest.LOAD_BYPASS_CACHE; + + // ChannelListener defined in head_channels.js + channel1.asyncOpen2(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 = Components.interfaces.nsIRequest.LOAD_FROM_CACHE; + + // ChannelListener defined in head_channels.js + channel2.asyncOpen2(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(Components.interfaces.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(Components.interfaces.nsIHttpChannel); + do_check_eq(request.responseStatus, 200); + do_check_eq(request.responseStatusText, "OK"); + do_check_true(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++; + do_check_eq(aValue, "value1, value2"); + } + if (aName == "Location") { + locationHeaderFound++; + do_check_eq(aValue, "loc"); + } + } + }); + do_check_eq(linkHeaderFound, 1); + do_check_eq(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) { + do_check_eq(aValue, ""); + } else if (linkOrgHeaderFound == 1 ) { + do_check_eq(aValue, "value1"); + } else { + do_check_eq(aValue, "value2"); + } + linkOrgHeaderFound++; + } + if (aName == "Location") { + locationOrgHeaderFound++; + do_check_eq(aValue, "loc"); + } + } + }); + do_check_eq(linkOrgHeaderFound, 3); + do_check_eq(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; + } + } + }); + do_check_false(linkHeaderFound2, "There should be no link header"); + do_check_false(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) { + do_check_eq(aValue, ""); + } else if (linkOrgHeaderFound2 == 1 ) { + do_check_eq(aValue, "value1"); + } else { + do_check_eq(aValue, "value2"); + } + linkOrgHeaderFound2++; + } + if (aName == "Location") { + locationOrgHeaderFound2++; + do_check_eq(aValue, "loc"); + } + } + }); + do_check_true(linkOrgHeaderFound2 == 3, + "Original link header still here."); + do_check_true(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) { + do_check_eq(aValue, ""); + } else if (linkOrgHeaderFound3 == 1 ) { + do_check_eq(aValue, "value1"); + } else { + do_check_eq(aValue, "value2"); + } + linkOrgHeaderFound3++; + } + }); + do_check_true(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..d804be673b --- /dev/null +++ b/netwerk/test/unit/test_parse_content_type.js @@ -0,0 +1,200 @@ +/* -*- 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/. */ + +var charset = {}; +var hadCharset = {}; +var type; + +function reset() { + delete charset.value; + delete hadCharset.value; + type = undefined; +} + +function check(aType, aCharset, aHadCharset) { + do_check_eq(type, aType); + do_check_eq(aCharset, charset.value); + do_check_eq(aHadCharset, hadCharset.value); + reset(); +} + +function run_test() { + var netutil = Components.classes["@mozilla.org/network/util;1"] + .getService(Components.interfaces.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..fed8094834 --- /dev/null +++ b/netwerk/test/unit/test_partial_response_entry_size_smart_shrink.js @@ -0,0 +1,84 @@ +/* + + 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. + +*/ + +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +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); + } +} + +var enforcePref; + +function run_test() +{ + enforcePref = Services.prefs.getBoolPref("network.http.enforce-framing.soft"); + Services.prefs.setBoolPref("network.http.enforce-framing.soft", false); + + httpServer = new HttpServer(); + httpServer.registerPathHandler("/content", contentHandler); + httpServer.start(-1); + + var chan = make_channel(URL + "/content"); + chan.asyncOpen2(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.asyncOpen2(new ChannelListener(finish_test, null)); +} + +function finish_test(request, buffer) +{ + do_check_eq(buffer, responseBody); + Services.prefs.setBoolPref("network.http.enforce-framing.soft", enforcePref); + 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..0e735fc91c --- /dev/null +++ b/netwerk/test/unit/test_permmgr.js @@ -0,0 +1,119 @@ +// tests nsIPermissionManager + +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() { + var pm = Components.classes["@mozilla.org/permissionmanager;1"] + .getService(Components.interfaces.nsIPermissionManager); + + var ioService = Components.classes["@mozilla.org/network/io-service;1"] + .getService(Components.interfaces.nsIIOService); + + var secMan = Components.classes["@mozilla.org/scriptsecuritymanager;1"] + .getService(Components.interfaces.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], null, null); + let principal = secMan.createCodebasePrincipal(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], null, null); + let principal = secMan.createCodebasePrincipal(uri, {}); + + do_check_eq(pm.testPermissionFromPrincipal(principal, results[i][1]), results[i][2]); + do_check_eq(pm.testExactPermissionFromPrincipal(principal, results[i][1]), results[i][3]); + } + + // test the enumerator ... + var j = 0; + var perms = new Array(); + var enumerator = pm.enumerator; + while (enumerator.hasMoreElements()) { + perms[j] = enumerator.getNext().QueryInterface(Components.interfaces.nsIPermission); + ++j; + } + do_check_eq(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], null, null), true) && + hosts[i][1] == perms[j].type && + hosts[i][2] == perms[j].capability) { + perms.splice(j, 1); + break; + } + } + } + do_check_eq(perms.length, 0); + + // ... and check the permmgr's empty + do_check_eq(pm.enumerator.hasMoreElements(), false); + + // 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 uri = ioService.newURI("http://" + utf8, null, null); + pm.add(uri, "utf8", 1); + var enumerator = pm.enumerator; + do_check_eq(enumerator.hasMoreElements(), true); + var ace = enumerator.getNext().QueryInterface(Components.interfaces.nsIPermission); + do_check_eq(ace.principal.URI.asciiHost, aceref); + do_check_eq(enumerator.hasMoreElements(), false); + + // test removeAll() + pm.removeAll(); + do_check_eq(pm.enumerator.hasMoreElements(), false); + + uri = ioService.newURI("https://www.example.com", null, null); + pm.add(uri, "offline-app", pm.ALLOW_ACTION); + principal = secMan.createCodebasePrincipalFromOrigin("https://www.example.com"); + // Remove existing entry. + 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); + do_check_eq(pm.enumerator.hasMoreElements(), false); +} diff --git a/netwerk/test/unit/test_ping_aboutnetworking.js b/netwerk/test/unit/test_ping_aboutnetworking.js new file mode 100644 index 0000000000..6db46cb933 --- /dev/null +++ b/netwerk/test/unit/test_ping_aboutnetworking.js @@ -0,0 +1,86 @@ +/* -*- 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/. */ + +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) { + do_test_pending(); + gDashboard.requestSockets(function(data) { + let index = -1; + do_print("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; + } + } + do_check_neq(index, -1); + do_check_eq(data.sockets[index].port, serverSocket.port); + do_check_eq(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); + + do_register_cleanup(function() { + ps.clearUserPref("network.notify.changed"); + }); + + let serverSocket = Components.classes["@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; + do_print("requestDNSInfo: " + JSON.stringify(data.entries)); + for (let i = 0; i < data.entries.length; i++) { + if (data.entries[i].hostname == "localhost") { + found = true; + break; + } + } + do_check_eq(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..39b1c764a2 --- /dev/null +++ b/netwerk/test/unit/test_pinned_app_cache.js @@ -0,0 +1,277 @@ +/* -*- 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) + * + */ + +Cu.import("resource://testing-common/httpd.js"); + +// const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; + +Cu.import("resource://gre/modules/Services.jsm"); + +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) { + do_print("manifest1\n"); + response.setHeader("content-type", "text/cache-manifest"); + + response.write(kManifest1); +} + +function manifest2_handler(metadata, response) { + do_print("manifest2\n"); + response.setHeader("content-type", "text/cache-manifest"); + + response.write(kManifest2); +} + +function app_handler(metadata, response) { + do_print("app_handler\n"); + response.setHeader("content-type", "text/html"); + + response.write("<html></html>"); +} + +function datafile_handler(metadata, response) { + do_print("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.setComplexValue("browser.cache.offline.parent_directory", + Ci.nsILocalFile, 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 (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(Components.interfaces.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); + + Services.perms.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: function QueryInterface(iftype) { + return this; + }, + + updateStateChanged: stateChangeHandler, + applicationCacheAvailable: cacheAvailHandler + }; + update.addObserver(observer, false); + + 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, null, null), + ioService.newURI(pageURL, null, null), + 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() { + do_print("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 (appcahe) { + do_print("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() { + let error_count = [0]; + do_print("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]; + do_print("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: + do_check_true(error_count[0] == 0, + "Do not discard app1?"); + httpServer.stop(do_test_finished); + break; + + case STATE_ERROR: + do_print("STATE_ERROR\n"); + error_count[0]++; + break; + } + }, + function (appcahe) { + do_print("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..9955a86b9e --- /dev/null +++ b/netwerk/test/unit/test_plaintext_sniff.js @@ -0,0 +1,194 @@ +// Test the plaintext-or-binary sniffer + +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +// 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(Components.interfaces.nsIHttpChannel); + + chan.loadFlags |= + Components.interfaces.nsIChannel.LOAD_CALL_CONTENT_SNIFFERS; + + return chan; +} + +function makeListener(headerIdx, bodyIdx) { + var listener = { + onStartRequest : function test_onStartR(request, ctx) { + try { + var chan = request.QueryInterface(Components.interfaces.nsIChannel); + + do_check_eq(chan.status, Components.results.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] + + "]."); + } + do_check_eq(expectedType, type); + } catch (e) { + do_throw("Unexpected exception: " + e); + } + + throw Components.results.NS_ERROR_ABORT; + }, + + onDataAvailable: function test_ODA() { + do_throw("Should not get any data!"); + }, + + onStopRequest: function test_onStopR(request, ctx, 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.asyncOpen2(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 again for everything for now (causes sporatic oranges) + return; + + // 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") { + return; + } + + 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_post.js b/netwerk/test/unit/test_post.js new file mode 100644 index 0000000000..934719e7df --- /dev/null +++ b/netwerk/test/unit/test_post.js @@ -0,0 +1,120 @@ +// +// POST test +// + +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +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: function (iid) { + if (iid.equals(Ci.nsISupports) || + iid.equals(Ci.nsIProgressEventSink)) + return this; + throw Cr.NS_ERROR_NO_INTERFACE; + }, + + getInterface: function (iid) { + if (iid.equals(Ci.nsIProgressEventSink)) + return this; + throw Cr.NS_ERROR_NO_INTERFACE; + }, + + onProgress: function (request, context, progress, progressMax) { + // this works because the response is 0 bytes and does not trigger onprogress + if (progress === progressMax) { + correctOnProgress = true; + } + }, + + onStatus: function (request, context, 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); + mime.addContentLength = true; + + 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.asyncOpen2(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) { + do_check_eq(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); + + do_check_eq(teststring1 + + read_stream(testfile_stream, testfile_stream.available()) + + teststring2, + data); +} + +function checkRequest(request, data, context) { + do_check_true(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..2f4f580f44 --- /dev/null +++ b/netwerk/test/unit/test_predictor.js @@ -0,0 +1,596 @@ +var Ci = Components.interfaces; +var Cu = Components.utils; +var Cr = Components.results; +var Cc = Components.classes; + +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/LoadContextInfo.jsm"); + +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 LoadContext = function _loadContext() { +}; + +LoadContext.prototype = { + usePrivateBrowsing: false, + + getInterface: function loadContext_getInterface(iid) { + return this.QueryInterface(iid); + }, + + QueryInterface: function loadContext_QueryInterface(iid) { + if (iid.equals(Ci.nsINetworkPredictorVerifier) || + iid.equals(Ci.nsILoadContext)) { + return this; + } + + throw Cr.NS_ERROR_NO_INTERFACE; + }, + + originAttributes: {} +}; + +var load_context = new LoadContext(); + +var ValidityChecker = function(verifier, httpStatus) { + this.verifier = verifier; + this.httpStatus = httpStatus; +}; + +ValidityChecker.prototype = { + verifier: null, + httpStatus: 0, + + QueryInterface: function listener_qi(iid) { + if (iid.equals(Ci.nsISupports) || + iid.equals(Ci.nsICacheEntryOpenCallback)) { + return this; + } + throw Components.results.NS_ERROR_NO_INTERFACE; + }, + + onCacheEntryCheck: function(entry, appCache) + { + return Ci.nsICacheEntryOpenCallback.ENTRY_WANTED; + }, + + onCacheEntryAvailable: function(entry, isnew, appCache, status) + { + // Check if forced valid + do_check_eq(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: function verifier_QueryInterface(iid) { + if (iid.equals(Ci.nsINetworkPredictorVerifier) || + iid.equals(Ci.nsISupports)) { + return this; + } + + throw Cr.NS_ERROR_NO_INTERFACE; + }, + + 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; + do_check_true(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) { + do_check_true(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, 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) { + do_check_true(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) { + do_check_true(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, null, null); +} + +var prepListener = { + numEntriesToOpen: 0, + numEntriesOpened: 0, + continueCallback: null, + + QueryInterface: function (iid) { + if (iid.equals(Ci.nsICacheEntryOpenCallback)) { + return this; + } + throw Cr.NS_ERROR_NO_INTERFACE; + }, + + init: function (entriesToOpen, cb) { + this.numEntriesOpened = 0; + this.numEntriesToOpen = entriesToOpen; + this.continueCallback = cb; + }, + + onCacheEntryCheck: function (entry, appCache) { + return Ci.nsICacheEntryOpenCallback.ENTRY_WANTED; + }, + + onCacheEntryAvailable: function (entry, isNew, appCache, result) { + do_check_eq(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(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, load_context, 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, load_context); + var preconns = []; + for (var i = 0; i < subresources.length; i++) { + var sruri = newURI(subresources[i]); + predictor.learn(sruri, pageload_toplevel, predictor.LEARN_LOAD_SUBRESOURCE, load_context); + preconns.push(extract_origin(sruri)); + } + + var verifier = new Verifier("pageload", [], preconns, []); + predictor.predict(pageload_toplevel, null, predictor.PREDICT_LOAD, load_context, 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_redrect() { + 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, load_context); + predictor.learn(redirect_targeturi, null, predictor.LEARN_LOAD_TOPLEVEL, load_context); + predictor.learn(redirect_targeturi, redirect_inituri, predictor.LEARN_LOAD_REDIRECT, load_context); + + var preconns = []; + preconns.push(extract_origin(redirect_targeturi)); + for (var i = 0; i < subresources.length; i++) { + var sruri = newURI(subresources[i]); + predictor.learn(sruri, redirect_targeturi, predictor.LEARN_LOAD_SUBRESOURCE, load_context); + preconns.push(extract_origin(sruri)); + } + + var verifier = new Verifier("redirect", [], preconns, []); + predictor.predict(redirect_inituri, null, predictor.PREDICT_LOAD, load_context, 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 = []; + for (var i = 0; i < uris.length; i++) { + var uri = newURI(uris[i]); + predictor.learn(uri, null, predictor.LEARN_STARTUP, load_context); + preconns.push(extract_origin(uri)); + } + + var verifier = new Verifier("startup", [], preconns, []); + predictor.predict(null, null, predictor.PREDICT_STARTUP, load_context, 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, load_context); + var sruri = newURI(subresource); + predictor.learn(sruri, dns_toplevel, predictor.LEARN_LOAD_SUBRESOURCE, load_context); + + var preresolves = [extract_origin(sruri)]; + var verifier = new Verifier("dns", [], [], preresolves); + predictor.predict(dns_toplevel, null, predictor.PREDICT_LOAD, load_context, 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, load_context); + var preconns = []; + for (var i = 0; i < subresources.length; i++) { + var sruri = newURI(subresources[i]); + predictor.learn(sruri, origin_toplevel, predictor.LEARN_LOAD_SUBRESOURCE, load_context); + var origin = extract_origin(sruri); + if (preconns.indexOf(origin) === -1) { + preconns.push(origin); + } + } + + var loaduri = newURI("http://localhost:4444/anotherpage.html"); + var verifier = new Verifier("origin", [], preconns, []); + predictor.predict(loaduri, null, predictor.PREDICT_LOAD, load_context, 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: function(request, ctx) { + do_check_eq(request.status, Cr.NS_OK); + }, + + onDataAvailable: function(request, cx, stream, offset, cnt) { + read_stream(stream, cnt); + }, + + onStopRequest: function(request, ctx, 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, load_context); + predictor.learn(prefetch_sruri, prefetch_tluri, predictor.LEARN_LOAD_SUBRESOURCE, load_context); + } else { + sendCommand("predictor.learn(prefetch_tluri, null, predictor.LEARN_LOAD_TOPLEVEL, load_context);"); + sendCommand("predictor.learn(prefetch_sruri, prefetch_tluri, predictor.LEARN_LOAD_SUBRESOURCE, load_context);"); + } + + // 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.referrer = prefetch_tluri; + channel.asyncOpen2(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, load_context, 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: function (iid) { + if (iid.equals(Ci.nsIObserver) || + iid.equals(Ci.nsISupports)) { + return this; + } + + throw Cr.NS_ERROR_NO_INTERFACE; + }, + + observe: function (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", false); +} + +function unregisterObserver() { + Services.obs.removeObserver(observer, "predictor-reset-complete"); +} + +function run_test_real() { + tests.forEach(add_test); + do_get_profile(); + + Services.prefs.setBoolPref("network.predictor.enabled", true); + Services.prefs.setBoolPref("network.predictor.cleaned-up", true); + Services.prefs.setBoolPref("browser.cache.use_new_backend_temp", true); + Services.prefs.setIntPref("browser.cache.use_new_backend", 1); + Services.prefs.setBoolPref("network.predictor.doing-tests", true); + + predictor = Cc["@mozilla.org/network/predictor;1"].getService(Ci.nsINetworkPredictor); + + registerObserver(); + + do_register_cleanup(() => { + Services.prefs.clearUserPref("network.predictor.preconnect-min-confidence"); + Services.prefs.clearUserPref("network.predictor.enabled"); + Services.prefs.clearUserPref("network.predictor.cleaned-up"); + Services.prefs.clearUserPref("browser.cache.use_new_backend_temp"); + Services.prefs.clearUserPref("browser.cache.use_new_backend"); + 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..6c6f31de47 --- /dev/null +++ b/netwerk/test/unit/test_private_cookie_changed.js @@ -0,0 +1,34 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +Components.utils.import("resource://gre/modules/Services.jsm"); +Components.utils.import("resource://gre/modules/NetUtil.jsm"); + +function makeChan(uri, isPrivate) { + var chan = NetUtil.newChannel ({ + uri: uri.spec, + loadUsingSystemPrincipal: true + }).QueryInterface(Components.interfaces.nsIHttpChannel); + + chan.QueryInterface(Ci.nsIPrivateBrowsingChannel).setPrivate(isPrivate); + return chan; +} + +function run_test() { + // Allow all cookies. + Services.prefs.setIntPref("network.cookie.cookieBehavior", 0); + + let publicNotifications = 0; + let privateNotifications = 0; + Services.obs.addObserver(function() {publicNotifications++;}, "cookie-changed", false); + Services.obs.addObserver(function() {privateNotifications++;}, "private-cookie-changed", false); + + let uri = NetUtil.newURI("http://foo.com/"); + let publicChan = makeChan(uri, false); + let svc = Services.cookies.QueryInterface(Ci.nsICookieService); + svc.setCookieString(uri, null, "oh=hai", publicChan); + let privateChan = makeChan(uri, true); + svc.setCookieString(uri, null, "oh=hai", privateChan); + do_check_eq(publicNotifications, 1); + do_check_eq(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..530d4e7e66 --- /dev/null +++ b/netwerk/test/unit/test_private_necko_channel.js @@ -0,0 +1,53 @@ +// +// Private channel test +// + +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +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.asyncOpen2(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) { + do_check_eq(count, 0) + get_device_entry_count("disk", LoadContextInfo.private, function(count) { + do_check_eq(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..e2dae9c09b --- /dev/null +++ b/netwerk/test/unit/test_progress.js @@ -0,0 +1,128 @@ +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +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: function (iid) { + if (iid.equals(Ci.nsISupports) || + iid.equals(Ci.nsIProgressEventSink) || + iid.equals(Ci.nsIStreamListener) || + iid.equals(Ci.nsIRequestObserver)) + return this; + throw Cr.NS_ERROR_NO_INTERFACE; + }, + + getInterface: function (iid) { + if (iid.equals(Ci.nsIProgressEventSink) || + iid.equals(Ci.nsIStreamListener) || + iid.equals(Ci.nsIRequestObserver)) + return this; + throw Cr.NS_ERROR_NO_INTERFACE; + }, + + onStartRequest: function(request, context) { + do_check_eq(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, context); + }, + + onDataAvailable: function(request, context, data, offset, count) { + do_check_eq(this._last_callback_handled, TYPE_ONPROGRESS); + this._last_callback_handled = TYPE_ONDATAAVAILABLE; + + this._listener.onDataAvailable(request, context, data, offset, count); + }, + + onStopRequest: function(request, context, status) { + do_check_eq(this._last_callback_handled, TYPE_ONDATAAVAILABLE); + do_check_true(this._got_onstatus_after_onstartrequest); + this._last_callback_handled = TYPE_ONSTOPREQUEST; + + this._listener.onStopRequest(request, context, status); + delete this._listener; + }, + + onProgress: function (request, context, progress, progressMax) { + do_check_eq(this._last_callback_handled, TYPE_ONSTATUS); + this._last_callback_handled = TYPE_ONPROGRESS; + + do_check_eq(mStatus, STATUS_RECEIVING_FROM); + last = progress; + max = progressMax; + }, + + onStatus: function (request, context, status, statusArg) { + if (!this._got_onstartrequest) { + // Ensure that all messages before onStartRequest are onStatus + if (this._last_callback_handled) + do_check_eq(this._last_callback_handled, TYPE_ONSTATUS); + } else if (this._last_callback_handled == TYPE_ONSTARTREQUEST) { + this._got_onstatus_after_onstartrequest = true; + } else { + do_check_eq(this._last_callback_handled, TYPE_ONDATAAVAILABLE); + } + this._last_callback_handled = TYPE_ONSTATUS; + + do_check_eq(statusArg, "localhost"); + mStatus = status; + }, + + mStatus: 0, +}; + +function run_test() { + httpserver.registerPathHandler(testpath, serverHandler); + httpserver.start(-1); + var channel = setupChannel(testpath); + channel.asyncOpen2(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) { + do_check_eq(last, httpbody.length*LOOPS); + do_check_eq(max, httpbody.length*LOOPS); + httpserver.stop(do_test_finished); +} diff --git a/netwerk/test/unit/test_protocolproxyservice.js b/netwerk/test/unit/test_protocolproxyservice.js new file mode 100644 index 0000000000..ff44fb4d83 --- /dev/null +++ b/netwerk/test/unit/test_protocolproxyservice.js @@ -0,0 +1,958 @@ +/* -*- 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(); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +var ios = Components.classes["@mozilla.org/network/io-service;1"] + .getService(Components.interfaces.nsIIOService); +var pps = Components.classes["@mozilla.org/network/protocol-proxy-service;1"] + .getService(); +var prefs = Components.classes["@mozilla.org/preferences-service;1"] + .getService(Components.interfaces.nsIPrefBranch); + +/** + * Test nsIProtocolHandler that allows proxying, but doesn't allow HTTP + * proxying. + */ +function TestProtocolHandler() { +} +TestProtocolHandler.prototype = { + QueryInterface: function(iid) { + if (iid.equals(Components.interfaces.nsIProtocolHandler) || + iid.equals(Components.interfaces.nsISupports)) + return this; + throw Components.results.NS_ERROR_NO_INTERFACE; + }, + scheme: "moz-test", + defaultPort: -1, + protocolFlags: Components.interfaces.nsIProtocolHandler.URI_NOAUTH | + Components.interfaces.nsIProtocolHandler.URI_NORELATIVE | + Components.interfaces.nsIProtocolHandler.ALLOWS_PROXY | + Components.interfaces.nsIProtocolHandler.URI_DANGEROUS_TO_LOAD, + newURI: function(spec, originCharset, baseURI) { + var uri = Components.classes["@mozilla.org/network/simple-uri;1"] + .createInstance(Components.interfaces.nsIURI); + uri.spec = spec; + return uri; + }, + newChannel2: function(uri, aLoadInfo) { + throw Components.results.NS_ERROR_NOT_IMPLEMENTED; + }, + newChannel: function(uri) { + throw Components.results.NS_ERROR_NOT_IMPLEMENTED; + }, + allowPort: function(port, scheme) { + return true; + } +}; + +function TestProtocolHandlerFactory() { +} +TestProtocolHandlerFactory.prototype = { + createInstance: function(delegate, iid) { + return new TestProtocolHandler().QueryInterface(iid); + }, + lockFactory: function(lock) { + } +}; + +function register_test_protocol_handler() { + var reg = Components.manager.QueryInterface( + Components.interfaces.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) { + do_check_neq(pi, null); + do_check_eq(pi.type, type); + do_check_eq(pi.host, host); + do_check_eq(pi.port, port); + if (flags != -1) + do_check_eq(pi.flags, flags); + if (timeout != -1) + do_check_eq(pi.failoverTimeout, timeout); + if (hasNext) + do_check_neq(pi.failoverProxy, null); + else + do_check_eq(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: function(iid) { + if (iid.equals(Components.interfaces.nsIProtocolProxyFilter) || + iid.equals(Components.interfaces.nsISupports)) + return this; + throw Components.results.NS_ERROR_NO_INTERFACE; + }, + applyFilter: function(pps, uri, pi) { + 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; + return pi; + } +}; + +function BasicFilter() {} +BasicFilter.prototype = { + QueryInterface: function(iid) { + if (iid.equals(Components.interfaces.nsIProtocolProxyFilter) || + iid.equals(Components.interfaces.nsISupports)) + return this; + throw Components.results.NS_ERROR_NO_INTERFACE; + }, + applyFilter: function(pps, uri, pi) { + return pps.newProxyInfo("http", "localhost", 8080, 0, 10, + pps.newProxyInfo("direct", "", -1, 0, 0, null)); + } +}; + +function BasicChannelFilter() {} +BasicChannelFilter.prototype = { + QueryInterface: function(iid) { + if (iid.equals(Components.interfaces.nsIProtocolProxyChannelFilter) || + iid.equals(Components.interfaces.nsISupports)) + return this; + throw Components.results.NS_ERROR_NO_INTERFACE; + }, + applyFilter: function(pps, channel, pi) { + return pps.newProxyInfo("http", channel.URI.host, 7777, 0, 10, + pps.newProxyInfo("direct", "", -1, 0, 0, null)); + } +}; + +function resolveCallback() { } +resolveCallback.prototype = { + nextFunction: null, + + QueryInterface : function (iid) { + const interfaces = [Components.interfaces.nsIProtocolProxyCallback, + Components.interfaces.nsISupports]; + if (!interfaces.some( function(v) { return iid.equals(v) } )) + throw Components.results.NS_ERROR_NO_INTERFACE; + return this; + }, + + onProxyAvailable : function (req, uri, 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; + var req = pps.asyncResolve(channel, 0, cb); +} + +var filter01; +var filter02; + +function filter_test0_1(pi) { + do_check_eq(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 + }); + var req = 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 + }); + var req = 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 + }); + var req = pps.asyncResolve(channel, 0, cb); +} + +var filter03; + +function filter_test0_4(pi) +{ + do_check_eq(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 + }); + var req = 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/", null, null); + pps.asyncResolve(uri, 0, cb); +} + +function filter_test_uri0_1(pi) { + do_check_eq(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/", null, null); + 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/", null, null); + 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/", null, null); + pps.asyncResolve(uri, 0, cb); +} + +function filter_test_uri0_4(pi) +{ + do_check_eq(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 + }); + var req = 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 + }); + var req = 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 + }); + var req = pps.asyncResolve(channel, 0, cb); +} + +function filter_test1_3(pi) { + do_check_eq(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; + var req = 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; + var req = pps.asyncResolve(channel, 0, cb); +} + +function pref_test1_1(pi) +{ + do_check_eq(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 + }); + var req = pps.asyncResolve(channel, 0, cb); +} + +function pref_test1_2(pi) +{ + // nothing yet configured + do_check_eq(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 + }); + var req = 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 + }); + var req = 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) +{ + do_check_eq(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: + function TestResolveCallback_QueryInterface(iid) { + if (iid.equals(Components.interfaces.nsIProtocolProxyCallback) || + iid.equals(Components.interfaces.nsISupports)) + return this; + throw Components.results.NS_ERROR_NO_INTERFACE; + }, + + onProxyAvailable: + function TestResolveCallback_onProxyAvailable(req, uri, pi, status) { + dump("*** uri=" + uri.spec + ", status=" + status + "\n"); + + if (this.type == null) { + do_check_eq(pi, null); + } else { + do_check_neq(req, null); + do_check_neq(uri, null); + do_check_eq(status, 0); + do_check_neq(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); + var req = 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); + + var req = 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); + + var req = 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); + var req = 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: + function TestResolveCallback_QueryInterface(iid) { + if (iid.equals(Components.interfaces.nsIProtocolProxyCallback) || + iid.equals(Components.interfaces.nsISupports)) + return this; + throw Components.results.NS_ERROR_NO_INTERFACE; + }, + + onProxyAvailable: + function TestResolveCancelationCallback_onProxyAvailable(req, uri, pi, status) { + dump("*** uri=" + uri.spec + ", status=" + status + "\n"); + + do_check_neq(req, null); + do_check_neq(uri, null); + do_check_eq(status, Components.results.NS_ERROR_ABORT); + do_check_eq(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(Components.results.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) { + var uri; + 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; + var req = pps.asyncResolve(channel, 0, cb); +} + +function host_filter_cb(proxy) +{ + if (bShouldBeFiltered) { + do_check_eq(proxy, null); + } else { + do_check_neq(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; + +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); + do_check_eq(prefs.getCharPref("network.proxy.no_proxies_on"), hostFilterList); + + var rv; + // 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>"); + do_check_eq(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", ""); + do_check_eq(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; + var req = pps.asyncResolve(channel, 0, cb); +} + +function myipaddress_callback(pi) +{ + do_check_neq(pi, null); + do_check_eq(pi.type, "http"); + do_check_eq(pi.port, 1234); + + // make sure we didn't return localhost + do_check_neq(pi.host, null); + do_check_neq(pi.host, "127.0.0.1"); + do_check_neq(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; + var req = pps.asyncResolve(channel, 0, cb); +} + +function myipaddress2_callback(pi) +{ + do_check_neq(pi, null); + do_check_eq(pi.type, "http"); + do_check_eq(pi.port, 5678); + + // make sure we didn't return localhost + do_check_neq(pi.host, null); + do_check_neq(pi.host, "127.0.0.1"); + do_check_neq(pi.host, "::1"); + + run_failed_script_test(); +} + +function run_failed_script_test() +{ + // test to make sure we go direct with invalid PAC + 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; + var req = pps.asyncResolve(channel, 0, cb); +} + +var directFilter; + +function failed_script_callback(pi) +{ + // we should go direct + do_check_eq(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 = Components.classes["@mozilla.org/observer-service;1"].getService(); + obs = obs.QueryInterface(Components.interfaces.nsIObserverService); + obs.addObserver(directFilterListener, "http-on-modify-request", false); + + var chan = NetUtil.newChannel({ + uri: "http://127.0.0.1:7247", + loadUsingSystemPrincipal: true + }); + chan.asyncOpen2(directFilterListener); +} + +var directFilterListener = { + onModifyRequestCalled : false, + + onStartRequest: function test_onStart(request, ctx) { }, + onDataAvailable: function test_OnData() { }, + + onStopRequest: function test_onStop(request, ctx, status) { + // check on the PI from the channel itself + request.QueryInterface(Components.interfaces.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 + do_check_true(this.onModifyRequestCalled); + var obs = Components.classes["@mozilla.org/observer-service;1"].getService(); + obs = obs.QueryInterface(Components.interfaces.nsIObserverService); + obs.removeObserver(this, "http-on-modify-request"); + + run_isresolvable_test(); + }, + + observe: function(subject, topic, data) { + if (topic === "http-on-modify-request" && + subject instanceof Components.interfaces.nsIHttpChannel && + subject instanceof Components.interfaces.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; + var req = pps.asyncResolve(channel, 0, cb); +} + +function isresolvable_callback(pi) +{ + do_check_neq(pi, null); + do_check_eq(pi.type, "http"); + do_check_eq(pi.port, 1234); + do_check_eq(pi.host, "127.0.0.1"); + + 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..6c81d093cc --- /dev/null +++ b/netwerk/test/unit/test_proxy-failover_canceled.js @@ -0,0 +1,53 @@ +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +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) +{ + do_check_eq(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("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.asyncOpen2(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..b2bf198dd7 --- /dev/null +++ b/netwerk/test/unit/test_proxy-failover_passing.js @@ -0,0 +1,43 @@ +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +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) +{ + do_check_eq(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.asyncOpen2(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..d25bfd2c1b --- /dev/null +++ b/netwerk/test/unit/test_proxy-replace_canceled.js @@ -0,0 +1,55 @@ +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +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) +{ + do_check_eq(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.asyncOpen2(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..e3447fedaa --- /dev/null +++ b/netwerk/test/unit/test_proxy-replace_passing.js @@ -0,0 +1,43 @@ +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +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) +{ + do_check_eq(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.asyncOpen2(new ChannelListener(finish_test, null)); + 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..251ffa6210 --- /dev/null +++ b/netwerk/test/unit/test_psl.js @@ -0,0 +1,36 @@ +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); + +var Cr = Components.results; + +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, "utf-8"); +} + +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) { + } + // 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); + } + do_check_eq(actualSuffix, expectedSuffix); +} diff --git a/netwerk/test/unit/test_range_requests.js b/netwerk/test/unit/test_range_requests.js new file mode 100644 index 0000000000..1850ade438 --- /dev/null +++ b/netwerk/test/unit/test_range_requests.js @@ -0,0 +1,434 @@ +// +// 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. + +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +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: function(iid) { + if (iid.equals(Ci.nsIStreamListener) || + iid.equals(Ci.nsIRequestObserver) || + iid.equals(Ci.nsISupports)) + return this; + throw Components.results.NS_ERROR_NO_INTERFACE; + }, + onStartRequest: function(request, context) { }, + + onDataAvailable: function(request, context, stream, offset, count) { + request.QueryInterface(Ci.nsIChannel) + .cancel(Components.results.NS_BINDING_ABORTED); + }, + onStopRequest: function(request, context, status) { + do_check_eq(status, Components.results.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: function(iid) { + if (iid.equals(Ci.nsIStreamListener) || + iid.equals(Ci.nsIRequestObserver) || + iid.equals(Ci.nsISupports)) + return this; + throw Components.results.NS_ERROR_NO_INTERFACE; + }, + onStartRequest: function(request, context) { this._buffer = ""; }, + + onDataAvailable: function(request, context, stream, offset, count) { + this._buffer = this._buffer.concat(read_stream(stream, count)); + }, + onStopRequest: function(request, context, status) { + this.continueFn(request, this._buffer); + } +}; + +var case_8_range_request = false; +function FailedChannelListener(continueFn) { + this.continueFn = continueFn; +} +FailedChannelListener.prototype = { + QueryInterface: function(iid) { + if (iid.equals(Ci.nsIStreamListener) || + iid.equals(Ci.nsIRequestObserver) || + iid.equals(Ci.nsISupports)) + return this; + throw Components.results.NS_ERROR_NO_INTERFACE; + }, + onStartRequest: function(request, context) { }, + + onDataAvailable: function(request, context, stream, offset, count) { }, + + onStopRequest: function(request, context, status) { + if (case_8_range_request) + do_check_eq(status, Components.results.NS_ERROR_CORRUPTED_CONTENT); + this.continueFn(request, null); + } +}; + +function received_cleartext(request, data) { + do_check_eq(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); + do_check_false(metadata.hasHeader("Range")); + response.bodyOutputStream.write(clearTextBody, clearTextBody.length); +} +function received_partial_2(request, data) { + do_check_eq(data, undefined); + var chan = make_channel("http://localhost:" + port + "/test_2"); + chan.asyncOpen2(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: + do_check_false(metadata.hasHeader("Range")); + body = body.slice(0, partial_data_length); + response.processAsync(); + response.bodyOutputStream.write(body, body.length); + response.finish(); + break; + case 1: + do_check_false(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) { + do_check_eq(partial_data_length, data.length); + var chan = make_channel("http://localhost:" + port + "/test_3"); + chan.asyncOpen2(new ChannelListener(received_cleartext, null)); +} + +var case_4_request_no = 0; +function handler_4(metadata, response) { + switch (case_4_request_no) { + case 0: + do_check_false(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, body.length); + response.finish(); + break; + case 1: + do_check_false(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.asyncOpen2(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: + do_check_false(metadata.hasHeader("Range")); + body = body.slice(0, partial_data_length); + response.processAsync(); + response.bodyOutputStream.write(body, body.length); + response.finish(); + break; + case 1: + do_check_false(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) { + do_check_eq(partial_data_length, data.length); + var chan = make_channel("http://localhost:" + port + "/test_5"); + chan.setRequestHeader("If-Match", "Some eTag", false); + chan.asyncOpen2(new ChannelListener(received_cleartext, null)); +} + +var case_6_request_no = 0; +function handler_6(metadata, response) { + switch (case_6_request_no) { + case 0: + do_check_false(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: + do_check_false(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 + do_check_eq(partial_data_length, data.length); + var chan = make_channel("http://localhost:" + port + "/test_6"); + chan.asyncOpen2(new ChannelListener(received_cleartext, null)); +} + +const simpleBody = "0123456789"; + +function received_simple(request, data) { + do_check_eq(simpleBody, data); + testFinished(); +} + +var case_7_request_no = 0; +function handler_7(metadata, response) { + switch (case_7_request_no) { + case 0: + do_check_false(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")) { + do_check_true(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 + do_check_eq(4, data.length); + // do it again to get the rest + var chan = make_channel("http://localhost:" + port + "/test_7"); + chan.asyncOpen2(new ChannelListener(received_simple, null)); +} + +var case_8_request_no = 0; +function handler_8(metadata, response) { + switch (case_8_request_no) { + case 0: + do_check_false(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")) { + do_check_true(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 + do_check_eq(4, data.length); + // do it again to get the rest + var chan = make_channel("http://localhost:" + port + "/test_8"); + chan.asyncOpen2(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: + do_check_false(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: + do_check_false(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) { + do_check_eq(partial_data_length, data.length); + var chan = make_channel("http://localhost:" + port + "/test_9"); + chan.asyncOpen2(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.asyncOpen2(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.asyncOpen2(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.asyncOpen2(new MyListener(received_partial_4)); + + // Case 5: conditional request-header set by client + var chan = make_channel("http://localhost:" + port + "/test_5"); + chan.asyncOpen2(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.asyncOpen2(new MyListener(received_partial_6)); + + // Case 7: a basic positive test + var chan = make_channel("http://localhost:" + port + "/test_7"); + chan.asyncOpen2(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.asyncOpen2(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.asyncOpen2(new MyListener(received_partial_9)); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_readline.js b/netwerk/test/unit/test_readline.js new file mode 100644 index 0000000000..798b2c2a78 --- /dev/null +++ b/netwerk/test/unit/test_readline.js @@ -0,0 +1,60 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +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..2371078654 --- /dev/null +++ b/netwerk/test/unit/test_redirect-caching_canceled.js @@ -0,0 +1,68 @@ +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +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); + return; +} + +function contentHandler(metadata, response) +{ + response.setHeader("Content-Type", "text/plain"); + response.bodyOutputStream.write(responseBody, responseBody.length); +} + +function firstTimeThrough(request, buffer) +{ + do_check_eq(buffer, responseBody); + var chan = make_channel(randomURI); + chan.asyncOpen2(new ChannelListener(secondTimeThrough, null)); +} + +function secondTimeThrough(request, buffer) +{ + do_check_eq(buffer, responseBody); + var chan = make_channel(randomURI); + chan.loadFlags |= Ci.nsIRequest.LOAD_FROM_CACHE; + chan.notificationCallbacks = new ChannelEventSink(ES_ABORT_REDIRECT); + chan.asyncOpen2(new ChannelListener(finish_test, null, CL_EXPECT_FAILURE)); +} + +function finish_test(request, buffer) +{ + do_check_eq(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.asyncOpen2(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..0e88c0d099 --- /dev/null +++ b/netwerk/test/unit/test_redirect-caching_failure.js @@ -0,0 +1,55 @@ +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +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", "httpx://localhost:" + + httpServer.identity.primaryPort + "/content", false); + response.setHeader("Cache-control", "max-age=1000", false); +} + +function makeSureNotInCache(request, buffer) +{ + do_check_eq(request.status, Components.results.NS_ERROR_UNKNOWN_PROTOCOL); + + // It's very unlikely that we'd somehow succeed when we try again from cache. + // Can't hurt to test though. + var chan = make_channel(randomURI); + chan.loadFlags |= Ci.nsIRequest.LOAD_ONLY_FROM_CACHE; + chan.asyncOpen2(new ChannelListener(finish_test, null, CL_EXPECT_FAILURE)); +} + +function finish_test(request, buffer) +{ + do_check_eq(request.status, Components.results.NS_ERROR_UNKNOWN_PROTOCOL); + do_check_eq(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.asyncOpen2(new ChannelListener(makeSureNotInCache, 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..e1d3ebe8fa --- /dev/null +++ b/netwerk/test/unit/test_redirect-caching_passing.js @@ -0,0 +1,59 @@ +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +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); + return; +} + +function contentHandler(metadata, response) +{ + response.setHeader("Content-Type", "text/plain"); + response.bodyOutputStream.write(responseBody, responseBody.length); +} + +function firstTimeThrough(request, buffer) +{ + do_check_eq(buffer, responseBody); + var chan = make_channel(randomURI); + chan.loadFlags |= Ci.nsIRequest.LOAD_FROM_CACHE; + chan.asyncOpen2(new ChannelListener(finish_test, null)); +} + +function finish_test(request, buffer) +{ + do_check_eq(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.asyncOpen2(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..aa1ce7dc42 --- /dev/null +++ b/netwerk/test/unit/test_redirect_baduri.js @@ -0,0 +1,42 @@ +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +/* + * 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) +{ + do_check_eq(request.status, Components.results.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.asyncOpen2(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..cd2a948eef --- /dev/null +++ b/netwerk/test/unit/test_redirect_canceled.js @@ -0,0 +1,51 @@ +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +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) +{ + do_check_eq(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.asyncOpen2(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..73aea57ca1 --- /dev/null +++ b/netwerk/test/unit/test_redirect_different-protocol.js @@ -0,0 +1,51 @@ +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +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) +{ + do_check_eq(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.asyncOpen2(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..b74a9102f4 --- /dev/null +++ b/netwerk/test/unit/test_redirect_failure.js @@ -0,0 +1,45 @@ +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +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", "httpx://localhost:" + + httpServer.identity.primaryPort + "/content", false); + response.setHeader("Cache-Control", "no-cache", false); +} + +function finish_test(request, buffer) +{ + do_check_eq(request.status, Components.results.NS_ERROR_UNKNOWN_PROTOCOL); + + do_check_eq(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.asyncOpen2(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..d296ab0d0b --- /dev/null +++ b/netwerk/test/unit/test_redirect_from_script.js @@ -0,0 +1,258 @@ +/* + * 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 nsIXMLHttpRequest objects. + * + * testViaAsyncOpen() checks that internal redirects occur correctly when made + * with nsIHTTPChannel.asyncOpen2(). + * + * 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. + * + */ + +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +// the topic we observe to use the API. http-on-opening-request might also +// work for some purposes. +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: function() + { + Cc["@mozilla.org/observer-service;1"]. + getService(Ci.nsIObserverService). + addObserver(this, redirectHook, true); + }, + + QueryInterface: function(iid) + { + if (iid.equals(Ci.nsIObserver) || + iid.equals(Ci.nsISupportsWeakReference) || + iid.equals(Ci.nsISupports)) + return this; + throw Components.results.NS_NOINTERFACE; + }, + + observe: function(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, null, null); + 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.asyncOpen2() + + // 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); + do_check_eq(httpChannel.getResponseHeader(testHeaderName), headerValue); + do_check_eq(buffer, redirectedText); + nextTask(); + }; + + // Produce a function to run an asyncOpen2 test using the above verifier + var test = function() + { + var chan = make_channel(uri); + chan.asyncOpen2(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 xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]; + + var req = xhr.createInstance(Ci.nsIXMLHttpRequest); + req.open("GET", uri, false); + req.send(); + do_check_eq(req.getResponseHeader(testHeaderName), headerValue); + do_check_eq(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..195508490c --- /dev/null +++ b/netwerk/test/unit/test_redirect_from_script_after-open_passing.js @@ -0,0 +1,258 @@ +/* + * 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 nsIXMLHttpRequest objects. + * + * testViaAsyncOpen() checks that internal redirects occur correctly when made + * with nsIHTTPChannel.asyncOpen2(). + * + * 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. + * + */ + +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +// the topic we observe to use the API. http-on-opening-request might also +// work for some purposes. +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: function() + { + Cc["@mozilla.org/observer-service;1"]. + getService(Ci.nsIObserverService). + addObserver(this, redirectHook, true); + }, + + QueryInterface: function(iid) + { + if (iid.equals(Ci.nsIObserver) || + iid.equals(Ci.nsISupportsWeakReference) || + iid.equals(Ci.nsISupports)) + return this; + throw Components.results.NS_NOINTERFACE; + }, + + observe: function(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, null, null); + 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.asyncOpen2() + + // 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); + do_check_eq(httpChannel.getResponseHeader(testHeaderName), headerValue); + do_check_eq(buffer, redirectedText); + nextTask(); + }; + + // Produce a function to run an asyncOpen2 test using the above verifier + var test = function() + { + var chan = make_channel(uri); + chan.asyncOpen2(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 xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]; + + var req = xhr.createInstance(Ci.nsIXMLHttpRequest); + req.open("GET", uri, false); + req.send(); + do_check_eq(req.getResponseHeader(testHeaderName), headerValue); + do_check_eq(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..07da064787 --- /dev/null +++ b/netwerk/test/unit/test_redirect_history.js @@ -0,0 +1,64 @@ +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +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) +{ + do_check_eq(buffer, responseBody); + let chan = request.QueryInterface(Ci.nsIChannel); + let redirectChain = chan.loadInfo.redirectChain; + + do_check_eq(numRedirects - 1, redirectChain.length); + for (let i = 0; i < numRedirects - 1; ++i) { + let principal = redirectChain[i]; + do_check_eq(URL + redirects[i], principal.URI.spec); + } + 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]); + chan.asyncOpen2(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..9efcecadb8 --- /dev/null +++ b/netwerk/test/unit/test_redirect_loop.js @@ -0,0 +1,86 @@ +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +/* + * 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) +{ + do_check_eq(request.status, Components.results.NS_ERROR_REDIRECT_LOOP); + + var chan = make_channel(relativeLoopURI); + chan.asyncOpen2(new ChannelListener(testRelativeLoop, null, CL_EXPECT_FAILURE)); +} + +function testRelativeLoop(request, buffer) +{ + do_check_eq(request.status, Components.results.NS_ERROR_REDIRECT_LOOP); + + var chan = make_channel(emptyLoopURI); + chan.asyncOpen2(new ChannelListener(testEmptyLoop, null, CL_EXPECT_FAILURE)); +} + +function testEmptyLoop(request, buffer) +{ + do_check_eq(request.status, Components.results.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.asyncOpen2(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..a9a515b5eb --- /dev/null +++ b/netwerk/test/unit/test_redirect_passing.js @@ -0,0 +1,57 @@ +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +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) +{ + do_check_eq(buffer, responseBody); + var chan = make_channel(randomURI); + chan.asyncOpen2(new ChannelListener(finish_test, null)); +} + +function finish_test(request, buffer) +{ + do_check_eq(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.asyncOpen2(new ChannelListener(firstTimeThrough, null)); + do_test_pending(); +} diff --git a/netwerk/test/unit/test_reentrancy.js b/netwerk/test/unit/test_reentrancy.js new file mode 100644 index 0000000000..2f8eec30f5 --- /dev/null +++ b/netwerk/test/unit/test_reentrancy.js @@ -0,0 +1,105 @@ +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +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 = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"] + .createInstance(Ci.nsIXMLHttpRequest); + xhr.open("GET", URL + testpath, false); + xhr.send(null); +} + +const MAX_TESTS = 2; + +var listener = { + _done_onStart: false, + _done_onData: false, + _test: 0, + + QueryInterface: function(iid) { + if (iid.equals(Components.interfaces.nsIStreamListener) || + iid.equals(Components.interfaces.nsIRequestObserver) || + iid.equals(Components.interfaces.nsISupports)) + return this; + throw Components.results.NS_ERROR_NO_INTERFACE; + }, + + onStartRequest: function(request, ctx) { + switch(this._test) { + case 0: + request.suspend(); + syncXHR(); + request.resume(); + break; + case 1: + request.suspend(); + syncXHR(); + do_execute_soon(function() { request.resume(); }); + break; + case 2: + do_execute_soon(function() { request.suspend(); }); + do_execute_soon(function() { request.resume(); }); + syncXHR(); + break; + } + + this._done_onStart = true; + }, + + onDataAvailable: function(request, context, stream, offset, count) { + do_check_true(this._done_onStart); + read_stream(stream, count); + this._done_onData = true; + }, + + onStopRequest: function(request, ctx, status) { + do_check_true(this._done_onData); + this._reset(); + if (this._test <= MAX_TESTS) + next_test(); + else + httpserver.stop(do_test_finished); + }, + + _reset: function() { + 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.asyncOpen2(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..5b9fc1c28d --- /dev/null +++ b/netwerk/test/unit/test_referrer.js @@ -0,0 +1,109 @@ +Cu.import("resource://gre/modules/NetUtil.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); + +function getTestReferrer(server_uri, referer_uri) { + var uri = NetUtil.newURI(server_uri, "", null) + let referrer = NetUtil.newURI(referer_uri, null, null); + let triggeringPrincipal = Services.scriptSecurityManager.createCodebasePrincipal(referrer, {}); + var chan = NetUtil.newChannel({ + uri: uri, + loadingPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), + triggeringPrincipal: triggeringPrincipal, + contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER + }); + + chan.QueryInterface(Components.interfaces.nsIHttpChannel); + chan.referrer = 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(Components.interfaces.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); + do_check_null(getTestReferrer(server_uri, referer_uri)); + prefs.setIntPref("network.http.sendRefererHeader", 2); + do_check_eq(getTestReferrer(server_uri, referer_uri), referer_uri); + + // test that https ref is not sent to http + do_check_null(getTestReferrer(server_uri_2, referer_uri_https)); + + // tests for referer.spoofSource + prefs.setBoolPref("network.http.referer.spoofSource", true); + do_check_eq(getTestReferrer(server_uri, referer_uri), server_uri); + prefs.setBoolPref("network.http.referer.spoofSource", false); + do_check_eq(getTestReferrer(server_uri, referer_uri), referer_uri); + + // tests for referer.XOriginPolicy + prefs.setIntPref("network.http.referer.XOriginPolicy", 2); + do_check_null(getTestReferrer(server_uri_2, referer_uri)); + do_check_eq(getTestReferrer(server_uri, referer_uri_2), referer_uri_2); + prefs.setIntPref("network.http.referer.XOriginPolicy", 1); + do_check_eq(getTestReferrer(server_uri_2, referer_uri), referer_uri); + do_check_null(getTestReferrer(server_uri, referer_uri)); + // https test + do_check_eq(getTestReferrer(server_uri_https, referer_uri_https), referer_uri_https); + prefs.setIntPref("network.http.referer.XOriginPolicy", 0); + do_check_eq(getTestReferrer(server_uri, referer_uri), referer_uri); + + // tests for referer.trimmingPolicy + prefs.setIntPref("network.http.referer.trimmingPolicy", 1); + do_check_eq(getTestReferrer(server_uri, referer_uri_2), "http://bar.examplesite.com/path3"); + do_check_eq(getTestReferrer(server_uri, referer_uri_idn), "http://sub1.xn--lt-uia.example/path"); + prefs.setIntPref("network.http.referer.trimmingPolicy", 2); + do_check_eq(getTestReferrer(server_uri, referer_uri_2), "http://bar.examplesite.com/"); + do_check_eq(getTestReferrer(server_uri, referer_uri_idn), "http://sub1.xn--lt-uia.example/"); + // https test + do_check_eq(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 + do_check_eq(getTestReferrer(server_uri, referer_uri_2_anchor), referer_uri_2); + + // tests for referer.XOriginTrimmingPolicy + prefs.setIntPref("network.http.referer.XOriginTrimmingPolicy", 1); + do_check_eq(getTestReferrer(server_uri, referer_uri), "http://foo.example.com/path"); + do_check_eq(getTestReferrer(server_uri, referer_uri_idn), "http://sub1.xn--lt-uia.example/path"); + do_check_eq(getTestReferrer(server_uri, referer_uri_2), "http://bar.examplesite.com/path3?q=blah"); + prefs.setIntPref("network.http.referer.trimmingPolicy", 1); + do_check_eq(getTestReferrer(server_uri, referer_uri_2), "http://bar.examplesite.com/path3"); + prefs.setIntPref("network.http.referer.XOriginTrimmingPolicy", 2); + do_check_eq(getTestReferrer(server_uri, referer_uri), "http://foo.example.com/"); + do_check_eq(getTestReferrer(server_uri, referer_uri_idn), "http://sub1.xn--lt-uia.example/"); + do_check_eq(getTestReferrer(server_uri, referer_uri_2), "http://bar.examplesite.com/path3"); + prefs.setIntPref("network.http.referer.trimmingPolicy", 0); + do_check_eq(getTestReferrer(server_uri, referer_uri_2), "http://bar.examplesite.com/path3?q=blah"); + // https tests + do_check_eq(getTestReferrer(server_uri_https, referer_uri_https), "https://bar.example.com/path3?q=blah"); + do_check_eq(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 + do_check_eq(getTestReferrer(server_uri, referer_uri_2_anchor), referer_uri_2); + + // 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); + do_check_eq(getTestReferrer(dest_uri, combo_referer_uri), "http://blah.foo.com:9999/spoofedpath"); + do_check_null(getTestReferrer(dest_uri, "http://gah.foo.com/anotherpath")); +} diff --git a/netwerk/test/unit/test_referrer_policy.js b/netwerk/test/unit/test_referrer_policy.js new file mode 100644 index 0000000000..f1f9dfd5a3 --- /dev/null +++ b/netwerk/test/unit/test_referrer_policy.js @@ -0,0 +1,95 @@ +Cu.import("resource://gre/modules/NetUtil.jsm"); + +function test_policy(test) { + do_print("Running test: " + test.toSource()); + + var uri = NetUtil.newURI(test.url, "", null) + var chan = NetUtil.newChannel({ + uri: uri, + loadUsingSystemPrincipal: true + }); + + var referrer = NetUtil.newURI(test.referrer, "", null); + chan.QueryInterface(Components.interfaces.nsIHttpChannel); + chan.setReferrerWithPolicy(referrer, test.policy); + if (test.expectedReferrerSpec === undefined) { + try { + chan.getRequestHeader("Referer"); + do_throw("Should not find a Referer header!"); + } catch(e) { + } + do_check_eq(chan.referrer, null); + } else { + var header = chan.getRequestHeader("Referer"); + do_check_eq(header, test.expectedReferrerSpec); + do_check_eq(chan.referrer.asciiSpec, test.expectedReferrerSpec); + } +} + +const nsIHttpChannel = Ci.nsIHttpChannel; +var gTests = [ + { + policy: nsIHttpChannel.REFERRER_POLICY_DEFAULT, + url: "https://test.example/foo", + referrer: "https://test.example/referrer", + expectedReferrerSpec: "https://test.example/referrer" + }, + { + policy: nsIHttpChannel.REFERRER_POLICY_DEFAULT, + url: "https://sub1.\xe4lt.example/foo", + referrer: "https://sub1.\xe4lt.example/referrer", + expectedReferrerSpec: "https://sub1.xn--lt-uia.example/referrer" + }, + { + policy: nsIHttpChannel.REFERRER_POLICY_DEFAULT, + url: "http://test.example/foo", + referrer: "https://test.example/referrer", + expectedReferrerSpec: undefined + }, + { + policy: nsIHttpChannel.REFERRER_POLICY_NO_REFERRER, + url: "https://test.example/foo", + referrer: "https://test.example/referrer", + expectedReferrerSpec: undefined + }, + { + policy: nsIHttpChannel.REFERRER_POLICY_ORIGIN, + url: "https://test.example/foo", + referrer: "https://test.example/referrer", + expectedReferrerSpec: "https://test.example/" + }, + { + policy: nsIHttpChannel.REFERRER_POLICY_ORIGIN, + url: "https://sub1.\xe4lt.example/foo", + referrer: "https://sub1.\xe4lt.example/referrer", + expectedReferrerSpec: "https://sub1.xn--lt-uia.example/" + }, + { + policy: nsIHttpChannel.REFERRER_POLICY_UNSAFE_URL, + url: "https://test.example/foo", + referrer: "https://test.example/referrer", + expectedReferrerSpec: "https://test.example/referrer" + }, + { + policy: nsIHttpChannel.REFERRER_POLICY_UNSAFE_URL, + url: "https://sub1.\xe4lt.example/foo", + referrer: "https://sub1.\xe4lt.example/referrer", + expectedReferrerSpec: "https://sub1.xn--lt-uia.example/referrer" + }, + { + policy: nsIHttpChannel.REFERRER_POLICY_UNSAFE_URL, + url: "http://test.example/foo", + referrer: "https://test.example/referrer", + expectedReferrerSpec: "https://test.example/referrer" + }, + { + policy: nsIHttpChannel.REFERRER_POLICY_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..b3744dfc53 --- /dev/null +++ b/netwerk/test/unit/test_reopen.js @@ -0,0 +1,141 @@ +// This testcase verifies that channels can't be reopened +// See https://bugzilla.mozilla.org/show_bug.cgi?id=372486 + +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +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(add_test); + +// 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) { + do_check_neq(error.indexOf(e.result), -1); + } else { + do_check_eq(e.result, error); + } + thrown = true; + } + do_check_true(thrown); +} + +function check_open_throws(error) { + check_throws(function() { + chan.open2(listener); + }, error); +} + +function check_async_open_throws(error) { + check_throws(function() { + chan.asyncOpen2(listener); + }, error); +} + +var listener = { + onStartRequest: function test_onStartR(request, ctx) { + check_async_open_throws(NS_ERROR_IN_PROGRESS); + }, + + onDataAvailable: function test_ODA(request, cx, inputStream, + offset, count) { + new BinaryInputStream(inputStream).readByteArray(count); // required by API + check_async_open_throws(NS_ERROR_IN_PROGRESS); + }, + + onStopRequest: function test_onStopR(request, ctx, 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(); + var inputStream = chan.open2(); + 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.asyncOpen2(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..7fda87209c --- /dev/null +++ b/netwerk/test/unit/test_reply_without_content_type.js @@ -0,0 +1,91 @@ +// +// tests HTTP replies that lack content-type (where we try to sniff content-type). +// + +// Note: sets Cc and Ci variables + +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +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.asyncOpen2(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.asyncOpen2(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, httpbodyGZip.length); + if (dbg) { print("============== serverHandler GZip: out"); } +} + +function checkRequest(request, data, context) { + if (dbg) { print("============== checkRequest: in"); } + do_check_eq(data, httpbody); + do_check_eq(request.QueryInterface(Ci.nsIChannel).contentType,"text/html"); + httpserver.stop(do_test_finished); + run_next_test(); + if (dbg) { print("============== checkRequest: out"); } +} + +function run_test() { + run_next_test(); +} diff --git a/netwerk/test/unit/test_resumable_channel.js b/netwerk/test/unit/test_resumable_channel.js new file mode 100644 index 0000000000..57a4481b96 --- /dev/null +++ b/netwerk/test/unit/test_resumable_channel.js @@ -0,0 +1,402 @@ +/* Tests various aspects of nsIResumableChannel in combination with HTTP */ + +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +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: function authprompt2_qi(iid) { + if (iid.equals(Components.interfaces.nsISupports) || + iid.equals(Components.interfaces.nsIAuthPrompt2)) + return this; + throw Components.results.NS_ERROR_NO_INTERFACE; + }, + + 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: function requestor_qi(iid) { + if (iid.equals(Components.interfaces.nsISupports) || + iid.equals(Components.interfaces.nsIInterfaceRequestor)) + return this; + throw Components.results.NS_ERROR_NO_INTERFACE; + }, + + getInterface: function requestor_gi(iid) { + if (iid.equals(Components.interfaces.nsIAuthPrompt2)) { + // Allow the prompt to store state by caching it here + if (!this.prompt2) + this.prompt2 = new AuthPrompt2(); + return this.prompt2; + } + + throw Components.results.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"); + do_check_true(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.asyncOpen2(new ChannelListener(try_resume, null, CL_EXPECT_FAILURE)); + } + + function try_resume(request, data, ctx) { + dump("*** try_resume()\n"); + do_check_eq(request.status, NS_ERROR_NOT_RESUMABLE); + + // Try a successful resume + var chan = make_channel(URL + "/range"); + chan.nsIResumableChannel.resumeAt(1, entityID); + chan.asyncOpen2(new ChannelListener(try_resume_zero, null)); + } + + function try_resume_zero(request, data, ctx) { + dump("*** try_resume_zero()\n"); + do_check_true(request.nsIHttpChannel.requestSucceeded); + do_check_eq(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.asyncOpen2(new ChannelListener(try_no_range, null, CL_EXPECT_FAILURE)); + } + + function try_no_range(request, data, ctx) { + dump("*** try_no_range()\n"); + do_check_true(request.nsIHttpChannel.requestSucceeded); + do_check_eq(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.asyncOpen2(new ChannelListener(try_bytes_range, null)); + } + + function try_bytes_range(request, data, ctx) { + dump("*** try_bytes_range()\n"); + do_check_true(request.nsIHttpChannel.requestSucceeded); + do_check_eq(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.asyncOpen2(new ChannelListener(try_foo_bar_range, null, CL_EXPECT_FAILURE)); + } + + function try_foo_bar_range(request, data, ctx) { + dump("*** try_foo_bar_range()\n"); + do_check_true(request.nsIHttpChannel.requestSucceeded); + do_check_eq(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.asyncOpen2(new ChannelListener(try_foobar_range, null, CL_EXPECT_FAILURE)); + } + + function try_foobar_range(request, data, ctx) { + dump("*** try_foobar_range()\n"); + do_check_true(request.nsIHttpChannel.requestSucceeded); + do_check_eq(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.asyncOpen2(new ChannelListener(try_bytes_foobar_range, null)); + } + + function try_bytes_foobar_range(request, data, ctx) { + dump("*** try_bytes_foobar_range()\n"); + do_check_true(request.nsIHttpChannel.requestSucceeded); + do_check_eq(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.asyncOpen2(new ChannelListener(try_bytesfoo_bar_range, null, CL_EXPECT_FAILURE)); + } + + function try_bytesfoo_bar_range(request, data, ctx) { + dump("*** try_bytesfoo_bar_range()\n"); + do_check_true(request.nsIHttpChannel.requestSucceeded); + do_check_eq(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.asyncOpen2(new ChannelListener(try_no_accept_ranges, null)); + } + + function try_no_accept_ranges(request, data, ctx) { + dump("*** try_no_accept_ranges()\n"); + do_check_true(request.nsIHttpChannel.requestSucceeded); + do_check_eq(data, rangeBody); + + // Try a successful suspend/resume from 0 + var chan = make_channel(URL + "/range"); + chan.nsIResumableChannel.resumeAt(0, entityID); + chan.asyncOpen2(new ChannelListener(try_suspend_resume, null, + CL_SUSPEND | CL_EXPECT_3S_DELAY)); + } + + function try_suspend_resume(request, data, ctx) { + dump("*** try_suspend_resume()\n"); + do_check_true(request.nsIHttpChannel.requestSucceeded); + do_check_eq(data, rangeBody); + + // Try a successful resume from 0 + var chan = make_channel(URL + "/range"); + chan.nsIResumableChannel.resumeAt(0, entityID); + chan.asyncOpen2(new ChannelListener(success, null)); + } + + function success(request, data, ctx) { + dump("*** success()\n"); + do_check_true(request.nsIHttpChannel.requestSucceeded); + do_check_eq(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.asyncOpen2(new ChannelListener(test_auth_nopw, null, CL_EXPECT_FAILURE)); + } + + function test_auth_nopw(request, data, ctx) { + dump("*** test_auth_nopw()\n"); + do_check_false(request.nsIHttpChannel.requestSucceeded); + do_check_eq(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.asyncOpen2(new ChannelListener(test_auth, null, CL_EXPECT_FAILURE)); + } + function test_auth(request, data, ctx) { + dump("*** test_auth()\n"); + do_check_eq(request.status, NS_ERROR_NOT_RESUMABLE); + do_check_true(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.asyncOpen2(new ChannelListener(test_auth_resume, null)); + } + + function test_auth_resume(request, data, ctx) { + dump("*** test_auth_resume()\n"); + do_check_eq(data, rangeBody.substring(1)); + do_check_true(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.asyncOpen2(new ChannelListener(test_404, null, CL_EXPECT_FAILURE)); + } + + function test_404(request, data, ctx) { + dump("*** test_404()\n"); + do_check_eq(request.status, NS_ERROR_ENTITY_CHANGED); + do_check_eq(request.nsIHttpChannel.responseStatus, 404); + + // 416 Requested Range Not Satisfiable + var chan = make_channel(URL + "/range"); + chan.nsIResumableChannel.resumeAt(1000, entityID); + chan.asyncOpen2(new ChannelListener(test_416, null, CL_EXPECT_FAILURE)); + } + + function test_416(request, data, ctx) { + dump("*** test_416()\n"); + do_check_eq(request.status, NS_ERROR_ENTITY_CHANGED); + do_check_eq(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.asyncOpen2(new ChannelListener(test_redir_resume, null)); + } + + function test_redir_resume(request, data, ctx) { + dump("*** test_redir_resume()\n"); + do_check_true(request.nsIHttpChannel.requestSucceeded); + do_check_eq(data, rangeBody.substring(1)); + do_check_eq(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.asyncOpen2(new ChannelListener(test_redir_noresume, null, CL_EXPECT_FAILURE)); + } + + function test_redir_noresume(request, data, ctx) { + dump("*** test_redir_noresume()\n"); + do_check_eq(request.status, NS_ERROR_NOT_RESUMABLE); + + httpserver.stop(do_test_finished); + } + + httpserver.start(-1); + var chan = make_channel(URL + "/range"); + chan.asyncOpen2(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="; + + 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); + + return true; + } + else + { + // 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..c23a91b714 --- /dev/null +++ b/netwerk/test/unit/test_resumable_truncate.js @@ -0,0 +1,88 @@ +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +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: function(iid) { + if (iid.equals(Ci.nsIStreamListener) || + iid.equals(Ci.nsIRequestObserver) || + iid.equals(Ci.nsISupports)) + return this; + throw Components.results.NS_ERROR_NO_INTERFACE; + }, + + onStartRequest: function(request, context) { + }, + + onDataAvailable: function(request, context, stream, offset, count) { + request.QueryInterface(Ci.nsIChannel) + .cancel(Components.results.NS_BINDING_ABORTED); + }, + + onStopRequest: function(request, context, status) { + do_check_eq(status, Components.results.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.asyncOpen2(new ChannelListener(finish_test, null)); +} + +function start_canceler() { + var chan = make_channel("http://localhost:" + + httpserver.identity.primaryPort + "/cached/test.gz"); + chan.asyncOpen2(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.asyncOpen2(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..3e41fcbb02 --- /dev/null +++ b/netwerk/test/unit/test_safeoutputstream.js @@ -0,0 +1,66 @@ +/* -*- 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/. */ + +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); + + do_check_eq(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..a7abb06e65 --- /dev/null +++ b/netwerk/test/unit/test_safeoutputstream_append.js @@ -0,0 +1,42 @@ +/* atomic-file-output-stream and safe-file-output-stream should throw and + * exception if PR_APPEND is explicity specified without PR_TRUNCATE. */ + +const PR_WRONLY = 0x02; +const PR_CREATE_FILE = 0x08; +const PR_APPEND = 0x10; +const PR_TRUNCATE = 0x20; + +const { Services } = Cu.import("resource://gre/modules/Services.jsm", {}); + +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_separate_connections.js b/netwerk/test/unit/test_separate_connections.js new file mode 100644 index 0000000000..d49b2885fd --- /dev/null +++ b/netwerk/test/unit/test_separate_connections.js @@ -0,0 +1,99 @@ +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +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: userContextId }; + return chan; +} + +let previousHashKeys = []; + +function Listener(userContextId) { + this.userContextId = userContextId; +} + +let gTestsRun = 0; +Listener.prototype = { + onStartRequest: function(request, context) { + request.QueryInterface(Ci.nsIHttpChannel) + .QueryInterface(Ci.nsIHttpChannelInternal); + + do_check_eq(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) { + do_check_eq(hashKey, previousHashKeys[userContextId]); + } else { + do_check_neq(hashKey, previousHashKeys[userContextId]); + } + } + } else { + // Set the hash keys in the first round. + previousHashKeys[this.userContextId] = hashKey; + } + }, + onDataAvailable: function(request, ctx, stream, off, cnt) { + read_stream(stream, cnt); + }, + onStopRequest: function() { + 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.asyncOpen2(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..7db93f55b4 --- /dev/null +++ b/netwerk/test/unit/test_signature_extraction.js @@ -0,0 +1,206 @@ +/* -*- 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 + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "FileUtils", + "resource://gre/modules/FileUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", + "resource://gre/modules/NetUtil.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Promise", + "resource://gre/modules/Promise.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Task", + "resource://gre/modules/Task.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. If the file is then created, it + * will be removed when tests in this file finish. + */ +function getTempFile(aLeafName) { + let file = FileUtils.getFile("TmpD", [aLeafName]); + do_register_cleanup(function GTF_cleanup() { + if (file.exists()) { + file.remove(false); + } + }); + return file; +} + +/** + * 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) { + let deferred = Promise.defer(); + aSaver.observer = { + onTargetChange: function BFSO_onSaveComplete(aSaver, aTarget) + { + if (aOnTargetChangeFn) { + aOnTargetChangeFn(aTarget); + } + }, + onSaveComplete: function BFSO_onSaveComplete(aSaver, aStatus) + { + if (Components.isSuccessCode(aStatus)) { + deferred.resolve(); + } else { + deferred.reject(new Components.Exception("Saver failed.", aStatus)); + } + }, + }; + return deferred.promise; +} + +/** + * 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) { + let deferred = Promise.defer(); + 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: function () { }, + onStopRequest: function (aRequest, aContext, aStatusCode) + { + if (Components.isSuccessCode(aStatusCode)) { + deferred.resolve(); + } else { + deferred.reject(new Components.Exception(aResult)); + } + }, + }, null); + return deferred.promise; +} + +var gStillRunning = true; + +//////////////////////////////////////////////////////////////////////////////// +//// Tests + +function run_test() +{ + run_next_test(); +} + +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(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 { + let signatureInfo = saver.signatureInfo; + do_throw("Can't get signature before saver is complete."); + } catch (ex if ex.result == Cr.NS_ERROR_NOT_AVAILABLE) { } + + saver.enableSignatureInfo(); + saver.setTarget(destFile, false); + yield promiseCopyToSaver(data, saver, true); + + saver.finish(Cr.NS_OK); + yield completionPromise; + + // There's only one nsIX509CertList in the signature array. + do_check_eq(1, saver.signatureInfo.length); + let certLists = saver.signatureInfo.enumerate(); + do_check_true(certLists.hasMoreElements()); + let certList = certLists.getNext().QueryInterface(Ci.nsIX509CertList); + do_check_false(certLists.hasMoreElements()); + + // Check that it has 3 certs. + let certs = certList.getEnumerator(); + do_check_true(certs.hasMoreElements()); + let signer = certs.getNext().QueryInterface(Ci.nsIX509Cert); + do_check_true(certs.hasMoreElements()); + let issuer = certs.getNext().QueryInterface(Ci.nsIX509Cert); + do_check_true(certs.hasMoreElements()); + let root = certs.getNext().QueryInterface(Ci.nsIX509Cert); + do_check_false(certs.hasMoreElements()); + + // Check that the certs have expected strings attached. + let organization = "Microsoft Corporation"; + do_check_eq("Microsoft Corporation", signer.commonName); + do_check_eq(organization, signer.organization); + do_check_eq("Copyright (c) 2002 Microsoft Corp.", signer.organizationalUnit); + + do_check_eq("Microsoft Code Signing PCA", issuer.commonName); + do_check_eq(organization, issuer.organization); + do_check_eq("Copyright (c) 2000 Microsoft Corp.", issuer.organizationalUnit); + + do_check_eq("Microsoft Root Authority", root.commonName); + do_check_false(root.organization); + do_check_eq("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..12ec717799 --- /dev/null +++ b/netwerk/test/unit/test_simple.js @@ -0,0 +1,56 @@ +// +// Simple HTTP test: fetches page +// + +// Note: sets Cc and Ci variables + +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +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.asyncOpen2(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"); } + do_check_eq(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..bfb64e3248 --- /dev/null +++ b/netwerk/test/unit/test_sockettransportsvc_available.js @@ -0,0 +1,8 @@ +function run_test() { + try { + var sts = Components.classes["@mozilla.org/network/socket-transport-service;1"] + .getService(Components.interfaces.nsISocketTransportService); + } catch(e) {} + + do_check_true(!!sts); +} diff --git a/netwerk/test/unit/test_socks.js b/netwerk/test/unit/test_socks.js new file mode 100644 index 0000000000..57c947979d --- /dev/null +++ b/netwerk/test/unit/test_socks.js @@ -0,0 +1,525 @@ +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.nsILocalFile); + 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; + } else { + 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: function(input) + { + var len = getAvailableBytes(input); + + if (len == 0) { + print('server: client closed!'); + do_check_eq(this.state, STATE_GOT_PONG); + 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: function(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: function(input) + { + input.asyncWait(this, 0, 0, currentThread); + }, + + waitWrite: function(output) + { + output.asyncWait(this, 0, 0, currentThread); + }, + + write: function(buf) + { + this.outbuf += buf; + this.waitWrite(this.client_out); + }, + + checkSocksGreeting: function() + { + 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: function() + { + if (this.inbuf.length < 8) + return; + + do_check_eq(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: function() + { + 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: function() + { + 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: function() + { + var str = this.readString(); + + if (str == null) + return; + + this.dest_name = str; + this.sendSocks4Response(); + }, + + sendSocks4Response: function() + { + this.outbuf = '\x00\x5a\x00\x00\x00\x00\x00\x00'; + this.sendPing(); + }, + + checkSocks5Greeting: function() + { + if (this.inbuf.length < 2) + return; + var nmethods = this.inbuf[1]; + if (this.inbuf.length < 2 + nmethods) + return; + + do_check_true(nmethods >= 1); + var methods = this.inbuf.slice(2, 2 + nmethods); + do_check_true(0 in methods); + + this.inbuf = []; + this.state = STATE_WAIT_SOCKS5_REQUEST; + this.write('\x05\x00'); + }, + + checkSocks5Request: function() + { + if (this.inbuf.length < 4) + return; + + do_check_eq(this.inbuf[0], 0x05); + do_check_eq(this.inbuf[1], 0x01); + do_check_eq(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; + + 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: function() + { + 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: function() + { + print('server: sending ping'); + this.state = STATE_WAIT_PONG; + this.outbuf += "PING!"; + this.inbuf = []; + this.waitWrite(this.client_out); + this.waitRead(this.client_in); + }, + + checkPong: function() + { + var pong = buf2str(this.inbuf); + do_check_eq(pong, "PONG!"); + this.state = STATE_GOT_PONG; + this.waitRead(this.client_in); + }, + + close: function() + { + 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: function(test) + { + test.finished = false; + test.port = this.test_port_id++; + this.test_cases.push(test); + }, + + pickTest: function(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: function(client) + { + var port_id = buf2int(client.dest_port); + var test = this.pickTest(port_id); + + print('server: test finished', test.port); + do_check_true(test != null); + do_check_eq(test.expectedType || test.type, client.type); + do_check_eq(test.port, port_id); + + if (test.remote_dns) + do_check_eq(test.host, client.dest_name); + else + do_check_eq(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: function() + { + 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: function(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: function(socket) + { + }, + + close: function() + { + if (this.client_subprocess) { + try { + this.client_subprocess.kill(); + } catch (x) { + do_note_exception(x, 'Killing subprocess failed'); + } + this.client_subprocess = null; + } + for (var client of this.client_connections) + client.close(); + this.client_connections = []; + if (this.listener) { + this.listener.close(); + this.listener = null; + } + } +}; + +function test_timeout() +{ + socks_test_server.close(); + do_throw("SOCKS test took too long!"); +} + +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_timeout(120 * 1000, test_timeout); + 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..51ae824882 --- /dev/null +++ b/netwerk/test/unit/test_speculative_connect.js @@ -0,0 +1,333 @@ +/* -*- 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/. */ + +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: function(iid) { + if (iid.equals(Ci.nsIServerSocket) || + iid.equals(Ci.nsISupports)) + return this; + throw Cr.NS_ERROR_NO_INTERFACE; + }, + onSocketAccepted: function(socket, trans) { + try { this.listener.close(); } catch(e) {} + do_check_true(true); + next_test(); + }, + + onStopListening: function(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"; +} + +TestFailedStreamCallback.prototype = { + QueryInterface: function(iid) { + if (iid.equals(Ci.nsIInputStreamCallback) || + iid.equals(Ci.nsIOutputStreamCallback) || + iid.equals(Ci.nsISupports)) + return this; + throw Cr.NS_ERROR_NO_INTERFACE; + }, + processException: function(e) { + do_check_instanceof(e, Ci.nsIException); + // A refusal to connect speculatively should throw an error. + do_check_eq(e.result, Cr.NS_ERROR_CONNECTION_REFUSED); + this.transport.close(Cr.NS_BINDING_ABORTED); + return true; + }, + onOutputStreamReady: function(outstream) { + do_print("outputstream handler."); + do_check_neq(typeof(outstream), undefined); + try { + outstream.write(this.dummyContent, this.dummyContent.length); + } catch (e) { + this.processException(e); + this.next(); + return; + } + do_print("no exception on write. Wait for read."); + }, + onInputStreamReady: function(instream) { + do_print("inputstream handler."); + do_check_neq(typeof(instream), undefined); + try { + instream.available(); + } catch (e) { + this.processException(e); + this.next(); + 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 URI = ios.newURI("http://localhost:" + serv.listener.port + "/just/a/test", null, null); + ios.QueryInterface(Ci.nsISpeculativeConnect) + .speculativeConnect(URI, 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) { + do_print(host); + var sts = Cc["@mozilla.org/network/socket-transport-service;1"] + .getService(Ci.nsISocketTransportService); + do_check_neq(typeof(sts), undefined); + var transport = sts.createTransport(null, 0, host, 80, null); + do_check_neq(typeof(transport), undefined); + + transport.connectionFlags = Ci.nsISocketTransport.DISABLE_RFC1918; + transport.setTimeout(Ci.nsISocketTransport.TIMEOUT_CONNECT, 1); + transport.setTimeout(Ci.nsISocketTransport.TIMEOUT_READ_WRITE, 1); + do_check_eq(1, transport.getTimeout(Ci.nsISocketTransport.TIMEOUT_CONNECT)); + + var outStream = transport.openOutputStream(Ci.nsITransport.OPEN_UNBUFFERED,0,0); + var inStream = transport.openInputStream(0,0,0); + do_check_neq(typeof(outStream), undefined); + do_check_neq(typeof(inStream), undefined); + + var callback = new TestFailedStreamCallback(transport, host, next); + do_check_neq(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) { + do_print("Proxy: " + proxyHost); + var sts = Cc["@mozilla.org/network/socket-transport-service;1"] + .getService(Ci.nsISocketTransportService); + do_check_neq(typeof(sts), undefined); + var pps = Cc["@mozilla.org/network/protocol-proxy-service;1"] + .getService(); + do_check_neq(typeof(pps), undefined); + + var proxyInfo = pps.newProxyInfo("http", proxyHost, 8080, 0, 1, null); + do_check_neq(typeof(proxyInfo), undefined); + + var transport = sts.createTransport(null, 0, "dummyHost", 80, proxyInfo); + do_check_neq(typeof(transport), undefined); + + transport.connectionFlags = Ci.nsISocketTransport.DISABLE_RFC1918; + + transport.setTimeout(Ci.nsISocketTransport.TIMEOUT_CONNECT, 1); + do_check_eq(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); + do_check_neq(typeof(outStream), undefined); + do_check_neq(typeof(inStream), undefined); + + var callback = new TestFailedStreamCallback(transport, proxyHost, next); + do_check_neq(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; + } + do_print("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); + + do_test_pending(); + next_test(); +} + diff --git a/netwerk/test/unit/test_standardurl.js b/netwerk/test/unit/test_standardurl.js new file mode 100644 index 0000000000..c4d44f41ff --- /dev/null +++ b/netwerk/test/unit/test_standardurl.js @@ -0,0 +1,455 @@ +"use strict"; + +const StandardURL = Components.Constructor("@mozilla.org/network/standard-url;1", + "nsIStandardURL", + "init"); +const nsIStandardURL = Components.interfaces.nsIStandardURL; + +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", "path", "filePath", "query", + "ref", "directory", "fileName", "fileBaseName", "fileExtension"] + .map(function(prop) { + dump("Testing '"+ prop + "'\n"); + do_check_eq(a[prop], b[prop]); + }); + } else { + do_check_neq(a.spec, b.spec); + } + do_check_eq(expect, a.equals(b)); + do_check_eq(expect, b.equals(a)); +} + +function stringToURL(str) { + return (new StandardURL(nsIStandardURL.URLTYPE_AUTHORITY, 80, + str, "UTF-8", null)) + .QueryInterface(Components.interfaces.nsIURL); +} + +function pairToURLs(pair) { + do_check_eq(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.path = ""; + target.path = ""; + + do_check_eq(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.query = "foo"; + + do_check_eq(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.query = "foo"; + symmetricEquality(false, provided, target); + + var newProvided = Components.classes["@mozilla.org/network/io-service;1"] + .getService(Components.interfaces.nsIIOService) + .newURI("#bar", null, provided) + .QueryInterface(Components.interfaces.nsIURL); + + do_check_eq(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.ref = ref; + var b = stringToURL(result); + + do_check_eq(a.spec, b.spec); + do_check_eq(ref, b.ref); + symmetricEquality(true, a, b); + + /* Test2: starting with non-empty */ + a.ref = "yyyy"; + var c = stringToURL(before); + c.ref = "yyyy"; + symmetricEquality(true, a, c); + + /* Test3: reset the ref */ + a.ref = ""; + symmetricEquality(true, a, stringToURL(before)); + + /* Test4: verify again after reset */ + a.ref = ref; + 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.host = "[2001::1]"; + do_check_eq(url.host, "2001::1"); + + url = stringToURL("http://example.com"); + url.hostPort = "[2001::1]:30"; + do_check_eq(url.host, "2001::1"); + do_check_eq(url.port, 30); + do_check_eq(url.hostPort, "[2001::1]:30"); + + url = stringToURL("http://example.com"); + url.hostPort = "2001:1"; + do_check_eq(url.host, "0.0.7.209"); + do_check_eq(url.port, 1); + do_check_eq(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.host = "2001::1"; }, "missing brackets"); + Assert.throws(() => { url.host = "[2001::1]:20"; }, "url.host with port"); + Assert.throws(() => { url.host = "[2001::1"; }, "missing last bracket"); + Assert.throws(() => { url.host = "2001::1]"; }, "missing first bracket"); + Assert.throws(() => { url.host = "2001[::1]"; }, "bad bracket position"); + Assert.throws(() => { url.host = "[]"; }, "empty IPv6 address"); + Assert.throws(() => { url.host = "[hello]"; }, "bad IPv6 address"); + Assert.throws(() => { url.host = "[192.168.1.1]"; }, "bad IPv6 address"); + Assert.throws(() => { url.hostPort = "2001::1"; }, "missing brackets"); + Assert.throws(() => { url.hostPort = "[2001::1]30"; }, "missing : after IP"); + Assert.throws(() => { url.hostPort = "[2001:1]"; }, "bad IPv6 address"); + Assert.throws(() => { url.hostPort = "[2001:1]10"; }, "bad IPv6 address"); + Assert.throws(() => { url.hostPort = "[2001:1]10:20"; }, "bad IPv6 address"); + Assert.throws(() => { url.hostPort = "[2001:1]:10:20"; }, "bad IPv6 address"); + Assert.throws(() => { url.hostPort = "[2001:1"; }, "bad IPv6 address"); + Assert.throws(() => { url.hostPort = "2001]:1"; }, "bad IPv6 address"); + Assert.throws(() => { url.hostPort = "2001:1]"; }, "bad IPv6 address"); + Assert.throws(() => { url.hostPort = ""; }, "Empty hostPort should fail"); + Assert.throws(() => { url.hostPort = "[2001::1]:"; }, "missing port number"); + Assert.throws(() => { url.hostPort = "[2001::1]:bad"; }, "bad port number"); + run_next_test(); +}); + +add_test(function test_clearedSpec() +{ + var url = stringToURL("http://example.com/path"); + Assert.throws(() => { url.spec = "http: example"; }, "set bad spec"); + Assert.throws(() => { url.spec = ""; }, "set empty spec"); + do_check_eq(url.spec, "http://example.com/path"); + url.host = "allizom.org"; + + 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"); + do_check_eq(url.spec, "http://example.com/?a[x]=1"); + + url = stringToURL("http://example.com/?a%5Bx%5D=1"); + do_check_eq(url.spec, "http://example.com/?a%5Bx%5D=1"); + + url = stringToURL("http://[2001::1]/?a[x]=1"); + do_check_eq(url.spec, "http://[2001::1]/?a[x]=1"); + + url = stringToURL("http://[2001::1]/?a%5Bx%5D=1"); + do_check_eq(url.spec, "http://[2001::1]/?a%5Bx%5D=1"); + + // Path + url = stringToURL("http://example.com/brackets[x]/test"); + do_check_eq(url.spec, "http://example.com/brackets[x]/test"); + + url = stringToURL("http://example.com/a%5Bx%5D/test"); + do_check_eq(url.spec, "http://example.com/a%5Bx%5D/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'"); + do_check_eq(url.spec, "http://example.com/dir'/file'.ext'"); + run_next_test(); +}); + +add_test(function test_accentEncoding() +{ + var url = stringToURL("http://example.com/?hello=`"); + do_check_eq(url.spec, "http://example.com/?hello=`"); + do_check_eq(url.query, "hello=`"); + + url = stringToURL("http://example.com/?hello=%2C"); + do_check_eq(url.spec, "http://example.com/?hello=%2C"); + do_check_eq(url.query, "hello=%2C"); + run_next_test(); +}); + +add_test(function test_percentDecoding() +{ + var url = stringToURL("http://%70%61%73%74%65%62%69%6E.com"); + do_check_eq(url.spec, "http://pastebin.com/"); + + // We shouldn't unescape characters that are not allowed in the hostname. + url = stringToURL("http://example.com%0a%23.google.com/"); + do_check_eq(url.spec, "http://example.com%0a%23.google.com/"); + 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 properties = ["spec", "scheme", "userPass", "username", + "password", "hostPort", "host", "path", "ref", + "query", "fileName", "filePath", "fileBaseName", "fileExtension"]; + for (let prop of properties) { + Assert.throws(() => url[prop] = hugeString, + /NS_ERROR_MALFORMED_URI/, + `Passing a huge string to "${prop}" 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 "); + do_check_eq(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.filePath = "pa\r\n\tth"; + do_check_eq(url.spec, "http://test.com/pa%0D%0A%09th?query#hash"); + url.query = "qu\r\n\tery"; + do_check_eq(url.spec, "http://test.com/pa%0D%0A%09th?qu%0D%0A%09ery#hash"); + url.ref = "ha\r\n\tsh"; + do_check_eq(url.spec, "http://test.com/pa%0D%0A%09th?qu%0D%0A%09ery#ha%0D%0A%09sh"); + url.fileName = "fi\r\n\tle.name"; + do_check_eq(url.spec, "http://test.com/fi%0D%0A%09le.name?qu%0D%0A%09ery#ha%0D%0A%09sh"); + + run_next_test(); +}); + +add_test(function test_backslashReplacement() +{ + var url = stringToURL("http:\\\\test.com\\path/to\\file?query\\backslash#hash\\"); + do_check_eq(url.spec, "http://test.com/path/to/file?query\\backslash#hash\\"); + + url = stringToURL("http:\\\\test.com\\example.org/path\\to/file"); + do_check_eq(url.spec, "http://test.com/example.org/path/to/file"); + do_check_eq(url.host, "test.com"); + do_check_eq(url.path, "/example.org/path/to/file"); + + 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 "); + do_check_eq(url.spec, "http://example.com/"); + url.spec = "\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 "; + do_check_eq(url.spec, "http://test.com/"); + Assert.throws(() => { url.spec = "\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 "; }, "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"); + do_check_eq(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.filePath = "pa\0th"; + do_check_eq(url.spec, "http://example.com/pa%00th?query#hash"); + url.query = "qu\0ery"; + do_check_eq(url.spec, "http://example.com/pa%00th?qu%00ery#hash"); + url.ref = "ha\0sh"; + do_check_eq(url.spec, "http://example.com/pa%00th?qu%00ery#ha%00sh"); + url.fileName = "fi\0le.name"; + do_check_eq(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) { + do_check_eq(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); + do_check_eq(url.spec, spec); + } + + var url = stringToURL("resource://path/to/resource/"); + url.host = "123"; + do_check_eq(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.host = "a" + String.fromCharCode(i) + "b"; }, "Trying to set hostname containing char code: " + i); + } + for (let c of "@[]*<>|:\"") { + Assert.throws(() => { url.host = "a" + c; }, "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(); +}); 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..12c6191430 --- /dev/null +++ b/netwerk/test/unit/test_standardurl_default_port.js @@ -0,0 +1,51 @@ +/* -*- 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"; + +var Cc = Components.classes; +var Ci = Components.interfaces; + +const StandardURL = Components.Constructor("@mozilla.org/network/standard-url;1", + "nsIStandardURL", + "init"); +function run_test() { + function stringToURL(str) { + return (new StandardURL(Ci.nsIStandardURL.URLTYPE_AUTHORITY, 80, + str, "UTF-8", null)) + .QueryInterface(Ci.nsIStandardURL); + } + + // Create a nsStandardURL: + var origUrlStr = "http://foo.com/"; + var stdUrl = stringToURL(origUrlStr); + var stdUrlAsUri = stdUrl.QueryInterface(Ci.nsIURI); + do_check_eq(-1, stdUrlAsUri.port); + + // Changing default port shouldn't adjust the value returned by "port", + // or the string representation. + stdUrl.setDefaultPort(100); + do_check_eq(-1, stdUrlAsUri.port); + do_check_eq(stdUrlAsUri.spec, origUrlStr); + + // Changing port directly should update .port and .spec, though: + stdUrlAsUri.port = "200"; + do_check_eq(200, stdUrlAsUri.port); + do_check_eq(stdUrlAsUri.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: + stdUrl.setDefaultPort(200); + do_check_eq(-1, stdUrlAsUri.port); + do_check_eq(stdUrlAsUri.spec, origUrlStr); + + // And further changes to default port should not make custom port reappear. + stdUrl.setDefaultPort(300); + do_check_eq(-1, stdUrlAsUri.port); + do_check_eq(stdUrlAsUri.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..cc0016964c --- /dev/null +++ b/netwerk/test/unit/test_standardurl_port.js @@ -0,0 +1,56 @@ +var Cc = Components.classes; +var Ci = Components.interfaces; + +function run_test() { + function makeURI(aURLSpec, aCharset) { + var ios = Cc["@mozilla.org/network/io-service;1"]. + getService(Ci.nsIIOService); + return ios.newURI(aURLSpec, aCharset, null); + } + + var httpURI = makeURI("http://foo.com"); + do_check_eq(-1, httpURI.port); + + // Setting to default shouldn't cause a change + httpURI.port = 80; + do_check_eq(-1, httpURI.port); + + // Setting to default after setting to non-default shouldn't cause a change (bug 403480) + httpURI.port = 123; + do_check_eq(123, httpURI.port); + httpURI.port = 80; + do_check_eq(-1, httpURI.port); + do_check_false(/80/.test(httpURI.spec)); + + // URL parsers shouldn't set ports to default value (bug 407538) + httpURI.spec = "http://foo.com:81"; + do_check_eq(81, httpURI.port); + httpURI.spec = "http://foo.com:80"; + do_check_eq(-1, httpURI.port); + do_check_false(/80/.test(httpURI.spec)); + + httpURI = makeURI("http://foo.com"); + do_check_eq(-1, httpURI.port); + do_check_false(/80/.test(httpURI.spec)); + + httpURI = makeURI("http://foo.com:80"); + do_check_eq(-1, httpURI.port); + do_check_false(/80/.test(httpURI.spec)); + + httpURI = makeURI("http://foo.com:80"); + do_check_eq(-1, httpURI.port); + do_check_false(/80/.test(httpURI.spec)); + + httpURI = makeURI("https://foo.com"); + do_check_eq(-1, httpURI.port); + do_check_false(/443/.test(httpURI.spec)); + + httpURI = makeURI("https://foo.com:443"); + do_check_eq(-1, httpURI.port); + do_check_false(/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..6354be5d28 --- /dev/null +++ b/netwerk/test/unit/test_streamcopier.js @@ -0,0 +1,53 @@ +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. + do_check_true(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: function(aRequest, aContext) { + do_check_eq(aContext.wrappedJSObject, ctx); + }, + onStopRequest: function(aRequest, aContext, aStatusCode) { + do_check_eq(aStatusCode, 0); + do_check_eq(aContext.wrappedJSObject, ctx); + 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) { + do_check_eq(e.result, Components.results.NS_BASE_STREAM_CLOSED); + } + do_check_eq(result, testStr); + do_test_finished(); + } + }; + + streamCopier.asyncCopy(observer, ctx); + do_test_pending(); +} 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..f41932a467 --- /dev/null +++ b/netwerk/test/unit/test_suspend_channel_before_connect.js @@ -0,0 +1,102 @@ + +var CC = Components.Constructor; + +Cu.import("resource://gre/modules/NetUtil.jsm"); + +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(Components.interfaces.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: function(socket, trans) { + do_check_true(false, "Socket should not have tried to connect!"); + }, + + onStopListening: function(socket) { + }, + + stop: function() { + try { this.listener.close(); } catch(ignore) {} + } +} + +var requestListenerObserver = { + + QueryInterface: function queryinterface(iid) { + if (iid.equals(Ci.nsISupports) || + iid.equals(Ci.nsIObserver)) + return this; + throw Components.results.NS_ERROR_NO_INTERFACE; + }, + + observe: function(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, ctx) { + }, + + onDataAvailable: function test_ODA() { + do_throw("Should not get any data!"); + }, + + onStopRequest: function test_onStopR(request, ctx, status) { + do_execute_soon(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() { + + serv = new TestServer(); + + obs.addObserver(requestListenerObserver, "http-on-modify-request", false); + var chan = NetUtil.newChannel({ + uri:"http://localhost:" + serv.port, + loadUsingSystemPrincipal: true + }); + chan.asyncOpen2(listener); + + do_register_cleanup(function(){ serv.stop(); }); +}); + +function run_test() { + run_next_test(); +} 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..a4f7c221e8 --- /dev/null +++ b/netwerk/test/unit/test_suspend_channel_on_modified.js @@ -0,0 +1,175 @@ +// This file tests async handling of a channel suspended in http-on-modify-request. + +var CC = Components.Constructor; + +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +var obs = Cc["@mozilla.org/observer-service;1"] + .getService(Ci.nsIObserverService); + +var ios = Cc["@mozilla.org/network/io-service;1"] + .getService(Components.interfaces.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); + do_check_true(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); + do_check_true(true, "Received expected request."); +} + +function onModifyListener(callback) { + obs.addObserver({ + observe: function(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", false); +} + +function startChannelRequest(baseUrl, flags, expectedResponse=null) { + var chan = NetUtil.newChannel({ + uri: baseUrl, + loadUsingSystemPrincipal: true + }); + chan.asyncOpen2(new ChannelListener((request, data, context) => { + if (expectedResponse) { + do_check_eq(data, expectedResponse); + } else { + do_check_true(!!!data, "no response"); + } + do_execute_soon(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(); + + do_register_cleanup(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..bad8047fe0 --- /dev/null +++ b/netwerk/test/unit/test_synthesized_response.js @@ -0,0 +1,243 @@ +"use strict"; + +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +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, null, null); +} + +// ensure the cache service is prepped when running the test +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: XPCOMUtils.generateQI([Ci.nsINetworkInterceptController, + Ci.nsIInterfaceRequestor, + Ci.nsIProgressEventSink]), + getInterface: function(iid) { + return this.QueryInterface(iid); + }, + onProgress: function(request, context, progress, progressMax) { + gotOnProgress = true; + }, + onStatus: function(request, context, status, statusArg) { + gotOnStatus = true; + }, + shouldPrepareForIntercept: function() { + do_check_eq(this.numChecks, 0); + this.numChecks++; + return true; + }, + channelIntercepted: function(channel) { + channel.QueryInterface(Ci.nsIInterceptedChannel); + if (body) { + var synthesized = Cc["@mozilla.org/io/string-input-stream;1"] + .createInstance(Ci.nsIStringInputStream); + synthesized.data = body; + + NetUtil.asyncCopy(synthesized, channel.responseBody, function() { + channel.finishSynthesizedResponse(''); + }); + } + if (cb) { + cb(channel); + } + return { + dispatch: function() { } + }; + }, + }; + 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) { + do_check_eq(buffer, NON_REMOTE_BODY); + do_check_true(gotOnStatus); + do_check_true(gotOnProgress); + run_next_test(); +} + +function handle_synthesized_response_2(request, buffer) { + do_check_eq(buffer, NON_REMOTE_BODY_2); + do_check_true(gotOnStatus); + do_check_true(gotOnProgress); + run_next_test(); +} + +function handle_remote_response(request, buffer) { + do_check_eq(buffer, REMOTE_BODY); + do_check_true(gotOnStatus); + do_check_true(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.asyncOpen2(new ChannelListener(handle_remote_response, null)); +}); + +// synthesize a response +add_test(function() { + var chan = make_channel(URL + '/body', NON_REMOTE_BODY); + chan.asyncOpen2(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.asyncOpen2(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.asyncOpen2(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; + NetUtil.asyncCopy(synthesized, channel.responseBody, function() { + channel.synthesizeHeader("Content-Length", NON_REMOTE_BODY.length); + channel.finishSynthesizedResponse(''); + }); + }); + }); + chan.asyncOpen2(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.asyncOpen2(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; + + NetUtil.asyncCopy(synthesized, intercepted.responseBody, function() { + // 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.finishSynthesizedResponse(''); + }); + }); + chan.asyncOpen2(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.cancel(Cr.NS_BINDING_ABORTED); + }); + chan.asyncOpen2(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.cancel(); + } catch (x) { + gotexception = true; + } + do_check_true(gotexception); + }); + }); + chan.asyncOpen2(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; + + NetUtil.asyncCopy(synthesized, intercepted.responseBody, function() { + let channel = intercepted.channel; + intercepted.finishSynthesizedResponse(''); + channel.cancel(Cr.NS_BINDING_ABORTED); + }); + }); + chan.asyncOpen2(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; + + NetUtil.asyncCopy(synthesized, intercepted.responseBody, function() { + intercepted.channel.cancel(Cr.NS_BINDING_ABORTED); + intercepted.finishSynthesizedResponse(''); + }); + }); + chan.asyncOpen2(new ChannelListener(run_next_test, null, + CL_EXPECT_FAILURE | CL_ALLOW_UNKNOWN_CL)); +}); + +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..97c119b994 --- /dev/null +++ b/netwerk/test/unit/test_throttlechannel.js @@ -0,0 +1,41 @@ +// Test nsIThrottledInputChannel interface. + +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +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(Components.interfaces.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.asyncOpen2(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..fdfa80d1b0 --- /dev/null +++ b/netwerk/test/unit/test_throttlequeue.js @@ -0,0 +1,23 @@ +// Test ThrottleQueue initialization. + +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..afb8278949 --- /dev/null +++ b/netwerk/test/unit/test_throttling.js @@ -0,0 +1,57 @@ +// Test nsIThrottledInputChannel interface. + +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +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); + mime.addContentLength = true; + + 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.asyncOpen2(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..6f3170af02 --- /dev/null +++ b/netwerk/test/unit/test_tldservice_nextsubdomain.js @@ -0,0 +1,28 @@ +var Cc = Components.classes; +var Ci = Components.interfaces; + +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); + do_check_eq(r, test.result); + } catch (e) { + do_check_true(test.throw); + } + }); +} diff --git a/netwerk/test/unit/test_tls_server.js b/netwerk/test/unit/test_tls_server.js new file mode 100644 index 0000000000..d805359c7b --- /dev/null +++ b/netwerk/test/unit/test_tls_server.js @@ -0,0 +1,237 @@ +/* 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 { Services } = Cu.import("resource://gre/modules/Services.jsm", {}); +const { NetUtil } = Cu.import("resource://gre/modules/NetUtil.jsm", {}); +const { Promise: promise } = + Cu.import("resource://gre/modules/Promise.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 run_test() { + run_next_test(); +} + +function getCert() { + let deferred = promise.defer(); + certService.getOrCreateCert("tls-test", { + handleCert: function(c, rv) { + if (rv) { + deferred.reject(rv); + return; + } + deferred.resolve(c); + } + }); + return deferred.promise; +} + +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: function(socket, transport) { + do_print("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: function(socket, status) { + do_print("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: function(input) { + NetUtil.asyncCopy(input, output); + } + }, 0, 0, Services.tm.currentThread); + }, + onStopListening: function() {} + }; + + tlsServer.setSessionCache(false); + tlsServer.setSessionTickets(false); + tlsServer.setRequestClientCertificate(clientCertificateConfig); + + 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, cert, expectingBadCertAlert) { + let SSL_ERROR_BASE = Ci.nsINSSErrorsService.NSS_SSL_ERROR_BASE; + let SSL_ERROR_BAD_CERT_ALERT = SSL_ERROR_BASE + 17; + let transport = + socketTransportService.createTransport(["ssl"], 1, "127.0.0.1", port, null); + let input; + let output; + + let inputDeferred = promise.defer(); + let outputDeferred = promise.defer(); + + let handler = { + + onTransportStatus: function(transport, status) { + if (status === Ci.nsISocketTransport.STATUS_CONNECTED_TO) { + output.asyncWait(handler, 0, 0, Services.tm.currentThread); + } + }, + + onInputStreamReady: function(input) { + try { + let data = NetUtil.readInputStreamToString(input, input.available()); + equal(data, "HELLO", "Echoed data received"); + input.close(); + output.close(); + ok(!expectingBadCertAlert, "No bad cert alert expected"); + inputDeferred.resolve(); + } catch (e) { + let errorCode = -1 * (e.result & 0xFFFF); + if (expectingBadCertAlert && errorCode == SSL_ERROR_BAD_CERT_ALERT) { + inputDeferred.resolve(); + } else { + inputDeferred.reject(e); + } + } + }, + + onOutputStreamReady: function(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); + do_print("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) { + do_print("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, + expectingBadCertAlert: false +}, { + expectingPeerCert: true, + clientCertificateConfig: Ci.nsITLSServerSocket.REQUIRE_ALWAYS, + sendClientCert: false, + expectingBadCertAlert: true +}, { + expectingPeerCert: true, + clientCertificateConfig: Ci.nsITLSServerSocket.REQUEST_ALWAYS, + sendClientCert: true, + expectingBadCertAlert: false +}, { + expectingPeerCert: false, + clientCertificateConfig: Ci.nsITLSServerSocket.REQUEST_ALWAYS, + sendClientCert: false, + expectingBadCertAlert: false +}, { + expectingPeerCert: false, + clientCertificateConfig: Ci.nsITLSServerSocket.REQUEST_NEVER, + sendClientCert: true, + expectingBadCertAlert: false +}, { + expectingPeerCert: false, + clientCertificateConfig: Ci.nsITLSServerSocket.REQUEST_NEVER, + sendClientCert: false, + expectingBadCertAlert: 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(function*() { + let cert = yield 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 port = startServer(cert, + t.expectingPeerCert, + t.clientCertificateConfig, + v.version, + v.versionStr); + storeCertOverride(port, cert); + yield startClient(port, t.sendClientCert ? cert : null, t.expectingBadCertAlert); + } + } +}); + +do_register_cleanup(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..b63c0189bd --- /dev/null +++ b/netwerk/test/unit/test_tls_server_multiple_clients.js @@ -0,0 +1,141 @@ +/* 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 { Services } = Cu.import("resource://gre/modules/Services.jsm", {}); +const { NetUtil } = Cu.import("resource://gre/modules/NetUtil.jsm", {}); +const { Promise: promise } = + Cu.import("resource://gre/modules/Promise.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 run_test() { + run_next_test(); +} + +function getCert() { + let deferred = promise.defer(); + certService.getOrCreateCert("tls-test", { + handleCert: function(c, rv) { + if (rv) { + deferred.reject(rv); + return; + } + deferred.resolve(c); + } + }); + return deferred.promise; +} + +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: function(socket, transport) { + do_print("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: function(socket, status) { + do_print("TLS handshake done"); + + input.asyncWait({ + onInputStreamReady: function(input) { + NetUtil.asyncCopy(input, output); + } + }, 0, 0, Services.tm.currentThread); + }, + onStopListening: function() {} + }; + + tlsServer.setSessionCache(true); + 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"], 1, "127.0.0.1", port, null); + let input; + let output; + + let inputDeferred = promise.defer(); + let outputDeferred = promise.defer(); + + let handler = { + + onTransportStatus: function(transport, status) { + if (status === Ci.nsISocketTransport.STATUS_CONNECTED_TO) { + output.asyncWait(handler, 0, 0, Services.tm.currentThread); + } + }, + + onInputStreamReady: function(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: function(output) { + try { + output.write("HELLO", 5); + do_print("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(function*() { + let cert = yield getCert(); + ok(!!cert, "Got self-signed cert"); + let port = startServer(cert); + storeCertOverride(port, cert); + yield startClient(port); + yield 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..00ccbb1277 --- /dev/null +++ b/netwerk/test/unit/test_traceable_channel.js @@ -0,0 +1,150 @@ +// 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. + +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +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: function(request, context) { + dump("*** tracing listener onStartRequest\n"); + + gotOnStartRequest = true; + + request.QueryInterface(Components.interfaces.nsIHttpChannelInternal); + +// local/remote addresses broken in e10s: disable for now + do_check_eq(request.localAddress, "127.0.0.1"); + do_check_eq(request.localPort > 0, true); + do_check_neq(request.localPort, PORT); + do_check_eq(request.remoteAddress, "127.0.0.1"); + do_check_eq(request.remotePort, PORT); + + // Make sure listener can't be replaced after OnStartRequest was called. + request.QueryInterface(Components.interfaces.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: function(request, context, statusCode) { + dump("*** tracing listener onStopRequest\n"); + + do_check_eq(gotOnStartRequest, true); + + try { + var sin = Components.classes["@mozilla.org/scriptableinputstream;1"]. + createInstance(Ci.nsIScriptableInputStream); + + streamSink.close(); + var input = pipe.inputStream; + sin.init(input); + do_check_eq(sin.available(), originalBody.length); + + var result = sin.read(originalBody.length); + do_check_eq(result, originalBody); + + input.close(); + } catch (e) { + dump("TracingListener.onStopRequest swallowing exception: " + e + "\n"); + } finally { + httpserver.stop(do_test_finished); + } + }, + + QueryInterface: function(iid) { + if (iid.equals(Components.interfaces.nsIRequestObserver) || + iid.equals(Components.interfaces.nsISupports) + ) + return this; + throw Components.results.NS_NOINTERFACE; + }, + + listener: null +} + + +function HttpResponseExaminer() {} + +HttpResponseExaminer.prototype = { + register: function() { + Cc["@mozilla.org/observer-service;1"]. + getService(Components.interfaces.nsIObserverService). + addObserver(this, "http-on-examine-response", true); + dump("Did HttpResponseExaminer.register\n"); + }, + + // Replace channel's listener. + observe: function(subject, topic, data) { + dump("In HttpResponseExaminer.observe\n"); + try { + subject.QueryInterface(Components.interfaces.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: function(iid) { + if (iid.equals(Components.interfaces.nsIObserver) || + iid.equals(Components.interfaces.nsISupportsWeakReference) || + iid.equals(Components.interfaces.nsISupports)) + return this; + throw Components.results.NS_NOINTERFACE; + } +} + +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(Components.interfaces.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.asyncOpen2(new ChannelListener(channel_finished)); + do_test_pending(); +} diff --git a/netwerk/test/unit/test_udp_multicast.js b/netwerk/test/unit/test_udp_multicast.js new file mode 100644 index 0000000000..0afa9c5b22 --- /dev/null +++ b/netwerk/test/unit/test_udp_multicast.js @@ -0,0 +1,114 @@ +// Bug 960397: UDP multicast options + +var { Constructor: CC } = Components; + +const UDPSocket = CC("@mozilla.org/network/udp-socket;1", + "nsIUDPSocket", + "init"); +const { Promise: promise } = Cu.import("resource://gre/modules/Promise.jsm", {}); +Cu.import("resource://gre/modules/Services.jsm"); + +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; +const isWinXP = ua.indexOf("Windows NT 5.1") != -1; + +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); + + let deferred = promise.defer(); + + socket.asyncListen({ + onPacketReceived: function(s, message) { + do_print("Received on port " + socket.port); + do_check_eq(message.data, ping); + socket.close(); + deferred.resolve(message.data); + }, + onStopListening: function(socket, status) {} + }); + + do_print("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(); + deferred.reject(); + }, TIMEOUT, Ci.nsITimer.TYPE_ONE_SHOT); + + return deferred.promise; +} + +add_test(() => { + do_print("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(() => { + do_print("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 + ); +}); + +// The following multicast interface test doesn't work on Windows XP, as it +// appears to allow packets no matter what address is given, so we'll skip the +// test there. +if (!isWinXP) { + add_test(() => { + do_print("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(() => { + do_print("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..c96be003ab --- /dev/null +++ b/netwerk/test/unit/test_udpsocket.js @@ -0,0 +1,63 @@ +/* -*- 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"; + +Cu.import("resource://gre/modules/Services.jsm"); + +const HELLO_WORLD = "Hello World"; + +add_test(function test_udp_message_raw_data() { + do_print("test for nsIUDPMessage.rawData"); + + let socket = Cc["@mozilla.org/network/udp-socket;1"].createInstance(Ci.nsIUDPSocket); + + socket.init(-1, true, Services.scriptSecurityManager.getSystemPrincipal()); + do_print("Port assigned : " + socket.port); + socket.asyncListen({ + QueryInterface : XPCOMUtils.generateQI([Ci.nsIUDPSocketListener]), + onPacketReceived : function(aSocket, aMessage){ + let recv_data = String.fromCharCode.apply(null, aMessage.rawData); + do_check_eq(recv_data, HELLO_WORLD); + do_check_eq(recv_data, aMessage.data); + socket.close(); + run_next_test(); + }, + onStopListening: function(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, rawData.length); + do_check_eq(written, HELLO_WORLD.length); +}); + +add_test(function test_udp_send_stream() { + do_print("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 : XPCOMUtils.generateQI([Ci.nsIUDPSocketListener]), + onPacketReceived : function(aSocket, aMessage){ + let recv_data = String.fromCharCode.apply(null, aMessage.rawData); + do_check_eq(recv_data, HELLO_WORLD); + socket.close(); + run_next_test(); + }, + onStopListening: function(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); +}); + +function run_test(){ + 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..d1e9494cd7 --- /dev/null +++ b/netwerk/test/unit/test_unescapestring.js @@ -0,0 +1,31 @@ +const ONLY_NONASCII = Components.interfaces.nsINetUtil.ESCAPE_URL_ONLY_NONASCII; +const SKIP_CONTROL = Components.interfaces.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 = Components.classes["@mozilla.org/network/util;1"] + .getService(Components.interfaces.nsINetUtil); + + for (var i = 0; i < tests.length; ++i) { + dump("Test " + i + " (" + tests[i][0] + ", " + tests[i][2] + ")\n"); + do_check_eq(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..5dda0c8644 --- /dev/null +++ b/netwerk/test/unit/test_unix_domain.js @@ -0,0 +1,545 @@ +// Exercise Unix domain sockets. + +var CC = Components.Constructor; + +const UnixServerSocket = CC("@mozilla.org/network/server-socket;1", + "nsIServerSocket", + "initWithFilename"); + +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; + } + + 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); + + 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'); + do_print("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. + do_print("creating socket: " + socketName.path); + let server = new UnixServerSocket(socketName, allPermissions, -1); + server.asyncListen({ + onSocketAccepted: function(aServ, aTransport) { + do_print("called test_echo's onSocketAccepted"); + log += 'a'; + + do_check_eq(aServ, server); + + let connection = aTransport; + + // Check the server socket's self address. + let connectionSelfAddr = connection.getScriptableSelfAddr(); + do_check_eq(connectionSelfAddr.family, Ci.nsINetAddr.FAMILY_LOCAL); + do_check_eq(connectionSelfAddr.address, socketName.path); + + // The client socket is anonymous, so the server transport should + // have an empty peer address. + do_check_eq(connection.host, ''); + do_check_eq(connection.port, 0); + let connectionPeerAddr = connection.getScriptablePeerAddr(); + do_check_eq(connectionPeerAddr.family, Ci.nsINetAddr.FAMILY_LOCAL); + do_check_eq(connectionPeerAddr.address, ''); + + let serverAsyncInput = connection.openInputStream(0, 0, 0).QueryInterface(Ci.nsIAsyncInputStream); + let serverOutput = connection.openOutputStream(0, 0, 0); + + serverAsyncInput.asyncWait(function (aStream) { + do_print("called test_echo's server's onInputStreamReady"); + let serverScriptableInput = new ScriptableInputStream(aStream); + + // Receive data from the client, and send back a response. + do_check_eq(serverScriptableInput.readBytes(17), "Mervyn Murgatroyd"); + do_print("server has read message from client"); + serverOutput.write("Ruthven Murgatroyd", 18); + do_print("server has written to client"); + }, 0, 0, threadManager.currentThread); + }, + + onStopListening: function(aServ, aStatus) { + do_print("called test_echo's onStopListening"); + log += 's'; + + do_check_eq(aServ, server); + do_check_eq(log, 'acs'); + + run_next_test(); + } + }); + + // Create a client socket, and connect to the server. + let client = socketTransportService.createUnixDomainTransport(socketName); + do_check_eq(client.host, socketName.path); + do_check_eq(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); + do_print("client has written to server"); + + clientAsyncInput.asyncWait(function (aStream) { + do_print("called test_echo's client's onInputStreamReady"); + log += 'c'; + + do_check_eq(aStream, clientAsyncInput); + + // Now that the connection has been established, we can check the + // transport's self and peer addresses. + let clientSelfAddr = client.getScriptableSelfAddr(); + do_check_eq(clientSelfAddr.family, Ci.nsINetAddr.FAMILY_LOCAL); + do_check_eq(clientSelfAddr.address, ''); + + do_check_eq(client.host, socketName.path); // re-check, but hey + let clientPeerAddr = client.getScriptablePeerAddr(); + do_check_eq(clientPeerAddr.family, Ci.nsINetAddr.FAMILY_LOCAL); + do_check_eq(clientPeerAddr.address, socketName.path); + + do_check_eq(clientInput.readBytes(18), "Ruthven Murgatroyd"); + do_print("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) { + do_print("called test_no_such_socket's onInputStreamReady"); + + do_check_eq(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. + let server = 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) { + do_print("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) { + do_print("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) { + do_print("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) { + do_print("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); + do_check_eq(serverScriptableInput.readBytes(8), "Hanratty"); + serverOutput.write("Ferlingatti", 11); + }, 0, 0, threadManager.currentThread); + } + + function client3InputStreamReady(aStream) { + do_print("called client3's onInputStreamReady"); + log += '3'; + + let client3Input = new ScriptableInputStream(aStream); + + do_check_eq(client3Input.readBytes(11), "Ferlingatti"); + + client3.close(Cr.NS_OK); + server.close(); + } + + function stopListening(aServ, aStatus) { + do_print("called test_connect_permission's server's stopListening"); + log += 's'; + + do_check_eq(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: 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) { + do_print("test_keep_when_offline: onAccepted called"); + log += 'a'; + do_check_eq(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'; + do_print("test_keep_when_offline: clientReady called: " + log); + do_check_eq(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); + do_check_eq(ex.result, Cr.NS_BASE_STREAM_CLOSED); + + do_print("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. + do_check_eq(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'; + do_print("test_keep_when_offline: serverReady called: " + log); + do_check_eq(aStream, serverInput); + + // Check the message from the client. + do_check_eq(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. + do_print("closing server transport"); + server.close(Cr.NS_OK); + log += '('; + } + } + + // We have stopped listening. + function onStopListening(aServ, aStatus) { + do_print("test_keep_when_offline: onStopListening called"); + log += 'L'; + do_check_eq(log, 'acscscscscsocscscscscs(c)L'); + + do_check_eq(aServ, listener); + do_check_eq(aStatus, Cr.NS_BINDING_ABORTED); + + run_next_test(); + } +} diff --git a/netwerk/test/unit/test_websocket_offline.js b/netwerk/test/unit/test_websocket_offline.js new file mode 100644 index 0000000000..f44360221f --- /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/. */ + +Cu.import("resource://gre/modules/Services.jsm"); + +// 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: function(aContext, aSize) {}, + onBinaryMessageAvailable: function(aContext, aMsg) {}, + onMessageAvailable: function(aContext, aMsg) {}, + onServerClose: function(aContext, aCode, aReason) {}, + onStart: function(aContext) + { + // onStart is not called when a connection fails + do_check_true(false); + }, + onStop: function(aContext, aStatusCode) + { + do_check_neq(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(Components.interfaces.nsIWebSocketChannel); + chan.initLoadInfo(null, // aLoadingNode + Services.scriptSecurityManager.getSystemPrincipal(), + null, // aTriggeringPrincipal + Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL, + Ci.nsIContentPolicy.TYPE_WEBSOCKET); + + var uri = Services.io.newURI(url, null, null); + 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..1f49a1aec0 --- /dev/null +++ b/netwerk/test/unit/test_xmlhttprequest.js @@ -0,0 +1,54 @@ + +Cu.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 = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"] + .createInstance(Ci.nsIXMLHttpRequest); + xhr.open("GET", "http://localhost:" + + httpserver.identity.primaryPort + testpath, async); + return xhr; +} + +function checkResults(xhr) +{ + if (xhr.readyState != 4) + return false; + + do_check_eq(xhr.status, 200); + do_check_eq(xhr.responseText, httpbody); + + var root_node = xhr.responseXML.getElementsByTagName('root').item(0); + do_check_eq(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); + }, false); + 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..0cbcbc423d --- /dev/null +++ b/netwerk/test/unit/xpcshell.ini @@ -0,0 +1,369 @@ +[DEFAULT] +head = head_channels.js head_cache.js head_cache2.js +tail = +support-files = + CA.cert.der + client_cert_chooser.js + client_cert_chooser.manifest + 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 + ../../dns/effective_tld_names.dat + +[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] +# GC, that this patch is dependent on, doesn't work well on Android. +skip-if = os == "android" +[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_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_assoc.js] +[test_auth_jar.js] +[test_auth_proxy.js] +[test_authentication.js] +[test_authpromptwrapper.js] +[test_auth_dialog_permission.js] +[test_backgroundfilesaver.js] +# Runs for a long time, causing intermittent time-outs on Android, bug 995686 +requesttimeoutfactor = 2 +[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_bug376660.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_bug455598.js] +[test_bug468426.js] +[test_bug468594.js] +[test_bug470716.js] +[test_bug477578.js] +[test_bug479413.js] +[test_bug479485.js] +[test_bug482601.js] +[test_bug484684.js] +[test_bug490095.js] +# Bug 675039: intermittent fail on Android-armv6 +skip-if = os == "android" +[test_bug504014.js] +[test_bug510359.js] +[test_bug515583.js] +[test_bug528292.js] +[test_bug536324_64bit_content_length.js] +[test_bug540566.js] +[test_bug543805.js] +[test_bug553970.js] +[test_bug561042.js] +# Bug 675039: test fails on Android 4.0 +skip-if = os == "android" +[test_bug561276.js] +[test_bug580508.js] +[test_bug586908.js] +[test_bug596443.js] +[test_bug618835.js] +[test_bug633743.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_bug667907.js] +[test_bug667818.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_bug1218029.js] +[test_udpsocket.js] +[test_doomentry.js] +[test_cacheflags.js] +[test_cache_jar.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_dns_cancel.js] +[test_dns_per_interface.js] +[test_data_protocol.js] +[test_dns_service.js] +[test_dns_offline.js] +[test_dns_onion.js] +[test_dns_localredirect.js] +[test_dns_proxy_bypass.js] +[test_duplicate_headers.js] +[test_chunked_responses.js] +[test_content_length_underrun.js] +[test_event_sink.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_partial_inputstream.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_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] +# IDNA2008 depends on ICU, not available on android +skip-if = os == "android" +[test_immutable.js] +skip-if = !hasNode +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_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_original_sent_received_head.js] +[test_parse_content_type.js] +[test_permmgr.js] +[test_plaintext_sniff.js] +[test_post.js] +[test_private_necko_channel.js] +[test_private_cookie_changed.js] +[test_progress.js] +[test_protocolproxyservice.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] +# Bug 675039: test fails consistently on Android +fail-if = os == "android" +[test_redirect-caching_passing.js] +[test_redirect_canceled.js] +[test_redirect_failure.js] +# Bug 675039: test fails consistently on Android +fail-if = os == "android" +[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_reentrancy.js] +[test_reopen.js] +[test_resumable_channel.js] +[test_resumable_truncate.js] +[test_safeoutputstream.js] +[test_simple.js] +[test_sockettransportsvc_available.js] +[test_socks.js] +# 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] +skip-if = !hasNode +run-sequentially = node server exceptions dont replay well +[test_altsvc.js] +skip-if = !hasNode +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] +# The xpcshell temp directory on Android doesn't seem to let us create +# Unix domain sockets. (Perhaps it's a FAT filesystem?) +skip-if = os == "android" +[test_addr_in_use_error.js] +[test_about_networking.js] +[test_ping_aboutnetworking.js] +[test_referrer.js] +[test_referrer_policy.js] +[test_predictor.js] +# Android version detection w/in gecko does not work right on infra, so we just +# disable this test on all android versions, even though it's enabled on 2.3+ in +# the wild. +skip-if = os == "android" +[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] +# The local cert service used by this test is not currently shipped on Android +# Disabled on XP in bug 1190674 for intermittent failures +skip-if = os == "android" || (os == "win" && (os_version == "5.1" || os_version == "5.2")) +reason = bug 1190674 +firefox-appdir = browser +[test_tls_server.js] +# The local cert service used by this test is not currently shipped on Android +# Disabled on XP in bug 1190674 for intermittent failures +skip-if = os == "android" || (os == "win" && (os_version == "5.1" || os_version == "5.2")) +reason = bug 1190674 +firefox-appdir = browser +[test_tls_server_multiple_clients.js] +# The local cert service used by this test is not currently shipped on Android +skip-if = os == "android" +[test_1073747.js] +[test_safeoutputstream_append.js] +[test_suspend_channel_before_connect.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] +[test_bug464591.js] +[test_alt-data_simple.js] +[test_alt-data_stream.js] +[test_cache-control_request.js] +[test_bug1279246.js] +[test_throttlequeue.js] +[test_throttlechannel.js] +[test_throttling.js] +[test_separate_connections.js] diff --git a/netwerk/test/unit_ipc/child_app_offline_notifications.js b/netwerk/test/unit_ipc/child_app_offline_notifications.js new file mode 100644 index 0000000000..870c22b397 --- /dev/null +++ b/netwerk/test/unit_ipc/child_app_offline_notifications.js @@ -0,0 +1,43 @@ +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +function is_app_offline(appId) { + let ioservice = Cc['@mozilla.org/network/io-service;1']. + getService(Ci.nsIIOService); + return ioservice.isAppOffline(appId); +} + +var events_observed_no = 0; + +// Holds the last observed app-offline event +var info = null; +function observer(aSubject, aTopic, aData) { + events_observed_no++; + info = aSubject.QueryInterface(Ci.nsIAppOfflineInfo); + dump("ChildObserver - subject: {" + aSubject.appId + ", " + aSubject.mode + "} "); +} + +// Add observer for the app-offline notification +function run_test() { + Services.obs.addObserver(observer, "network:app-offline-status-changed", false); +} + +// Chech that the app has the proper offline status +function check_status(appId, status) +{ + do_check_eq(is_app_offline(appId), status == Ci.nsIAppOfflineInfo.OFFLINE); +} + +// Check that the app has the proper offline status +// and that the correct notification has been received +function check_notification_and_status(appId, status) { + do_check_eq(info.appId, appId); + do_check_eq(info.mode, status); + do_check_eq(is_app_offline(appId), status == Ci.nsIAppOfflineInfo.OFFLINE); +} + +// Remove the observer from the child process +function finished() { + Services.obs.removeObserver(observer, "network:app-offline-status-changed"); + do_check_eq(events_observed_no, 2); +} diff --git a/netwerk/test/unit_ipc/child_channel_id.js b/netwerk/test/unit_ipc/child_channel_id.js new file mode 100644 index 0000000000..55cb2fd145 --- /dev/null +++ b/netwerk/test/unit_ipc/child_channel_id.js @@ -0,0 +1,45 @@ +/** + * Send HTTP requests and notify the parent about their channelId + */ + +Cu.import("resource://gre/modules/NetUtil.jsm"); + +let shouldQuit = false; + +function run_test() { + // keep the event loop busy and the test alive until a "finish" command + // is issued by parent + do_timeout(100, function keepAlive() { + if (!shouldQuit) { + do_timeout(100, keepAlive); + } + }); +} + +function makeRequest(uri) { + let requestChannel = NetUtil.newChannel({uri, loadUsingSystemPrincipal: true}); + requestChannel.asyncOpen2(new ChannelListener(checkResponse, requestChannel)); + requestChannel.QueryInterface(Ci.nsIHttpChannel); + dump(`Child opened request: ${uri}, channelId=${requestChannel.channelId}\n`); +} + +function checkResponse(request, buffer, requestChannel) { + // notify the parent process about the original request channel + requestChannel.QueryInterface(Ci.nsIHttpChannel); + do_send_remote_message(`request:${requestChannel.channelId}`); + + // the response channel can be different (if it was redirected) + let responseChannel = request.QueryInterface(Ci.nsIHttpChannel); + + let uri = responseChannel.URI.spec; + let origUri = responseChannel.originalURI.spec; + let id = responseChannel.channelId; + dump(`Child got response to: ${uri} (orig=${origUri}), channelId=${id}\n`); + + // notify the parent process about this channel's ID + do_send_remote_message(`response:${id}`); +} + +function finish() { + shouldQuit = true; +} diff --git a/netwerk/test/unit_ipc/head_cc.js b/netwerk/test/unit_ipc/head_cc.js new file mode 100644 index 0000000000..2765f95f2a --- /dev/null +++ b/netwerk/test/unit_ipc/head_cc.js @@ -0,0 +1,4 @@ +var Cc = Components.classes; +var Ci = Components.interfaces; +var Cu = Components.utils; +var Cr = Components.results; diff --git a/netwerk/test/unit_ipc/head_channels_clone.js b/netwerk/test/unit_ipc/head_channels_clone.js new file mode 100644 index 0000000000..f5eb45cbb8 --- /dev/null +++ b/netwerk/test/unit_ipc/head_channels_clone.js @@ -0,0 +1,8 @@ +// +// Load standard base class for network tests into child process +// + +Components.utils.import('resource://gre/modules/XPCOMUtils.jsm'); + +load("../unit/head_channels.js"); + diff --git a/netwerk/test/unit_ipc/test_XHR_redirects.js b/netwerk/test/unit_ipc/test_XHR_redirects.js new file mode 100644 index 0000000000..472817a9ec --- /dev/null +++ b/netwerk/test/unit_ipc/test_XHR_redirects.js @@ -0,0 +1,3 @@ +function run_test() { + run_test_in_child("../unit/test_XHR_redirects.js"); +} diff --git a/netwerk/test/unit_ipc/test_alt-data_simple_wrap.js b/netwerk/test/unit_ipc/test_alt-data_simple_wrap.js new file mode 100644 index 0000000000..04441c22a4 --- /dev/null +++ b/netwerk/test/unit_ipc/test_alt-data_simple_wrap.js @@ -0,0 +1,3 @@ +function run_test() { + run_test_in_child("../unit/test_alt-data_simple.js"); +} diff --git a/netwerk/test/unit_ipc/test_alt-data_stream_wrap.js b/netwerk/test/unit_ipc/test_alt-data_stream_wrap.js new file mode 100644 index 0000000000..1eee2f2434 --- /dev/null +++ b/netwerk/test/unit_ipc/test_alt-data_stream_wrap.js @@ -0,0 +1,3 @@ +function run_test() { + run_test_in_child("../unit/test_alt-data_stream.js"); +} diff --git a/netwerk/test/unit_ipc/test_bug248970_cookie_wrap.js b/netwerk/test/unit_ipc/test_bug248970_cookie_wrap.js new file mode 100644 index 0000000000..bb8b288150 --- /dev/null +++ b/netwerk/test/unit_ipc/test_bug248970_cookie_wrap.js @@ -0,0 +1,7 @@ +Cu.import("resource://gre/modules/Services.jsm"); + +function run_test() { + // Allow all cookies. + Services.prefs.setIntPref("network.cookie.cookieBehavior", 0); + run_test_in_child("../unit/test_bug248970_cookie.js"); +}
\ No newline at end of file diff --git a/netwerk/test/unit_ipc/test_bug528292_wrap.js b/netwerk/test/unit_ipc/test_bug528292_wrap.js new file mode 100644 index 0000000000..117ada7484 --- /dev/null +++ b/netwerk/test/unit_ipc/test_bug528292_wrap.js @@ -0,0 +1,6 @@ +Cu.import("resource://gre/modules/Services.jsm"); + +function run_test() { + Services.prefs.setIntPref("network.cookie.cookieBehavior", 1); + run_test_in_child("../unit/test_bug528292.js"); +}
\ No newline at end of file diff --git a/netwerk/test/unit_ipc/test_cache_jar_wrap.js b/netwerk/test/unit_ipc/test_cache_jar_wrap.js new file mode 100644 index 0000000000..fa2bb82a8d --- /dev/null +++ b/netwerk/test/unit_ipc/test_cache_jar_wrap.js @@ -0,0 +1,4 @@ +function run_test() { + run_test_in_child("../unit/head_cache2.js"); + run_test_in_child("../unit/test_cache_jar.js"); +} diff --git a/netwerk/test/unit_ipc/test_cacheflags_wrap.js b/netwerk/test/unit_ipc/test_cacheflags_wrap.js new file mode 100644 index 0000000000..e269e1ee57 --- /dev/null +++ b/netwerk/test/unit_ipc/test_cacheflags_wrap.js @@ -0,0 +1,7 @@ +// +// Run test script in content process instead of chrome (xpcshell's default) +// + +function run_test() { + run_test_in_child("../unit/test_cacheflags.js"); +} diff --git a/netwerk/test/unit_ipc/test_channel_close_wrap.js b/netwerk/test/unit_ipc/test_channel_close_wrap.js new file mode 100644 index 0000000000..ead9999579 --- /dev/null +++ b/netwerk/test/unit_ipc/test_channel_close_wrap.js @@ -0,0 +1,3 @@ +function run_test() { + run_test_in_child("../unit/test_channel_close.js"); +} diff --git a/netwerk/test/unit_ipc/test_channel_id.js b/netwerk/test/unit_ipc/test_channel_id.js new file mode 100644 index 0000000000..d82e5f5df6 --- /dev/null +++ b/netwerk/test/unit_ipc/test_channel_id.js @@ -0,0 +1,109 @@ +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/Promise.jsm"); + +/* + * Test that when doing HTTP requests, the nsIHttpChannel is detected in + * both parent and child and shares the same channelId across processes. + */ + +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(next) { + httpserver.stop(next); +} + +let expectedParentChannels = []; +let expectedChildMessages = []; + +let maybeFinishWaitForParentChannels; +let parentChannelsDone = new Promise(resolve => { + maybeFinishWaitForParentChannels = () => { + if (expectedParentChannels.length == 0) { + dump("All expected parent channels were detected\n"); + resolve(); + } + }; +}); + +function observer(subject, topic, data) { + let channel = subject.QueryInterface(Ci.nsIHttpChannel); + + let uri = channel.URI.spec; + let origUri = channel.originalURI.spec; + let id = channel.channelId; + dump(`Parent detected channel: ${uri} (orig=${origUri}): channelId=${id}\n`); + + // did we expect a new channel? + let expected = expectedParentChannels.shift(); + do_check_true(!!expected); + + // Start waiting for the messages about request/response from child + for (let event of expected) { + let message = `${event}:${id}`; + dump(`Expecting message from child: ${message}\n`); + + let messagePromise = do_await_remote_message(message).then(() => { + dump(`Expected message from child arrived: ${message}\n`); + }); + expectedChildMessages.push(messagePromise); + } + + // If we don't expect any further parent channels, finish the parent wait + maybeFinishWaitForParentChannels(); +} + +function run_test() { + startHttpServer(); + Services.obs.addObserver(observer, "http-on-modify-request", false); + run_test_in_child("child_channel_id.js", makeRequests); +} + +function makeRequests() { + // First, a normal request without any redirect. Expect one channel detected + // in parent, used by both request and response. + expectedParentChannels.push(["request", "response"]); + sendCommand(`makeRequest("http://localhost:${port}/resource");`); + + // Second request will be redirected. Expect two channels, one with the + // original request, then the redirected one which gets the final response. + expectedParentChannels.push(["request"], ["response"]); + sendCommand(`makeRequest("http://localhost:${port}/redirect");`); + + waitForParentChannels(); +} + +function waitForParentChannels() { + parentChannelsDone.then(waitForChildMessages); +} + +function waitForChildMessages() { + dump(`Waiting for ${expectedChildMessages.length} child messages\n`); + Promise.all(expectedChildMessages).then(finish); +} + +function finish() { + Services.obs.removeObserver(observer, "http-on-modify-request"); + sendCommand("finish();", () => stopHttpServer(do_test_finished)); +} diff --git a/netwerk/test/unit_ipc/test_chunked_responses_wrap.js b/netwerk/test/unit_ipc/test_chunked_responses_wrap.js new file mode 100644 index 0000000000..72bb40554c --- /dev/null +++ b/netwerk/test/unit_ipc/test_chunked_responses_wrap.js @@ -0,0 +1,7 @@ +// +// Run test script in content process instead of chrome (xpcshell's default) +// + +function run_test() { + run_test_in_child("../unit/test_chunked_responses.js"); +} diff --git a/netwerk/test/unit_ipc/test_cookie_header_wrap.js b/netwerk/test/unit_ipc/test_cookie_header_wrap.js new file mode 100644 index 0000000000..3a071a6c15 --- /dev/null +++ b/netwerk/test/unit_ipc/test_cookie_header_wrap.js @@ -0,0 +1,11 @@ +// +// Run test script in content process instead of chrome (xpcshell's default) +// + +Cu.import("resource://gre/modules/Services.jsm"); + +function run_test() { + // Allow all cookies. + Services.prefs.setIntPref("network.cookie.cookieBehavior", 0); + run_test_in_child("../unit/test_cookie_header.js"); +} diff --git a/netwerk/test/unit_ipc/test_cookiejars_wrap.js b/netwerk/test/unit_ipc/test_cookiejars_wrap.js new file mode 100644 index 0000000000..dc3ea7d9fc --- /dev/null +++ b/netwerk/test/unit_ipc/test_cookiejars_wrap.js @@ -0,0 +1,7 @@ +Cu.import("resource://gre/modules/Services.jsm"); + +function run_test() { + // Allow all cookies. + Services.prefs.setIntPref("network.cookie.cookieBehavior", 0); + run_test_in_child("../unit/test_cookiejars.js"); +} diff --git a/netwerk/test/unit_ipc/test_dns_cancel_wrap.js b/netwerk/test/unit_ipc/test_dns_cancel_wrap.js new file mode 100644 index 0000000000..2f38aa4ecb --- /dev/null +++ b/netwerk/test/unit_ipc/test_dns_cancel_wrap.js @@ -0,0 +1,7 @@ +// +// Run test script in content process instead of chrome (xpcshell's default) +// + +function run_test() { + run_test_in_child("../unit/test_dns_cancel.js"); +} diff --git a/netwerk/test/unit_ipc/test_dns_per_interface_wrap.js b/netwerk/test/unit_ipc/test_dns_per_interface_wrap.js new file mode 100644 index 0000000000..25e7217b0e --- /dev/null +++ b/netwerk/test/unit_ipc/test_dns_per_interface_wrap.js @@ -0,0 +1,7 @@ +// +// Run test script in content process instead of chrome (xpcshell's default) +// + +function run_test() { + run_test_in_child("../unit/test_dns_per_interface.js"); +} diff --git a/netwerk/test/unit_ipc/test_dns_service_wrap.js b/netwerk/test/unit_ipc/test_dns_service_wrap.js new file mode 100644 index 0000000000..fdbecf16d3 --- /dev/null +++ b/netwerk/test/unit_ipc/test_dns_service_wrap.js @@ -0,0 +1,7 @@ +// +// Run test script in content process instead of chrome (xpcshell's default) +// + +function run_test() { + run_test_in_child("../unit/test_dns_service.js"); +} diff --git a/netwerk/test/unit_ipc/test_duplicate_headers_wrap.js b/netwerk/test/unit_ipc/test_duplicate_headers_wrap.js new file mode 100644 index 0000000000..6225d593d9 --- /dev/null +++ b/netwerk/test/unit_ipc/test_duplicate_headers_wrap.js @@ -0,0 +1,7 @@ +// +// Run test script in content process instead of chrome (xpcshell's default) +// + +function run_test() { + run_test_in_child("../unit/test_duplicate_headers.js"); +} diff --git a/netwerk/test/unit_ipc/test_event_sink_wrap.js b/netwerk/test/unit_ipc/test_event_sink_wrap.js new file mode 100644 index 0000000000..908c971f8a --- /dev/null +++ b/netwerk/test/unit_ipc/test_event_sink_wrap.js @@ -0,0 +1,7 @@ +// +// Run test script in content process instead of chrome (xpcshell's default) +// + +function run_test() { + run_test_in_child("../unit/test_event_sink.js"); +} diff --git a/netwerk/test/unit_ipc/test_getHost_wrap.js b/netwerk/test/unit_ipc/test_getHost_wrap.js new file mode 100644 index 0000000000..f74ab7d152 --- /dev/null +++ b/netwerk/test/unit_ipc/test_getHost_wrap.js @@ -0,0 +1,3 @@ +function run_test() { + run_test_in_child("../unit/test_getHost.js"); +} diff --git a/netwerk/test/unit_ipc/test_head_wrap.js b/netwerk/test/unit_ipc/test_head_wrap.js new file mode 100644 index 0000000000..13f0702e55 --- /dev/null +++ b/netwerk/test/unit_ipc/test_head_wrap.js @@ -0,0 +1,7 @@ +// +// Run test script in content process instead of chrome (xpcshell's default) +// + +function run_test() { + run_test_in_child("../unit/test_head.js"); +} diff --git a/netwerk/test/unit_ipc/test_headers_wrap.js b/netwerk/test/unit_ipc/test_headers_wrap.js new file mode 100644 index 0000000000..e0bae4080d --- /dev/null +++ b/netwerk/test/unit_ipc/test_headers_wrap.js @@ -0,0 +1,7 @@ +// +// Run test script in content process instead of chrome (xpcshell's default) +// + +function run_test() { + run_test_in_child("../unit/test_headers.js"); +} diff --git a/netwerk/test/unit_ipc/test_httpsuspend_wrap.js b/netwerk/test/unit_ipc/test_httpsuspend_wrap.js new file mode 100644 index 0000000000..348541283a --- /dev/null +++ b/netwerk/test/unit_ipc/test_httpsuspend_wrap.js @@ -0,0 +1,3 @@ +function run_test() { + run_test_in_child("../unit/test_httpsuspend.js"); +}
\ No newline at end of file diff --git a/netwerk/test/unit_ipc/test_original_sent_received_head_wrap.js b/netwerk/test/unit_ipc/test_original_sent_received_head_wrap.js new file mode 100644 index 0000000000..91a8a00f09 --- /dev/null +++ b/netwerk/test/unit_ipc/test_original_sent_received_head_wrap.js @@ -0,0 +1,7 @@ +// +// Run test script in content process instead of chrome (xpcshell's default) +// + +function run_test() { + run_test_in_child("../unit/test_original_sent_received_head.js"); +} diff --git a/netwerk/test/unit_ipc/test_post_wrap.js b/netwerk/test/unit_ipc/test_post_wrap.js new file mode 100644 index 0000000000..27afae5b40 --- /dev/null +++ b/netwerk/test/unit_ipc/test_post_wrap.js @@ -0,0 +1,3 @@ +function run_test() { + run_test_in_child("../unit/test_post.js"); +} diff --git a/netwerk/test/unit_ipc/test_predictor_wrap.js b/netwerk/test/unit_ipc/test_predictor_wrap.js new file mode 100644 index 0000000000..de4ddc174e --- /dev/null +++ b/netwerk/test/unit_ipc/test_predictor_wrap.js @@ -0,0 +1,36 @@ +// This is the bit that runs in the parent process when the test begins. Here's +// what happens: +// +// - Load the entire single-process test in the parent +// - Setup the test harness within the child process +// - Send a command to the child to have the single-process test loaded there, as well +// - Send a command to the child to make the predictor available +// - run_test_real in the parent +// - run_test_real does a bunch of pref-setting and other fun things on the parent +// - once all that's done, it calls run_next_test IN THE PARENT +// - every time run_next_test is called, it is called IN THE PARENT +// - the test that gets started then does any parent-side setup that's necessary +// - once the parent-side setup is done, it calls continue_<testname> IN THE CHILD +// - when the test is done running on the child, the child calls predictor.reset +// this causes some code to be run on the parent which, when complete, sends an +// obserer service notification IN THE PARENT, which causes run_next_test to be +// called again, bumping us up to the top of the loop outlined here +// - when the final test is done, cleanup happens IN THE PARENT and we're done +// +// This is a little confusing, but it's what we have to do in order to have some +// things that must run on the parent (the setup - opening cache entries, etc) +// but with most of the test running on the child (calls to the predictor api, +// verification, etc). +// +function run_test() { + var test_path = do_get_file("../unit/test_predictor.js").path.replace(/\\/g, "/"); + load(test_path); + do_load_child_test_harness(); + do_test_pending(); + sendCommand("load(\"" + test_path + "\");", function () { + sendCommand("predictor = Cc[\"@mozilla.org/network/predictor;1\"].getService(Ci.nsINetworkPredictor);", function() { + run_test_real(); + do_test_finished(); + }); + }); +} diff --git a/netwerk/test/unit_ipc/test_progress_wrap.js b/netwerk/test/unit_ipc/test_progress_wrap.js new file mode 100644 index 0000000000..c4a658c094 --- /dev/null +++ b/netwerk/test/unit_ipc/test_progress_wrap.js @@ -0,0 +1,3 @@ +function run_test() { + run_test_in_child("../unit/test_progress.js"); +} diff --git a/netwerk/test/unit_ipc/test_redirect-caching_canceled_wrap.js b/netwerk/test/unit_ipc/test_redirect-caching_canceled_wrap.js new file mode 100644 index 0000000000..a1b8adfb9b --- /dev/null +++ b/netwerk/test/unit_ipc/test_redirect-caching_canceled_wrap.js @@ -0,0 +1,7 @@ +// +// Run test script in content process instead of chrome (xpcshell's default) +// +// +function run_test() { + run_test_in_child("../unit/test_redirect-caching_canceled.js"); +} diff --git a/netwerk/test/unit_ipc/test_redirect-caching_failure_wrap.js b/netwerk/test/unit_ipc/test_redirect-caching_failure_wrap.js new file mode 100644 index 0000000000..4fea2fdf65 --- /dev/null +++ b/netwerk/test/unit_ipc/test_redirect-caching_failure_wrap.js @@ -0,0 +1,7 @@ +// +// Run test script in content process instead of chrome (xpcshell's default) +// +// +function run_test() { + run_test_in_child("../unit/test_redirect-caching_failure.js"); +} diff --git a/netwerk/test/unit_ipc/test_redirect-caching_passing_wrap.js b/netwerk/test/unit_ipc/test_redirect-caching_passing_wrap.js new file mode 100644 index 0000000000..8519501865 --- /dev/null +++ b/netwerk/test/unit_ipc/test_redirect-caching_passing_wrap.js @@ -0,0 +1,7 @@ +// +// Run test script in content process instead of chrome (xpcshell's default) +// +// +function run_test() { + run_test_in_child("../unit/test_redirect-caching_passing.js"); +} diff --git a/netwerk/test/unit_ipc/test_redirect_canceled_wrap.js b/netwerk/test/unit_ipc/test_redirect_canceled_wrap.js new file mode 100644 index 0000000000..546e051410 --- /dev/null +++ b/netwerk/test/unit_ipc/test_redirect_canceled_wrap.js @@ -0,0 +1,7 @@ +// +// Run test script in content process instead of chrome (xpcshell's default) +// +// +function run_test() { + run_test_in_child("../unit/test_redirect_canceled.js"); +} diff --git a/netwerk/test/unit_ipc/test_redirect_different-protocol_wrap.js b/netwerk/test/unit_ipc/test_redirect_different-protocol_wrap.js new file mode 100644 index 0000000000..c45e7810ab --- /dev/null +++ b/netwerk/test/unit_ipc/test_redirect_different-protocol_wrap.js @@ -0,0 +1,7 @@ +// +// Run test script in content process instead of chrome (xpcshell's default) +// + +function run_test() { + run_test_in_child("../unit/test_redirect_different-protocol.js"); +} diff --git a/netwerk/test/unit_ipc/test_redirect_failure_wrap.js b/netwerk/test/unit_ipc/test_redirect_failure_wrap.js new file mode 100644 index 0000000000..fbe27697f0 --- /dev/null +++ b/netwerk/test/unit_ipc/test_redirect_failure_wrap.js @@ -0,0 +1,7 @@ +// +// Run test script in content process instead of chrome (xpcshell's default) +// + +function run_test() { + run_test_in_child("../unit/test_redirect_failure.js"); +} diff --git a/netwerk/test/unit_ipc/test_redirect_from_script_wrap.js b/netwerk/test/unit_ipc/test_redirect_from_script_wrap.js new file mode 100644 index 0000000000..74f77a9ea1 --- /dev/null +++ b/netwerk/test/unit_ipc/test_redirect_from_script_wrap.js @@ -0,0 +1,3 @@ +function run_test() { + run_test_in_child("../unit/test_redirect_from_script.js"); +} diff --git a/netwerk/test/unit_ipc/test_redirect_history_wrap.js b/netwerk/test/unit_ipc/test_redirect_history_wrap.js new file mode 100644 index 0000000000..38cdfa35ec --- /dev/null +++ b/netwerk/test/unit_ipc/test_redirect_history_wrap.js @@ -0,0 +1,7 @@ +// +// Run test script in content process instead of chrome (xpcshell's default) +// + +function run_test() { + run_test_in_child("../unit/test_redirect_history.js"); +} diff --git a/netwerk/test/unit_ipc/test_redirect_passing_wrap.js b/netwerk/test/unit_ipc/test_redirect_passing_wrap.js new file mode 100644 index 0000000000..597ac35fb4 --- /dev/null +++ b/netwerk/test/unit_ipc/test_redirect_passing_wrap.js @@ -0,0 +1,7 @@ +// +// Run test script in content process instead of chrome (xpcshell's default) +// + +function run_test() { + run_test_in_child("../unit/test_redirect_passing.js"); +} diff --git a/netwerk/test/unit_ipc/test_reentrancy_wrap.js b/netwerk/test/unit_ipc/test_reentrancy_wrap.js new file mode 100644 index 0000000000..43d72dd059 --- /dev/null +++ b/netwerk/test/unit_ipc/test_reentrancy_wrap.js @@ -0,0 +1,3 @@ +function run_test() { + run_test_in_child("../unit/test_reentrancy.js"); +}
\ No newline at end of file diff --git a/netwerk/test/unit_ipc/test_reply_without_content_type_wrap.js b/netwerk/test/unit_ipc/test_reply_without_content_type_wrap.js new file mode 100644 index 0000000000..f2d90c33d0 --- /dev/null +++ b/netwerk/test/unit_ipc/test_reply_without_content_type_wrap.js @@ -0,0 +1,7 @@ +// +// Run test script in content process instead of chrome (xpcshell's default) +// + +function run_test() { + run_test_in_child("../unit/test_reply_without_content_type.js"); +} diff --git a/netwerk/test/unit_ipc/test_resumable_channel_wrap.js b/netwerk/test/unit_ipc/test_resumable_channel_wrap.js new file mode 100644 index 0000000000..573ab25b64 --- /dev/null +++ b/netwerk/test/unit_ipc/test_resumable_channel_wrap.js @@ -0,0 +1,3 @@ +function run_test() { + run_test_in_child("../unit/test_resumable_channel.js"); +} diff --git a/netwerk/test/unit_ipc/test_simple_wrap.js b/netwerk/test/unit_ipc/test_simple_wrap.js new file mode 100644 index 0000000000..8c6957e944 --- /dev/null +++ b/netwerk/test/unit_ipc/test_simple_wrap.js @@ -0,0 +1,7 @@ +// +// Run test script in content process instead of chrome (xpcshell's default) +// + +function run_test() { + run_test_in_child("../unit/test_simple.js"); +} diff --git a/netwerk/test/unit_ipc/test_synthesized_response_wrap.js b/netwerk/test/unit_ipc/test_synthesized_response_wrap.js new file mode 100644 index 0000000000..337646df52 --- /dev/null +++ b/netwerk/test/unit_ipc/test_synthesized_response_wrap.js @@ -0,0 +1,3 @@ +function run_test() { + run_test_in_child("../unit/test_synthesized_response.js"); +} diff --git a/netwerk/test/unit_ipc/test_xmlhttprequest_wrap.js b/netwerk/test/unit_ipc/test_xmlhttprequest_wrap.js new file mode 100644 index 0000000000..00b4a0b85f --- /dev/null +++ b/netwerk/test/unit_ipc/test_xmlhttprequest_wrap.js @@ -0,0 +1,3 @@ +function run_test() { + run_test_in_child("../unit/test_xmlhttprequest.js"); +} diff --git a/netwerk/test/unit_ipc/xpcshell.ini b/netwerk/test/unit_ipc/xpcshell.ini new file mode 100644 index 0000000000..ec7a13df9f --- /dev/null +++ b/netwerk/test/unit_ipc/xpcshell.ini @@ -0,0 +1,99 @@ +[DEFAULT] +head = head_channels_clone.js head_cc.js +tail = +skip-if = toolkit == 'android' +support-files = + child_channel_id.js + !/netwerk/test/unit/test_XHR_redirects.js + !/netwerk/test/unit/test_bug248970_cookie.js + !/netwerk/test/unit/test_bug528292.js + !/netwerk/test/unit/test_cache_jar.js + !/netwerk/test/unit/test_cacheflags.js + !/netwerk/test/unit/test_channel_close.js + !/netwerk/test/unit/test_cookie_header.js + !/netwerk/test/unit/test_cookiejars.js + !/netwerk/test/unit/test_dns_cancel.js + !/netwerk/test/unit/test_dns_per_interface.js + !/netwerk/test/unit/test_dns_service.js + !/netwerk/test/unit/test_duplicate_headers.js + !/netwerk/test/unit/test_event_sink.js + !/netwerk/test/unit/test_getHost.js + !/netwerk/test/unit/test_head.js + !/netwerk/test/unit/test_headers.js + !/netwerk/test/unit/test_httpsuspend.js + !/netwerk/test/unit/test_post.js + !/netwerk/test/unit/test_predictor.js + !/netwerk/test/unit/test_progress.js + !/netwerk/test/unit/test_redirect-caching_canceled.js + !/netwerk/test/unit/test_redirect-caching_failure.js + !/netwerk/test/unit/test_redirect-caching_passing.js + !/netwerk/test/unit/test_redirect_canceled.js + !/netwerk/test/unit/test_redirect_different-protocol.js + !/netwerk/test/unit/test_redirect_failure.js + !/netwerk/test/unit/test_redirect_from_script.js + !/netwerk/test/unit/test_redirect_history.js + !/netwerk/test/unit/test_redirect_passing.js + !/netwerk/test/unit/test_reentrancy.js + !/netwerk/test/unit/test_reply_without_content_type.js + !/netwerk/test/unit/test_resumable_channel.js + !/netwerk/test/unit/test_simple.js + !/netwerk/test/unit/test_synthesized_response.js + !/netwerk/test/unit/test_xmlhttprequest.js + !/netwerk/test/unit/head_channels.js + !/netwerk/test/unit/head_cache2.js + !/netwerk/test/unit/data/image.png + !/netwerk/test/unit/data/system_root.lnk + !/netwerk/test/unit/data/test_psl.txt + !/netwerk/test/unit/data/test_readline1.txt + !/netwerk/test/unit/data/test_readline2.txt + !/netwerk/test/unit/data/test_readline3.txt + !/netwerk/test/unit/data/test_readline4.txt + !/netwerk/test/unit/data/test_readline5.txt + !/netwerk/test/unit/data/test_readline6.txt + !/netwerk/test/unit/data/test_readline7.txt + !/netwerk/test/unit/data/test_readline8.txt + !/netwerk/test/unit/data/signed_win.exe + !/netwerk/test/unit/test_alt-data_simple.js + !/netwerk/test/unit/test_alt-data_stream.js + +[test_bug528292_wrap.js] +[test_bug248970_cookie_wrap.js] +[test_cacheflags_wrap.js] +[test_cache_jar_wrap.js] +[test_channel_close_wrap.js] +[test_cookie_header_wrap.js] +[test_cookiejars_wrap.js] +[test_dns_cancel_wrap.js] +[test_dns_per_interface_wrap.js] +[test_dns_service_wrap.js] +[test_duplicate_headers_wrap.js] +[test_event_sink_wrap.js] +[test_head_wrap.js] +[test_headers_wrap.js] +[test_httpsuspend_wrap.js] +[test_post_wrap.js] +[test_predictor_wrap.js] +[test_progress_wrap.js] +[test_redirect-caching_canceled_wrap.js] +[test_redirect-caching_failure_wrap.js] +[test_redirect-caching_passing_wrap.js] +[test_redirect_canceled_wrap.js] +[test_redirect_failure_wrap.js] +# Do not test the channel.redirectTo() API under e10s until 827269 is resolved +[test_redirect_from_script_wrap.js] +skip-if = true +[test_redirect_passing_wrap.js] +[test_redirect_different-protocol_wrap.js] +[test_reentrancy_wrap.js] +[test_resumable_channel_wrap.js] +[test_simple_wrap.js] +[test_synthesized_response_wrap.js] +[test_xmlhttprequest_wrap.js] +[test_XHR_redirects.js] +[test_redirect_history_wrap.js] +[test_reply_without_content_type_wrap.js] +[test_getHost_wrap.js] +[test_alt-data_simple_wrap.js] +[test_alt-data_stream_wrap.js] +[test_original_sent_received_head_wrap.js] +[test_channel_id.js] diff --git a/netwerk/test/urlparse.dat b/netwerk/test/urlparse.dat new file mode 100644 index 0000000000..3b607d4e60 --- /dev/null +++ b/netwerk/test/urlparse.dat @@ -0,0 +1,103 @@ +# Any blank lines and those beginning with # are comments and +# ignored. To add additional test cases that could potentially +# break URL parsing in mozilla add the input URL on a new line +# and follow it with the expected output for the standard URL +# parser and one line for the case when the URL is really +# created. Then run urltest with the -std option and without it +# on this file and hopefully the expected output should match +# the one from the program. +# - Gagan Saksena 03/28/00 +# + +http://username:password@hostname.com:80/pathname/./more/stuff/../path +http,username,password,hostname.com,80,/pathname/more/,path,,,,,http://username:password@hostname.com:80/pathname/more/path +http,username,password,hostname.com,80,/pathname/more/,path,,,,,http://username:password@hostname.com/pathname/more/path + +username@host:8080/path +,username,,host,8080,/,path,,,,,username@host:8080/path +Can not create URL + +http://gagan/ +http,,,gagan,-1,/,,,,,,http://gagan/ +http,,,gagan,-1,/,,,,,,http://gagan/ + +scheme:host/netlib +scheme,,,host,-1,/,netlib,,,,,scheme://host/netlib +Can not create URL + +mailbox:///foo +mailbox,,,,-1,/,foo,,,,,mailbox:///foo +mailbox,,,,-1,/,foo,,,,,mailbox:///foo + +scheme:user@hostname.edu:80/pathname +scheme,user,,hostname.edu,80,/,pathname,,,,,scheme://user@hostname.edu:80/pathname +Can not create URL + +http://username:password@hostname:80/pathname +http,username,password,hostname,80,/,pathname,,,,,http://username:password@hostname:80/pathname +http,username,password,hostname,80,/,pathname,,,,,http://username:password@hostname/pathname + +http://username:password@hostname:8080/path/filebasename.fileextension;param?query#ref +http,username,password,hostname,8080,/path/,filebasename,fileextension,param,query,ref,http://username:password@hostname:8080/path/filebasename.fileextension;param?query#ref +http,username,password,hostname,8080,/path/,filebasename,fileextension,param,query,ref,http://username:password@hostname:8080/path/filebasename.fileextension;param?query#ref + +resource:/pathname +resource,,,,-1,/,pathname,,,,,resource:///pathname +resource,,,,-1,/,pathname,,,,,resource:///pathname + +ftp://uname%here.com:pwd@there.com/aPath/a.html +ftp,uname%here.com,pwd,there.com,-1,/aPath/,a,html,,,,ftp://uname%here.com:pwd@there.com/aPath/a.html +ftp,uname%here.com,pwd,there.com,-1,/aPath/,a,html,,,,ftp://uname%here.com:pwd@there.com/aPath/a.html + +http://www.inf.bme.hu?foo=bar +http,,,www.inf.bme.hu,-1,/,,,,foo=bar,,http://www.inf.bme.hu/?foo=bar +http,,,www.inf.bme.hu,-1,/,,,,foo=bar,,http://www.inf.bme.hu/?foo=bar + +http://test.com/aPath/a.html#/1/2 +http,,,test.com,-1,/aPath/,a,html,,,/1/2,http://test.com/aPath/a.html#/1/2 +http,,,test.com,-1,/aPath/,a,html,,,/1/2,http://test.com/aPath/a.html#/1/2 + +http://user:pass@ipaddres:2/get?foo/something +http,user,pass,ipaddres,2,/,get,,,foo/something,,http://user:pass@ipaddres:2/get?foo/something +http,user,pass,ipaddres,2,/,get,,,foo/something,,http://user:pass@ipaddres:2/get?foo/something + +# testing different versions of http urls +http:www.mozilla.org +http,,,www.mozilla.org,-1,/,,,,,,http://www.mozilla.org/ +http,,,www.mozilla.org,-1,/,,,,,,http://www.mozilla.org/ + +http:/www.mozilla.org +http,,,,-1,/,www.mozilla,org,,,,http:///www.mozilla.org +http,,,www.mozilla.org,-1,/,,,,,,http://www.mozilla.org/ + +# testing cap letters (23927) +HtTp://wWw.mozilLa.org +http,,,www.mozilla.org,-1,/,,,,,,http://www.mozilla.org/ +http,,,www.mozilla.org,-1,/,,,,,,http://www.mozilla.org/ + +# testing spaces (15150) +go.com.au?mozilla bug reports +,,,go.com.au,-1,/,,,,mozilla%20bug%20reports,,go.com.au/?mozilla%20bug%20reports +Can not create URL + +http://go.com.au?mozilla bug reports +http,,,go.com.au,-1,/,,,,mozilla%20bug%20reports,,http://go.com.au/?mozilla%20bug%20reports +http,,,go.com.au,-1,/,,,,mozilla%20bug%20reports,,http://go.com.au/?mozilla%20bug%20reports + +# testing for multiple params (14801) +http://ad.doubleclick.net/ad/cbsmw.button.com/SIDEBAR_BUTTONS;sz=88x31;kw=DBCC;tile=4;ord=1864641213378545414 +http,,,ad.doubleclick.net,-1,/ad/cbsmw.button.com/,SIDEBAR_BUTTONS,,sz=88x31;kw=DBCC;tile=4;ord=1864641213378545414,,,http://ad.doubleclick.net/ad/cbsmw.button.com/SIDEBAR_BUTTONS;sz=88x31;kw=DBCC;tile=4;ord=1864641213378545414 +http,,,ad.doubleclick.net,-1,/ad/cbsmw.button.com/,SIDEBAR_BUTTONS,,sz=88x31;kw=DBCC;tile=4;ord=1864641213378545414,,,http://ad.doubleclick.net/ad/cbsmw.button.com/SIDEBAR_BUTTONS;sz=88x31;kw=DBCC;tile=4;ord=1864641213378545414 + +fxqn:/us/va/reston/cnri/ietf/24/asdf%*.fred +fxqn,,,,-1,/us/va/reston/cnri/ietf/24/,asdf%*,fred,,,,fxqn:///us/va/reston/cnri/ietf/24/asdf%*.fred +Can not create URL + +news:3B5C133C.2080505@foobar.net +news,3B5C133C.2080505,,foobar.net,-1,/,,,,,,news://3B5C133C.2080505@foobar.net/ +news,3B5C133C.2080505,,foobar.net,-1,/,,,,,,news://3B5C133C.2080505@foobar.net/ + +http://host/path/%2E%2E/file%2Ehtml +http,,,host,-1,/,file%2Ehtml,,,,,http://host/file%2Ehtml +http,,,host,-1,/,file%2Ehtml,,,,,http://host/file%2Ehtml + diff --git a/netwerk/test/urlparse_mac.dat b/netwerk/test/urlparse_mac.dat new file mode 100644 index 0000000000..f9209d0a51 --- /dev/null +++ b/netwerk/test/urlparse_mac.dat @@ -0,0 +1,14 @@ +# Any blank lines and those beginning with # are comments and +# ignored. To add additional test cases that could potentially +# break URL parsing in mozilla add the input URL on a new line +# and follow it with the expected output for the standard URL +# parser and one line for the case when the URL is really +# created. Then run urltest with the -std option and without it +# on this file and hopefully the expected output should match +# the one from the program. +# - Gagan Saksena 03/28/00 +# +# This version is specifically for the Mac platform. +# + +# testing different versions of file urls diff --git a/netwerk/test/urlparse_unx.dat b/netwerk/test/urlparse_unx.dat new file mode 100644 index 0000000000..909d1a9a03 --- /dev/null +++ b/netwerk/test/urlparse_unx.dat @@ -0,0 +1,34 @@ +# Any blank lines and those beginning with # are comments and +# ignored. To add additional test cases that could potentially +# break URL parsing in mozilla add the input URL on a new line +# and follow it with the expected output for the standard URL +# parser and one line for the case when the URL is really +# created. Then run urltest with the -std option and without it +# on this file and hopefully the expected output should match +# the one from the program. +# - Gagan Saksena 03/28/00 +# +# This version is specifically *not* for PC platforms like Windows or OS/2. +# It's testcases for the file protocol target a unix-like filesystem. + +# testing different versions of file urls +file:home +file,,,home,-1,/,,,,,,file://home/ +file,,,,-1,/,home,,,,,file:///home + +file:/home +file,,,,-1,/,home,,,,,file:///home +file,,,,-1,/,home,,,,,file:///home + +file://home +file,,,home,-1,/,,,,,,file://home/ +file,,,home,-1,/,,,,,,file://home/ + +file:///home +file,,,,-1,/,home,,,,,file:///home +file,,,,-1,/,home,,,,,file:///home + +# testing UNC filepaths +file:////server/path +file,,,,-1,//server/,path,,,,,file:////server/path +file,,,,-1,//server/,path,,,,,file:////server/path diff --git a/netwerk/test/urlparse_win.dat b/netwerk/test/urlparse_win.dat new file mode 100644 index 0000000000..cf418fefb0 --- /dev/null +++ b/netwerk/test/urlparse_win.dat @@ -0,0 +1,60 @@ +# Any blank lines and those beginning with # are comments and +# ignored. To add additional test cases that could potentially +# break URL parsing in mozilla add the input URL on a new line +# and follow it with the expected output for the standard URL +# parser and one line for the case when the URL is really +# created. Then run urltest with the -std option and without it +# on this file and hopefully the expected output should match +# the one from the program. +# - Gagan Saksena 03/28/00 +# +# This version is specifically for PC platforms like Windows or OS/2. +# It has testcases for the file protocol targeting typical +# drive:/path filesystems +# + +# testing different versions of file urls +file:c: +file,,,,-1,/,c:,,,,,file:///c%3A +file,,,,-1,/c:/,,,,,,file:///c:/ + +file:c:/ +file,,,,-1,/c:/,,,,,,file:///c:/ +file,,,,-1,/c:/,,,,,,file:///c:/ + +file:/c:/ +file,,,,-1,/c:/,,,,,,file:///c:/ +file,,,,-1,/c:/,,,,,,file:///c:/ + +file://c:/ +file,,,,-1,/c:/,,,,,,file:///c:/ +file,,,,-1,/c:/,,,,,,file:///c:/ + +file:///c:/ +file,,,,-1,/c:/,,,,,,file:///c:/ +file,,,,-1,/c:/,,,,,,file:///c:/ + +# testing UNC filepaths +file:server/path +file,,,server,-1,/,path,,,,,file://server/path +file,,,,-1,///server/,path,,,,,file://///server/path + +file:/server/path +file,,,,-1,/server/,path,,,,,file:///server/path +file,,,,-1,///server/,path,,,,,file://///server/path + +file://server/path +file,,,server,-1,/,path,,,,,file://server/path +file,,,server,-1,///,path,,,,,file://server///path + +file:///server/path +file,,,,-1,/server/,path,,,,,file:///server/path +file,,,,-1,///server/,path,,,,,file://///server/path + +file:////server/path +file,,,,-1,//server/,path,,,,,file:////server/path +file,,,,-1,///server/,path,,,,,file://///server/path + +file://///server/path +file,,,,-1,///server/,path,,,,,file://///server/path +file,,,,-1,///server/,path,,,,,file://///server/path diff --git a/netwerk/test/urltest.cpp b/netwerk/test/urltest.cpp new file mode 100644 index 0000000000..df6f3f301f --- /dev/null +++ b/netwerk/test/urltest.cpp @@ -0,0 +1,461 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 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/. */ + +/* + A test file to check default URL parsing. + -Gagan Saksena 03/25/99 +*/ + +#include <stdio.h> + +#include "TestCommon.h" +#include "plstr.h" +#include "nsIServiceManager.h" +#include "nsIIOService.h" +#include "nsIURL.h" +#include "nsCOMPtr.h" +#include "nsStringAPI.h" +#include "nsNetCID.h" +#include "nsIComponentRegistrar.h" +#include "nsComponentManagerUtils.h" +#include "nsServiceManagerUtils.h" +#include "nsXPCOM.h" +#include "prprf.h" +#include "mozilla/Sprintf.h" + +// Define CIDs... +static NS_DEFINE_CID(kIOServiceCID, NS_IOSERVICE_CID); +static NS_DEFINE_CID(kStdURLCID, NS_STANDARDURL_CID); + +char* gFileIO = 0; + +enum { + URL_FACTORY_DEFAULT, + URL_FACTORY_STDURL +}; + +nsresult writeoutto(const char* i_pURL, char** o_Result, int32_t urlFactory = URL_FACTORY_DEFAULT) +{ + if (!o_Result || !i_pURL) + return NS_ERROR_FAILURE; + *o_Result = 0; + nsCOMPtr<nsIURI> pURL; + nsresult result = NS_OK; + + switch (urlFactory) { + case URL_FACTORY_STDURL: { + nsIURI* url; + result = CallCreateInstance(kStdURLCID, &url); + if (NS_FAILED(result)) + { + printf("CreateInstance failed\n"); + return NS_ERROR_FAILURE; + } + pURL = url; + result = pURL->SetSpec(nsDependentCString(i_pURL)); + if (NS_FAILED(result)) + { + printf("SetSpec failed\n"); + return NS_ERROR_FAILURE; + } + break; + } + case URL_FACTORY_DEFAULT: { + nsCOMPtr<nsIIOService> pService = + do_GetService(kIOServiceCID, &result); + if (NS_FAILED(result)) + { + printf("Service failed!\n"); + return NS_ERROR_FAILURE; + } + result = pService->NewURI(nsDependentCString(i_pURL), nullptr, nullptr, getter_AddRefs(pURL)); + } + } + + nsCString output; + if (NS_SUCCEEDED(result)) + { + nsCOMPtr<nsIURL> tURL = do_QueryInterface(pURL); + nsAutoCString temp; + int32_t port; + nsresult rv; + +#define RESULT() NS_SUCCEEDED(rv) ? temp.get() : "" + + rv = tURL->GetScheme(temp); + output += RESULT(); + output += ','; + rv = tURL->GetUsername(temp); + output += RESULT(); + output += ','; + rv = tURL->GetPassword(temp); + output += RESULT(); + output += ','; + rv = tURL->GetHost(temp); + output += RESULT(); + output += ','; + rv = tURL->GetPort(&port); + char portbuffer[40]; + SprintfLiteral(portbuffer, "%d", port); + output.Append(portbuffer); + output += ','; + rv = tURL->GetDirectory(temp); + output += RESULT(); + output += ','; + rv = tURL->GetFileBaseName(temp); + output += RESULT(); + output += ','; + rv = tURL->GetFileExtension(temp); + output += RESULT(); + output += ','; + // removed with https://bugzilla.mozilla.org/show_bug.cgi?id=665706 + // rv = tURL->GetParam(temp); + // output += RESULT(); + output += ','; + rv = tURL->GetQuery(temp); + output += RESULT(); + output += ','; + rv = tURL->GetRef(temp); + output += RESULT(); + output += ','; + rv = tURL->GetSpec(temp); + output += RESULT(); + *o_Result = ToNewCString(output); + } else { + output = "Can not create URL"; + *o_Result = ToNewCString(output); + } + return NS_OK; +} + +nsresult writeout(const char* i_pURL, int32_t urlFactory = URL_FACTORY_DEFAULT) +{ + if (!i_pURL) return NS_ERROR_FAILURE; + nsCString temp; + nsresult rv = writeoutto(i_pURL, getter_Copies(temp), urlFactory); + printf("%s\n%s\n", i_pURL, temp.get()); + return rv; +} + +/* construct a url and print out its elements separated by commas and + the whole spec */ +nsresult testURL(const char* i_pURL, int32_t urlFactory = URL_FACTORY_DEFAULT) +{ + + if (i_pURL) + return writeout(i_pURL, urlFactory); + + if (!gFileIO) + return NS_ERROR_FAILURE; + + FILE *testfile = fopen(gFileIO, "rt"); + if (!testfile) + { + fprintf(stderr, "Cannot open testfile: %s\n", gFileIO); + return NS_ERROR_FAILURE; + } + + char temp[512]; + int count=0; + int failed=0; + nsCString prevResult; + nsCString tempurl; + + while (fgets(temp,512,testfile)) + { + if (*temp == '#' || !*temp) + continue; + + if (0 == count%3) + { + printf("Testing: %s\n", temp); + writeoutto(temp, getter_Copies(prevResult), urlFactory); + } + else if (1 == count%3) { + tempurl.Assign(temp); + } else { + if (prevResult.IsEmpty()) + printf("no results to compare to!\n"); + else + { + int32_t res; + printf("Result: %s\n", prevResult.get()); + if (urlFactory != URL_FACTORY_DEFAULT) { + printf("Expected: %s\n", tempurl.get()); + res = PL_strcmp(tempurl.get(), prevResult.get()); + } else { + printf("Expected: %s\n", temp); + res = PL_strcmp(temp, prevResult.get()); + } + + if (res == 0) + printf("\tPASSED\n\n"); + else + { + printf("\tFAILED\n\n"); + failed++; + } + } + } + count++; + } + if (failed>0) { + printf("%d tests FAILED out of %d\n", failed, count/3); + return NS_ERROR_FAILURE; + } else { + printf("All %d tests PASSED.\n", count/3); + return NS_OK; + } +} + +nsresult makeAbsTest(const char* i_BaseURI, const char* relativePortion, + const char* expectedResult) +{ + if (!i_BaseURI) + return NS_ERROR_FAILURE; + + // build up the base URL + nsresult status; + nsCOMPtr<nsIURI> baseURL = do_CreateInstance(kStdURLCID, &status); + if (NS_FAILED(status)) + { + printf("CreateInstance failed\n"); + return status; + } + status = baseURL->SetSpec(nsDependentCString(i_BaseURI)); + if (NS_FAILED(status)) return status; + + + // get the new spec + nsAutoCString newURL; + status = baseURL->Resolve(nsDependentCString(relativePortion), newURL); + if (NS_FAILED(status)) return status; + + printf("Analyzing %s\n", baseURL->GetSpecOrDefault().get()); + printf("With %s\n", relativePortion); + + printf("Got %s\n", newURL.get()); + if (expectedResult) { + printf("Expect %s\n", expectedResult); + int res = PL_strcmp(newURL.get(), expectedResult); + if (res == 0) { + printf("\tPASSED\n\n"); + return NS_OK; + } else { + printf("\tFAILED\n\n"); + return NS_ERROR_FAILURE; + } + } + return NS_OK; +} + +nsresult doMakeAbsTest(const char* i_URL = 0, const char* i_relativePortion=0) +{ + if (i_URL && i_relativePortion) + { + return makeAbsTest(i_URL, i_relativePortion, nullptr); + } + + // Run standard tests. These tests are based on the ones described in + // rfc2396 with the exception of the handling of ?y which is wrong as + // notified by on of the RFC authors. + + /* Section C.1. Normal Examples + + g:h = <URL:g:h> + g = <URL:http://a/b/c/g> + ./g = <URL:http://a/b/c/g> + g/ = <URL:http://a/b/c/g/> + /g = <URL:http://a/g> + //g = <URL:http://g> + ?y = <URL:http://a/b/c/d;p?y> + g?y = <URL:http://a/b/c/g?y> + g?y/./x = <URL:http://a/b/c/g?y/./x> + #s = <URL:http://a/b/c/d;p?q#s> + g#s = <URL:http://a/b/c/g#s> + g#s/./x = <URL:http://a/b/c/g#s/./x> + g?y#s = <URL:http://a/b/c/g?y#s> + ;x = <URL:http://a/b/c/;x> + g;x = <URL:http://a/b/c/g;x> + g;x?y#s = <URL:http://a/b/c/g;x?y#s> + . = <URL:http://a/b/c/> + ./ = <URL:http://a/b/c/> + .. = <URL:http://a/b/> + ../ = <URL:http://a/b/> + ../g = <URL:http://a/b/g> + ../.. = <URL:http://a/> + ../../ = <URL:http://a/> + ../../g = <URL:http://a/g> + */ + + struct test { + const char* baseURL; + const char* relativeURL; + const char* expectedResult; + }; + + test tests[] = { + // Tests from rfc2396, section C.1 with the exception of the + // handling of ?y + { "http://a/b/c/d;p?q#f", "g:h", "g:h" }, + { "http://a/b/c/d;p?q#f", "g", "http://a/b/c/g" }, + { "http://a/b/c/d;p?q#f", "./g", "http://a/b/c/g" }, + { "http://a/b/c/d;p?q#f", "g/", "http://a/b/c/g/" }, + { "http://a/b/c/d;p?q#f", "/g", "http://a/g" }, + { "http://a/b/c/d;p?q#f", "//g", "http://g" }, + { "http://a/b/c/d;p?q#f", "?y", "http://a/b/c/d;p?y" }, + { "http://a/b/c/d;p?q#f", "g?y", "http://a/b/c/g?y" }, + { "http://a/b/c/d;p?q#f", "g?y/./x", "http://a/b/c/g?y/./x" }, + { "http://a/b/c/d;p?q#f", "#s", "http://a/b/c/d;p?q#s" }, + { "http://a/b/c/d;p?q#f", "g#s", "http://a/b/c/g#s" }, + { "http://a/b/c/d;p?q#f", "g#s/./x", "http://a/b/c/g#s/./x" }, + { "http://a/b/c/d;p?q#f", "g?y#s", "http://a/b/c/g?y#s" }, + { "http://a/b/c/d;p?q#f", ";x", "http://a/b/c/;x" }, + { "http://a/b/c/d;p?q#f", "g;x", "http://a/b/c/g;x" }, + { "http://a/b/c/d;p?q#f", "g;x?y#s", "http://a/b/c/g;x?y#s" }, + { "http://a/b/c/d;p?q#f", ".", "http://a/b/c/" }, + { "http://a/b/c/d;p?q#f", "./", "http://a/b/c/" }, + { "http://a/b/c/d;p?q#f", "..", "http://a/b/" }, + { "http://a/b/c/d;p?q#f", "../", "http://a/b/" }, + { "http://a/b/c/d;p?q#f", "../g", "http://a/b/g" }, + { "http://a/b/c/d;p?q#f", "../..", "http://a/" }, + { "http://a/b/c/d;p?q#f", "../../", "http://a/" }, + { "http://a/b/c/d;p?q#f", "../../g", "http://a/g" }, + + // Our additional tests... + { "http://a/b/c/d;p?q#f", "#my::anchor", "http://a/b/c/d;p?q#my::anchor" }, + { "http://a/b/c/d;p?q#f", "get?baseRef=viewcert.jpg", "http://a/b/c/get?baseRef=viewcert.jpg" }, + + // Make sure relative query's work right even if the query + // string contains absolute urls or other junk. + { "http://a/b/c/d;p?q#f", "?http://foo", "http://a/b/c/d;p?http://foo" }, + { "http://a/b/c/d;p?q#f", "g?http://foo", "http://a/b/c/g?http://foo" }, + {"http://a/b/c/d;p?q#f", "g/h?http://foo", "http://a/b/c/g/h?http://foo" }, + { "http://a/b/c/d;p?q#f", "g/h/../H?http://foo","http://a/b/c/g/H?http://foo" }, + { "http://a/b/c/d;p?q#f", "g/h/../H?http://foo?baz", "http://a/b/c/g/H?http://foo?baz" }, + { "http://a/b/c/d;p?q#f", "g/h/../H?http://foo;baz", "http://a/b/c/g/H?http://foo;baz" }, + { "http://a/b/c/d;p?q#f", "g/h/../H?http://foo#bar", "http://a/b/c/g/H?http://foo#bar" }, + { "http://a/b/c/d;p?q#f", "g/h/../H;baz?http://foo", "http://a/b/c/g/H;baz?http://foo" }, + { "http://a/b/c/d;p?q#f", "g/h/../H;baz?http://foo#bar", "http://a/b/c/g/H;baz?http://foo#bar" }, + { "http://a/b/c/d;p?q#f", R"(g/h/../H;baz?C:\temp)", R"(http://a/b/c/g/H;baz?C:\temp)" }, + { "http://a/b/c/d;p?q#f", "", "http://a/b/c/d;p?q" }, + { "http://a/b/c/d;p?q#f", "#", "http://a/b/c/d;p?q#" }, + { "http://a/b/c;p/d;p?q#f", "../g;p" , "http://a/b/g;p" }, + + }; + + const int numTests = sizeof(tests) / sizeof(tests[0]); + int failed = 0; + nsresult rv; + for (auto & test : tests) + { + rv = makeAbsTest(test.baseURL, test.relativeURL, + test.expectedResult); + if (NS_FAILED(rv)) + failed++; + } + if (failed>0) { + printf("%d tests FAILED out of %d\n", failed, numTests); + return NS_ERROR_FAILURE; + } else { + printf("All %d tests PASSED.\n", numTests); + return NS_OK; + } +} + +void printusage(void) +{ + printf("urltest [-std] [-file <filename>] <URL> " + " [-abs <relative>]\n\n" + "\t-std : Generate results using nsStdURL.\n" + "\t-file : Read URLs from file.\n" + "\t-abs : Make an absolute URL from the base (<URL>) and the\n" + "\t\trelative path specified. If -abs is given without\n" + "\t\ta base URI standard RFC 2396 relative URL tests\n" + "\t\tare performed. Implies -std.\n" + "\t<URL> : The string representing the URL.\n"); +} + +int main(int argc, char **argv) +{ + if (test_common_init(&argc, &argv) != 0) + return -1; + + if (argc < 2) { + printusage(); + return 0; + } + { + nsCOMPtr<nsIServiceManager> servMan; + NS_InitXPCOM2(getter_AddRefs(servMan), nullptr, nullptr); + + // end of all messages from register components... + printf("------------------\n\n"); + + int32_t urlFactory = URL_FACTORY_DEFAULT; + bool bMakeAbs= false; + char* relativePath = 0; + char* url = 0; + for (int i=1; i<argc; i++) { + if (PL_strcasecmp(argv[i], "-std") == 0) + { + urlFactory = URL_FACTORY_STDURL; + if (i+1 >= argc) + { + printusage(); + return 0; + } + } + else if (PL_strcasecmp(argv[i], "-abs") == 0) + { + if (!gFileIO) + { + relativePath = argv[i+1]; + i++; + } + bMakeAbs = true; + } + else if (PL_strcasecmp(argv[i], "-file") == 0) + { + if (i+1 >= argc) + { + printusage(); + return 0; + } + gFileIO = argv[i+1]; + i++; + } + else + { + url = argv[i]; + } + } + PRTime startTime = PR_Now(); + if (bMakeAbs) + { + if (url && relativePath) { + doMakeAbsTest(url, relativePath); + } else { + doMakeAbsTest(); + } + } + else + { + if (gFileIO) { + testURL(0, urlFactory); + } else { + testURL(url, urlFactory); + } + } + if (gFileIO) + { + PRTime endTime = PR_Now(); + printf("Elapsed time: %d micros.\n", (int32_t) + (endTime - startTime)); + } + } // this scopes the nsCOMPtrs + // no nsCOMPtrs are allowed to be alive when you call NS_ShutdownXPCOM + return NS_FAILED(NS_ShutdownXPCOM(nullptr)) ? 1 : 0; +} diff --git a/netwerk/test/urltests.dat b/netwerk/test/urltests.dat new file mode 100644 index 0000000000..d1f42fc76a --- /dev/null +++ b/netwerk/test/urltests.dat @@ -0,0 +1,43 @@ +# Any blank lines and those beginning with # are comments and +# ignored. To add additional test cases that could potentially +# break URL parsing in mozilla add the input URL on a new line +# and follow it with the expected output. Then run urltest on +# this file and hopefully the expected output should match the +# one from the program. +# - Gagan Saksena 03/28/00 + +http://username:password@hostname.com:80/pathname/./more/stuff/../path +http,username:password,hostname.com,80,,/pathname/more/path + +username@host:8080/path +,username,host,8080,,/path + +http://gagan/ +http,,gagan,-1,,/ + +scheme:host/netlib +scheme,,host,-1,,/netlib + +mailbox:///foo +mailbox,,,-1,,/foo + +scheme:user@hostname.edu:80/pathname +scheme,user,hostname.edu,80,,/pathname + +http://username:password@hostname:80/pathname +http,username:password,hostname,80,,/pathname + +resource:/pathname +resource,,,-1,,/pathname + +ftp://uname%here.com:pwd@there.com/aPath/a.html +ftp,uname%here.com:pwd,there.com,-1,,/aPath/a.html + +http://www.inf.bme.hu?foo=bar +http,,www.inf.bme.hu,-1,foo=bar,/?foo=bar + +http://test.com/aPath/a.html#/1/2 +http,,test.com,-1,,/aPath/a.html#/1/2 + +http://user:pass@ipaddres:2/get?foo/something +http,user:pass,ipaddres,2,foo/something,/get?foo/something |