HTTP POST multipart/form-data afhandelen

Pagina: 1
Acties:

Acties:
  • 0 Henk 'm!

  • vdvleon
  • Registratie: Januari 2008
  • Laatst online: 08-06-2023
Hallo iedereen,

Ik wil een http post met type miltipary/form-data kunnen ontvangen. Opzich is dit mij al wel gelukt.
Maar met code die ik daar voor gebruikte was de cpu zwaar bezig (het was wel snel genoeg).

Eerst zou ik eens uitleggen wat ik precies bedoel.

In het HTTP protocol kan je op verschillende manier een pagina op vragen. GET en POST zijn de meeste gebruikte methodes. Met een POST stuurt de client zelf ook gegevens mee (bijv. een formulier (met of zonder bestanden)). Deze zogenaamde 'POST body' kan op verschillende manieren gecodeerd worden. Hier zijn application/x-www-form-urlencoded en multipart/form-data de meest gebruikte.

Standaart word application/x-www-form-urlencoded gebruikt. Hier worden variables net zoals in de url verstuurd. (in de var=value&var2=valu2&... vorm).

Bij multipart/form-data word dat in deze vorm gedaan:

(er word voor deze request een boundary mee bestuurd, dit is een willekeurige tekst)

code:
1
2
3
4
5
6
7
8
9
10
11
12
13
--BOUNDARY
sub header 1

body 1
--BOUNDARY
sub header 2

body 2
--BOUNDARY
sub header 3

body 3
--BOUNDARY--     <-- einde boundary


Het nadeel van dit systeem ten opzichte van application/x-www-form-urlencoded is dat je van te voren niet weet hoe groot een body gaat zijn. Of te wel, je moet steeds alles in een buffer wegschrijven om te weten wanneer die boundary voor komt. Persoonlijk vind ik dit heel slecht ontworpen (als ze nou gewoon van te voren per body zouden melden hoe groot deze is...)

Ik heb dit dus afgevangen dmv van een buffer. Dit werkte, maar omdat ik alles dmv van een buffer moest doen kreeg de cpu het heel druk. In plaats van een bestand dat upgeload word direct weg te schrijven in een bestand, moest alles via de cpu gecontrolleerd worden etc.

Als het een gewone variable werd gepost schreef ik deze gewoon weg in een string, maar als het file was schreef ik deze weg in een bestand.

Weet iemand misschien een wel handige manier om dit te doen? (ik heb op internet niet echt veel kunnen vinden). Misschien een slimmigheidje om met de buffer om te gaan of een andere methode?

PS.
Ik schrijf dus een http server (in C++). Alles behalve een goede multipart/form-data handler heb ik al wel.

Acties:
  • 0 Henk 'm!

  • Face_-_LeSS
  • Registratie: September 2004
  • Niet online
Is het niet mogelijk om het complete request binnen te hengelen en vervolgens te splitten op de boundary?

Acties:
  • 0 Henk 'm!

  • Juup
  • Registratie: Februari 2000
  • Niet online
Wat Face_-_LeSS zegt, of je zoekt naar boundaries terwijl je data binnenkomt, dan kun je de parts al gaan verwerken.

Als je met een buffer werkt hoeft de CPU het niet heel druk te krijgen. Post eens wat relevante code?

Putin 2022: Ukraine is not a real country
Musk 2025: Canada is not a real country
Uh oh...


Acties:
  • 0 Henk 'm!

  • vdvleon
  • Registratie: Januari 2008
  • Laatst online: 08-06-2023
Omdat het grote bestanden (100 MB) kunnen zijn wil je niet alles in je geheugen laden.

Zoals ik het eerste deed was als volgt:

code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
state = HEADER

loop:
    read block to buffer (1024 bytes or so)
    if no extra data:
        stop loop   

    switch state:
        case HEADER:
            if can read header line:
                if empty line:
                    state = VAR ( | FILE)  (ligt aan welke gegevens in de header staan)
                else:
                    handle line
        case VAR:
            if end boundary is in buffer:
                write buffer till boundary to var
                return
            else if boundary is in buffer:
                write buffer till boundary to var
                state = HEADER
            else:
                add to var (half buffer)
        case FILE:
            if end boundary is in buffer:
                write buffer till boundary to file
                return
            else if boundary is in buffer:
                write buffer till boundary to file
                state = HEADER
            else:
                add to file (half buffer)


