views:

8502

answers:

11

I am trying to fill a form in a php application from a C# client (Outlook addin). I used Fiddler to see the original request from within the php application and the form is transmitted as a multipart/form. Unfortunately .Net does not come with native support for this type of forms (WebClient has only a method for uploading a file). Does anybody know a library or has some code to achieve this? I want to post different values and additionally (but only sometimes) a file.

Thanks for your help, Sebastian

+7  A: 

This is cut and pasted from some sample code I wrote, hopefully it should give the basics. It only supports File data and form-data at the moment.

public class PostData
{

    private List<PostDataParam> m_Params;

    public List<PostDataParam> Params
    {
     get { return m_Params; }
     set { m_Params = value; }
    }

    public PostData()
    {
     m_Params = new List<PostDataParam>();

     // Add sample param
     m_Params.Add(new PostDataParam("email", "MyEmail", PostDataParamType.Field));
    }


    /// <summary>
    /// Returns the parameters array formatted for multi-part/form data
    /// </summary>
    /// <returns></returns>
    public string GetPostData()
    {
     // Get boundary, default is --AaB03x
     string boundary = ConfigurationManager.AppSettings["ContentBoundary"].ToString();

     StringBuilder sb = new StringBuilder();
     foreach (PostDataParam p in m_Params)
     {
      sb.AppendLine(boundary);

      if (p.Type == PostDataParamType.File)
      {
       sb.AppendLine(string.Format("Content-Disposition: file; name=\"{0}\"; filename=\"{1}\"", p.Name, p.FileName));
       sb.AppendLine("Content-Type: text/plain");
       sb.AppendLine();
       sb.AppendLine(p.Value);     
      }
      else
      {
       sb.AppendLine(string.Format("Content-Disposition: form-data; name=\"{0}\"", p.Name));
       sb.AppendLine();
       sb.AppendLine(p.Value);
      }
     }

     sb.AppendLine(boundary);

     return sb.ToString();   
    }
}

public enum PostDataParamType
{
    Field,
    File
}

public class PostDataParam
{


    public PostDataParam(string name, string value, PostDataParamType type)
    {
     Name = name;
     Value = value;
     Type = type;
    }

    public string Name;
    public string FileName;
    public string Value;
    public PostDataParamType Type;
}

To send the data you then need to:

HttpWebRequest oRequest = null;
oRequest = (HttpWebRequest)HttpWebRequest.Create(oURL.URL);
oRequest.ContentType = "multipart/form-data";        
oRequest.Method = "POST";
PostData pData = new PostData();

byte[] buffer = encoding.GetBytes(pData.GetPostData());

// Set content length of our data
oRequest.ContentLength = buffer.Length;

// Dump our buffered postdata to the stream, booyah
oStream = oRequest.GetRequestStream();
oStream.Write(buffer, 0, buffer.Length);
oStream.Close();

// get the response
oResponse = (HttpWebResponse)oRequest.GetResponse();

Hope thats clear, i've cut and pasted from a few sources to get that tidier.

dnolan
A: 

Trebz,

