Skip to content

Commit

Permalink
Merge be1baf0 into 6b8321e
Browse files Browse the repository at this point in the history
  • Loading branch information
marysieek committed Mar 23, 2021
2 parents 6b8321e + be1baf0 commit 1f242ee
Show file tree
Hide file tree
Showing 12 changed files with 124 additions and 118 deletions.
4 changes: 2 additions & 2 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
The MIT License (MIT)

Copyright (c) 2020 Castle Intelligence, Inc.
Copyright (c) 2021 Castle Intelligence, Inc.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand All @@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
SOFTWARE.
25 changes: 12 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,8 @@ For events where you don't require a response.
```csharp
castleClient.Track(new ActionRequest()
{
Event = Castle.Events.LogoutSucceeded,
Event = "$logout",
Status = "$succeeded",
UserId = user.Id,
UserTraits = new Dictionary<string, string>()
{
Expand All @@ -130,19 +131,17 @@ For events where you require a response. It is used in the same way as `Track`,
```csharp
var verdict = await castleClient.Authenticate(new ActionRequest()
{
Event = Castle.Events.LogoutSucceeded,
Event = "$logout",
Status = "$succeeded",
UserId = user.Id,
UserTraits = new Dictionary<string, string>()
{
["email"] = user.Email,
["registered_at"] = user.RegisteredAt
},
Context = new RequestContext()
{
Ip = Request.HttpContext.Connection.RemoteIpAddress.ToString(),
ClientId = Request.Cookies["__cid"],
Headers = Request.Headers.ToDictionary(x => x.Key, y => y.Value.FirstOrDefault());
}
Ip = Request.HttpContext.Connection.RemoteIpAddress.ToString(),
Fingerprint = Request.Cookies["__cid"],
Headers = Request.Headers.ToDictionary(x => x.Key, y => y.Value.FirstOrDefault())
});
```

Expand All @@ -166,23 +165,23 @@ If no failover strategy is set (i.e. `None`), a `Castle.Infrastructure.Exception

