Its rare to find devs anticipating Unicode control characters showing up in user input. And the most fun when unanticipated is the Right-To-Left Override character U+202E. Unicode characters have an implicit direction so that for example by default Hebrew characters are rendered from right to left, and English characters are rendered left to right. The override characters force an explicit direction for all the text that follows.
I chose my Twitter display name to include the HTML encoding of the Right-To-Left Override character #x202E;
as a sort of joke or shout out to my favorite Unicode control character.
I did not anticipate that some Twitter clients in some of their UI would fail to encode it correctly. There's no way I can remove that from my display name now.
Try it on Amazon.
The scrollbars in UWP WebView and in Edge have different default behavior leading to many emails to my team. (Everything I talk about here is for the EdgeHtml based WebView and Edge browser and does not apply to the Chromium based Edge browser and WebView2).
There is a Edge only -ms-overflow-style
CSS property that controls scroll behavior. We have a
different default for this in the WebView as compared to the Edge browser. If you want the appearance of the scrollbar in the WebView to match the browser then you must explicitly set that CSS
property. The Edge browser default is scrollbar
which gives us a Windows desktop styled non-auto-hiding scrollbar. The WebView default is -ms-autohiding-scrollbar
which
gives a sort of compromise between desktop and UWP app scrollbar behavior. In this configuration it is auto-hiding. When used with the mouse you'll get Windows desktop styled scrollbars and when
used with touch you'll get the UWP styled scrollbars.
Since WebViews are intended to be used in apps this style is the default in order to better match the app's scrollbars. However this difference between the browser and WebView has led to confusion.
Here’s an -ms-overflow-style JSFiddle showing the difference between the two styles. Try it in the Edge browser and in WebView. An easy way to try it in the Edge WebView is using the JavaScript Browser.
There are two main differences in terms of security between a JavaScript UWP app and the Edge browser:
A JavaScript UWP app has one process (technically not true with background tasks and other edge cases but ignoring that for the moment) that runs in the corresponding appcontainer defined by the app's appx manifest. This one process is where edgehtml is loaded and is rendering HTML, talking to the network, and executing script. Specifically, the UWP main UI thread is the one where your script is running and calling into WinRT.
In the Edge browser there is a browser process running in the same appcontainer defined by its appx manifest, but there are also tab processes. These tab processes are running in restricted app containers that have fewer appx capabilities. The browser process has XAML loaded and coordinates between tabs and handles some (non-WinRT) brokering from the tab processes. The tab processes load edgehtml and that is where they render HTML, talk to the network and execute script.
There is no way to configure the JavaScript UWP app's process model but using WebViews you can approximate it. You can create out of process WebViews and to some extent configure their capabilities, although not to the same extent as the browser. The WebView processes in this case are similar to the browser's tab processes. See the MSWebViewProcess object for configuring out of process WebView creation. I also implemented out of proc WebView tabs in my JSBrowser fork.
The ApplicationContentUriRules (ACUR) section of the appx manifest lets an application define what URIs are considered app code. See a previous post for the list of ACUR effects.
Notably app code is able to access WinRT APIs. Because of this, DOM security restrictions are loosended to match what is possible with WinRT.
Privileged DOM APIs like geolocation, camera, mic etc require a user prompt in the browser before use. App code does not show the same browser prompt. There still may be an OS prompt – the same prompt that applies to any UWP app, but that’s usually per app not per origin.
App code also gets to use XMLHttpRequest or fetch to access cross origin content. Because UWP apps have separate state, cross origin here might not mean much to an attacker unless your app also has the user login to Facebook or some other interesting cross origin target.
The GoBack and GoForward methods on the UWP WebView (x-ms-webview in HTML, Windows.UI.Xaml.Controls.WebView in XAML, and Windows.Web.UI.Interop.WebViewControl in Win32) act the same as the Back and Forward buttons in the Edge browser. They don't necessarily change the top level document of the WebView. If inside the webview an iframe navigates then that navigation will be recorded in the forward/back history and the GoBack / GoForward call may result in navigating that iframe. This makes sense as an end user using the Edge browser since if I click a link to navigate one place and then hit Back I expect to sort of undo that most recent navigation regardless of if that navigation happened in an iframe or the top level document.
If that doesn't make sense for your application and you want to navigate forward or back ignoring iframe navigates, unfortunately there's no perfect workaround.
One workaround could be to try calling GoBack and then checking if a FrameNavigationStarting event fires or a NavigationStarting event fires. If a frame navigates then try calling GoBack again. There could be async races in this case since other navigates could come in and send you the wrong signal and interrupt your multi step GoBack operation.
You could also try keeping track of all top level document navigations and manually navigate back to the URIs you care about. However, GoBack and GoForward also restore some amount of user state (form fills etc) in addition to navigating. Manually calling navigate will not give this same behavior.
If you're developing with the new Windows.Web.UI.Interop.WebViewControl you may have noticed you cannot navigate to localhost HTTP servers. This is because the WebViewControl's WebView process is a UWP process. All UWP processes by default cannot use the loopback adapter as a security precaution. For development purposes you can allow localhost access using the checknetisolation command line tool on the WebViewControl's package just as you can for any other UWP app. The command should be the following:
checknetisolation loopbackexempt -a -n=Microsoft.Win32WebViewHost_cw5n1h2txyewy
As a warning checknetisolation is not good on errors. If you attempt to add a package but get its package family name wrong, checknetisolation just says OK:
C:\Users\davris>checknetisolation LoopbackExempt -a -n=Microsoft.BingWeather_4.21.2492.0_x86__8wekyb3d8bbwe
OK.
And if you then list the result of the add with the
bad name you'll see the following:
[1] -----------------------------------------------------------------
Name: AppContainer NOT FOUND
SID: S-1-15-...
There's also a UI tool for modifying loopback exemption for packages available on GitHub and also one available with Fiddler.
As an additional note, I mentioned above you can try this for development. Do not do this in shipping products as this turns off the security protection for any consumer of the WebViewControl.
Folks familiar with JavaScript UWP apps in Win10 have often been confused by what PWAs in Win10 actually are. TLDR: PWAs in Win10 are simply JavaScript UWP apps. The main difference between these JS UWP Apps and our non-PWA JS UWP apps are our target end developer audience, and how we get Win10 PWAs into the Microsoft Store. See this Win10 blog post on PWAs on Win10 for related info.
On the web a subset of web sites are web apps. These are web sites that have app like behavior - that is a user might call it an app like Outlook, Maps or Gmail. And they may also have a W3C app manifest.
A subset of web apps are progressive web apps. Progressive web apps are web apps that have a W3C app manifest and a service worker. Various OSes are beginning to support PWAs as first class apps on their platform. This is true for Win10 as well in which PWAs are run as a WWA.
In Win10 a WWA (Windows Web App) is an unofficial term for a JavaScript UWP app. These are UWP apps so they have an AppxManifest.xml, they are packaged in an Appx package, they run in an App Container, they use WinRT APIs, and are installed via the Microsoft Store. Specific to WWAs though, is that the AppxManifest.xml specifies a StartPage attribute identifying some HTML content to be used as the app. When the app is activated the OS will create a WWAHost.exe process that hosts the HTML content using the EdgeHtml rendering engine.
Within that we have a notion of a packaged web app and an HWA (hosted web app). There's no real technical distinction for the end developer between these two. The only real difference is whether the StartPage identifies remote HTML content on the web (HWA), or packaged HTML content from the app's appx package (packaged web app). An end developer may create an app that is a mix of these as well, with HTML content in the package and HTML content from the web. These terms are more like ends on a continuum and identifying two different developer scenarios since the underlying technical aspect is pretty much identical.
Win10 PWAs are simply HWAs that specify a StartPage of a URI for a PWA on the web. These are still JavaScript UWP apps with all the same behavior and abilities as other UWP apps. We have two ways of getting PWAs into the Microsoft Store as Win10 PWAs. The first is PWA Builder which is a tool that helps PWA end developers create and submit to the Microsoft Store a Win10 PWA appx package. The second is a crawler that runs over the web looking for PWAs which we convert and submit to the Store using an automated PWA Builder-like tool to create a Win10 PWA from PWAs on the web (see Welcoming PWAs to Win10 for more info). In both cases the conversion involves examining the PWAs W3C app manifest and producing a corresponding AppxManifest.xml. Not all features supported by AppxManifest.xml are also available in the W3c app manifest. But the result of PWA Builder can be a working starting point for end developers who can then update the AppxManifest.xml as they like to support features like share targets or others not available in W3C app manifests.
JSBrowser is a basic browser built as a Win10 JavaScript UWP app around the WebView HTML element. Its fun and relatively simple to implement tiny browser features in JavaScript and in this post I'm implementing crash resistance.
The normal DOM mechanisms for creating an HTML WebView create an in-process WebView, in which the WebView runs on a unique UI thread. But we can use the MSWebView constructor instead to create an out-of-process WebView in which the WebView runs in its own distinct WebView process. Unlike an in-process WebView, Web content running in an out-of-process WebView can only crash the WebView process and not the app process.
this.replaceWebView = () => {
let webview = document.querySelector("#WebView");
// Cannot access webview.src - anything that would need to communicate with the webview process may fail
let oldSrc = browser.currentUrl;
const webviewParent = webview.parentElement;
webviewParent.removeChild(webview);
webview = new MSWebView();
Object.assign(this, {
"webview": webview
});
webview.setAttribute("id", "WebView");
// During startup our currentUrl field is blank. If the WebView has crashed
// and we were on a URI then we may obtain it from this property.
if (browser.currentUrl && browser.currentUrl != "") {
this.trigger("newWebview");
this.navigateTo(browser.currentUrl);
}
webviewParent.appendChild(webview);
I run replaceWebView during startup to replace the in-process WebView created via HTML markup with an out-of-process WebView. I could be doing more to dynamically copy styles, attributes, etc but I know what I need to set on the WebView and just do that.
When a WebView process crashes the corresponding WebView object is no longer useful and a new WebView element must be created. In fact if the old WebView object is used it may throw and will no longer have valid state. Accordingly when the WebView crashes I run replaceWebView again. Additionally, I need to store the last URI we've navigated to (browser.currentUrl in the above) since the crashed WebView object won't know what URI it is on after it crashes.
webview.addEventListener("MSWebViewProcessExited", () => {
if (browser.currentUrl === browser.lastCrashUrl) { ++browser.lastCrashUrlCrashCount;
}
else {
browser.lastCrashUrl = browser.currentUrl;
browser.lastCrashUrlCrashCount = 1;
}
// If we crash again and again on the same URI, maybe stop trying to load that URI.
if (browser.lastCrashUrlCrashCount >= 3) {
browser.lastCrashUrl = "";
browser.lastCrashUrlCrashCount = 0;
browser.currentUrl = browser.startPage;
}
this.replaceWebView();
});
I also keep track of the last URI that we recovered and how many times we've recovered that same URI. If the same URI crashes more than 3 times in a row then I assume that it will keep happening and I navigate to the start URI instead.
JSBrowser is a basic browser built as a Win10 JavaScript UWP app around the WebView HTML element. Its fun and relatively simple to implement tiny browser features in JavaScript and in this post I'm implementing zoom.
My plan to implement zoom is to add a zoom slider to the settings div that controls the scale of the WebView element via CSS transform. My resulting zoom change is in git and you can try the whole thing out in my JSBrowser fork.
I can implement the zoom settings slider as a range type input HTML element. This conveniently provides me a min, max, and step property and suits exactly my purposes. I chose some values that I thought would be reasonable so the browser can scale between half to 3x by increments of one quarter. This is a tiny browser feature after all so there's no custom zoom entry.
<a><label for="webviewZoom">Zoom</label><input type="range" min="50" max="300" step="25" value="100" id="webviewZoom" /></a>
To let the user know this slider is for controlling zoom, I make a label HTML element that says Zoom. The label HTML element has a for attribute which takes the id of another HTML element. This lets the browser know what the label is labelling and lets the browser do things like when the label is clicked to put focus on the slider.
There are no explicit scale APIs for WebView so to change the size of the content in the WebView we use CSS.
this.applyWebviewZoom = state => {
const minValue = this.webviewZoom.getAttribute("min");
const maxValue = this.webviewZoom.getAttribute("max");
const scaleValue = Math.max(Math.min(parseInt(this.webviewZoom.value, 10), maxValue), minValue) / 100;
// Use setAttribute so they all change together to avoid weird visual glitches
this.webview.setAttribute("style", [
["width", (100 / scaleValue) + "%"],
["height", "calc(" + (-40 / scaleValue) + "px + " + (100 / scaleValue) + "%)"],
["transform", "scale(" + scaleValue + ")"]
].map(pair => pair[0] + ": " + pair[1]).join("; "));
};
Because the user changes the scale at runtime I accordingly replace the static CSS for the WebView element with the script above to programmatically modify the style of the WebView. I change the style with one setAttribute call to do my best to avoid the browser performing unnecessary work or displaying the WebView in an intermediate and incomplete state. Applying the scale to the element is as simple as adding 'transform: scale(X)' but then there are two interesting problems.
The first is that the size of the WebView is also scaled not just the content within it. To keep the WebView the same effective size so that it still fits properly into our browser UI, we must compensate for the scale in the WebView width and height. Accordingly, you can see that we scale up by scaleValue and then in width and height we divide by the scaleValue.
transform-origin: 0% 0%;
The other issue is that by default the scale transform's origin is the center of the WebView element. This means when scaled up all sides of the WebView would expand out. But when modifying the width and height those apply relative to the upper left of the element so our inverse scale application to the width and height above aren't quite enough. We also have to change the origin of the scale transform to match the origin of the changes to the width and height.
In Win8.1 JavaScript UWP apps we supported multiple windows using MSApp DOM APIs. In Win10 we use window.open and window and a new MSApp API getViewId and the previous MSApp APIs are gone:
Win10 | Win8.1 | |
---|---|---|
Create new window | window.open | MSApp.createNewView |
New window object | window | MSAppView |
viewId | MSApp.getViewId(window) | MSAppView.viewId |
We use window.open and window for creating new windows, but then to interact with WinRT APIs we add the MSApp.getViewId API. It takes a window object as a parameter and returns a viewId number that can be used with the various Windows.UI.ViewManagement.ApplicationViewSwitcher APIs.
Views in WinRT normally start hidden and the end developer uses something like TryShowAsStandaloneAsync
to display the view once it is fully prepared. In the web world, window.open shows a window immediately and the end user can watch as content is loaded and rendered. To have your new windows act
like views in WinRT and not display immediately we have added a window.open option. For example
let newWindow = window.open("https://example.com", null, "msHideView=yes");
The primary window that is initially opened by the OS acts differently than the secondary windows that it opens:
Primary | Secondary | |
---|---|---|
window.open | Allowed | Disallowed |
window.close | Close app | Close window |
Navigation restrictions | ACUR only | No restrictions |
The restriction on secondary windows such that they cannot open secondary windows could change in the future depending on feedback.
Lastly, there is a very difficult technical issue preventing us from properly supporting synchronous, same-origin, cross-window, script calls. That is, when you open a window that's same origin, script in one window is allowed to directly call functions in the other window and some of these calls will fail. postMessage calls work just fine and is the recommended way to do things if that's possible for you. Otherwise we continue to work on improving this.
The documentation for printing in JavaScript UWP apps is out of date as it all references MSApp.getHtmlPrintDocumentSource but that method has been replaced by MSApp.getHtmlPrintDocumentSourceAsync since WinPhone 8.1.
Previous to WinPhone 8.1 the WebView's HTML content ran on the UI thread of the app. This is troublesome for rendering arbitrary web content since in the extreme case the JavaScript of some arbitrary web page might just sit in a loop and never return control to your app's UI. With WinPhone 8.1 we added off thread WebView in which the WebView runs HTML content on a separate UI thread.
Off thread WebView required changing our MSApp.getHtmlPrintDocumentSource API which could no longer synchronously produce an HtmlPrintDocumentSource. With WebViews running on their own threads it may take some time for them to generate their print content for the HtmlPrintDocumentSource and we don't want to hang the app's UI thread in the interim. So the MSApp.getHtmlPrintDocumentSource API was replaced with MSApp.getHtmlPrintDocumentSourceAsync which returns a promise the resolved value of which is the eventual HtmlPrintDocumentSource.
However, the usage of the API is otherwise unchanged. So in sample code you see referencing MSApp.getHtmlPrintDocumentSource the sample code is still reasonable but you need to call MSApp.getHtmlPrintDocumentSourceAsync instead and wait for the promise to complete. For example the PrintManager docs has an example implementing a PrintTaskRequested event handler in a JavaScript UWP app.
function onPrintTaskRequested(printEvent) {
var printTask = printEvent.request.createPrintTask("Print Sample", function (args) {
args.setSource(MSApp.getHtmlPrintDocumentSource(document));
});
Instead we need to obtain a deferral in the event handler so we can asynchronously wait for getHtmlPrintDocumentSourceAsync to complete:
function onPrintTaskRequested(printEvent) {
var printTask = printEvent.request.createPrintTask("Print Sample", function (args) {
const deferral = args.getDeferral();
MSApp.getHtmlPrintDocumentSourceAsync(document).then(htmlPrintDocumentSource => {
args.setSource(htmlPrintDocumentSource);
deferral.complete();
}, error => {
console.error("Error: " + error.message + " " + error.stack);
deferral.complete();
});
});
The x-ms-webview HTML element has the void addWebAllowedObject(string name, any value) method and the webview XAML element has the void AddWebAllowedObject(String name, Object value) method. The object parameter is projected into the webview’s top-level HTML document’s script engine as a new property on the global object with property name set to the name parameter. It is not injected into the current document but rather it is projected during initialization of the next top-level HTML document to which the webview navigates.
If AddWebAllowedObject is called during a NavigationStarting event handler the object will be injected into the document resulting from the navigation corresponding to that event.
If AddWebAllowedObject is called outside of the NavigationStarting event handler it will apply to the navigation corresponding to the next explicit navigate method called on the webview or the navigation corresponding to the next NavigationStarting event handler that fires, whichever comes first.
To avoid this potential race, you should use AddWebAllowedObject in one of two ways: 1. During a NavigationStarting event handler, 2. Before calling a Navigate method and without returning to the main loop.
If called both before calling a navigate method and in the NavigationStarting event handler then the result is the aggregate of all those calls.
If called multiple times for the same document with the same name the last call wins and the previous are silently ignored.
If AddWebAllowedObject is called for a navigation and that navigation fails or redirects to a different URI, the AddWebAllowedObject call is silently ignored.
After successfully adding an object to a document, the object will no longer be projected once a navigation to a new document occurs.
If AddWebAllowedObject is called for a document with All WinRT access then projection will succeed and the object will be added.
If AddWebAllowedObject is called for a document which has a URI which has no declared WinRT access via ApplicationContentUriRules then Allow for web only WinRT access is given to that document.
If the document has Allow for web only WinRT access then projection will succeed only if the object’s runtimeclass has the Windows.Foundation.Metadata.AllowForWeb metadata attribute.
The object must implement the IAgileObject interface. Because the XAML and HTML webview elements run on ASTA view threads and the webview’s content’s JavaScript thread runs on another ASTA thread a developer should not create their non-agile runtimeclass on the view thread. To encourage end developers to do this correctly we require the object implements IAgileObject.
The name parameter must be a valid JavaScript property name, otherwise the call will fail silently. If the name is already a property name on the global object, that property is overwritten if the property is configurable. Non-configurable properties on the global object are not overwritten and the AddWebAllowedObject call fails silently. On success, the projected property is writable, configurable, and enumerable.
Some errors as described above fail silently. Other issues, such as lack of IAgileObject or lack of the AllowForWeb attribute result in an error in the JavaScript developer console.
Previously I described Application Content URI Rules (ACUR) parsing and ACUR ordering. This post describes what you get from putting a URI in ACUR.
URIs in the ACUR gain the following which is otherwise unavailable:
URIs in the ACUR that also have full WinRT access additionally gain the following:
JavaScript Microsoft Store apps have some details related to activation that are specific to JavaScript Store apps and that are poorly documented which I’ll describe here.
The StartPage attributes in the AppxManifest.xml (Package/Applications/Application/@StartPage, Package/Applications/Extensions/Extension/@StartPage) define the HTML page entry point for that kind of activation. That is, Application/@StartPage defines the entry point for tile activation, Extension[@Category="windows.protocol"]/@StartPage defines the entry point for URI handling activation, etc. There are two kinds of supported values in StartPage attributes: relative Windows file paths and absolute URIs. If the attribute doesn’t parse as an absolute URI then it is instead interpreted as relative Windows file path.
This implies a few things that I’ll declare explicitly here. Windows file paths, unlike URIs, don’t have a query or fragment, so if you are using a relative Windows file path for your StartPage attribute you cannot include anything like ‘?param=value’ at the end. Absolute URIs use percent-encoding for reserved characters like ‘%’ and ‘#’. If you have a ‘#’ in your HTML filename then you need to percent-encode that ‘#’ for a URI and not for a relative Windows file path.
If you specify a relative Windows file path, it is turned into an ms-appx URI by changing all backslashes to forward slashes, percent-encoding reserved characters, and combining the result with a base URI of ms-appx:///. Accordingly the relative Windows file paths are relative to the root of your package. If you are using a relative Windows file path as your StartPage and need to switch to using a URI so you can include a query or fragment, you can follow the same steps above.
The validity of the StartPage is not determined before activation. If the StartPage is a relative Windows file path for a file that doesn’t exist, or an absolute URI that is not in the Application Content URI Rules, or something that doesn’t parse as a Windows file path or URI, or otherwise an absolute URI that fails to resolve (404, bad hostname, etc etc) then the JavaScript app will navigate to the app’s navigation error page (perhaps more on that in a future blog post). Just to call it out explicitly because I have personally accidentally done this: StartPage URIs are not automatically included in the Application Content URI Rules and if you forget to include your StartPage in your ACUR you will always fail to navigate to that StartPage.
When your app is activated for a particular activation kind, the StartPage value from the entry in your app’s manifest that corresponds to that activation kind is used as the navigation target.
If the app is not already running, the app is activated, navigated to that StartPage value and then the Windows.UI.WebUI.WebUIApplication activated
event is fired (more details on
the order of various events in a moment). If, however, your app is already running and an activation occurs, we navigate or don’t navigate to the corresponding StartPage depending on the current
page of the app. Take the app’s current top level document’s URI and if after removing the fragment it already matches the StartPage value then we won’t navigate and will jump straight to firing
the WebUIApplication activated event.
Since navigating the top-level document means destroying the current JavaScript engine instance and losing all your state, this behavior might be a problem for you. If so, you can use the
MSApp.pageHandlesAllApplicationActivations(true)
API to always skip navigating to the StartPage and instead always jump straight to firing the WebUIApplication activated event. This
does require of course that all of your pages all handle all activation kinds about which any part of your app cares.
Application Content URI Rules (ACUR from now on) defines the bounds of the web that make up the Microsoft Store application. Package content via the ms-appx URI scheme is automatically considered part of the app. But if you have content on the web via http or https you can use ACUR to declare to Windows that those URIs are also part of your application. When your app navigates to URIs on the web those URIs will be matched against the ACUR to determine if they are part of your app or not. The documentation for how matching is done on the wildcard URIs in the ACUR Rule elements is not very helpful on MSDN so here are some notes.
You can have up to 100 Rule XML elements per ApplicationContentUriRules element. Each has a Match attribute that can be up to 2084 characters long. The content of the Match attribute is parsed with CreateUri and when matching against URIs on the web additional wildcard processing is performed. I’ll call the URI from the ACUR Rule the rule URI and the URI we compare it to found during app navigation the navigation URI.
The rule URI is matched to a navigation URI by URI component: scheme, username, password, host, port, path, query, and fragment. If a component does not exist on the rule URI then it matches any value of that component in the navigation URI. For example, a rule URI with no fragment will match a navigation URI with no fragment, with an empty string fragment, or a fragment with any value in it.
Each component except the port may have up to 8 asterisks. Two asterisks in a row counts as an escape and will match 1 literal asterisk. For scheme, username, password, query and fragment the asterisk matches whatever it can within the component.
For the host, if the host consists of exactly one single asterisk then it matches anything. Otherwise an asterisk in a host only matches within its domain name label. For example, http://*.example.com will match http://a.example.com/ but not http://b.a.example.com/ or http://example.com/. And http://*/ will match http://example.com, http://a.example.com/, and http://b.a.example.com/. However the Store places restrictions on submitting apps that use the http://* rule or rules with an asterisk in the second effective domain name label. For example, http://*.com is also restricted for Store submission.
For the path, an asterisk matches within the path segment. For example, http://example.com/a/*/c will match http://example.com/a/b/c and http://example.com/a//c but not http://example.com/a/b/b/c or http://example.com/a/c
Additionally for the path, if the path ends with a slash then it matches any path that starts with that same path. For example, http://example.com/a/ will match http://example.com/a/b and http://example.com/a/b/c/d/e/, but not http://example.com/b/.
If the path doesn’t end with a slash then there is no suffix matching performed. For example, http://example.com/a will match only http://example.com/a and no URIs with a different path.
As a part of parsing the rule URI and the navigation URI, CreateUri will perform URI normalization and so the hostname and scheme will be made lower case (casing matters in all other parts of the URI and case sensitive comparisons will be performed), IDN normalization will be performed, ‘.’ and ‘..’ path segments will be resolved and other normalizations as described in the CreateUri documentation.