Hello all, I promised a simple example a few weeks ago, so here it is.
I was going to wait until my “Klient” software was released, but I’ve just been too busy so I decided to go ahead and push this out.
This is an example of setting up an Azure Relay Hybrid Connection
This can be used to communicate from Epicor in the cloud, to your on premises resources without opening up holes in your firewall.
You set up Azure Relay as an intermediary with Microsoft, and you have an on premises “listener” that connects to it and hosts your API.
Inside Epicor, you send a message to Azure Relay, and your listener will pick it up, and respond if necessary.
Anyway, here is the library, code, and a snippet from my Klient Documentation about how to set up a relay.
Library → AzureRelayExample.efxj (6.6 KB)
Listener → AzureRelayReceiver.zip (35.7 KB)
Epicor Function 1 → CreateSASToken
/*
* ==========================================================================================
* AUTHOR: Kevin Lincecum
* COPYRIGHT: Kevin Lincecum 2024
* LICENSE: MIT
* ==========================================================================================
* Library: AzureRelayExample
* Function: CreateSASToken
* Description: Creates a token for your Azure Relay Hybrid Connection Listener
* ==========================================================================================
*
* INPUTS:
* STRING: ResourceURI -> What URI this is for
* STRING: KeyName -> They key name
* STRING: Key -> The secret key
* INT: ValidSeconds -> How long this is valid for (seconds)
*
* OUTPUTS:
* BOOL: Success -> Did we succeed?
* STRING: ErrorJson -> Serialized exception
* STRING: Token -> SAS Token Generated
*
* CHANGELOG:
* 2024/10/09 | klincecum | Kevin Lincecum | Initial Implementation
*
* ==========================================================================================
*/
try
{
TimeSpan sinceEpoch = DateTime.UtcNow - new DateTime(1970, 1, 1);
string expiry = Convert.ToString((int)sinceEpoch.TotalSeconds + ValidSeconds);
string stringToSign = HttpUtility.UrlEncode(ResourceURI) + "\n" + expiry;
HMACSHA256 hmac = new HMACSHA256(Encoding.UTF8.GetBytes(Key));
string signature = Convert.ToBase64String(hmac.ComputeHash(Encoding.UTF8.GetBytes(stringToSign)));
Token = $"SharedAccessSignature sr={HttpUtility.UrlEncode(ResourceURI)}&sig={HttpUtility.UrlEncode(signature)}&se={expiry}&skn={KeyName}";
Success = true;
hmac.Dispose();
}
catch (Exception ex)
{
Success = false;
ErrorJson = JsonConvert.SerializeObject(ex);
Token = String.Empty;
}
Epicor Function 2 → SendMessageExample
/*
* ==========================================================================================
* AUTHOR: Kevin Lincecum
* COPYRIGHT: Kevin Lincecum 2024
* LICENSE: MIT
* ==========================================================================================
* Library: AzureRelayExample
* Function: SendMessageExample
* Description: Sends an example message to your Azure Relay Hybrid Connection
* ==========================================================================================
*
* INPUTS:
* STRING: Message -> A string message for the relay listener
*
* OUTPUTS:
* STRING: Response -> A response from the relay listerner
*
* CHANGELOG:
* 2024/10/09 | klincecum | Kevin Lincecum | Initial Implementation
*
* ==========================================================================================
*/
//using Newtonsoft.Json;
//using RestSharp;
try
{
string RelayName = "yourRelayName";
string RelayNamespace = "servicebus.windows.net";
string RelayFullNamespace = $"{RelayName}.{RelayNamespace}";
string ConnectionName = "yourConnectionName"; //Session.UserID.ToLower(); //I have it set up as my name is why this is here
string KeyName = "RootManageSharedAccessKey";
string Key = "longSecretKeyHere";
int KeyValidSeconds = 3600;
var tokenResponse = ThisLib.CreateSASToken($"https://{RelayFullNamespace}", KeyName, Key, KeyValidSeconds);
if(tokenResponse.Success == false) throw new BLException(tokenResponse.ErrorJson);
string token = tokenResponse.Token;
string endPoint = $"https://{RelayFullNamespace}/{ConnectionName}";
var testObject = new { Message };
var client = new RestClient();
var request = new RestRequest(endPoint){Method = Method.POST};
request.AddHeader("ServiceBusAuthorization", token);
request.AddJsonBody(testObject);
Response = client.Execute(request).Content;
}
catch (Exception ex)
{
Response = ex.ToString();
}
ListenerProject → AzureRelayReceiver
using System.Net;
using System.Text;
using Newtonsoft.Json; //Didn't use in this example
using Microsoft.Azure.Relay;
namespace AzureRelayReceiver;
class Program
{
static ManualResetEvent terminateEvent = new ManualResetEvent(false);
static string RelayName = "yourRelayName";
static string RelayNamespace = "servicebus.windows.net";
static string RelayFullNamespace => $"{RelayName}.{RelayNamespace}";
static string ConnectionName = "yourConnectionName";
static string KeyName = "RootManageSharedAccessKey";
static string Key = "longSecretKeyHere";
static void Main(string[] args)
{
Console.CancelKeyPress += (sender, eArgs) => {
terminateEvent.Set();
eArgs.Cancel = true;
};
RunAsync().GetAwaiter().GetResult();
}
private static async Task RunAsync()
{
var tokenProvider = TokenProvider.CreateSharedAccessSignatureTokenProvider(KeyName, Key);
var listener = new HybridConnectionListener(new Uri(string.Format("sb://{0}/{1}", RelayFullNamespace, ConnectionName)), tokenProvider);
listener.Connecting += (o, e) => { Console.WriteLine("Connecting"); };
listener.Offline += (o, e) => { Console.WriteLine("Offline"); };
listener.Online += (o, e) => { Console.WriteLine("Online"); };
listener.RequestHandler = HttpRequestHandler;
await listener.OpenAsync();
Console.WriteLine("Server listening");
terminateEvent.WaitOne();
await listener.CloseAsync();
}
static async void HttpRequestHandler(RelayedHttpListenerContext context)
{
try
{
// Do something with context.Request.Url, HttpMethod, Headers, InputStream...
context.Response.StatusCode = HttpStatusCode.OK;
context.Response.StatusDescription = "Got it.";
if( context.Request.HttpMethod.ToLower() == "get") {}
if( context.Request.HttpMethod.ToLower() == "post" && context.Request.HasEntityBody)
{
string inputBody = ReadContextStream(context.Request.InputStream);
Console.WriteLine("Received ->");
Console.WriteLine(inputBody);
string returnMessage = "I got your message.";
WriteContextStream(context.Response.OutputStream, returnMessage);
Console.WriteLine();
Console.WriteLine($"Returned -> {returnMessage}");
Console.WriteLine();
Console.Beep(1000, 150);
}
}
catch (Exception ex)
{
context.Response.StatusCode = HttpStatusCode.InternalServerError;
context.Response.StatusDescription = "Uh oh...";
WriteContextStream(context.Response.OutputStream, ex.ToString());
Console.WriteLine();
Console.WriteLine($"Returned -> {ex.ToString()}");
Console.WriteLine();
}
finally
{
// The context MUST be closed here
context.Response.Close();
}
}
static string ReadContextStream(Stream contextStream)
{
string data = String.Empty;
using (StreamReader reader = new StreamReader(contextStream, Encoding.UTF8))
{
data = reader.ReadToEnd();
}
return data;
}
static void WriteContextStream(Stream contextStream, string content)
{
using (StreamWriter streamWriter = new StreamWriter(contextStream))
{
streamWriter.WriteLine(content);
}
}
}