// Invoked once per request. public async Task Invoke(IOwinContext context) { using (var trace = new Trace(context.Request.Uri.PathAndQuery.ToString())) { string json, templateName, templateData; if (context.Request.Uri.PathAndQuery == "/favicon.ico") { context.Response.StatusCode = 404; await context.Response.WriteAsync(new byte[] { }); return; } try { // pass the same method through for all requests HttpMethod method = new HttpMethod(context.Request.Method); var uri = (HandlebarsProxyConfiguration.Instance.Scheme + "://" + HandlebarsProxyConfiguration.Instance.Domain + ":" + HandlebarsProxyConfiguration.Instance.DomainPort + GetProxyUri(context.Request.Uri)).ToLower(); HttpRequestMessage request = new HttpRequestMessage(method, new Uri(uri)); // keep the same user agent request.Headers.TryAddWithoutValidation("User-Agent", context.Request.Headers["User-Agent"]); // hack the cookies we do and don't want if (context.Request.Headers["Cookie"] != null && !string.IsNullOrEmpty(context.Request.Headers["Cookie"])) { request.Headers.TryAddWithoutValidation("Cookie", context.Request.Headers["Cookie"]); } // check if there is a response for this URI HttpResponseMessage cachedResponse = _store[uri] as HttpResponseMessage; if (cachedResponse != null && context.Request.Method == "GET") { // add an "IfNoneMatch" to the new request to validate this one. if (cachedResponse.Headers.ETag != null) { request.Headers.TryAddWithoutValidation("If-None-Match", cachedResponse.Headers.ETag.ToString()); } } byte[] data; HttpResponseMessage response; switch (context.Request.Method) { case "PUT": case "POST": using (var ms = new MemoryStream()) { context.Request.Body.CopyTo(ms); var originalFormContent = Encoding.UTF8.GetString(ms.ToArray()); var newFormContent = new FormUrlEncodedContent(originalFormContent.Split('&') .Select(_ => { var parts = _.Split('='); var kvp = new KeyValuePair <string, string>(Uri.UnescapeDataString(parts[0]), Uri.UnescapeDataString(parts[1])); return(kvp); })); request.Content = newFormContent; // request.Content.Headers.ContentType = new MediaTypeHeaderValue(context.Request.Headers["Content-Type"]); response = await _client.SendAsync(request); data = await response.Content.ReadAsByteArrayAsync(); if (response.Headers.Contains("Set-Cookie")) { var setCookie = response.Headers.GetValues("Set-Cookie").FirstOrDefault(); if (!string.IsNullOrEmpty(setCookie)) { var cookie = setCookie.ToString(); cookie = cookie.Replace("." + HandlebarsProxyConfiguration.Instance.Domain.Replace("www", "").Trim('.'), HandlebarsProxyConfiguration.Instance.Hostname); Console.WriteLine("Set Cookie: " + cookie); context.Response.Headers["Set-Cookie"] = cookie; } } } break; default: response = await _client.SendAsync(request); data = await response.Content.ReadAsByteArrayAsync(); break; } // use the cached version if (cachedResponse != null && response.StatusCode == HttpStatusCode.NotModified) { response = cachedResponse; data = await cachedResponse.Content.ReadAsByteArrayAsync(); await Console.Out.WriteLineAsync("Using Cached Response"); } // save the response in the cache else if (response.Headers.ETag != null && cachedResponse == null) { CacheItemPolicy policy = new CacheItemPolicy(); policy.AbsoluteExpiration = DateTimeOffset.Now.AddDays(1); _store.Set(request.RequestUri.ToString(), response, policy); } // set headers we need for compatibility in weird situations context.Response.Headers["Access-Control-Allow-Origin"] = "*"; context.Response.Headers["Access-Control-Allow-Methods"] = "GET,PUT,POST,DELETE"; context.Response.Headers["Access-Control-Allow-Headers"] = "Content-Type"; // if this is a redirect, pass it through var location = response.Headers.Location; if (location != null) { var locationUri = new Uri(location.ToString()); // locationUri.Host = HandlebarsProxyConfiguration.Instance.Hostname; // locationUri.Port = HandlebarsProxyConfiguration.Instance.Port; context.Response.Headers["Location"] = locationUri.PathAndQuery; } // if this is an API request, then pass it staight to the client, no template logic if (context.Request.Uri.PathAndQuery.StartsWith("/api") || !response.Content.Headers.ContentType.MediaType.Contains("application/json") || response.StatusCode == HttpStatusCode.Found || response.StatusCode == HttpStatusCode.Redirect || response.StatusCode == HttpStatusCode.TemporaryRedirect) { context.Response.StatusCode = (int)response.StatusCode; context.Response.ContentType = response.Content.Headers.ContentType.MediaType; await context.Response.WriteAsync(data); return; } // ok, now we apply some smarts, first things first, get the data as a JSON object json = Encoding.UTF8.GetString(data); var o = JObject.Parse(json); o["debug"] = true; // here we need to ensure that we are replacing the hostname with the configured local version // this means any logic around hostnames uses this o["_config"]["api"] = "http://" + HandlebarsProxyConfiguration.Instance.Hostname + ":" + HandlebarsProxyConfiguration.Instance.Port; o["_config"]["cdn"] = HandlebarsProxyConfiguration.Instance.ContentDeliveryNetwork; // "//cdn.archfashion.dev"; o["_request"]["protocol"] = "http"; o["_request"]["gzip"] = false; o["_request"]["fqdn"] = "http://" + HandlebarsProxyConfiguration.Instance.Hostname + ":" + HandlebarsProxyConfiguration.Instance.Port; o["_request"]["hostname"] = HandlebarsProxyConfiguration.Instance.Hostname + ":" + HandlebarsProxyConfiguration.Instance.Port; // Add an experiment with the variation as instructed by the request var qs = new UrlEncodingParser(context.Request.QueryString.ToUriComponent()); if (!string.IsNullOrEmpty(qs["experiment"]) && !string.IsNullOrEmpty(qs["alias"])) { JObject experiment = o["_experiment"] as JObject; if (experiment == null) { experiment = new JObject(); o["_experiment"] = experiment; } experiment.RemoveAll(); experiment.Add(qs["experiment"].Urlify(), JObject.Parse(@"{""id"":""rAnDoMlEtTeRs"",""variation"":0,""variation_alias"":""" + qs["alias"].Urlify() + @"""}")); } json = o.ToString(Formatting.None); // send the modified response back to the client (obviously the developer is // trying to work out what is being combined with the template) if (qs["x-format"] == "json") { context.Response.StatusCode = 200; context.Response.ContentType = response.Content.Headers.ContentType.MediaType; await context.Response.WriteAsync(json); return; } // get the header which has the handlebars template in it var xTemplate = response.Headers.GetValues("x-template").FirstOrDefault(); if (string.IsNullOrEmpty(xTemplate)) { await Console.Error.WriteLineAsync("No x-template header for URL: " + request.RequestUri.ToString()); await context.Response.WriteAsync("No x-template header for URL: " + request.RequestUri.ToString()); return; } // get the local template templateName = xTemplate.ToString(); var templatePath = Path.Combine(HandlebarsProxyConfiguration.Instance.Directory + "\\template", templateName.Replace("/", "\\") + ".handlebars"); // try different file extension if (!File.Exists(templatePath)) { templatePath = Path.Combine(HandlebarsProxyConfiguration.Instance.Directory + "\\template", templateName.Replace("/", "\\") + ".hbs"); } // get the data of the template templateData = File.ReadAllText(templatePath); // if this is file not found send a friendly message using (var render = new Trace("render")) { var r = _template.Render(templateName, json); var html = FillSectionData(r, json); var donuts = new List <string>(); var templates = new Dictionary <string, string>(); // detect the donuts int index = html.IndexOf("####donut:", 0, false); int length = 4; while (index != -1) { length = html.IndexOf("####", index + 10, false) - index - 10; donuts.Add(html.ToString(index + 10, length)); if (index + length > html.Length) { break; } index = html.IndexOf("####donut:", index + length, false); } // execute any donuts var sync = new object(); var tasks = new List <Task <KeyValuePair <string, string> > >(donuts.Count); if (donuts.Count > 0) { using (var handler = new WebRequestHandler() { UseCookies = false, AllowAutoRedirect = false }) using (HttpClient client = new HttpClient(handler, false) { BaseAddress = new Uri(HandlebarsProxyConfiguration.Instance.Scheme + "://" + HandlebarsProxyConfiguration.Instance.Domain + ":" + HandlebarsProxyConfiguration.Instance.DomainPort) }) { foreach (var donut in donuts) { var duri = new Uri((HandlebarsProxyConfiguration.Instance.Scheme + "://" + HandlebarsProxyConfiguration.Instance.Domain + ":" + HandlebarsProxyConfiguration.Instance.DomainPort + GetProxyUri(donut)).ToLower()); await Console.Out.WriteLineAsync("Getting Donut: " + duri.ToString()); HttpRequestMessage drequest = new HttpRequestMessage(HttpMethod.Get, duri); drequest.Headers.Authorization = _authHeader; drequest.Headers.TryAddWithoutValidation("User-Agent", context.Request.Headers["User-Agent"]); drequest.Headers.TryAddWithoutValidation("Cookie", context.Request.Headers["Cookie"]); // the donut response's don't have etags so instead compare the cookies // as changes to the cookie state will be what impacts the cache, eg: login and out // or experiment changes HttpResponseMessage dcachedResponse = _store[duri.ToString()] as HttpResponseMessage; if (dcachedResponse != null && (dcachedResponse.Headers.Contains("Cookie") && string.Join(",", dcachedResponse.RequestMessage.Headers.GetValues("Cookie").ToArray()) == string.Join(",", drequest.Headers.GetValues("Cookie").ToArray())) && (dcachedResponse.Headers.Contains("User-Agent") && string.Join(",", dcachedResponse.RequestMessage.Headers.GetValues("User-Agent").ToArray()) == string.Join(",", drequest.Headers.GetValues("User-Agent").ToArray())) ) { // OK, from here we can do the continue task without actually "dong anything" await Console.Out.WriteLineAsync("Cached Response: " + duri.ToString()); var dDataTask = dcachedResponse.Content.ReadAsByteArrayAsync(); Task.WaitAll(dDataTask); var doo = JObject.Parse(Encoding.UTF8.GetString(dDataTask.Result)); // within the handlebars environment replace the doo["_config"]["api"] = "http://" + HandlebarsProxyConfiguration.Instance.Hostname + ":" + HandlebarsProxyConfiguration.Instance.Port; doo["_config"]["cdn"] = HandlebarsProxyConfiguration.Instance.ContentDeliveryNetwork; doo["_request"]["protocol"] = "http"; doo["_request"]["gzip"] = false; doo["_request"]["fqdn"] = "http://" + HandlebarsProxyConfiguration.Instance.Hostname + ":" + HandlebarsProxyConfiguration.Instance.Port; doo["_request"]["hostname"] = HandlebarsProxyConfiguration.Instance.Hostname + ":" + HandlebarsProxyConfiguration.Instance.Port; try { doo.Remove("_experiment"); doo.Add("_experiment", o["_experiment"].ToString(Formatting.Indented)); } catch (Exception exp) { Console.Out.WriteLineAsync("Cached Donut Error: " + duri.ToString() + " : " + exp.Message); } var dxTemplate = donut; if (dcachedResponse.Headers.Contains("x-template")) { dxTemplate = dcachedResponse.Headers.GetValues("x-template").FirstOrDefault(); } var t = Task.FromResult(new KeyValuePair <string, string>(donut, _template.Render(dxTemplate, doo.ToString(Formatting.None)))); lock (sync) { tasks.Add(t); } } else { var t = client.SendAsync(drequest, CancellationToken.None) .ContinueWith(_ => { if (_.IsFaulted || !_.Result.IsSuccessStatusCode) { Console.Out.WriteLine("Live Donut Error: " + duri.ToString()); } else { CacheItemPolicy policy = new CacheItemPolicy(); policy.AbsoluteExpiration = DateTimeOffset.Now.AddHours(1); _store.Set(_.Result.RequestMessage.RequestUri.ToString(), _.Result, policy); } var tcontent = _.Result.Content.ReadAsStringAsync(); Task.WaitAll(tcontent); KeyValuePair <string, string> kvp; var dxTemplate = donut; if (_.Result.Headers.Contains("x-template")) { dxTemplate = _.Result.Headers.GetValues("x-template").FirstOrDefault(); } try { var doo = JObject.Parse(tcontent.Result); // within the handlebars environment replace the doo["_config"]["api"] = "http://" + HandlebarsProxyConfiguration.Instance.Hostname + ":" + HandlebarsProxyConfiguration.Instance.Port; doo["_config"]["cdn"] = HandlebarsProxyConfiguration.Instance.ContentDeliveryNetwork; doo["_request"]["protocol"] = "http"; doo["_request"]["gzip"] = false; doo["_request"]["fqdn"] = "http://" + HandlebarsProxyConfiguration.Instance.Hostname + ":" + HandlebarsProxyConfiguration.Instance.Port; doo["_request"]["hostname"] = HandlebarsProxyConfiguration.Instance.Hostname + ":" + HandlebarsProxyConfiguration.Instance.Port; try { doo.Remove("_experiment"); doo.Add("_experiment", o["_experiment"].ToString(Formatting.Indented)); } catch (Exception exp) { Console.Out.WriteLine("Live Donut Error: " + duri.ToString()); Console.WriteLine(exp.Message); Console.WriteLine(exp.StackTrace); } kvp = new KeyValuePair <string, string>(dxTemplate, _template.Render(donut, doo.ToString(Formatting.None))); } catch (JsonReaderException) { kvp = new KeyValuePair <string, string>(dxTemplate, tcontent.Result); } return(kvp); }); if (!t.IsFaulted) { lock (sync) { tasks.Add(t); } } } } try { Task.WaitAll(tasks.ToArray()); } catch (Exception exp) { Console.WriteLine("Awaiting Donuts: " + exp.Message); } } } // go through the sucessful tasks foreach (var task in tasks) { html.Replace("####donut:" + task.Result.Key + "####", task.Result.Value); } context.Response.ContentType = "text/html"; // make sure the temp partials are cleared _handlebars.Clear(); await context.Response.WriteAsync(html.ToString()); Console.WriteLine("Request Complete: " + context.Request.Uri.ToString()); return; } } catch (Exception exp) { // output any errors Console.Error.WriteLine(context.Request.Uri.ToString() + "\n" + exp.Message + "\n" + exp.StackTrace); // send them to the client as well Task.WaitAll(context.Response.WriteAsync(context.Request.Uri.ToString() + "\n" + exp.Message + "\n" + exp.StackTrace)); return; } } }
public StringBuilder FillSectionData(string contents, string json) { var sectionMatches = SectionRegex.Matches(contents); var sections = new Dictionary <string, SectionData>(); foreach (Match match in sectionMatches) { // extract the matched sections into variables var sectionData = match.Value.Split(':'); var operation = sectionData[1]; var name = sectionData[2].TrimEnd('#', '-', '>'); if (!sections.ContainsKey(name)) { sections.Add(name, new SectionData() { NameLength = name.Length }); } switch (operation) { case "start": sections[name].Start = match.Index + match.Length; break; case "stop": sections[name].Stop = match.Index; sections[name].Contents = contents.Substring(sections[name].Start, sections[name].Stop - sections[name].Start).Trim(' ', '\n', '\t', '\r'); break; } } // find the master for this template // ###master // todo: // return an HTML error describing the missing var masterMatch = MasterRegex.Match(contents, 0); if (!masterMatch.Success) { return(new StringBuilder(contents, contents.Length * 2)); } var removal = sections.Values.OrderByDescending(_ => _.Stop); foreach (SectionData sd in removal) { // <!--####section:start:####--> // <!--####section:stop:####--> int start = sd.Start - sd.NameLength - 29; int stop = sd.Stop + sd.NameLength + 28; contents = contents.Remove(start, stop - start); } // remove the master tag from the render pipeline contents = contents.Remove(masterMatch.Index, masterMatch.Length); // this logic is only needed if there is a master template with sections // any content not in a section will be automatically assumed as the // "content" section and appended to it (if it was already created) if (!sections.ContainsKey("contents")) { sections.Add("contents", new SectionData { }); sections["contents"].Contents = contents.Trim(' ', '\n', '\t', '\r'); } var masterPath = masterMatch.Value.Split(':')[1].TrimEnd('#'); string master = _template.Render(masterPath, json); // recycle variable for efficiency sectionMatches = SectionRegex.Matches(master); // foreach section in the master, // replace the section with the contents from the template // if the sections don't exist then leave them because there // might be default content var masterSections = new Dictionary <string, SectionData>(); foreach (Match match in sectionMatches) { // extract the matched sections into variables var sectionData = match.Value.Split(':'); var operation = sectionData[1]; var name = sectionData[2].TrimEnd('#', '-', '>'); if (!masterSections.ContainsKey(name)) { masterSections.Add(name, new SectionData() { NameLength = name.Length }); } switch (operation) { case "start": masterSections[name].Start = match.Index + match.Length; break; case "stop": masterSections[name].Stop = match.Index; break; } } // use a pesamistic estimate for the length of the string builder (considering we might get donuts later var sb = new StringBuilder(master, (master.Length + sections.Sum(_ => _.Value.Contents.Length)) * 2); var replacement = masterSections.OrderByDescending(_ => _.Value.Stop); foreach (KeyValuePair <string, SectionData> kvp in replacement) { if (sections.ContainsKey(kvp.Key)) { sb.Remove(masterSections[kvp.Key].Start, masterSections[kvp.Key].Stop - masterSections[kvp.Key].Start); sb.Insert(masterSections[kvp.Key].Start, "\n" + sections[kvp.Key].Contents + "\n"); } } return(sb); }