[VB6] Using WinInet to post binary file

Here are a couple of VB6 functions that can be used to upload a zip file through an http post request.

Using ready-made Microsoft.XMLHTTP or WinHttp.WinHttpRequest.5.1

First the easy one, using XMLHTTP to do the actual work

Private Function pvPostFile(sUrl As String, sFileName As String, Optional ByVal bAsync As Boolean) As String
    Const STR_BOUNDARY  As String = "3fbd04f5-b1ed-4060-99b9-fca7ff59c113"
    Dim nFile           As Integer
    Dim baBuffer()      As Byte
    Dim sPostData       As String

    '--- read file
    nFile = FreeFile
    Open sFileName For Binary Access Read As nFile
    If LOF(nFile) > 0 Then
        ReDim baBuffer(0 To LOF(nFile) - 1) As Byte
        Get nFile, , baBuffer
        sPostData = StrConv(baBuffer, vbUnicode)
    End If
    Close nFile
    '--- prepare body
    sPostData = "--" & STR_BOUNDARY & vbCrLf & _
        "Content-Disposition: form-data; name=""uploadfile""; filename=""" & Mid$(sFileName, InStrRev(sFileName, "\") + 1) & """" & vbCrLf & _
        "Content-Type: application/octet-stream" & vbCrLf & vbCrLf & _
        sPostData & vbCrLf & _
        "--" & STR_BOUNDARY & "--"
    '--- post
    With CreateObject("Microsoft.XMLHTTP")
        .Open "POST", sUrl, bAsync
        .SetRequestHeader "Content-Type", "multipart/form-data; boundary=" & STR_BOUNDARY
        .Send pvToByteArray(sPostData)
        If Not bAsync Then
            pvPostFile = .ResponseText
        End If
    End With
End Function

Private Function pvToByteArray(sText As String) As Byte()
    pvToByteArray = StrConv(sText, vbFromUnicode)
End Function

The biggest benefit of using XMLHTTP is the async option. MSXML spawns a worker thread that sends the request even if the last reference to the object is set to nothing.

Instead of Microsoft.XMLHTTP one can use WinHttp.WinHttpRequest.5.1 but setting the last object reference to nothing cancels the async request. Edit: Not true! It turned out Microsoft.XMLHTTP seems to get corrupted more often in the wild due to bad installers. We are now using WinHttp.WinHttpRequest.5.1 exclusively for our uploads.

Another caveat is the pvToByteArray function. It turns out send method can not handle “byref” byte arrays, so for instance passing baBuffer will fail, as VB6 sets up VT_BYREF bit of the type of the variant parameter.

Using wininet.dll API

Here is the hard-core API version. Biggest drawback is that it’s syncronous by nature.

Private Const INTERNET_AUTODIAL_FORCE_ONLINE As Long = 1
Private Const INTERNET_OPEN_TYPE_PRECONFIG  As Long = 0
Private Const INTERNET_DEFAULT_HTTP_PORT    As Long = 80
Private Const INTERNET_SERVICE_HTTP         As Long = 3
Private Const INTERNET_FLAG_RELOAD          As Long = &H80000000
Private Const HTTP_ADDREQ_FLAG_REPLACE      As Long = &H80000000
Private Const HTTP_ADDREQ_FLAG_ADD          As Long = &H20000000

Private Declare Function InternetAutodial Lib "wininet.dll" (ByVal dwFlags As Long, ByVal dwReserved As Long) As Long
Private Declare Function InternetOpen Lib "wininet.dll" Alias "InternetOpenA" (ByVal sAgent As String, ByVal lAccessType As Long, ByVal sProxyName As String, ByVal sProxyBypass As String, ByVal lFlags As Long) As Long
Private Declare Function InternetConnect Lib "wininet.dll" Alias "InternetConnectA" (ByVal hInternetSession As Long, ByVal sServerName As String, ByVal nServerPort As Integer, ByVal sUsername As String, ByVal sPassword As String, ByVal lService As Long, ByVal lFlags As Long, ByVal lContext As Long) As Long
Private Declare Function HttpOpenRequest Lib "wininet.dll" Alias "HttpOpenRequestA" (ByVal hHttpSession As Long, ByVal sVerb As String, ByVal sObjectName As String, ByVal sVersion As String, ByVal sReferer As String, ByVal something As Long, ByVal lFlags As Long, ByVal lContext As Long) As Long
Private Declare Function HttpAddRequestHeaders Lib "wininet.dll" Alias "HttpAddRequestHeadersA" (ByVal hHttpRequest As Long, ByVal sHeaders As String, ByVal lHeadersLength As Long, ByVal lModifiers As Long) As Long
Private Declare Function HttpSendRequest Lib "wininet.dll" Alias "HttpSendRequestA" (ByVal hHttpRequest As Long, ByVal sHeaders As String, ByVal lHeadersLength As Long, ByVal sOptional As String, ByVal lOptionalLength As Long) As Long
Private Declare Function InternetCloseHandle Lib "wininet.dll" (ByVal hInet As Long) As Long

Private Function pvPostFile(sUrl As String, sFileName As String) As Boolean
    Const STR_APP_NAME  As String = "Uploader"
    Dim hOpen           As Long
    Dim hConnection     As Long
    Dim hRequest        As Long
    Dim sHeader         As String
    Dim sBoundary       As String
    Dim nFile           As Integer
    Dim baData()        As Byte
    Dim sPostData       As String
    Dim sHttpServer     As String
    Dim lHttpPort       As Long
    Dim sUploadPage     As String

    '--- read file
    nFile = FreeFile
    Open sFileName For Binary Access Read As nFile
    If LOF(nFile) > 0 Then
        ReDim baData(0 To LOF(nFile) - 1) As Byte
        Get nFile, , baData
        sPostData = StrConv(baData, vbUnicode)
    End If
    Close nFile
    '--- parse url
    sHttpServer = sUrl
    If InStr(sHttpServer, "://") > 0 Then
        sHttpServer = Mid$(sHttpServer, InStr(sHttpServer, "://") + 3)
    End If
    If InStr(sHttpServer, "/") > 0 Then
        sUploadPage = Mid$(sHttpServer, InStr(sHttpServer, "/"))
        sHttpServer = Left$(sHttpServer, InStr(sHttpServer, "/") - 1)
    End If
    If InStr(sHttpServer, ":") > 0 Then
        On Error Resume Next
        lHttpPort = CLng(Mid$(sHttpServer, InStr(sHttpServer, ":") + 1))
        On Error GoTo 0
        sHttpServer = Left$(sHttpServer, InStr(sHttpServer, ":") - 1)
    End If
    '--- prepare request
    If InternetAutodial(INTERNET_AUTODIAL_FORCE_ONLINE, 0) = 0 Then
        GoTo QH
    End If
    hOpen = InternetOpen(STR_APP_NAME, INTERNET_OPEN_TYPE_PRECONFIG, vbNullString, vbNullString, 0)
    If hOpen = 0 Then
        GoTo QH
    End If
    hConnection = InternetConnect(hOpen, sHttpServer, IIf(lHttpPort <> 0, lHttpPort, INTERNET_DEFAULT_HTTP_PORT), vbNullString, vbNullString, INTERNET_SERVICE_HTTP, 0, 0)
    If hConnection = 0 Then
        GoTo QH
    End If
    hRequest = HttpOpenRequest(hConnection, "POST", sUploadPage, "HTTP/1.0", vbNullString, 0, INTERNET_FLAG_RELOAD, 0)
    If hRequest = 0 Then
        GoTo QH
    End If
    '--- prepare headers
    sBoundary = "3fbd04f5-b1ed-4060-99b9-fca7ff59c113"
    sHeader = "Content-Type: multipart/form-data; boundary=" & sBoundary & vbCrLf
    If HttpAddRequestHeaders(hRequest, sHeader, Len(sHeader), HTTP_ADDREQ_FLAG_REPLACE Or HTTP_ADDREQ_FLAG_ADD) = 0 Then
        GoTo QH
    End If
    '--- post data
    sPostData = "--" & sBoundary & vbCrLf & _
        "Content-Disposition: multipart/form-data; name=""uploadfile""; filename=""" & Mid$(sFileName, InStrRev(sFileName, "\") + 1) & """" & vbCrLf & _
        "Content-Type: application/octet-stream" & vbCrLf & vbCrLf & _
        sPostData & vbCrLf & _
        "--" & sBoundary & "--"
    If HttpSendRequest(hRequest, vbNullString, 0, sPostData, Len(sPostData)) = 0 Then
        GoTo QH
    End If
    '--- success
    pvPostFile = True
QH:
    If hRequest <> 0 Then
        Call InternetCloseHandle(hRequest)
    End If
    If hConnection <> 0 Then
        Call InternetCloseHandle(hConnection)
    End If
    If hOpen <> 0 Then
        Call InternetCloseHandle(hOpen)
    End If
End Function

It’s not hard adding authentication to send request but I don’t need it so never implemented it. The binary nature ot the upload is set by Content-Type being application/octet-stream.

Note that you cannot use Content-Transfer-Encoding with http, it’s reserved for e-mail only, i.e. no base64 encoding is necessary. Binary file is sent directly in the http stream. Note: feel free to change the boundary (use uuidgen.exe to generate a guid or anything unique).

The server-side script

Here is a simple upload_errors.php script that can be used as a target of the post request

<?php
$base_dir = dirname( __FILE__ ) . '/../ErrorsUpload/' . $_GET["id"];
if(!is_dir($base_dir))
    mkdir($base_dir, 0777);
move_uploaded_file($_FILES["uploadfile"]["tmp_name"], $base_dir . '/' . $_FILES["uploadfile"]["name"]);
?>

Basicly it expects an id param in the url and an uploadfile param in the body. Id is used to create a sub-directory in an off-site (publicly not visible) directory ErrorsUpload where the uploaded zip file is stored.

Here as the caller code I’m using that accesses the above upload_errors.php

Private Sub Command1_Click()
    pvPostFile "http://{{your_server_here}}/upload_errors.php?id={A0AD2346-9849-4EF0-9A93-ACFE17910734}", "C:\TEMP\Errors_2011_07_11.zip"
End Sub

I’m preparing a zip file with the errors that are logged at the client site, then I’m using the client id in the url, posting the zip file.

Edit: JScript implementation

Turns out byte-array to string handling is not so straight-forward in JScript. Here is a sample implementation of postFile function extensively using ADODB.Stream to handle conversions:

function readBinaryFile(fileName) {
    var stream = WScript.CreateObject("ADODB.Stream");
    stream.Type = 1;
    stream.Open();
    stream.LoadFromFile(fileName);
    return stream.Read();
}

function toArray(str) {
    var stream = WScript.CreateObject("ADODB.Stream");
    stream.Type = 2;
    stream.Charset = "_autodetect";
    stream.Open()
    stream.WriteText(str);
    stream.Position = 0;
    stream.Type = 1;
    return stream.Read();
}

function postFile(url, fileName, async) {
    var STR_BOUNDARY = "3fbd04f5-b1ed-4060-99b9-fca7ff59c113";

    // prepare post data
    var stream = WScript.CreateObject("ADODB.Stream");
    stream.Type = 1;
    stream.Open()
    stream.Write(toArray("--" + STR_BOUNDARY + "\n" +
        "Content-Disposition: form-data; name=\"uploadfile\"; filename=\"" + fileName.substr(fileName.lastIndexOf("\\") + 1, fileName.length) + "\"\n" +
        "Content-Type: application/octet-stream\n\n"));
    stream.Write(readBinaryFile(fileName));
    stream.Write(toArray("\n--" + STR_BOUNDARY + "--"));
    stream.Position = 0;
    // post request
    var xhr = WScript.CreateObject("Microsoft.XMLHTTP");
    xhr.Open("POST", url, async);
    xhr.SetRequestHeader("Content-Type", "multipart/form-data; boundary=" + STR_BOUNDARY);
    xhr.Send(stream.Read());
    if (async) WScript.Sleep(1);
}
Advertisements
This entry was posted in Articles and tagged , , , , . Bookmark the permalink.

32 Responses to [VB6] Using WinInet to post binary file

  1. Mario Simoes says:

    Hello, I am trying to use winhttp from jscript and of course i am having trouble with the actual POST data. In this case you opened the file as binary, then converted it to UTF8 and then back to byte array.
    In my case since jscript only supports variants i am trying to do the same but without much success, using adodb.stream. What i fail to understand so far is exactly what kind of encoding does the send data needs. Do you know this? Thanks, Mario

  2. wqweto says:

    @Mario Simoes: Hi, just updated the post to include a sample JScript implementation. You can check it out for more ideas.

  3. iD0QS says:

    Hi,
    Just came here to say that you are a fucking wizard works perfectly!
    Used the wininet.dll method to upload Excel Sheets from the User to the Server.
    Never tough that HTTP upload via VBA was possible.

  4. TJ says:

    This is amazing, works perfectly! After many hours of research and toying with the code, the solution I came up with was slowly becoming your solution above, and I used your solution to fill in the missing pieces. In the end, I just copied your solution over, and it is exactly what I need. Thanks!

  5. FLASHCODER says:

    Very good my friend!

  6. Susan says:

    This was very helpful for me and works great for me!

  7. Akshat says:

    Hi I have a problem i want to replicate a multi part form post in Vba excel where i am sending a form data as http post apart from attachment can you tell me how to use this solution to cater to that problem ?

  8. messed-up says:

    Thanks you so much for this solution. I have a small question, how do i capture response returned after HTTP POST is completed? Basically i’m looking to see if i can tell the user if the upload is success or failure.

  9. wqweto says:

    @messed-up: The result is in `ResponseText` property after synchronous `Send`. Just updated the sample `pvPostFile` function to return server response if called synchronously.

  10. messed-up says:

    With CreateObject(“Microsoft.XMLHTTP”)
    .Open “POST”, sUrl, False
    .SetRequestHeader “Content-Type”, “multipart/form-data; boundary=” & STR_BOUNDARY
    .Send pvToByteArray(sPostData)
    pvPostFile = .ResponseText
    End With

    So, this is the modified part. But still the ‘pvPostFile’ is null/empty. Any idea?

    • wqweto says:

      Must be something server-side. Try posting to google.com and check if there is a response text just to be sure the function is implemented correctly.

      I’ve updated the sample code above, note that `ResponseText` is available only upon synchronous execution.

  11. messed-up says:

    Yes, you were right. The server was not returning anything.

  12. this work in windows8 64 bits?

  13. Chris Chambers says:

    Hi,

    I have managed to implement your code ‘as is’ and it works on my server – thanks!

    In addition I’d like to pass binary files to a another site that requires user/password as well as a couple of other fields. How do I submit those parameters? I have tried as part of the URL but that seems to corrupt the password – at least that’s what the error message says, though I’m not 100% sure I can trust that. I’ve also tried as a plaintext part of the multi-part form – again, to no avail.

    I may have an embarrassing gap in my learning and that is, in your example code, how are the various uses of ‘upload’ (in server-side PHP, in client VBA) linked? Equally, how does the server know to properly name the uploaded file?

    Yours,

    Puzzled

    • Chris Chambers says:

      I’ve solved my problems.

      The main one was the various erroneous tries I’d made to avoid corrupting the binary file I was sending. In actuality the difficulty was a downstream bug; the site owner had allowed a hack for a particular file-type to become general.

      The advertised code now works for me in multiple scenarios.

      In addition, the ability to fill plaintext POST variables can be handled like this in the XMLHTTP setting:

          sPostData = "--" & STR_BOUNDARY & vbCrLf
      
          sPostData = sPostData & "Content-Disposition: form-data; name=""user""" & _
                      vbCrLf & vbCrLf & "your-username" & vbCrLf & "--" & STR_BOUNDARY & vbCrLf
          
          sPostData = sPostData & "Content-Disposition: form-data; name=""password""" & _
                      vbCrLf & vbCrLf & "your-password" & vbCrLf & "--" & STR_BOUNDARY & vbCrLf
      
  14. Yes, I got there when I thought my problems were solely at my end.

    I do confess though that I was unable to make the multi-file example at the bottom of that page work – and also understand the reason for the two boundaries (unless that was exampling the ability to change). I presume that the multipart section can be nested but I’m now getting back to exploiting my upload code rather than creating it.

    BTW, I note in the code I posted, double hyphens (without a separating space) have been transformed into an n-dash (?).

    Lastly – nice fern!

  15. justme5461 says:

    How to post multiple files along with some form data?

  16. Supertramp says:

    Hi! Nice code it works!!

    But in server side I can’t receive the file with that function “$input = print_r($_FILES, true);”
    only with that one, because the code converts my file in binary:
    $i = strpos($input, “\r\n\r\n”);
    if ($i !== false)
    $input = substr($input, $i + 4);
    file_put_contents(‘resultado.zip’, $input);

    I just wanna send zip files, dont wanna convert in binary. any solution?

    • wqweto says:

      Just use `move_uploaded_file` function as shown in the code above in section “The server-side script”. Your binary zip file is already stored to a temp location by PHP for your convenience. You just have to move/rename it with `move_uploaded_file` function to `resultado.zip`. No need to fiddle with `$_FILES` contents, IMO.

      • Supertramp says:

        Thanks for the fast awnser.
        Something like that?
        move_uploaded_file($_FILES[“resultado.zip”][“resultado.zip”], $base_dir . ‘/’ . $_FILES[“resultado.zip”][“resultado.zip”]);

    • wqweto says:

      Did you try the original code as is? It will use the original filename on the client machine which PHP puts in $_FILES["uploadfile"]["name"] — this gets to be the filename you passed as argument to VB’s `pvPostFile` function.

      If you really want to use a different (hard-coded) name for the uploaded file on the web server use something like move_uploaded_file($_FILES["uploadfile"]["tmp_name"], $base_dir . '/' . 'resultado.zip')

      • Supertramp says:

        it creates me a ErrorsUpload folder, and nothing in there. Don’t work for me, that function.

        Do you know how to implement hash code in your VBA function to make the upload of the file with security?

        Thanks

    • wqweto says:

      From your VB code try to access the backend URL w/ something in the `id` parameter like this:

      pvPostFile "http://{{your_server_here}}/upload_errors.php?id=test", "C:\TEMP\resultado.zip"

      This wll create a subdirectory `test` in `ErrorsUpload` and place `resultado.zip` there. This is the behavior I needed for my puposes, `id` in my case is unique per client machine.

      For security you can try using ssl/tls on your web server (this involves certificates).

      For checksums for upload verification you have to device some protocol on your own, nothing ready-made here.

  17. Supertramp says:

    The first function ends with a “End Sub” is that a mistake right?

  18. demy says:

    The code works, but I think the file is not the same after send to the server and before, well is almost the same, nothing is missing, but if we look to the file size they are not the same, for only a few bytes.
    If we compare the 2 MD5, they are not the same too.

  19. demy says:

    How do I do to send the File Name in the header?

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s