thank you for your answer. I'll give it a try but it looks promising. Unfortunately I don't have the reputation yet to vote your answer up :-(

Bye, Sebastian

bash74
+6  A: 

Building on dnolans example, this is the version I could actually get to work (there were some errors with the boundary, encoding wasn't set) :-)

To send the data:

HttpWebRequest oRequest = null;
oRequest = (HttpWebRequest)HttpWebRequest.Create("http://you.url.here");
oRequest.ContentType = "multipart/form-data; boundary=" + PostData.boundary;
oRequest.Method = "POST";
PostData pData = new PostData();
Encoding encoding = Encoding.UTF8;
Stream oStream = null;

/* ... set the parameters, read files, etc. IE:
   pData.Params.Add(new PostDataParam("email", "[email protected]", PostDataParamType.Field));
   pData.Params.Add(new PostDataParam("fileupload", "filename.txt", "filecontents" PostDataParamType.File));
*/

byte[] buffer = encoding.GetBytes(pData.GetPostData());

oRequest.ContentLength = buffer.Length;

oStream = oRequest.GetRequestStream();
oStream.Write(buffer, 0, buffer.Length);
oStream.Close();

HttpWebResponse oResponse = (HttpWebResponse)oRequest.GetResponse();

The PostData class should look like:

public class PostData
{
    // Change this if you need to, not necessary
    public static string boundary = "AaB03x";

    private List<PostDataParam> m_Params;

    public List<PostDataParam> Params
    {
        get { return m_Params; }
        set { m_Params = value; }
    }

    public PostData()
    {
        m_Params = new List<PostDataParam>();
    }

    /// <summary>
    /// Returns the parameters array formatted for multi-part/form data
    /// </summary>
    /// <returns></returns>
    public string GetPostData()
    {
        StringBuilder sb = new StringBuilder();
        foreach (PostDataParam p in m_Params)
        {
            sb.AppendLine("--" + boundary);

            if (p.Type == PostDataParamType.File)
            {
                sb.AppendLine(string.Format("Content-Disposition: file; name=\"{0}\"; filename=\"{1}\"", p.Name, p.FileName));
                sb.AppendLine("Content-Type: application/octet-stream");
                sb.AppendLine();
                sb.AppendLine(p.Value);
            }
            else
            {
                sb.AppendLine(string.Format("Content-Disposition: form-data; name=\"{0}\"", p.Name));
                sb.AppendLine();
                sb.AppendLine(p.Value);
            }
        }

        sb.AppendLine("--" + boundary + "--");

        return sb.ToString();
    }
}

public enum PostDataParamType
{
    Field,
    File
}

public class PostDataParam
{
    public PostDataParam(string name, string value, PostDataParamType type)
    {
        Name = name;
        Value = value;
        Type = type;
    }

    public PostDataParam(string name, string filename, string value, PostDataParamType type)
    {
        Name = name;
        Value = value;
        FileName = filename;
        Type = type;
    }

    public string Name;
    public string FileName;
    public string Value;
    public PostDataParamType Type;
}
jmoeller
A: 

I needed to simulate a browser login to a website to get a login cookie, and the login form was multipart/form-data.

I took some clues from the other answers here, and then tried to get my own scenario working. It took a bit of frustrating trial and error before it worked right, but here is the code:

    public static class WebHelpers
    {
        /// <summary>
        /// Post the data as a multipart form
        /// </summary>
       public static HttpWebResponse MultipartFormDataPost(string postUrl, string userAgent, Dictionary<string, string> values)
       {
           string formDataBoundary = "---------------------------" + WebHelpers.RandomHexDigits(12);
           string contentType = "multipart/form-data; boundary=" + formDataBoundary;

           string formData = WebHelpers.MakeMultipartForm(values, formDataBoundary);
           return WebHelpers.PostForm(postUrl, userAgent, contentType, formData);
       }

        /// <summary>
        /// Post a form
        /// </summary>
        public static HttpWebResponse PostForm(string postUrl, string userAgent, string contentType, string formData)
        {
            HttpWebRequest request = WebRequest.Create(postUrl) as HttpWebRequest;

            if (request == null)
            {
                throw new NullReferenceException("request is not a http request");
            }

            // Add these, as we're doing a POST
            request.Method = "POST";
            request.ContentType = contentType;
            request.UserAgent = userAgent;
            request.CookieContainer = new CookieContainer();

            // We need to count how many bytes we're sending. 
            byte[] postBytes = Encoding.UTF8.GetBytes(formData);
            request.ContentLength = postBytes.Length;

            using (Stream requestStream = request.GetRequestStream())
            {
                // Push it out there
                requestStream.Write(postBytes, 0, postBytes.Length);
                requestStream.Close();
            }

            return request.GetResponse() as HttpWebResponse;
        }

        /// <summary>
        /// Generate random hex digits 
        /// </summary>
        public static string RandomHexDigits(int count)
        {
            Random random = new Random();
            StringBuilder result = new StringBuilder();
            for (int i = 0; i < count; i++)
            {
                int digit = random.Next(16);
                result.AppendFormat("{0:x}", digit);
            }

            return result.ToString();
        }

        /// <summary>
        /// Turn the key and value pairs into a multipart form
        /// </summary>
        private static string MakeMultipartForm(Dictionary<string, string> values, string boundary)
        {
            StringBuilder sb = new StringBuilder();

            foreach (var pair in values)
            {
                sb.AppendFormat("--{0}\r\nContent-Disposition: form-data; name=\"{1}\"\r\n\r\n{2}\r\n", boundary, pair.Key, pair.Value);
            }

            sb.AppendFormat("--{0}--\r\n", boundary);

            return sb.ToString();    
        }
    }
}

It doesn't handle file data, just form since that's all that I needed. I called like this:

    try
    {
        using (HttpWebResponse response = WebHelpers.MultipartFormDataPost(postUrl, UserAgentString, this.loginForm)) 
        {
            if (response != null)
            {
                Cookie loginCookie = response.Cookies["logincookie"];
                .....
Anthony
+9  A: 

Thanks for the answers, everybody! I recently had to get this to work, and used your suggestions heavily. However, there were a couple of tricky parts that did not work as expected, mostly having to do with actually including the file (which was an important part of the question). There are a lot of answers here already, but I think this may be useful to someone in the future (I could not find many clear examples of this online). I wrote a blog post that explains it a little more.

Basically, I first tried to pass in the file data as a UTF8 encoded string, but I was having problems with encoding files (it worked fine for a plain text file, but when uploading a Word Document, for example, if I tried to save the file that was passed through to the posted form using Request.Files[0].SaveAs(), opening the file in Word did not work properly. I found that if you write the file data directly using a Stream (rather than a StringBuilder), it worked as expected. Also, I made a couple of modifications that made it easier for me to understand.

By the way, the Multipart Forms Request for Comments and the W3C Recommendation for mulitpart/form-data are a couple of useful resources in case anyone needs a reference for the specification.

I changed the WebHelpers class to be a bit smaller and have simpler interfaces. If you pass a Dictionary element with type byte[], it will assume that it is a file, and if you pass a string, it will treat it as a standard name/value combination.

Here is the WebHelpers class:

public static class WebHelpers
{
    public static Encoding encoding = Encoding.UTF8;

    /// <summary>
    /// Post the data as a multipart form
    /// postParameters with a value of type byte[] will be passed in the form as a file, and value of type string will be
    /// passed as a name/value pair.
    /// </summary>
    public static HttpWebResponse MultipartFormDataPost(string postUrl, string userAgent, Dictionary<string, object> postParameters)
    {
        string formDataBoundary = "-----------------------------28947758029299";
        string contentType = "multipart/form-data; boundary=" + formDataBoundary;

        byte[] formData = WebHelpers.GetMultipartFormData(postParameters, formDataBoundary);

        return WebHelpers.PostForm(postUrl, userAgent, contentType, formData);
    }

    /// <summary>
    /// Post a form
    /// </summary>
    private static HttpWebResponse PostForm(string postUrl, string userAgent, string contentType, byte[] formData)
    {
        HttpWebRequest request = WebRequest.Create(postUrl) as HttpWebRequest;

        if (request == null)
        {
            throw new NullReferenceException("request is not a http request");
        }

        // Add these, as we're doing a POST
        request.Method = "POST";
        request.ContentType = contentType;
        request.UserAgent = userAgent;
        request.CookieContainer = new CookieContainer();

        // We need to count how many bytes we're sending. 
        request.ContentLength = formData.Length;

        using (Stream requestStream = request.GetRequestStream())
        {
            // Push it out there
            requestStream.Write(formData, 0, formData.Length);
            requestStream.Close();
        }

        return request.GetResponse() as HttpWebResponse;
    }

    /// <summary>
    /// Turn the key and value pairs into a multipart form.
    /// See http://www.ietf.org/rfc/rfc2388.txt for issues about file uploads
    /// </summary>
    private static byte[] GetMultipartFormData(Dictionary<string, object> postParameters, string boundary)
    {
        Stream formDataStream = new System.IO.MemoryStream();

        foreach (var param in postParameters)
        {
            if (param.Value is byte[])
            {
                byte[] fileData = param.Value as byte[];

                // Add just the first part of this param, since we will write the file data directly to the Stream
                string header = string.Format("--{0}\r\nContent-Disposition: form-data; name=\"{1}\"; filename=\"{2}\";\r\nContent-Type: application/octet-stream\r\n\r\n", boundary, param.Key, param.Key);
                formDataStream.Write(encoding.GetBytes(header), 0, header.Length);

                // Write the file data directly to the Stream, rather than serializing it to a string.  This 
                formDataStream.Write(fileData, 0, fileData.Length);
            }
            else
            {
                string postData = string.Format("--{0}\r\nContent-Disposition: form-data; name=\"{1}\"\r\n\r\n{2}\r\n", boundary, param.Key, param.Value);
                formDataStream.Write(encoding.GetBytes(postData), 0, postData.Length);
            }
        }

        // Add the end of the request
        string footer = "\r\n--" + boundary + "--\r\n";
        formDataStream.Write(encoding.GetBytes(footer), 0, footer.Length);

        // Dump the Stream into a byte[]
        formDataStream.Position = 0;
        byte[] formData = new byte[formDataStream.Length];
        formDataStream.Read(formData, 0, formData.Length);
        formDataStream.Close();

        return formData;
    }
}

Here is the calling code, which uploads a file and a few normal post parameters:

        // Read file data
        FileStream fs = new FileStream("c:\\people.doc", FileMode.Open, FileAccess.Read);
        byte[] data = new byte[fs.Length];
        fs.Read(data, 0, data.Length);
        fs.Close();

        // Generate post objects
        Dictionary<string, object> postParameters = new Dictionary<string, object>();
        postParameters.Add("filename", "People.doc");
        postParameters.Add("fileformat", "doc");
        postParameters.Add("file", data);

        // Create request and receive response
        string postURL = "http://stackoverflow.com";
        string userAgent = "Someone";
        HttpWebResponse webResponse = WebHelpers.MultipartFormDataPost(postURL, userAgent, postParameters);

        // Process response
        StreamReader responseReader = new StreamReader(webResponse.GetResponseStream());
        string fullResponse = responseReader.ReadToEnd();
        webResponse.Close();
        Response.Write(fullResponse);
Brian Grinstead
dude, you're a champ!
andy
Amazing code. Thank you.
Ronnie Overby
Spectacular. I modified to show progress, support PUT requests and use my own cookiecontainer but this was a ton of help to get me on my way.
Jeff Hellman
A: 

I want to post data to S3 Amamzon, I've provide all the required parameters but server says Bad Request 400

A: 

Below is the code which I'm using //This URL not exist, it's only an example. string url = "http://myBox.s3.amazonaws.com/"; //Instantiate new CustomWebRequest class CustomWebRequest wr = new CustomWebRequest(url); //Set values for parameters wr.ParamsCollection.Add(new ParamsStruct("key", "${filename}")); wr.ParamsCollection.Add(new ParamsStruct("acl", "public-read")); wr.ParamsCollection.Add(new ParamsStruct("success_action_redirect", "http://www.yahoo.com")); wr.ParamsCollection.Add(new ParamsStruct("x-amz-meta-uuid", "14365123651274")); wr.ParamsCollection.Add(new ParamsStruct("x-amz-meta-tag", "")); wr.ParamsCollection.Add(new ParamsStruct("AWSAccessKeyId", "zzzz"));
wr.ParamsCollection.Add(new ParamsStruct("Policy", "adsfadsf")); wr.ParamsCollection.Add(new ParamsStruct("Signature", "hH6lK6cA=")); //For file type, send the inputstream of selected file StreamReader sr = new StreamReader(@"file.txt"); wr.ParamsCollection.Add(new ParamsStruct("file", sr, ParamsStruct.ParamType.File, "file.txt"));

wr.PostData();

from the following link I've downloaded the same code http://www.codeproject.com/KB/cs/multipart_request_C_.aspx

Any Help

+4  A: 

In the version of .NET I am using you also have to do this:

System.Net.ServicePointManager.Expect100Continue = false;

If you don't, the HttpWebRequest class will automatically add the Expect:100-continue request header which fouls everything up.

Also I learned the hard way that you have to have the right number of dashes. whatever you say is the "boundary" in the Content-Type header has to be preceded by two dashes

--THEBOUNDARY

and at the end

--THEBOUNDARY--

exactly as it does in the example code. If your boundary is a lot of dashes followed by a number then this mistake won't be obvious by looking at the http request in a proxy server

eeeeaaii
THIS IS IMPORTANT! Thank you so much for mentioning this, I never would have considered it otherwise and it was the only thing messing me up!
ovinophile
+2  A: 

This works like a charm www.briangrinstead.com/blog

Chathuranga Wijeratna
+1  A: 

A little optimization of the class before. In this version the files are not totally loaded into memory.

Security advice: a check for the boundary is missing, if the file contains the bounday it will crash.

namespace WindowsFormsApplication1
{
    public static class FormUpload
    {
        private static string NewDataBoundary()
        {
            Random rnd = new Random();
            string formDataBoundary = "";
            while (formDataBoundary.Length < 15)
            {
                formDataBoundary = formDataBoundary + rnd.Next();
            }
            formDataBoundary = formDataBoundary.Substring(0, 15);
            formDataBoundary = "-----------------------------" + formDataBoundary;
            return formDataBoundary;
        }

        public static HttpWebResponse MultipartFormDataPost(string postUrl, IEnumerable<Cookie> cookies, Dictionary<string, string> postParameters)
        {
            string boundary = NewDataBoundary();

            HttpWebRequest request = WebRequest.Create(postUrl) as HttpWebRequest;

            // Set up the request properties
            request.Method = "POST";
            request.ContentType = "multipart/form-data; boundary=" + boundary;
            request.UserAgent = "PhasDocAgent 1.0";
            request.CookieContainer = new CookieContainer();

            foreach (var cookie in cookies)
            {
                request.CookieContainer.Add(cookie);
            }

            #region WRITING STREAM
            using (Stream formDataStream = request.GetRequestStream())
            {
                foreach (var param in postParameters)
                {
                    if (param.Value.StartsWith("file://"))
                    {
                        string filepath = param.Value.Substring(7);

                        // Add just the first part of this param, since we will write the file data directly to the Stream
                        string header = string.Format("--{0}\r\nContent-Disposition: form-data; name=\"{1}\"; filename=\"{2}\";\r\nContent-Type: {3}\r\n\r\n",
                            boundary,
                            param.Key,
                            Path.GetFileName(filepath) ?? param.Key,
                            MimeTypes.GetMime(filepath));

                        formDataStream.Write(Encoding.UTF8.GetBytes(header), 0, header.Length);

                        // Write the file data directly to the Stream, rather than serializing it to a string.

                        byte[] buffer = new byte[2048];

                        FileStream fs = new FileStream(filepath, FileMode.Open);

                        for (int i = 0; i < fs.Length; )
                        {
                            int k = fs.Read(buffer, 0, buffer.Length);
                            if (k > 0)
                            {
                                formDataStream.Write(buffer, 0, k);
                            }
                            i = i + k;
                        }
                        fs.Close();
                    }
                    else
                    {
                        string postData = string.Format("--{0}\r\nContent-Disposition: form-data; name=\"{1}\"\r\n\r\n{2}\r\n",
                            boundary,
                            param.Key,
                            param.Value);
                        formDataStream.Write(Encoding.UTF8.GetBytes(postData), 0, postData.Length);
                    }
                }
                // Add the end of the request
                byte[] footer = Encoding.UTF8.GetBytes("\r\n--" + boundary + "--\r\n");
                formDataStream.Write(footer, 0, footer.Length);
                request.ContentLength = formDataStream.Length;
                formDataStream.Close();
            }
            #endregion

            return request.GetResponse() as HttpWebResponse;
        }
    }
}
TheQult
A: 

Thanks for the code, it saved me a lot of time (including the Except100 error!).

Anyway, I found a bug in the code, here:

formDataStream.Write(encoding.GetBytes(postData), 0, postData.Length);

In case your POST data is utf-16, postData.Length, will return the number of characters and not the number of bytes. This will truncate the data being posted (for example, if you have 2 chars that are encoded as utf-16, they take 4 bytes, but postData.Length will say it takes 2 bytes, and you loose the 2 final bytes of the posted data).

Solution - replace that line with:

byte[] aPostData=encoding.GetBytes(postData);
formDataStream.Write(aPostData, 0, aPostData.Length);

Using this, the length is calculated by the size of the byte[], not the string size.

Luis Domingues