Beetje snel verzonnen net. Ik houd er rekening mee dat de boundary maar half in de buffer aanwezig is, dus de buffer is altijd minimaal 2 blocks groot (een block is minimaal zo groot als de boundary).

Acties:
  • 0 Henk 'm!

  • Soultaker
  • Registratie: September 2000
  • Laatst online: 11:52
Een eenvoudige oplossing is er niet, ben ik bang. Online string matching is gewoon vervelend.

Het simpelste wat ik kan verzinnen is een circulaire buffer bijhouden met de laatste K karakters die je hebt gelezen (waarbij K de lengte van de boundary is); elke iteratie haal je het laatste karakter uit de buffer (en die schrijf je naar een file of whatever) en voeg je een nieuwe toe, en daarna check je of de inhoud van de buffer gelijk is aan de boundary. Nadeel is dat als de invoer dan N bytes lang is, het algoritme complexiteit O(NK) heeft (in het slechtste geval tenminste; gemiddeld is het wel O(N)).

Als variatie daarop zou je KMP kunnen implementeren zodat je in O(1) kunt matchen. Dan heb je ook geen buffers meer nodig (het maakt niet uit of je databuffer groter of kleiner is dan de boundary string).

Acties:
  • 0 Henk 'm!

  • vdvleon
  • Registratie: Januari 2008
  • Laatst online: 08-06-2023
Ik zou straks eens naar KMP kijken. Ik heb het nu eerst eff druk met school etc. Ik kijk er naar het weekend weer naar ;) (heb 3 deel-tentames deze week :S)

Acties:
  • 0 Henk 'm!

  • Creepy
  • Registratie: Juni 2001
  • Nu online

Creepy

Tactical Espionage Splatterer

Vaak heb je per part een content-lenght header. Dus op dat moment weet je precies hoveel data er gaat komen van dat deel. Ook moet je niet vergeten dat je per part ook nog een content-encoding hebt. Als de data als base64 meekomt dan kan je dit natuurlijk ook niet direct wegschrijven. Bedenk wel dat je nu zelf een mime parsers aan het maken bent. Niet erg natuurlijk, maar daar zijn kant en klare oplossing voor te vinden voor de meeste ontwikkelomgeving (als deze ze al niet standaard aan boord hebben)

[ Voor 27% gewijzigd door Creepy op 01-12-2009 22:06 ]

"I had a problem, I solved it with regular expressions. Now I have two problems". That's shows a lack of appreciation for regular expressions: "I know have _star_ problems" --Kevlin Henney


Acties:
  • 0 Henk 'm!

  • vdvleon
  • Registratie: Januari 2008
  • Laatst online: 08-06-2023
Je hebt gelijk. Ik kan denk ik idd ook een mime parser gebruiken. Alleen zijn deze vaak voor emails geschreven en ondersteunen deze volgens mij geen multipart/form-data. En volgens mij sturen de meeste browsers bij hun body parts geen content-length mee.

Dit is bijv. een voorbeeld van een form via multipart/form-data encoded:

code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
-----------------------------14309761618935
Content-Disposition: form-data; name="t"

c
-----------------------------14309761618935
Content-Disposition: form-data; name="h"

y
-----------------------------14309761618935
Content-Disposition: form-data; name="g"

x
-----------------------------14309761618935
Content-Disposition: form-data; name="q"; filename="tekst.txt"
Content-Type: text/plain

bdsgsgdsg
-----------------------------14309761618935--

Acties:
  • 0 Henk 'm!

  • Janoz
  • Registratie: Oktober 2000
  • Nu online

Janoz

Moderator Devschuur®

!litemod

