Search Unity

Best HTTP Released

Discussion in 'Assets and Asset Store' started by BestHTTP, Sep 11, 2013.

  1. GuyTidhar

    GuyTidhar

    Joined:
    Jun 24, 2009
    Posts:
    320
    Great to hear! Thanks.
     
  2. Xorxor

    Xorxor

    Joined:
    Oct 15, 2014
    Posts:
    24
    I'd like to make a request, but add some data to it that I could then access in the callback. Is this possible? Thanks!!
     
  3. BestHTTP

    BestHTTP

    Joined:
    Sep 11, 2013
    Posts:
    1,664
    @Xorxor

    You can use the Tag property of the HTTPRequest object as in my previous sample.
    If you have to store multiple data, you can create a class with multiple fields and store an instance of this object:
    Code (CSharp):
    1. sealed class DataCollector
    2. {
    3.     public int Id { get; private set; }
    4.     public DateTime SentAt { get; private set; }
    5.  
    6.     public DataCollector(int id, DateTime sent)
    7.     {
    8.         this.Id = id;
    9.         this.SentAt = sent;
    10.     }
    11. }
    12.  
    13. static int lastId = 0;
    14.  
    15. Uri uri = new Uri("http://httpbin.org");
    16.  
    17. HTTPRequest request = new HTTPRequest(uri, (req, resp) =>
    18. {
    19.     switch (req.State)
    20.     {
    21.         // The request finished without any problem.
    22.         case HTTPRequestStates.Finished:
    23.             if (resp.IsSuccess)
    24.             {
    25.                 Debug.Log("Request Finished Successfully! Response: " + resp.DataAsText);
    26.  
    27.                 DataCollector data = req.Tag as DataCollector;
    28.  
    29.                 TimeSpan requestTime = DateTime.UtcNow - data.SentAt;
    30.                 Debug.Log(string.Format("[{0}] Request time: {1}(ms)", data.Id, requestTime.TotalMilliseconds.ToString()));
    31.             }
    32.             else // Internal server error?
    33.                 Debug.LogWarning(string.Format("Request Finished Successfully, but the server sent an error. Status Code: {0}-{1} Message: {2}",
    34.                                                 resp.StatusCode,
    35.                                                 resp.Message,
    36.                                                 resp.DataAsText));
    37.             break;
    38.  
    39.         // The request finished with an unexpected error. The request's Exception property may contain more info about the error.
    40.         case HTTPRequestStates.Error:
    41.             Debug.LogWarning("Request Finished with Error! " + (req.Exception != null ? (req.Exception.Message + "\n" + req.Exception.StackTrace) : "No Exception"));
    42.             break;
    43.  
    44.         // The request aborted, initiated by the user.
    45.         case HTTPRequestStates.Aborted:
    46.             Debug.LogWarning("Request Aborted!");
    47.             break;
    48.  
    49.         // Ceonnecting to the server is timed out.
    50.         case HTTPRequestStates.ConnectionTimedOut:
    51.             Debug.LogError("Connection Timed Out!");
    52.             break;
    53.  
    54.         // The request didn't finished in the given time.
    55.         case HTTPRequestStates.TimedOut:
    56.             Debug.LogError("Processing the request Timed Out!");
    57.             break;
    58.     }
    59. });
    60.  
    61. request.Tag = new DataCollector(++lastId, DateTime.UtcNow);
    62. request.Send();
     
  4. Xorxor

    Xorxor

    Joined:
    Oct 15, 2014
    Posts:
    24
    Perfect. Thank you!
     
  5. wjpeters

    wjpeters

    Joined:
    Jan 3, 2013
    Posts:
    16
    Hi,

    I wan't to use Azure blob storage, but i can't get it to work with bestHTTP Pro. Getting error message (Server failed to authenticate the request. Make sure the value of Authorization header is formed correctly including the signature.) I think it has to do with the way bestHTTP constructed the header because if i use a example code that uses HttpWebRequest it works perfectly. Any idea what I'm doing wrong?

    This code without bestHTTP works:
    Code (CSharp):
    1.  
    2. privatevoidPutBlob(StringcontainerName,StringblobName)
    3. {
    4. //https://social.msdn.microsoft.com/Forums/azure/en-US/ee551d65-ecd6-4e6c-9da9-a412f98b7c8b/blob-rest-authentication-examples?forum=windowsazuredata
    5. StringrequestMethod="PUT";
    6.  
    7. Stringcontent="TheNameofThisBandisTalkingHeads";
    8. UTF8Encodingutf8Encoding=newUTF8Encoding();
    9. Byte[]blobContent=utf8Encoding.GetBytes(content);
    10. Int32blobLength=blobContent.Length;
    11.  
    12. constStringblobType="BlockBlob";
    13.  
    14. StringurlPath=String.Format("{0}/{1}",containerName,blobName);
    15. StringmsVersion="2009-09-19";
    16. StringdateInRfc1123Format=DateTime.UtcNow.ToString("R",CultureInfo.InvariantCulture);
    17.  
    18. StringcanonicalizedHeaders=String.Format("x-ms-blob-type:{0}\nx-ms-date:{1}\nx-ms-version:{2}",blobType,dateInRfc1123Format,msVersion);
    19. StringcanonicalizedResource=String.Format("/{0}/{1}",AzureStorageConstants.Account,urlPath);
    20. StringstringToSign=String.Format("{0}\n\n\n{1}\n\n\n\n\n\n\n\n\n{2}\n{3}",requestMethod,blobLength,canonicalizedHeaders,canonicalizedResource);
    21. StringauthorizationHeader=CreateAuthorizationHeader(stringToSign);
    22.  
    23. Uriuri=newUri(AzureStorageConstants.BlobEndPoint+urlPath);
    24. HttpWebRequestrequest=(HttpWebRequest)WebRequest.Create(uri);
    25. request.Method=requestMethod;
    26. request.Headers.Add("x-ms-blob-type",blobType);
    27. request.Headers.Add("x-ms-date",dateInRfc1123Format);
    28. request.Headers.Add("x-ms-version",msVersion);
    29. request.Headers.Add("Authorization",authorizationHeader);
    30. request.ContentLength=blobLength;
    31.  
    32. //Debug.Log(string.Format("{0}",request.res);
    33.  
    34. using(StreamrequestStream=request.GetRequestStream())
    35. {
    36. requestStream.Write(blobContent,0,blobLength);
    37. }
    38.  
    39. using(HttpWebResponseresponse=(HttpWebResponse)request.GetResponse())
    40. {
    41. StringETag=response.Headers["ETag"];
    42. Debug.Log(response.Headers["ETag"]);
    43. }
    44. }
    This code with bestHTTP failed to authenticate the request.
    Code (CSharp):
    1. private void PutBlobPro(String containerName, String blobName)
    2.         {
    3.             //https://social.msdn.microsoft.com/Forums/azure/en-US/ee551d65-ecd6-4e6c-9da9-a412f98b7c8b/blob-rest-authentication-examples?forum=windowsazuredata
    4.             String requestMethod = "PUT";
    5.  
    6.             String content = "The Name of This Band is Talking Heads";
    7.             UTF8Encoding utf8Encoding = new UTF8Encoding();
    8.             Byte[] blobContent = utf8Encoding.GetBytes(content);
    9.             Int32 blobLength = blobContent.Length;
    10.  
    11.             const String blobType = "BlockBlob";
    12.  
    13.             String urlPath = String.Format("{0}/{1}", containerName, blobName);
    14.             String msVersion = "2009-09-19";
    15.             String dateInRfc1123Format = DateTime.UtcNow.ToString("R", CultureInfo.InvariantCulture);
    16.  
    17.             String canonicalizedHeaders = String.Format("x-ms-blob-type:{0}\nx-ms-date:{1}\nx-ms-version:{2}", blobType, dateInRfc1123Format, msVersion);
    18.             String canonicalizedResource = String.Format("/{0}/{1}", AzureStorageConstants.Account, urlPath);
    19.             String stringToSign = String.Format("{0}\n\n\n{1}\n\n\n\n\n\n\n\n\n{2}\n{3}", requestMethod, blobLength, canonicalizedHeaders, canonicalizedResource);
    20.             String authorizationHeader = CreateAuthorizationHeader(stringToSign);
    21.  
    22.             Debug.Log("Request Log\n" + authorizationHeader);
    23.  
    24.             HTTPRequest request = new HTTPRequest(new Uri(AzureStorageConstants.BlobEndPoint + urlPath), (req, resp) =>
    25.                 {
    26.                     switch (req.State)
    27.                     {
    28.                         // The request finished without any problem.
    29.                         case HTTPRequestStates.Finished:
    30.                         Debug.Log("Request Finished Successfully!\n" + resp.DataAsText);
    31.                         Debug.Log("Request Log\n" + req.DumpHeaders());
    32.                         break;
    33.                         // The request finished with an unexpected error.
    34.                         // The request's Exception property may contain more information about the error.
    35.                         case HTTPRequestStates.Error:
    36.                         Debug.LogError("Request Finished with Error! " +
    37.                             (req.Exception != null ?
    38.                                 (req.Exception.Message + "\n" +
    39.                                     req.Exception.StackTrace) :
    40.                                 "No Exception"));
    41.                         break;
    42.                         // The request aborted, initiated by the user.
    43.                         case HTTPRequestStates.Aborted: Debug.LogWarning("Request Aborted!"); break;
    44.                         // Ceonnecting to the server timed out.
    45.                         case HTTPRequestStates.ConnectionTimedOut: Debug.LogError("Connection Timed Out!"); break;
    46.                         // The request didn't finished in the given time.
    47.                         case HTTPRequestStates.TimedOut: Debug.LogError("Processing the request Timed Out!"); break;
    48.                     }
    49.                 });
    50.             request.Timeout = TimeSpan.FromSeconds(5);
    51.             request.DisableCache = true;
    52.  
    53.             request.SetHeader("x-ms-blob-type", blobType);
    54.             request.SetHeader("x-ms-date", dateInRfc1123Format);
    55.             request.SetHeader("x-ms-version", msVersion);
    56.             request.SetHeader("Authorization", authorizationHeader);
    57.             request.SetHeader("Accept-Charset", "UTF-8");
    58.             //request.UploadStream = new FileStream(blobContent, FileMode.Create);
    59.             request.Send();
    60.         }
    61.  
     
  6. BestHTTP

    BestHTTP

    Joined:
    Sep 11, 2013
    Posts:
    1,664
    @wjpeters

    One thing that might a problem that you create the authorization header with request method "PUT", however with the BestHTTP version you are not sending a put request. It should be like this:
    Code (CSharp):
    1. HTTPRequest request = new HTTPRequest(new Uri(AzureStorageConstants.BlobEndPoint + urlPath), HTTPMethods.Put, (req, resp) =>
     
  7. BestHTTP

    BestHTTP

    Joined:
    Sep 11, 2013
    Posts:
    1,664
    @wjpeters Also, the plugin will place a space between header key and value:
    Code (CSharp):
    1. String canonicalizedHeaders = String.Format("x-ms-blob-type: {0}\nx-ms-date: {1}\nx-ms-version: {2}", blobType, dateInRfc1123Format, msVersion);
     
  8. wjpeters

    wjpeters

    Joined:
    Jan 3, 2013
    Posts:
    16
    Thanks for your answer. I think this is the problem. Constructing the canonicalized headers needs to trim any white space around the colon. Is it possible to stop the plugin from placing spaces between the header key and value?
     
  9. wjpeters

    wjpeters

    Joined:
    Jan 3, 2013
    Posts:
    16
    I checked the header from HttpWebRequest and it also places a space between header key and value. So it must be something else.
     
  10. wjpeters

    wjpeters

    Joined:
    Jan 3, 2013
    Posts:
    16
    Is it possible to remove headers from the HTTPRequest? The HttpWebRequest header doesn't send the following headers:

    Host: myspacetoazure.blob.core.windows.net
    Accept-Encoding: gzip, identity
    Connection: Keep-Alive, TE
    TE: identity
    User-Agent: BestHTTP
    Content-Length: 0
     
  11. BestHTTP

    BestHTTP

    Joined:
    Sep 11, 2013
    Posts:
    1,664
    @wjpeters If you have the Pro version of the plugin, you can edit the EnumerateHeaders in the HTTPRequest.cs starting around line 824.
    You can remove the space between the header key and value at line 1005.
     
  12. wjpeters

    wjpeters

    Joined:
    Jan 3, 2013
    Posts:
    16
    Thanks. I removed SetHeader("Host",CurrentUri.Authority); and no the call gets authorized. But when i use a get request the get string has to be:

    I [HTTPRequest]: Sending request: GET /container-test/willem

    the plugin puts HTTP/1.1 to the ending of the get string.

    I [HTTPRequest]: Sending request: GET /container-test/willem HTTP/1.1

    When i remove HTTP/1.1 from HTTPRequest.cs line 1060

    Code (CSharp):
    1. stringrequestLine=string.Format("{0}{1}HTTP/1.1",MethodNames[(byte)MethodType],
    I get:

    Request Finished with Error! Array index is out of range.
    at BestHTTP.HTTPResponse.Receive (Int32 forceReadRawContentLength, Boolean readPayloadData) [0x0006d] in /Users/qb/Documents/Projects/IDMM/IDM_Poject/IDMM/Assets/Best HTTP (Pro)/BestHTTP/HTTPResponse.cs:207
    at BestHTTP.HTTPConnection.Receive () [0x0005a] in /Users/qb/Documents/Projects/IDMM/IDM_Poject/IDMM/Assets/Best HTTP (Pro)/BestHTTP/HTTPConnection.cs:563
    at BestHTTP.HTTPConnection.ThreadFunc (System.Object param) [0x00128] in /Users/qb/Documents/Projects/IDMM/IDM_Poject/IDMM/Assets/Best HTTP (Pro)/BestHTTP/HTTPConnection.cs:169
    UnityEngine.Debug:LogError(Object)
    Assets.Scripts.Core.AzureBridge:<GetBlobPro>m__1(HTTPRequest, HTTPResponse) (at Assets/AppIDMeditor/Scripts/Azure/AzureBridge.cs:119)
    BestHTTP.HTTPRequest:CallCallback() (at Assets/Best HTTP (Pro)/BestHTTP/HTTPRequest.cs:1186)
    BestHTTP.ConnectionBase:HandleCallback() (at Assets/Best HTTP (Pro)/BestHTTP/Connections/ConnectionBase.cs:171)
    BestHTTP.HTTPManager:OnUpdate() (at Assets/Best HTTP (Pro)/BestHTTP/HTTPManager.cs:620)
    BestHTTP.HTTPUpdateDelegator:Update() (at Assets/Best HTTP (Pro)/BestHTTP/HTTPUpdateDelegator.cs:154)
     
  13. BestHTTP

    BestHTTP

    Joined:
    Sep 11, 2013
    Posts:
    1,664
    @wjpeters

    It would be good to know what the server really sent back. If you delete the "HTTP/1.1" from the request, the server most probably will send a HTTP/1.0 response. But it has the same format as HTTP/1.1, so it should work correctly.

    It's usually fails on this line when the server closes the connection unexpectedly. I don't know if it's the case, but i think it's worth to mention.
     
    wjpeters likes this.
  14. BestHTTP

    BestHTTP

    Joined:
    Sep 11, 2013
    Posts:
    1,664
    @wjpeters

    Took my time to create a complete example to access Azure Blob Storage. The plugin was capable to send requests successfully, but to make it more easier added a new event that will be fired just before the plugin sends out the headers. In this event we can add new headers too.

    The complete example:
    Code (CSharp):
    1. void AzureBlobTest()
    2. {
    3.     string accountName = "your account name";
    4.     string container = "container name";
    5.     string blobName = "blob name";
    6.  
    7.     string accessKey = "your azure blobl storage access key";
    8.  
    9.     var credentials = new BestHTTP.Authentication.Credentials(accountName, accessKey);
    10.  
    11.     //UploadBlob(container, blobName, credentials);
    12.  
    13.     ListBlobs(container, credentials);
    14. }
    15.  
    16. void ListBlobs(string container, BestHTTP.Authentication.Credentials accessKey)
    17. {
    18.     string ListBlobsEndpoint = string.Format("http://{0}.blob.core.windows.net/{1}?restype=container&comp=list&delimiter=%2F", accessKey.UserName, container);
    19.  
    20.     HTTPRequest request = new HTTPRequest(new Uri(ListBlobsEndpoint), OnAzureBlob_RequestFinished);
    21.  
    22.     request.AddHeader("x-ms-version", "2015-04-05");
    23.     request.AddHeader("x-ms-date", DateTime.UtcNow.ToString("R", System.Globalization.CultureInfo.InvariantCulture));
    24.  
    25.     // This callback will sign our request
    26.     request.OnBeforeHeaderSend += SignRequestCallback;
    27.  
    28.     // Store our access key in the Tag property, so we can access it in the SignRequestCallback function
    29.     request.Tag = accessKey;
    30.  
    31.     request.Send();
    32. }
    33.  
    34. void UploadBlob(string container, string blobName, BestHTTP.Authentication.Credentials accessKey)
    35. {
    36.     string UploadBlobEndpoint = string.Format("http://{0}.blob.core.windows.net/{1}/{2}", accessKey.UserName, container, blobName);
    37.  
    38.     HTTPRequest request = new HTTPRequest(new Uri(UploadBlobEndpoint), HTTPMethods.Put, OnAzureBlob_RequestFinished);
    39.  
    40.     request.AddHeader("x-ms-version", "2015-04-05");
    41.     request.AddHeader("x-ms-date", DateTime.UtcNow.ToString("R", System.Globalization.CultureInfo.InvariantCulture));
    42.  
    43.     request.AddHeader("x-ms-blob-type", "BlockBlob");
    44.     request.UploadStream = new FileStream("path to file", FileMode.Open);
    45.  
    46.     // This callback will sign our request
    47.     request.OnBeforeHeaderSend += SignRequestCallback;
    48.  
    49.     // Store our access key in the Tag property, so we can access it in the SignRequestCallback function
    50.     request.Tag = accessKey;
    51.  
    52.     request.Send();
    53. }
    54.  
    55. void OnAzureBlob_RequestFinished(HTTPRequest req, HTTPResponse resp)
    56. {
    57.     switch (req.State)
    58.     {
    59.         // The request finished without any problem.
    60.         case HTTPRequestStates.Finished:
    61.             if (resp.IsSuccess)
    62.             {
    63.                 Debug.Log("Request Finished Successfully! Response: " + resp.DataAsText);
    64.             }
    65.             else // Internal server error?
    66.                 Debug.LogWarning(string.Format("Request Finished Successfully, but the server sent an error. Status Code: {0}-{1} Message: {2}",
    67.                                                 resp.StatusCode,
    68.                                                 resp.Message,
    69.                                                 resp.DataAsText));
    70.             break;
    71.  
    72.         // The request finished with an unexpected error. The request's Exception property may contain more info about the error.
    73.         case HTTPRequestStates.Error:
    74.             Debug.LogWarning("Request Finished with Error! " + (req.Exception != null ? (req.Exception.Message + "\n" + req.Exception.StackTrace) : "No Exception"));
    75.             break;
    76.  
    77.         // The request aborted, initiated by the user.
    78.         case HTTPRequestStates.Aborted:
    79.             Debug.LogWarning("Request Aborted!");
    80.             break;
    81.  
    82.         // Connecting to the server is timed out.
    83.         case HTTPRequestStates.ConnectionTimedOut:
    84.             Debug.LogError("Connection Timed Out!");
    85.             break;
    86.  
    87.         // The request didn't finished in the given time.
    88.         case HTTPRequestStates.TimedOut:
    89.             Debug.LogError("Processing the request Timed Out!");
    90.             break;
    91.     }
    92. }
    93.  
    94. void SignRequestCallback(HTTPRequest req)
    95. {
    96.     BestHTTP.Authentication.Credentials credentials = req.Tag as BestHTTP.Authentication.Credentials;
    97.  
    98.     // Add the method (GET, POST, PUT, or HEAD).
    99.     CanonicalizedString canonicalizedString = new CanonicalizedString(HTTPRequest.MethodNames[(byte)req.MethodType]);
    100.  
    101.     // Add the Content-* HTTP headers. Empty values are allowed.
    102.     canonicalizedString.AppendCanonicalizedElement(req.GetFirstHeaderValue("Content-Encoding"));
    103.     canonicalizedString.AppendCanonicalizedElement(req.GetFirstHeaderValue("Content-Language"));
    104.     AppendCanonicalizedContentLengthHeader(canonicalizedString, req);
    105.     canonicalizedString.AppendCanonicalizedElement(req.GetFirstHeaderValue("Content-MD5"));
    106.     canonicalizedString.AppendCanonicalizedElement(req.GetFirstHeaderValue("Content-Type"));
    107.  
    108.     // Add the Date HTTP header (only if the x-ms-date header is not being used)
    109.     AppendCanonicalizedDateHeader(canonicalizedString, req);
    110.  
    111.     // Add If-* headers and Range header
    112.     canonicalizedString.AppendCanonicalizedElement(req.GetFirstHeaderValue("If-Modified-Since"));
    113.     canonicalizedString.AppendCanonicalizedElement(req.GetFirstHeaderValue("If-Match"));
    114.     canonicalizedString.AppendCanonicalizedElement(req.GetFirstHeaderValue("If-None-Match"));
    115.     canonicalizedString.AppendCanonicalizedElement(req.GetFirstHeaderValue("If-Unmodified-Since"));
    116.     canonicalizedString.AppendCanonicalizedElement(req.GetFirstHeaderValue("Range"));
    117.  
    118.     // Add any custom headers
    119.     AppendCanonicalizedCustomHeaders(req, canonicalizedString);
    120.  
    121.     // Add the canonicalized URI element
    122.     string resourceString = GetCanonicalizedResourceString(req.CurrentUri, credentials.UserName);
    123.     canonicalizedString.AppendCanonicalizedElement(resourceString);
    124.  
    125.     string message = canonicalizedString.ToString();
    126.  
    127.     req.AddHeader("Authorization", CreateAuthorizationHeader(message, Convert.FromBase64String(credentials.Password), credentials.UserName));
    128. }
    129.  
    130. public static IDictionary<string, string> ParseQueryString(string query)
    131. {
    132.     Dictionary<string, string> retVal = new Dictionary<string, string>();
    133.     if (string.IsNullOrEmpty(query))
    134.     {
    135.         return retVal;
    136.     }
    137.  
    138.     if (query.StartsWith("?", StringComparison.Ordinal))
    139.     {
    140.         if (query.Length == 1)
    141.         {
    142.             return retVal;
    143.         }
    144.  
    145.         query = query.Substring(1);
    146.     }
    147.  
    148.     string[] valuePairs = query.Split('&');
    149.     foreach (string pair in valuePairs)
    150.     {
    151.         string key;
    152.         string value;
    153.  
    154.         int equalDex = pair.IndexOf("=", StringComparison.Ordinal);
    155.         if (equalDex < 0)
    156.         {
    157.             key = string.Empty;
    158.             value = Uri.UnescapeDataString(pair);
    159.         }
    160.         else
    161.         {
    162.             key = Uri.UnescapeDataString(pair.Substring(0, equalDex));
    163.             value = Uri.UnescapeDataString(pair.Substring(equalDex + 1));
    164.         }
    165.  
    166.         string existingValue;
    167.         if (retVal.TryGetValue(key, out existingValue))
    168.         {
    169.             retVal[key] = string.Concat(existingValue, ",", value);
    170.         }
    171.         else
    172.         {
    173.             retVal[key] = value;
    174.         }
    175.     }
    176.  
    177.     return retVal;
    178. }
    179.  
    180. static void AppendCanonicalizedDateHeader(CanonicalizedString canonicalizedString, HTTPRequest request, bool allowMicrosoftDateHeader = false)
    181. {
    182.     string microsoftDateHeaderValue = request.GetFirstHeaderValue("x-ms-date");
    183.     if (string.IsNullOrEmpty(microsoftDateHeaderValue))
    184.     {
    185.         canonicalizedString.AppendCanonicalizedElement(request.GetFirstHeaderValue("Date"));
    186.     }
    187.     else if (allowMicrosoftDateHeader)
    188.     {
    189.         canonicalizedString.AppendCanonicalizedElement(microsoftDateHeaderValue);
    190.     }
    191.     else
    192.     {
    193.         canonicalizedString.AppendCanonicalizedElement(null);
    194.     }
    195. }
    196.  
    197. private static string GetAbsolutePathWithoutSecondarySuffix(Uri uri, string accountName)
    198. {
    199.     string absolutePath = uri.AbsolutePath;
    200.     string secondaryAccountName = string.Concat(accountName, "-secondary");
    201.  
    202.     int startIndex = absolutePath.IndexOf(secondaryAccountName, StringComparison.OrdinalIgnoreCase);
    203.     if (startIndex == 1)
    204.     {
    205.         startIndex += accountName.Length;
    206.         absolutePath = absolutePath.Remove(startIndex, "-secondary".Length);
    207.     }
    208.  
    209.     return absolutePath;
    210. }
    211. static string GetCanonicalizedResourceString(Uri uri, string accountName, bool isSharedKeyLiteOrTableService = false)
    212. {
    213.     StringBuilder canonicalizedResource = new StringBuilder(100);
    214.     canonicalizedResource.Append('/');
    215.     canonicalizedResource.Append(accountName);
    216.     canonicalizedResource.Append(GetAbsolutePathWithoutSecondarySuffix(uri, accountName));
    217.  
    218.     IDictionary<string, string> queryParameters = ParseQueryString(uri.Query);
    219.     if (!isSharedKeyLiteOrTableService)
    220.     {
    221.         List<string> queryParameterNames = new List<string>(queryParameters.Keys);
    222.         queryParameterNames.Sort(StringComparer.OrdinalIgnoreCase);
    223.  
    224.         foreach (string queryParameterName in queryParameterNames)
    225.         {
    226.             canonicalizedResource.Append('\n');
    227.             canonicalizedResource.Append(queryParameterName.ToLowerInvariant());
    228.             canonicalizedResource.Append(':');
    229.             canonicalizedResource.Append(queryParameters[queryParameterName]);
    230.         }
    231.     }
    232.     else
    233.     {
    234.         // Add only the comp parameter
    235.         string compQueryParameterValue;
    236.         if (queryParameters.TryGetValue("comp", out compQueryParameterValue))
    237.         {
    238.             canonicalizedResource.Append("?comp=");
    239.             canonicalizedResource.Append(compQueryParameterValue);
    240.         }
    241.     }
    242.  
    243.     return canonicalizedResource.ToString();
    244. }
    245.  
    246. static void AppendCanonicalizedContentLengthHeader(CanonicalizedString canonicalizedString, HTTPRequest request)
    247. {
    248.     int contentLength = int.Parse(request.GetFirstHeaderValue("Content-Length") ?? "-1");
    249.     if (contentLength != -1L && contentLength != 0)
    250.         canonicalizedString.AppendCanonicalizedElement(contentLength.ToString(CultureInfo.InvariantCulture));
    251.     else
    252.         canonicalizedString.AppendCanonicalizedElement(null);
    253. }
    254.  
    255. /// <summary>
    256. /// Appends the values of the x-ms-* headers to the specified canonicalized string.
    257. /// </summary>
    258. static void AppendCanonicalizedCustomHeaders(HTTPRequest request, CanonicalizedString canonicalizedString)
    259. {
    260.     List<string> headerNames = new List<string>();
    261.     request.EnumerateHeaders((name, values) =>
    262.     {
    263.         if (name.StartsWith("x-ms-", StringComparison.OrdinalIgnoreCase))
    264.         {
    265.             headerNames.Add(name.ToLowerInvariant());
    266.         }
    267.     });
    268.  
    269.     System.Globalization.CultureInfo sortingCulture = new CultureInfo("en-US");
    270.     StringComparer sortingComparer = StringComparer.Create(sortingCulture, false);
    271.     headerNames.Sort(sortingComparer);
    272.  
    273.     StringBuilder canonicalizedElement = new StringBuilder(headerNames.Count);
    274.     foreach (string headerName in headerNames)
    275.     {
    276.         string value = request.GetFirstHeaderValue(headerName);
    277.         if (!string.IsNullOrEmpty(value))
    278.         {
    279.             canonicalizedElement.Length = 0;
    280.             canonicalizedElement.Append(headerName);
    281.             canonicalizedElement.Append(":");
    282.             canonicalizedElement.Append(value.TrimStart().Replace("\r\n", string.Empty));
    283.  
    284.             canonicalizedString.AppendCanonicalizedElement(canonicalizedElement.ToString());
    285.         }
    286.     }
    287. }
    288.  
    289. class CanonicalizedString
    290. {
    291.     private const int DefaultCapacity = 300;
    292.     private const char ElementDelimiter = '\n';
    293.  
    294.     /// <summary>
    295.     /// Stores the internal <see cref="StringBuilder"/> that holds the canonicalized string.
    296.     /// </summary>
    297.     private readonly StringBuilder canonicalizedString;
    298.  
    299.     /// <summary>
    300.     /// Initializes a new instance of the <see cref="CanonicalizedString"/> class.
    301.     /// </summary>
    302.     /// <param name="initialElement">The first canonicalized element to start the string with.</param>
    303.     public CanonicalizedString(string initialElement)
    304.         : this(initialElement, CanonicalizedString.DefaultCapacity)
    305.     {
    306.     }
    307.  
    308.     /// <summary>
    309.     /// Initializes a new instance of the <see cref="CanonicalizedString"/> class.
    310.     /// </summary>
    311.     /// <param name="initialElement">The first canonicalized element to start the string with.</param>
    312.     /// <param name="capacity">The starting size of the string.</param>
    313.     public CanonicalizedString(string initialElement, int capacity)
    314.     {
    315.         this.canonicalizedString = new StringBuilder(initialElement, capacity);
    316.     }
    317.  
    318.     /// <summary>
    319.     /// Append additional canonicalized element to the string.
    320.     /// </summary>
    321.     /// <param name="element">An additional canonicalized element to append to the string.</param>
    322.     public void AppendCanonicalizedElement(string element)
    323.     {
    324.         this.canonicalizedString.Append(CanonicalizedString.ElementDelimiter);
    325.         this.canonicalizedString.Append(element);
    326.     }
    327.  
    328.     /// <summary>
    329.     /// Converts the value of this instance to a string.
    330.     /// </summary>
    331.     /// <returns>A string whose value is the same as this instance.</returns>
    332.     public override string ToString()
    333.     {
    334.         return this.canonicalizedString.ToString();
    335.     }
    336. }
    337.  
    338. private String CreateAuthorizationHeader(String canonicalizedString, byte[] key, string account)
    339. {
    340.     if (String.IsNullOrEmpty(canonicalizedString))
    341.     {
    342.         throw new ArgumentNullException("canonicalizedString");
    343.     }
    344.  
    345.     String signature = CreateHmacSignature(canonicalizedString, key);
    346.     String authorizationHeader = String.Format(System.Globalization.CultureInfo.InvariantCulture, "SharedKey {0}:{1}", account, signature);
    347.  
    348.     return authorizationHeader;
    349. }
    350.  
    351. private String CreateHmacSignature(String unsignedString, Byte[] key)
    352. {
    353.     if (String.IsNullOrEmpty(unsignedString))
    354.     {
    355.         throw new ArgumentNullException("unsignedString");
    356.     }
    357.  
    358.     if (key == null)
    359.     {
    360.         throw new ArgumentNullException("key");
    361.     }
    362.  
    363.     Byte[] dataToHmac = System.Text.Encoding.UTF8.GetBytes(unsignedString);
    364.     using (System.Security.Cryptography.HMACSHA256 hmacSha256 = new System.Security.Cryptography.HMACSHA256(key))
    365.     {
    366.         return Convert.ToBase64String(hmacSha256.ComputeHash(dataToHmac));
    367.     }
    368. }
    I will send a link to an updated package in private.

    You can find the Blob Service REST API reference here: https://msdn.microsoft.com/en-us/library/azure/dd135733.aspx
    It contains the optional and mandatory headers and uri parameters.
     
    Last edited: Mar 12, 2016
    wjpeters likes this.
  15. wjpeters

    wjpeters

    Joined:
    Jan 3, 2013
    Posts:
    16
    @BestHTTP
    Thank you for taking time to create a complete example to access Azure Blob Storage. That's very kind of you!!

    The updated package and example works perfectly. now I can upload and list blobs with BestHTTP on Azure without any problems. This is exactly what i needed and you saved me a lot of work.

    I will express my gratitude in the Asset Store :)
     
    Tinjaw likes this.
  16. jGate99

    jGate99

    Joined:
    Oct 22, 2013
    Posts:
    1,943
    @BestHTTP

    How do i dispose HTTP Request and Response instance along with loaded data (textures)?

    Thanks
     
  17. BestHTTP

    BestHTTP

    Joined:
    Sep 11, 2013
    Posts:
    1,664
    @JohnGate

    The plugin will not keep a reference to the request/response objects, so its memory will be reclaimed by the GC.
     
  18. BestHTTP

    BestHTTP

    Joined:
    Sep 11, 2013
    Posts:
    1,664
    @wjpeters Thanks, very much appreciated!
     
  19. jGate99

    jGate99

    Joined:
    Oct 22, 2013
    Posts:
    1,943
    ok lets say i send a request for a texture in Scene A, i apply that texture directly to a RawImage (which uses pure texture). i load another sceneB.
    Now what happens to that texture? was it automatically disposed?
     
  20. BestHTTP

    BestHTTP

    Joined:
    Sep 11, 2013
    Posts:
    1,664
    @JohnGate
    I think so. Unity will most probably purge out not used resources.
    The plugin itself will not keep references to downloaded data. It might keep it in the disk cache, but not in memory.
     
  21. jGate99

    jGate99

    Joined:
    Oct 22, 2013
    Posts:
    1,943
    Alright Thanks :)
     
  22. talcompedia

    talcompedia

    Joined:
    Jul 14, 2014
    Posts:
    4
    Does BestHTTP use HTTP/1.1 request pipelining?
     
  23. BestHTTP

    BestHTTP

    Joined:
    Sep 11, 2013
    Posts:
    1,664
    @talcompedia

    No.
    But connection pooling is enabled by default. It's not the same, but it greatly improves performance too.
     
  24. talcompedia

    talcompedia

    Joined:
    Jul 14, 2014
    Posts:
    4
    Too bad, but thanks for the quick response!
     
  25. ChrisHoefgenTMG

    ChrisHoefgenTMG

    Joined:
    Jul 19, 2013
    Posts:
    2
    We have been using BestHTTP for some of our editor data tools to talk to Google Docs. Everything was fine until we updated to Unity 5.3.3p3.

    I have managed to isolate the issue to the HTTPUpdateDelegator which is throwing an exception on line 66, it looks like GameObject.DontDestroyOnLoad() doesn't like to be called indirectly via an Editor script.

    I seemed to have fixed the issue by surrounding the line with the following directive:

    Code (CSharp):
    1. #if !UNITY_EDITOR
    2. GameObject.DontDestroyOnLoad(go);
    3. #endif
    Not sure if this is safe but it does seem to correct the issue for our tools. Was this a bug or are we using BestHTTP wrong?

    -Chris
     
  26. BestHTTP

    BestHTTP

    Joined:
    Sep 11, 2013
    Posts:
    1,664
    @ChrisHoefgenTMG

    Hi Chris.

    Thanks for letting me know about this problem. I will make sure that it will work in editor mode too.
    By not compiling DontDestroyOnLoad will be safe until you didn't change scenes while there is an ongoing request. When this happens your callbacks will not be called until you make a new request.
     
  27. JMEA

    JMEA

    Joined:
    Sep 27, 2013
    Posts:
    23
    I'm seeing various issues cancelling requests with HTTPRequest.Abort when using HTTPS. Here are a few of the callstacks, they seem to be timing related. Should a specific state be checked before calling Abort? It is pretty easy to reproduce if you create several https downloads and call abort on them at random times.

    Eg 1
    Ex [HTTPRequest]: SendOutTo - Message: The object was used after being disposed. StackTrace: at System.Net.Sockets.NetworkStream.CheckDisposed () [0x00000] in <filename unknown>:0
    at System.Net.Sockets.NetworkStream.EndWrite (IAsyncResult ar) [0x00000] in <filename unknown>:0
    at Mono.Security.Protocol.Tls.SslStreamBase.InternalWriteCallback (IAsyncResult ar) [0x00000] in <filename unknown>:0

    Eg 2
    Ex [HTTPRequest]: SendOutTo - Message: This operation is invalid until it is successfully authenticated StackTrace: at System.Net.Security.SslStream.CheckConnectionAuthenticated () [0x00000] in <filename unknown>:0
    at System.Net.Security.SslStream.BeginWrite (System.Byte[] buffer, Int32 offset, Int32 count, System.AsyncCallback asyncCallback, System.Object asyncState) [0x00000] in <filename unknown>:0
    at System.Net.Security.SslStream.Write (System.Byte[] buffer, Int32 offset, Int32 count) [0x00000] in <filename unknown>:0
    at System.IO.BinaryWriter.Write (System.Byte[] buffer) [0x0002c] in /Users/builduser/buildslave/mono-runtime-and-classlibs/build/mcs/class/corlib/System.IO/BinaryWriter.cs:130
    at BestHTTP.HTTPRequest.SendOutTo (System.IO.Stream stream) [0x0009c] in Assets\Plugins\Best HTTP (Pro)\BestHTTP\HTTPRequest.cs:1060

    Also I've seen connections get stuck in this state in the HTTPConnectionStates.AbortRequested state without ever being cleaned up, possibly because their thread which calls the abort is never started?

    Any recommendations would be great, thanks.
     
  28. BestHTTP

    BestHTTP

    Joined:
    Sep 11, 2013
    Posts:
    1,664
    @JMEA

    Logging of these exceptions was a mistake, they are inevitable. Removed this logging, and also fixed two bugs too. Stuck connections should gone now too.
    I will send a link to an updated package in private.
     
  29. insominx

    insominx

    Joined:
    Jan 23, 2012
    Posts:
    32
    Hi, I'm trying to set up unit and integration tests involving BestHTTP Pro and am having issues. At a minimum, I'd expect the integration test to work. Here's a very simple integration test:

    Code (CSharp):
    1.  
    2. using UnityEngine;
    3. using System.Collections;
    4. using System;
    5. using BestHTTP;
    6.  
    7. public class TestWebRequest : MonoBehaviour {
    8.  
    9.   const string mapDownloadUrl = "https://www.google.com";
    10.  
    11.   public bool Finished { get; set; }
    12.   public bool ReceivedResponse { get; set; }
    13.  
    14.   void Awake() {
    15.     BestHTTP.HTTPManager.Setup();
    16.   }
    17.  
    18.     void Start () {
    19.       Uri uri = new Uri(mapDownloadUrl);
    20.  
    21.       HTTPRequest request = new HTTPRequest(uri, OnRequestReceived);
    22.       request.Send();
    23.     }
    24.  
    25.   void OnRequestReceived(HTTPRequest req, HTTPResponse res) {
    26.     Finished = true;
    27.     ReceivedResponse = (res != null);
    28.  
    29.     Debug.Log("received response");
    30.   }
    31. }
    32.  
    Here's what my scene looks like:

    upload_2016-3-23_15-1-53.png

    What happens is that the callback from the WebRequest doesn't seem to return until the test times out (set at 10 seconds) which leaves this in the console:

    upload_2016-3-23_15-4-46.png

    Am I missing something?

    Also, here's a video showing the whole thing:

     

    Attached Files:

    Last edited: Mar 23, 2016
  30. insominx

    insominx

    Joined:
    Jan 23, 2012
    Posts:
    32
    Ok, nevermind all of that. The problem is how assertions are treated by default. Namely from this:

    upload_2016-3-23_16-43-44.png

    You have to actually check that box to "Succeed on assertions". Otherwise, it is expecting you to call

    Code (CSharp):
    1. IntegrationTest.Pass();
    somewhere in your code. The other thing to make this work is too make sure your timeout on the TestComponent is larger than the timeout on your AssertionComponent. Hope this is helpfull to someone else.
     
  31. tatata

    tatata

    Joined:
    Jul 11, 2013
    Posts:
    12
    Hi,

    I have a problem related to link.xml with Best HTTP 1.9.10.


    Build for iOS with:

    original link.xml and Api Compatibility Level .Net 2.0
    => Build succeeded

    original link.xml and Api Compatibility Level .Net 2.0 Subset
    => Build failed

    changed link.xml (below) and Api Compatibility Level .Net 2.0 Subset
    => Build succeeded


    changed link.xml:
    error when build failed:
    The used unity version is 5.3.1f1.

    Should I change link.xml if I use .Net 2.0 Subset?
    May there be a problem with subset? (eg. ExecutionEngineException by AOT)
     
  32. BestHTTP

    BestHTTP

    Joined:
    Sep 11, 2013
    Posts:
    1,664
    @tatata The plugin should work fine with subset too, but here is the previous link.xml that should work anyway:
     
  33. insominx

    insominx

    Joined:
    Jan 23, 2012
    Posts:
    32
    In case anyone was wondering how to use this with a unit test, I seem to have got that working too:

    Code (CSharp):
    1. [Test]
    2.   public void TestServerReachable() {
    3.  
    4.     BestHTTP.HTTPManager.Setup();
    5.     Thread.Sleep(10);
    6.  
    7.     bool finished = false;
    8.  
    9.     try {
    10.       Uri uri = new Uri("https://www.google.com");
    11.  
    12.       HTTPRequest request = new HTTPRequest(uri, (HTTPRequest req, HTTPResponse res) => {
    13.         finished = true;
    14.       });
    15.  
    16.       request.Send();
    17.  
    18.     } catch (Exception e) {
    19.       Debug.Log(e.ToString());
    20.     }
    21.  
    22.     // Wait for the http request and manually tick BestHTTP in order to receive the callback
    23.     while (!finished) {
    24.       BestHTTP.HTTPManager.OnUpdate();
    25.       Thread.Sleep(100);
    26.     }
    27.  
    28.     Assert.IsTrue(finished);
    29.   }
     
    BestHTTP likes this.
  34. tatata

    tatata

    Joined:
    Jul 11, 2013
    Posts:
    12
    Thanks.
    Built successfully with your previous link.xml.
     
  35. jGate99

    jGate99

    Joined:
    Oct 22, 2013
    Posts:
    1,943
    Hi
    Can i use BestHTTP Pro to load assets from Resources folders? I dont want to use Resources.LoadAsync as it require monobehaviour for coroutines.

    If so, how? a code example?
     
  36. BestHTTP

    BestHTTP

    Joined:
    Sep 11, 2013
    Posts:
    1,664
    @JohnGate No, you can't use for this kind of tasks.
     
  37. jGate99

    jGate99

    Joined:
    Oct 22, 2013
    Posts:
    1,943
    @BestHTTP can you please add it as a feature request?
     
  38. BestHTTP

    BestHTTP

    Joined:
    Sep 11, 2013
    Posts:
    1,664
    @JohnGate Added it to my todo list, but i can't promise anything or give an eta on it.
     
    jGate99 likes this.
  39. phpnato

    phpnato

    Joined:
    Aug 16, 2013
    Posts:
    14
    This error occurred after upgrading to version 1.9.10 (Basic)

    Internal TLS error, this could be an attack
    at Org.BouncyCastle.Crypto.Tls.TlsProtocol.FailWithError (Byte alertLevel, Byte alertDescription, System.String message, System.Exception cause) [0x00041] in d:\Projects\WWW_Pro_35\Assets\Best HTTP (Pro)\BestHTTP\SecureProtocol\crypto\tls\TlsProtocol.cs:624
    at Org.BouncyCastle.Crypto.Tls.TlsProtocol.SafeReadRecord () [0x00020] in d:\Projects\WWW_Pro_35\Assets\Best HTTP (Pro)\BestHTTP\SecureProtocol\crypto\tls\TlsProtocol.cs:475
    at Org.BouncyCastle.Crypto.Tls.TlsProtocol.CompleteHandshake () [0x0000b] in d:\Projects\WWW_Pro_35\Assets\Best HTTP (Pro)\BestHTTP\SecureProtocol\crypto\tls\TlsProtocol.cs:159
    at Org.BouncyCastle.Crypto.Tls.TlsClientProtocol.Connect (TlsClient tlsClient) [0x000d0] in d:\Projects\WWW_Pro_35\Assets\Best HTTP (Pro)\BestHTTP\SecureProtocol\crypto\tls\TlsClientProtocol.cs:76
    at BestHTTP.HTTPConnection.Connect () [0x0053f] in d:\Projects\WWW_Pro_35\Assets\Best HTTP (Pro)\BestHTTP\HTTPConnection.cs:595
    at BestHTTP.HTTPConnection.ThreadFunc (System.Object param) [0x00078] in d:\Projects\WWW_Pro_35\Assets\Best HTTP (Pro)\BestHTTP\HTTPConnection.cs:186
    UnityEngine.Debug:Log(Object)

    How to fix?
     
  40. BestHTTP

    BestHTTP

    Joined:
    Sep 11, 2013
    Posts:
    1,664
    @phpnato

    You can try to use the default SSL handler. You can place this somewhere in your startup code:
    Code (CSharp):
    1. BestHTTP.HTTPManager.UseAlternateSSLDefaultValue = false;
     
  41. jGate99

    jGate99

    Joined:
    Oct 22, 2013
    Posts:
    1,943
    @BestHTTP Getting following error whenever i try to compile for WebGL.
    `BestHTTP.HTTPRequest' does not contain a definition for `DisableCache' and no extension method `DisableCache' of type `BestHTTP.HTTPRequest' could be found
     
  42. BestHTTP

    BestHTTP

    Joined:
    Sep 11, 2013
    Posts:
    1,664
    @JohnGate
    To help save as many build size as we just can, code that have no effect on WebGL builds are not compiled into these builds.
    You can place it between compiler directives:
    Code (CSharp):
    1. #if !BESTHTTP_DISABLE_CACHING && (!UNITY_WEBGL || UNITY_EDITOR)
    2.         request.DisableCache = true;
    3. #endif
    4.  
     
    jGate99 likes this.
  43. jGate99

    jGate99

    Joined:
    Oct 22, 2013
    Posts:
    1,943
    Thanks a lot :)
     
  44. John Beiber

    John Beiber

    Joined:
    Mar 26, 2015
    Posts:
    1
    Thank you for such a good plug-in, I encountered a problem in use, I upload more than 100M of files, memory full, and ask how to handle this situation? Add, upload the first time there is no problem, but the memory will not be released, it is how to release the memory?
     
  45. BestHTTP

    BestHTTP

    Joined:
    Sep 11, 2013
    Posts:
    1,664
    @John Beiber

    Can you share how you are uploading your files and on what platform?
     
  46. Xtremedev

    Xtremedev

    Joined:
    Nov 6, 2015
    Posts:
    5
    I have a GameManager that initialises a NetworkManager that in turns makes calls to our servers and responds accordingly.

    What I need is to be able to successfully capture the responses and act upon them. My HTTPPost call is aligned to the recommendations in your doco. The issue I have is that the response time from the NetworkManager is misaligned to that of the GameManager and as such I never see the response.


    Code (CSharp):
    1. public static void HttpPost(string _host, string _restendpoint, string[] _paramName, string[] _paramValue)
    2.         {
    3.  
    4.         if (_paramName == null)
    5.         {
    6.             Debug.Log("NetworkManager.cs - HttpPost routine failed NO parameters passed");
    7.         }
    8.         else {
    9.             HTTPRequest request = new HTTPRequest(new Uri(_host + _restendpoint), HTTPMethods.Post, OnRequestFinished);
    10.  
    11.             for (int i = 0; i < _paramName.Length; i++)
    12.             {
    13.                 request.AddField(_paramName[i], _paramValue[i]);  
    14.             }
    15.             request.Send();        
    16.           }
    17.     }
    OnRequestFinished: presents the response and is sent to a JSON parser, the keyvalue pair should be returned to the GameManager

    What I need in the end is a response handler, where the results can be parsed and additional actions performed.
     
    Last edited: Apr 5, 2016
  47. BestHTTP

    BestHTTP

    Joined:
    Sep 11, 2013
    Posts:
    1,664
    @Xtremedev

    "The issue I have is that the response time from the NetworkManager is misaligned to that of the GameManager and as such I never see the response."

    Can you explain this in more detail?
     
  48. Xtremedev

    Xtremedev

    Joined:
    Nov 6, 2015
    Posts:
    5
    Hi BestHTTP

    Apologies it appears I was a victim of copy and paste. I'll summarise:

    GameManager.Initialise - Called.

    GetServerVersion called to request current server version from network, response is parsed via a JSON Parser, the resulting string "1.0.0" should be returned to variable _serverVersion


    Code (CSharp):
    1.  _serverVersion = NetworkManager.instance.GetServerVersion(host, endpoint, param, paramvalue);
    2.  
    GameManager script continues without waiting for http response
     
  49. BestHTTP

    BestHTTP

    Joined:
    Sep 11, 2013
    Posts:
    1,664
    @Xtremedev

    Thanks, I think I now understand it. What you want to achieve is to call a function, send out a request in this function, wait for the server answer, parse it, then return from this function with this parsed object.
    Well, it isn't impossible, but i wouldn't recommend it. It would block your main (Unity) thread for an undefined amount of time...

    Instead I would recommend to rewrite your NetworkManager's functions to support callbacks too. So, you would be able to pass a callback to the GetServerVersion function that will be called when the server response is returned.
    So it would look like something like this:
    NetworkManager.instance.GetServerVersion(host, endpoint, param, paramvalue, OnServerVersionReceived);
    void OnServerVersionReceived(string version) {
    _serverVersion = version;
    }
     
  50. Xtremedev

    Xtremedev

    Joined:
    Nov 6, 2015
    Posts:
    5
    Hi BestHTTP

    Thanks for the response. I'm coming to the end of a 36hour stint - and starting to forget my own name :) Your recommendation sounds like the way to go - thx