mirror of
https://github.com/PepperDash/PepperDashCore.git
synced 2026-02-06 16:24:51 +00:00
feat!: support only .NET Framework 4.7.2
In order to conform with the plugin format and the workflow, the .csproj file was moved up a level to the root of the `src` folder and the solution file was renamed. Workflows were also changed to match the plugin workflows. BREAKING CHANGE: Supports ONLY .NET Framework 4.7.2
This commit is contained in:
356
src/Web/BouncyCertificate.cs
Normal file
356
src/Web/BouncyCertificate.cs
Normal file
@@ -0,0 +1,356 @@
|
||||
using Crestron.SimplSharp;
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Org.BouncyCastle.Asn1;
|
||||
using Org.BouncyCastle.Asn1.X509;
|
||||
using Org.BouncyCastle.Crypto;
|
||||
using Org.BouncyCastle.Crypto.Generators;
|
||||
using Org.BouncyCastle.Crypto.Prng;
|
||||
using Org.BouncyCastle.Pkcs;
|
||||
using Org.BouncyCastle.Security;
|
||||
using Org.BouncyCastle.Utilities;
|
||||
using Org.BouncyCastle.X509;
|
||||
using X509Certificate2 = System.Security.Cryptography.X509Certificates.X509Certificate2;
|
||||
using X509KeyStorageFlags = System.Security.Cryptography.X509Certificates.X509KeyStorageFlags;
|
||||
using X509ContentType = System.Security.Cryptography.X509Certificates.X509ContentType;
|
||||
using Org.BouncyCastle.Crypto.Operators;
|
||||
using BigInteger = Org.BouncyCastle.Math.BigInteger;
|
||||
using X509Certificate = Org.BouncyCastle.X509.X509Certificate;
|
||||
|
||||
namespace PepperDash.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Taken From https://github.com/rlipscombe/bouncy-castle-csharp/
|
||||
/// </summary>
|
||||
internal class BouncyCertificate
|
||||
{
|
||||
public string CertificatePassword { get; set; } = "password";
|
||||
public X509Certificate2 LoadCertificate(string issuerFileName, string password)
|
||||
{
|
||||
// We need to pass 'Exportable', otherwise we can't get the private key.
|
||||
var issuerCertificate = new X509Certificate2(issuerFileName, password, X509KeyStorageFlags.Exportable);
|
||||
return issuerCertificate;
|
||||
}
|
||||
|
||||
public X509Certificate2 IssueCertificate(string subjectName, X509Certificate2 issuerCertificate, string[] subjectAlternativeNames, KeyPurposeID[] usages)
|
||||
{
|
||||
// It's self-signed, so these are the same.
|
||||
var issuerName = issuerCertificate.Subject;
|
||||
|
||||
var random = GetSecureRandom();
|
||||
var subjectKeyPair = GenerateKeyPair(random, 2048);
|
||||
|
||||
var issuerKeyPair = DotNetUtilities.GetKeyPair(issuerCertificate.PrivateKey);
|
||||
|
||||
var serialNumber = GenerateSerialNumber(random);
|
||||
var issuerSerialNumber = new BigInteger(issuerCertificate.GetSerialNumber());
|
||||
|
||||
const bool isCertificateAuthority = false;
|
||||
var certificate = GenerateCertificate(random, subjectName, subjectKeyPair, serialNumber,
|
||||
subjectAlternativeNames, issuerName, issuerKeyPair,
|
||||
issuerSerialNumber, isCertificateAuthority,
|
||||
usages);
|
||||
return ConvertCertificate(certificate, subjectKeyPair, random);
|
||||
}
|
||||
|
||||
public X509Certificate2 CreateCertificateAuthorityCertificate(string subjectName, string[] subjectAlternativeNames, KeyPurposeID[] usages)
|
||||
{
|
||||
// It's self-signed, so these are the same.
|
||||
var issuerName = subjectName;
|
||||
|
||||
var random = GetSecureRandom();
|
||||
var subjectKeyPair = GenerateKeyPair(random, 2048);
|
||||
|
||||
// It's self-signed, so these are the same.
|
||||
var issuerKeyPair = subjectKeyPair;
|
||||
|
||||
var serialNumber = GenerateSerialNumber(random);
|
||||
var issuerSerialNumber = serialNumber; // Self-signed, so it's the same serial number.
|
||||
|
||||
const bool isCertificateAuthority = true;
|
||||
var certificate = GenerateCertificate(random, subjectName, subjectKeyPair, serialNumber,
|
||||
subjectAlternativeNames, issuerName, issuerKeyPair,
|
||||
issuerSerialNumber, isCertificateAuthority,
|
||||
usages);
|
||||
return ConvertCertificate(certificate, subjectKeyPair, random);
|
||||
}
|
||||
|
||||
public X509Certificate2 CreateSelfSignedCertificate(string subjectName, string[] subjectAlternativeNames, KeyPurposeID[] usages)
|
||||
{
|
||||
// It's self-signed, so these are the same.
|
||||
var issuerName = subjectName;
|
||||
|
||||
var random = GetSecureRandom();
|
||||
var subjectKeyPair = GenerateKeyPair(random, 2048);
|
||||
|
||||
// It's self-signed, so these are the same.
|
||||
var issuerKeyPair = subjectKeyPair;
|
||||
|
||||
var serialNumber = GenerateSerialNumber(random);
|
||||
var issuerSerialNumber = serialNumber; // Self-signed, so it's the same serial number.
|
||||
|
||||
const bool isCertificateAuthority = false;
|
||||
var certificate = GenerateCertificate(random, subjectName, subjectKeyPair, serialNumber,
|
||||
subjectAlternativeNames, issuerName, issuerKeyPair,
|
||||
issuerSerialNumber, isCertificateAuthority,
|
||||
usages);
|
||||
return ConvertCertificate(certificate, subjectKeyPair, random);
|
||||
}
|
||||
|
||||
private SecureRandom GetSecureRandom()
|
||||
{
|
||||
// Since we're on Windows, we'll use the CryptoAPI one (on the assumption
|
||||
// that it might have access to better sources of entropy than the built-in
|
||||
// Bouncy Castle ones):
|
||||
var randomGenerator = new CryptoApiRandomGenerator();
|
||||
var random = new SecureRandom(randomGenerator);
|
||||
return random;
|
||||
}
|
||||
|
||||
private X509Certificate GenerateCertificate(SecureRandom random,
|
||||
string subjectName,
|
||||
AsymmetricCipherKeyPair subjectKeyPair,
|
||||
BigInteger subjectSerialNumber,
|
||||
string[] subjectAlternativeNames,
|
||||
string issuerName,
|
||||
AsymmetricCipherKeyPair issuerKeyPair,
|
||||
BigInteger issuerSerialNumber,
|
||||
bool isCertificateAuthority,
|
||||
KeyPurposeID[] usages)
|
||||
{
|
||||
var certificateGenerator = new X509V3CertificateGenerator();
|
||||
|
||||
certificateGenerator.SetSerialNumber(subjectSerialNumber);
|
||||
|
||||
var issuerDN = new X509Name(issuerName);
|
||||
certificateGenerator.SetIssuerDN(issuerDN);
|
||||
|
||||
// Note: The subject can be omitted if you specify a subject alternative name (SAN).
|
||||
var subjectDN = new X509Name(subjectName);
|
||||
certificateGenerator.SetSubjectDN(subjectDN);
|
||||
|
||||
// Our certificate needs valid from/to values.
|
||||
var notBefore = DateTime.UtcNow.Date;
|
||||
var notAfter = notBefore.AddYears(2);
|
||||
|
||||
certificateGenerator.SetNotBefore(notBefore);
|
||||
certificateGenerator.SetNotAfter(notAfter);
|
||||
|
||||
// The subject's public key goes in the certificate.
|
||||
certificateGenerator.SetPublicKey(subjectKeyPair.Public);
|
||||
|
||||
AddAuthorityKeyIdentifier(certificateGenerator, issuerDN, issuerKeyPair, issuerSerialNumber);
|
||||
AddSubjectKeyIdentifier(certificateGenerator, subjectKeyPair);
|
||||
//AddBasicConstraints(certificateGenerator, isCertificateAuthority);
|
||||
|
||||
if (usages != null && usages.Any())
|
||||
AddExtendedKeyUsage(certificateGenerator, usages);
|
||||
|
||||
if (subjectAlternativeNames != null && subjectAlternativeNames.Any())
|
||||
AddSubjectAlternativeNames(certificateGenerator, subjectAlternativeNames);
|
||||
|
||||
// Set the signature algorithm. This is used to generate the thumbprint which is then signed
|
||||
// with the issuer's private key. We'll use SHA-256, which is (currently) considered fairly strong.
|
||||
const string signatureAlgorithm = "SHA256WithRSA";
|
||||
|
||||
// The certificate is signed with the issuer's private key.
|
||||
ISignatureFactory signatureFactory = new Asn1SignatureFactory(signatureAlgorithm, issuerKeyPair.Private, random);
|
||||
var certificate = certificateGenerator.Generate(signatureFactory);
|
||||
return certificate;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The certificate needs a serial number. This is used for revocation,
|
||||
/// and usually should be an incrementing index (which makes it easier to revoke a range of certificates).
|
||||
/// Since we don't have anywhere to store the incrementing index, we can just use a random number.
|
||||
/// </summary>
|
||||
/// <param name="random"></param>
|
||||
/// <returns></returns>
|
||||
private BigInteger GenerateSerialNumber(SecureRandom random)
|
||||
{
|
||||
var serialNumber =
|
||||
BigIntegers.CreateRandomInRange(
|
||||
BigInteger.One, BigInteger.ValueOf(Int64.MaxValue), random);
|
||||
return serialNumber;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate a key pair.
|
||||
/// </summary>
|
||||
/// <param name="random">The random number generator.</param>
|
||||
/// <param name="strength">The key length in bits. For RSA, 2048 bits should be considered the minimum acceptable these days.</param>
|
||||
/// <returns></returns>
|
||||
private AsymmetricCipherKeyPair GenerateKeyPair(SecureRandom random, int strength)
|
||||
{
|
||||
var keyGenerationParameters = new KeyGenerationParameters(random, strength);
|
||||
|
||||
var keyPairGenerator = new RsaKeyPairGenerator();
|
||||
keyPairGenerator.Init(keyGenerationParameters);
|
||||
var subjectKeyPair = keyPairGenerator.GenerateKeyPair();
|
||||
return subjectKeyPair;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add the Authority Key Identifier. According to http://www.alvestrand.no/objectid/2.5.29.35.html, this
|
||||
/// identifies the public key to be used to verify the signature on this certificate.
|
||||
/// In a certificate chain, this corresponds to the "Subject Key Identifier" on the *issuer* certificate.
|
||||
/// The Bouncy Castle documentation, at http://www.bouncycastle.org/wiki/display/JA1/X.509+Public+Key+Certificate+and+Certification+Request+Generation,
|
||||
/// shows how to create this from the issuing certificate. Since we're creating a self-signed certificate, we have to do this slightly differently.
|
||||
/// </summary>
|
||||
/// <param name="certificateGenerator"></param>
|
||||
/// <param name="issuerDN"></param>
|
||||
/// <param name="issuerKeyPair"></param>
|
||||
/// <param name="issuerSerialNumber"></param>
|
||||
private void AddAuthorityKeyIdentifier(X509V3CertificateGenerator certificateGenerator,
|
||||
X509Name issuerDN,
|
||||
AsymmetricCipherKeyPair issuerKeyPair,
|
||||
BigInteger issuerSerialNumber)
|
||||
{
|
||||
var authorityKeyIdentifierExtension =
|
||||
new AuthorityKeyIdentifier(
|
||||
SubjectPublicKeyInfoFactory.CreateSubjectPublicKeyInfo(issuerKeyPair.Public),
|
||||
new GeneralNames(new GeneralName(issuerDN)),
|
||||
issuerSerialNumber);
|
||||
certificateGenerator.AddExtension(
|
||||
X509Extensions.AuthorityKeyIdentifier.Id, false, authorityKeyIdentifierExtension);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add the "Subject Alternative Names" extension. Note that you have to repeat
|
||||
/// the value from the "Subject Name" property.
|
||||
/// </summary>
|
||||
/// <param name="certificateGenerator"></param>
|
||||
/// <param name="subjectAlternativeNames"></param>
|
||||
private void AddSubjectAlternativeNames(X509V3CertificateGenerator certificateGenerator,
|
||||
IEnumerable<string> subjectAlternativeNames)
|
||||
{
|
||||
var subjectAlternativeNamesExtension =
|
||||
new DerSequence(
|
||||
subjectAlternativeNames.Select(name => new GeneralName(GeneralName.DnsName, name))
|
||||
.ToArray<Asn1Encodable>());
|
||||
certificateGenerator.AddExtension(
|
||||
X509Extensions.SubjectAlternativeName.Id, false, subjectAlternativeNamesExtension);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add the "Extended Key Usage" extension, specifying (for example) "server authentication".
|
||||
/// </summary>
|
||||
/// <param name="certificateGenerator"></param>
|
||||
/// <param name="usages"></param>
|
||||
private void AddExtendedKeyUsage(X509V3CertificateGenerator certificateGenerator, KeyPurposeID[] usages)
|
||||
{
|
||||
certificateGenerator.AddExtension(
|
||||
X509Extensions.ExtendedKeyUsage.Id, false, new ExtendedKeyUsage(usages));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add the "Basic Constraints" extension.
|
||||
/// </summary>
|
||||
/// <param name="certificateGenerator"></param>
|
||||
/// <param name="isCertificateAuthority"></param>
|
||||
private void AddBasicConstraints(X509V3CertificateGenerator certificateGenerator,
|
||||
bool isCertificateAuthority)
|
||||
{
|
||||
certificateGenerator.AddExtension(
|
||||
X509Extensions.BasicConstraints.Id, true, new BasicConstraints(isCertificateAuthority));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add the Subject Key Identifier.
|
||||
/// </summary>
|
||||
/// <param name="certificateGenerator"></param>
|
||||
/// <param name="subjectKeyPair"></param>
|
||||
private void AddSubjectKeyIdentifier(X509V3CertificateGenerator certificateGenerator,
|
||||
AsymmetricCipherKeyPair subjectKeyPair)
|
||||
{
|
||||
var subjectKeyIdentifierExtension =
|
||||
new SubjectKeyIdentifier(
|
||||
SubjectPublicKeyInfoFactory.CreateSubjectPublicKeyInfo(subjectKeyPair.Public));
|
||||
certificateGenerator.AddExtension(
|
||||
X509Extensions.SubjectKeyIdentifier.Id, false, subjectKeyIdentifierExtension);
|
||||
}
|
||||
|
||||
private X509Certificate2 ConvertCertificate(X509Certificate certificate,
|
||||
AsymmetricCipherKeyPair subjectKeyPair,
|
||||
SecureRandom random)
|
||||
{
|
||||
// Now to convert the Bouncy Castle certificate to a .NET certificate.
|
||||
// See http://web.archive.org/web/20100504192226/http://www.fkollmann.de/v2/post/Creating-certificates-using-BouncyCastle.aspx
|
||||
// ...but, basically, we create a PKCS12 store (a .PFX file) in memory, and add the public and private key to that.
|
||||
var store = new Pkcs12StoreBuilder().Build();
|
||||
|
||||
// What Bouncy Castle calls "alias" is the same as what Windows terms the "friendly name".
|
||||
string friendlyName = certificate.SubjectDN.ToString();
|
||||
|
||||
// Add the certificate.
|
||||
var certificateEntry = new X509CertificateEntry(certificate);
|
||||
store.SetCertificateEntry(friendlyName, certificateEntry);
|
||||
|
||||
// Add the private key.
|
||||
store.SetKeyEntry(friendlyName, new AsymmetricKeyEntry(subjectKeyPair.Private), new[] { certificateEntry });
|
||||
|
||||
// Convert it to an X509Certificate2 object by saving/loading it from a MemoryStream.
|
||||
// It needs a password. Since we'll remove this later, it doesn't particularly matter what we use.
|
||||
|
||||
var stream = new MemoryStream();
|
||||
store.Save(stream, CertificatePassword.ToCharArray(), random);
|
||||
|
||||
var convertedCertificate =
|
||||
new X509Certificate2(stream.ToArray(),
|
||||
CertificatePassword,
|
||||
X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.Exportable);
|
||||
return convertedCertificate;
|
||||
}
|
||||
|
||||
public void WriteCertificate(X509Certificate2 certificate, string outputDirectory, string certName)
|
||||
{
|
||||
// This password is the one attached to the PFX file. Use 'null' for no password.
|
||||
// Create PFX (PKCS #12) with private key
|
||||
try
|
||||
{
|
||||
var pfx = certificate.Export(X509ContentType.Pfx, CertificatePassword);
|
||||
File.WriteAllBytes(string.Format("{0}.pfx", Path.Combine(outputDirectory, certName)), pfx);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
CrestronConsole.PrintLine(string.Format("Failed to write x509 cert pfx\r\n{0}", ex.Message));
|
||||
}
|
||||
// Create Base 64 encoded CER (public key only)
|
||||
using (var writer = new StreamWriter($"{Path.Combine(outputDirectory, certName)}.cer", false))
|
||||
{
|
||||
try
|
||||
{
|
||||
var contents = string.Format("-----BEGIN CERTIFICATE-----\r\n{0}\r\n-----END CERTIFICATE-----", Convert.ToBase64String(certificate.Export(X509ContentType.Cert), Base64FormattingOptions.InsertLineBreaks));
|
||||
writer.Write(contents);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
CrestronConsole.PrintLine(string.Format("Failed to write x509 cert cer\r\n{0}", ex.Message));
|
||||
}
|
||||
}
|
||||
}
|
||||
public bool AddCertToStore(X509Certificate2 cert, System.Security.Cryptography.X509Certificates.StoreName st, System.Security.Cryptography.X509Certificates.StoreLocation sl)
|
||||
{
|
||||
bool bRet = false;
|
||||
|
||||
try
|
||||
{
|
||||
var store = new System.Security.Cryptography.X509Certificates.X509Store(st, sl);
|
||||
store.Open(System.Security.Cryptography.X509Certificates.OpenFlags.ReadWrite);
|
||||
store.Add(cert);
|
||||
|
||||
store.Close();
|
||||
bRet = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
CrestronConsole.PrintLine(string.Format("AddCertToStore Failed\r\n{0}\r\n{1}", ex.Message, ex.StackTrace));
|
||||
}
|
||||
|
||||
return bRet;
|
||||
}
|
||||
}
|
||||
}
|
||||
17
src/Web/RequestHandlers/DefaultRequestHandler.cs
Normal file
17
src/Web/RequestHandlers/DefaultRequestHandler.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using Crestron.SimplSharp.WebScripting;
|
||||
|
||||
namespace PepperDash.Core.Web.RequestHandlers
|
||||
{
|
||||
/// <summary>
|
||||
/// Web API default request handler
|
||||
/// </summary>
|
||||
public class DefaultRequestHandler : WebApiBaseRequestHandler
|
||||
{
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
/// </summary>
|
||||
public DefaultRequestHandler()
|
||||
: base(true)
|
||||
{ }
|
||||
}
|
||||
}
|
||||
163
src/Web/RequestHandlers/WebApiBaseRequestAsyncHandler.cs
Normal file
163
src/Web/RequestHandlers/WebApiBaseRequestAsyncHandler.cs
Normal file
@@ -0,0 +1,163 @@
|
||||
using Crestron.SimplSharp.WebScripting;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace PepperDash.Core.Web.RequestHandlers
|
||||
{
|
||||
public abstract class WebApiBaseRequestAsyncHandler:IHttpCwsHandler
|
||||
{
|
||||
private readonly Dictionary<string, Func<HttpCwsContext, Task>> _handlers;
|
||||
protected readonly bool EnableCors;
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
/// </summary>
|
||||
protected WebApiBaseRequestAsyncHandler(bool enableCors)
|
||||
{
|
||||
EnableCors = enableCors;
|
||||
|
||||
_handlers = new Dictionary<string, Func<HttpCwsContext, Task>>
|
||||
{
|
||||
{"CONNECT", HandleConnect},
|
||||
{"DELETE", HandleDelete},
|
||||
{"GET", HandleGet},
|
||||
{"HEAD", HandleHead},
|
||||
{"OPTIONS", HandleOptions},
|
||||
{"PATCH", HandlePatch},
|
||||
{"POST", HandlePost},
|
||||
{"PUT", HandlePut},
|
||||
{"TRACE", HandleTrace}
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
/// </summary>
|
||||
protected WebApiBaseRequestAsyncHandler()
|
||||
: this(false)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles CONNECT method requests
|
||||
/// </summary>
|
||||
/// <param name="context"></param>
|
||||
protected virtual async Task HandleConnect(HttpCwsContext context)
|
||||
{
|
||||
context.Response.StatusCode = 501;
|
||||
context.Response.StatusDescription = "Not Implemented";
|
||||
context.Response.End();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles DELETE method requests
|
||||
/// </summary>
|
||||
/// <param name="context"></param>
|
||||
protected virtual async Task HandleDelete(HttpCwsContext context)
|
||||
{
|
||||
context.Response.StatusCode = 501;
|
||||
context.Response.StatusDescription = "Not Implemented";
|
||||
context.Response.End();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles GET method requests
|
||||
/// </summary>
|
||||
/// <param name="context"></param>
|
||||
protected virtual async Task HandleGet(HttpCwsContext context)
|
||||
{
|
||||
context.Response.StatusCode = 501;
|
||||
context.Response.StatusDescription = "Not Implemented";
|
||||
context.Response.End();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles HEAD method requests
|
||||
/// </summary>
|
||||
/// <param name="context"></param>
|
||||
protected virtual async Task HandleHead(HttpCwsContext context)
|
||||
{
|
||||
context.Response.StatusCode = 501;
|
||||
context.Response.StatusDescription = "Not Implemented";
|
||||
context.Response.End();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles OPTIONS method requests
|
||||
/// </summary>
|
||||
/// <param name="context"></param>
|
||||
protected virtual async Task HandleOptions(HttpCwsContext context)
|
||||
{
|
||||
context.Response.StatusCode = 501;
|
||||
context.Response.StatusDescription = "Not Implemented";
|
||||
context.Response.End();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles PATCH method requests
|
||||
/// </summary>
|
||||
/// <param name="context"></param>
|
||||
protected virtual async Task HandlePatch(HttpCwsContext context)
|
||||
{
|
||||
context.Response.StatusCode = 501;
|
||||
context.Response.StatusDescription = "Not Implemented";
|
||||
context.Response.End();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles POST method requests
|
||||
/// </summary>
|
||||
/// <param name="context"></param>
|
||||
protected virtual async Task HandlePost(HttpCwsContext context)
|
||||
{
|
||||
context.Response.StatusCode = 501;
|
||||
context.Response.StatusDescription = "Not Implemented";
|
||||
context.Response.End();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles PUT method requests
|
||||
/// </summary>
|
||||
/// <param name="context"></param>
|
||||
protected virtual async Task HandlePut(HttpCwsContext context)
|
||||
{
|
||||
context.Response.StatusCode = 501;
|
||||
context.Response.StatusDescription = "Not Implemented";
|
||||
context.Response.End();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles TRACE method requests
|
||||
/// </summary>
|
||||
/// <param name="context"></param>
|
||||
protected virtual async Task HandleTrace(HttpCwsContext context)
|
||||
{
|
||||
context.Response.StatusCode = 501;
|
||||
context.Response.StatusDescription = "Not Implemented";
|
||||
context.Response.End();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Process request
|
||||
/// </summary>
|
||||
/// <param name="context"></param>
|
||||
public void ProcessRequest(HttpCwsContext context)
|
||||
{
|
||||
if (!_handlers.TryGetValue(context.Request.HttpMethod, out Func<HttpCwsContext, Task> handler))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (EnableCors)
|
||||
{
|
||||
context.Response.Headers.Add("Access-Control-Allow-Origin", "*");
|
||||
context.Response.Headers.Add("Access-Control-Allow-Methods", "POST, GET, OPTIONS");
|
||||
}
|
||||
|
||||
var handlerTask = handler(context);
|
||||
|
||||
handlerTask.GetAwaiter().GetResult();
|
||||
}
|
||||
}
|
||||
}
|
||||
165
src/Web/RequestHandlers/WebApiBaseRequestHandler.cs
Normal file
165
src/Web/RequestHandlers/WebApiBaseRequestHandler.cs
Normal file
@@ -0,0 +1,165 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Crestron.SimplSharp.WebScripting;
|
||||
|
||||
namespace PepperDash.Core.Web.RequestHandlers
|
||||
{
|
||||
/// <summary>
|
||||
/// CWS Base Handler, implements IHttpCwsHandler
|
||||
/// </summary>
|
||||
public abstract class WebApiBaseRequestHandler : IHttpCwsHandler
|
||||
{
|
||||
private readonly Dictionary<string, Action<HttpCwsContext>> _handlers;
|
||||
protected readonly bool EnableCors;
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
/// </summary>
|
||||
protected WebApiBaseRequestHandler(bool enableCors)
|
||||
{
|
||||
EnableCors = enableCors;
|
||||
|
||||
_handlers = new Dictionary<string, Action<HttpCwsContext>>
|
||||
{
|
||||
{"CONNECT", HandleConnect},
|
||||
{"DELETE", HandleDelete},
|
||||
{"GET", HandleGet},
|
||||
{"HEAD", HandleHead},
|
||||
{"OPTIONS", HandleOptions},
|
||||
{"PATCH", HandlePatch},
|
||||
{"POST", HandlePost},
|
||||
{"PUT", HandlePut},
|
||||
{"TRACE", HandleTrace}
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
/// </summary>
|
||||
protected WebApiBaseRequestHandler()
|
||||
: this(false)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles CONNECT method requests
|
||||
/// </summary>
|
||||
/// <param name="context"></param>
|
||||
protected virtual void HandleConnect(HttpCwsContext context)
|
||||
{
|
||||
context.Response.StatusCode = 501;
|
||||
context.Response.StatusDescription = "Not Implemented";
|
||||
context.Response.End();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles DELETE method requests
|
||||
/// </summary>
|
||||
/// <param name="context"></param>
|
||||
protected virtual void HandleDelete(HttpCwsContext context)
|
||||
{
|
||||
context.Response.StatusCode = 501;
|
||||
context.Response.StatusDescription = "Not Implemented";
|
||||
context.Response.End();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles GET method requests
|
||||
/// </summary>
|
||||
/// <param name="context"></param>
|
||||
protected virtual void HandleGet(HttpCwsContext context)
|
||||
{
|
||||
context.Response.StatusCode = 501;
|
||||
context.Response.StatusDescription = "Not Implemented";
|
||||
context.Response.End();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles HEAD method requests
|
||||
/// </summary>
|
||||
/// <param name="context"></param>
|
||||
protected virtual void HandleHead(HttpCwsContext context)
|
||||
{
|
||||
context.Response.StatusCode = 501;
|
||||
context.Response.StatusDescription = "Not Implemented";
|
||||
context.Response.End();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles OPTIONS method requests
|
||||
/// </summary>
|
||||
/// <param name="context"></param>
|
||||
protected virtual void HandleOptions(HttpCwsContext context)
|
||||
{
|
||||
context.Response.StatusCode = 501;
|
||||
context.Response.StatusDescription = "Not Implemented";
|
||||
context.Response.End();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles PATCH method requests
|
||||
/// </summary>
|
||||
/// <param name="context"></param>
|
||||
protected virtual void HandlePatch(HttpCwsContext context)
|
||||
{
|
||||
context.Response.StatusCode = 501;
|
||||
context.Response.StatusDescription = "Not Implemented";
|
||||
context.Response.End();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles POST method requests
|
||||
/// </summary>
|
||||
/// <param name="context"></param>
|
||||
protected virtual void HandlePost(HttpCwsContext context)
|
||||
{
|
||||
context.Response.StatusCode = 501;
|
||||
context.Response.StatusDescription = "Not Implemented";
|
||||
context.Response.End();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles PUT method requests
|
||||
/// </summary>
|
||||
/// <param name="context"></param>
|
||||
protected virtual void HandlePut(HttpCwsContext context)
|
||||
{
|
||||
context.Response.StatusCode = 501;
|
||||
context.Response.StatusDescription = "Not Implemented";
|
||||
context.Response.End();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles TRACE method requests
|
||||
/// </summary>
|
||||
/// <param name="context"></param>
|
||||
protected virtual void HandleTrace(HttpCwsContext context)
|
||||
{
|
||||
context.Response.StatusCode = 501;
|
||||
context.Response.StatusDescription = "Not Implemented";
|
||||
context.Response.End();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Process request
|
||||
/// </summary>
|
||||
/// <param name="context"></param>
|
||||
public void ProcessRequest(HttpCwsContext context)
|
||||
{
|
||||
Action<HttpCwsContext> handler;
|
||||
|
||||
if (!_handlers.TryGetValue(context.Request.HttpMethod, out handler))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (EnableCors)
|
||||
{
|
||||
context.Response.Headers.Add("Access-Control-Allow-Origin", "*");
|
||||
context.Response.Headers.Add("Access-Control-Allow-Methods", "POST, GET, OPTIONS");
|
||||
}
|
||||
|
||||
handler(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
284
src/Web/WebApiServer.cs
Normal file
284
src/Web/WebApiServer.cs
Normal file
@@ -0,0 +1,284 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Crestron.SimplSharp;
|
||||
using Crestron.SimplSharp.WebScripting;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using PepperDash.Core.Web.RequestHandlers;
|
||||
|
||||
namespace PepperDash.Core.Web
|
||||
{
|
||||
/// <summary>
|
||||
/// Web API server
|
||||
/// </summary>
|
||||
public class WebApiServer : IKeyName
|
||||
{
|
||||
private const string SplusKey = "Uninitialized Web API Server";
|
||||
private const string DefaultName = "Web API Server";
|
||||
private const string DefaultBasePath = "/api";
|
||||
|
||||
private const uint DebugTrace = 0;
|
||||
private const uint DebugInfo = 1;
|
||||
private const uint DebugVerbose = 2;
|
||||
|
||||
private readonly CCriticalSection _serverLock = new CCriticalSection();
|
||||
private HttpCwsServer _server;
|
||||
|
||||
/// <summary>
|
||||
/// Web API server key
|
||||
/// </summary>
|
||||
public string Key { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Web API server name
|
||||
/// </summary>
|
||||
public string Name { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// CWS base path, will default to "/api" if not set via initialize method
|
||||
/// </summary>
|
||||
public string BasePath { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Indicates CWS is registered with base path
|
||||
/// </summary>
|
||||
public bool IsRegistered { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Http request handler
|
||||
/// </summary>
|
||||
//public IHttpCwsHandler HttpRequestHandler
|
||||
//{
|
||||
// get { return _server.HttpRequestHandler; }
|
||||
// set
|
||||
// {
|
||||
// if (_server == null) return;
|
||||
// _server.HttpRequestHandler = value;
|
||||
// }
|
||||
//}
|
||||
|
||||
/// <summary>
|
||||
/// Received request event handler
|
||||
/// </summary>
|
||||
//public event EventHandler<HttpCwsRequestEventArgs> ReceivedRequestEvent
|
||||
//{
|
||||
// add { _server.ReceivedRequestEvent += new HttpCwsRequestEventHandler(value); }
|
||||
// remove { _server.ReceivedRequestEvent -= new HttpCwsRequestEventHandler(value); }
|
||||
//}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for S+. Make sure to set necessary properties using init method
|
||||
/// </summary>
|
||||
public WebApiServer()
|
||||
: this(SplusKey, DefaultName, null)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="basePath"></param>
|
||||
public WebApiServer(string key, string basePath)
|
||||
: this(key, DefaultName, basePath)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="name"></param>
|
||||
/// <param name="basePath"></param>
|
||||
public WebApiServer(string key, string name, string basePath)
|
||||
{
|
||||
Key = key;
|
||||
Name = string.IsNullOrEmpty(name) ? DefaultName : name;
|
||||
BasePath = string.IsNullOrEmpty(basePath) ? DefaultBasePath : basePath;
|
||||
|
||||
if (_server == null) _server = new HttpCwsServer(BasePath);
|
||||
|
||||
_server.setProcessName(Key);
|
||||
_server.HttpRequestHandler = new DefaultRequestHandler();
|
||||
|
||||
CrestronEnvironment.ProgramStatusEventHandler += CrestronEnvironment_ProgramStatusEventHandler;
|
||||
CrestronEnvironment.EthernetEventHandler += CrestronEnvironment_EthernetEventHandler;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Program status event handler
|
||||
/// </summary>
|
||||
/// <param name="programEventType"></param>
|
||||
void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType)
|
||||
{
|
||||
if (programEventType != eProgramStatusEventType.Stopping) return;
|
||||
|
||||
Debug.Console(DebugInfo, this, "Program stopping. stopping server");
|
||||
|
||||
Stop();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ethernet event handler
|
||||
/// </summary>
|
||||
/// <param name="ethernetEventArgs"></param>
|
||||
void CrestronEnvironment_EthernetEventHandler(EthernetEventArgs ethernetEventArgs)
|
||||
{
|
||||
// Re-enable the server if the link comes back up and the status should be connected
|
||||
if (ethernetEventArgs.EthernetEventType == eEthernetEventType.LinkUp && IsRegistered)
|
||||
{
|
||||
Debug.Console(DebugInfo, this, "Ethernet link up. Server is alreedy registered.");
|
||||
return;
|
||||
}
|
||||
|
||||
Debug.Console(DebugInfo, this, "Ethernet link up. Starting server");
|
||||
|
||||
Start();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes CWS class
|
||||
/// </summary>
|
||||
public void Initialize(string key, string basePath)
|
||||
{
|
||||
Key = key;
|
||||
BasePath = string.IsNullOrEmpty(basePath) ? DefaultBasePath : basePath;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a route to CWS
|
||||
/// </summary>
|
||||
public void AddRoute(HttpCwsRoute route)
|
||||
{
|
||||
if (route == null)
|
||||
{
|
||||
Debug.Console(DebugInfo, this, "Failed to add route, route parameter is null");
|
||||
return;
|
||||
}
|
||||
|
||||
_server.Routes.Add(route);
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a route from CWS
|
||||
/// </summary>
|
||||
/// <param name="route"></param>
|
||||
public void RemoveRoute(HttpCwsRoute route)
|
||||
{
|
||||
if (route == null)
|
||||
{
|
||||
Debug.Console(DebugInfo, this, "Failed to remote route, orute parameter is null");
|
||||
return;
|
||||
}
|
||||
|
||||
_server.Routes.Remove(route);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a list of the current routes
|
||||
/// </summary>
|
||||
public HttpCwsRouteCollection GetRouteCollection()
|
||||
{
|
||||
return _server.Routes;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts CWS instance
|
||||
/// </summary>
|
||||
public void Start()
|
||||
{
|
||||
try
|
||||
{
|
||||
_serverLock.Enter();
|
||||
|
||||
if (_server == null)
|
||||
{
|
||||
Debug.Console(DebugInfo, this, "Server is null, unable to start");
|
||||
return;
|
||||
}
|
||||
|
||||
if (IsRegistered)
|
||||
{
|
||||
Debug.Console(DebugInfo, this, "Server has already been started");
|
||||
return;
|
||||
}
|
||||
|
||||
IsRegistered = _server.Register();
|
||||
|
||||
Debug.Console(DebugInfo, this, "Starting server, registration {0}", IsRegistered ? "was successful" : "failed");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.Console(DebugInfo, this, "Start Exception Message: {0}", ex.Message);
|
||||
Debug.Console(DebugVerbose, this, "Start Exception StackTrace: {0}", ex.StackTrace);
|
||||
if (ex.InnerException != null)
|
||||
Debug.Console(DebugVerbose, this, "Start Exception InnerException: {0}", ex.InnerException);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_serverLock.Leave();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stop CWS instance
|
||||
/// </summary>
|
||||
public void Stop()
|
||||
{
|
||||
try
|
||||
{
|
||||
_serverLock.Enter();
|
||||
|
||||
if (_server == null)
|
||||
{
|
||||
Debug.Console(DebugInfo, this, "Server is null or has already been stopped");
|
||||
return;
|
||||
}
|
||||
|
||||
IsRegistered = _server.Unregister() == false;
|
||||
|
||||
Debug.Console(DebugInfo, this, "Stopping server, unregistration {0}", IsRegistered ? "failed" : "was successful");
|
||||
|
||||
_server.Dispose();
|
||||
_server = null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.Console(DebugInfo, this, "Server Stop Exception Message: {0}", ex.Message);
|
||||
Debug.Console(DebugVerbose, this, "Server Stop Exception StackTrace: {0}", ex.StackTrace);
|
||||
if (ex.InnerException != null)
|
||||
Debug.Console(DebugVerbose, this, "Server Stop Exception InnerException: {0}", ex.InnerException);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_serverLock.Leave();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Received request handler
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is here for development and testing
|
||||
/// </remarks>
|
||||
/// <param name="sender"></param>
|
||||
/// <param name="args"></param>
|
||||
public void ReceivedRequestEventHandler(object sender, HttpCwsRequestEventArgs args)
|
||||
{
|
||||
try
|
||||
{
|
||||
var j = JsonConvert.SerializeObject(args.Context, Formatting.Indented);
|
||||
Debug.Console(DebugVerbose, this, "RecieveRequestEventHandler Context:\x0d\x0a{0}", j);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.Console(DebugInfo, this, "ReceivedRequestEventHandler Exception Message: {0}", ex.Message);
|
||||
Debug.Console(DebugVerbose, this, "ReceivedRequestEventHandler Exception StackTrace: {0}", ex.StackTrace);
|
||||
if (ex.InnerException != null)
|
||||
Debug.Console(DebugVerbose, this, "ReceivedRequestEventHandler Exception InnerException: {0}", ex.InnerException);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user