diff --git a/MediaBrowser.Common/Net/INetworkManager.cs b/MediaBrowser.Common/Net/INetworkManager.cs index 979dae49c9..6b8136d872 100644 --- a/MediaBrowser.Common/Net/INetworkManager.cs +++ b/MediaBrowser.Common/Net/INetworkManager.cs @@ -2,6 +2,7 @@ using MediaBrowser.Model.IO; using MediaBrowser.Model.Net; using System.Collections.Generic; using System.Net; +using MediaBrowser.Model.Logging; namespace MediaBrowser.Common.Net { @@ -51,5 +52,12 @@ namespace MediaBrowser.Common.Net /// The endpoint. /// true if [is in local network] [the specified endpoint]; otherwise, false. bool IsInLocalNetwork(string endpoint); + + /// + /// Generates a self signed certificate at the locatation specified by . + /// + /// The path to generate the certificate. + /// The common name for the certificate. + void GenerateSelfSignedSslCertificate(string certificatePath, string hostname); } } \ No newline at end of file diff --git a/MediaBrowser.Server.Mono/MediaBrowser.Server.Mono.csproj b/MediaBrowser.Server.Mono/MediaBrowser.Server.Mono.csproj index e780f447fe..232caba4ff 100644 --- a/MediaBrowser.Server.Mono/MediaBrowser.Server.Mono.csproj +++ b/MediaBrowser.Server.Mono/MediaBrowser.Server.Mono.csproj @@ -57,6 +57,11 @@ False ..\packages\Mono.Posix.4.0.0.0\lib\net40\Mono.Posix.dll + + False + ..\ThirdParty\Mono.Security\Mono.Security.dll + False + ..\ThirdParty\ServiceStack\ServiceStack.Interfaces.dll @@ -70,6 +75,7 @@ Properties\SharedVersion.cs + diff --git a/MediaBrowser.Server.Mono/Networking/CertificateGenerator.cs b/MediaBrowser.Server.Mono/Networking/CertificateGenerator.cs new file mode 100644 index 0000000000..52909a5446 --- /dev/null +++ b/MediaBrowser.Server.Mono/Networking/CertificateGenerator.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; +using MediaBrowser.Model.Logging; +using Mono.Security.X509; + +namespace MediaBrowser.Server.Mono.Networking +{ + internal class CertificateGenerator + { + private const string MonoTestRootAgency = "v/4nALBxCE+9JgEC0LnDUvKh6e96PwTpN4Rj+vWnqKT7IAp1iK/JjuqvAg6DQ2vTfv0dTlqffmHH51OyioprcT5nzxcSTsZb/9jcHScG0s3/FRIWnXeLk/fgm7mSYhjUaHNI0m1/NTTktipicjKxo71hGIg9qucCWnDum+Krh/k=AQAB

9jbKxMXEruW2CfZrzhxtull4O8P47+mNsEL+9gf9QsRO1jJ77C+jmzfU6zbzjf8+ViK+q62tCMdC1ZzulwdpXQ==

x5+p198l1PkK0Ga2mRh0SIYSykENpY2aLXoyZD/iUpKYAvATm0/wvKNrE4dKJyPCA+y3hfTdgVag+SP9avvDTQ==ISSjCvXsUfbOGG05eddN1gXxL2pj+jegQRfjpk7RAsnWKvNExzhqd5x+ZuNQyc6QH5wxun54inP4RTUI0P/IaQ==R815VQmR3RIbPqzDXzv5j6CSH6fYlcTiQRtkBsUnzhWmkd/y3XmamO+a8zJFjOCCx9CcjpVuGziivBqi65lVPQ==iYiu0KwMWI/dyqN3RJYUzuuLj02/oTD1pYpwo2rvNCXU1Q5VscOeu2DpNg1gWqI+1RrRCsEoaTNzXB1xtKNlSw==nIfh1LYF8fjRBgMdAH/zt9UKHWiaCnc+jXzq5tkR8HVSKTVdzitD8bl1JgAfFQD8VjSXiCJqluexy/B5SGrCXQ49c78NIQj0hD+J13Y8/E0fUbW1QYbhj6Ff7oHyhaYe1WOQfkp2t/h+llHOdt1HRf7bt7dUknYp7m8bQKGxoYE=
"; + + internal static void CreateSelfSignCertificatePfx( + string fileName, + string hostname, + ILogger logger) + { + try + { + if (string.IsNullOrWhiteSpace(fileName)) + { + logger.Info("No certificate filename specified."); + return; + } + + if (File.Exists(fileName)) + { + logger.Info("Certificate file already exists. To regenerate, delete {0}", fileName); + return; + } + + byte[] sn = Guid.NewGuid().ToByteArray(); + string subject = string.Format("CN={0}", hostname); + string issuer = subject; + DateTime notBefore = DateTime.Now.AddDays(-2); + DateTime notAfter = DateTime.Now.AddYears(10); + + RSA issuerKey = RSA.Create(); + issuerKey.FromXmlString(MonoTestRootAgency); + RSA subjectKey = RSA.Create(); + + // serial number MUST be positive + if ((sn[0] & 0x80) == 0x80) + sn[0] -= 0x80; + + issuer = subject; + issuerKey = subjectKey; + + X509CertificateBuilder cb = new X509CertificateBuilder(3); + cb.SerialNumber = sn; + cb.IssuerName = issuer; + cb.NotBefore = notBefore; + cb.NotAfter = notAfter; + cb.SubjectName = subject; + cb.SubjectPublicKey = subjectKey; + + // signature + cb.Hash = "SHA256"; + byte[] rawcert = cb.Sign(issuerKey); + + PKCS12 p12 = new PKCS12(); + + + ArrayList list = new ArrayList(); + // we use a fixed array to avoid endianess issues + // (in case some tools requires the ID to be 1). + list.Add(new byte[4] {1, 0, 0, 0}); + Hashtable attributes = new Hashtable(1); + attributes.Add(PKCS9.localKeyId, list); + + p12.AddCertificate(new X509Certificate(rawcert), attributes); + + p12.AddPkcs8ShroudedKeyBag(subjectKey, attributes); + p12.SaveToFile(fileName); + } + catch (Exception e) + { + logger.ErrorException("Error generating self signed ssl certificate: {0}", e, fileName); + } + + } + } +} diff --git a/MediaBrowser.Server.Mono/Networking/NetworkManager.cs b/MediaBrowser.Server.Mono/Networking/NetworkManager.cs index 60c2501157..b4133f6dd6 100644 --- a/MediaBrowser.Server.Mono/Networking/NetworkManager.cs +++ b/MediaBrowser.Server.Mono/Networking/NetworkManager.cs @@ -35,5 +35,15 @@ namespace MediaBrowser.Server.Mono.Networking { return new List (); } + + /// + /// Generates a self signed certificate at the locatation specified by . + /// + /// The path to generate the certificate. + /// The common name for the certificate. + public void GenerateSelfSignedSslCertificate(string certificatePath, string hostname) + { + CertificateGenerator.CreateSelfSignCertificatePfx(certificatePath, hostname, Logger); + } } } diff --git a/MediaBrowser.Server.Startup.Common/ApplicationHost.cs b/MediaBrowser.Server.Startup.Common/ApplicationHost.cs index 16cf4258a4..4e05bceff6 100644 --- a/MediaBrowser.Server.Startup.Common/ApplicationHost.cs +++ b/MediaBrowser.Server.Startup.Common/ApplicationHost.cs @@ -779,6 +779,13 @@ namespace MediaBrowser.Server.Startup.Common { try { + if (ServerConfigurationManager.Configuration.EnableHttps) + { + NetworkManager.GenerateSelfSignedSslCertificate( + ServerConfigurationManager.Configuration.CertificatePath, + GetHostnameFromExternalDns(ServerConfigurationManager.Configuration.WanDdns)); + } + ServerManager.Start(GetUrlPrefixes(), ServerConfigurationManager.Configuration.CertificatePath); } catch (Exception ex) @@ -1183,5 +1190,29 @@ namespace MediaBrowser.Server.Startup.Common NativeApp.ConfigureAutoRun(autorun); } } + + /// + /// This returns localhost in the case of no external dns, and the hostname if the + /// dns is prefixed with a valid Uri prefix. + /// + /// The external dns prefix to get the hostname of. + /// The hostname in + private static string GetHostnameFromExternalDns(string externalDns) + { + if (string.IsNullOrWhiteSpace(externalDns)) + { + return "localhost"; + } + + try + { + Uri uri = new Uri(externalDns); + return uri.Host; + } + catch (Exception e) + { + return externalDns; + } + } } } diff --git a/MediaBrowser.ServerApplication/MediaBrowser.ServerApplication.csproj b/MediaBrowser.ServerApplication/MediaBrowser.ServerApplication.csproj index cc0bf31266..8df2d3ab06 100644 --- a/MediaBrowser.ServerApplication/MediaBrowser.ServerApplication.csproj +++ b/MediaBrowser.ServerApplication/MediaBrowser.ServerApplication.csproj @@ -109,6 +109,7 @@ + diff --git a/MediaBrowser.ServerApplication/Networking/CertificateGenerator.cs b/MediaBrowser.ServerApplication/Networking/CertificateGenerator.cs new file mode 100644 index 0000000000..a0200ba1d4 --- /dev/null +++ b/MediaBrowser.ServerApplication/Networking/CertificateGenerator.cs @@ -0,0 +1,263 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Security; +using System.Text; +using System.Threading.Tasks; +using MediaBrowser.Model.Logging; + +namespace MediaBrowser.ServerApplication.Networking +{ + // Copied from: http://blogs.msdn.com/b/dcook/archive/2014/05/16/9143036.aspx + // In case anybody is interested, source code is attached and is free for use by anybody as long as you don't hold me or Microsoft liable for it -- + // I have no idea whether this is actually the right or best way to do this. Give it the X500 distinguished name, validity start and end dates, + // and an optional password for encrypting the key data, and it will give you the PFX file data. Let me know if you find any bugs or have any suggestions. + internal class CertificateGenerator + { + internal static void CreateSelfSignCertificatePfx( + string fileName, + string hostname, + ILogger logger) + { + try + { + if (string.IsNullOrWhiteSpace(fileName)) + { + logger.Info("No certificate filename specified."); + return; + } + + if (File.Exists(fileName)) + { + logger.Info("Certificate file already exists. To regenerate, delete {0}", fileName); + return; + } + + string x500 = string.Format("CN={0}", hostname); + + DateTime startTime = DateTime.Now.AddDays(-2); + DateTime endTime = DateTime.Now.AddYears(10); + + byte[] pfxData = CreateSelfSignCertificatePfx( + x500, + startTime, + endTime); + + File.WriteAllBytes(fileName, pfxData); + } + catch (Exception e) + { + logger.ErrorException("Error generating self signed ssl certificate: {0}", e, fileName); + } + } + + private static byte[] CreateSelfSignCertificatePfx( + string x500, + DateTime startTime, + DateTime endTime) + { + byte[] pfxData; + + if (x500 == null) + { + x500 = ""; + } + + SystemTime startSystemTime = ToSystemTime(startTime); + SystemTime endSystemTime = ToSystemTime(endTime); + string containerName = Guid.NewGuid().ToString(); + + GCHandle dataHandle = new GCHandle(); + IntPtr providerContext = IntPtr.Zero; + IntPtr cryptKey = IntPtr.Zero; + IntPtr certContext = IntPtr.Zero; + IntPtr certStore = IntPtr.Zero; + IntPtr storeCertContext = IntPtr.Zero; + IntPtr passwordPtr = IntPtr.Zero; + RuntimeHelpers.PrepareConstrainedRegions(); + try + { + Check(NativeMethods.CryptAcquireContextW( + out providerContext, + containerName, + null, + 1, // PROV_RSA_FULL + 8)); // CRYPT_NEWKEYSET + + Check(NativeMethods.CryptGenKey( + providerContext, + 1, // AT_KEYEXCHANGE + 1 | 2048 << 16, // CRYPT_EXPORTABLE 2048 bit key + out cryptKey)); + + IntPtr errorStringPtr; + int nameDataLength = 0; + byte[] nameData; + + // errorStringPtr gets a pointer into the middle of the x500 string, + // so x500 needs to be pinned until after we've copied the value + // of errorStringPtr. + dataHandle = GCHandle.Alloc(x500, GCHandleType.Pinned); + + if (!NativeMethods.CertStrToNameW( + 0x00010001, // X509_ASN_ENCODING | PKCS_7_ASN_ENCODING + dataHandle.AddrOfPinnedObject(), + 3, // CERT_X500_NAME_STR = 3 + IntPtr.Zero, + null, + ref nameDataLength, + out errorStringPtr)) + { + string error = Marshal.PtrToStringUni(errorStringPtr); + throw new ArgumentException(error); + } + + nameData = new byte[nameDataLength]; + + if (!NativeMethods.CertStrToNameW( + 0x00010001, // X509_ASN_ENCODING | PKCS_7_ASN_ENCODING + dataHandle.AddrOfPinnedObject(), + 3, // CERT_X500_NAME_STR = 3 + IntPtr.Zero, + nameData, + ref nameDataLength, + out errorStringPtr)) + { + string error = Marshal.PtrToStringUni(errorStringPtr); + throw new ArgumentException(error); + } + + dataHandle.Free(); + + dataHandle = GCHandle.Alloc(nameData, GCHandleType.Pinned); + CryptoApiBlob nameBlob = new CryptoApiBlob( + nameData.Length, + dataHandle.AddrOfPinnedObject()); + + CryptKeyProviderInformation kpi = new CryptKeyProviderInformation(); + kpi.ContainerName = containerName; + kpi.ProviderType = 1; // PROV_RSA_FULL + kpi.KeySpec = 1; // AT_KEYEXCHANGE + + CryptAlgorithmIdentifier sha256Identifier = new CryptAlgorithmIdentifier(); + sha256Identifier.pszObjId = "1.2.840.113549.1.1.11"; + + certContext = NativeMethods.CertCreateSelfSignCertificate( + providerContext, + ref nameBlob, + 0, + ref kpi, + ref sha256Identifier, + ref startSystemTime, + ref endSystemTime, + IntPtr.Zero); + Check(certContext != IntPtr.Zero); + dataHandle.Free(); + + certStore = NativeMethods.CertOpenStore( + "Memory", // sz_CERT_STORE_PROV_MEMORY + 0, + IntPtr.Zero, + 0x2000, // CERT_STORE_CREATE_NEW_FLAG + IntPtr.Zero); + Check(certStore != IntPtr.Zero); + + Check(NativeMethods.CertAddCertificateContextToStore( + certStore, + certContext, + 1, // CERT_STORE_ADD_NEW + out storeCertContext)); + + NativeMethods.CertSetCertificateContextProperty( + storeCertContext, + 2, // CERT_KEY_PROV_INFO_PROP_ID + 0, + ref kpi); + + CryptoApiBlob pfxBlob = new CryptoApiBlob(); + Check(NativeMethods.PFXExportCertStoreEx( + certStore, + ref pfxBlob, + passwordPtr, + IntPtr.Zero, + 7)); // EXPORT_PRIVATE_KEYS | REPORT_NO_PRIVATE_KEY | REPORT_NOT_ABLE_TO_EXPORT_PRIVATE_KEY + + pfxData = new byte[pfxBlob.DataLength]; + dataHandle = GCHandle.Alloc(pfxData, GCHandleType.Pinned); + pfxBlob.Data = dataHandle.AddrOfPinnedObject(); + Check(NativeMethods.PFXExportCertStoreEx( + certStore, + ref pfxBlob, + passwordPtr, + IntPtr.Zero, + 7)); // EXPORT_PRIVATE_KEYS | REPORT_NO_PRIVATE_KEY | REPORT_NOT_ABLE_TO_EXPORT_PRIVATE_KEY + dataHandle.Free(); + } + finally + { + if (passwordPtr != IntPtr.Zero) + { + Marshal.ZeroFreeCoTaskMemUnicode(passwordPtr); + } + + if (dataHandle.IsAllocated) + { + dataHandle.Free(); + } + + if (certContext != IntPtr.Zero) + { + NativeMethods.CertFreeCertificateContext(certContext); + } + + if (storeCertContext != IntPtr.Zero) + { + NativeMethods.CertFreeCertificateContext(storeCertContext); + } + + if (certStore != IntPtr.Zero) + { + NativeMethods.CertCloseStore(certStore, 0); + } + + if (cryptKey != IntPtr.Zero) + { + NativeMethods.CryptDestroyKey(cryptKey); + } + + if (providerContext != IntPtr.Zero) + { + NativeMethods.CryptReleaseContext(providerContext, 0); + NativeMethods.CryptAcquireContextW( + out providerContext, + containerName, + null, + 1, // PROV_RSA_FULL + 0x10); // CRYPT_DELETEKEYSET + } + } + + return pfxData; + } + + private static SystemTime ToSystemTime(DateTime dateTime) + { + long fileTime = dateTime.ToFileTime(); + SystemTime systemTime; + Check(NativeMethods.FileTimeToSystemTime(ref fileTime, out systemTime)); + return systemTime; + } + + private static void Check(bool nativeCallSucceeded) + { + if (!nativeCallSucceeded) + { + int error = Marshal.GetHRForLastWin32Error(); + Marshal.ThrowExceptionForHR(error); + } + } + } +} diff --git a/MediaBrowser.ServerApplication/Networking/NativeMethods.cs b/MediaBrowser.ServerApplication/Networking/NativeMethods.cs index 20f7ac23d8..037b1f75b5 100644 --- a/MediaBrowser.ServerApplication/Networking/NativeMethods.cs +++ b/MediaBrowser.ServerApplication/Networking/NativeMethods.cs @@ -47,9 +47,108 @@ namespace MediaBrowser.ServerApplication.Networking /// System.Int32. [DllImport("Netapi32", SetLastError = true), SuppressUnmanagedCodeSecurity] - public static extern int NetApiBufferFree( IntPtr pBuf); + + [DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool FileTimeToSystemTime( + [In] ref long fileTime, + out SystemTime systemTime); + + [DllImport("AdvApi32.dll", SetLastError = true, ExactSpelling = true)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool CryptAcquireContextW( + out IntPtr providerContext, + [MarshalAs(UnmanagedType.LPWStr)] string container, + [MarshalAs(UnmanagedType.LPWStr)] string provider, + int providerType, + int flags); + + [DllImport("AdvApi32.dll", SetLastError = true, ExactSpelling = true)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool CryptReleaseContext( + IntPtr providerContext, + int flags); + + [DllImport("AdvApi32.dll", SetLastError = true, ExactSpelling = true)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool CryptGenKey( + IntPtr providerContext, + int algorithmId, + int flags, + out IntPtr cryptKeyHandle); + + [DllImport("AdvApi32.dll", SetLastError = true, ExactSpelling = true)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool CryptDestroyKey( + IntPtr cryptKeyHandle); + + [DllImport("Crypt32.dll", SetLastError = true, ExactSpelling = true)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool CertStrToNameW( + int certificateEncodingType, + IntPtr x500, + int strType, + IntPtr reserved, + [MarshalAs(UnmanagedType.LPArray)] [Out] byte[] encoded, + ref int encodedLength, + out IntPtr errorString); + + [DllImport("Crypt32.dll", SetLastError = true, ExactSpelling = true)] + public static extern IntPtr CertCreateSelfSignCertificate( + IntPtr providerHandle, + [In] ref CryptoApiBlob subjectIssuerBlob, + int flags, + [In] ref CryptKeyProviderInformation keyProviderInformation, + [In] ref CryptAlgorithmIdentifier algorithmIdentifier, + [In] ref SystemTime startTime, + [In] ref SystemTime endTime, + IntPtr extensions); + + [DllImport("Crypt32.dll", SetLastError = true, ExactSpelling = true)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool CertFreeCertificateContext( + IntPtr certificateContext); + + [DllImport("Crypt32.dll", SetLastError = true, ExactSpelling = true)] + public static extern IntPtr CertOpenStore( + [MarshalAs(UnmanagedType.LPStr)] string storeProvider, + int messageAndCertificateEncodingType, + IntPtr cryptProvHandle, + int flags, + IntPtr parameters); + + [DllImport("Crypt32.dll", SetLastError = true, ExactSpelling = true)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool CertCloseStore( + IntPtr certificateStoreHandle, + int flags); + + [DllImport("Crypt32.dll", SetLastError = true, ExactSpelling = true)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool CertAddCertificateContextToStore( + IntPtr certificateStoreHandle, + IntPtr certificateContext, + int addDisposition, + out IntPtr storeContextPtr); + + [DllImport("Crypt32.dll", SetLastError = true, ExactSpelling = true)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool CertSetCertificateContextProperty( + IntPtr certificateContext, + int propertyId, + int flags, + [In] ref CryptKeyProviderInformation data); + + [DllImport("Crypt32.dll", SetLastError = true, ExactSpelling = true)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool PFXExportCertStoreEx( + IntPtr certificateStoreHandle, + ref CryptoApiBlob pfxBlob, + IntPtr password, + IntPtr reserved, + int flags); } //create a _SERVER_INFO_100 STRUCTURE @@ -69,4 +168,59 @@ namespace MediaBrowser.ServerApplication.Networking [MarshalAs(UnmanagedType.LPWStr)] internal string sv100_name; } + + [StructLayout(LayoutKind.Sequential)] + public struct SystemTime + { + public short Year; + public short Month; + public short DayOfWeek; + public short Day; + public short Hour; + public short Minute; + public short Second; + public short Milliseconds; + } + + [StructLayout(LayoutKind.Sequential)] + public struct CryptObjIdBlob + { + public uint cbData; + public IntPtr pbData; + } + + [StructLayout(LayoutKind.Sequential)] + public struct CryptAlgorithmIdentifier + { + [MarshalAs(UnmanagedType.LPStr)] + public String pszObjId; + public CryptObjIdBlob Parameters; + } + + [StructLayout(LayoutKind.Sequential)] + public struct CryptoApiBlob + { + public int DataLength; + public IntPtr Data; + + public CryptoApiBlob(int dataLength, IntPtr data) + { + this.DataLength = dataLength; + this.Data = data; + } + } + + [StructLayout(LayoutKind.Sequential)] + public struct CryptKeyProviderInformation + { + [MarshalAs(UnmanagedType.LPWStr)] + public string ContainerName; + [MarshalAs(UnmanagedType.LPWStr)] + public string ProviderName; + public int ProviderType; + public int Flags; + public int ProviderParameterCount; + public IntPtr ProviderParameters; // PCRYPT_KEY_PROV_PARAM + public int KeySpec; + } } diff --git a/MediaBrowser.ServerApplication/Networking/NetworkManager.cs b/MediaBrowser.ServerApplication/Networking/NetworkManager.cs index fc4d263636..978a29db00 100644 --- a/MediaBrowser.ServerApplication/Networking/NetworkManager.cs +++ b/MediaBrowser.ServerApplication/Networking/NetworkManager.cs @@ -155,6 +155,16 @@ namespace MediaBrowser.ServerApplication.Networking }); } + /// + /// Generates a self signed certificate at the locatation specified by . + /// + /// The path to generate the certificate. + /// The common name for the certificate. + public void GenerateSelfSignedSslCertificate(string certificatePath, string hostname) + { + CertificateGenerator.CreateSelfSignCertificatePfx(certificatePath, hostname, Logger); + } + /// /// Gets the network prefix. ///