Posted on: June 26th, 2012 by jsolutions 6 Comments

Since writing this post yesterday, it has been brought to my attention that this approach may in fact be circumnavigating some of the security constraints imposed on loading local content. The MSDN Link here states the following:

Application data and resources

Sometimes it is useful to refer to resources you have downloaded from the Internet to your app’s local ApplicationData storage (via Windows Runtime APIs). To refer to such content, you must use the scheme “ms-appdata:”, with the path to the file within your ApplicationData local storage. Note that, for security reasons, you cannot navigate to HTML you have downloaded to this location and you cannot run any executable or potentially executable code, such as script or CSS. It is intended for media such as images or videos and the like. Finally, you may also refer to resources that are included in your app, such as localized images, via the scheme “ms-resource:”.

The original article follows ….

Developing Metro apps for Windows 8 is great, until you come across a restriction that appears to serve no purpose and has little in the way of documentation. The WebView control for Windows 8 apps is one of the controls where I have experienced this.

The source code can be found here.

The Problem – Local HTML Resources

The problem is that the webview has no way of loading local resources based on a URL, but rather requires the developer to load the file and use NavigateToString to render the content, which of course loses all base URI context and hence cannot handle images, css, javascript or any other extra resources. I’m still a bit unclear as to why the situation is as it is, but rather than spend time investigating that, I needed to come up with a solution.

The Solution – In Process HTTP Server

In my scenario, I want to be able to view content whilst disconnected, downloading resources for viewing later. A simple solution is to embed a small HTTP server in my metro app and use this to serve up the local content. Metro apps are permitted to communicate to themselves via sockets on the loopback address, so this was an ideal solution. It will also prove useful moving forward as interactions with the content can also be cached whilst disconnected, which is another use case for the application I am working on.

Step 1 – Create a Metro Project with the correct Capabilities defined in the App Manifest.

The example included with the post was created using a new Blank C# Windows Metro App in Visual Studio 2012 RC. And the following Capabilities were added to the App Manifest:

  • Home or Work Networking
  • Internet (Client & Server)
  • Internet (Client) - this probably isn’t needed but was set by default I believe.

This allows the app to open sockets for accepting incoming connections, which is obviously vital for our HTTP server to work correctly.

Step 2 – Prepare Some Content for Serving via HTTP.

I added a simple html file, with a css, js and image file to serve up slightly differing content. These were added to the project as content to be deployed with the application. Equally this could be content that is downloaded prior to serving or documents in a shared part of the disk.

Step 3 – Add WebView control to the MainPage XAML.

As this is just an example app, I didn’t bother with anything beyond hard coding a URL into the WebView markup in the MainPage.xaml file:

<Grid Background={StaticResource ...}>
    <WebView Source="http://localhost:8088/index.html"/>
</Grid>

The port was hardcoded for now.

Step 4 – Create the HTTP Server class.

The HTTP Server class listens on the socket using a StreamSocketListener:

/// <summary>
/// A simple HTTP Server class.
/// </summary>
class HttpService
{
    ...

    // The default port to listen on
    private const string DEFAULT_PORT = "8088";

    // a socket listener instance
    private readonly StreamSocketListener _listener;

    ...

    /// <summary>
    /// create an instance of a HttpService class./>
    /// </summary>
    public HttpService()
    {
        _listener = new StreamSocketListener();

        ...

        // start the service listening
        StartService();
    }

    /// <summary>
    /// Start the HTTP Server
    /// </summary>
    private async void StartService()
    {
        // when a connection is recieved, process
        // the request.
        _listener.ConnectionReceived += (s, e) =>
        {
            ProcessRequestAsync(e.Socket);
        };

        // Bind the service to the default port.
        await _listener.BindServiceNameAsync(DEFAULT_PORT);
    }

    ...
}

When a connection is recieved, the request is extracted. Only the GET verb is supported in this example code:

