aboutsummaryrefslogtreecommitdiff
path: root/MediaBrowser.Common/Net
diff options
context:
space:
mode:
Diffstat (limited to 'MediaBrowser.Common/Net')
-rw-r--r--MediaBrowser.Common/Net/Handlers/BaseEmbeddedResourceHandler.cs23
-rw-r--r--MediaBrowser.Common/Net/Handlers/BaseHandler.cs430
-rw-r--r--MediaBrowser.Common/Net/Handlers/BaseSerializationHandler.cs90
-rw-r--r--MediaBrowser.Common/Net/Handlers/StaticFileHandler.cs249
-rw-r--r--MediaBrowser.Common/Net/HttpServer.cs40
-rw-r--r--MediaBrowser.Common/Net/MimeTypes.cs160
-rw-r--r--MediaBrowser.Common/Net/Request.cs18
7 files changed, 1010 insertions, 0 deletions
diff --git a/MediaBrowser.Common/Net/Handlers/BaseEmbeddedResourceHandler.cs b/MediaBrowser.Common/Net/Handlers/BaseEmbeddedResourceHandler.cs
new file mode 100644
index 000000000..579e341fe
--- /dev/null
+++ b/MediaBrowser.Common/Net/Handlers/BaseEmbeddedResourceHandler.cs
@@ -0,0 +1,23 @@
+using System.IO;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Common.Net.Handlers
+{
+ public abstract class BaseEmbeddedResourceHandler : BaseHandler
+ {
+ protected BaseEmbeddedResourceHandler(string resourcePath)
+ : base()
+ {
+ ResourcePath = resourcePath;
+ }
+
+ protected string ResourcePath { get; set; }
+
+ protected override Task WriteResponseToOutputStream(Stream stream)
+ {
+ return GetEmbeddedResourceStream().CopyToAsync(stream);
+ }
+
+ protected abstract Stream GetEmbeddedResourceStream();
+ }
+}
diff --git a/MediaBrowser.Common/Net/Handlers/BaseHandler.cs b/MediaBrowser.Common/Net/Handlers/BaseHandler.cs
new file mode 100644
index 000000000..a5058e6ca
--- /dev/null
+++ b/MediaBrowser.Common/Net/Handlers/BaseHandler.cs
@@ -0,0 +1,430 @@
+using MediaBrowser.Common.Logging;
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.IO;
+using System.IO.Compression;
+using System.Linq;
+using System.Net;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Common.Net.Handlers
+{
+ public abstract class BaseHandler
+ {
+ public abstract bool HandlesRequest(HttpListenerRequest request);
+
+ private Stream CompressedStream { get; set; }
+
+ public virtual bool? UseChunkedEncoding
+ {
+ get
+ {
+ return null;
+ }
+ }
+
+ private bool _totalContentLengthDiscovered;
+ private long? _totalContentLength;
+ public long? TotalContentLength
+ {
+ get
+ {
+ if (!_totalContentLengthDiscovered)
+ {
+ _totalContentLength = GetTotalContentLength();
+ _totalContentLengthDiscovered = true;
+ }
+
+ return _totalContentLength;
+ }
+ }
+
+ protected virtual bool SupportsByteRangeRequests
+ {
+ get
+ {
+ return false;
+ }
+ }
+
+ /// <summary>
+ /// The original HttpListenerContext
+ /// </summary>
+ protected HttpListenerContext HttpListenerContext { get; set; }
+
+ /// <summary>
+ /// The original QueryString
+ /// </summary>
+ protected NameValueCollection QueryString
+ {
+ get
+ {
+ return HttpListenerContext.Request.QueryString;
+ }
+ }
+
+ private List<KeyValuePair<long, long?>> _requestedRanges;
+ protected IEnumerable<KeyValuePair<long, long?>> RequestedRanges
+ {
+ get
+ {
+ if (_requestedRanges == null)
+ {
+ _requestedRanges = new List<KeyValuePair<long, long?>>();
+
+ if (IsRangeRequest)
+ {
+ // Example: bytes=0-,32-63
+ string[] ranges = HttpListenerContext.Request.Headers["Range"].Split('=')[1].Split(',');
+
+ foreach (string range in ranges)
+ {
+ string[] vals = range.Split('-');
+
+ long start = 0;
+ long? end = null;
+
+ if (!string.IsNullOrEmpty(vals[0]))
+ {
+ start = long.Parse(vals[0]);
+ }
+ if (!string.IsNullOrEmpty(vals[1]))
+ {
+ end = long.Parse(vals[1]);
+ }
+
+ _requestedRanges.Add(new KeyValuePair<long, long?>(start, end));
+ }
+ }
+ }
+
+ return _requestedRanges;
+ }
+ }
+
+ protected bool IsRangeRequest
+ {
+ get
+ {
+ return HttpListenerContext.Request.Headers.AllKeys.Contains("Range");
+ }
+ }
+
+ private bool ClientSupportsCompression
+ {
+ get
+ {
+ string enc = HttpListenerContext.Request.Headers["Accept-Encoding"] ?? string.Empty;
+
+ return enc.IndexOf("deflate", StringComparison.OrdinalIgnoreCase) != -1 || enc.IndexOf("gzip", StringComparison.OrdinalIgnoreCase) != -1;
+ }
+ }
+
+ private string CompressionMethod
+ {
+ get
+ {
+ string enc = HttpListenerContext.Request.Headers["Accept-Encoding"] ?? string.Empty;
+
+ if (enc.IndexOf("deflate", StringComparison.OrdinalIgnoreCase) != -1)
+ {
+ return "deflate";
+ }
+ if (enc.IndexOf("gzip", StringComparison.OrdinalIgnoreCase) != -1)
+ {
+ return "gzip";
+ }
+
+ return null;
+ }
+ }
+
+ public virtual async Task ProcessRequest(HttpListenerContext ctx)
+ {
+ HttpListenerContext = ctx;
+
+ string url = ctx.Request.Url.ToString();
+ Logger.LogInfo("Http Server received request at: " + url);
+ Logger.LogInfo("Http Headers: " + string.Join(",", ctx.Request.Headers.AllKeys.Select(k => k + "=" + ctx.Request.Headers[k])));
+
+ ctx.Response.AddHeader("Access-Control-Allow-Origin", "*");
+
+ ctx.Response.KeepAlive = true;
+
+ try
+ {
+ if (SupportsByteRangeRequests && IsRangeRequest)
+ {
+ ctx.Response.Headers["Accept-Ranges"] = "bytes";
+ }
+
+ ResponseInfo responseInfo = await GetResponseInfo().ConfigureAwait(false);
+
+ if (responseInfo.IsResponseValid)
+ {
+ // Set the initial status code
+ // When serving a range request, we need to return status code 206 to indicate a partial response body
+ responseInfo.StatusCode = SupportsByteRangeRequests && IsRangeRequest ? 206 : 200;
+ }
+
+ ctx.Response.ContentType = responseInfo.ContentType;
+
+ if (!string.IsNullOrEmpty(responseInfo.Etag))
+ {
+ ctx.Response.Headers["ETag"] = responseInfo.Etag;
+ }
+
+ if (ctx.Request.Headers.AllKeys.Contains("If-Modified-Since"))
+ {
+ DateTime ifModifiedSince;
+
+ if (DateTime.TryParse(ctx.Request.Headers["If-Modified-Since"], out ifModifiedSince))
+ {
+ // If the cache hasn't expired yet just return a 304
+ if (IsCacheValid(ifModifiedSince.ToUniversalTime(), responseInfo.CacheDuration, responseInfo.DateLastModified))
+ {
+ // ETag must also match (if supplied)
+ if ((responseInfo.Etag ?? string.Empty).Equals(ctx.Request.Headers["If-None-Match"] ?? string.Empty))
+ {
+ responseInfo.StatusCode = 304;
+ }
+ }
+ }
+ }
+
+ Logger.LogInfo("Responding with status code {0} for url {1}", responseInfo.StatusCode, url);
+
+ if (responseInfo.IsResponseValid)
+ {
+ await ProcessUncachedRequest(ctx, responseInfo).ConfigureAwait(false);
+ }
+ else
+ {
+ ctx.Response.StatusCode = responseInfo.StatusCode;
+ ctx.Response.SendChunked = false;
+ }
+ }
+ catch (Exception ex)
+ {
+ // It might be too late if some response data has already been transmitted, but try to set this
+ ctx.Response.StatusCode = 500;
+
+ Logger.LogException(ex);
+ }
+ finally
+ {
+ DisposeResponseStream();
+ }
+ }
+
+ private async Task ProcessUncachedRequest(HttpListenerContext ctx, ResponseInfo responseInfo)
+ {
+ long? totalContentLength = TotalContentLength;
+
+ // By default, use chunked encoding if we don't know the content length
+ bool useChunkedEncoding = UseChunkedEncoding == null ? (totalContentLength == null) : UseChunkedEncoding.Value;
+
+ // Don't force this to true. HttpListener will default it to true if supported by the client.
+ if (!useChunkedEncoding)
+ {
+ ctx.Response.SendChunked = false;
+ }
+
+ // Set the content length, if we know it
+ if (totalContentLength.HasValue)
+ {
+ ctx.Response.ContentLength64 = totalContentLength.Value;
+ }
+
+ var compressResponse = responseInfo.CompressResponse && ClientSupportsCompression;
+
+ // Add the compression header
+ if (compressResponse)
+ {
+ ctx.Response.AddHeader("Content-Encoding", CompressionMethod);
+ }
+
+ if (responseInfo.DateLastModified.HasValue)
+ {
+ ctx.Response.Headers[HttpResponseHeader.LastModified] = responseInfo.DateLastModified.Value.ToString("r");
+ }
+
+ // Add caching headers
+ if (responseInfo.CacheDuration.Ticks > 0)
+ {
+ CacheResponse(ctx.Response, responseInfo.CacheDuration);
+ }
+
+ // Set the status code
+ ctx.Response.StatusCode = responseInfo.StatusCode;
+
+ if (responseInfo.IsResponseValid)
+ {
+ // Finally, write the response data
+ Stream outputStream = ctx.Response.OutputStream;
+
+ if (compressResponse)
+ {
+ if (CompressionMethod.Equals("deflate", StringComparison.OrdinalIgnoreCase))
+ {
+ CompressedStream = new DeflateStream(outputStream, CompressionLevel.Fastest, false);
+ }
+ else
+ {
+ CompressedStream = new GZipStream(outputStream, CompressionLevel.Fastest, false);
+ }
+
+ outputStream = CompressedStream;
+ }
+
+ await WriteResponseToOutputStream(outputStream).ConfigureAwait(false);
+ }
+ else
+ {
+ ctx.Response.SendChunked = false;
+ }
+ }
+
+ private void CacheResponse(HttpListenerResponse response, TimeSpan duration)
+ {
+ response.Headers[HttpResponseHeader.CacheControl] = "public, max-age=" + Convert.ToInt32(duration.TotalSeconds);
+ response.Headers[HttpResponseHeader.Expires] = DateTime.UtcNow.Add(duration).ToString("r");
+ }
+
+ protected abstract Task WriteResponseToOutputStream(Stream stream);
+
+ protected virtual void DisposeResponseStream()
+ {
+ if (CompressedStream != null)
+ {
+ CompressedStream.Dispose();
+ }
+
+ HttpListenerContext.Response.OutputStream.Dispose();
+ }
+
+ private bool IsCacheValid(DateTime ifModifiedSince, TimeSpan cacheDuration, DateTime? dateModified)
+ {
+ if (dateModified.HasValue)
+ {
+ DateTime lastModified = NormalizeDateForComparison(dateModified.Value);
+ ifModifiedSince = NormalizeDateForComparison(ifModifiedSince);
+
+ return lastModified <= ifModifiedSince;
+ }
+
+ DateTime cacheExpirationDate = ifModifiedSince.Add(cacheDuration);
+
+ if (DateTime.UtcNow < cacheExpirationDate)
+ {
+ return true;
+ }
+
+ return false;
+ }
+
+ /// <summary>
+ /// When the browser sends the IfModifiedDate, it's precision is limited to seconds, so this will account for that
+ /// </summary>
+ private DateTime NormalizeDateForComparison(DateTime date)
+ {
+ return new DateTime(date.Year, date.Month, date.Day, date.Hour, date.Minute, date.Second, date.Kind);
+ }
+
+ protected virtual long? GetTotalContentLength()
+ {
+ return null;
+ }
+
+ protected abstract Task<ResponseInfo> GetResponseInfo();
+
+ private Hashtable _formValues;
+
+ /// <summary>
+ /// Gets a value from form POST data
+ /// </summary>
+ protected async Task<string> GetFormValue(string name)
+ {
+ if (_formValues == null)
+ {
+ _formValues = await GetFormValues(HttpListenerContext.Request).ConfigureAwait(false);
+ }
+
+ if (_formValues.ContainsKey(name))
+ {
+ return _formValues[name].ToString();
+ }
+
+ return null;
+ }
+
+ /// <summary>
+ /// Extracts form POST data from a request
+ /// </summary>
+ private async Task<Hashtable> GetFormValues(HttpListenerRequest request)
+ {
+ var formVars = new Hashtable();
+
+ if (request.HasEntityBody)
+ {
+ if (request.ContentType.IndexOf("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase) != -1)
+ {
+ using (Stream requestBody = request.InputStream)
+ {
+ using (var reader = new StreamReader(requestBody, request.ContentEncoding))
+ {
+ string s = await reader.ReadToEndAsync().ConfigureAwait(false);
+
+ string[] pairs = s.Split('&');
+
+ for (int x = 0; x < pairs.Length; x++)
+ {
+ string pair = pairs[x];
+
+ int index = pair.IndexOf('=');
+
+ if (index != -1)
+ {
+ string name = pair.Substring(0, index);
+ string value = pair.Substring(index + 1);
+ formVars.Add(name, value);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ return formVars;
+ }
+ }
+
+ public class ResponseInfo
+ {
+ public string ContentType { get; set; }
+ public string Etag { get; set; }
+ public DateTime? DateLastModified { get; set; }
+ public TimeSpan CacheDuration { get; set; }
+ public bool CompressResponse { get; set; }
+ public int StatusCode { get; set; }
+
+ public ResponseInfo()
+ {
+ CacheDuration = TimeSpan.FromTicks(0);
+
+ CompressResponse = true;
+
+ StatusCode = 200;
+ }
+
+ public bool IsResponseValid
+ {
+ get
+ {
+ return StatusCode == 200 || StatusCode == 206;
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Common/Net/Handlers/BaseSerializationHandler.cs b/MediaBrowser.Common/Net/Handlers/BaseSerializationHandler.cs
new file mode 100644
index 000000000..53b3ee817
--- /dev/null
+++ b/MediaBrowser.Common/Net/Handlers/BaseSerializationHandler.cs
@@ -0,0 +1,90 @@
+using MediaBrowser.Common.Serialization;
+using System;
+using System.IO;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Common.Net.Handlers
+{
+ public abstract class BaseSerializationHandler<T> : BaseHandler
+ where T : class
+ {
+ public SerializationFormat SerializationFormat
+ {
+ get
+ {
+ string format = QueryString["dataformat"];
+
+ if (string.IsNullOrEmpty(format))
+ {
+ return SerializationFormat.Json;
+ }
+
+ return (SerializationFormat)Enum.Parse(typeof(SerializationFormat), format, true);
+ }
+ }
+
+ protected string ContentType
+ {
+ get
+ {
+ switch (SerializationFormat)
+ {
+ case SerializationFormat.Jsv:
+ return "text/plain";
+ case SerializationFormat.Protobuf:
+ return "application/x-protobuf";
+ default:
+ return MimeTypes.JsonMimeType;
+ }
+ }
+ }
+
+ protected override async Task<ResponseInfo> GetResponseInfo()
+ {
+ ResponseInfo info = new ResponseInfo
+ {
+ ContentType = ContentType
+ };
+
+ _objectToSerialize = await GetObjectToSerialize().ConfigureAwait(false);
+
+ if (_objectToSerialize == null)
+ {
+ info.StatusCode = 404;
+ }
+
+ return info;
+ }
+
+ private T _objectToSerialize;
+
+ protected abstract Task<T> GetObjectToSerialize();
+
+ protected override Task WriteResponseToOutputStream(Stream stream)
+ {
+ return Task.Run(() =>
+ {
+ switch (SerializationFormat)
+ {
+ case SerializationFormat.Jsv:
+ JsvSerializer.SerializeToStream(_objectToSerialize, stream);
+ break;
+ case SerializationFormat.Protobuf:
+ ProtobufSerializer.SerializeToStream(_objectToSerialize, stream);
+ break;
+ default:
+ JsonSerializer.SerializeToStream(_objectToSerialize, stream);
+ break;
+ }
+ });
+ }
+ }
+
+ public enum SerializationFormat
+ {
+ Json,
+ Jsv,
+ Protobuf
+ }
+
+}
diff --git a/MediaBrowser.Common/Net/Handlers/StaticFileHandler.cs b/MediaBrowser.Common/Net/Handlers/StaticFileHandler.cs
new file mode 100644
index 000000000..11438b164
--- /dev/null
+++ b/MediaBrowser.Common/Net/Handlers/StaticFileHandler.cs
@@ -0,0 +1,249 @@
+using MediaBrowser.Common.Logging;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Net;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Common.Net.Handlers
+{
+ public class StaticFileHandler : BaseHandler
+ {
+ public override bool HandlesRequest(HttpListenerRequest request)
+ {
+ return false;
+ }
+
+ private string _path;
+ public virtual string Path
+ {
+ get
+ {
+ if (!string.IsNullOrWhiteSpace(_path))
+ {
+ return _path;
+ }
+
+ return QueryString["path"];
+ }
+ set
+ {
+ _path = value;
+ }
+ }
+
+ private Stream SourceStream { get; set; }
+
+ protected override bool SupportsByteRangeRequests
+ {
+ get
+ {
+ return true;
+ }
+ }
+
+ private bool ShouldCompressResponse(string contentType)
+ {
+ // Can't compress these
+ if (IsRangeRequest)
+ {
+ return false;
+ }
+
+ // Don't compress media
+ if (contentType.StartsWith("audio/", StringComparison.OrdinalIgnoreCase) || contentType.StartsWith("video/", StringComparison.OrdinalIgnoreCase))
+ {
+ return false;
+ }
+
+ // It will take some work to support compression within this handler
+ return false;
+ }
+
+ protected override long? GetTotalContentLength()
+ {
+ return SourceStream.Length;
+ }
+
+ protected override Task<ResponseInfo> GetResponseInfo()
+ {
+ ResponseInfo info = new ResponseInfo
+ {
+ ContentType = MimeTypes.GetMimeType(Path),
+ };
+
+ try
+ {
+ SourceStream = File.OpenRead(Path);
+ }
+ catch (FileNotFoundException ex)
+ {
+ info.StatusCode = 404;
+ Logger.LogException(ex);
+ }
+ catch (DirectoryNotFoundException ex)
+ {
+ info.StatusCode = 404;
+ Logger.LogException(ex);
+ }
+ catch (UnauthorizedAccessException ex)
+ {
+ info.StatusCode = 403;
+ Logger.LogException(ex);
+ }
+
+ info.CompressResponse = ShouldCompressResponse(info.ContentType);
+
+ if (SourceStream != null)
+ {
+ info.DateLastModified = File.GetLastWriteTimeUtc(Path);
+ }
+
+ return Task.FromResult<ResponseInfo>(info);
+ }
+
+ protected override Task WriteResponseToOutputStream(Stream stream)
+ {
+ if (IsRangeRequest)
+ {
+ KeyValuePair<long, long?> requestedRange = RequestedRanges.First();
+
+ // If the requested range is "0-" and we know the total length, we can optimize by avoiding having to buffer the content into memory
+ if (requestedRange.Value == null && TotalContentLength != null)
+ {
+ return ServeCompleteRangeRequest(requestedRange, stream);
+ }
+ if (TotalContentLength.HasValue)
+ {
+ // This will have to buffer a portion of the content into memory
+ return ServePartialRangeRequestWithKnownTotalContentLength(requestedRange, stream);
+ }
+
+ // This will have to buffer the entire content into memory
+ return ServePartialRangeRequestWithUnknownTotalContentLength(requestedRange, stream);
+ }
+
+ return SourceStream.CopyToAsync(stream);
+ }
+
+ protected override void DisposeResponseStream()
+ {
+ base.DisposeResponseStream();
+
+ if (SourceStream != null)
+ {
+ SourceStream.Dispose();
+ }
+ }
+
+ /// <summary>
+ /// Handles a range request of "bytes=0-"
+ /// This will serve the complete content and add the content-range header
+ /// </summary>
+ private Task ServeCompleteRangeRequest(KeyValuePair<long, long?> requestedRange, Stream responseStream)
+ {
+ long totalContentLength = TotalContentLength.Value;
+
+ long rangeStart = requestedRange.Key;
+ long rangeEnd = totalContentLength - 1;
+ long rangeLength = 1 + rangeEnd - rangeStart;
+
+ // Content-Length is the length of what we're serving, not the original content
+ HttpListenerContext.Response.ContentLength64 = rangeLength;
+ HttpListenerContext.Response.Headers["Content-Range"] = string.Format("bytes {0}-{1}/{2}", rangeStart, rangeEnd, totalContentLength);
+
+ if (rangeStart > 0)
+ {
+ SourceStream.Position = rangeStart;
+ }
+
+ return SourceStream.CopyToAsync(responseStream);
+ }
+
+ /// <summary>
+ /// Serves a partial range request where the total content length is not known
+ /// </summary>
+ private async Task ServePartialRangeRequestWithUnknownTotalContentLength(KeyValuePair<long, long?> requestedRange, Stream responseStream)
+ {
+ // Read the entire stream so that we can determine the length
+ byte[] bytes = await ReadBytes(SourceStream, 0, null).ConfigureAwait(false);
+
+ long totalContentLength = bytes.LongLength;
+
+ long rangeStart = requestedRange.Key;
+ long rangeEnd = requestedRange.Value ?? (totalContentLength - 1);
+ long rangeLength = 1 + rangeEnd - rangeStart;
+
+ // Content-Length is the length of what we're serving, not the original content
+ HttpListenerContext.Response.ContentLength64 = rangeLength;
+ HttpListenerContext.Response.Headers["Content-Range"] = string.Format("bytes {0}-{1}/{2}", rangeStart, rangeEnd, totalContentLength);
+
+ await responseStream.WriteAsync(bytes, Convert.ToInt32(rangeStart), Convert.ToInt32(rangeLength)).ConfigureAwait(false);
+ }
+
+ /// <summary>
+ /// Serves a partial range request where the total content length is already known
+ /// </summary>
+ private async Task ServePartialRangeRequestWithKnownTotalContentLength(KeyValuePair<long, long?> requestedRange, Stream responseStream)
+ {
+ long totalContentLength = TotalContentLength.Value;
+ long rangeStart = requestedRange.Key;
+ long rangeEnd = requestedRange.Value ?? (totalContentLength - 1);
+ long rangeLength = 1 + rangeEnd - rangeStart;
+
+ // Only read the bytes we need
+ byte[] bytes = await ReadBytes(SourceStream, Convert.ToInt32(rangeStart), Convert.ToInt32(rangeLength)).ConfigureAwait(false);
+
+ // Content-Length is the length of what we're serving, not the original content
+ HttpListenerContext.Response.ContentLength64 = rangeLength;
+
+ HttpListenerContext.Response.Headers["Content-Range"] = string.Format("bytes {0}-{1}/{2}", rangeStart, rangeEnd, totalContentLength);
+
+ await responseStream.WriteAsync(bytes, 0, Convert.ToInt32(rangeLength)).ConfigureAwait(false);
+ }
+
+ /// <summary>
+ /// Reads bytes from a stream
+ /// </summary>
+ /// <param name="input">The input stream</param>
+ /// <param name="start">The starting position</param>
+ /// <param name="count">The number of bytes to read, or null to read to the end.</param>
+ private async Task<byte[]> ReadBytes(Stream input, int start, int? count)
+ {
+ if (start > 0)
+ {
+ input.Position = start;
+ }
+
+ if (count == null)
+ {
+ var buffer = new byte[16 * 1024];
+
+ using (var ms = new MemoryStream())
+ {
+ int read;
+ while ((read = await input.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false)) > 0)
+ {
+ await ms.WriteAsync(buffer, 0, read).ConfigureAwait(false);
+ }
+ return ms.ToArray();
+ }
+ }
+ else
+ {
+ var buffer = new byte[count.Value];
+
+ using (var ms = new MemoryStream())
+ {
+ int read = await input.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false);
+
+ await ms.WriteAsync(buffer, 0, read).ConfigureAwait(false);
+
+ return ms.ToArray();
+ }
+ }
+
+ }
+ }
+}
diff --git a/MediaBrowser.Common/Net/HttpServer.cs b/MediaBrowser.Common/Net/HttpServer.cs
new file mode 100644
index 000000000..276e14eb3
--- /dev/null
+++ b/MediaBrowser.Common/Net/HttpServer.cs
@@ -0,0 +1,40 @@
+using System;
+using System.Net;
+using System.Reactive.Linq;
+
+namespace MediaBrowser.Common.Net
+{
+ public class HttpServer : IObservable<HttpListenerContext>, IDisposable
+ {
+ private readonly HttpListener _listener;
+ private readonly IObservable<HttpListenerContext> _stream;
+
+ public HttpServer(string url)
+ {
+ _listener = new HttpListener();
+ _listener.Prefixes.Add(url);
+ _listener.Start();
+ _stream = ObservableHttpContext();
+ }
+
+ private IObservable<HttpListenerContext> ObservableHttpContext()
+ {
+ return Observable.Create<HttpListenerContext>(obs =>
+ Observable.FromAsync(() => _listener.GetContextAsync())
+ .Subscribe(obs))
+ .Repeat()
+ .Retry()
+ .Publish()
+ .RefCount();
+ }
+ public void Dispose()
+ {
+ _listener.Stop();
+ }
+
+ public IDisposable Subscribe(IObserver<HttpListenerContext> observer)
+ {
+ return _stream.Subscribe(observer);
+ }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Common/Net/MimeTypes.cs b/MediaBrowser.Common/Net/MimeTypes.cs
new file mode 100644
index 000000000..fb85b0f2a
--- /dev/null
+++ b/MediaBrowser.Common/Net/MimeTypes.cs
@@ -0,0 +1,160 @@
+using System;
+using System.IO;
+
+namespace MediaBrowser.Common.Net
+{
+ public static class MimeTypes
+ {
+ public static string JsonMimeType = "application/json";
+
+ public static string GetMimeType(string path)
+ {
+ var ext = Path.GetExtension(path);
+
+ // http://en.wikipedia.org/wiki/Internet_media_type
+ // Add more as needed
+
+ // Type video
+ if (ext.EndsWith("mpg", StringComparison.OrdinalIgnoreCase) || ext.EndsWith("mpeg", StringComparison.OrdinalIgnoreCase))
+ {
+ return "video/mpeg";
+ }
+ if (ext.EndsWith("mp4", StringComparison.OrdinalIgnoreCase))
+ {
+ return "video/mp4";
+ }
+ if (ext.EndsWith("ogv", StringComparison.OrdinalIgnoreCase))
+ {
+ return "video/ogg";
+ }
+ if (ext.EndsWith("mov", StringComparison.OrdinalIgnoreCase))
+ {
+ return "video/quicktime";
+ }
+ if (ext.EndsWith("webm", StringComparison.OrdinalIgnoreCase))
+ {
+ return "video/webm";
+ }
+ if (ext.EndsWith("mkv", StringComparison.OrdinalIgnoreCase))
+ {
+ return "video/x-matroska";
+ }
+ if (ext.EndsWith("wmv", StringComparison.OrdinalIgnoreCase))
+ {
+ return "video/x-ms-wmv";
+ }
+ if (ext.EndsWith("flv", StringComparison.OrdinalIgnoreCase))
+ {
+ return "video/x-flv";
+ }
+ if (ext.EndsWith("avi", StringComparison.OrdinalIgnoreCase))
+ {
+ return "video/avi";
+ }
+ if (ext.EndsWith("m4v", StringComparison.OrdinalIgnoreCase))
+ {
+ return "video/x-m4v";
+ }
+ if (ext.EndsWith("asf", StringComparison.OrdinalIgnoreCase))
+ {
+ return "video/x-ms-asf";
+ }
+ if (ext.EndsWith("3gp", StringComparison.OrdinalIgnoreCase))
+ {
+ return "video/3gpp";
+ }
+ if (ext.EndsWith("3g2", StringComparison.OrdinalIgnoreCase))
+ {
+ return "video/3gpp2";
+ }
+ if (ext.EndsWith("ts", StringComparison.OrdinalIgnoreCase))
+ {
+ return "video/mp2t";
+ }
+
+ // Type text
+ if (ext.EndsWith("css", StringComparison.OrdinalIgnoreCase))
+ {
+ return "text/css";
+ }
+ if (ext.EndsWith("csv", StringComparison.OrdinalIgnoreCase))
+ {
+ return "text/csv";
+ }
+ if (ext.EndsWith("html", StringComparison.OrdinalIgnoreCase) || ext.EndsWith("html", StringComparison.OrdinalIgnoreCase))
+ {
+ return "text/html";
+ }
+ if (ext.EndsWith("txt", StringComparison.OrdinalIgnoreCase))
+ {
+ return "text/plain";
+ }
+
+ // Type image
+ if (ext.EndsWith("gif", StringComparison.OrdinalIgnoreCase))
+ {
+ return "image/gif";
+ }
+ if (ext.EndsWith("jpg", StringComparison.OrdinalIgnoreCase) || ext.EndsWith("jpeg", StringComparison.OrdinalIgnoreCase))
+ {
+ return "image/jpeg";
+ }
+ if (ext.EndsWith("png", StringComparison.OrdinalIgnoreCase))
+ {
+ return "image/png";
+ }
+ if (ext.EndsWith("ico", StringComparison.OrdinalIgnoreCase))
+ {
+ return "image/vnd.microsoft.icon";
+ }
+
+ // Type audio
+ if (ext.EndsWith("mp3", StringComparison.OrdinalIgnoreCase))
+ {
+ return "audio/mpeg";
+ }
+ if (ext.EndsWith("m4a", StringComparison.OrdinalIgnoreCase) || ext.EndsWith("aac", StringComparison.OrdinalIgnoreCase))
+ {
+ return "audio/mp4";
+ }
+ if (ext.EndsWith("webma", StringComparison.OrdinalIgnoreCase))
+ {
+ return "audio/webm";
+ }
+ if (ext.EndsWith("wav", StringComparison.OrdinalIgnoreCase))
+ {
+ return "audio/wav";
+ }
+ if (ext.EndsWith("wma", StringComparison.OrdinalIgnoreCase))
+ {
+ return "audio/x-ms-wma";
+ }
+ if (ext.EndsWith("flac", StringComparison.OrdinalIgnoreCase))
+ {
+ return "audio/flac";
+ }
+ if (ext.EndsWith("aac", StringComparison.OrdinalIgnoreCase))
+ {
+ return "audio/x-aac";
+ }
+ if (ext.EndsWith("ogg", StringComparison.OrdinalIgnoreCase) || ext.EndsWith("oga", StringComparison.OrdinalIgnoreCase))
+ {
+ return "audio/ogg";
+ }
+
+ // Playlists
+ if (ext.EndsWith("m3u8", StringComparison.OrdinalIgnoreCase))
+ {
+ return "application/x-mpegURL";
+ }
+
+ // Misc
+ if (ext.EndsWith("dll", StringComparison.OrdinalIgnoreCase))
+ {
+ return "application/x-msdownload";
+ }
+
+ throw new InvalidOperationException("Argument not supported: " + path);
+ }
+ }
+}
diff --git a/MediaBrowser.Common/Net/Request.cs b/MediaBrowser.Common/Net/Request.cs
new file mode 100644
index 000000000..795c9c36b
--- /dev/null
+++ b/MediaBrowser.Common/Net/Request.cs
@@ -0,0 +1,18 @@
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+
+namespace MediaBrowser.Common.Net
+{
+ public class Request
+ {
+ public string HttpMethod { get; set; }
+ public IDictionary<string, IEnumerable<string>> Headers { get; set; }
+ public Stream InputStream { get; set; }
+ public string RawUrl { get; set; }
+ public int ContentLength
+ {
+ get { return int.Parse(Headers["Content-Length"].First()); }
+ }
+ }
+} \ No newline at end of file