dev

Deploying and Hosting Static Web Sites (ReactJS, Angular) in Azure App Service

Outline

On Azure, you can host a static web multiple ways

  • As separate App Service
  • Via Blob Storage and CDN
  • As a part of App Service (that hosts, e.g., an API)

Hosting As a Part of App Service

You you want to host the site in some existing App Service that already hosts some app, e.g., Web Api, it is a very simple process – just copy the built web into wwwroot folder. There is only one problem – proper routing. Because now there are basically two sources that are provided:

  • Web site
  • Api endpoints

the simplest way how to distinguish between Api and Web App is to use different ports. But it is not possible in our case because App Service supports only port 80/443. So it must be done in a different way.

Rewrite Rules

In out case we will use rewrite rules defined in immortal web.config file. Basically, we have to distinguish between following resources:

  • Api routes (third negation in wwwroot-spa-routing-to-index rule)
  • Web App root file, e.g., index.html (rules wwwroot-static-files and wwwroot-spa-routing-to-index)
  • Static files, e.g., images, fonts, styles, etc. (content of element handlers)
<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
    <system.webServer xdt:Transform="Insert">
        <rewrite>
            <rules>
                <rule name="wwwroot-static-files" stopProcessing="true">
                    <!-- Following extensions should be defined in <handlers /> section -->
                    <match url="([\S]+[.](html|htm|svg|js|css|png|gif|jpg|jpeg|woff|woff2|eot|ttf|map))" />
                    <action type="Rewrite" url="wwwroot/{R:1}" />
                </rule>
                <rule name="wwwroot-index" stopProcessing="true">
                    <match url="^$" />
                    <action type="Rewrite" url="wwwroot/index.html" />
                </rule>
                <!--
                    Note: root path must be set to "/" ,i.e., <base href="/" />
                    otherwise relative paths will not work
                -->
                <rule name="wwwroot-spa-routing-to-index" stopProcessing="true">
                    <match url=".*" />
                    <conditions logicalGrouping="MatchAll">
                        <add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
                        <add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" />
                        <add input="{REQUEST_URI}" pattern="api/" negate="true" />
                    </conditions>
                    <action type="Rewrite" url="wwwroot/index.html" />
                </rule>
            </rules>
        </rewrite>
        <handlers>
            <add name="StaticFileModuleHtml" path="*.htm*" verb="*" modules="StaticFileModule" resourceType="File"                  requireAccess="Read" />
            <add name="StaticFileModuleSvg" path="*.svg" verb="*" modules="StaticFileModule" resourceType="File"                  requireAccess="Read" />
            <add name="StaticFileModuleJs" path="*.js" verb="*" modules="StaticFileModule" resourceType="File"                  requireAccess="Read" />
            <add name="StaticFileModuleCss" path="*.css" verb="*" modules="StaticFileModule" resourceType="File"                  requireAccess="Read" />
            <add name="StaticFileModuleJpeg" path="*.jpeg" verb="*" modules="StaticFileModule" resourceType="File"                  requireAccess="Read" />
            <add name="StaticFileModuleJpg" path="*.jpg" verb="*" modules="StaticFileModule" resourceType="File"                  requireAccess="Read" />
            <add name="StaticFileModulePng" path="*.png" verb="*" modules="StaticFileModule" resourceType="File"                  requireAccess="Read" />
            <add name="StaticFileModuleGif" path="*.gif" verb="*" modules="StaticFileModule" resourceType="File"                  requireAccess="Read" />
            <add name="StaticFileModuleWoff" path="*.woff" verb="*" modules="StaticFileModule" resourceType="File"                  requireAccess="Read" />
            <add name="StaticFileModuleWoff2" path="*.woff2" verb="*" modules="StaticFileModule" resourceType="File"                  requireAccess="Read" />
            <add name="StaticFileModuleEot" path="*.eot" verb="*" modules="StaticFileModule" resourceType="File"                  requireAccess="Read" />
            <add name="StaticFileModuleTtf" path="*.ttf" verb="*" modules="StaticFileModule" resourceType="File"                  requireAccess="Read" />
            <add name="StaticFileModuleMap" path="*.map" verb="*" modules="StaticFileModule" resourceType="File"                  requireAccess="Read" />
            <add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModule" resourceType="Unspecified" />
        </handlers>
        <aspNetCore processPath="%LAUNCHER_PATH%" arguments="%LAUNCHER_ARGS%" stdoutLogEnabled="false"                     stdoutLogFile=".\logs\stdout" forwardWindowsAuthToken="false" />
    </system.webServer>