vdvleon schreef op woensdag 02 december 2009 @ 09:45:
Je hebt gelijk. Ik kan denk ik idd ook een mime parser gebruiken. Alleen zijn deze vaak voor emails geschreven en ondersteunen deze volgens mij geen multipart/form-data. En volgens mij sturen de meeste browsers bij hun body parts geen content-length mee.
De opmaak van multipart/form-data is exact gelijk aan de opmaak van een multipart email bericht. Daarnaast is het meegeven van een contentlength inderdaad niet verplicht.


Persoonlijk vind ik dat je nogal van een mug een olifant aan het maken bent. Ik zie niet echt veel efficiëntie bottlenecks. Wanneer je eigen afhandeling wel traag is vermoed ik eerder dat je een ietwat brakke implementatie hebt gemaakt. Het inlezen en zoeken naar boundaries is misschien wel een beetje tricky, maar zeker nietondoenelijk en al helemaal niet overmatig inefficiënt.

bedenk trouwens dat het best vaak voor kan komen dat je helemaal niet weet hoe groot de content is die je gaat versturen. Een content-length verplicht stellen zorgt er dan alleen maar voor dat de server het compleet moet bufferen, de lengte moet bepalen en daarna pas kan versturen.

Ken Thompson's famous line from V6 UNIX is equaly applicable to this post:
'You are not expected to understand this'


Acties:
  • 0 Henk 'm!

  • Soultaker
  • Registratie: September 2000
  • Laatst online: 11:52
Ik denk dat vdleon uit is op een parser die een kleine/vaste hoeveelheid tijd en geheugen nodig heeft om die bestanden ergens weg te schrijven (naar temp files ofzo). Een webserver is bijna per definitie voor buitenstaanders bereikbaar, en dan wil je niet dat er simpel een DoS attack opgezet kan worden door een belachelijk grote post-body te sturen.

Het parsen van MIME headers kan vrij makkelijk als je de MIME headers limiteert in grootte (wat voor HTTP headers ook gebruikelijk is). Base64 decoding kan efficiënt zonder veel geheugen te gebruiken (voor elke vier bytes die je leest, schrijf je er drie weg). Dan is dus alleen de boundary detection nog van belang. Ik zou er persoonlijk niet voor kiezen om dan maar alles in het geheugen te lezen, maar even de moeite doen om die boundaries gewoon online te detecteren.

Acties:
  • 0 Henk 'm!

  • vdvleon
  • Registratie: Januari 2008
  • Laatst online: 08-06-2023
@ Soultaker

Je slaat de spijker precies raak :P

Maar zoals ik al eerder zei, ik ga dit in het weekend eens goed schrijven (de code). Ik laat dan wel weten hoe en wat.

Acties:
  • 0 Henk 'm!

  • vdvleon
  • Registratie: Januari 2008
  • Laatst online: 08-06-2023
Ik heb het opgelost. Ik heb mijn hele project op nieuw geschreven, maar dan met een andere library, namelijk Poco (http://pocoproject.org/). En hier zag ik dat deze support hebben voor HTTPServer, en Mime :P Bespaard mij weer veel tijd. Ook met deze mime parser is het afhandelen van multipart een zwaar taakje voor de cpu, dus ik deed niet zo heel veel fout :P (wel is de poco mime parser minder zwaar als die van mij hoor :P)

Acties:
  • 0 Henk 'm!

  • GlowMouse
  • Registratie: November 2002
  • Niet online
Janoz schreef op woensdag 02 december 2009 @ 10:31:
[...]

De opmaak van multipart/form-data is exact gelijk aan de opmaak van een multipart email bericht.
Dat is wel leuk, want dat betekent dat je per onderdeel van zo'n multipart/form-data ook weer een sub-multipart/form-data kunt krijgen (met eigen boundary) 8)

Acties:
  • 0 Henk 'm!

  • vdvleon
  • Registratie: Januari 2008
  • Laatst online: 08-06-2023
Dat zou in princiepe kunnen ja :P maar daar ga ik geen rekening mee houden! :P
Voor een http server is dat beetje irrelevant ;)

Zoals ik al eerder zij, multipart, leuk systeem. Maar dat het met een boundary werkt is erg irritant.
Als web browser is het namelijk helemaal niet nodig. Als je een form verstuurd weet je altijd van te voren
hoe groot alle velden zijn, en als het een bestand is, tja, een bestand grote opvragen is ook zo moeilijk niet :P Content-Length zou dan erg fijn zijn :P

