Envoi de fichier par une requête post en multipart/form-data ("simlulation" d'un formulaire web contenant un fichier à envoy

Contenu du snippet

Bonjour,

J'ai longuement cherché la facon de faire pour envoyer une requête POST qui inclut le contenu d'un fichier (content-type: multipart/form-data), mais sans succès, et c'est pourquoi j'ai donc du le créer moi même :-)

Le but est donc de simuler un formulaire d'une page web de ce type:

<form method="POST" action="http://site.com" enctype="multipart/form-data">
<input type="hidden" name="MAX_FILE_SIZE" value="Taille_Octets">
<input type="file" name="fichier" size="30"><br>
<input type="submit" name="telechargement" value="telecharger">
</form>

Comment ca marche?
J'utilise un HttpWebRequest pour créé la requête, puis j'y ajoute le contenu (que je forme avec une class que j'ai créé et que je vais partager ici) grâce à la méthode GetRequestStream du HttpWebRequest.

Pour former le contenu, il faut savoir quel champ du formulaire doit être envoyé. Si vous voulez "simuler" une page web existante, je vous conseil de "sniffer" votre requête envoyé par la page web en question (avec Ethereal par exemple, un programme simple d'emplois et gratuit: http://www.ethereal.com/)

Commencez la capture dans Ethereal puis envoyer votre fichier avec la page web. Une fois que c'est envoyé, arretez la capture et trouvez le flux TCP qui correspond à la communication qui vous intéresse (bouton droit et "follow TCP stream")

Ca donne un truc de ce genre:

POST / HTTP/1.1
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; fr; rv:1.8.0.7) Gecko/20060909 Firefox/1.5.0.7
Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5
Accept-Language: fr,fr-fr;q=0.8,en-us;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Keep-Alive: 300
Referer: http://imageshack.us/
Content-Type: multipart/form-data; boundary=---------------------------41184676334
Host: imageshack.us
Content-Length: 1517
Connection: Keep-Alive

-----------------------------41184676334
Content-Disposition: form-data; name="uploadtype"

on
-----------------------------41184676334
Content-Disposition: form-data; name="fileupload"; filename="Sans titre.JPG"
Content-Type: image/jpeg

[..... Contenu du fichier .....]
-----------------------------41184676334
Content-Disposition: form-data; name="MAX_FILE_SIZE"

3145728
-----------------------------41184676334
Content-Disposition: form-data; name="refer"

http://site.com/
-----------------------------41184676334
Content-Disposition: form-data; name="brand"

-----------------------------41184676334
Content-Disposition: form-data; name="optsize"

320x320
-----------------------------41184676334--

Ici par exemple, si il y a les champs suivants (leur nom) avec leur valeurs respectives:
Champ "uploadtype", valeur "on"
Champ "fileupload", ici il s'agit d'un champ FILE, il y a donc le paramètre "filename", le paramètre "Content-Type: image/jpeg", et la valeur = [Contenu du fichier]]
Champ "MAX_FILE_SIZE", valeur "3145728"
Champ "refer", valeur "http://site.com/"
Champ "brand", valeur ""
Champ "optsize", valeur "320x320"

On ne sait pas de quel type sont le champ (TEXT, RADIO, CHECKBOX..) mais ce n'est pas important, sauf pour le champ FILE.

Source / Exemple :


' La requête se fait avec un HttpWebRequest, et on créé les données par la class que j'ai créé qui s'appel MultipartFormDataBodyCreator.
'
' Propriété et procédures publiques de MultipartFormDataBodyCreator:
'
' Public Sub Add(Name as String, _                         ' Le nom du champs à ajouter
'                Value as String, _                        ' La valeur 
'                Optional IsFileField as Bollean = False") ' Si il s'agit d'un fichier, mettre IsFileField = true
'                                                          ' et Value = le chemin du fichier
'
' Public Property Boundary as String  ' Le Boundary à utiliser (sert à délimiter les champs dans la requête) 
'
' Public Function Create() as Byte()  ' Retourne les données à envoyer sous forme d'un tableau de byte
'
'
' Pour envoyer la requête présenté juste au dessus, il faut donc faire comme ceci:

