diff --git a/README.md b/README.md index 444376f..081b288 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,13 @@ Based on Simon Wittber's UnityWeb code (http://code.google.com/p/unityweb/). +# LICENSE + +UnityHTTP falls under the GPL due to its basis on Simon Wittber's UnityWeb code, +which is licensed under the GPL. + +You should be aware of this license and determine if it is acceptable for your project. + # About This is a TcpClient-based HTTP library for use in Unity. It should work in diff --git a/lib/Ionic.Zlib.CF.dll b/lib/Ionic.Zlib.CF.dll new file mode 100755 index 0000000..949f804 Binary files /dev/null and b/lib/Ionic.Zlib.CF.dll differ diff --git a/lib/Ionic.Zlib.dll b/lib/Ionic.Zlib.dll deleted file mode 100755 index 0afce7f..0000000 Binary files a/lib/Ionic.Zlib.dll and /dev/null differ diff --git a/src/CookieJar.cs b/src/CookieJar.cs index 8d6cc7a..d226e5c 100644 --- a/src/CookieJar.cs +++ b/src/CookieJar.cs @@ -4,7 +4,7 @@ // Based on node-cookiejar (https://github.com/bmeck/node-cookiejar) -namespace HTTP +namespace UnityHTTP { public class CookieAccessInfo { diff --git a/src/DiskCache.cs b/src/DiskCache.cs index 45ac010..f5357e3 100644 --- a/src/DiskCache.cs +++ b/src/DiskCache.cs @@ -2,9 +2,8 @@ using System.Collections; using System.IO; using System; -using HTTP; -namespace HTTP +namespace UnityHTTP { public class DiskCacheOperation { @@ -91,7 +90,7 @@ public DiskCacheOperation Fetch (Request request) IEnumerator DownloadAndSave (Request request, string filename, DiskCacheOperation handle) { var useCachedVersion = File.Exists(filename); - Action< HTTP.Request > callback = request.completedCallback; + Action< UnityHTTP.Request > callback = request.completedCallback; request.Send(); // will clear the completedCallback while (!request.isDone) yield return new WaitForEndOfFrame (); diff --git a/src/FormDataStream.cs b/src/FormDataStream.cs new file mode 100644 index 0000000..02b44ca --- /dev/null +++ b/src/FormDataStream.cs @@ -0,0 +1,168 @@ +using System; +using System.IO; +using System.Text; +using System.Collections.Generic; +namespace UnityHTTP { + + public class FormPart { + byte[] header; + Stream contents; + int position = 0; + + public FormPart(string fieldName, string mimeType, string boundary, Stream contents, string fileName=null){ + string filenameheader = ""; + if (fileName != null){ + filenameheader = "; filename=\"" + fileName +"\""; + } + header = Encoding.ASCII.GetBytes( + "\r\n--" + boundary + "\r\n" + + "Content-Type: " + mimeType + "\r\n" + + "Content-disposition: form-data; name=\"" + fieldName + "\"" + filenameheader + "\r\n\r\n" + ); + this.contents = contents; + } + public long Length { + get { + return header.Length + contents.Length; + } + } + public int Read(byte[] buffer, int offset, int size){ + int writed = 0; + int bytesToWrite; + if (position < header.Length){ + bytesToWrite = (int)(header.Length - position) > size ? size : (int)(header.Length - position); + Array.Copy ( + header, // from header + position, // started from position + buffer, // to buffer + offset, // started with offset + bytesToWrite + ); + writed += bytesToWrite; + position += bytesToWrite; + } + if (writed >= size){ + return writed; + } + bytesToWrite = contents.Read(buffer, writed + offset, size - writed); + writed += bytesToWrite; + position += bytesToWrite; + return writed; + } + + public void Dispose(){ + header = null; + contents.Close(); + } + } + + public class FormDataStream: Stream { + long position = 0; + List parts = new List(); + bool dirty = false; + byte[] footer; + string boundary; + + public FormDataStream(string boundary){ + this.boundary = boundary; + footer = Encoding.ASCII.GetBytes("\r\n--" + boundary + "--\r\n"); + } + + public override bool CanRead { get { return true; } } + public override bool CanSeek { get { return false; } } + public override bool CanTimeout { get { return false; } } + public override bool CanWrite { get { return false; } } + public override int ReadTimeout { get { return 0; } set { } } + public override int WriteTimeout { get { return 0; } set { } } + public override long Position { + get { + return position; + } + set { + throw new NotImplementedException("FormDataStream is non-seekable stream"); + } + } + public override long Length { + get { + if (parts.Count == 0){ + return 0; + } + dirty = true; + long len = 0; + foreach (var part in parts){ + len += part.Length; + } + return len + footer.Length; + } + } + + public override void Flush(){ + throw new NotImplementedException("FormDataStream is readonly stream"); + } + + public override int Read(byte[] buffer, int offset, int count){ + dirty = true; + int writed = 0; + int bytesToWrite = 0; + + // write parts + long partsSize = 0; + foreach (var part in parts){ + partsSize += part.Length; + if (position > partsSize){ + continue; + } + bytesToWrite = part.Read(buffer, writed + offset, count - writed); + writed += bytesToWrite; + position += bytesToWrite; + if (writed >= count){ + return count; + } + } + + + // write footer + bytesToWrite = count - writed > footer.Length? footer.Length : count - writed; + Array.Copy (footer, 0, buffer, writed + offset, bytesToWrite); + position += bytesToWrite; + writed += bytesToWrite; + return writed; + } + + public override long Seek(long amount, SeekOrigin origin){ + throw new NotImplementedException("FormDataStream is non-seekable stream"); + } + + public override void SetLength (long len){ + throw new NotImplementedException("FormDataStream is readonly stream"); + } + + public override void Write(byte[] source, int offset, int count){ + throw new NotImplementedException("FormDataStream is readonly stream"); + } + + public void AddPart(string fieldName, string mimeType, Stream contents, string fileName=null){ + if (dirty){ + throw new InvalidOperationException("You can't change form data, form already readed"); + } + parts.Add(new FormPart(fieldName, mimeType, boundary, contents, fileName)); + } + + public void AddPart(FormPart part){ + if (dirty){ + throw new InvalidOperationException("You can't change form data, form already readed"); + } + parts.Add(part); + } + + public override void Close(){ + foreach (var part in parts){ + part.Dispose(); + } + base.Close(); + } + } + +} + + diff --git a/src/Logger.cs b/src/Logger.cs new file mode 100644 index 0000000..d466a6f --- /dev/null +++ b/src/Logger.cs @@ -0,0 +1,71 @@ +using System; + +namespace UnityHTTP +{ + public interface ILogger + { + void Log(string msg); + void LogError(string msg); + void LogException(Exception e); + void LogWarning(string msg); + } +#if UNITY_EDITOR + public class UnityLogger : ILogger + { + public void Log(string msg) + { + UnityEngine.Debug.Log(msg); + } + public void LogError(string msg) + { + UnityEngine.Debug.LogError(msg); + } + public void LogException(Exception e) + { + UnityEngine.Debug.LogException(e); + } + public void LogWarning(string msg) + { + UnityEngine.Debug.LogWarning(msg); + } + } +#endif + public class ConsoleLogger : ILogger + { + public void Log(string msg) + { + Console.WriteLine(msg); + } + public void LogError(string msg) + { + Console.WriteLine(msg); + } + public void LogException(Exception e) + { + Console.WriteLine(e); + } + public void LogWarning(string msg) + { + Console.WriteLine(msg); + } + } + public class DiscardLogger : ILogger + { + public void Log(string msg) + { + // discard logs + } + public void LogError(string msg) + { + // discard logs + } + public void LogException(Exception e) + { + // discard logs + } + public void LogWarning(string msg) + { + // discard logs + } + } +} \ No newline at end of file diff --git a/src/Request.cs b/src/Request.cs index c9b358d..429a52d 100644 --- a/src/Request.cs +++ b/src/Request.cs @@ -11,7 +11,7 @@ using System.Security.Authentication; using System.Security.Cryptography.X509Certificates; -namespace HTTP +namespace UnityHTTP { public class HTTPException : Exception { @@ -26,15 +26,23 @@ public enum RequestState { public class Request { + public static bool LogAllRequests = false; public static bool VerboseLogging = false; public static string unityVersion = Application.unityVersion; - public static string operatingSystem = SystemInfo.operatingSystem; + public static string operatingSystem = SystemInfo.operatingSystem; + public static ILogger Logger = +#if !UNITY_EDITOR + new ConsoleLogger() +#else + new UnityLogger() +#endif + ; public CookieJar cookieJar = CookieJar.Instance; public string method = "GET"; public string protocol = "HTTP/1.1"; - public byte[] bytes; + public Stream byteStream; public Uri uri; public static byte[] EOL = { (byte)'\r', (byte)'\n' }; public Response response = null; @@ -46,8 +54,11 @@ public class Request public RequestState state = RequestState.Waiting; public long responseTime = 0; // in milliseconds public bool synchronous = false; + public int bufferSize = 4 * 1024; + + public ILogger logger = Request.Logger; - public Action< HTTP.Request > completedCallback = null; + public Action< UnityHTTP.Request > completedCallback = null; Dictionary> headers = new Dictionary> (); static Dictionary etags = new Dictionary (); @@ -69,25 +80,42 @@ public Request (string method, string uri, byte[] bytes) { this.method = method; this.uri = new Uri (uri); - this.bytes = bytes; + this.byteStream = new MemoryStream(bytes); + } + + public Request(string method, string uri, StreamedWWWForm form){ + this.method = method; + this.uri = new Uri (uri); + this.byteStream = form.stream; + foreach ( DictionaryEntry entry in form.headers ) + { + this.AddHeader( (string)entry.Key, (string)entry.Value ); + } } public Request( string method, string uri, WWWForm form ) { this.method = method; this.uri = new Uri (uri); - this.bytes = form.data; + this.byteStream = new MemoryStream(form.data); +#if UNITY_5 || UNITY_5_3_OR_NEWER + foreach ( var entry in form.headers ) + { + this.AddHeader( entry.Key, entry.Value ); + } +#else foreach ( DictionaryEntry entry in form.headers ) { this.AddHeader( (string)entry.Key, (string)entry.Value ); } +#endif } public Request( string method, string uri, Hashtable data ) { this.method = method; this.uri = new Uri( uri ); - this.bytes = Encoding.UTF8.GetBytes( JSON.JsonEncode( data ) ); + this.byteStream = new MemoryStream(Encoding.UTF8.GetBytes( JSON.JsonEncode( data ) )); this.AddHeader( "Content-Type", "application/json" ); } @@ -165,7 +193,8 @@ private void GetResponse() { var ssl = ostream as SslStream; ssl.AuthenticateAsClient (uri.Host); } catch (Exception e) { - Debug.LogError ("Exception: " + e.Message); + logger.LogError ("SSL authentication failed."); + logger.LogException(e); return; } } @@ -195,15 +224,21 @@ private void GetResponse() { } } catch (Exception e) { - Console.WriteLine ("Unhandled Exception, aborting request."); - Console.WriteLine (e); + logger.LogError("Unhandled Exception, aborting request."); + logger.LogException(e); exception = e; response = null; } + state = RequestState.Done; isDone = true; responseTime = curcall.ElapsedMilliseconds; + if ( byteStream != null ) + { + byteStream.Close(); + } + if ( completedCallback != null ) { if (synchronous) { @@ -217,25 +252,25 @@ private void GetResponse() { if ( LogAllRequests ) { #if !UNITY_EDITOR - System.Console.WriteLine("NET: " + InfoString( VerboseLogging )); + logger.Log("NET: " + InfoString( VerboseLogging )); #else if ( response != null && response.status >= 200 && response.status < 300 ) { - Debug.Log( InfoString( VerboseLogging ) ); + logger.Log( InfoString( VerboseLogging ) ); } else if ( response != null && response.status >= 400 ) { - Debug.LogError( InfoString( VerboseLogging ) ); + logger.LogError( InfoString( VerboseLogging ) ); } else { - Debug.LogWarning( InfoString( VerboseLogging ) ); + logger.LogWarning( InfoString( VerboseLogging ) ); } #endif } } - public virtual void Send( Action< HTTP.Request > callback = null) + public virtual void Send( Action< UnityHTTP.Request > callback = null) { if (!synchronous && callback != null && ResponseCallbackDispatcher.Singleton == null ) @@ -267,8 +302,8 @@ public virtual void Send( Action< HTTP.Request > callback = null) SetHeader( "cookie", cookieString ); } - if ( bytes != null && bytes.Length > 0 && GetHeader ("Content-Length") == "" ) { - SetHeader( "Content-Length", bytes.Length.ToString() ); + if ( byteStream != null && byteStream.Length > 0 && GetHeader ("Content-Length") == "" ) { + SetHeader( "Content-Length", byteStream.Length.ToString() ); } if ( GetHeader( "User-Agent" ) == "" ) { @@ -298,15 +333,15 @@ public virtual void Send( Action< HTTP.Request > callback = null) } public string Text { - set { bytes = System.Text.Encoding.UTF8.GetBytes (value); } + set { byteStream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes (value)); } } public static bool ValidateServerCertificate (object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) { #if !UNITY_EDITOR - System.Console.WriteLine( "NET: SSL Cert: " + sslPolicyErrors.ToString() ); + Logger.LogWarning( "NET: SSL Cert: " + sslPolicyErrors.ToString() ); #else - Debug.LogWarning("SSL Cert Error: " + sslPolicyErrors.ToString ()); + Logger.LogWarning("SSL Cert Error: " + sslPolicyErrors.ToString ()); #endif return true; } @@ -328,8 +363,16 @@ void WriteToStream( Stream outputStream ) stream.Write( EOL ); - if ( bytes != null && bytes.Length > 0 ) { - stream.Write( bytes ); + if (byteStream == null){ + return; + } + + long numBytesToRead = byteStream.Length; + byte[] buffer = new byte[bufferSize]; + while (numBytesToRead > 0){ + int readed = byteStream.Read(buffer, 0, bufferSize); + stream.Write(buffer, 0, readed); + numBytesToRead -= readed; } } diff --git a/src/Response.cs b/src/Response.cs index 7a1a9d7..ac48e70 100644 --- a/src/Response.cs +++ b/src/Response.cs @@ -6,7 +6,7 @@ using System.Globalization; using Ionic.Zlib; -namespace HTTP +namespace UnityHTTP { public class Response { @@ -210,7 +210,7 @@ public void ReadFromStream( Stream inputStream ) AddHeader( parts[0], parts[1] ); } - } else { + } else if (request.method.ToUpper() != "HEAD") { // Read Body int contentLength = 0; diff --git a/src/ResponseCallbackDispatcher.cs b/src/ResponseCallbackDispatcher.cs index f87c31c..d49a31e 100644 --- a/src/ResponseCallbackDispatcher.cs +++ b/src/ResponseCallbackDispatcher.cs @@ -2,7 +2,7 @@ using System; using System.Collections; -namespace HTTP +namespace UnityHTTP { public class ResponseCallbackDispatcher : MonoBehaviour { @@ -33,6 +33,7 @@ public static void Init() } singletonGameObject = new GameObject(); + GameObject.DontDestroyOnLoad(singletonGameObject); singleton = singletonGameObject.AddComponent< ResponseCallbackDispatcher >(); singletonGameObject.name = "HTTPResponseCallbackDispatcher"; } @@ -42,7 +43,7 @@ public void Update() { while( requests.Count > 0 ) { - HTTP.Request request = (Request)requests.Dequeue(); + UnityHTTP.Request request = (Request)requests.Dequeue(); request.completedCallback( request ); } } diff --git a/src/StreamedWWWForm.cs b/src/StreamedWWWForm.cs new file mode 100644 index 0000000..0510e48 --- /dev/null +++ b/src/StreamedWWWForm.cs @@ -0,0 +1,62 @@ +using System; +using System.IO; +using System.Text; +using System.Collections; + +namespace UnityHTTP +{ + public class StreamedWWWForm { + string boundary; + public FormDataStream stream; + + public Hashtable headers { + get { + return new Hashtable { + { "Content-Type", "multipart/form-data; boundary=\"" + boundary + "\""} + }; + } + } + + public StreamedWWWForm(){ + byte[] bytes = new byte[40]; + var random = new Random(); + for (int i=0; i<40; i++){ + bytes[i] = (byte)(48 + random.Next(62)); + if (bytes[i] > 57){ + bytes[i] += 7; + } + if (bytes[i] > 90){ + bytes[i] += 6; + } + } + boundary = Encoding.ASCII.GetString(bytes); + stream = new FormDataStream(boundary); + } + + public void AddField(string fieldName, string fieldValue){ + var contentStream = new MemoryStream(Encoding.UTF8.GetBytes(fieldValue)); + stream.AddPart(fieldName, "text/plain; charset=\"utf-8\"", contentStream); + + } + public void AddBinaryData(string fieldName, byte[] contents=null, string mimeType = null){ + var contentStream = new MemoryStream(contents); + if (mimeType == null){ + mimeType = "application/octet-stream"; + } + stream.AddPart(fieldName, mimeType, contentStream, fieldName + ".dat"); + } + public void AddBinaryData(string fieldName, Stream contents=null, string mimeType = null){ + if (mimeType == null){ + mimeType = "application/octet-stream"; + } + stream.AddPart(fieldName, mimeType, contents, fieldName + ".dat"); + } + public void AddFile(string fieldName, string path, string mimeType=null){ + if (mimeType == null){ + mimeType = "application/octet-stream"; + } + var contents = new FileInfo(path).Open(FileMode.Open); + stream.AddPart(fieldName, mimeType, contents, fieldName + ".dat"); + } + } +}