diff --git a/.gitignore b/.gitignore index d41ce12..4837c81 100644 --- a/.gitignore +++ b/.gitignore @@ -281,4 +281,6 @@ __pycache__/ # tools/** # !tools/packages.config -# End of https://www.gitignore.io/api/VisualStudio +.vscode + +# End of https://www.gitignore.io/api/VisualStudio \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index a65397e..f3a8fdf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,89 +3,94 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) -##[2.7.0] - 2018-09-16 +## [3.0.0] - 2019-01-17 ### Added - - Support for simulated_block +- Added PXHD handling +- Added cookie names extraction +- Added data enrichment cookie handling to context +- Added custom block page with redirects feature + +## [2.7.0] - 2018-09-16 +### Added +- Support for simulated_block ### Fixed - - Captcha v2 template and error handling - - Various stablity and performance fixes +- Captcha v2 template and error handling +- Various stablity and performance fixes -##[2.6.0] - 2018-08-07 +## [2.6.0] - 2018-08-07 ### Added - - Support for captcha v2 +- Support for captcha v2 -##[2.5.1] - 2018-11-06 +## [2.5.1] - 2018-11-06 ### Fixed - - Mobile token extraction in cookie validator +- Mobile token extraction in cookie validator -##[2.5.0] - 2018-14-03 +## [2.5.0] - 2018-14-03 ### Added - - Support for first party +- Support for first party -##[2.4.0] - 2018-21-02 +## [2.4.0] - 2018-21-02 ### Added - - Support enforced specific routes +- Support enforced specific routes -##[2.3.0] - 2018-05-02 +## [2.3.0] - 2018-05-02 ### Added - - Support for mobile sdk - - Support for original tokens - - Support funCaptcha in mobile - - Enforcer Telemetry +- Support for mobile sdk +- Support for original tokens +- Support funCaptcha in mobile +- Enforcer Telemetry ### Modified - - Edit block page footer - - Edit reCaptcha template to use b64 captcha - - Enrichment for async activities +- Edit block page footer +- Edit reCaptcha template to use b64 captcha +- Enrichment for async activities ### Fixed - - Handling duplicate cookies +- Handling duplicate cookies -##[2.2.0] - 2017-11-10 +## [2.2.0] - 2017-11-10 ### Fixed - - Fixed default value for sensitive_route - - Using action_block to render block pages - - Naming for s2s expired_cookie reason to cookie_expired +- Fixed default value for sensitive_route +- Using action_block to render block pages +- Naming for s2s expired_cookie reason to cookie_expired ### Added - - JS Challenge support - - FunCaptcha support - - CustomVerificationHandler support - - MonitorMode and set default to true - Please note: MonitorMode is breaking backward support - if you upgrade to this version or further - and want to keep your blocking active, please set its value to False +- JS Challenge support +- FunCaptcha support +- CustomVerificationHandler support +- MonitorMode and set default to true + Please note: MonitorMode is breaking backward support + if you upgrade to this version or further + and want to keep your blocking active, please set its value to False -##[2.1.0] - 2017-04-06 +## [2.1.0] - 2017-04-06 ### Fixed - - Renamed risk_score to block_score in activity details - - Fixed block score threshold +- Renamed risk_score to block_score in activity details +- Fixed block score threshold ## Added - - Support for sensitive routes - - Log page requested reason - - Mesure risk rout trip time +- Support for sensitive routes +- Log page requested reason +- Mesure risk rout trip time - -##[2.0.3] - 2017-15-05 +## [2.0.3] - 2017-15-05 ### Fixed - - Collect right Hostname in context - - Renamed module_version +- Collect right Hostname in context +- Renamed module_version ### Added - - Block/Page Requested Activities now sends module_verison and risk_socre - - Support Cookie v3 - - Support RiskAPI v2 +- Block/Page Requested Activities now sends module_verison and risk_socre +- Support Cookie v3 +- Support RiskAPI v2 ### Changed - - Moved PxModule verification code, request state, api calls to managable files - - New classes, Validators, DataContracts (Cookies, Activities, Requests etc...) - - Refactor module to work with PxContext - - Reordered library into folders - +- Moved PxModule verification code, request state, api calls to managable files +- New classes, Validators, DataContracts (Cookies, Activities, Requests etc...) +- Refactor module to work with PxContext +- Reordered library into folders -##[1.2.0] - 2017-24-04 +## [1.2.0] - 2017-24-04 - Support custom header for user-agent -##[1.1.1] - 2017-20-04 +## [1.1.1] - 2017-20-04 - added .axd files to whitelist files - sending px_orig_value when decryption fails -##[1.1] - 2017-28-03 +## [1.1] - 2017-28-03 - Moved server url to new URL - New design for block pages - Block page customisation diff --git a/PerimeterXModule/DataContracts/Activities/Activity.cs b/PerimeterXModule/DataContracts/Activities/Activity.cs index 249d9d6..50fd441 100644 --- a/PerimeterXModule/DataContracts/Activities/Activity.cs +++ b/PerimeterXModule/DataContracts/Activities/Activity.cs @@ -32,5 +32,8 @@ public class Activity [DataMember(Name = "http_method", EmitDefaultValue = false)] public string HttpMethod; + + [DataMember(Name = "pxhd", EmitDefaultValue = false)] + public string Pxhd; } } diff --git a/PerimeterXModule/DataContracts/Activities/ActivityDetails.cs b/PerimeterXModule/DataContracts/Activities/ActivityDetails.cs index a994034..426e049 100644 --- a/PerimeterXModule/DataContracts/Activities/ActivityDetails.cs +++ b/PerimeterXModule/DataContracts/Activities/ActivityDetails.cs @@ -31,7 +31,11 @@ public class ActivityDetails : IActivityDetails [DataMember(Name = "risk_rtt")] public long RiskRoundtripTime; - } + + [DataMember(Name = "block_action")] + public string BlockAction; + + } [DataContract] public class EnforcerTelemetryActivityDetails : IActivityDetails @@ -51,4 +55,4 @@ public class EnforcerTelemetryActivityDetails : IActivityDetails [DataMember(Name = "enforcer_configs")] public string EnforcerConfigs; } -} \ No newline at end of file +} diff --git a/PerimeterXModule/DataContracts/Requests/Additional.cs b/PerimeterXModule/DataContracts/Requests/Additional.cs index 99859a9..2232cf7 100644 --- a/PerimeterXModule/DataContracts/Requests/Additional.cs +++ b/PerimeterXModule/DataContracts/Requests/Additional.cs @@ -44,5 +44,10 @@ public class Additional [DataMember(Name = "simulated_block")] public object SimulatedBlock; + [DataMember(Name = "request_cookie_names")] + public string[] RequestCookieNames; + + [DataMember(Name = "enforcer_vid_source", EmitDefaultValue = false)] + public string VidSource; } } diff --git a/PerimeterXModule/DataContracts/Requests/RiskRequest.cs b/PerimeterXModule/DataContracts/Requests/RiskRequest.cs index 3946a4c..299c7aa 100644 --- a/PerimeterXModule/DataContracts/Requests/RiskRequest.cs +++ b/PerimeterXModule/DataContracts/Requests/RiskRequest.cs @@ -19,5 +19,8 @@ public class RiskRequest [DataMember(Name = "additional", EmitDefaultValue = false)] public Additional Additional; + + [DataMember(Name = "pxhd", EmitDefaultValue = false)] + public string Pxhd; } } diff --git a/PerimeterXModule/DataContracts/Responses/RiskResponse.cs b/PerimeterXModule/DataContracts/Responses/RiskResponse.cs index 0cc7cb2..84697d7 100644 --- a/PerimeterXModule/DataContracts/Responses/RiskResponse.cs +++ b/PerimeterXModule/DataContracts/Responses/RiskResponse.cs @@ -25,6 +25,13 @@ public class RiskResponse [DataMember(Name = "error_msg")] public string ErrorMessage; + + [DataMember(Name = "data_enrichment")] + public object DataEnrichment; + + [DataMember(Name = "pxhd")] + public string Pxhd; + } diff --git a/PerimeterXModule/Internals/Cookies/DataEnrichmentCookie.cs b/PerimeterXModule/Internals/Cookies/DataEnrichmentCookie.cs new file mode 100644 index 0000000..ae324c8 --- /dev/null +++ b/PerimeterXModule/Internals/Cookies/DataEnrichmentCookie.cs @@ -0,0 +1,19 @@ +using System.Text; + +namespace PerimeterX.DataContracts.Cookies +{ + public sealed class DataEnrichmentCookie + { + private bool isValid = false; + private dynamic jsonPayload; + + public bool IsValid { set { isValid = value; } get { return isValid; } } + public dynamic JsonPayload { set { jsonPayload = value; } get { return jsonPayload; } } + + public DataEnrichmentCookie(dynamic jsonPayload, bool isValid) + { + this.jsonPayload = jsonPayload; + this.isValid = isValid; + } + } +} diff --git a/PerimeterXModule/Internals/Cookies/PxCookieUtils.cs b/PerimeterXModule/Internals/Cookies/PxCookieUtils.cs index 6aeeff5..8a35402 100644 --- a/PerimeterXModule/Internals/Cookies/PxCookieUtils.cs +++ b/PerimeterXModule/Internals/Cookies/PxCookieUtils.cs @@ -7,61 +7,90 @@ namespace PerimeterX { - public static class PxCookieUtils - { - public static IPxCookie BuildCookie(PxModuleConfigurationSection config, Dictionary cookies, ICookieDecoder cookieDecoder) - { - if (cookies.Count == 0) + public static class PxCookieUtils + { + public static IPxCookie BuildCookie(PxModuleConfigurationSection config, Dictionary cookies, ICookieDecoder cookieDecoder) + { + if (cookies.ContainsKey(PxConstants.COOKIE_V1_PREFIX)) + { + return new PxCookieV1(cookieDecoder, cookies[PxConstants.COOKIE_V1_PREFIX]); + } + else if(cookies.ContainsKey(PxConstants.COOKIE_V3_PREFIX)) { - return null; + return new PxCookieV3(cookieDecoder, cookies[PxConstants.COOKIE_V3_PREFIX]); } - if (cookies.ContainsKey(PxConstants.COOKIE_V1_PREFIX)) - { - return new PxCookieV1(cookieDecoder, cookies[PxConstants.COOKIE_V1_PREFIX]); - } - - return new PxCookieV3(cookieDecoder, cookies[PxConstants.COOKIE_V3_PREFIX]); + return null; } - public static T Deserialize(ICookieDecoder cookieDecoder, string rawCookie) - { - string cookieString = cookieDecoder.Decode(rawCookie); - if (string.IsNullOrEmpty(cookieString)) - { - return default(T); - } + public static T Deserialize(ICookieDecoder cookieDecoder, string rawCookie) + { + string cookieString = cookieDecoder.Decode(rawCookie); + if (string.IsNullOrEmpty(cookieString)) + { + return default(T); + } - return JSON.Deserialize(cookieString, PxConstants.JSON_OPTIONS); - } + return JSON.Deserialize(cookieString, PxConstants.JSON_OPTIONS); + } - public static bool IsExpired(double date) - { - double now = DateTime.UtcNow - .Subtract(new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)) - .TotalMilliseconds; - return date < now; - } + public static bool IsExpired(double date) + { + double now = DateTime.UtcNow + .Subtract(new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)) + .TotalMilliseconds; + return date < now; + } - public static bool IsHMACValid(string cookieKey, string UncodedHmac, string CookieHmac) - { - var cookieKeyBytes = Encoding.UTF8.GetBytes(cookieKey); - var hash = new HMACSHA256(cookieKeyBytes); - var expectedHashBytes = hash.ComputeHash(Encoding.UTF8.GetBytes(UncodedHmac)); - var encodedHmac = ByteArrayToHexString(expectedHashBytes); - return encodedHmac == CookieHmac; - } + public static bool IsHMACValid(string cookieKey, string UncodedHmac, string CookieHmac) + { + var cookieKeyBytes = Encoding.UTF8.GetBytes(cookieKey); + var hash = new HMACSHA256(cookieKeyBytes); + var expectedHashBytes = hash.ComputeHash(Encoding.UTF8.GetBytes(UncodedHmac)); + var encodedHmac = ByteArrayToHexString(expectedHashBytes); + return encodedHmac == CookieHmac; + } - public static string ByteArrayToHexString(byte[] input) - { - StringBuilder sb = new StringBuilder(input.Length * 2); - foreach (byte b in input) - { - sb.Append(PxConstants.HEX_ALPHABET[b >> 4]); - sb.Append(PxConstants.HEX_ALPHABET[b & 0xF]); - } - return sb.ToString(); - } - } + public static string ByteArrayToHexString(byte[] input) + { + StringBuilder sb = new StringBuilder(input.Length * 2); + foreach (byte b in input) + { + sb.Append(PxConstants.HEX_ALPHABET[b >> 4]); + sb.Append(PxConstants.HEX_ALPHABET[b & 0xF]); + } + return sb.ToString(); + } + + public static DataEnrichmentCookie GetDataEnrichmentCookie(Dictionary PxCookies, string cookieKey) + { + DataEnrichmentCookie dataEnrichment = new DataEnrichmentCookie(JSON.DeserializeDynamic("{}"), false); + if (PxCookies.ContainsKey(PxConstants.COOKIE_DATA_ENRICHMENT_PREFIX)) + { + string rawCookie = PxCookies[PxConstants.COOKIE_DATA_ENRICHMENT_PREFIX]; + string[] splitRawCookie = rawCookie.Split(new char[] { ':' }, 2); + if (splitRawCookie.Length != 2) + { + return dataEnrichment; + } + + string hmac = splitRawCookie[0]; + string encodedPayload = splitRawCookie[1]; + bool isValid = IsHMACValid(cookieKey, encodedPayload, hmac); + dataEnrichment.IsValid = isValid; + byte[] bytes = Convert.FromBase64String(encodedPayload); + string decodedPayload = Encoding.UTF8.GetString(bytes); + try + { + dataEnrichment.JsonPayload = JSON.DeserializeDynamic(decodedPayload); + } + catch (Exception err) + { + PxLoggingUtils.LogDebug(string.Format("Failed deserializing pxde into json")); + } + } + return dataEnrichment; + } + } } diff --git a/PerimeterXModule/Internals/Helpers/HttpHandler.cs b/PerimeterXModule/Internals/Helpers/HttpHandler.cs index d1bfe1a..0ba60bd 100644 --- a/PerimeterXModule/Internals/Helpers/HttpHandler.cs +++ b/PerimeterXModule/Internals/Helpers/HttpHandler.cs @@ -38,7 +38,7 @@ public string Post(string requestJson, string uri) } } - public void Dispose() + public void Dispose() { this.httpClient.Dispose(); this.httpClient = null; diff --git a/PerimeterXModule/Internals/Helpers/PxConstants.cs b/PerimeterXModule/Internals/Helpers/PxConstants.cs index da34f85..e6ea3d0 100644 --- a/PerimeterXModule/Internals/Helpers/PxConstants.cs +++ b/PerimeterXModule/Internals/Helpers/PxConstants.cs @@ -10,10 +10,15 @@ namespace PerimeterX public static class PxConstants { public static readonly string HEX_ALPHABET = "0123456789abcdef"; - public static readonly string[] PX_COOKIES_PREFIX = { COOKIE_V1_PREFIX, COOKIE_V3_PREFIX }; + public static readonly string[] PX_COOKIES_PREFIX = { COOKIE_V1_PREFIX, COOKIE_V3_PREFIX, COOKIE_DATA_ENRICHMENT_PREFIX, COOKIE_PXHD_PREFIX, COOKIE_VID_PREFIX, "_" + COOKIE_VID_PREFIX }; public static readonly string[] PX_TOKEN_PREFIX = { TOKEN_V1_PREFIX, TOKEN_V3_PREFIX }; public const string COOKIE_V1_PREFIX = "_px"; public const string COOKIE_V3_PREFIX = "_px3"; + public const string COOKIE_PXHD_PREFIX = "_pxhd"; + public const string COOKIE_VID_PREFIX = "pxvid"; + public const string VID_COOKIE = "vid_cookie"; + public const string RISK_COOKIE = "risk_cookie"; + public const string COOKIE_DATA_ENRICHMENT_PREFIX = "_pxde"; public const string TOKEN_V1_PREFIX = "1"; public const string TOKEN_V3_PREFIX = "3"; public static readonly string PX_VALIDATED_HEADER = "X-PX-VALIDATED"; @@ -28,9 +33,10 @@ public static class PxConstants public static readonly string ENFORCER_TRUE_IP_HEADER = "x-px-enforcer-true-ip"; public static readonly string FIRST_PARTY_HEADER = "X-PX-FIRST-PARTY"; public static readonly string FIRST_PARTY_VALUE = "1"; + public static readonly string COOKIE_HEADER = "cookie"; // Endpoints - public const string RISK_API_V2 = "/api/v2/risk"; + public const string RISK_API_PATH = "/api/v3/risk"; public const string ACTIVITIES_API_PATH = "/api/v1/collector/s2s"; public const string ENFORCER_TELEMETRY_API_PATH = "/api/v2/risk/telemetry"; diff --git a/PerimeterXModule/Internals/PxBlock.cs b/PerimeterXModule/Internals/PxBlock.cs new file mode 100644 index 0000000..aa1ca96 --- /dev/null +++ b/PerimeterXModule/Internals/PxBlock.cs @@ -0,0 +1,158 @@ +using Jil; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; + +namespace PerimeterX.Internals +{ + class PxBlock + { + private HttpClient httpClient; + + public PxBlock(PxModuleConfigurationSection config) + { + if (config.CustomBlockUrl != null) + { + this.httpClient = PxConstants.CreateHttpClient(false, config.ApiTimeout, false, config); + } + } + + public bool IsJsonResponse(PxContext pxContext) + { + Dictionary headers = pxContext.GetHeadersAsDictionary(); + string jsonHeader; + bool jsonHeaderExists = headers.TryGetValue("accept", out jsonHeader) || headers.TryGetValue("content-type", out jsonHeader); + if (jsonHeaderExists) + { + string[] values = jsonHeader.Split(','); + if (Array.Exists(values, element => element == "application/json")) + { + return true; + } + } + + return false; + } + + public string injectCaptchaScript(string vid, string uuid) + { + return ""; + } + + public void ResponseBlockPage(PxContext pxContext, PxModuleConfigurationSection config) + { + string template = "block_template"; + + if (pxContext.BlockAction == "r") + { + template = "ratelimit"; + } + + // In the case of a challenge, the challenge response is taken directly from BlockData. Otherwise, generate html template. + string content = pxContext.BlockAction == "j" && !string.IsNullOrEmpty(pxContext.BlockData) ? pxContext.BlockData : + TemplateFactory.getTemplate(template, config, pxContext.UUID, pxContext.Vid, pxContext.IsMobileRequest, pxContext.BlockAction); + + pxContext.ApplicationContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; + + if (pxContext.IsMobileRequest) + { + pxContext.ApplicationContext.Response.ContentType = "application/json"; + using (var output = new StringWriter()) + { + JSON.Serialize( + new MobileResponse() + { + AppId = config.AppId, + Uuid = pxContext.UUID, + Action = pxContext.MapBlockAction(), + Vid = pxContext.Vid, + Page = Convert.ToBase64String(Encoding.UTF8.GetBytes(content)), + CollectorUrl = string.Format(config.CollectorUrl, config.AppId) + }, output); + content = output.ToString(); + } + + pxContext.ApplicationContext.Response.Write(content); + return; + } + + // json response + if (IsJsonResponse(pxContext)) + { + pxContext.ApplicationContext.Response.ContentType = "application/json"; + using (var output = new StringWriter()) + { + var props = TemplateFactory.getProps(config, pxContext.UUID, pxContext.Vid, pxContext.IsMobileRequest, pxContext.BlockAction); + JSON.Serialize( + new JsonResponse() + { + AppId = config.AppId, + Uuid = pxContext.UUID, + Vid = pxContext.Vid, + JsClientSrc = props["jsClientSrc"], + HostUrl = props["hostUrl"], + BlockScript = props["blockScript"] + }, output); + content = output.ToString(); + } + + pxContext.ApplicationContext.Response.Write(content); + return; + } + + if (pxContext.BlockAction != "c" && pxContext.BlockAction != "b") + { + if (pxContext.BlockAction == "r") + { + pxContext.ApplicationContext.Response.StatusCode = 429; // HTTP/1.1 429 TooManyRequests + } + + pxContext.ApplicationContext.Response.Write(content); + return; + } + + if (pxContext.CustomBlockUrl != "") + { + if (pxContext.RedirectOnCustomUrl) + { + string uri = pxContext.ApplicationContext.Request.Url.AbsoluteUri; + string encodedUri = Convert.ToBase64String(Encoding.UTF8.GetBytes(uri)); + string redirectUrl = string.Format("{0}?url={1}&uuid={2}&vid={3}", pxContext.CustomBlockUrl, encodedUri, pxContext.UUID, pxContext.Vid); + PxLoggingUtils.LogDebug("Redirecting to custom block page: " + redirectUrl); + pxContext.ApplicationContext.Response.Redirect(redirectUrl); + return; + } + + HttpResponseMessage response = httpClient.GetAsync(pxContext.CustomBlockUrl).Result; + if ((int)response.StatusCode >= 300) + { + pxContext.ApplicationContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError; + pxContext.ApplicationContext.Response.Write("Unable to fetch custom block url. Status: " + response.StatusCode.ToString()); + return; + } + + content = response.Content.ReadAsStringAsync().Result; + if (pxContext.BlockAction == "c") + { + PxLoggingUtils.LogDebug("Injecting captcha to page"); + StringBuilder builder = new StringBuilder(content); + builder.Replace("", injectCaptchaScript(pxContext.Vid, pxContext.UUID) + ""); + builder.Replace("::BLOCK_REF::", pxContext.UUID); + content = builder.ToString(); + } + + pxContext.ApplicationContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; + pxContext.ApplicationContext.Response.Write(content); + return; + } + + PxLoggingUtils.LogDebug("Enforcing action: " + pxContext.MapBlockAction() + " page is served"); + pxContext.ApplicationContext.Response.Write(content); + } + } +} diff --git a/PerimeterXModule/Internals/PxContext.cs b/PerimeterXModule/Internals/PxContext.cs index 4e8ed14..cf2b0da 100644 --- a/PerimeterXModule/Internals/PxContext.cs +++ b/PerimeterXModule/Internals/PxContext.cs @@ -1,9 +1,9 @@ using System.Collections.Generic; using System.Web; using System; -using System.Net; using System.Collections.Specialized; using System.Linq; +using PerimeterX.DataContracts.Cookies; namespace PerimeterX { @@ -43,8 +43,16 @@ public class PxContext public object DecodedOriginalToken { get; set; } public bool IsMobileRequest { get; set; } public string MobileHeader { get; set; } + public string[] CookieNames; + public bool IsPxdeVerified { get; set; } + public dynamic Pxde { get; set; } + public string CustomBlockUrl { get; set; } + public bool RedirectOnCustomUrl { get; set; } + public string VidSource { get; set; } + public string Pxhd { get; set; } + public bool MonitorRequest { get; set; } - public PxContext(HttpContext context, PxModuleConfigurationSection pxConfiguration) + public PxContext(HttpContext context, PxModuleConfigurationSection pxConfiguration) { ApplicationContext = context; @@ -57,6 +65,7 @@ public PxContext(HttpContext context, PxModuleConfigurationSection pxConfigurati // Get Headers // if userAgentOverride is present override the default user-agent + CookieNames = extractCookieNames(context.Request.Headers[PxConstants.COOKIE_HEADER]); string userAgentOverride = pxConfiguration.UserAgentOverride; if (!string.IsNullOrEmpty(userAgentOverride)) { @@ -142,8 +151,25 @@ public PxContext(HttpContext context, PxModuleConfigurationSection pxConfigurati PxCookies[key] = contextCookie.Get(key).Value; } } - } + DataEnrichmentCookie deCookie = PxCookieUtils.GetDataEnrichmentCookie(PxCookies, pxConfiguration.CookieKey); + IsPxdeVerified = deCookie.IsValid; + Pxde = deCookie.JsonPayload; + if (PxCookies.ContainsKey(PxConstants.COOKIE_VID_PREFIX)) + { + Vid = PxCookies[PxConstants.COOKIE_VID_PREFIX]; + VidSource = PxConstants.VID_COOKIE; + } + if (PxCookies.ContainsKey("_" + PxConstants.COOKIE_VID_PREFIX)) + { + Vid = PxCookies["_" + PxConstants.COOKIE_VID_PREFIX]; + VidSource = PxConstants.VID_COOKIE; + } + if (PxCookies.ContainsKey(PxConstants.COOKIE_PXHD_PREFIX)) + { + Pxhd = PxCookies[PxConstants.COOKIE_PXHD_PREFIX]; + } + } Hostname = context.Request.Url.Host; @@ -160,6 +186,49 @@ public PxContext(HttpContext context, PxModuleConfigurationSection pxConfigurati HttpMethod = context.Request.HttpMethod; SensitiveRoute = CheckSensitiveRoute(pxConfiguration.SensitiveRoutes, Uri); + + CustomBlockUrl = pxConfiguration.CustomBlockUrl; + RedirectOnCustomUrl = pxConfiguration.RedirectOnCustomUrl; + + MonitorRequest = shouldMonitorRequest(context.Request.Url.AbsolutePath, pxConfiguration); + } + + private bool shouldMonitorRequest(String uri, PxModuleConfigurationSection pxConfiguration) + { + if (uri.IndexOf("/", StringComparison.Ordinal) == 0) + { + uri = uri.Substring(1); + } + + var mitigationUrls = pxConfiguration.MitigationUrls; + if (mitigationUrls.Count > 0) + { + if (mitigationUrls.Contains(uri)) + { + return false; + } + return true; + } + else + { + return pxConfiguration.MonitorMode; + } + + } + + private string[] extractCookieNames(string cookieHeader) + { + string[] cookieNames = null; + if (cookieHeader != null) + { + var cookies = cookieHeader.Split(';'); + cookieNames = new string[cookies.Length]; + for (int i = 0; i < cookies.Length; i++) + { + cookieNames[i] = cookies[i].Split('=')[0].Trim(); + } + } + return cookieNames; } private bool CheckSensitiveRoute(StringCollection sensitiveRoutes, string uri) diff --git a/PerimeterXModule/Internals/TemplateFactory.cs b/PerimeterXModule/Internals/TemplateFactory.cs index 56ae608..bc25d9a 100644 --- a/PerimeterXModule/Internals/TemplateFactory.cs +++ b/PerimeterXModule/Internals/TemplateFactory.cs @@ -8,70 +8,68 @@ namespace PerimeterX { - abstract class TemplateFactory - { - private static readonly string CLIENT_SRC_FP = "/{0}/init.js"; - private static readonly string CLIENT_SRC_TP = "{0}/{1}/main.min.js"; - private static readonly string CAPTCHA_QUERY_PARAMS = "?a={0}&u={1}&v={2}&m={3}"; - private static readonly string CAPTCHA_SRC_FP = "/{0}/captcha/captcha.js{1}"; - private static readonly string CAPTCHA_SRC_TP = "{0}/{1}/captcha.js{2}"; - private static readonly string HOST_FP = "/{0}/xhr"; + abstract class TemplateFactory + { + private static readonly string CLIENT_SRC_FP = "/{0}/init.js"; + private static readonly string CLIENT_SRC_TP = "{0}/{1}/main.min.js"; + private static readonly string CAPTCHA_QUERY_PARAMS = "?a={0}&u={1}&v={2}&m={3}"; + private static readonly string CAPTCHA_SRC_FP = "/{0}/captcha/captcha.js{1}"; + private static readonly string CAPTCHA_SRC_TP = "{0}/{1}/captcha.js{2}"; + private static readonly string HOST_FP = "/{0}/xhr"; + public static string getTemplate(string template, PxModuleConfigurationSection pxConfiguration, string uuid, string vid, bool isMobileRequest, string action) + { + PxLoggingUtils.LogDebug(string.Format("Using {0} template", template)); + string templateStr = getTemplateString(template); + return Render.StringToString(templateStr, getProps(pxConfiguration, uuid, vid, isMobileRequest, action)); + } - public static string getTemplate(string template, PxModuleConfigurationSection pxConfiguration, string uuid, string vid, bool isMobileRequest,string action) - { - PxLoggingUtils.LogDebug(string.Format("Using {0} template", template)); - string templateStr = getTemplateString(template); - return Render.StringToString(templateStr, getProps(pxConfiguration, uuid, vid, isMobileRequest, action)); + private static string getTemplateString(string template) + { + string templateStr = ""; + Assembly _assembly = Assembly.GetExecutingAssembly(); + StreamReader _textStream = new StreamReader(_assembly.GetManifestResourceStream(string.Format("PerimeterX.Internals.Templates.{0}.mustache", template))); - } + while (_textStream.Peek() != -1) + { + templateStr = string.Concat(templateStr, _textStream.ReadLine()); + } + if (string.IsNullOrEmpty(templateStr)) + { + throw new Exception(string.Format("Unable to read template {0} from asm", template)); + } - private static string getTemplateString(string template) - { - string templateStr = ""; - Assembly _assembly = Assembly.GetExecutingAssembly(); - StreamReader _textStream = new StreamReader(_assembly.GetManifestResourceStream(string.Format("PerimeterX.Internals.Templates.{0}.mustache", template))); + return templateStr; + } - while (_textStream.Peek() != -1) - { - templateStr = string.Concat(templateStr, _textStream.ReadLine()); - } - if (string.IsNullOrEmpty(templateStr)) - { - throw new Exception(string.Format("Unable to read template {0} from asm", template)); - } + public static IDictionary getProps(PxModuleConfigurationSection pxConfiguration, string uuid, string vid, bool isMobileRequest, string action) + { + IDictionary props = new Dictionary(); + string captchaParams = string.Format(CAPTCHA_QUERY_PARAMS, action, uuid, vid, isMobileRequest ? "1" : "0"); + props.Add("refId", uuid); + props.Add("appId", pxConfiguration.AppId); + props.Add("vid", vid); + props.Add("uuid", uuid); + props.Add("customLogo", pxConfiguration.CustomLogo); + props.Add("cssRef", pxConfiguration.CssRef); + props.Add("jsRef", pxConfiguration.JsRef); + props.Add("logoVisibility", string.IsNullOrEmpty(pxConfiguration.CustomLogo) ? "hidden" : "visible"); - return templateStr; - } - - private static IDictionary getProps(PxModuleConfigurationSection pxConfiguration, string uuid, string vid, bool isMobileRequest, string action) - { - IDictionary props = new Dictionary(); - string captchaParams = string.Format(CAPTCHA_QUERY_PARAMS, action, uuid, vid, isMobileRequest ? "1" : "0"); - props.Add("refId", uuid); - props.Add("appId", pxConfiguration.AppId); - props.Add("vid", vid); - props.Add("uuid", uuid); - props.Add("customLogo", pxConfiguration.CustomLogo); - props.Add("cssRef", pxConfiguration.CssRef); - props.Add("jsRef", pxConfiguration.JsRef); - props.Add("logoVisibility", string.IsNullOrEmpty(pxConfiguration.CustomLogo) ? "hidden" : "visible"); - - if (pxConfiguration.FirstPartyEnabled && !isMobileRequest) - { - props.Add("jsClientSrc", string.Format(CLIENT_SRC_FP, pxConfiguration.AppId.Substring(2))); - props.Add("blockScript", string.Format(CAPTCHA_SRC_FP, pxConfiguration.AppId.Substring(2), captchaParams)); - props.Add("hostUrl", string.Format(HOST_FP, pxConfiguration.AppId.Substring(2))); - props.Add("firstPartyEnabled", "true"); - } - else - { - props.Add("jsClientSrc", string.Format(CLIENT_SRC_TP, Regex.Replace(pxConfiguration.ClientHostUrl, "https?:", ""), pxConfiguration.AppId)); - props.Add("hostUrl", string.Format(pxConfiguration.CollectorUrl, pxConfiguration.AppId)); - props.Add("blockScript", string.Format(CAPTCHA_SRC_TP, pxConfiguration.CaptchaHostUrl, pxConfiguration.AppId, captchaParams)); - props.Add("firstPartyEnabled", "false"); - } - return props; - } - } + if (pxConfiguration.FirstPartyEnabled && !isMobileRequest) + { + props.Add("jsClientSrc", string.Format(CLIENT_SRC_FP, pxConfiguration.AppId.Substring(2))); + props.Add("blockScript", string.Format(CAPTCHA_SRC_FP, pxConfiguration.AppId.Substring(2), captchaParams)); + props.Add("hostUrl", string.Format(HOST_FP, pxConfiguration.AppId.Substring(2))); + props.Add("firstPartyEnabled", "true"); + } + else + { + props.Add("jsClientSrc", string.Format(CLIENT_SRC_TP, Regex.Replace(pxConfiguration.ClientHostUrl, "https?:", ""), pxConfiguration.AppId)); + props.Add("hostUrl", string.Format(pxConfiguration.CollectorUrl, pxConfiguration.AppId)); + props.Add("blockScript", string.Format(CAPTCHA_SRC_TP, pxConfiguration.CaptchaHostUrl, pxConfiguration.AppId, captchaParams)); + props.Add("firstPartyEnabled", "false"); + } + return props; + } + } } diff --git a/PerimeterXModule/Internals/Templates/IJsonResponse.cs b/PerimeterXModule/Internals/Templates/IJsonResponse.cs new file mode 100644 index 0000000..f68565e --- /dev/null +++ b/PerimeterXModule/Internals/Templates/IJsonResponse.cs @@ -0,0 +1,6 @@ +namespace PerimeterX +{ + interface IJsonResponse + { + } +} diff --git a/PerimeterXModule/Internals/Templates/JsonResponse.cs b/PerimeterXModule/Internals/Templates/JsonResponse.cs new file mode 100644 index 0000000..f42698f --- /dev/null +++ b/PerimeterXModule/Internals/Templates/JsonResponse.cs @@ -0,0 +1,21 @@ +using System.Runtime.Serialization; + +namespace PerimeterX +{ + [DataContract] + class JsonResponse : IJsonResponse + { + [DataMember(Name = "appId")] + public string AppId; + [DataMember(Name = "jsClientSrc")] + public string JsClientSrc; + [DataMember(Name = "uuid")] + public string Uuid; + [DataMember(Name = "vid")] + public string Vid; + [DataMember(Name = "hostUrl")] + public string HostUrl; + [DataMember(Name = "blockScript")] + public string BlockScript; + } +} diff --git a/PerimeterXModule/Internals/Templates/block_template.mustache b/PerimeterXModule/Internals/Templates/block_template.mustache index 900df7c..cedd184 100644 --- a/PerimeterXModule/Internals/Templates/block_template.mustache +++ b/PerimeterXModule/Internals/Templates/block_template.mustache @@ -151,8 +151,21 @@ window._pxUuid = '{{uuid}}'; window._pxHostUrl = '{{{hostUrl}}}'; - - + {{#jsRef}} diff --git a/PerimeterXModule/Internals/Templates/ratelimit.mustache b/PerimeterXModule/Internals/Templates/ratelimit.mustache new file mode 100644 index 0000000..36fd393 --- /dev/null +++ b/PerimeterXModule/Internals/Templates/ratelimit.mustache @@ -0,0 +1,9 @@ + + + Too Many Requests + + +