Private Sub SendFile()

    Try

        Dim bodyCreator As New MultipartFormDataBodyCreater ' La classe que j'ai créé et que je présente plus loins
        Dim File as String = "C:\Image.jpg" ' Le fichier à envoyer

        ' Création des données à envoyer
        ' --------------------
        bodyCreator.Fields.Add("uploadtype", "on")
        bodyCreator.Fields.Add("fileupload", File, True)   ' C'est le fichier
        bodyCreator.Fields.Add("MAX_FILE_SIZE", "3145728")
        bodyCreator.Fields.Add("refer", "http://site.com/")
        bodyCreator.Fields.Add("brand", "")
        bodyCreator.Fields.Add("optsize", "320x320")
    
        Dim body() as Byte = bodyCreator.Create ' Les données à envoyer sous forme de tableau de Byte
        Dim boundary as String = bodyCreater.Boundary ' Le boudary délimitant les champs. Utile pour créer la requête
 
        ' Création de la requête
        ' ------------------------
        Dim Request as HttpWebRequest = HttpWebRequest.Create("http://site.com")
        Request.Method = "POST"
        Request.UserAgent = "Mozilla/5.0 (Windows; U; Windows NT 5.1; fr; rv:1.8.0.7) Gecko/20060909 Firefox/1.5.0.7"
        Request.KeepAlive = True
        Request.ServicePoint.Expect100Continue = False   ' Important si le serveur ne gère pas les Expect
        Request.Referer = "http://site.com/"
        Request.ContentType = "multipart/form-data; boundary=" & boundary ' Ajouter le boundary après le Content-Type
        Request.ContentLength = body.Length
 
        ' Ecriture des données
        ' ------------------------------------
        Dim writer As Stream = Request.GetRequestStream
        writer.Write(body, 0, body.Length)
        writer.Close()

        ' Réponse
        ' -----------------
        Dim response = Request.GetResponse()
        Dim reader As New IO.StreamReader(response.GetResponseStream())
        Dim ResponseBody = reader.ReadToEnd
        reader.Close()
        Response.Close()

        Console.WriteLine(ResponseBody) ' Le réponse du serveur

    Catch ex As Exception
        Console.WriteLine(ex.ToString)
    End Try
End Sub

