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 255889 edadb5717a9c10399b0c1d4f2897feadbca2b726
parent 255888 9ccc4502a5f07e10f9cebf0304273643d7d65ce6
child 255890 4168d9fc720767621dbbb39e572ce4655f164951
push id4610
push userjlund@mozilla.com
push dateMon, 30 Mar 2015 18:32:55 +0000
treeherdermozilla-beta@4df54044d9ef [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewershonzab
bugs1120985
milestone38.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
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]