| Option | Description
| --- | ---
| Event | The event generated by the user. It can be either an event from the SDK constants in `Castle.Events` or a custom one.
| Event | The event generated by the user. List of Recognized Events can be found in the [docs](https://docs.castle.io/api_reference/#list-of-recognized-events).
| UserId | Your internal ID for the end user.
| UserTraits | An optional, recommended, dictionary of user information, such as `email` and `registered_at`.
| Properties | An optional dictionary of custom information.
| Timestamp | An optional datetime indicating when the event occurred, in cases where this might be different from the time when the request is made.
| DeviceToken | The optional device token, used for mitigating or escalating.
| Context | The request context information. See information below.

#### Request context
#### Request options

| Option | Description
| --- | ---
| Ip | The IP address of the request. Note that this needs to be the original request IP, not the IP of an internal proxy, such as nginx.
| ClientId | The client ID, generated by the `c.js` integration on the front end. Commonly found in the `__cid` cookie in `Request.Cookies`, or in some cases the `X-CASTLE-CLIENT-ID` header.
| Headers | Headers mapped from the the original request (most likely `Request.Headers`).

You can call `Castle.Context.FromHttpRequest(request)` to get a ready-made `RequestContext` instance from your current request.
You can call `Castle.Options.FromHttpRequest(request)` to get a ready-made `RequestOptions` instance from your current request.

##### ASP&#46;NET MVC 5
```csharp
Expand All @@ -192,7 +191,7 @@ public class HomeController : Controller
{
var actionRequest = new ActionRequest()
{
Context = Castle.Context.FromHttpRequest(Request)
Options = Castle.Options.FromHttpRequest(Request)
...
```

Expand All @@ -204,7 +203,7 @@ public class IndexModel : PageModel
{
var actionRequest = new ActionRequest()
{
Context = Castle.Context.FromHttpRequest(Request)
Options = Castle.Options.FromHttpRequest(Request)
...
```

Expand Down
5 changes: 0 additions & 5 deletions src/Castle.Sdk/Castle.Sdk.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

26 changes: 0 additions & 26 deletions src/Castle.Sdk/Events.cs

This file was deleted.

34 changes: 29 additions & 5 deletions src/Castle.Sdk/Messages/Requests/ActionRequest.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
using System;
using System.Collections.Generic;
using Castle.Infrastructure;
using Castle.Infrastructure.Json;

using Newtonsoft.Json;


namespace Castle.Messages.Requests
{
public class ActionRequest
Expand All @@ -16,24 +19,45 @@ public class ActionRequest

public string Event { get; set; }

public string Status { get; set; }

public string Email { get; set; }

public string UserId { get; set; }

[JsonConverter(typeof(EmptyStringToFalseConverter))]
public string Fingerprint { get; set; }

public string Ip { get; set; }

[JsonProperty(ItemConverterType = typeof(StringScrubConverter))]
public IDictionary<string, string> Headers { get; set; } = new Dictionary<string, string>();

public IDictionary<string, string> UserTraits { get; set; } = new Dictionary<string, string>();

public IDictionary<string, string> Properties { get; set; } = new Dictionary<string, string>();

public RequestContext Context { get; set; } = new RequestContext();

[JsonIgnore]
public RequestOptions Options { get; set; } = new RequestOptions();

internal ActionRequest PrepareApiCopy(string[] allowList, string[] denyList)
{
var copy = (ActionRequest) MemberwiseClone();
var scrubbed = HeaderScrubber.Scrub(Context.Headers, allowList, denyList);
copy.Context = Context.WithHeaders(scrubbed);

copy.SentAt = DateTime.Now;
var scrubbed = HeaderScrubber.Scrub(Options.Headers, allowList, denyList);
var opts = Options.WithHeaders(scrubbed);

// Assign Fingerprint, IP and Headers from options
// Newtonsoft.Json doesn't apply custom converter to null values, so this must be empty instead
copy.Context.ClientId = copy.Context.ClientId ?? "";
var newFingerprint = opts.Fingerprint ?? "";
copy.Fingerprint = copy.Fingerprint ?? newFingerprint;
copy.Ip = opts.Ip;
copy.Headers = opts.Headers;

copy.Context = Context.WithLibrary();

copy.SentAt = DateTime.Now;

return copy;
}
Expand Down
15 changes: 2 additions & 13 deletions src/Castle.Sdk/Messages/Requests/RequestContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,14 @@ namespace Castle.Messages.Requests
{
public class RequestContext
{
[JsonConverter(typeof(EmptyStringToFalseConverter))]
public string ClientId { get; set; }

public string Ip { get; set; }

[JsonProperty(ItemConverterType = typeof(StringScrubConverter))]
public IDictionary<string, string> Headers { get; set; } = new Dictionary<string, string>();

[JsonProperty]
internal LibraryInfo Library { get; set; } = new LibraryInfo();

internal RequestContext WithHeaders(IDictionary<string, string> headers)
internal RequestContext WithLibrary()
{
return new RequestContext()
{
ClientId = ClientId,
Ip = Ip,
Library = Library,
Headers = headers
Library = Library
};
}
}
Expand Down
27 changes: 27 additions & 0 deletions src/Castle.Sdk/Messages/Requests/RequestOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using System.Collections.Generic;
using Castle.Infrastructure.Json;
using Newtonsoft.Json;

namespace Castle.Messages.Requests
{
public class RequestOptions
{
[JsonConverter(typeof(EmptyStringToFalseConverter))]
public string Fingerprint { get; set; }

public string Ip { get; set; }

[JsonProperty(ItemConverterType = typeof(StringScrubConverter))]
public IDictionary<string, string> Headers { get; set; } = new Dictionary<string, string>();

internal RequestOptions WithHeaders(IDictionary<string, string> headers)
{
return new RequestOptions()
{
Fingerprint = Fingerprint,
Ip = Ip,
Headers = headers
};
}
}
}
20 changes: 10 additions & 10 deletions src/Castle.Sdk/Context.cs → src/Castle.Sdk/Options.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,33 +10,33 @@

namespace Castle
{
public static class Context
public static class Options
{
#if NET461 || NET48
public static RequestContext FromHttpRequest(System.Web.HttpRequestBase request, string[] ipHeaders = null)
public static RequestOptions FromHttpRequest(System.Web.HttpRequestBase request, string[] ipHeaders = null)
{
var headers = new Dictionary<string, string>();
foreach (string key in request.Headers.Keys)
{
headers.Add(key, request.Headers[key]);
}

var clientId = GetClientIdForFramework(request.Headers, name => request.Cookies[name]?.Value);
var fingerprint = GetFingerprintForFramework(request.Headers, name => request.Cookies[name]?.Value);

var ip = GetIpForFramework(request.Headers, ipHeaders,
() => request.UserHostAddress,
() => CastleConfiguration.Configuration);

return new RequestContext()
return new RequestOptions()
{
ClientId = clientId,
Fingerprint = fingerprint,
Headers = headers,
Ip = ip
};
}
#endif

internal static string GetClientIdForFramework(NameValueCollection headers, Func<string, string> getCookieValue)
internal static string GetFingerprintForFramework(NameValueCollection headers, Func<string, string> getCookieValue)
{
return headers.AllKeys.Contains("X-Castle-Client-ID", StringComparer.OrdinalIgnoreCase)
? headers["X-Castle-Client-ID"]
Expand Down Expand Up @@ -118,19 +118,19 @@ string RemoveProxies(string[] ips)
}

#if NETSTANDARD2_0 || NETCOREAPP
public static RequestContext FromHttpRequest(Microsoft.AspNetCore.Http.HttpRequest request, string[] ipHeaders = null)
public static RequestOptions FromHttpRequest(Microsoft.AspNetCore.Http.HttpRequest request, string[] ipHeaders = null)
{
return new RequestContext()
return new RequestOptions()
{
ClientId = GetClientIdForCore(request.Headers, request.Cookies),
Fingerprint = GetFingerprintForCore(request.Headers, request.Cookies),
Headers = request.Headers.ToDictionary(x => x.Key, y => y.Value.FirstOrDefault()),
Ip = GetIpForCore(request.Headers, ipHeaders,
() => request.HttpContext.Connection.RemoteIpAddress?.ToString(),
() => CastleConfiguration.Configuration)
};
}

internal static string GetClientIdForCore(
internal static string GetFingerprintForCore(
IDictionary<string, Microsoft.Extensions.Primitives.StringValues> headers,
Microsoft.AspNetCore.Http.IRequestCookieCollection cookies)
{
Expand Down
12 changes: 6 additions & 6 deletions src/Tests/Actions/When_preparing_request.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public void Should_scrub_headers(ActionRequest request, CastleConfiguration opti
{
var result = request.PrepareApiCopy(options.AllowList, options.DenyList);

result.Context.Headers.Should().NotBeSameAs(request.Context.Headers);
result.Headers.Should().NotBeSameAs(request.Headers);
}

[Theory, AutoFakeData]
Expand All @@ -26,21 +26,21 @@ public void Should_set_sent_date(ActionRequest request, CastleConfiguration opti
}

[Theory, AutoFakeData]
public void Should_set_null_clientid_to_empty(ActionRequest request, CastleConfiguration options)
public void Should_set_null_fingerprint_to_default(ActionRequest request, CastleConfiguration options)
{
request.Context.ClientId = null;
request.Fingerprint = null;

var result = request.PrepareApiCopy(options.AllowList, options.DenyList);

result.Context.ClientId.Should().Be("");
result.Fingerprint.Should().NotBe(null);
}

[Theory, AutoFakeData]
public void Should_preserve_valid_clientid(ActionRequest request, CastleConfiguration options)
public void Should_preserve_valid_fingerprint(ActionRequest request, CastleConfiguration options)
{
var result = request.PrepareApiCopy(options.AllowList, options.DenyList);

result.Context.ClientId.Should().Be(request.Context.ClientId);
result.Fingerprint.Should().Be(request.Fingerprint);
}
}
}
10 changes: 5 additions & 5 deletions src/Tests/Json/When_serializing_special_properties.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ public class When_serializing_special_properties
{
// Null values are skipped by Newtonsoft.Json, so we don't test those
[Theory]
[InlineData("non-empty", "\"client_id\":\"non-empty\"")]
[InlineData("", "\"client_id\":false")]
public void Should_serialize_client_id_to_false_if_empty(string value, string expected)
[InlineData("non-empty", "\"fingerprint\":\"non-empty\"")]
[InlineData("", "\"fingerprint\":false")]
public void Should_serialize_fingerprint_to_false_if_empty(string value, string expected)
{
var obj = new RequestContext() { ClientId = value };
var obj = new RequestOptions() { Fingerprint = value };

var result = JsonForCastle.SerializeObject(obj);

Expand Down Expand Up @@ -51,7 +51,7 @@ public void Should_only_convert_strings_for_for_empty_string()
string value,
string expected)
{
var obj = new RequestContext()
var obj = new RequestOptions()
{
Headers = new Dictionary<string, string>()
{
Expand Down
Loading

0 comments on commit 1f242ee

Please sign in to comment.