Skip to content

Some servers don't understand quoted boundaries in multipart documents #2544

Closed
@FichteFoll

Description

Long story short

After a prolonged debugging session I discovered that the usage of quotes in the boundary of a multipart document caused my request to be rejected by the server since it didn't support this.
Preventing the quotation marks from being added in the code makes this work.

The server in question is using nginx, but I don't know which version yet (asked their support).

Specification

https://tools.ietf.org/html/rfc7231#section-3.1.1.5
https://tools.ietf.org/html/rfc7231#appendix-D
https://tools.ietf.org/html/rfc7230#section-3.2.6

Content-Type = media-type

media-type = type "/" subtype *( OWS ";" OWS parameter )
parameter = token "=" ( token / quoted-string )

token          = 1*tchar
quoted-string  = DQUOTE *( qdtext / quoted-pair ) DQUOTE
qdtext         = HTAB / SP /%x21 / %x23-5B / %x5D-7E / obs-text
obs-text       = %x80-FF
quoted-pair    = "\" ( HTAB / SP / VCHAR / obs-text )
tchar          = "!" / "#" / "$" / "%" / "&" / "'" / "*"
                   / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~"
                   / DIGIT / ALPHA
                   ; any VCHAR, except delimiters

Expected behaviour

Specify the multipart boundary without quotes, unless they are required.

Actual behaviour

Multipart boundary is always quoted.

Steps to reproduce

Since I don't know which nginx version is affected, the only reliable way to reproduce right now is by filing a request at share-online's upload API.

A sample script is provided below (fill in your data):

import asyncio
import pathlib

import aiohttp

SESSION_URL = "http://www.share-online.biz/upv3_session.php"
USER = ""
PASS = ""
path = pathlib.Path(".gitignore")


async def async_main():
    async with aiohttp.ClientSession() as session:
        data = {'username': USER, 'password': PASS}
        async with session.post(SESSION_URL, data=data) as resp:
            text = await resp.text()
        session_name, session_endpoint = text.split(";")
        session_endpoint = f"http://{session_endpoint}"

        file_size = path.stat().st_size
        with path.open('rb') as fp:
            data = aiohttp.FormData()
            data.add_field('username', USER)
            data.add_field('password', PASS)
            data.add_field('upload_session', session_name)
            data.add_field('chunk_no', "1")
            data.add_field('chunk_number', "1")
            data.add_field('filesize', str(file_size))
            data.add_field('fn', fp, filename=path.name, content_type="application/octet-steam")
            data.add_field('finalize', "1")

            print(f"Starting upload of {path.name}; size: {file_size}")
            async with session.post(session_endpoint, data=data) as resp:
                # print(f"response headers: {resp.headers}")
                resp.raise_for_status()
                text = await resp.text()
                print(f"reply: {text}")


def main():
    loop = asyncio.get_event_loop()
    task = asyncio.ensure_future(async_main())
    try:
        loop.run_until_complete(task)
    finally:
        loop.close()


if __name__ == '__main__':
    main()

With this script, the upload will fail with *** EXCEPTION session creation/reuse failed - 11-21-2017, 3:13 pm ***.

Removing the quotes at

ctype = 'multipart/{}; boundary="{}"'.format(subtype, boundary)
makes the request work.

Your environment

Arch Linux, Python 3.6, aiohttp 2.3.3 (client)

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions