16.4.1 • Published 11 days ago

@sap/approuter v16.4.1

Weekly downloads
15,444
License
SEE LICENSE IN LI...
Repository
-
Last release
11 days ago

@sap/approuter

Overview

When a business application consists of several different apps (microservices), the application router is used to provide a single entry point to that business application. It has the responsibility to:

  • Dispatch requests to backend microservices (reverse proxy)
  • Authenticate users
  • Serve static content

Application router overview diagram

Let's think of the different apps (microservices) as destinations to which the incoming request will be forwarded. The rules that determine which request should be forwarded to which destination are called routes. For every destination there can be more than one route. You may read more on the concept of routes later in this document. If the backend microservices require authentication, the application router can be configured to authenticate the users and propagate the user information. Again by using routes, the application router can serve static content.

The application router is designed to work in XS Advanced - Cloud Foundry and XS OnPremise Runtime.

A calling component accesses a target service by means of the application router only if there is no JWT token available, for example, if a user invokes the application from a Web browser. If a JWT token is already available, for example, because the user has already been authenticated, or the calling component uses a JWT token for its own OAuth client, the calling component calls the target service directly; it does not need to use the application router.

Note that the application router does not hide the backend microservices in any way. They are still directly accessible bypassing the application router. So the backend microservices must protect all their endpoints by validating the JWT token and implementing proper scope checks. Network isolation is not provided currently by the platform.

Deploying a business application with microservices

For example we can have a business application that has the following structure:

The manifest.yml file is used to deploy the business application on Cloud Foundry and the manifest-op.yml - on the XS OnPremise Runtime. These files should describe all the microservices for that business application.

Folders are used to isolate the different microservices. Let's assume that the application router is the microservice in the web folder (every business application has its own application router). Here is how we can include the application router:

  • Manually create the node_modules folder in the web folder.
  • Copy and paste the folder that contains the self-contained application router into node_modules. In this example the name of that folder is @sap/approuter, see the start script in the package.json below.
  • Check the version of the application router you just copied.
  • Create a package.json file in web with content similar to the following and replace the version's value with the version of your application router:
{
    "name": "hello-world-approuter",
    "dependencies": {
       "@sap/approuter": "2.6.1"
    },
    "scripts": {
        "start": "node node_modules/@sap/approuter/approuter.js"
    }
}

In order to use the application router you don't have to write any JavaScript code. Only some configurations have to be provided in the web folder. Here is a complete example:

The web folder contains the package.json, node_modules, some configuration files used by the application router, and static resources to be served. You can read more about the configurations later in this document.

By default, the application router runs on port 5000 (if started locally) or it takes the port from the PORT environment variable.

Working directory

The working directory contains configuration files that the application router needs and static resources that can be served at runtime. In the previous example, the web folder is the working directory. By default the current directory is the working directory. It is possible to configure it during start up of the application router with the following command line argument:

node approuter.js -w <working-dir>

Application router will abort if the working directory does not contain xs-app.json file.

Configurations

The application router makes use of the following configurations:

  • Main configuration - this is the xs-app.json file. This file is mandatory and contains the main configurations of the application router.

  • UAA configuration - the application router reads this configuration either from the VCAP_SERVICES environment variable (when deployed on Cloud Foundry or XS Advanced OnPremise Runtime) or from the default-services.json file (when running locally). Refer to the documentation of the @sap/xsenv package for more details.

  • Configurations from the environment - these configurations are either read from the application router's environment (when deployed on Cloud Foundry or XS Advanced OnPremise Runtime) or from the default-env.json file (when running locally). Refer to the documentation of the @sap/xsenv package for more details. The environment variables that the application router takes into account are:

