// Copyright © 2021 The CefSharp Authors. All rights reserved.
|
//
|
// Use of this source code is governed by a BSD-style license that can be found in the LICENSE file.
|
|
using System;
|
using System.Collections.Generic;
|
using System.Collections.Specialized;
|
using System.IO;
|
using System.Linq;
|
using System.Net;
|
using System.Threading.Tasks;
|
|
namespace CefSharp.SchemeHandler
|
{
|
//Shorthand for Owin pipeline func
|
using AppFunc = Func<IDictionary<string, object>, Task>;
|
|
/// <summary>
|
/// <see cref="ResourceHandler"/> implementation that uses an OWIN capable host of fulfilling requests.
|
/// Can be used with NancyFx or AspNet Core
|
/// </summary>
|
/// TODO:
|
/// - Multipart post data
|
/// - Cancellation Token
|
public class OwinResourceHandler : ResourceHandler
|
{
|
private static readonly Dictionary<int, string> StatusCodeToStatusTextMapping = new Dictionary<int, string>
|
{
|
{200, "OK"},
|
{301, "Moved Permanently"},
|
{304, "Not Modified"},
|
{404, "Not Found"}
|
};
|
|
private readonly AppFunc appFunc;
|
|
/// <summary>
|
/// OwinResourceHandler
|
/// </summary>
|
/// <param name="appFunc">Owin pipeline func</param>
|
public OwinResourceHandler(AppFunc appFunc)
|
{
|
this.appFunc = appFunc;
|
}
|
|
/// <summary>
|
/// Read the request, then process it through the OWEN pipeline
|
/// then populate the response properties.
|
/// </summary>
|
/// <param name="request">request</param>
|
/// <param name="callback">callback</param>
|
/// <returns>always returns true as we'll handle all requests this handler is registered for.</returns>
|
public override CefReturnValue ProcessRequestAsync(IRequest request, ICallback callback)
|
{
|
// PART 1 - Read the request - here we read the request and create a dictionary
|
// that follows the OWEN standard
|
|
var responseStream = new MemoryStream();
|
var requestBody = Stream.Null;
|
|
if (request.Method == "POST")
|
{
|
using (var postData = request.PostData)
|
{
|
if (postData != null)
|
{
|
var postDataElements = postData.Elements;
|
|
|
var firstPostDataElement = postDataElements.First();
|
|
var bytes = firstPostDataElement.Bytes;
|
|
requestBody = new MemoryStream(bytes, 0, bytes.Length);
|
|
//TODO: Investigate how to process multi part POST data
|
//var charSet = request.GetCharSet();
|
//foreach (var element in elements)
|
//{
|
// if (element.Type == PostDataElementType.Bytes)
|
// {
|
// var body = element.GetBody(charSet);
|
// }
|
//}
|
}
|
}
|
}
|
|
//var cancellationTokenSource = new CancellationTokenSource();
|
//var cancellationToken = cancellationTokenSource.Token;
|
var uri = new Uri(request.Url);
|
var requestHeaders = ToDictionary(request.Headers);
|
//Add Host header as per http://owin.org/html/owin.html#5-2-hostname
|
requestHeaders.Add("Host", new[] { uri.Host + (uri.Port > 0 ? (":" + uri.Port) : "") });
|
|
//http://owin.org/html/owin.html#3-2-environment
|
//The Environment dictionary stores information about the request,
|
//the response, and any relevant server state.
|
//The server is responsible for providing body streams and header collections for both the request and response in the initial call.
|
//The application then populates the appropriate fields with response data, writes the response body, and returns when done.
|
//Keys MUST be compared using StringComparer.Ordinal.
|
var owinEnvironment = new Dictionary<string, object>(StringComparer.Ordinal)
|
{
|
//Request http://owin.org/html/owin.html#3-2-1-request-data
|
{"owin.RequestBody", requestBody},
|
{"owin.RequestHeaders", requestHeaders},
|
{"owin.RequestMethod", request.Method},
|
{"owin.RequestPath", uri.AbsolutePath},
|
{"owin.RequestPathBase", "/"},
|
{"owin.RequestProtocol", "HTTP/1.1"},
|
//To conform to the OWIN spec we need to remove the leading '?'
|
{"owin.RequestQueryString", string.IsNullOrEmpty(uri.Query) ? string.Empty : uri.Query.Substring(1)},
|
{"owin.RequestScheme", uri.Scheme},
|
//Response http://owin.org/html/owin.html#3-2-2-response-data
|
{"owin.ResponseHeaders", new Dictionary<string, string[]>(StringComparer.OrdinalIgnoreCase)},
|
{"owin.ResponseBody", responseStream},
|
//Other Data
|
{"owin.Version", "1.0.0"},
|
//{"owin.CallCancelled", cancellationToken}
|
};
|
|
//PART 2 - Spawn a new task to execute the OWIN pipeline
|
//We execute this in an async fashion and return true so other processing
|
//can occur
|
Task.Run(async () =>
|
{
|
//Call into the OWEN pipeline
|
try
|
{
|
await appFunc(owinEnvironment);
|
|
//Response has been populated - reset the position to 0 so it can be read
|
responseStream.Position = 0;
|
|
int statusCode;
|
|
if (owinEnvironment.ContainsKey("owin.ResponseStatusCode"))
|
{
|
statusCode = Convert.ToInt32(owinEnvironment["owin.ResponseStatusCode"]);
|
//TODO: Improve status code mapping - see if CEF has a helper function that can be exposed
|
//StatusText = StatusCodeToStatusTextMapping[response.StatusCode];
|
}
|
else
|
{
|
statusCode = (int)HttpStatusCode.OK;
|
//StatusText = "OK";
|
}
|
|
//Grab a reference to the ResponseHeaders
|
var responseHeaders = (Dictionary<string, string[]>)owinEnvironment["owin.ResponseHeaders"];
|
|
//Populate the response properties
|
Stream = responseStream;
|
ResponseLength = responseStream.Length;
|
StatusCode = statusCode;
|
|
if(responseHeaders.ContainsKey("Content-Type"))
|
{
|
var contentType = responseHeaders["Content-Type"].First();
|
MimeType = contentType.Split(';').First();
|
}
|
else
|
{
|
MimeType = DefaultMimeType;
|
}
|
|
//Add the response headers from OWIN to the Headers NameValueCollection
|
foreach (var responseHeader in responseHeaders)
|
{
|
//It's possible for headers to have multiple values
|
foreach (var val in responseHeader.Value)
|
{
|
Headers.Add(responseHeader.Key, val);
|
}
|
}
|
}
|
catch(Exception ex)
|
{
|
int statusCode = (int)HttpStatusCode.InternalServerError;
|
|
var responseData = GetByteArray("Error: " + ex.ToString(), System.Text.Encoding.UTF8, true);
|
|
//Populate the response properties
|
Stream = new MemoryStream(responseData);
|
ResponseLength = responseData.Length;
|
StatusCode = statusCode;
|
MimeType = "text/html";
|
}
|
|
//Once we've finished populating the properties we execute the callback
|
//Callback wraps an unmanaged resource, so let's explicitly Dispose when we're done
|
using (callback)
|
{
|
callback.Continue();
|
}
|
});
|
|
return CefReturnValue.ContinueAsync;
|
}
|
|
private static IDictionary<string, string[]> ToDictionary(NameValueCollection nameValueCollection)
|
{
|
var dict = new Dictionary<string, string[]>(StringComparer.OrdinalIgnoreCase);
|
foreach (var key in nameValueCollection.AllKeys)
|
{
|
if (!dict.ContainsKey(key))
|
{
|
dict.Add(key, new string[0]);
|
}
|
var strings = nameValueCollection.GetValues(key);
|
if (strings == null)
|
{
|
continue;
|
}
|
foreach (string value in strings)
|
{
|
var values = dict[key].ToList();
|
values.Add(value);
|
dict[key] = values.ToArray();
|
}
|
}
|
return dict;
|
}
|
}
|
}
|