/// <summary>
/// When a connection is recieved, process the request.
/// </summary>
/// <param name="socket">the incoming socket connection.</param>
private async void ProcessRequestAsync(StreamSocket socket)
{
    StringBuilder inputRequestBuilder = new StringBuilder();

    // Read all the request data.
    // (This is assuming it is all text data of course)
    using(var input = socket.InputStream)
    {
        var data = new Windows.Storage.Streams.Buffer(BUFFER_SIZE);
        uint dataRead = BUFFER_SIZE;

        while (dataRead == BUFFER_SIZE)
        {
            await input.ReadAsync(data, BUFFER_SIZE,
                InputStreamOptions.Partial);

            var dataArray = data.ToArray();
            var dataString = Encoding.UTF8.GetString(dataArray, 0, dataArray.Length);

            inputRequestBuilder.Append(dataString);

            dataRead = data.Length;
        }
    }

    using(var output = socket.OutputStream)
    {
        // extract the request string.
        var request = inputRequestBuilder.ToString();
        var requestMethod = request.Split('\n')[0];
        var requestParts = requestMethod.Split(' ');

        if (requestParts[0].CompareTo("GET") == 0)
        {
            // process the request and write the response.
            await WriteResponseAsync(requestParts[1], socket.OutputStream);
        }
    }
}

The server then checks the file extension against a list of content types, loads the file and posts it back via the socket:

/// <summary>
/// Write the HTTP response to the request out to the output
/// stream on the socket.
/// </summary>
/// <param name="resourceName">The resource name to retrieve.</param>
/// <param name="outputStream">The output stream to write to.</param>
/// <returns>A task object.</returns>
private async Task WriteResponseAsync(string resourceName, IOutputStream outputStream)
{
    using(var writeStream = outputStream.AsStreamForWrite())
    {
        // check the extension is supported.
        var extension = Path.GetExtension(resourceName);

        if(_contentTypes.ContainsKey(extension))
        {
            string contentType = _contentTypes[extension];

            // read the local data.
            var localFolder = Windows.ApplicationModel.Package.Current.InstalledLocation;

            var requestedFile = await localFolder.GetFileAsync("Data" + resourceName.Replace('/', '\\'));
            var fileStream = await requestedFile.OpenReadAsync();
            var size = fileStream.Size;

            // write out the HTTP headers.
            var header = String.Format("HTTP/1.1 200 OK\n" +
                                     "Content-Type: {0}\n" +
                                     "Content-Length: {1}\n" +
                                     "Connection: close\n" +
                                     "\n",
                                     contentType,
                                     fileStream.Size);

            var headerArray = Encoding.UTF8.GetBytes(header);

            await writeStream.WriteAsync(headerArray, 0, headerArray.Length);

            // copy the requested file to the output stream.
            await fileStream.AsStreamForRead().CopyToAsync(writeStream);
        }
        else
        {
            // unrecognised file type, just handle as
            // a not found.

            var header = "HTTP/1.1 404 Not Found\n" +
                         "Connection: close\n\n";
            var headerArray = Encoding.UTF8.GetBytes(header);
            await writeStream.WriteAsync(headerArray, 0, headerArray.Length);
        }
        await writeStream.FlushAsync();
    }
}

The “Connection: close” header is important for correct operation, otherwise the WebView attempts to re-use the sockets which isn’t supported by this code.

Summary

Whether the WebView control will support local URLs when Windows 8 is finally released is doubtful. However this solutions not only solves the problem but offers a lot more potential for disconnected scenarios.

The source code can be found here.

Tags: , , , , ,

6 Responses

  1. balint says:

    hi,
    thanks for sharing!
    I’ve tried your example code but, if I try to load more files (images or js), it fails badly with IO access errors. If I insert a breakpoint and step it through manually, it works. Any ideas?

  2. Smith says:

    Hi,

    I try to load a complex html file. After few interaction with the web, i meet an exception:
    An existing connection was forcibly closed by the remote host. (Exception from HRESULT: 0×80072746)

    Did you have any experience on that ?

    Thanks

  3. jsolutions says:

    I have to admit to having dropped this idea, as it appears to circumnavigate security features enforced on Metro applications.

    If you are loading resources from the packaged application itself, rather than from ApplicationData storage, you can just use the ms-appx-web:// scheme with the WebView control.

    The example I gave here did in fact use data from the packaged application itself, rather than the ApplicationData storage. So where I have:

    This can just be substitued with:

    And it will work fine, without all the malarkey of having an in process http server.

    If however you really want to serve up data from ms-appdata, I’m afraid it looks like MS won’t let you, so bypassing it with the mechanism I have described here may fail app store approval ….. which I agree is a major pain!

  4. H.King says:

    HI, i hava download some web to the local: Windows.Storage.ApplicationData.Current.LocalFolder.Path
    , but i can display , how do i?

Leave a Reply