Too Many Requests

+

Reached maximum requests limitation, try again soon.

+ + \ No newline at end of file diff --git a/PerimeterXModule/Internals/Validators/PXCookieValidator.cs b/PerimeterXModule/Internals/Validators/PXCookieValidator.cs index 71b77f2..b0a095e 100644 --- a/PerimeterXModule/Internals/Validators/PXCookieValidator.cs +++ b/PerimeterXModule/Internals/Validators/PXCookieValidator.cs @@ -71,6 +71,7 @@ public virtual bool Verify(PxContext context, IPxCookie pxCookie) context.Score = pxCookie.Score; context.UUID = pxCookie.Uuid; context.Vid = pxCookie.Vid; + context.VidSource = PxConstants.RISK_COOKIE; context.BlockAction = pxCookie.BlockAction; context.PxCookieHmac = pxCookie.Hmac; diff --git a/PerimeterXModule/Internals/Validators/PXS2SValidator.cs b/PerimeterXModule/Internals/Validators/PXS2SValidator.cs index 013a3c0..9ab339a 100644 --- a/PerimeterXModule/Internals/Validators/PXS2SValidator.cs +++ b/PerimeterXModule/Internals/Validators/PXS2SValidator.cs @@ -2,6 +2,7 @@ using System; using System.Diagnostics; using System.Threading.Tasks; +using PerimeterX.DataContracts.Cookies; namespace PerimeterX { @@ -25,13 +26,13 @@ public bool VerifyS2S(PxContext PxContext) { RiskResponse riskResponse = SendRiskResponse(PxContext); PxContext.MadeS2SCallReason = true; - if (riskResponse.Score >= 0 && !string.IsNullOrEmpty(riskResponse.RiskResponseAction)) { int score = riskResponse.Score; PxContext.Score = score; PxContext.UUID = riskResponse.Uuid; PxContext.BlockAction = riskResponse.RiskResponseAction; + PxContext.Pxhd = riskResponse.Pxhd; if (PxContext.BlockAction == PxConstants.JS_CHALLENGE_ACTION && !string.IsNullOrEmpty(riskResponse.RiskResponseActionData.Body)) @@ -54,6 +55,16 @@ public bool VerifyS2S(PxContext PxContext) PxContext.S2SHttpErrorMessage = riskResponse.ErrorMessage; retVal = false; } + + DataEnrichmentCookie deCookie = new DataEnrichmentCookie(JSON.DeserializeDynamic("{}"), true); + if (riskResponse.DataEnrichment != null) + { + string dataEnrichmentString = riskResponse.DataEnrichment.ToString(); + var dataEnrichmentPayload = JSON.DeserializeDynamic(dataEnrichmentString); + deCookie = new DataEnrichmentCookie(dataEnrichmentPayload, true); + } + PxContext.IsPxdeVerified = deCookie.IsValid; + PxContext.Pxde = deCookie.JsonPayload; } catch (Exception ex) { @@ -72,29 +83,36 @@ public bool VerifyS2S(PxContext PxContext) public RiskResponse SendRiskResponse(PxContext PxContext) { - var riskMode = ModuleMode.BLOCK_MODE; - if (PxConfig.MonitorMode == true) + if (PxConfig.MonitorMode) { riskMode = ModuleMode.MONITOR_MODE; } - + string vid = PxContext.Vid; + string pxhd = PxContext.Pxhd; + string callReason = PxContext.S2SCallReason; + if (PxContext.Pxhd != null && PxContext.S2SCallReason == "no_cookie") + { + callReason = "no_cookie_w_vid"; + } RiskRequest riskRequest = new RiskRequest { - Vid = PxContext.Vid, + Vid = vid, + Pxhd = pxhd, Request = Request.CreateRequestFromContext(PxContext), Additional = new Additional { - CallReason = PxContext.S2SCallReason, + CallReason = callReason, ModuleVersion = PxConstants.MODULE_VERSION, HttpMethod = PxContext.HttpMethod, HttpVersion = PxContext.HttpVersion, RiskMode = riskMode, PxCookieHMAC = PxContext.PxCookieHmac, - CookieOrigin = PxContext.CookieOrigin + CookieOrigin = PxContext.CookieOrigin, + RequestCookieNames = PxContext.CookieNames, + VidSource = PxContext.VidSource }, FirstParty = PxConfig.FirstPartyEnabled - }; if (!string.IsNullOrEmpty(PxContext.Vid)) @@ -133,9 +151,8 @@ public RiskResponse SendRiskResponse(PxContext PxContext) riskRequest.Additional.SimulatedBlock = PxConfig.MonitorMode; - string requestJson = JSON.SerializeDynamic(riskRequest, PxConstants.JSON_OPTIONS); - var responseJson = httpHandler.Post(requestJson, PxConstants.RISK_API_V2); + var responseJson = httpHandler.Post(requestJson, PxConstants.RISK_API_PATH); return JSON.Deserialize(responseJson, PxConstants.JSON_OPTIONS); } } diff --git a/PerimeterXModule/PerimeterXModule.csproj b/PerimeterXModule/PerimeterXModule.csproj index 9f13d2d..00401f2 100644 --- a/PerimeterXModule/PerimeterXModule.csproj +++ b/PerimeterXModule/PerimeterXModule.csproj @@ -63,11 +63,13 @@ + + @@ -79,7 +81,9 @@ + + diff --git a/PerimeterXModule/Properties/AssemblyInfo.cs b/PerimeterXModule/Properties/AssemblyInfo.cs index 5f2e0e1..3782a4e 100644 --- a/PerimeterXModule/Properties/AssemblyInfo.cs +++ b/PerimeterXModule/Properties/AssemblyInfo.cs @@ -23,5 +23,5 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("2.7.1")] -[assembly: AssemblyFileVersion("2.7.1")] +[assembly: AssemblyVersion("3.0.0")] +[assembly: AssemblyFileVersion("3.0.0")] diff --git a/PerimeterXModule/PxModule.cs b/PerimeterXModule/PxModule.cs index 3b586a5..4b30a05 100644 --- a/PerimeterXModule/PxModule.cs +++ b/PerimeterXModule/PxModule.cs @@ -36,6 +36,8 @@ using System.Reflection; using System.Collections; using Jil; +using System.Collections.Generic; +using PerimeterX.Internals; namespace PerimeterX { @@ -49,6 +51,7 @@ public class PxModule : IHttpModule private readonly IPXCookieValidator PxCookieValidator; private readonly IPXS2SValidator PxS2SValidator; private readonly IReverseProxy ReverseProxy; + private readonly PxBlock pxBlock; private readonly bool enabled; private readonly bool sendPageActivites; @@ -145,6 +148,8 @@ public PxModule() // Build reverse proxy ReverseProxy = new ReverseProxy(config); + pxBlock = new PxBlock(config); + PxLoggingUtils.LogDebug(ModuleName + " initialized"); } @@ -247,6 +252,7 @@ private void PostBlockActivity(PxContext pxContext) { PostActivity(pxContext, "block", new ActivityDetails { + BlockAction = pxContext.BlockAction, BlockReason = pxContext.BlockReason, BlockUuid = pxContext.UUID, ModuleVersion = PxConstants.MODULE_VERSION, @@ -308,7 +314,7 @@ private void PostActivity(PxContext pxContext, string eventType, ActivityDetails SocketIP = pxContext.Ip, Url = pxContext.FullUrl, Details = details, - Headers = pxContext.GetHeadersAsDictionary() + Headers = pxContext.GetHeadersAsDictionary(), }; if (eventType.Equals("page_requested")) { @@ -320,10 +326,15 @@ private void PostActivity(PxContext pxContext, string eventType, ActivityDetails activity.Vid = pxContext.Vid; } + if (!string.IsNullOrEmpty(pxContext.Pxhd) && (eventType == "page_requested" || eventType == "block")) + { + activity.Pxhd = pxContext.Pxhd; + } + reporter.Post(activity); } - public static void BlockRequest(PxContext pxContext, PxModuleConfigurationSection config) + public void BlockRequest(PxContext pxContext, PxModuleConfigurationSection config) { pxContext.ApplicationContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; pxContext.ApplicationContext.Response.TrySkipIisCustomErrors = true; @@ -333,49 +344,10 @@ public static void BlockRequest(PxContext pxContext, PxModuleConfigurationSectio } else { - ResponseBlockPage(pxContext, config); + pxBlock.ResponseBlockPage(pxContext, config); } } - public static void ResponseBlockPage(PxContext pxContext, PxModuleConfigurationSection config) - { - string template = "block_template"; - - if (pxContext.BlockAction == "j") - { - template = "challenge"; - } - else if (pxContext.BlockAction == "b") - { - template = "block"; - } - - // In the case of a challenge, the challenge response is taken directly from BlockData. Otherwise, generate html template. - string content = template == "challenge" && !string.IsNullOrEmpty(pxContext.BlockData) ? pxContext.BlockData : - TemplateFactory.getTemplate(template, config, pxContext.UUID, pxContext.Vid, pxContext.IsMobileRequest, pxContext.BlockAction); - - pxContext.ApplicationContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; - if (pxContext.IsMobileRequest) - { - pxContext.ApplicationContext.Response.ContentType = "application/json"; - using (var output = new StringWriter()) - { - JSON.Serialize( - new MobileResponse() - { - AppId = config.AppId, - Uuid = pxContext.UUID, - Action = pxContext.MapBlockAction(), - Vid = pxContext.Vid, - Page = Convert.ToBase64String(Encoding.UTF8.GetBytes(content)), - CollectorUrl = string.Format(config.CollectorUrl, config.AppId) - }, output); - content = output.ToString(); - } - } - pxContext.ApplicationContext.Response.Write(content); - } - public void Dispose() { if (httpHandler != null) @@ -415,7 +387,7 @@ private bool IsFilteredRequest(HttpContext context) { foreach (var prefix in routesWhitelist) { - if (url.StartsWith(prefix)) + if (url.StartsWith(prefix) || url == pxContext.CustomBlockUrl) { return true; } @@ -506,7 +478,8 @@ private void HandleVerification(HttpApplication application) PxLoggingUtils.LogDebug(string.Format("Invalid request to {0}", application.Context.Request.RawUrl)); PostBlockActivity(pxContext); } - + + SetPxhdAndVid(pxContext); // If implemented, run the customVerificationHandler. if (!string.IsNullOrEmpty(customVerificationHandler)) { @@ -523,13 +496,23 @@ private void HandleVerification(HttpApplication application) } } // No custom verification handler -> continue regular flow - else if (!verified && !config.MonitorMode) + else if (!verified && !pxContext.MonitorRequest) { BlockRequest(pxContext, config); application.CompleteRequest(); } } + private static void SetPxhdAndVid(PxContext pxContext) + { + + if (!string.IsNullOrEmpty(pxContext.Pxhd)) + { + string pxhd = PxConstants.COOKIE_PXHD_PREFIX + "=" + pxContext.Pxhd + "; path=/"; + pxContext.ApplicationContext.Response.AddHeader("Set-Cookie", pxhd); + } + } + /// /// Uses reflection to check whether an IVerificationHandler was implemented by the customer. /// diff --git a/PerimeterXModule/PxModuleConfigurationSection.cs b/PerimeterXModule/PxModuleConfigurationSection.cs index 6696785..7836383 100644 --- a/PerimeterXModule/PxModuleConfigurationSection.cs +++ b/PerimeterXModule/PxModuleConfigurationSection.cs @@ -22,504 +22,548 @@ // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE // SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // +using System.Collections.Generic; using System.Collections.Specialized; using System.ComponentModel; using System.Configuration; namespace PerimeterX { - public class PxModuleConfigurationSection : ConfigurationSection - { - - [ConfigurationProperty("enabled", DefaultValue = true)] - public bool Enabled - { - get - { - return (bool)this["enabled"]; - } - set - { - this["enabled"] = value; - } - } - - [ConfigurationProperty("appId", IsRequired = true)] - public string AppId - { - get - { - return (string)this["appId"]; - } - set - { - this["appId"] = value; - } - } - - [ConfigurationProperty("cookieName", DefaultValue = "_px")] - public string CookieName - { - get - { - return (string)this["cookieName"]; - } - set - { - this["cookieName"] = value; - } - } - - [ConfigurationProperty("cookieKey", IsRequired = true)] - public string CookieKey - { - get - { - return (string)this["cookieKey"]; - } - set - { - this["cookieKey"] = value; - } - } - - [ConfigurationProperty("encryptionEnabled", DefaultValue = true)] - public bool EncryptionEnabled - { - get - { - return (bool)this["encryptionEnabled"]; - } - set - { - this["encryptionEnabled"] = value; - } - } - - [ConfigurationProperty("captchaEnabled", DefaultValue = true)] - public bool CaptchaEnabled - { - get - { - return (bool)this["captchaEnabled"]; - } - set - { - this["captchaEnabled"] = value; - } - } - - [ConfigurationProperty("challengeEnabled", DefaultValue = true)] - public bool ChallengeEnabled - { - get - { - return (bool)this["challengeEnabled"]; - } - set - { - this["challengeEnabled"] = value; - } - } - - [ConfigurationProperty("signedWithUserAgent", DefaultValue = true)] - public bool SignedWithUserAgent - { - get - { - return (bool)this["signedWithUserAgent"]; - } - set - { - this["signedWithUserAgent"] = value; - } - } - - [ConfigurationProperty("signedWithIP", DefaultValue = false)] - public bool SignedWithIP - { - get - { - return (bool)this["signedWithIP"]; - } - set - { - this["signedWithIP"] = value; - } - } - - [ConfigurationProperty("blockingScore", DefaultValue = 100)] - public int BlockingScore - { - get - { - return (int)this["blockingScore"]; - } - set - { - this["blockingScore"] = value; - } - } - - [ConfigurationProperty("apiToken", DefaultValue = "")] - public string ApiToken - { - get - { - return (string)this["apiToken"]; - } - set - { - this["apiToken"] = value; - } - } - - [ConfigurationProperty("baseUri", DefaultValue = "https://sapi-{0}.perimeterx.net")] - public string BaseUri - { - get - { - return (string)this["baseUri"]; - } - set - { - this["baseUri"] = value; - } - } - - [ConfigurationProperty("apiTimeout", DefaultValue = 1500)] - public int ApiTimeout - { - get - { - return (int)this["apiTimeout"]; - } - set - { - this["apiTimeout"] = value; - } - } - - [ConfigurationProperty("reporterApiTimeout", DefaultValue = 5000)] - public int ReporterApiTimeout - { - get - { - return (int)this["reporterApiTimeout"]; - } - set - { - this["reporterApiTimeout"] = value; - } - } - - [ConfigurationProperty("socketIpHeader")] - public string SocketIpHeader - { - get - { - return (string)this["socketIpHeader"]; - } - set - { - this["socketIpHeader"] = value; - } - } - - [ConfigurationProperty("suppressContentBlock", DefaultValue = false)] - public bool SuppressContentBlock - { - get - { - return (bool)this["suppressContentBlock"]; - } - set - { - this["suppressContentBlock"] = value; - } - } - - [ConfigurationProperty("activitiesCapacity", DefaultValue = 512)] - public int ActivitiesCapacity - { - get - { - return (int)this["activitiesCapacity"]; - } - set - { - this["activitiesCapacity"] = value; - } - } - - [ConfigurationProperty("activitiesBulkSize", DefaultValue = 10)] - public int ActivitiesBulkSize - { - get - { - return (int)this["activitiesBulkSize"]; - } - set - { - this["activitiesBulkSize"] = value; - } - } - - [ConfigurationProperty("sendPageActivities", DefaultValue = true)] - public bool SendPageActivites - { - get - { - return (bool)this["sendPageActivities"]; - } - set - { - this["sendPageActivities"] = value; - } - } - - [ConfigurationProperty("sendBlockActivities", DefaultValue = true)] - public bool SendBlockActivites - { - get - { - return (bool)this["sendBlockActivities"]; - } - set - { - this["sendBlockActivities"] = value; - } - } - - [ConfigurationProperty("sensitiveHeaders", DefaultValue = "cookie,cookies")] - [TypeConverter(typeof(CommaDelimitedStringCollectionConverter))] - public StringCollection SensitiveHeaders - { - get - { - return (StringCollection)this["sensitiveHeaders"]; - } - set - { - this["sensitiveHeaders"] = value; - } - } - - - [ConfigurationProperty("fileExtWhitelist", DefaultValue = ".axd,.css,.bmp,.tif,.ttf,.docx,.woff2,.js,.pict,.tiff,.eot,.xlsx,.jpg,.csv,.eps,.woff,.xls,.jpeg,.doc,.ejs,.otf,.pptx,.gif,.pdf,.swf,.svg,.ps,.ico,.pls,.midi,.svgz,.class,.png,.ppt,.mid,.jar")] - [TypeConverter(typeof(CommaDelimitedStringCollectionConverter))] - public StringCollection FileExtWhitelist - { - get - { - return (StringCollection)this["fileExtWhitelist"]; - } - set - { - this["fileExtWhitelist"] = value; - } - } - - [ConfigurationProperty("routesWhitelist")] - [TypeConverter(typeof(CommaDelimitedStringCollectionConverter))] - public StringCollection RoutesWhitelist - { - get - { - return (StringCollection)this["routesWhitelist"]; - } - set - { - this["routesWhitelist"] = value; - } - } - - [ConfigurationProperty("useragentsWhitelist")] - [TypeConverter(typeof(CommaDelimitedStringCollectionConverter))] - public StringCollection UseragentsWhitelist - { - get - { - return (StringCollection)this["useragentsWhitelist"]; - } - set - { - this["useragentsWhitelist"] = value; - } - } - - [ConfigurationProperty("customLogo")] - public string CustomLogo - { - get - { - return (string)this["customLogo"]; - } - set - { - this["logoVisibility"] = "visible"; - this["customLogo"] = value; - } - } - - [ConfigurationProperty("cssRef")] - public string CssRef - { - get - { - return (string)this["cssRef"]; - } - set - { - this["cssRef"] = value; - } - } - - [ConfigurationProperty("jsRef")] - public string JsRef - { - get - { - return (string)this["jsRef"]; - } - set - { - this["jsRef"] = value; - } - } - - [ConfigurationProperty("useragentOverride")] - public string UserAgentOverride - { - get - { - return (string)this["useragentOverride"]; - } - set - { - this["useragentOverride"] = value; - } - } - - [ConfigurationProperty("monitorMode", DefaultValue = true)] - public bool MonitorMode - { - get - { - return (bool)this["monitorMode"]; - } - set - { - this["monitorMode"] = value; - } - } - - [ConfigurationProperty("sensitiveRoutes")] - [TypeConverter(typeof(CommaDelimitedStringCollectionConverter))] - public StringCollection SensitiveRoutes - { - get - { - return (StringCollection)this["sensitiveRoutes"]; - } - set - { - this["sensitiveRoutes"] = value; - } - } - - [ConfigurationProperty("customVerificationHandler")] - public string CustomVerificationHandler - { - get - { - return (string)this["customVerificationHandler"]; - } - set - { - this["customVerificationHandler"] = value; - } - } - - [ConfigurationProperty("collectorUrl", DefaultValue = "https://collector-{0}.perimeterx.net")] - public string CollectorUrl - { - get - { - return (string)this["collectorUrl"]; - } - set - { - this["collectorUrl"] = value; - } - } - - [ConfigurationProperty("enforceSpecificRoutes")] - [TypeConverter(typeof(CommaDelimitedStringCollectionConverter))] - public StringCollection EnforceSpecificRoutes - { - get - { - return (StringCollection)this["enforceSpecificRoutes"]; - } - set - { - this["enforceSpecificRoutes"] = value; - } - } - - [ConfigurationProperty("firstPartyEnabled", DefaultValue = true)] - public bool FirstPartyEnabled - { - get - { - return (bool)this["firstPartyEnabled"]; - } - set - { - this["firstPartyEnabled"] = value; - } - } - - [ConfigurationProperty("firstPartyXhrEnabled", DefaultValue = true)] - public bool FirstPartyXhrEnabled - { - get - { - return (bool)this["firstPartyXhrEnabled"]; - } - set - { - this["firstPartyXhrEnabled"] = value; - } - } - - [ConfigurationProperty("clientHostUrl", DefaultValue = "https://client.perimeterx.net")] - public string ClientHostUrl - { - get - { - return (string)this["clientHostUrl"]; - } - - set - { - this["clientHostUrl"] = value; - } - } - [ConfigurationProperty("captchaHostUrl", DefaultValue = "https://captcha.perimeterx.net")] - public string CaptchaHostUrl - { - get - { - return (string)this["captchaHostUrl"]; - } - - set - { - this["captchaHostUrl"] = value; - } - } - } + public class PxModuleConfigurationSection : ConfigurationSection + { + + [ConfigurationProperty("enabled", DefaultValue = true)] + public bool Enabled + { + get + { + return (bool)this["enabled"]; + } + set + { + this["enabled"] = value; + } + } + + [ConfigurationProperty("appId", IsRequired = true)] + public string AppId + { + get + { + return (string)this["appId"]; + } + set + { + this["appId"] = value; + } + } + + [ConfigurationProperty("cookieName", DefaultValue = "_px")] + public string CookieName + { + get + { + return (string)this["cookieName"]; + } + set + { + this["cookieName"] = value; + } + } + + [ConfigurationProperty("cookieKey", IsRequired = true)] + public string CookieKey + { + get + { + return (string)this["cookieKey"]; + } + set + { + this["cookieKey"] = value; + } + } + + [ConfigurationProperty("encryptionEnabled", DefaultValue = true)] + public bool EncryptionEnabled + { + get + { + return (bool)this["encryptionEnabled"]; + } + set + { + this["encryptionEnabled"] = value; + } + } + + [ConfigurationProperty("captchaEnabled", DefaultValue = true)] + public bool CaptchaEnabled + { + get + { + return (bool)this["captchaEnabled"]; + } + set + { + this["captchaEnabled"] = value; + } + } + + [ConfigurationProperty("challengeEnabled", DefaultValue = true)] + public bool ChallengeEnabled + { + get + { + return (bool)this["challengeEnabled"]; + } + set + { + this["challengeEnabled"] = value; + } + } + + [ConfigurationProperty("signedWithUserAgent", DefaultValue = true)] + public bool SignedWithUserAgent + { + get + { + return (bool)this["signedWithUserAgent"]; + } + set + { + this["signedWithUserAgent"] = value; + } + } + + [ConfigurationProperty("signedWithIP", DefaultValue = false)] + public bool SignedWithIP + { + get + { + return (bool)this["signedWithIP"]; + } + set + { + this["signedWithIP"] = value; + } + } + + [ConfigurationProperty("blockingScore", DefaultValue = 100)] + public int BlockingScore + { + get + { + return (int)this["blockingScore"]; + } + set + { + this["blockingScore"] = value; + } + } + + [ConfigurationProperty("apiToken", DefaultValue = "")] + public string ApiToken + { + get + { + return (string)this["apiToken"]; + } + set + { + this["apiToken"] = value; + } + } + + [ConfigurationProperty("baseUri", DefaultValue = "https://sapi-{0}.perimeterx.net")] + public string BaseUri + { + get + { + return (string)this["baseUri"]; + } + set + { + this["baseUri"] = value; + } + } + + [ConfigurationProperty("apiTimeout", DefaultValue = 1500)] + public int ApiTimeout + { + get + { + return (int)this["apiTimeout"]; + } + set + { + this["apiTimeout"] = value; + } + } + + [ConfigurationProperty("reporterApiTimeout", DefaultValue = 5000)] + public int ReporterApiTimeout + { + get + { + return (int)this["reporterApiTimeout"]; + } + set + { + this["reporterApiTimeout"] = value; + } + } + + [ConfigurationProperty("socketIpHeader")] + public string SocketIpHeader + { + get + { + return (string)this["socketIpHeader"]; + } + set + { + this["socketIpHeader"] = value; + } + } + + [ConfigurationProperty("suppressContentBlock", DefaultValue = false)] + public bool SuppressContentBlock + { + get + { + return (bool)this["suppressContentBlock"]; + } + set + { + this["suppressContentBlock"] = value; + } + } + + [ConfigurationProperty("activitiesCapacity", DefaultValue = 512)] + public int ActivitiesCapacity + { + get + { + return (int)this["activitiesCapacity"]; + } + set + { + this["activitiesCapacity"] = value; + } + } + + [ConfigurationProperty("activitiesBulkSize", DefaultValue = 10)] + public int ActivitiesBulkSize + { + get + { + return (int)this["activitiesBulkSize"]; + } + set + { + this["activitiesBulkSize"] = value; + } + } + + [ConfigurationProperty("sendPageActivities", DefaultValue = true)] + public bool SendPageActivites + { + get + { + return (bool)this["sendPageActivities"]; + } + set + { + this["sendPageActivities"] = value; + } + } + + [ConfigurationProperty("sendBlockActivities", DefaultValue = true)] + public bool SendBlockActivites + { + get + { + return (bool)this["sendBlockActivities"]; + } + set + { + this["sendBlockActivities"] = value; + } + } + + [ConfigurationProperty("sensitiveHeaders", DefaultValue = "cookie,cookies")] + [TypeConverter(typeof(CommaDelimitedStringCollectionConverter))] + public StringCollection SensitiveHeaders + { + get + { + return (StringCollection)this["sensitiveHeaders"]; + } + set + { + this["sensitiveHeaders"] = value; + } + } + + + [ConfigurationProperty("fileExtWhitelist", DefaultValue = ".axd,.css,.bmp,.tif,.ttf,.docx,.woff2,.js,.pict,.tiff,.eot,.xlsx,.jpg,.csv,.eps,.woff,.xls,.jpeg,.doc,.ejs,.otf,.pptx,.gif,.pdf,.swf,.svg,.ps,.ico,.pls,.midi,.svgz,.class,.png,.ppt,.mid,.jar")] + [TypeConverter(typeof(CommaDelimitedStringCollectionConverter))] + public StringCollection FileExtWhitelist + { + get + { + return (StringCollection)this["fileExtWhitelist"]; + } + set + { + this["fileExtWhitelist"] = value; + } + } + + [ConfigurationProperty("routesWhitelist")] + [TypeConverter(typeof(CommaDelimitedStringCollectionConverter))] + public StringCollection RoutesWhitelist + { + get + { + return (StringCollection)this["routesWhitelist"]; + } + set + { + this["routesWhitelist"] = value; + } + } + + [ConfigurationProperty("useragentsWhitelist")] + [TypeConverter(typeof(CommaDelimitedStringCollectionConverter))] + public StringCollection UseragentsWhitelist + { + get + { + return (StringCollection)this["useragentsWhitelist"]; + } + set + { + this["useragentsWhitelist"] = value; + } + } + + [ConfigurationProperty("customLogo")] + public string CustomLogo + { + get + { + return (string)this["customLogo"]; + } + set + { + this["logoVisibility"] = "visible"; + this["customLogo"] = value; + } + } + + [ConfigurationProperty("cssRef")] + public string CssRef + { + get + { + return (string)this["cssRef"]; + } + set + { + this["cssRef"] = value; + } + } + + [ConfigurationProperty("jsRef")] + public string JsRef + { + get + { + return (string)this["jsRef"]; + } + set + { + this["jsRef"] = value; + } + } + + [ConfigurationProperty("useragentOverride")] + public string UserAgentOverride + { + get + { + return (string)this["useragentOverride"]; + } + set + { + this["useragentOverride"] = value; + } + } + + [ConfigurationProperty("monitorMode", DefaultValue = true)] + public bool MonitorMode + { + get + { + return (bool)this["monitorMode"]; + } + set + { + this["monitorMode"] = value; + } + } + + [ConfigurationProperty("sensitiveRoutes")] + [TypeConverter(typeof(CommaDelimitedStringCollectionConverter))] + public StringCollection SensitiveRoutes + { + get + { + return (StringCollection)this["sensitiveRoutes"]; + } + set + { + this["sensitiveRoutes"] = value; + } + } + + [ConfigurationProperty("customVerificationHandler")] + public string CustomVerificationHandler + { + get + { + return (string)this["customVerificationHandler"]; + } + set + { + this["customVerificationHandler"] = value; + } + } + + [ConfigurationProperty("collectorUrl", DefaultValue = "https://collector-{0}.perimeterx.net")] + public string CollectorUrl + { + get + { + return (string)this["collectorUrl"]; + } + set + { + this["collectorUrl"] = value; + } + } + + [ConfigurationProperty("enforceSpecificRoutes")] + [TypeConverter(typeof(CommaDelimitedStringCollectionConverter))] + public StringCollection EnforceSpecificRoutes + { + get + { + return (StringCollection)this["enforceSpecificRoutes"]; + } + set + { + this["enforceSpecificRoutes"] = value; + } + } + + [ConfigurationProperty("firstPartyEnabled", DefaultValue = true)] + public bool FirstPartyEnabled + { + get + { + return (bool)this["firstPartyEnabled"]; + } + set + { + this["firstPartyEnabled"] = value; + } + } + + [ConfigurationProperty("firstPartyXhrEnabled", DefaultValue = true)] + public bool FirstPartyXhrEnabled + { + get + { + return (bool)this["firstPartyXhrEnabled"]; + } + set + { + this["firstPartyXhrEnabled"] = value; + } + } + + [ConfigurationProperty("clientHostUrl", DefaultValue = "https://client.perimeterx.net")] + public string ClientHostUrl + { + get + { + return (string)this["clientHostUrl"]; + } + + set + { + this["clientHostUrl"] = value; + } + } + + [ConfigurationProperty("captchaHostUrl", DefaultValue = "https://captcha.perimeterx.net")] + public string CaptchaHostUrl + { + get + { + return (string)this["captchaHostUrl"]; + } + + set + { + this["captchaHostUrl"] = value; + } + } + + [ConfigurationProperty("customBlockUrl", DefaultValue = null)] + public string CustomBlockUrl + { + get + { + return (string)this["customBlockUrl"]; + } + + set + { + this["customBlockUrl"] = value; + } + } + + [ConfigurationProperty("redirectOnCustomUrl", DefaultValue = false)] + public bool RedirectOnCustomUrl + { + get + { + return (bool)this["redirectOnCustomUrl"]; + } + + set + { + this["redirectOnCustomUrl"] = value; + } + } + + [ConfigurationProperty("mitigationUrls", DefaultValue = "")] + [TypeConverter(typeof(CommaDelimitedStringCollectionConverter))] + public StringCollection MitigationUrls + { + get + { + return (StringCollection) this["mitigationUrls"]; + } + + set + { + this["mitigationUrls"] = value; + } + } + } } - diff --git a/README.md b/README.md index 1206e53..00bfcd2 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [PerimeterX](http://www.perimeterx.com) ASP.NET SDK =================================================== -> Latest stable version: [v2.7.1](https://www.nuget.org/packages/PerimeterXModule/2.7.1) +> Latest stable version: [v3.0.0](https://www.nuget.org/packages/PerimeterXModule/3.0.0) Table of Contents ----------------- @@ -14,6 +14,7 @@ Table of Contents * [Dependencies](#dependencies) * [Installation](#installation) * [Basic Usage Example](#basic-usage) + * [Upgrading](#upgrade) **[Configuration](#configuration)** * [Customizing Default Block Pages](#custom-block-page) @@ -22,7 +23,6 @@ Table of Contents * [Enable/Disable Captcha](#captcha-support) * [First Party Mode](#first-party) * [Extracting Real IP Address](#real-ip) - * [Override UA header](#override-ua) * [Filter Sensitive Headers](#sensitive-headers) * [Sensitive Routes](#sensitive-routes) * [Whitelist Routes](#whitelist-routes) @@ -31,6 +31,9 @@ Table of Contents * [Send Page Activities](#send-page-activities) * [Monitor Mode](#monitor-mode) * [Base URI](#base-uri) + * [Override UA header](#override-ua) + * [Mitigation Urls](#mitigiation-urls) + **[Contributing](#contributing)** * [Tests](#tests) @@ -87,12 +90,25 @@ Add site specific configuration (configuration level) appId="" apiToken="" cookieKey="" - monitorMode="false" - blockingScore="70" + monitorMode="true" + blockingScore="100" > ``` +### Upgrading +To upgrade to the latest Enforcer version: + +1. In Visual Studio, right click on the solution and Select **Manage NuGet packages for solution**. +2. Search for `perimeterxmodule` in the updates section, and update. + + **OR** + +Run `Install-Package PerimeterXModule` in the Package Manager Console + +Your Enforcer version is now upgraded to the latest enforcer version. + +For more information, contact [PerimeterX Support](support@perimeterx.com). ### Configuration Options @@ -107,8 +123,9 @@ Configuration options are set in `pxModuleConfigurationSection` - apiToken #### Customizing Default Block Pages -###### Custom Logo -Adding a custom logo to the blocking page is by providing the pxConfig a key ```customLogo``` , the logo will be displayed at the top div of the the block page The logo's ```max-heigh``` property would be 150px and width would be set to ```auto``` + +##### Custom Logo +Adding a custom logo to the blocking page is by providing the pxConfig a key ```customLogo``` , the logo will be displayed at the top div of the the block page The logo's ```max-height``` property would be 150px and width would be set to ```auto``` The key ```customLogo``` expects a valid URL address such as ```https://s.perimeterx.net/logo.png``` @@ -119,7 +136,7 @@ Example below: ... ``` -Custom JS/CSS +##### Custom JS/CSS The block page can be modified with a custom CSS by adding to the ```pxConfig``` the key ```cssRef``` and providing a valid URL to the css In addition there is also the option to add a custom JS file by adding ```jsRef``` key to the ```pxConfig``` and providing the JS file that will be loaded with the block page, this key also expects a valid URL @@ -128,17 +145,65 @@ On both cases if the URL is not a valid format an exception will be thrown Example below: ```xml ... - jsRef="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" - cssRef="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" + jsRef="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" + cssRef="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" ... ``` + +##### Redirect to a Custom Block Page URL +Customizes the block page to meet branding and message requirements by specifying the URL of the block page HTML file. The page can also implement CAPTCHA. + +**Default:** "" (empty string) + +Example: + +```xml +customBlockUrl = "http://sub.domain.com/block.html" +``` + +> Note: This URI is whitelisted automatically to avoid infinite redirects. + +##### Redirect on Custom URL + +The `redirectOnCustomUrl` boolean flag to redirect users to a block page. + +**Default:** false + +Example: + +```xml + redirectOnCustomUrl = "false" +``` + +By default, when a user exceeds the blocking threshold and blocking is enabled, the user is redirected to the block page defined by the `customBlockUrl` variable. The defined block page displays a **307 (Temporary Redirect)** HTTP Response Code. + +When the flag is set to false, a **403 (Unauthorized)** HTTP Response Code is displayed on the blocked page URL. + + +Setting the flag to true (enabling redirects) results in the following URL upon blocking: + +``` +http://www.example.com/block.html?url=L3NvbWVwYWdlP2ZvbyUzRGJhcg==&uuid=e8e6efb0-8a59-11e6-815c-3bdad80c1d39&vid=08320300-6516-11e6-9308-b9c827550d47 +``` + +Setting the flag to false does not require the block page to include any of the examples below, as they are injected into the blocking page via the PerimeterX ASP.NET Enforcer. + +> NOTE: The `url` parameter should be built with the URL *Encoded* query parameters (of the original request) with both the original path and variables Base64 Encoded (to avoid collisions with block page query params). + +##### Custom Block Pages Requirements + +As of version 2.8.0, Captcha logic is being handled through the JavaScript snippet and not through the Enforcer. + +Users who have Custom Block Pages must include the new script tag and a new div in the _.html_ block page. + + #### Changing the Minimum Score for Blocking -**default:** 70 +**default:** 100 ```xml ... - blockingScore="70" + blockingScore="100" ... ``` @@ -322,7 +387,7 @@ Set this flag to false to disable monitor mode ```xml ... - monitorMode="false" + monitorMode="true" ... ``` **default:** true @@ -351,22 +416,53 @@ The user's user agent can be returned to the PerimeterX module using a name of a ... ``` +#### Data Enrichment + +Users can use the additional activity handler to retrieve information for the request using the data-enrichment object. First, check that the data enrichment object is verified, then you can access it's properties. + +```c# +... + +#### Mitigation Urls + +Users can define custom paths that allow blocking. All other paths will be set to monitoring mode. +```c# +mitigiation-urls="path1, path2" +... + +namespace MyApp +{ + public class MyVerificationHandler : IVerificationHandler + { + public void Handle(HttpApplication application, PxContext pxContext, PxModuleConfigurationSection pxConfig) + { + ... + if (pxContext.IsPxdeVerified) { + dynamic pxde = pxContext.Pxde; + // do something with the data enrichment + } + ... + } + } +} +``` + Contributing ---------------------------------------- The following steps are welcome when contributing to our project. -###Fork/Clone +### Fork/Clone First and foremost, [Create a fork](https://guides.github.com/activities/forking/) of the repository, and clone it locally. Create a branch on your fork, preferably using a self descriptive branch name. -###Code/Run +### Code/Run Code your way out of your mess, and help improve our project by implementing missing features, adding capabilities or fixing bugs. To run the code, simply follow the steps in the [installation guide](#installation). Grab the keys from the PerimeterX Portal, and try refreshing your page several times continuously. If no default behaviours have been overridden, you should see the PerimeterX block page. Solve the CAPTCHA to clean yourself and start fresh again. -###Pull Request +### Pull Request After you have completed the process, create a pull request to the Upstream repository. Please provide a complete and thorough description explaining the changes. Remember this code has to be read by our maintainers, so keep it simple, smart and accurate. -###Thanks +### Thanks After all, you are helping us by contributing to this project, and we want to thank you for it. We highly appreciate your time invested in contributing to our project, and are glad to have people like you - kind helpers.