</configuration>

 

Thats all. Routes matching api/ pattern will be routed to your Web Api. All other routes except static files will be routed to the index.html, where the SPA’s routing will do its work.

Conclusion

There are many methods how to publish SPA web site in Azure. If you want to host together with your Web Api in the same App Service, all you need is a proper web.config setup.

Advertisements
dev

New Version of the GPWebpayNet v1.1.0 Has Been Released

New version of the GPWebpayNet v1.1.0 has been released. It adds validation of Digest1 property returned by GPWebpay Gateway that should brings more security while validating gateway response.

Install it from NuGet via

Install-Package GPWebpayNet.Sdk -Version 1.1.0

Clone it on GitHub here.

Enjoy!

 

 

dev

GPWebpayNet – A .NET Library for Communication With GP Webpay Payment Gateway

Overview

In these days we are interconnecting multiple services into our information systems. When we are deciding which service we will integrate, except the functionality, pricing, etc., an important aspect is ease of integration. The service should contain some documentation, but support such as SDK or package for as the most popular languages should be available too.

This is not exactly the case of GP Webpay. They provide documentation for their solution. But for example a documentation for HTTP API is from year 2016. There is no SDK or source code example, except sample code for evaluation that our implementation of calculation of digest (a hash calculated from request data) is returning valid value. These samples do nothing more. Another thing is that C# code sample is written .NET 3.5. Really, in 2018. But in newer version of .NET and especially with .NETCore, interfaces changed. So it is quite obsolete.

As I mentioned, document describing how the request should be created, signed, etc., is provided. There is funny sentence that implementation will take three days in average. Ok, but if they provide some SDK, it can take few hours. Next, when the GP Webpay change their protocol, they can just provide new SDK version so consumers can update their code very simply. Without any inspection of internals.

So when I was implementing communication with this gateway, I extracted the code and created a library.

Library Description

Library supports following .NET versions:

  • Full .NET Framework 4.6.1
  • .netstandart 1.6
  • .netstandart 2.0

Use Cases

You can use is together with WebAPI and SPA of with ASP MVC. It is up to you.

The base GP webpay process (simplified) is as follows:

  1. Generate payment request
  2. Send it via GET or POST go payment gateway
  3. Process GET request from GP webpay

The process with API and SPA is as follows:

  1. SPA send order request to API
  2. API generates payment request for GET (url) and sends it back to SPA
  3. SPA use url and redirects to it
  4. GP webpay send request to API
  5. API process request and redirect to SPA with corresponding result

The process with MVC is as follows:

  1. Send request to MVC
  2. MVC generates payment request for GET/POST and redirects to GP webpay
  3. GP webpay send request to MVC
  4. MVC process request and redirect to SPA with corresponding result

Project Structure

GPWebpayNet.Example

It contains examples how to use this SDK. For more details you can check GPWebpayNet.SDK.Spec too.

GPWebpayNet.SDK

The main project. The main project class is GPWebpayNet.Sdk.Services.ClientService that provides methods needed for payment process. Other classes are used by this class. Whole project is designed to be used with some IoC framework so classes are decoupled.

GPWebpayNet.Example

It contains SDK unit tests.

Usage Samples

Get Redirect URL for Payment Request

public string GetRedirectUrl()
{
  var loggerFactory = new LoggerFactory().AddConsole();

  const string url = Constants.GPWebpayUrlTest;
  const string privateCertificateFile = "certs/test.pfx";
  const string privateCertificateFilePassword = "test";
  const string publicCertificateFile = "certs/test.pfx";
  const string publicCertificateFilePassword = "test";
  var doc = new XmlDocument();
  doc.AppendChild(doc.CreateElement("Info"));

  var request = new PaymentRequest
  {
    MerchantNumber = "235235",
    OrderNumber = 2412,
    Amount = new decimal(64.6546),
    Currency = CurrencyCodeEnum.Eur,
    DepositFlag = 1,
    MerOrderNumber = "MerOrderNumber",
    Url = "https://www.example.org",
    Description = "Lorem ipsum dolor sit amet, consectetuer adipiscing elit.",
    MD = "Lorem ipsum dolor sit amet, consectetuer adipiscing elit.",
    PaymentMethod = PaymentMethodEnum.Mps,
    DisabledPaymentMethod = PaymentMethodEnum.Crd,
    PaymentMethods = new[] {PaymentMethodEnum.Mcm, PaymentMethodEnum.NotSet},
    Email = "user@example.org",
    ReferenceNumber = "77987",
    AddInfo = doc.DocumentElement,
    Lang = "CZ"
  };

  var encodingLogger = loggerFactory.CreateLogger();
  var clientServiceLogger = loggerFactory.CreateLogger();
  var clientService = new ClientService(new EncodingService(encodingLogger),
  new PaymentRequestTransformer(), new PaymentResponseTransformer(), clientServiceLogger);				data-mce-type="bookmark" 				id="mce_SELREST_start" 				data-mce-style="overflow:hidden;line-height:0" 				style="overflow:hidden;line-height:0" 			>clientServiceLogger);

  return clientService.GenerateGPWebPayRedirectUrl(
    url,
    request,
    privateCertificateFile,
    privateCertificateFilePassword,
    publicCertificateFile,
    publicCertificateFilePassword);
}

