diff options
Diffstat (limited to 'Emby.Common.Implementations/Net')
| -rw-r--r-- | Emby.Common.Implementations/Net/DisposableManagedObjectBase.cs | 74 | ||||
| -rw-r--r-- | Emby.Common.Implementations/Net/NetSocket.cs | 97 | ||||
| -rw-r--r-- | Emby.Common.Implementations/Net/SocketAcceptor.cs | 127 | ||||
| -rw-r--r-- | Emby.Common.Implementations/Net/SocketFactory.cs | 160 | ||||
| -rw-r--r-- | Emby.Common.Implementations/Net/UdpSocket.cs | 242 |
5 files changed, 700 insertions, 0 deletions
diff --git a/Emby.Common.Implementations/Net/DisposableManagedObjectBase.cs b/Emby.Common.Implementations/Net/DisposableManagedObjectBase.cs new file mode 100644 index 000000000..8476cea32 --- /dev/null +++ b/Emby.Common.Implementations/Net/DisposableManagedObjectBase.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Emby.Common.Implementations.Net +{ + /// <summary> + /// Correclty implements the <see cref="IDisposable"/> interface and pattern for an object containing only managed resources, and adds a few common niceities not on the interface such as an <see cref="IsDisposed"/> property. + /// </summary> + public abstract class DisposableManagedObjectBase : IDisposable + { + + #region Public Methods + + /// <summary> + /// Override this method and dispose any objects you own the lifetime of if disposing is true; + /// </summary> + /// <param name="disposing">True if managed objects should be disposed, if false, only unmanaged resources should be released.</param> + protected abstract void Dispose(bool disposing); + + /// <summary> + /// Throws and <see cref="System.ObjectDisposedException"/> if the <see cref="IsDisposed"/> property is true. + /// </summary> + /// <seealso cref="IsDisposed"/> + /// <exception cref="System.ObjectDisposedException">Thrown if the <see cref="IsDisposed"/> property is true.</exception> + /// <seealso cref="Dispose()"/> + protected virtual void ThrowIfDisposed() + { + if (this.IsDisposed) throw new ObjectDisposedException(this.GetType().FullName); + } + + #endregion + + #region Public Properties + + /// <summary> + /// Sets or returns a boolean indicating whether or not this instance has been disposed. + /// </summary> + /// <seealso cref="Dispose()"/> + public bool IsDisposed + { + get; + private set; + } + + #endregion + + #region IDisposable Members + + /// <summary> + /// Disposes this object instance and all internally managed resources. + /// </summary> + /// <remarks> + /// <para>Sets the <see cref="IsDisposed"/> property to true. Does not explicitly throw an exception if called multiple times, but makes no promises about behaviour of derived classes.</para> + /// </remarks> + /// <seealso cref="IsDisposed"/> + public void Dispose() + { + try + { + IsDisposed = true; + + Dispose(true); + } + finally + { + GC.SuppressFinalize(this); + } + } + + #endregion + } +} diff --git a/Emby.Common.Implementations/Net/NetSocket.cs b/Emby.Common.Implementations/Net/NetSocket.cs new file mode 100644 index 000000000..bc012dfe2 --- /dev/null +++ b/Emby.Common.Implementations/Net/NetSocket.cs @@ -0,0 +1,97 @@ +using System; +using System.Net; +using System.Net.Sockets; +using System.Threading; +using Emby.Common.Implementations.Networking; +using MediaBrowser.Model.Net; +using MediaBrowser.Model.Logging; + +namespace Emby.Common.Implementations.Net +{ + public class NetSocket : ISocket + { + public Socket Socket { get; private set; } + private readonly ILogger _logger; + + public bool DualMode { get; private set; } + + public NetSocket(Socket socket, ILogger logger, bool isDualMode) + { + if (socket == null) + { + throw new ArgumentNullException("socket"); + } + if (logger == null) + { + throw new ArgumentNullException("logger"); + } + + Socket = socket; + _logger = logger; + DualMode = isDualMode; + } + + public IpEndPointInfo LocalEndPoint + { + get + { + return NetworkManager.ToIpEndPointInfo((IPEndPoint)Socket.LocalEndPoint); + } + } + + public IpEndPointInfo RemoteEndPoint + { + get + { + return NetworkManager.ToIpEndPointInfo((IPEndPoint)Socket.RemoteEndPoint); + } + } + + public void Close() + { +#if NET46 + Socket.Close(); +#else + Socket.Dispose(); +#endif + } + + public void Shutdown(bool both) + { + if (both) + { + Socket.Shutdown(SocketShutdown.Both); + } + else + { + // Change interface if ever needed + throw new NotImplementedException(); + } + } + + public void Listen(int backlog) + { + Socket.Listen(backlog); + } + + public void Bind(IpEndPointInfo endpoint) + { + var nativeEndpoint = NetworkManager.ToIPEndPoint(endpoint); + + Socket.Bind(nativeEndpoint); + } + + private SocketAcceptor _acceptor; + public void StartAccept(Action<ISocket> onAccept, Func<bool> isClosed) + { + _acceptor = new SocketAcceptor(_logger, Socket, onAccept, isClosed, DualMode); + + _acceptor.StartAccept(); + } + + public void Dispose() + { + Socket.Dispose(); + } + } +} diff --git a/Emby.Common.Implementations/Net/SocketAcceptor.cs b/Emby.Common.Implementations/Net/SocketAcceptor.cs new file mode 100644 index 000000000..d4c6d33e5 --- /dev/null +++ b/Emby.Common.Implementations/Net/SocketAcceptor.cs @@ -0,0 +1,127 @@ +using System; +using System.Net.Sockets; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Net; + +namespace Emby.Common.Implementations.Net +{ + public class SocketAcceptor + { + private readonly ILogger _logger; + private readonly Socket _originalSocket; + private readonly Func<bool> _isClosed; + private readonly Action<ISocket> _onAccept; + private readonly bool _isDualMode; + + public SocketAcceptor(ILogger logger, Socket originalSocket, Action<ISocket> onAccept, Func<bool> isClosed, bool isDualMode) + { + if (logger == null) + { + throw new ArgumentNullException("logger"); + } + if (originalSocket == null) + { + throw new ArgumentNullException("originalSocket"); + } + if (onAccept == null) + { + throw new ArgumentNullException("onAccept"); + } + if (isClosed == null) + { + throw new ArgumentNullException("isClosed"); + } + + _logger = logger; + _originalSocket = originalSocket; + _isClosed = isClosed; + _isDualMode = isDualMode; + _onAccept = onAccept; + } + + public void StartAccept() + { + Socket dummy = null; + StartAccept(null, ref dummy); + } + + public void StartAccept(SocketAsyncEventArgs acceptEventArg, ref Socket accepted) + { + if (acceptEventArg == null) + { + acceptEventArg = new SocketAsyncEventArgs(); + acceptEventArg.Completed += new EventHandler<SocketAsyncEventArgs>(AcceptEventArg_Completed); + } + else + { + // socket must be cleared since the context object is being reused + acceptEventArg.AcceptSocket = null; + } + + try + { + bool willRaiseEvent = _originalSocket.AcceptAsync(acceptEventArg); + + if (!willRaiseEvent) + { + ProcessAccept(acceptEventArg); + } + } + catch (Exception ex) + { + if (accepted != null) + { + try + { +#if NET46 + accepted.Close(); +#else + accepted.Dispose(); +#endif + } + catch + { + } + accepted = null; + } + } + } + + // This method is the callback method associated with Socket.AcceptAsync + // operations and is invoked when an accept operation is complete + // + void AcceptEventArg_Completed(object sender, SocketAsyncEventArgs e) + { + ProcessAccept(e); + } + + private void ProcessAccept(SocketAsyncEventArgs e) + { + if (_isClosed()) + { + return; + } + + // http://msdn.microsoft.com/en-us/library/system.net.sockets.socket.acceptasync%28v=vs.110%29.aspx + // Under certain conditions ConnectionReset can occur + // Need to attept to re-accept + if (e.SocketError == SocketError.ConnectionReset) + { + _logger.Error("SocketError.ConnectionReset reported. Attempting to re-accept."); + Socket dummy = null; + StartAccept(e, ref dummy); + return; + } + + var acceptSocket = e.AcceptSocket; + if (acceptSocket != null) + { + //ProcessAccept(acceptSocket); + _onAccept(new NetSocket(acceptSocket, _logger, _isDualMode)); + } + + // Accept the next connection request + StartAccept(e, ref acceptSocket); + } + } +} diff --git a/Emby.Common.Implementations/Net/SocketFactory.cs b/Emby.Common.Implementations/Net/SocketFactory.cs new file mode 100644 index 000000000..70c7ba845 --- /dev/null +++ b/Emby.Common.Implementations/Net/SocketFactory.cs @@ -0,0 +1,160 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Threading.Tasks; +using Emby.Common.Implementations.Networking; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Net; + +namespace Emby.Common.Implementations.Net +{ + public class SocketFactory : ISocketFactory + { + // THIS IS A LINKED FILE - SHARED AMONGST MULTIPLE PLATFORMS + // Be careful to check any changes compile and work for all platform projects it is shared in. + + // Not entirely happy with this. Would have liked to have done something more generic/reusable, + // but that wasn't really the point so kept to YAGNI principal for now, even if the + // interfaces are a bit ugly, specific and make assumptions. + + private readonly ILogger _logger; + + public SocketFactory(ILogger logger) + { + if (logger == null) + { + throw new ArgumentNullException("logger"); + } + + _logger = logger; + } + + public ISocket CreateSocket(IpAddressFamily family, MediaBrowser.Model.Net.SocketType socketType, MediaBrowser.Model.Net.ProtocolType protocolType, bool dualMode) + { + try + { + var addressFamily = family == IpAddressFamily.InterNetwork + ? AddressFamily.InterNetwork + : AddressFamily.InterNetworkV6; + + var socket = new Socket(addressFamily, System.Net.Sockets.SocketType.Stream, System.Net.Sockets.ProtocolType.Tcp); + + if (dualMode) + { + socket.DualMode = true; + } + + return new NetSocket(socket, _logger, dualMode); + } + catch (SocketException ex) + { + throw new SocketCreateException(ex.SocketErrorCode.ToString(), ex); + } + } + + #region ISocketFactory Members + + /// <summary> + /// Creates a new UDP socket and binds it to the specified local port. + /// </summary> + /// <param name="localPort">An integer specifying the local port to bind the socket to.</param> + public IUdpSocket CreateUdpSocket(int localPort) + { + if (localPort < 0) throw new ArgumentException("localPort cannot be less than zero.", "localPort"); + + var retVal = new Socket(AddressFamily.InterNetwork, System.Net.Sockets.SocketType.Dgram, System.Net.Sockets.ProtocolType.Udp); + try + { + retVal.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); + return new UdpSocket(retVal, localPort, IPAddress.Any); + } + catch + { + if (retVal != null) + retVal.Dispose(); + + throw; + } + } + + /// <summary> + /// Creates a new UDP socket that is a member of the SSDP multicast local admin group and binds it to the specified local port. + /// </summary> + /// <returns>An implementation of the <see cref="IUdpSocket"/> interface used by RSSDP components to perform socket operations.</returns> + public IUdpSocket CreateSsdpUdpSocket(IpAddressInfo localIpAddress, int localPort) + { + if (localPort < 0) throw new ArgumentException("localPort cannot be less than zero.", "localPort"); + + var retVal = new Socket(AddressFamily.InterNetwork, System.Net.Sockets.SocketType.Dgram, System.Net.Sockets.ProtocolType.Udp); + try + { + retVal.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); + retVal.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.MulticastTimeToLive, 4); + + var localIp = NetworkManager.ToIPAddress(localIpAddress); + + retVal.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(IPAddress.Parse("239.255.255.250"), localIp)); + return new UdpSocket(retVal, localPort, localIp); + } + catch + { + if (retVal != null) + retVal.Dispose(); + + throw; + } + } + + /// <summary> + /// Creates a new UDP socket that is a member of the specified multicast IP address, and binds it to the specified local port. + /// </summary> + /// <param name="ipAddress">The multicast IP address to make the socket a member of.</param> + /// <param name="multicastTimeToLive">The multicast time to live value for the socket.</param> + /// <param name="localPort">The number of the local port to bind to.</param> + /// <returns></returns> + public IUdpSocket CreateUdpMulticastSocket(string ipAddress, int multicastTimeToLive, int localPort) + { + if (ipAddress == null) throw new ArgumentNullException("ipAddress"); + if (ipAddress.Length == 0) throw new ArgumentException("ipAddress cannot be an empty string.", "ipAddress"); + if (multicastTimeToLive <= 0) throw new ArgumentException("multicastTimeToLive cannot be zero or less.", "multicastTimeToLive"); + if (localPort < 0) throw new ArgumentException("localPort cannot be less than zero.", "localPort"); + + var retVal = new Socket(AddressFamily.InterNetwork, System.Net.Sockets.SocketType.Dgram, System.Net.Sockets.ProtocolType.Udp); + + try + { +#if NET46 + retVal.ExclusiveAddressUse = false; +#else + // The ExclusiveAddressUse socket option is a Windows-specific option that, when set to "true," tells Windows not to allow another socket to use the same local address as this socket + // See https://github.com/dotnet/corefx/pull/11509 for more details + if (System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Windows)) + { + retVal.ExclusiveAddressUse = false; + } +#endif + //retVal.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.Broadcast, true); + retVal.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); + retVal.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.MulticastTimeToLive, multicastTimeToLive); + + var localIp = IPAddress.Any; + + retVal.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(IPAddress.Parse(ipAddress), localIp)); + retVal.MulticastLoopback = true; + + return new UdpSocket(retVal, localPort, localIp); + } + catch + { + if (retVal != null) + retVal.Dispose(); + + throw; + } + } + + #endregion + } +} diff --git a/Emby.Common.Implementations/Net/UdpSocket.cs b/Emby.Common.Implementations/Net/UdpSocket.cs new file mode 100644 index 000000000..b2af9d162 --- /dev/null +++ b/Emby.Common.Implementations/Net/UdpSocket.cs @@ -0,0 +1,242 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Security; +using System.Threading.Tasks; +using Emby.Common.Implementations.Networking; +using MediaBrowser.Model.Net; + +namespace Emby.Common.Implementations.Net +{ + // THIS IS A LINKED FILE - SHARED AMONGST MULTIPLE PLATFORMS + // Be careful to check any changes compile and work for all platform projects it is shared in. + + internal sealed class UdpSocket : DisposableManagedObjectBase, IUdpSocket + { + + #region Fields + + private Socket _Socket; + private int _LocalPort; + #endregion + + #region Constructors + + public UdpSocket(Socket socket, int localPort, IPAddress ip) + { + if (socket == null) throw new ArgumentNullException("socket"); + + _Socket = socket; + _LocalPort = localPort; + LocalIPAddress = NetworkManager.ToIpAddressInfo(ip); + + _Socket.Bind(new IPEndPoint(ip, _LocalPort)); + } + + #endregion + + public IpAddressInfo LocalIPAddress + { + get; + private set; + } + + #region IUdpSocket Members + + public Task<SocketReceiveResult> ReceiveAsync() + { + ThrowIfDisposed(); + + var tcs = new TaskCompletionSource<SocketReceiveResult>(); + + EndPoint receivedFromEndPoint = new IPEndPoint(IPAddress.Any, 0); + var state = new AsyncReceiveState(_Socket, receivedFromEndPoint); + state.TaskCompletionSource = tcs; + +#if NETSTANDARD1_6 + _Socket.ReceiveFromAsync(new ArraySegment<Byte>(state.Buffer),SocketFlags.None, state.RemoteEndPoint) + .ContinueWith((task, asyncState) => + { + if (task.Status != TaskStatus.Faulted) + { + var receiveState = asyncState as AsyncReceiveState; + receiveState.RemoteEndPoint = task.Result.RemoteEndPoint; + ProcessResponse(receiveState, () => task.Result.ReceivedBytes, LocalIPAddress); + } + }, state); +#else + _Socket.BeginReceiveFrom(state.Buffer, 0, state.Buffer.Length, SocketFlags.None, ref state.RemoteEndPoint, ProcessResponse, state); +#endif + + return tcs.Task; + } + + public Task SendAsync(byte[] buffer, int size, IpEndPointInfo endPoint) + { + ThrowIfDisposed(); + + if (buffer == null) throw new ArgumentNullException("messageData"); + if (endPoint == null) throw new ArgumentNullException("endPoint"); + + var ipEndPoint = NetworkManager.ToIPEndPoint(endPoint); + +#if NETSTANDARD1_6 + + if (size != buffer.Length) + { + byte[] copy = new byte[size]; + Buffer.BlockCopy(buffer, 0, copy, 0, size); + buffer = copy; + } + + _Socket.SendTo(buffer, ipEndPoint); + return Task.FromResult(true); +#else + var taskSource = new TaskCompletionSource<bool>(); + + try + { + _Socket.BeginSendTo(buffer, 0, size, SocketFlags.None, ipEndPoint, result => + { + try + { + _Socket.EndSend(result); + taskSource.TrySetResult(true); + } + catch (Exception ex) + { + taskSource.TrySetException(ex); + } + + }, null); + } + catch (Exception ex) + { + taskSource.TrySetException(ex); + } + + //_Socket.SendTo(messageData, new System.Net.IPEndPoint(IPAddress.Parse(RemoteEndPoint.IPAddress), RemoteEndPoint.Port)); + + return taskSource.Task; +#endif + } + + #endregion + + #region Overrides + + protected override void Dispose(bool disposing) + { + if (disposing) + { + var socket = _Socket; + if (socket != null) + socket.Dispose(); + } + } + + #endregion + + #region Private Methods + + private static void ProcessResponse(AsyncReceiveState state, Func<int> receiveData, IpAddressInfo localIpAddress) + { + try + { + var bytesRead = receiveData(); + + var ipEndPoint = state.RemoteEndPoint as IPEndPoint; + state.TaskCompletionSource.SetResult( + new SocketReceiveResult + { + Buffer = state.Buffer, + ReceivedBytes = bytesRead, + RemoteEndPoint = ToIpEndPointInfo(ipEndPoint), + LocalIPAddress = localIpAddress + } + ); + } + catch (ObjectDisposedException) + { + state.TaskCompletionSource.SetCanceled(); + } + catch (SocketException se) + { + if (se.SocketErrorCode != SocketError.Interrupted && se.SocketErrorCode != SocketError.OperationAborted && se.SocketErrorCode != SocketError.Shutdown) + state.TaskCompletionSource.SetException(se); + else + state.TaskCompletionSource.SetCanceled(); + } + catch (Exception ex) + { + state.TaskCompletionSource.SetException(ex); + } + } + + private static IpEndPointInfo ToIpEndPointInfo(IPEndPoint endpoint) + { + if (endpoint == null) + { + return null; + } + + return NetworkManager.ToIpEndPointInfo(endpoint); + } + + private void ProcessResponse(IAsyncResult asyncResult) + { +#if NET46 + var state = asyncResult.AsyncState as AsyncReceiveState; + try + { + var bytesRead = state.Socket.EndReceiveFrom(asyncResult, ref state.RemoteEndPoint); + + var ipEndPoint = state.RemoteEndPoint as IPEndPoint; + state.TaskCompletionSource.SetResult( + new SocketReceiveResult + { + Buffer = state.Buffer, + ReceivedBytes = bytesRead, + RemoteEndPoint = ToIpEndPointInfo(ipEndPoint), + LocalIPAddress = LocalIPAddress + } + ); + } + catch (ObjectDisposedException) + { + state.TaskCompletionSource.SetCanceled(); + } + catch (Exception ex) + { + state.TaskCompletionSource.SetException(ex); + } +#endif + } + + #endregion + + #region Private Classes + + private class AsyncReceiveState + { + public AsyncReceiveState(Socket socket, EndPoint remoteEndPoint) + { + this.Socket = socket; + this.RemoteEndPoint = remoteEndPoint; + } + + public EndPoint RemoteEndPoint; + public byte[] Buffer = new byte[8192]; + + public Socket Socket { get; private set; } + + public TaskCompletionSource<SocketReceiveResult> TaskCompletionSource { get; set; } + + } + + #endregion + + } +} |
