Azure Relay (Hybrid Connections) Basic Example

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);
        }
    }    
}
4 Likes

This snippet is from my Klient documentation, but it has the info for setup.

Setting up Azure Relay Hybrid Connections

  • You need to have an Azure Account.

  • Go to Home - Microsoft Azure

  • Click Create a Resource, search for Relay, select Create → Relay under the Relay Tile

  • Project Details: Select your subscription, and chose a resource group, or create a new one.

  • Instance Details: Create a name for your relay, and chose a location. (Plugin config file: RelayName)

  • Click Review + Create Then Create (After Validation Success)

  • Wait for it to deploy…

  • After deployment, chose Go To Resource

  • Click Shared access policies

  • Click RootManageSharedAccessKey (Plugin config file: KeyName)

  • Copy your Primary Key (Plugin config file: Key)

  • Click Overview in the left tree, then + Hybrid Connection at the top.

  • Provide a name. If you are using this individually, use your Epicor User ID. If you are want to use this in a custom fashion, for example on a server, then chose a custom name.

  • Unless you want the whole world to be able to send messages, make sure you leave the Requires Client Authorization box checked!!!

  • The above name you created will be the ConnectionName in the plugin config file.

  • Click Create

  • You should be done.


AzureRelayExample

@tmayfield @TomAlexander

2 Likes

Please note, this is just a simple example, not production worthy code.
Need to add your own error checking and logic to get it up to snuff.

Thank you so much for this. You are one of the Yoda’s on this site.

I do have a question. Are you using Azure API Management (API Gateway) to apply policies and secure the communication to Azure Relay?

1 Like

No, and to be honest, I don’t even know how.

I’m relying on the SAS Token.

Aw okay. I’m looking into that. Once I figure it out, I’ll post it out here.

1 Like

If you enroll the local server using Azure Arc (free), Azure will assign a managed identity to the server as if it were in Azure. You can then use that to grant permissions to your local server for many Azure services (Key Vault, Azure Relay, etc.).

2 Likes

Very interesting Mark, I may look into that for some other purposes.

Microsoft Azure Managed Identity Deep Dive (John Savill - youtube.com)

2 Likes

48 minutes → nice, but gonna procrastinate :wink:

2 Likes

Hard to believe there aren’t more questions on this.

If I didn’t explain it very well, y’all let me know.

It’s pretty useful.