Public Class MultipartFormDataBodyCreator

    ' Le Boundary délimitant chaque élément (Field) dans le corps du message
    ' On y met une valeure constante, mais normalement il faudrait s'assurer que cette
    ' chaine de caractère n'existe pas ailleur dans les données à envoyer (si c'est le cas, l'envois va planter)
    Public Property Boundary() As String
        Get
            Return _Boundary
        End Get
        Set(ByVal value As String)
            _Boundary = value
        End Set
    End Property
    Private _Boundary As String = "-----------------768299458641184676334"

    ' Les éléments à inclure dans le corps du message, tous défini par une paire Nom/Valeur
    ' Une propriété IsFileField as Bollean définie si c'est le champs du fichier à envoyer 
    ' (dans quel cas Value contiendra le chemin du fichier à envoyer)
    Public Property Fields() As FieldList
        Get
            Return _Fields
        End Get
        Set(ByVal value As FieldList)
            _Fields = value
        End Set
    End Property
    Private _Fields As New FieldList

    ' Création d'un tableau de bytes contenant toutes les données à envoyer durant le request
    Public Function Create() As Byte()

        Dim encoding As New System.Text.ASCIIEncoding() ' Pour convertir le text en bytes
        Dim body(-1) As Byte ' Le contenu final

        ' Parcourir tous les champs pour créer le body
        For Each Field As Field In _Fields
            Dim sb As New System.Text.StringBuilder ' Pour créer les champs texte
            Dim FieldAsBytes() As Byte ' Le champs à ajouter au body en bytes

            ' Ajouter ce qui est commun aux champs FILE et les autres, c'est à dire une 
            ' ligne avec le bundary puis la disposition et le nom du champ sur une autre ligne
            sb.AppendLine("--" & Boundary)  ' Le bundary précédé de "--"
            sb.Append("Content-Disposition: form-data; name=""" & Field.Name & """") ' La disposition et le nom du champs (sans finir la ligne!)

            ' La suite dépend si c'est un fichier ou non
            If Field.IsFileField Then ' C'est un fichier

                ' Ajouter le nom du fichier sur la meme ligne que le nom du champ
                ' Sur les lignes suivantes, le type de contenu et 1 ligne vide (puis viendra le fichier)
                sb.AppendLine("; filename=""" & System.IO.Path.GetFileName(Field.Value) & """")
                sb.AppendLine("Content-Type: image/jpeg" & vbCrLf)
                FieldAsBytes = encoding.GetBytes(sb.ToString) ' Le 1er bout du champ codé en bytes

                ' On ajoute le contenu du fichier fichier au tableau FieldAsBytes
                Dim FileBytes() As Byte = IO.File.ReadAllBytes(Field.Value) ' Le contenu du fichier
                Array.Resize(FieldAsBytes, FieldAsBytes.Length + FileBytes.Length) ' Agrandir FieldAsBytes
                Array.Copy(FileBytes, 0, FieldAsBytes, sb.Length, FileBytes.Length) ' Ajouter le contenu du fichier

                ' Ajouter un vbcrlf pour finir le champ
                Array.Resize(FieldAsBytes, FieldAsBytes.Length + 2)
                Array.Copy(encoding.GetBytes(vbCrLf), 0, FieldAsBytes, FieldAsBytes.Length - 2, 2)
            Else

                ' C'est un élément du formulaire autre qu'un fichier
                sb.AppendLine("") ' Finir la ligne précédente
                sb.AppendLine(vbCrLf & Field.Value) ' Une ligne vide et la valeur
                FieldAsBytes = encoding.GetBytes(sb.ToString) ' Créer un tableau de Byte

            End If

            ' Ajouter la nouvelle entrée dans le body
            Dim CurrentBodySize As Integer = body.Length
            Array.Resize(body, body.Length + FieldAsBytes.Length)   ' Agrandir le body
            Array.Copy(FieldAsBytes, 0, body, body.Length - FieldAsBytes.Length, FieldAsBytes.Length) ' Copier FieldAsBytes à la fin du body
        Next
        ' A la fin on ajoute le bundary et "--"
        Dim EndLine() As Byte = encoding.GetBytes("--" & Boundary & "--" & vbCrLf)
        Array.Resize(body, body.Length + EndLine.Length)
        Array.Copy(EndLine, 0, body, body.Length - EndLine.Length, EndLine.Length)

        Return body
    End Function

End Class
Public Class FieldList
    Inherits Generic.List(Of Field)

    ' Cette class éritée d'une collection générique List(Of Field) est créé juste pour ajouter les 2 procédures suivantes:
    Public Overloads Sub Add(ByVal Name As String, ByVal Value As String, Optional ByVal IsFileField As Boolean = False)
        MyBase.Add(New Field(Name, Value, IsFileField))
    End Sub
    Public Overloads Sub Insert(ByVal Index As Integer, ByVal Name As String, ByVal Value As String, Optional ByVal IsFileField As Boolean = False)
        MyBase.Insert(Index, New Field(Name, Value, IsFileField))
    End Sub
End Class
Public Class Field
    ' Représente un champs de formulaire
    Public IsFileField As Boolean = False
    Public Name As String = ""
    Public Value As String = ""
    Public Sub New()

    End Sub
    Public Sub New(ByVal Name As String, ByVal Value As String, Optional ByVal IsFileField As Boolean = False)
        Me.Name = Name
        Me.Value = Value
        Me.IsFileField = IsFileField
    End Sub
End Class

Conclusion :


Cette classe à été créé selon mes besoins, et elle n'est pas capable de créer n'importe quelle requête. Notament le contenu du fichier (Content-Type:) est dans mon code fixé à "image/jpeg", donc je ne sais pas ce que ca donne si on envois autre chose.

En tout cas, dans mon cas ca fonctionne. Si vous avez d'autre type de champs à envoyer qui ne sont pas géré dans cette classe, ca vous permet tout de même d'avoir une base qui fonctionne.

Attention, le boundary est fixé à "-----------------768299458641184676334" dans mon code. Normalement il faut s'assurer que la chaine de caratères formant le boundary ne soit présente nul part ailleur dans les données à envoyer, car si c'est le cas, la requête échouera.

Le contenu du fichier est entièrement lu pour créer les données à envoyer, puis envoyé en 1 seul bloc. Cette méthode ne convient donc pas pour l'envois de gros fichiers, dans quel cas il faudrait découper le fichier en petites partie et les envoyer les une après les autres.

Voilà, c'est tout. J'espère que ce code sera utile à certains, car j'ai vu bcp de gens chercher à faire ca, mais très souvent sans avoir eu la réponse :-)

PS. Je ne donne pas de zip, car mon projet est nettement plus gros et j'utilise cette classe en multi-threading (dans un BackGroundWorker). J'ai juste retapé la procédure SendFile pour vous faire part de l'usage.

A voir également

Vous n'êtes pas encore membre ?

inscrivez-vous, c'est gratuit et ça prend moins d'une minute !

Les membres obtiennent plus de réponses que les utilisateurs anonymes.

Le fait d'être membre vous permet d'avoir un suivi détaillé de vos demandes et codes sources.

Le fait d'être membre vous permet d'avoir des options supplémentaires.