ConfigurationEnvironment variableDescription
UAA service nameUAA_SERVICE_NAMEContains the name of the UAA service to be used.
DestinationsdestinationsProvides information about the available destinations.
Additional headershttpHeadersProvides headers that the application router will return to the client in its responses.
Additional cookie attributesCOOKIESThe application router triggers the generation of the following third-party cookies and sends them to the client: locationAfterLogin fragmentAfterLogin signature JSESSIONID To control the behavior and usage of the third-party cookies, you can configure the attributes SameSite and Partitioned.For more information about third-party cookies, please see KBA 3409306.
PluginspluginsA plugin is just like a route except that you can't configure some inner properties.
Session timeoutSESSION_TIMEOUTPositive integer representing the session timeout in minutes. The default timeout is 15 minutes.
X-Frame-OptionsSEND_XFRAMEOPTIONS, httpHeadersConfiguration for the X-Frame-Options header value.
Allowlist serviceCJ_PROTECT_WHITELISTConfiguration for the allowlist that is preventing clickjack attacks.
Web Sockets origins allowlistWS_ALLOWED_ORIGINSAn allowlist configuration that is used for verifying the Origin header of the initial upgrade request when establishing a web socket connection.
JWT Token refreshJWT_REFRESHThe time in minutes before a JWT token expires and the application router should trigger a token refresh routine.
Incoming connection timeoutINCOMING_CONNECTION_TIMEOUTMaximum time in milliseconds for a client connection. After that time the connection is closed. If set to 0, the timeout is disabled. Default: 120000 (2 min)
Tenant host patternTENANT_HOST_PATTERNString containing a regular expression with a capturing group. The request host is matched against this regular expression. The value of the first capturing group is used as tenant id.
Destination host patternDESTINATION_HOST_PATTERNString containing a regular expression with a capturing group. The request host is matched against this regular expression. The value of the capturing group is used as destination name.
CompressionCOMPRESSIONConfiguration regarding compressing resources before responding to the client.
Secure flag of session cookieSECURE_SESSION_COOKIECan be set to true or false. By default, the Secure flag of the session cookie is set depending on the environment the application router runs in. For example, when application router is behind a router (Cloud Foundry's router or SAP Web Dispatcher) that is configured to serve HTTPS traffic, then this flag will be present. During local development the flag is not set. This environment variable can be used to enforce setting or omitting the Secure flag. Note: If the Secure flag is enforced, the application router will reject requests sent over unencrypted connection (http).
Trusted CA certificatesXS_CACERT_PATHList of files paths with trusted CA certificates used for outbound https connections (UAA, destinations, etc.). File paths are separated by path.delimiter. If this is omitted, several well known "root" CAs (like VeriSign) will be used. This variable is set automatically by XSA On-premise runtime.
Reject untrusted certificatesNODE_TLS_REJECT_UNAUTHORIZEDBy default an outbound https connection is terminated if the remote end does not provide a trusted certificate. This check can be disabled by setting NODE_TLS_REJECT_UNAUTHORIZED to 0. This is a built-in feature of Node.js. Note: Do not use this in production as it compromises security!
External reverse proxy flagEXTERNAL_REVERSE_PROXYBoolean value that indicates the use of application router behind an external reverse proxy (outside of Cloud Foundry domain)
Skip client credentials tokens load on startSKIP_CLIENT_CREDENTIALS_TOKENS_LOADBoolean value that indicates that no client credentials tokens should be created during the application router start phase
Cross-Origin Resource SharingCORSConfiguration regarding CORS enablement.
Preserve URL fragmentPRESERVE_FRAGMENTWhen set to true or not set, fragment part of the URL provided during first request of not logged-in user to protected route will be preserved, and after login flow user is redirected to original URL including fragment part. However, this may break programmatic access to Approuter (e.g. e2e tests), since it introduces change in login flow, which is incompatible with Approuter version 4.0.1 and earlier. Setting value to false makes login flow backward compatible, however will not take fragment part of the URL into account.
Direct Routing URI PatternsDIRECT_ROUTING_URI_PATTERNSConfiguration for direct routing URI patterns.
NodeJS Minimal Logging LevelCF_NODEJS_LOGGING_LEVELConfiguration for NodeJS minimal logging level.
Dynamic Identity ProviderDYNAMIC_IDENTITY_PROVIDERConfiguration for dynamic identity provider.
Backend Cookies SecretBACKEND_COOKIES_SECRETSecret that is used to encrypt backend session cookies in service to Application Router flow. Should be set in case multiple instances of Application Router are used. By default a random sequence of characters is used.
Service to Application RouterSERVICE_2_APPROUTERIf true, when the SAP Passport header is received from the application router, it will be transferred without modification to the backend application.
Client certificate header nameCLIENT_CERTIFICATE_HEADER_NAMEWhen set application router will use this header name to get the client certificate from the request header in subscription callback. If not provided the default header name x-forwarded-client-cert is used.
Server Keep AliveSERVER_KEEP_ALIVEserver keep alive timeout (positive integer in milliseconds).
Minimum Token ValidityMINIMUM_TOKEN_VALIDITYpositive integer in seconds. When set, approuter will check that the token returned from the authorization service has an expiration time higher than the minimum token validity value.
State Parameter SecretSTATE_PARAMETER_SECRETenables the use of state parameters to prevent CSRF attacks. If this environment variable is set, the application router creates a state parameter for each initial authorization request. By validating that the authentication server returns the same state parameter in its response, the application server can verify that the response did not originate from a third party. Note: this feature is only available in Cloud Foundry runtime
HTTP2 SupportHTTP2_SUPPORTEnables the application router to start as an HTTP/2 server. Note: To configure HTTP/2 support, you must use Cloud Foundry routes with an HTTP/2 destination protocol. See Configuring HTTP/2 Support in the Cloud Foundry Documentation. As connection-specific header fields aren't supported by the HTTP/2 protocol, see rfc9113, the application router removes such headers automatically when they are returned from a backend to prevent a failure of the HTTP/2 response.
Store CSRF token in external sessionSVC2AR_STORE_CSRF_IN_EXTERNAL_SESSIONIf true and have enabled external session management, the application router can generate and validate CSRF tokens in service-to-application-router flows by storing the token in an external session.
Cache service credentialsCACHE_SERVICE_CREDENTIALSIf true, services credentials are cached in the application router memory
Enable x-forwarded-host header validationENABLE_X_FORWARDED_HOST_VALIDATIONIf true, x-forwarded-host validation will be performed, allowing letters, digits, hypens (-), underscores (_) and dots (.). As well as it validates hostname length.
Add the content security policy headers to the responseENABLE_FRAME_ANCESTORS_CSP_HEADERSIf true, Approuter will include the content security policy (CSP) header using subaccount trusted domains with frame-ancestors policy.
Time cache value for frame ancestors CSP headerFRAME_ANCESTORS_CSP_HEADER_CACHE_TIMETime in seconds for the frame ancestors CSP header to be cached. The default value is 300 seconds.
Store backend session cookies in external session storeSTORE_SESSION_COOKIES_IN_EXTERNAL_SESSION_STOREIf true, the application router will store backend session cookies in an external session store in the service-to-application-router flow. In this case the "ARBE" cookie will not be returned to the calling service.

Note: all those environment variables are optional.

Destinations

The destinations configuration can be provided by the destinations environment variable or by destination service. There has to be a destination for every single app (microservice) that is a part of the business application.

Environment destinations

The destinations configuration is an array of objects. Here are the properties that a destination can have:

PropertyTypeOptionalDescription
nameStringA unique alphanumeric identifier of the destination.
urlStringURL of the app (microservice).
proxyHostStringxThe host of the proxy server used in case the request should go through a proxy to reach the destination.
proxyPortStringxThe port of the proxy server used in case the request should go through a proxy to reach the destination.
forwardAuthTokenBooleanxIf true, the OAuth token is sent to the destination. The default value is false. This token contains user identity, scopes and other attributes. It is signed by the UAA or IAS service, so it can be used for user authentication and authorization with backend services.
forwardAuthCertificatesBooleanxIf true, the certificates and key of the authentication service are added to the HTTP connection to the destination. The default value is false. For more information see: Mutual TLS Authentication (mTLS) and Certificates Handling.
strictSSLBooleanxConfigures whether the application router should reject untrusted certificates. The default value is true.Note: Do not use this in production as it compromises security!
timeoutNumberxPositive integer representing the maximum wait time for a response (in milliseconds) from the destination. Default is 30000ms.
setXForwardedHeadersBooleanxIf true , the application router adds X-Forwarded-(Host, Path, Proto) headers to the backend request.Default value is true.
proxyTypeStringxConfigures whether the destination is used to access applications in on-premise networks or on public Internet. Possible value: OnPremise. if the property is not provided, it is assumed that it is a public Internet access. Note: if OnPremise value is set, binding to SAP Cloud Platform connectivity service is required, and forwardAuthToken property should not be set.
IASDependencyNameStringxConfigures the name of the IAS dependency that is used to exchange the IAS login token. The exchanged token is also forwarded to the backend application.

Note: The timeout specified will also apply to the destination's logout path or service's logout path (if you have set one). Note: proxyHost and proxyPort are optional, but if one of them is defined, then the other one becomes mandatory.

Sample content of the destinations environment variable:

[
  {
    "name" : "ui5",
    "url" : "https://ui5.sap.com",
    "proxyHost" : "proxy",
    "proxyPort" : "8080",
    "forwardAuthToken" : false,
    "timeout" : 1200
  }
]

It is also possible to include the destinations in the manifest.yml and manifest-op.yml files:

- name: node-hello-world
  memory: 100M
  path: web
  env:
    destinations: >
                  [
                    {"name":"ui5", "url":"https://ui5.sap.com"}
                  ]

Destination service

Destination configuration can also be read from destination service . Here are the Approuter limitations to destination properties configuration from destination service :

PropertyAdditional PropertyDescription
Typeonly HTTP supported.
AuthenticationAll authentication types are supported. Note: User and Password are mandatory if the authentication type is basic authentication.Note: if the authentication type set to principal propagation the ProxyType have to be on-premise.Note: if the authentication type set to OAuth2SAMLBearerAssertion, uaa.user scope in xs-security.json is required.
ProxyTypeSupported proxy type : on-premise, internet, private-link. Note: if ProxyType set to on-premise, binding to SAP Cloud Platform connectivity service is required. Note: To check the availability of the private-link proxy type, see SAP Private Link ServiceInformation published on SAP site in the SAP Discovery Center.
Optional additional properties:
PropertyAdditional PropertyDescription
HTML5.ForwardAuthTokenxIf true the OAuth token will be sent to the destination. The default value is false. This token contains user identity, scopes and other attributes. It is signed by the UAA so it can be used for user authentication and authorization with backend services. Note: if ProxyType set to on-premise, ForwardAuthToken property should not be set. Note: if Authentication type is other than NoAuthentication, ForwardAuthToken property should not be set.
HTML5.ForwardAuthCertificatesxIf true, the certificates and key of the authentication service are added to the HTTP connection to the destination. The default value is false. For more information see: Mutual TLS Authentication (mTLS) and Certificates Handling.
HTML5.TimeoutxPositive integer representing the maximum wait time for a response (in milliseconds) from the destination. Default is 30000ms.Note: The timeout specified will also apply to the destination's logout path or service's logout path (if you have set one).
HTML5.PreserveHostHeaderxIf true , the application router preserves the host header in the backend request.This is expected by some back-end systems like AS ABAP, which do not process x-forwarded-* headers.
HTML5.DynamicDestinationxIf true , the application router allows to use this destination dynamically on host or path level.
HTML5.SetXForwardedHeadersxIf true , the application router adds X-Forwarded-(Host, Path, Proto) headers to the backend request.Default value is true.
HTML5.IASDependencyNamexConfigures the name of the IAS dependency that is used to exchange the IAS login token. The exchanged token is also forwarded to the backend application.
sap-clientxIf provided, the application router propagates the sap-client and its value as a header in the backend request.This is expected by ABAP back-end systems.
URL.headers.<header-name>xIf provided, the application router propagates this special attribute in the destination as the header. The application router can get the headers list from the destination API. Existing request headers are not overwritten.

Note:

  • In case destination with the same name is defined both in environment destination and destination service, the destination configuration will load from the environment.
  • Destinations on destination service instance level are supported.
  • Only destination client certificates of type p12 are supported.
  • Only destination trust certificates of the type privacy-enhanced mail (PEM) are supported.

UAA configuration

The User Account and Authentication (UAA) server is responsible for user authentication. In Cloud Foundry and XS OnPremise Runtime a service is created for this configuration and by using the standard service binding mechanism the content of this configuration is available in the VCAP_SERVICES environment variable. Note: The service should have xsuaa in its tags or the environment variable UAA_SERVICE_NAME should be specified (stating the exact name of the UAA service). During local development the UAA configuration is provided in the default-services.json file. When the UAA is used for authentication the user is redirected to the UAA's login page to enter their credentials.

Sample content for a default-services.json file:

{
    "uaa": {
        "url" : "http://my.uaa.server/",
        "clientid" : "client-id",
        "clientsecret" : "client-secret",
        "xsappname" : "my-business-application"
    }
}

The application router supports the $XSAPPNAME placeholder (upper case letters). You may use it in your route configurations in the scope property. The value of $XSAPPNAME is taken from the UAA configuration (the xsappname property).

Additional headers configuration

If configured, the application router can send additional http headers in its responses to the client. Additional headers can be set in the httpHeaders environment variable.

Sample configuration for additional headers:

[
  {
    "X-Frame-Options": "ALLOW-FROM http://localhost"
  },
  {
    "Test-Additional-Header": "1"
  }
]

In this case, the application router sends two additional headers in the responses to the client. Custom response headers, configured in the application router configuration file (xs-app.json) are added to the list of additional http headers. If the response header name already exists in the additional http headers list, the value of the response header name overrides the value of the http header.

Caution: For security reasons, the following headers must not be configured: authorization', 'cookie', and 'set-cookie'.

Additional Cookie Attributes

If configured, the application router sends additional cookie attributes in its responses to the client. The cookie attributes can be set in the COOKIES environment variable.

Example of configuration for cookies in the manifest.yml :

  env:
   COOKIES: >
     {
       "SameSite":"None",
       "Partitioned":
            {
               "supportedPartitionAgents":"^Mozilla.*(Chrome|Chromium|)/((109)|(1[1-9][0-9])|([2-9][0-9][0-9]))",
               "unsupportedPartitionAgents":"PostmanRuntime/7.29.2"
            }
     }

In this example, the application router sets the SameSite attribute of the cookie to "None" and the specifies a Partitioned attribute that is sent in the responses to the client. Note: Currently, only the value "None" are supported for the SameSite attribute. The value "Strict" is not supported. The Partitioned attribute contains two required regular expressions:

  • supportedPartitionAgents for supported agents

  • unsupportedPartitionAgents for unsupported agents

You can use a wildcard '(.*)' in supportedAgents to allow all agents to use the Partitioned attribute. Note: unsupportedPartionAgents overwrites the configurations in supportedAgents. If an agent is allowed in supportedPartitionAgents but disallowed in unsupportedPartitionAgents, the Partitioned attribute will not be returned.

Plugins configuration

A plugin serves almost the same purpose as routes. The difference is that plugins can be configured through the environment and that way you can add new routes to the application router without changing the design-time artefact xs-app.json. The plugin configuration properties are the same as those of a route except that you can't configure localDir, replace and cacheControl.

PropertyTypeOptionalDescription
nameStringThe name of this plugin
sourceString/ObjectDescribes a regular expression that matches the incoming request URL. Note: A request matches a particular route if its path contains the given pattern. To ensure the RegExp matches the complete path, use the following form: ^$`. Note: Be aware that the RegExp is applied to on the full URL including query parameters.
targetStringxDefines how the incoming request path will be rewritten for the corresponding destination.
destinationStringAn alphanumeric name of the destination to which the incoming request should be forwarded.
authenticationTypeStringxThe value can be ias, xsuaa, basic, or none. The default authenticationType depends on the authentication service binding: If the application router is bound to the Identity Authentication service, the default authenticationType is ias. Otherwise, the default value is xsuaa. If xsuaa or ias are used, the specified authentication server (Identity Authentication or User Account and Authentication) handles the authentication (the user is redirected to the login form of Identity Authentication or User Account and Authentication). The basic authenticationType works with SAP HANA users, SAP ID service, and Identity Authentication service. For more information, see the SAP Note 3015211 - BASIC authentication options for SAP BTP Cloud Foundry applications. If the value none is used, no authentication is required for this route.

. csrfProtection | Boolean | x | Enable CSRF protection for this route. The default value is true. scope | Array/String/Object | x | Scopes are related to the permissions a user needs to access a resource. This property holds the required scopes to access the target path. Access is granted if the user has at least one of the listed scopes. Note: Scopes are defined as part of the xsuaa service instance configuration. You can use ias as authenticationType and xsuaa scopes for authorization if the application router is bound to both (ias and xsuaa)."

Sample content of the plugins environment variable:

[
  {
    "name": "insecurePlugin",
    "source": "/plugin",
    "destination": "plugin",
    "target": "/",
    "csrfProtection": false,
    "scope": ["viewer", "reader"]
  },
  {
    "name": "publicPlugin",
    "source": "/public-plugin",
    "destination": "publicPlugin",
    "authenticationType": "none"
  }
]

Session timeout configuration

For example, if you have the following line in your manifest.yml or manifest-op.yml file:

- name: node-hello-world
  memory: 100M
  path: web
  env:
    SESSION_TIMEOUT: 40

After 40 minutes of user inactivity (no requests have been sent to the application router), a Central Logout will be triggered due to session timeout.

Note: The application router depends on the UAA server for user authentication, if the authenticationType for a route is xsuaa. The UAA server may have a different session timeout configured. It is recommended that the configurations of the application router and the UAA are identical.

X-Frame-Options configuration

Application router sends X-Frame-Options header by default with value SAMEORIGIN. This behaviour can be changed in 2 ways:

  • Disable sending the default header value by setting SEND_XFRAMEOPTIONS environment variable to false
  • Override the value to be sent via additional headers configuration

Cross-Origin Resource Sharing configuration

The CORS keyword enables you to provide support for cross-origin requests, for example, by allowing the modification of the request header. Cross-origin resource sharing (CORS) permits Web pages from other domains to make HTTP requests to your application domain, where normally such requests would automatically be refused by the Web browser's security policy. Cross-origin resource sharing(CORS) is a mechanism that allows restricted resources on a webpage to be requested from another domain (/protocol/port) outside the domain (/protocol/port) from which the first resource was served. CORS configuration enables you to define details to control access to your application resource from other Web browsers. For example, you can specify where requests can originate from or what is allowed in the request and response headers.

The CORS configuration can be provided in the CORS environment variable or in the CORS property of the application router configuration file (xs-app.json). If a cross-origin resource sharing (CORS) configuration exists in both the environment variables and the application router configuration file (xs-app.json), the application router gives priority to the CORS configuration in the application router configuration file.

The CORS configuration is an array of objects. Here are the properties that a CORS object can have:

PropertyTypeOptionalDescription
uriPatternStringA regular expression representing for which source routes CORS configuration is applicable. To ensure the RegExp matches the complete path, surround it with ^ and $. Defaults: none.
allowedOriginArrayA comma-separated list of objects that each one of them containing host name, port and protocol that are allowed by the server.for example: {?host?: "www.sap.com"} or {?host?: ?*.sap.com?}. Note: matching is case-sensitive. In addition, if port or protocol are not specified the default is ?*?. Defaults: none.
allowedMethodsArray of upper-case HTTP methodsxComma-separated list of HTTP methods that are allowed by the server. Defaults: ?GET?, ?POST?, ?HEAD?, ?OPTIONS? applies. Note: matching is case-sensitive.
maxAgeNumberxA single value specifying how long, in seconds, a preflight response should be cached. A negative value will prevent CORS Filter from adding this response header to pre-flight response. Defaults: 1800.
allowedHeadersArray of headersxComma-separated list of request headers that are allowed by the serve. Defaults: ?Origin?, ?Accept?, ?X-Requested-With?, ?Content-Type?, ?Access-Control-Request-Method?, ?Access-Control-Request-Headers?.
exposeHeadersArray of headersxComma-separated list of response headers (other than simple headers) that can be exposed. Defaults: none.
allowedCredentialsBooleanxA flag that indicates whether the resource supports user credentials. Defaults: true.

Sample content of the CORS environment variable:

[
  {
      "uriPattern": "^\route1$",
      "allowedMethods": [
        "GET"
      ],
      "allowedOrigin": [
        {
          "host": "my_example.my_domain",
          "protocol": "https",
          "port": 345
        }
      ],
      "maxAge": 3600,
      "allowedHeaders": [
        "Authorization",
        "Content-Type"
      ],
      "exposeHeaders": [
        "customHeader1",
        "customHeader2"
      ],
      "allowedCredentials": true
    }
]

It is also possible to include the CORS in the manifest.yml and manifest-op.yml files:

- name: node-hello-world
  memory: 100M
  path: web
  env:
    CORS: >
      [
        {
          "allowedOrigin":[
                            {
                                "host":"my_host",
                                "protocol":"https"
                            }
                          ],
          "uriPattern":"^/route1$"
        }
      ]

For route with source that match the REGEX ?^\route1$?, the CORS configuration is enabled.

Direct Routing URI Patterns configuration

With the direct routing URI patterns configuration, you can define a list of URIs that are directed to the routing configuration file (xs-app.json file) of the application router instead of to a specific application's xs-app.json file that is stored in the HTML5 Application Repository. This configuration improves the application loading time and monitoring options because it prevents unnecessary calls to the HTML5 Application Repository.

The configuration is an array of strings or regular expressions. Note that the following regular expressions are preconfigured in the configuration array: "^favicon.ico$", "^login$". Therefore, do not name your HTML5 applications "favicon.ico" or "login"!

You have to provide only the first segment in the URL, after the approuter host. For example, for the URL https://approuter-host/route1/index.html, you enter "route1" in the direct routing URI patterns array.

Sample content of the Direct Routing URI Patterns environment variable:

  env:
    DIRECT_ROUTING_URI_PATTERNS: >
      ["route1", "^route2$", "route3"]

NodeJS Minimal Logging Level configuration

With this configuration, you can set the minimal logging level of the cf-nodejs-logging-support library of the application router. The following levels are available:

  • off

  • error

  • warn

  • info

  • verbose

  • debug

  • silly

The default value is "error".

Here is a sample content for the NodeJS minimal logging level environment variable:

  env:
    CF_NODEJS_LOGGING_LEVEL: "debug"

Note The application router also uses the @sap/logging library. To configure the log level for this library, you use the XS_APP_LOG_LEVEL environment variable.

Dynamic Identity Provider configuration

If dynamicIdentityProvider is true, the end user can set the identity provider (IDP) for the application’s login process by filling the request query parameter sap_idp with the IDP Origin Key. If IdentityProvider property is defined in the route, its value will be overwritten by the sap_idp query parameter value. The default value for dynamicIdentityProvider is false. This configuration is relevant for a standalone approuter scenario and it is set for all routes.

Here is a sample content for the dynamic identity provider environment variable:

  env:
    DYNAMIC_IDENTITY_PROVIDER: true

Routes

A route is a configuration that instructs the application router how to process an incoming request with a specific path.

PropertyTypeOptionalDescription
sourceString/ObjectDescribes a regular expression that matches the incoming request URL. Note: A request matches a particular route if its path contains the given pattern. To ensure the RegExp matches the complete path, use the following form: ^$`. Note: Be aware that the RegExp is applied to on the full URL including query parameters.
httpMethodsArray of upper-case HTTP methodsxWhich HTTP methods will be served by this route; the methods supported are: DELETE, GET, HEAD, OPTIONS, POST, PUT, TRACE, PATCH (no extension methods are supported). If this option is not specified, the route will serve any HTTP method.
targetStringxDefines how the incoming request path will be rewritten for the corresponding destination or static resource.
destinationStringxThe name of the destination to which the incoming request should be forwarded. The destination name can be a static string or a regular expression that defines how to dynamically fetch the destination name from the source property or from the host.
serviceStringxThe name of the service to which the incoming request should be forwarded.
endpointStringxThe name of the endpoint within the service to which the incoming request should be forwarded. Can only be used in a route containing a service attribute.
localDirStringxFolder in the working directory from which the application router will serve static content Note: localDir routes support only HEAD and GET requests; requests with any other method receive a 405 Method Not Allowed.
preferLocalBooleanxDefines from which subaccount the destination is retrieved. If preferLocal is true, the destination is retrieved from the provider subaccount. If preferLocal is false or undefined, the destination is retrieved from the subscriber subaccount.
replaceObjectxAn object that contains the configuration for replacing placeholders with values from the environment. It is only relevant for static resources. Its structure is described in Replacements.
authenticationTypeStringxThe value can be xsuaa,ias, basic or none. The default one is ias, if subaccount trusts an ias tenant, else xsuaa. When xsuaa or ias are used the specified authentication server will handle the authentication (the user is redirected to the authentication service login form). The basic mechanism works with SAP HANA users, SAP ID Service and SAP Identity Authentication service. Find more details in SAP Note 3015211 - BASIC authentication options for SAP BTP Cloud Foundry applications. If none is used then no authentication is needed for this route.
csrfProtectionBooleanxEnable CSRF protection for this route. The default value is true.
scopeArray/String/ObjectxScopes are related to the permissions a user needs to access a resource. This property holds the required scopes to access the target path.
cacheControlStringxString representing the value of the Cache-Control header, which is set on the response when serving static resources. By default the Cache-Control header is not set. It is only relevant for static resources.
identityProviderStringxThe name of the identity provider to use if provided in route’s definition. If not provided, the route will be authenticated with the default identity provider. Note: If the authenticationType is set to Basic Authentication or None, do not define the identityProvider property.
dynamicIdentityProviderBooleanxIf dynamicIdentityProvider is true, the end user can set the identity provider (IDP) for the application’s login process by filling the request query parameter sap_idp with the IDP Origin Key. If IdentityProvider property is defined in the route, its value will be overwritten by the sap_idp query parameter value. The default value for dynamicIdentityProvider is false.

Note: The properties destination, localDir and service are optional, but exactly one of them must be defined. Note: When using the property replace it is mandatory to define the localDir property. Note: The cacheControl property is effective only when one of the following settings is performed:

  • The localDir property was set
  • A service pointing to HTML5 Application Repository ("service": "html5-apps-repo-rt") was set

Example routes

For example, if you have a configuration with the following destination:

[
  {
    "name" : "app-1",
    "url" : "http://localhost:3001"
  }
]

Here are some sample route configurations:

  • Route with a destination and no target
{
    "source": "^/app1/(.*)$",
    "destination": "app-1"
}

Since there is no target property for that route, no path rewriting will take place. If we receive /app1/a/b as a path, then a request to http://localhost:3001/app1/a/b is sent. The source path is appended to the destination URL.

  • Route with case-insensitive matching
{
    "source": {
      "path": "^/app1/(.*)$",
      "matchCase": false
    },
    "destination": "app-1"
}

This example is much like the previous one, but instead of accepting only paths starting with /app1/, we accept any variation of app1's case. That means if we receive /ApP1/a/B, then a request to http://localhost:3001/ApP1/a/B is sent. Note: The property matchCase has to be of type boolean. It is optional and has a default value true.

  • Route with a destination and a target
{
    "source": "^/app1/(.*)$",
    "target": "/before/$1/after",
    "destination": "app-1"
}
  • Route with a service, a target and an endpoint
{
     "source": "^/odata/v2/(.*)$",
     "target": "$1",
     "service": "com.sap.appbasic.country",
     "endpoint": "countryservice"
}

When a request with path /app1/a/b is received, the path rewriting is done according to the rules in the target property. The request will be forwarded to http://localhost:3001/before/a/b/after.

Note: In regular expressions there is the term capturing group. If a part of a regular expression is surrounded with parenthesis, then what has been matched can be accessed using $ + the number of the group (starting from 1). In the last example $1 is mapped to the (.*) part of the regular expression in the source property.

  • Route with dynamic destination and target
{
      "source": "^/destination/([^/]+)/(.*)$",
      "target": "$2",
      "destination": "$1",
      "authenticationType": "xsuaa"
    }

If you have a another destination configured:

[
	{
	"name" : "myDestination",
	"url" : "http://localhost:3002"
	}
]

when a request with the path /destination/myDestination/myTarget is received, the destination will be replaced with the url from "myDestination", the target will get "myTarget" and the request will be redirected to http://localhost:3002/myTarget

Note: You can use a dynamic value (regex) or a static string for both destination and target values

Note: The approuter first looks for the destination name in the manifest.yaml file, and if not found, looks for it in the destination service.

  • Destination In Host

For legacy applications that do not support relative URL paths, you need to define your URL in the following way to enable the destination to be extracted from the host the url should be defined in the following way:

https://<tenant>-<destination>.<customdomain>/<pathtofile>

To enable the application router to determine the destination of the URL host, a DESTINATION_HOST_PATTERN attribute must be provided as an environment variable.

Example: When a request with the path https://myDestination.some-approuter.someDomain.com/app1/myTarget is received, the following route is used:

 {
      "source": "^/app1/([^/]+)/",
      "target": "$1",
      "destination": "*",
      "authenticationType": "xsuaa"
 }

In this example, the target will be extracted from the source and the ‘$1’ value is replaced with ‘myTarget’. The destination value is extracted from the host and the ‘*’ value is replaced with ‘myDestination’.

  • Route with a localDir and no target
{
    "source": "^/web-pages/(.*)$",
    "localDir": "my-static-resources"
}

Since there is no target property for that route, no path rewriting will take place. If we receive a request with a path /web-pages/welcome-page.html, the local file at my-static-resources/web-pages/welcome-page.html under the working directory will be served.

  • Route with a localDir and a target
{
    "source": "^/web-pages/(.*)$",
    "target": "$1",
    "localDir": "my-static-resources"
}

If we receive a request with a path '/web-pages/welcome-page.html', the local file at 'my-static-resources/welcome-page.html' under the working directory will be served. Note: The capturing group used in the target property.

  • Route with localDir and cacheControl
{
  "source": "^/web-pages/",
  "localDir": "my-static-resources",
  "cacheControl": "public, max-age=1000,must-revalidate"
}
  • Route with service "html5-apps-repo-rt" and cacheControl
{
  "source": "^/index.html$",
  "service": "html5-apps-repo-rt",
  "authenticationType": "xsuaa",
  "cacheControl":"public,max-age=1000,must-revalidate"
}
  • Route with httpMethods restrictions

The httpMethods option allows you to split the same path across different targets depending on the HTTP method. For example:

{
  "source": "^/app1/(.*)$",
  "target": "/before/$1/after",
  "httpMethods": ["GET", "POST"]
}

This route will be able to serve only GET and POST requests. Any other method (including extension ones) will get a 405 Method Not Allowed response. The same endpoint can be split across multiple destinations depending on the HTTP method of the requests:

{
  "source": "^/app1/(.*)$",
  "destination" : "dest-1",
  "httpMethods": ["GET"]
},
{
  "source": "^/app1/(.*)$",
  "destination" : "dest-2",
  "httpMethods": ["DELETE", "POST", "PUT"]
}

The setup above will route GET requests to the target dest-1, DELETE, POST and PUT to dest-2, and any other method receives a 405. It is also possible to specify "catchAll" routes, namely those that do not specify httpMethods restrictions:

{
  "source": "^/app1/(.*)$",
  "destination" : "dest-1",
  "httpMethods": ["GET"]
},
{
  "source": "^/app1/(.*)$",
  "destination" : "dest-2"
}

In the setup above, GET requests will be routed to dest-1, and all the rest to dest-2.

Why using httpMethods? It is often useful to split the implementation of microservices across multiple, highly specialized applications. For example, a Java application written to serve high amounts of GET requests that return large payloads is implemented, sized, scaled and load-tested differently than applications that offer APIs to upload limited amounts of data. httpMethods allows you to split your REST APIs, e.g., /Things to different applications depending on the HTTP methods of the requests, without having to make the difference visible in the URL of the endpoints.

Another usecase for httpMethods is to "disable" parts of the REST API. For example, it may be necessary to disable some endpoints that accept DELETE for external usage. By allowing only certain methods in the route, you can hide functionalities of your microservice that should not be consumable without having to modify the code or configurations of your service.

Note: localDir and httpMethods are incompatible. The following route is invalid:

{
  "source": "^/app1/(.*)$",
  "target": "/before/$1/after",
  "localDir": "resources",
  "httpMethods": ["GET", "POST"]
}

However, since localDir supports only GET and HEAD requests, returning 405 to requests with any other method, any localDir route is "implicitly" restricted in terms of supported HTTP methods.

  • Route with a scope

An application specific scope has the following format:

<application-name>.<scope-name>

It is possible to configure what scope the user needs to possess in order to access a specific resource. Those configurations are per route.

In this example, the user should have at least one of the scopes in order to access the corresponding resource.

{
    "source": "^/web-pages/(.*)$",
    "target": "$1",
    "scope": ["$XSAPPNAME.viewer", "$XSAPPNAME.reader", "$XSAPPNAME.writer"]
}

For convenience if our route requires only one scope the scope property can be a string instead of an array. The following configuration is valid as well:

{
    "source": "^/web-pages/(.*)$",
    "target": "$1",
    "scope": "$XSAPPNAME.viewer"
}

You can configure scopes for the different HTTP methods (GET, POST, PUT, HEAD, DELETE, CONNECT, TRACE, PATCH and OPTIONS). If some of the HTTP methods are not explicitly set, the behaviour for them is defined by the default property. In case there is no default property specified and the HTTP method is also not specified, the request is rejected by default.

{
    "source": "^/web-pages/(.*)$",
    "target": "$1",
    "scope": {
      "GET": "$XSAPPNAME.viewer",
      "POST": ["$XSAPPNAME.reader", "$XSAPPNAME.writer"],
      "default": "$XSAPPNAME.guest"
    }
}

The application router supports the $XSAPPNAME placeholder. Its value is taken (and then substituted in the routes) from the UAA configuration. You may read more about it here. Note: The substitution is case sensitive.

You can use the name of the business application directly instead of using the $XSAPPNAME placeholder:

{
    "source": "^/backend/(.*)$",
    "scope": "my-business-application.viewer"
}
  • Route with an identityProvider

For example, we can define several identity providers for different types of users. In this example, there are 2 categories: hospital patients and hospital personnel: 1. patientsIDP – use for authenticating patients. 2. hospitalIDP – use for authenticating all hospital personnel (doctors, nurses etc..).

We can configure 2 routes with the following identityProvider properties:

[
    { 
	"source": "^/patients/sap/opu/odata/(.*)",
	"target": "/sap/opu/odata$1",
	"destination": "backend",
	"authenticationType": "xsuaa",
	"identityProvider": "patientsIDP"
    },
    {
	"source": "^/hospital/sap/opu/odata/(.*)",
	"target": "/sap/opu/odata$1",
	"destination": "backend", "authenticationType": "xsuaa",
	"identityProvider": "hospitalIDP"
    }
]

So, a patient who tries to log into the system will be authenticated by patientIDP, and a doctor who tries to log in will be authenticated by hospitalIDP.

Note: After logging in using one of the identity providers, to switch to the other one it is necessary to logout and perform a new log in.

Note: Currently, dynamic provisioning of the subscriber account identity provider is not supported.

Note: Identity provider configuration is only supported in the client side login redirect flow.

  • Route with a dynamicIdentityProvider

For example, we can define a route where the value of identityProvider is patientsIDP and where dynamic identity provider provisioning is enabled by setting dynamicIdentityProvider to true:

[
    { 
        "source": "^/patients/index.html",
        "target": "/patients-index.html",
        "service": "html5-apps-repo-rt",
        "identityProvider": "patientsIDP",
        "dynamicIdentityProvider": true
    }
]

In this example, the patientsIDP value for the identityProvider is replaced by hospitalIDP if a request with sap_idp=hospitalIDP is executed, for example, if the request is https://shiva.health-center-approuter.cfapps.hana.ondemand.com/healthreport/patients/index.html?sap_idp=hospitalIDP.

Replacements

This object configures the placeholder replacement in static text resources.

PropertyTypeDescription
pathSuffixesArrayAn array containing the path suffixes that are relative to localDir. Only files with a path ending with any of these suffixes will be processed.
varsArrayA list with the environment variables that will be replaced in the files matching the suffix.
servicesObjectAn object describing bound services that will provide replacement values. Each property of this object is used to lookup a separate service. The property names are arbitrary. Service lookup format is described in Service Query section in @sap/xsenv documentation.

The supported tags for replacing environment variables are: {{ENV_VAR}} and {{{ENV_VAR}}}. If there is such an environment variable it will be replaced, otherwise it will be just an empty string.

For services you can specify a property from the credentials section of the service binding which will be replaced. For example {{{my_service.property}}} and {{my_service.property}}

Every variable that is replaced using two-brackets syntax will be HTML-escaped.

For example if the value of the environment variable is ab"cd the result will be ab&amp;quot;cd. The triple brackets syntax is used when the replaced values don't need to be escaped and all values will be unchanged.

For example, if somewhere in your xs-app.json you have a route:

{
  "source": "^/get/home(.*)",
  "target": "$1",
  "localDir": "resources",
  "replace": {
    "pathSuffixes": ["index.html"],
    "vars": ["escaped_text", "NOT_ESCAPED"],
    "services": {
      "my-sapui5-service": {
        "tag": "ui5"
      }
    }
  }
}

and you have the following index.html:

<html>
  <head>
    <title>{{escaped_text}}</title>
    <script src="{{{NOT_ESCAPED}}}/index.js"/>
    <script src="{{{my-sapui5-service.url}}}"/>
  </head>
</html>

then in index.html, {{escaped_text}} and {{{NOT_ESCAPED}}} will be replaced with the values of the environment variables escaped_text and NOT_ESCAPED.

If you have a service in VCAP_SERVICES like:

{
  "sapui5_service": [{
    "name": "sapui5",
    "tags": ["ui5"],
    "credentials": {
      "url": "http://sapui5url"
    }
  }]
}

then {{{my-sapui5-service.url}}} will be replaced with the url property from sapui5 service - in this case http://sapui5url.

Note: All index.html files will be processed. If you want to replace only specific files, you have to set the path of the file relative to localDir.

Note: All files should be UTF-8 encoded.

Note: If a service is not found an error is thrown on startup.

Note: If a service and an environment variable from vars have the same name, an error is thrown on startup.

The returned content type is based on the file extension. Currently the supported file extensions are:

  • .json - application/json
  • .txt - text/plain
  • .html - text/html
  • .js - application/javascript
  • .css - test/css

If the file extension is different, the default content type is text/html.

Example for pathSuffixes:

{
  "pathSuffixes": [".html"]
}

The suffix .html means that all files with the extension .html under localDir and it's subfolders will be processed.

{
  "pathSuffixes": ["/abc/main.html", "some.html"]
}

The suffix /abc/main.html means that all files named main.html which are inside a folder named abc will be processed.

The suffix some.html means that all files which have a name that ends with some.html will be processed. For example: some.html, awesome.html.

{
  "pathSuffixes": ["/some.html"]
}

The suffix /some.html means that all files which have the exact name some.html will be processed. For example: some.html, /abc/some.html.

Note: URL path parameters are not supported for replacements. For example, replacements will not work if the path looks like '/test;color=red/index.html'. For more information regarding path parameters refer to http://tools.ietf.org/html/rfc3986#section-3.3.

xs-app.json configuration file

This is the main configuration file of the application router. It contains a JSON object with the following properties:

PropertyTypeOptionalDescription
welcomeFileStringxThe client is redirected to this page by default, if the request does not have a path. For more information, see welcomeFile.
authenticationMethodStringxIf set to none the UAA login roundtrip is disabled. If the property is not set and authentication is defined per route, the value is set to route by default.
sessionTimeoutNumberxUsed to set session timeout. The default is 15 minutes. If the SESSION_TIMEOUT environment variable is set this property will be overwritten.
routesArrayxContains all route configurations. The position of a configuration in this array is of significance for the application router in case a path matches more than one source. The first route whose source matches the path of the incoming request gets activated.
loginObjectxContains the configuration for the endpoint of the application router which will be used by the UAA during the OAuth2 authentication routine. By default this endpoint is /login/callback.
logoutObjectxProvides options for a Central Logout endpoint and a page to which the client to be redirected by the UAA after logout.
destinationsObjectxAdditional options for your destinations (besides the ones in the destinations environment variable).
servicesObjectxAdditional options for your business services.
responseHeadersArrayxContains the optional response headers configuration.
compressionObjectxConfiguration regarding compressing resources before responding to the client. If the COMPRESSION environment variable is set it will overwrite existing values.
pluginMetadataEndpointStringxAdds an endpoint that will serve a JSON representing all configured plugins.
whitelistServiceObjectxOptions for the allowlist service preventing clickjack attacks.
websocketsObjectxOptions for the web socket communication.
errorPageArrayxOptional configuration to set-up a custom error pages whenever the approuter encouters an error.
corsArrayxContains the configuration for cross-origin resource sharing.

welcomeFile property

Approuter will redirect to this URL when /(root path) is requested. This could be a file located inside the static resources folder or a resource hosted at a different location.

Note: Approuter will serve the content of the resource instead of returning a redirect if the request contains a x-csrf-token: fetch header. See CSRF Protection.

Example:

"welcomeFile": "/web-pages/hello-world.html"

`web-page

16.4.1

11 days ago

16.4.0

13 days ago

16.3.0

26 days ago

16.2.1

2 months ago

16.2.0

2 months ago

16.1.1

2 months ago

16.1.0

3 months ago

16.0.2

4 months ago

16.0.1

4 months ago

16.0.0

4 months ago

15.0.0

4 months ago

14.4.3

5 months ago

14.2.0

10 months ago

14.2.1

9 months ago

14.3.0

9 months ago

14.3.1

8 months ago

14.3.2

8 months ago

14.3.3

7 months ago

14.3.4

6 months ago

14.4.0

5 months ago

14.4.1

5 months ago

14.4.2

5 months ago

14.1.2

11 months ago

14.1.0

1 year ago

14.1.1

1 year ago

13.1.1

1 year ago

14.0.0

1 year ago

12.0.3

1 year ago

12.0.2

1 year ago

13.0.2

1 year ago

13.0.0

1 year ago

13.0.1

1 year ago

13.1.0

1 year ago

12.0.0

1 year ago

12.0.1

1 year ago

11.6.0

2 years ago

11.6.1

1 year ago

11.5.1

2 years ago

11.5.0

2 years ago

11.4.0

2 years ago

11.4.1

2 years ago

11.3.3

2 years ago

11.3.4

2 years ago

11.2.0

2 years ago

11.2.1

2 years ago

11.3.1

2 years ago

11.3.2

2 years ago

11.3.0

2 years ago

10.15.4

2 years ago

11.1.0

2 years ago

11.0.0

2 years ago

11.0.1

2 years ago

10.15.1

2 years ago

10.15.2

2 years ago

10.15.0

2 years ago

10.15.3

2 years ago

10.14.2

2 years ago

10.14.0

2 years ago

10.14.1

2 years ago

10.13.2

2 years ago

10.11.3

2 years ago

10.11.1

2 years ago

10.11.2

2 years ago

10.11.0

2 years ago

10.12.0

2 years ago

10.13.1

2 years ago

10.13.0

2 years ago

10.9.2

2 years ago

10.10.4

2 years ago

10.10.2

2 years ago

10.10.3

2 years ago

10.10.0

2 years ago

10.10.1

2 years ago

10.9.1

2 years ago

10.8.2

3 years ago

10.9.0

3 years ago

10.8.1

3 years ago

10.8.0

3 years ago

10.7.1

3 years ago

10.6.1

3 years ago

10.6.0

3 years ago

10.5.1

3 years ago

10.5.0

3 years ago

10.4.3

3 years ago

10.4.2

3 years ago

10.4.1

3 years ago

10.4.0

3 years ago

10.3.0

3 years ago

10.2.0

3 years ago

10.1.0

3 years ago

9.4.0

3 years ago

10.0.0

3 years ago

9.3.0

3 years ago

9.2.0

3 years ago

9.1.0

3 years ago

9.0.2

3 years ago

9.0.1

3 years ago

9.0.0

3 years ago

8.6.1

3 years ago

8.6.0

3 years ago

8.5.5

4 years ago

8.5.4

4 years ago

8.5.3

4 years ago

8.5.2

4 years ago

8.5.1

4 years ago

8.5.0

4 years ago

8.4.1

4 years ago

8.4.0

4 years ago

8.3.1

4 years ago

8.3.0

4 years ago

8.2.2

4 years ago

8.2.1

4 years ago

8.2.0

4 years ago

8.1.3

4 years ago

8.1.2

4 years ago

8.1.1

4 years ago

8.1.0

4 years ago

6.8.0

4 years ago

5.10.0

4 years ago

5.3.0

4 years ago

6.1.1

4 years ago

6.0.1

4 years ago

5.12.0

4 years ago

2.9.1

4 years ago

6.1.0

4 years ago

7.1.1

4 years ago

6.7.1

4 years ago

5.6.2

4 years ago

7.0.0

4 years ago

6.2.0

4 years ago

6.6.0

4 years ago

6.7.2

4 years ago

5.2.0

4 years ago

2.6.1

4 years ago

5.1.0

4 years ago

5.10.2

4 years ago

6.5.1

4 years ago

6.1.2

4 years ago

6.3.0

4 years ago

8.0.0

4 years ago

5.9.0

4 years ago

6.4.1

4 years ago

4.0.1

4 years ago

5.4.1

4 years ago

6.4.0

4 years ago

5.8.0

4 years ago

6.5.0

4 years ago

5.2.1

4 years ago

5.6.4

4 years ago

6.8.1

4 years ago

5.6.3

4 years ago

2.7.1

4 years ago

6.0.2

4 years ago

5.5.0

4 years ago

7.1.2

4 years ago

6.8.2

4 years ago

5.6.1

4 years ago

3.0.1

4 years ago

5.0.0

4 years ago

5.13.0

4 years ago

5.4.2

4 years ago

5.14.1

4 years ago

2.7.0

4 years ago

5.11.0

4 years ago

4.0.0

4 years ago

5.13.1

4 years ago

5.10.1

4 years ago

7.1.3

4 years ago

5.15.0

4 years ago

5.14.0

4 years ago

2.10.0

4 years ago

6.0.0

4 years ago

5.7.0

4 years ago

6.7.0

4 years ago

7.1.0

4 years ago