Bug 1120985 - Allow nsMultiMixedConv to compute its boundary if content-type=application/package r=honzab
authorValentin Gosu <valentin.gosu@gmail.com>
Thu, 12 Feb 2015 00:11:19 +0200
changeset 228776 edadb5717a9c10399b0c1d4f2897feadbca2b726
parent 228775 9ccc4502a5f07e10f9cebf0304273643d7d65ce6
child 228777 4168d9fc720767621dbbb39e572ce4655f164951
push idunknown
push userunknown
push dateunknown
reviewershonzab
bugs1120985
milestone38.0a1
Bug 1120985 - Allow nsMultiMixedConv to compute its boundary if content-type=application/package r=honzab Also makes nsMultiMixedConv/nsPartChannel save and return individual headers for each part of the resource file.
netwerk/base/moz.build
netwerk/base/nsIResponseHeadProvider.idl
netwerk/protocol/http/nsHttpHeaderArray.h
netwerk/protocol/http/nsHttpResponseHead.h
netwerk/streamconv/converters/nsMultiMixedConv.cpp
netwerk/streamconv/converters/nsMultiMixedConv.h
netwerk/test/unit/test_multipart_streamconv_application_package.js
netwerk/test/unit/xpcshell.ini
--- a/netwerk/base/moz.build
+++ b/netwerk/base/moz.build
@@ -82,16 +82,17 @@ XPIDL_SOURCES += [
     'nsIProxyInfo.idl',
     'nsIRandomGenerator.idl',
     'nsIRedirectChannelRegistrar.idl',
     'nsIRedirectHistory.idl',
     'nsIRedirectResultListener.idl',
     'nsIRequest.idl',
     'nsIRequestObserver.idl',
     'nsIRequestObserverProxy.idl',
+    'nsIResponseHeadProvider.idl',
     'nsIResumableChannel.idl',
     'nsISecretDecoderRing.idl',
     'nsISecureBrowserUI.idl',
     'nsISecurityEventSink.idl',
     'nsISecurityInfoProvider.idl',
     'nsISerializationHelper.idl',
     'nsIServerSocket.idl',
     'nsISimpleStreamListener.idl',
new file mode 100644
--- /dev/null
+++ b/netwerk/base/nsIResponseHeadProvider.idl
@@ -0,0 +1,35 @@
+/* -*- 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 "nsISupports.idl"
+
+interface nsIHttpHeaderVisitor;
+
+%{C++
+namespace mozilla {
+namespace net {
+class nsHttpResponseHead;
+}
+}
+%}
+
+[ptr] native nsHttpResponseHeadPtr(mozilla::net::nsHttpResponseHead);
+
+/**
+ * nsIResponseHeadProvider
+ */
+[scriptable, builtinclass, uuid(cd0d0804-2e0c-4bff-aa0a-78a3e3159b69)]
+interface nsIResponseHeadProvider : nsISupports
+{
+  /**
+   * Returns a pointer to a nsHttpResponseHead. May return null.
+   */
+  [notxpcom] nsHttpResponseHeadPtr GetResponseHead();
+
+  /**
+   * May be used to iterate through the response headers
+   */
+  void visitResponseHeaders(in nsIHttpHeaderVisitor aVisitor);
+};
--- a/netwerk/protocol/http/nsHttpHeaderArray.h
+++ b/netwerk/protocol/http/nsHttpHeaderArray.h
@@ -8,16 +8,22 @@
 #define nsHttpHeaderArray_h__
 
 #include "nsHttp.h"
 #include "nsTArray.h"
 #include "nsString.h"
 
 class nsIHttpHeaderVisitor;
 
+// This needs to be forward declared here so we can include only this header
+// without also including PHttpChannelParams.h
+namespace IPC {
+    template <typename> struct ParamTraits;
+}
+
 namespace mozilla { namespace net {
 
 class nsHttpHeaderArray
 {
 public:
     const char *PeekHeader(nsHttpAtom header) const;
 
     // Used by internal setters: to set header from network use SetHeaderFromNet
--- a/netwerk/protocol/http/nsHttpResponseHead.h
+++ b/netwerk/protocol/http/nsHttpResponseHead.h
@@ -5,16 +5,22 @@
 
 #ifndef nsHttpResponseHead_h__
 #define nsHttpResponseHead_h__
 
 #include "nsHttpHeaderArray.h"
 #include "nsHttp.h"
 #include "nsString.h"
 
+// This needs to be forward declared here so we can include only this header
+// without also including PHttpChannelParams.h
+namespace IPC {
+    template <typename> struct ParamTraits;
+}
+
 namespace mozilla { namespace net {
 
 //-----------------------------------------------------------------------------
 // nsHttpResponseHead represents the status line and headers from an HTTP
 // response.
 //-----------------------------------------------------------------------------
 
 class nsHttpResponseHead
--- a/netwerk/streamconv/converters/nsMultiMixedConv.cpp
+++ b/netwerk/streamconv/converters/nsMultiMixedConv.cpp
@@ -104,16 +104,17 @@ NS_IMPL_ADDREF(nsPartChannel)
 NS_IMPL_RELEASE(nsPartChannel)
 
 NS_INTERFACE_MAP_BEGIN(nsPartChannel)
     NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIChannel)
     NS_INTERFACE_MAP_ENTRY(nsIRequest)
     NS_INTERFACE_MAP_ENTRY(nsIChannel)
     NS_INTERFACE_MAP_ENTRY(nsIByteRangeRequest)
     NS_INTERFACE_MAP_ENTRY(nsIMultiPartChannel)
+    NS_INTERFACE_MAP_ENTRY(nsIResponseHeadProvider)
 NS_INTERFACE_MAP_END
 
 //
 // nsIRequest implementation...
 //
 
 NS_IMETHODIMP
 nsPartChannel::GetName(nsACString &aResult)
@@ -377,16 +378,34 @@ nsPartChannel::GetPartID(uint32_t *aPart
 NS_IMETHODIMP
 nsPartChannel::GetIsLastPart(bool *aIsLastPart)
 {
     *aIsLastPart = mIsLastPart;
     return NS_OK;
 }
 
 //
+// nsIResponseHeadProvider
+//
+
+NS_IMETHODIMP_(mozilla::net::nsHttpResponseHead *)
+nsPartChannel::GetResponseHead()
+{
+    return mResponseHead;
+}
+
+NS_IMETHODIMP
+nsPartChannel::VisitResponseHeaders(nsIHttpHeaderVisitor *visitor)
+{
+    if (!mResponseHead)
+        return NS_ERROR_NOT_AVAILABLE;
+    return mResponseHead->Headers().VisitHeaders(visitor);
+}
+
+//
 // nsIByteRangeRequest implementation...
 //
 
 NS_IMETHODIMP 
 nsPartChannel::GetIsByteRangeRequest(bool *aIsByteRangeRequest)
 {
     *aIsByteRangeRequest = mIsByteRangeRequest;
 
@@ -478,20 +497,16 @@ private:
   char *mBuffer;
 };
 
 // nsIStreamListener implementation
 NS_IMETHODIMP
 nsMultiMixedConv::OnDataAvailable(nsIRequest *request, nsISupports *context,
                                   nsIInputStream *inStr, uint64_t sourceOffset,
                                   uint32_t count) {
-
-    if (mToken.IsEmpty()) // no token, no love.
-        return NS_ERROR_FAILURE;
-
     nsresult rv = NS_OK;
     AutoFree buffer(nullptr);
     uint32_t bufLen = 0, read = 0;
 
     NS_ASSERTION(request, "multimixed converter needs a request");
 
     nsCOMPtr<nsIChannel> channel = do_QueryInterface(request, &rv);
     if (NS_FAILED(rv)) return rv;
@@ -524,26 +539,50 @@ nsMultiMixedConv::OnDataAvailable(nsIReq
     if (mFirstOnData) {
         // this is the first OnData() for this request. some servers
         // don't bother sending a token in the first "part." This is
         // illegal, but we'll handle the case anyway by shoving the
         // boundary token in for the server.
         mFirstOnData = false;
         NS_ASSERTION(!mBufLen, "this is our first time through, we can't have buffered data");
         const char * token = mToken.get();
-           
+
         PushOverLine(cursor, bufLen);
 
-        if (bufLen < mTokenLen+2) {
+        bool needMoreChars = bufLen < mTokenLen + 2;
+        nsAutoCString firstBuffer(buffer, bufLen);
+        int32_t posCR = firstBuffer.Find("\r");
+
+        if (needMoreChars || (posCR == kNotFound)) {
             // we don't have enough data yet to make this comparison.
             // skip this check, and try again the next time OnData()
             // is called.
             mFirstOnData = true;
-        }
-        else if (!PL_strnstr(cursor, token, mTokenLen+2)) {
+        } else if (mPackagedApp) {
+            // We need to check the line starts with --
+            if (!StringBeginsWith(firstBuffer, NS_LITERAL_CSTRING("--"))) {
+                return NS_ERROR_FAILURE;
+            }
+
+            // If the boundary was set in the header,
+            // we need to check it matches with the one in the file.
+            if (mTokenLen &&
+                !StringBeginsWith(Substring(firstBuffer, 2), mToken)) {
+                return NS_ERROR_FAILURE;
+            }
+
+            // Save the token.
+            if (!mTokenLen) {
+                mToken = nsCString(Substring(firstBuffer, 2).BeginReading(),
+                                   posCR - 2);
+                mTokenLen = mToken.Length();
+            }
+
+            cursor = buffer;
+        } else if (!PL_strnstr(cursor, token, mTokenLen + 2)) {
             char *newBuffer = (char *) realloc(buffer, bufLen + mTokenLen + 1);
             if (!newBuffer)
                 return NS_ERROR_OUT_OF_MEMORY;
             buffer = newBuffer;
 
             memmove(buffer + mTokenLen + 1, buffer, bufLen);
             memcpy(buffer, token, mTokenLen);
             buffer[mTokenLen] = '\n';
@@ -552,16 +591,19 @@ nsMultiMixedConv::OnDataAvailable(nsIReq
 
             // need to reset cursor to the buffer again (bug 100595)
             cursor = buffer;
         }
     }
 
     char *token = nullptr;
 
+    // This may get initialized by ParseHeaders and the resulting
+    // HttpResponseHead will be passed to nsPartChannel by SendStart
+
     if (mProcessingHeaders) {
         // we were not able to process all the headers
         // for this "part" given the previous buffer given to 
         // us in the previous OnDataAvailable callback.
         bool done = false;
         rv = ParseHeaders(channel, cursor, bufLen, &done);
         if (NS_FAILED(rv)) return rv;
 
@@ -689,47 +731,71 @@ nsMultiMixedConv::OnStartRequest(nsIRequ
         rv = httpChannel->GetResponseHeader(NS_LITERAL_CSTRING("content-type"), delimiter);
         if (NS_FAILED(rv)) return rv;
     } else {
         // try asking the channel directly
         rv = channel->GetContentType(delimiter);
         if (NS_FAILED(rv)) return NS_ERROR_FAILURE;
     }
 
+    // http://www.w3.org/TR/web-packaging/#streamable-package-format
+    // Although it is compatible with multipart/* this format does not require
+    // the boundary to be included in the header, as it can be ascertained from
+    // the content of the file.
+    if (delimiter.Find("application/package") != kNotFound) {
+        mPackagedApp = true;
+        mToken.Truncate();
+        mTokenLen = 0;
+    }
+
     bndry = strstr(delimiter.BeginWriting(), "boundary");
-    if (!bndry) return NS_ERROR_FAILURE;
+
+    if (!bndry && mPackagedApp) {
+        return NS_OK;
+    }
+
+    if (!bndry) {
+        return NS_ERROR_FAILURE;
+    }
 
     bndry = strchr(bndry, '=');
     if (!bndry) return NS_ERROR_FAILURE;
 
     bndry++; // move past the equals sign
 
     char *attrib = (char *) strchr(bndry, ';');
     if (attrib) *attrib = '\0';
 
     nsAutoCString boundaryString(bndry);
     if (attrib) *attrib = ';';
 
     boundaryString.Trim(" \"");
 
     mToken = boundaryString;
     mTokenLen = boundaryString.Length();
-    
-    if (mTokenLen == 0)
-        return NS_ERROR_FAILURE;
+
+   if (mTokenLen == 0 && !mPackagedApp) {
+       return NS_ERROR_FAILURE;
+   }
 
     return NS_OK;
 }
 
 NS_IMETHODIMP
 nsMultiMixedConv::OnStopRequest(nsIRequest *request, nsISupports *ctxt,
                                 nsresult aStatus) {
 
-    if (mToken.IsEmpty())  // no token, no love.
-        return NS_ERROR_FAILURE;
+    nsresult rv = NS_OK;
+
+    // We should definitely have found a token at this point. Not having one
+    // is clearly an error, so we need to pass it to the listener.
+    if (mToken.IsEmpty()) {
+        aStatus = NS_ERROR_FAILURE;
+        rv = NS_ERROR_FAILURE;
+    }
 
     if (mPartChannel) {
         mPartChannel->SetIsLastPart();
 
         // we've already called SendStart() (which sets up the mPartChannel,
         // and fires an OnStart()) send any data left over, and then fire the stop.
         if (mBufLen > 0 && mBuffer) {
             (void) SendData(mBuffer, mBufLen);
@@ -748,17 +814,17 @@ nsMultiMixedConv::OnStopRequest(nsIReque
         // if we send the start, the URI Loader's m_targetStreamListener, may
         // be pointing at us causing a nice stack overflow.  So, don't call 
         // OnStartRequest!  -  This breaks necko's semantecs. 
         //(void) mFinalListener->OnStartRequest(request, ctxt);
         
         (void) mFinalListener->OnStopRequest(request, ctxt, aStatus);
     }
 
-    return NS_OK;
+    return rv;
 }
 
 
 // nsMultiMixedConv methods
 nsMultiMixedConv::nsMultiMixedConv() :
   mCurrentPartID(0)
 {
     mTokenLen           = 0;
@@ -766,16 +832,17 @@ nsMultiMixedConv::nsMultiMixedConv() :
     mContentLength      = UINT64_MAX;
     mBuffer             = nullptr;
     mBufLen             = 0;
     mProcessingHeaders  = false;
     mByteRangeStart     = 0;
     mByteRangeEnd       = 0;
     mTotalSent          = 0;
     mIsByteRangeRequest = false;
+    mPackagedApp        = false;
 }
 
 nsMultiMixedConv::~nsMultiMixedConv() {
     NS_ASSERTION(!mBuffer, "all buffered data should be gone");
     if (mBuffer) {
         free(mBuffer);
         mBuffer = nullptr;
     }
@@ -830,16 +897,19 @@ nsMultiMixedConv::SendStart(nsIChannel *
         newChannel->InitializeByteRange(mByteRangeStart, mByteRangeEnd);
     }
 
     mTotalSent = 0;
 
     // Set up the new part channel...
     mPartChannel = newChannel;
 
+    // We pass the headers to the nsPartChannel
+    mPartChannel->SetResponseHead(mResponseHead.forget());
+
     rv = mPartChannel->SetContentType(mContentType);
     if (NS_FAILED(rv)) return rv;
 
     rv = mPartChannel->SetContentLength(mContentLength);
     if (NS_FAILED(rv)) return rv;
 
     mPartChannel->SetContentDisposition(mContentDisposition);
 
@@ -936,17 +1006,24 @@ nsMultiMixedConv::ParseHeaders(nsIChanne
                                uint32_t &aLen, bool *_retval) {
     // NOTE: this data must be ascii.
     // NOTE: aPtr is NOT null terminated!
     nsresult rv = NS_OK;
     char *cursor = aPtr, *newLine = nullptr;
     uint32_t cursorLen = aLen;
     bool done = false;
     uint32_t lineFeedIncrement = 1;
-    
+
+    // We only create an nsHttpResponseHead for packaged app channels
+    // It may already be initialized, from a previous call of ParseHeaders
+    // since the headers for a single part may come in more then one chunk
+    if (mPackagedApp && !mResponseHead) {
+        mResponseHead = new nsHttpResponseHead();
+    }
+
     mContentLength = UINT64_MAX; // XXX what if we were already called?
     while (cursorLen && (newLine = (char *) memchr(cursor, nsCRT::LF, cursorLen))) {
         // adjust for linefeeds
         if ((newLine > cursor) && (newLine[-1] == nsCRT::CR) ) { // CRLF
             lineFeedIncrement = 2;
             newLine--;
         }
         else
@@ -960,16 +1037,23 @@ nsMultiMixedConv::ParseHeaders(nsIChanne
             cursorLen -= lineFeedIncrement;
 
             done = true;
             break;
         }
 
         char tmpChar = *newLine;
         *newLine = '\0'; // cursor is now null terminated
+
+        if (mResponseHead) {
+            // ParseHeaderLine is destructive. We create a copy
+            nsAutoCString tmpHeader(cursor);
+            mResponseHead->ParseHeaderLine(tmpHeader.get());
+        }
+
         char *colon = (char *) strchr(cursor, ':');
         if (colon) {
             *colon = '\0';
             nsAutoCString headerStr(cursor);
             headerStr.CompressWhitespace();
             *colon = ':';
 
             nsAutoCString headerVal(colon + 1);
--- a/netwerk/streamconv/converters/nsMultiMixedConv.h
+++ b/netwerk/streamconv/converters/nsMultiMixedConv.h
@@ -9,16 +9,20 @@
 #include "nsIChannel.h"
 #include "nsString.h"
 #include "nsCOMPtr.h"
 #include "nsIByteRangeRequest.h"
 #include "nsILoadInfo.h"
 #include "nsIMultiPartChannel.h"
 #include "nsAutoPtr.h"
 #include "mozilla/Attributes.h"
+#include "nsIResponseHeadProvider.h"
+#include "nsHttpResponseHead.h"
+
+using mozilla::net::nsHttpResponseHead;
 
 #define NS_MULTIMIXEDCONVERTER_CID                         \
 { /* 7584CE90-5B25-11d3-A175-0050041CAF44 */         \
     0x7584ce90,                                      \
     0x5b25,                                          \
     0x11d3,                                          \
     {0xa1, 0x75, 0x0, 0x50, 0x4, 0x1c, 0xaf, 0x44}       \
 }
@@ -27,45 +31,49 @@
 // nsPartChannel is a "dummy" channel which represents an individual part of
 // a multipart/mixed stream...
 //
 // Instances on this channel are passed out to the consumer through the
 // nsIStreamListener interface.
 //
 class nsPartChannel MOZ_FINAL : public nsIChannel,
                                 public nsIByteRangeRequest,
+                                public nsIResponseHeadProvider,
                                 public nsIMultiPartChannel
 {
 public:
   nsPartChannel(nsIChannel *aMultipartChannel, uint32_t aPartID,
                 nsIStreamListener* aListener);
 
   void InitializeByteRange(int64_t aStart, int64_t aEnd);
   void SetIsLastPart() { mIsLastPart = true; }
   nsresult SendOnStartRequest(nsISupports* aContext);
   nsresult SendOnDataAvailable(nsISupports* aContext, nsIInputStream* aStream,
                                uint64_t aOffset, uint32_t aLen);
   nsresult SendOnStopRequest(nsISupports* aContext, nsresult aStatus);
   /* SetContentDisposition expects the full value of the Content-Disposition
    * header */
   void SetContentDisposition(const nsACString& aContentDispositionHeader);
+  void SetResponseHead(nsHttpResponseHead * head) { mResponseHead = head; }
 
   NS_DECL_ISUPPORTS
   NS_DECL_NSIREQUEST
   NS_DECL_NSICHANNEL
   NS_DECL_NSIBYTERANGEREQUEST
+  NS_DECL_NSIRESPONSEHEADPROVIDER
   NS_DECL_NSIMULTIPARTCHANNEL
 
 protected:
   ~nsPartChannel();
 
 protected:
   nsCOMPtr<nsIChannel>    mMultipartChannel;
   nsCOMPtr<nsIStreamListener> mListener;
-  
+  nsAutoPtr<nsHttpResponseHead> mResponseHead;
+
   nsresult                mStatus;
   nsLoadFlags             mLoadFlags;
 
   nsCOMPtr<nsILoadGroup>  mLoadGroup;
 
   nsCString               mContentType;
   nsCString               mContentCharset;
   uint32_t                mContentDisposition;
@@ -163,11 +171,17 @@ protected:
     // The following members are for tracking the byte ranges in
     // multipart/mixed content which specified the 'Content-Range:'
     // header...
     int64_t             mByteRangeStart;
     int64_t             mByteRangeEnd;
     bool                mIsByteRangeRequest;
 
     uint32_t            mCurrentPartID;
+
+    // This is true if the content-type is application/package
+    // Streamable packages don't require the boundary in the header
+    // as it can be ascertained from the package file.
+    bool                mPackagedApp;
+    nsAutoPtr<nsHttpResponseHead> mResponseHead;
 };
 
 #endif /* __nsmultimixedconv__h__ */
new file mode 100644
--- /dev/null
+++ b/netwerk/test/unit/test_multipart_streamconv_application_package.js
@@ -0,0 +1,211 @@
+// Tests:
+//   test_multipart
+//     Loads the multipart file returned by contentHandler()
+//     The boundary is ascertained from the first line in the multipart file
+//   test_multipart_with_boundary
+//     Loads the multipart file returned by contentHandler_with_boundary()
+//     The boundary is given in the Content-Type headers, and is also present
+//     in the first line of the file.
+//   test_multipart_chunked_headers
+//     Tests that the headers are properly passed even when they come in multiple
+//     chunks (several calls to OnDataAvailable). It first passes the first 60
+//     characters, then the rest of the response.
+
+// testData.token - the multipart file's boundary
+// Call testData.getData() to get the file contents as a string
+
+// multipartListener
+//   - a listener that checks that the multipart file is correctly split into multiple parts
+
+// headerListener
+//   - checks that the headers for each part is set correctly
+
+Cu.import("resource://testing-common/httpd.js");
+Cu.import("resource://gre/modules/Services.jsm");
+
+var httpserver = null;
+
+XPCOMUtils.defineLazyGetter(this, "uri", function() {
+  return "http://localhost:" + httpserver.identity.primaryPort;
+});
+
+function make_channel(url) {
+  var ios = Cc["@mozilla.org/network/io-service;1"].
+            getService(Ci.nsIIOService);
+  return ios.newChannel2(url,
+                         "",
+                         null,
+                         null,      // aLoadingNode
+                         Services.scriptSecurityManager.getSystemPrincipal(),
+                         null,      // aTriggeringPrincipal
+                         Ci.nsILoadInfo.SEC_NORMAL,
+                         Ci.nsIContentPolicy.TYPE_OTHER);
+}
+
+function contentHandler(metadata, response)
+{
+  response.setHeader("Content-Type", 'application/package');
+  var body = testData.getData();
+  response.bodyOutputStream.write(body, body.length);
+}
+
+function contentHandler_with_boundary(metadata, response)
+{
+  response.setHeader("Content-Type", 'application/package; boundary="'+testData.token+'"');
+  var body = testData.getData();
+  response.bodyOutputStream.write(body, body.length);
+}
+
+function contentHandler_chunked_headers(metadata, response)
+{
+  response.setHeader("Content-Type", 'application/package');
+  var body = testData.getData();
+
+  response.bodyOutputStream.write(body.substring(0,60), 60);
+  response.processAsync();
+  do_timeout(5, function() {
+    response.bodyOutputStream.write(body.substring(60), body.length-60);
+    response.finish();
+  });
+}
+
+var testData = {
+  content: [
+   { headers: ["Content-Location: /index.html", "Content-Type: text/html"], data: "<html>\r\n  <head>\r\n    <script src=\"/scripts/app.js\"></script>\r\n    ...\r\n  </head>\r\n  ...\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;
+  }
+}
+
+function multipartListener(test) {
+  this._buffer = "";
+  this.testNum = 0;
+  this.test = test;
+  this.numTests = this.test.content.length;
+}
+
+multipartListener.prototype.responseHandler = function(request, buffer) {
+    equal(buffer, this.test.content[this.testNum].data);
+    equal(request.QueryInterface(Ci.nsIChannel).contentType, this.test.content[this.testNum].type);
+    if (++this.testNum == this.numTests) {
+      run_next_test();
+    }
+}
+
+multipartListener.prototype.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;
+}
+
+multipartListener.prototype.onStartRequest = function(request, context) {
+  this._buffer = "";
+  this.headerListener = new headerListener(this.test.content[this.testNum].headers);
+  let headerProvider = request.QueryInterface(Ci.nsIResponseHeadProvider);
+  if (headerProvider) {
+    headerProvider.visitResponseHeaders(this.headerListener);
+  }
+}
+
+multipartListener.prototype.onDataAvailable = function(request, context, stream, offset, count) {
+  try {
+    this._buffer = this._buffer.concat(read_stream(stream, count));
+  } catch (ex) {
+    do_throw("Error in onDataAvailable: " + ex);
+  }
+}
+
+multipartListener.prototype.onStopRequest = function(request, context, status) {
+  try {
+    equal(this.headerListener.index, this.test.content[this.testNum].headers.length);
+    this.responseHandler(request, this._buffer);
+  } catch (ex) {
+    do_throw("Error in closure function: " + ex);
+  }
+}
+
+function headerListener(headers) {
+  this.expectedHeaders = headers;
+  this.index = 0;
+}
+
+headerListener.prototype.QueryInterface = function(iid) {
+  if (iid.equals(Components.interfaces.nsIHttpHeaderVisitor) ||
+      iid.equals(Components.interfaces.nsISupports))
+    return this;
+  throw Components.results.NS_ERROR_NO_INTERFACE;
+}
+
+headerListener.prototype.visitHeader = function(header, value) {
+  ok(this.index <= this.expectedHeaders.length);
+  equal(header + ": " + value, this.expectedHeaders[this.index]);
+  this.index++;
+}
+
+function test_multipart() {
+  var streamConv = Cc["@mozilla.org/streamConverters;1"]
+                     .getService(Ci.nsIStreamConverterService);
+  var conv = streamConv.asyncConvertData("multipart/mixed",
+           "*/*",
+           new multipartListener(testData),
+           null);
+
+  var chan = make_channel(uri + "/multipart");
+  chan.asyncOpen(conv, null);
+}
+
+function test_multipart_with_boundary() {
+  var streamConv = Cc["@mozilla.org/streamConverters;1"]
+                     .getService(Ci.nsIStreamConverterService);
+  var conv = streamConv.asyncConvertData("multipart/mixed",
+           "*/*",
+           new multipartListener(testData),
+           null);
+
+  var chan = make_channel(uri + "/multipart2");
+  chan.asyncOpen(conv, null);
+}
+
+function test_multipart_chunked_headers() {
+  var streamConv = Cc["@mozilla.org/streamConverters;1"]
+                     .getService(Ci.nsIStreamConverterService);
+  var conv = streamConv.asyncConvertData("multipart/mixed",
+           "*/*",
+           new multipartListener(testData),
+           null);
+
+  var chan = make_channel(uri + "/multipart3");
+  chan.asyncOpen(conv, null);
+}
+
+function run_test()
+{
+  httpserver = new HttpServer();
+  httpserver.registerPathHandler("/multipart", contentHandler);
+  httpserver.registerPathHandler("/multipart2", contentHandler_with_boundary);
+  httpserver.registerPathHandler("/multipart3", contentHandler_chunked_headers);
+  httpserver.start(-1);
+
+  run_next_test();
+}
+
+add_test(test_multipart);
+add_test(test_multipart_with_boundary);
+add_test(test_multipart_chunked_headers);
--- a/netwerk/test/unit/xpcshell.ini
+++ b/netwerk/test/unit/xpcshell.ini
@@ -304,9 +304,10 @@ skip-if = os != "win"
 [test_udp_multicast.js]
 [test_redirect_history.js]
 [test_reply_without_content_type.js]
 [test_websocket_offline.js]
 [test_tls_server.js]
 # The local cert service used by this test is not currently shipped on Android
 skip-if = os == "android"
 [test_1073747.js]
+[test_multipart_streamconv_application_package.js]
 [test_safeoutputstream_append.js]