Acties:
  • 0 Henk 'm!

  • Soultaker
  • Registratie: September 2000
  • Laatst online: 11:52
GlowMouse schreef op maandag 07 december 2009 @ 00:06:
Dat is wel leuk, want dat betekent dat je per onderdeel van zo'n multipart/form-data ook weer een sub-multipart/form-data kunt krijgen (met eigen boundary) 8)
Is heel leuk voor je, maar die multipart-file wordt dan gewoon als één bestand op disk opgeslagen natuurlijk, en pas geparset als de applicatie besluit er wat mee te doen. ;) Het is niet alsof je form fields kunt nesten op die manier. (Meestal verwacht een applicatie maar een beperkt aantal bestandstypes - plaatjes bijvoorbeeld - of worden files verbatim opgeslagen zonder de inhoud te bekijken.)
Zoals ik al eerder zij, multipart, leuk systeem. Maar dat het met een boundary werkt is erg irritant.
Als web browser is het namelijk helemaal niet nodig. Als je een form verstuurd weet je altijd van te voren hoe groot alle velden zijn, en als het een bestand is, tja, een bestand grote opvragen is ook zo moeilijk niet :P Content-Length zou dan erg fijn zijn :P
Wordt weer lastiger als je verschillende encodings kunt gebruiken. Maar voor HTTP POST is dat eigenlijk niet echt nodig omdat je altijd wel binary data kunt posten. Het had simpeler gekund, maar ja, een standaard is een standaard. :)

Acties:
  • 0 Henk 'm!

  • Janoz
  • Registratie: Oktober 2000
  • Nu online

Janoz

Moderator Devschuur®

!litemod

vdvleon schreef op maandag 07 december 2009 @ 02:32:
Zoals ik al eerder zij, multipart, leuk systeem. Maar dat het met een boundary werkt is erg irritant.
Waarom weer wat nieuws verzinnen terwijl de multipart specs al duidelijk zijn?
Als web browser is het namelijk helemaal niet nodig. Als je een form verstuurd weet je altijd van te voren
hoe groot alle velden zijn, en als het een bestand is, tja, een bestand grote opvragen is ook zo moeilijk niet :P
Aannames. Ten eerste hoeft het helemaal niet een browser te zijn die een post request verstuurd. Ik kan mij best voorstellen dat er situaties zijn waarbij je spullen opstuurt waarvan je van te voren niet weet hoe groot dit is.

Dat aannemende is het niet verplicht stellen van de contentlength juist de meest voor de hand liggende keuze. Het ontvangen en opknippen van een stream met onbekende lengte is goed te doen, maar het vooraf kunnen bepalen van de lengte van een stream is soms gewoon onmogelijk.
Content-Length zou dan erg fijn zijn :P
Daarom zit ie er ook in ;).

Ken Thompson's famous line from V6 UNIX is equaly applicable to this post:
'You are not expected to understand this'


Acties:
  • 0 Henk 'm!

  • Soultaker
  • Registratie: September 2000
  • Laatst online: 11:52
Niet verplicht dus, en in de praktijk ook niet, dus als je die POST data parser aan 't bouwen bent heb je er niets aan, want dan zal je toch het scenario moeten support dat je zelf boundaries moet parsen.

Acties:
  • 0 Henk 'm!

  • Janoz
  • Registratie: Oktober 2000
  • Nu online

Janoz

Moderator Devschuur®

!litemod

Je sais. Scenario supporten of input niet accepteren (en dan gewoon niet de volledige specs ondersteunen)

Ken Thompson's famous line from V6 UNIX is equaly applicable to this post:
'You are not expected to understand this'


  • vdvleon
  • Registratie: Januari 2008
  • Laatst online: 08-06-2023
Janoz schreef op maandag 07 december 2009 @ 13:29:
[...]