Process Incomming GP webpay request

public void ProcessIncommingGPWPRequest()
{
  var loggerFactory = new LoggerFactory().AddConsole();

  // Args from Request object
  var queryArgs = new QueryCollection(new Dictionary()
  {
    {"OPERATION", new StringValues("Operation")},
    {"ORDERNUMBER", new StringValues("12332")},
    {"PRCODE", new StringValues("0")},
    {"SRCODE", new StringValues("0")},
    {"RESULTTEXT", new StringValues("ResultText")},
    {"USERPARAM1", new StringValues("UserParam1")},
    {"DIGEST", new StringValues("Digest")},
    {"DIGEST1", new StringValues("Digest1")},
  });

  const string publicCertificateFile = "certs/test.pfx";
  const string password = "test";

  var encodingLogger = loggerFactory.CreateLogger();
  var clientServiceLogger = loggerFactory.CreateLogger();
  var clientService = new ClientService(new EncodingService(encodingLogger),
  new PaymentRequestTransformer(), new PaymentResponseTransformer(), clientServiceLogger);

  // Service will creates PaymentResponse from incomming args and validate response digest with
  // public certificate provided by GPWP and then check if "PRCODE" and "SRCODE" values have correct or error values
  // ReSharper disable once UnusedVariable
  var paymentResponse = clientService.ProcessGPWebPayResponse(queryArgs, publicCertificateFile, password);
}

That’s all – two methods. Yes, you should read how the process and communication with the gateway works, but it is really not important to know how exactly the protocol, especially creation of message, is done if you really do not need it – which is exactly this case. So GP webpay, do it. You will have more satisfied customers.

Where To Get It

You can find NuGet package here. Just add it to your project:

PM> Install-Package GPWebpayNet.Sdk

Or clone in on GitHub.

You can of course support this project too :).

 

dev

Deploying Azure Webjobs With the Same Name Within Different App Services Instances

Deploying single project within different Azure App Services can be a common task – one service for test, one service for prelife and another one for production. There is no problem with such scenario. But when you are using Webjobs project, it can cause an issue.

Webjobs are using Storage for various purposes. You must specify AzureWebJobsDashboard and AzureWebJobsStorage connection strings in Webjob configuration.

If you are deploying same project with Webjobs within a different App Service, you MUST SET CONNECTION STRINGS TO DIFFERENT STORAGE ACCOUNT. Otherwise these Webjobs will collide – they will access same metadata because of the same path name. E.g., in case of time-triggered WebJob it can happen that only the first WebJob will be executed periodically and the seconds one never.

Note: There is opened GitHub issue that is still open because of low priority.

dev

Show Browser Window on Raspberry After Login

Overview

If you are using your RaspberryPi with display, e.g., like here, you probably want to launch it to the kiosk mode after start or reboot automatically.

How-to

The simplest way is to execute a script. In the script you can start Chromium or Epiphany browser, etc. The following code snippet represents a file start_kiosk.sh. It launches Epiphany browser on localhost in full-screen mode.

epiphany-browser -a --profile /home/pi/.config http://localhost --display=:0 &
sleep 15s
xte "key F11" -x:0

The next step is to setup script execution. It can be done by adding execution script into file /home/pi/.config/lxsession/LXDE-pi/autostart:

@/usr/bin/start_kiosk.sh

Conclusion

By this setup you can change your Raspberry into a simple kiosk device. There exist more sophisticated solution for the real kiosk mode, e.g., disabling access to users, etc.

This simple solution just save manual start of the browser or other app after Raspberry start.