Waarom weer wat nieuws verzinnen terwijl de multipart specs al duidelijk zijn?
Hoezo verzin ik weer wat nieuws? Ik zeg alleen dat ik multipart opzich handig vind, maar het boundary systeem niet ideaal vind. Wat is daar iets nieuws aan verzinnen?

  • Janoz
  • Registratie: Oktober 2000
  • Nu online

Janoz

Moderator Devschuur®

!litemod

De boundary is een essentieel onderdeel van multipart. Als jij iets anders wilt als alternatief voor het boundary systeem dan moet er dus wat nieuws verzonnen worden.

Ken Thompson's famous line from V6 UNIX is equaly applicable to this post:
'You are not expected to understand this'


Acties:
  • 0 Henk 'm!

  • vdvleon
  • Registratie: Januari 2008
  • Laatst online: 08-06-2023
O ok. Op die fiets. Dan begrijp ik je. Dan heb je idd wel gelijk.

Ik snap dat multipart op zich een boundary princiepe nodig heeft alleen had dat niet nodig hoeven zijn in het http protocol (vind ik). Omdat je eigenlijk altijd wel weet hoe groot alle bestanden en velden zijn had je ook gewoon verplicht overal een content-length mee kunnen sturen. Als 'eind boundary' stuur je dan in het laatste blok gewoon een lege header (\r\n\r\n) zodat je weet dat alles verstuurd is. Zo zou ik dat denk ik gedaan hebben.

Acties:
  • 0 Henk 'm!

  • Janoz
  • Registratie: Oktober 2000
  • Nu online

Janoz

Moderator Devschuur®

!litemod

Omdat je eigenlijk altijd wel weet hoe groot alle bestanden en velden zijn
Is dat zo? Zoals ik Janoz in "HTTP POST multipart/form-data afhandelen" al aangeef kan ik me best voorstellen dat er best situaties zijn waardoor je juist behoorlijk in de knoop gaat komen.

Het optioneel maken van de content-length header is misschien lastig. Het verplicht stellen maakt sommige toepassing onmogelijk. Vandaar de keuze voor de eerste optie.

Ken Thompson's famous line from V6 UNIX is equaly applicable to this post:
'You are not expected to understand this'


Acties:
  • 0 Henk 'm!

  • Soultaker
  • Registratie: September 2000
  • Laatst online: 11:52
In HTTP 1.1 is dat opgelost door ófwel een Content-Length verplicht te stellen, ófwel chunked transfer encoding te gebruiken. Die laatste is nuttig als je dynamisch veel content wil versturen en niet eerst de hele body wil hoeven bufferen om de lengte te bepalen. Daarmee kun je aparte requests van elkaar onderscheiden en kun je HTTP pipelining ondersteunen.

In principe hadden ze iets soortgelijks kunnen definiëren voor multi-part MIME data, maar gezien de oorsprong van MIME in het gebruik van e-mail data, wilden ze waarschijnlijk een codering kiezen die overeind blijft wanneer mail servers tussendoor karakters hercoderen, newlines anders coderen of regels rewrappen. De boundary (mits slim gekozen) is daar redelijk immuun voor, terwijl de HTTP Content-Length en chunked transfer encoding allebei vereisen dat de body data ongewijzigd blijft.

offtopic:
Ik weet ook niet wat je officieel hoort te doen met een MIME part die wél een content-length én een correcte boundary heeft, maar waarbij de grootte niet overeenkomt met de gespecificeerde content-length. Negeer je die header dan, of weiger je te parsen? In zekere zin wordt het gebruik van twee verschillende methoden om boundaries aan te geven de boel alleen maar ingewikkelder. (De auteurs van de HTTP spec waren ook verstandig genoeg om te stellen dat als je chunked transfer encoding gebruikt, dat de Content-Length header dan verboden is!)


Ik denk dat die overweging het verschil verklaart. Ik ben het met vdvleon eens dat je bij het gebruik i.c.m. HTTP die flexibiliteit niet nodig hebt, maar ja, Janoz heeft gewoon gelijk dat als je HTTP multipart/form-data wil accepteren je gewoon de standaard moet supporten, ook al is die vervelend om te implementeren. :+

[ Voor 19% gewijzigd door Soultaker op 11-12-2009 17:15 ]

Pagina: 1