Skip to content

Commit 8ed38c5

Browse files
committed
more stuff
1 parent b497464 commit 8ed38c5

8 files changed

Lines changed: 228 additions & 41 deletions

File tree

.github/workflows/update-cloudflare-proxies.yml

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,6 @@ on:
55
- cron: '0 0 1 * *' # runs at 00:00 UTC on the 1st day of every month
66
workflow_dispatch:
77

8-
env:
9-
IP_MERGED_FILE: Common.ASPNET/cloudflare-ips.txt
10-
118
jobs:
129
update-proxies:
1310
runs-on: ubuntu-latest
@@ -17,23 +14,16 @@ jobs:
1714
with:
1815
ref: ${{ github.ref }}
1916

20-
- name: Fetch Cloudflare IPs and Update Files
21-
env:
22-
IPV4_URL: https://www.cloudflare.com/ips-v4
23-
IPV6_URL: https://www.cloudflare.com/ips-v6
24-
run: |
25-
set -euo pipefail
17+
- uses: actions/setup-dotnet@v4
2618

27-
echo "Fetching Cloudflare IP lists and merging"
28-
curl -s $IPV4_URL > $IP_MERGED_FILE
29-
echo "" >> $IP_MERGED_FILE
30-
curl -s $IPV6_URL >> $IP_MERGED_FILE
19+
- name: Build to regenerate Cloudflare IPs
20+
run: dotnet build Common/Common.csproj
3121

3222
- name: Commit and Push Changes
3323
run: |
3424
git config user.name "github-actions[bot]"
3525
git config user.email "github-actions[bot]@users.noreply.github.com"
36-
git add ${{ env.IP_MERGED_FILE }}
26+
git add Common/Utils/CloudflareNetworks.g.cs
3727
3828
if git diff --cached --quiet; then
3929
echo "No changes detected."

CLAUDE.md

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Project Overview
6+
7+
OpenShock Common .NET library — shared utilities for the OpenShock ecosystem. Contains two main libraries: **Common** (utilities, validation, crypto, geolocation) and **DynamicLinq** (dynamic LINQ query builder for PostgreSQL/EF Core).
8+
9+
## Build Commands
10+
11+
```bash
12+
# Build
13+
dotnet build
14+
15+
# Run all tests
16+
dotnet test
17+
18+
# Run a single test project
19+
dotnet test Common.Tests/Common.Tests.csproj
20+
dotnet test DynamicLinq.Tests/DynamicLinq.Tests.csproj
21+
22+
# CI-style build + test
23+
dotnet build --configuration Release && dotnet test --configuration Release --no-build
24+
```
25+
26+
No separate lint command — warnings are treated as errors in Debug configuration.
27+
28+
## Solution Structure
29+
30+
- **Common/** — Main utility library (NuGet package output)
31+
- **Common.Tests/** — Unit tests for Common
32+
- **DynamicLinq/** — Dynamic LINQ expression builder targeting PostgreSQL via EF Core
33+
- **DynamicLinq.Tests/** — Tests for DynamicLinq (uses Testcontainers with PostgreSQL)
34+
35+
Solution file: `Common.slnx`
36+
37+
## Build Configuration
38+
39+
- .NET 9.0, latest C# language version
40+
- Nullable reference types enabled globally
41+
- Central package management via `Directory.Packages.props`
42+
- `Directory.Build.props` enables implicit usings, XML docs, and treats warnings as errors in Debug
43+
- GitVersion used in CI for semantic versioning (requires full git history)
44+
45+
## Testing
46+
47+
Uses **TUnit** (not xUnit/NUnit). Test methods use TUnit attributes and async patterns. DynamicLinq tests use **Testcontainers** for real PostgreSQL instances and **Bogus** for test data generation.
48+
49+
## Architecture & Key Patterns
50+
51+
### Common Library Namespaces
52+
- `Constants` — Domain hard limits (username/password lengths, shocker limits, durations, intensities)
53+
- `Utils``HashingUtils` (BCrypt + legacy PBKDF2, SHA-256 token hashing), `CryptoUtils` (cryptographic RNG), `MathUtils` (Haversine), `LatencyEmulator` (timing-safe operations with Gaussian noise)
54+
- `Geo``Alpha2CountryCode` (value-type struct wrapping ushort), `DistanceLookup` (pre-computed frozen dictionary of country-to-country distances)
55+
- `Validation``CharsetMatchers` for detecting emojis, zalgo text, zero-width spaces, control chars
56+
- `JsonSerialization` — Custom converters (Unix milliseconds, flag-guarded enum strings)
57+
58+
### DynamicLinq Library
59+
- `QueryStringTokenizer` — Parses filter strings with quoted values and escape sequences
60+
- `DBExpressionBuilder` — Converts string filter queries into LINQ expressions (supports `eq`, `neq`, `lt`, `gt`, `lte`, `gte`, `ilike`; multiple filters joined with `and`)
61+
- `OrderByQueryBuilder` — Dynamic OrderBy/ThenBy from string input
62+
- Properties marked with `[IgnoreDataMember]` are excluded from query building for security
63+
64+
### Performance Conventions
65+
- `stackalloc` / `Span<T>` for short-lived buffers
66+
- `ArrayPool<byte>` for temporary allocations
67+
- `FrozenDictionary` for immutable lookup tables
68+
- `CollectionsMarshal` for optimized dictionary operations
69+
- `GeneratedRegex` for compiled regex patterns
70+
71+
### Security Conventions
72+
- Password hashing uses BCrypt with SHA-512; PBKDF2 is legacy-only
73+
- Token hashing uses SHA-256
74+
- Timing-safe verification via `LatencyEmulator` sliding window statistics
75+
- Expression builder hides internal structure in error messages
76+
77+
## License
78+
79+
AGPL-3.0-or-later

Common/Common.csproj

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,50 @@
1010
<PackageReference Include="OneOf" />
1111
<PackageReference Include="Serilog" />
1212
</ItemGroup>
13+
14+
<!-- Fetch Cloudflare IPs at build time and generate C# source -->
15+
<UsingTask TaskName="GenerateCloudflareIPsSource" TaskFactory="RoslynCodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)/Microsoft.Build.Tasks.Core.dll">
16+
<ParameterGroup>
17+
<IPv4File ParameterType="System.String" Required="true" />
18+
<IPv6File ParameterType="System.String" Required="true" />
19+
<OutputFile ParameterType="System.String" Required="true" />
20+
</ParameterGroup>
21+
<Task>
22+
<Code Type="Fragment" Language="cs"><![CDATA[
23+
var lines = new List<string>();
24+
lines.AddRange(File.ReadAllLines(IPv4File).Where(l => !string.IsNullOrWhiteSpace(l)));
25+
lines.AddRange(File.ReadAllLines(IPv6File).Where(l => !string.IsNullOrWhiteSpace(l)));
26+
27+
var sb = new System.Text.StringBuilder();
28+
sb.AppendLine("// <auto-generated/>");
29+
sb.AppendLine("using System.Net;");
30+
sb.AppendLine();
31+
sb.AppendLine("namespace OpenShock.Common.Utils;");
32+
sb.AppendLine();
33+
sb.AppendLine("public static partial class TrustedProxiesFetcher");
34+
sb.AppendLine("{");
35+
sb.AppendLine(" private static readonly IPNetwork[] CloudflareNetworks =");
36+
sb.AppendLine(" [");
37+
foreach (var line in lines)
38+
sb.AppendLine($" IPNetwork.Parse(\"{line.Trim()}\"),");
39+
sb.AppendLine(" ];");
40+
sb.AppendLine("}");
41+
42+
File.WriteAllText(OutputFile, sb.ToString());
43+
]]></Code>
44+
</Task>
45+
</UsingTask>
46+
47+
<Target Name="FetchCloudflareIPs" BeforeTargets="PrepareForBuild">
48+
<MakeDir Directories="$(IntermediateOutputPath)" />
49+
<DownloadFile SourceUrl="https://www.cloudflare.com/ips-v4"
50+
DestinationFolder="$(IntermediateOutputPath)"
51+
DestinationFileName="cf-v4.txt" />
52+
<DownloadFile SourceUrl="https://www.cloudflare.com/ips-v6"
53+
DestinationFolder="$(IntermediateOutputPath)"
54+
DestinationFileName="cf-v6.txt" />
55+
<GenerateCloudflareIPsSource IPv4File="$(IntermediateOutputPath)cf-v4.txt"
56+
IPv6File="$(IntermediateOutputPath)cf-v6.txt"
57+
OutputFile="$(MSBuildProjectDirectory)/Utils/CloudflareNetworks.g.cs" />
58+
</Target>
1359
</Project>

Common/Utils/HashingUtils.cs

Lines changed: 10 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
using System.Buffers;
2-
using System.Diagnostics;
1+
using System.Diagnostics;
32
using System.Security.Cryptography;
43
using System.Text;
54
using BCrypt.Net;
@@ -19,38 +18,24 @@ public static class HashingUtils
1918
private static readonly VerifyHashResult VerifyHashFailureResult = new(false, false);
2019

2120
/// <summary>
22-
/// Hashes string using SHA-256 and returns the result as a uppercase string
21+
/// Hashes string using SHA-256 and returns the result as a lowercase string
2322
/// </summary>
2423
/// <param name="str"></param>
2524
/// <returns></returns>
2625
public static string HashSha256(string str)
2726
{
2827
Span<byte> hashDigest = stackalloc byte[SHA256.HashSizeInBytes];
2928

30-
const int maxStackSize = 256; // Threshold for stack allocation
31-
int byteCount = Encoding.UTF8.GetByteCount(str);
29+
var nAlloc = str.Length <= 512
30+
? Encoding.UTF8.GetMaxByteCount(str.Length)
31+
: Encoding.UTF8.GetByteCount(str);
32+
var buffer = nAlloc <= 256
33+
? stackalloc byte[nAlloc]
34+
: new byte[nAlloc];
3235

33-
if (byteCount > maxStackSize)
34-
{
35-
byte[] decodedBytes = ArrayPool<byte>.Shared.Rent(byteCount);
36-
37-
try
38-
{
39-
int decodedCount = Encoding.UTF8.GetBytes(str, decodedBytes);
40-
SHA256.HashData(decodedBytes.AsSpan(0, decodedCount), hashDigest);
41-
}
42-
finally
43-
{
44-
ArrayPool<byte>.Shared.Return(decodedBytes, true);
45-
}
46-
}
47-
else
48-
{
49-
Span<byte> decodedBytes = stackalloc byte[maxStackSize];
50-
int decodedCount = Encoding.UTF8.GetBytes(str, decodedBytes);
51-
SHA256.HashData(decodedBytes[..decodedCount], hashDigest);
52-
}
36+
var byteCount = Encoding.UTF8.GetBytes(str, buffer);
5337

38+
SHA256.HashData(buffer[..byteCount], hashDigest);
5439

5540
return Convert.ToHexStringLower(hashDigest);
5641
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
using System.Net;
2+
3+
namespace OpenShock.Common.Utils;
4+
5+
public static partial class TrustedProxiesFetcher
6+
{
7+
private static readonly HttpClient Client = new();
8+
9+
public static readonly string[] PrivateNetworks =
10+
[
11+
// Loopback
12+
"127.0.0.0/8",
13+
"::1/128",
14+
"::ffff:127.0.0.0/8",
15+
16+
// Private IPv4
17+
"10.0.0.0/8",
18+
"172.16.0.0/12",
19+
"192.168.0.0/16",
20+
21+
// Private IPv6
22+
"fc00::/7",
23+
"fe80::/10",
24+
];
25+
26+
private static readonly IPNetwork[] PrivateNetworksParsed = [.. PrivateNetworks.Select(IPNetwork.Parse)];
27+
28+
private static readonly char[] NewLineSeperators = ['\r', '\n', '\t'];
29+
30+
private static async Task<IReadOnlyList<IPNetwork>> FetchCloudflareIPs(Uri uri, CancellationToken ct)
31+
{
32+
using var response = await Client.GetAsync(uri, ct);
33+
var stringResponse = await response.Content.ReadAsStringAsync(ct);
34+
35+
return ParseNetworks(stringResponse);
36+
}
37+
38+
private static IPNetwork[] ParseNetworks(string response)
39+
{
40+
var lines = response.Split(NewLineSeperators, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
41+
var networks = new IPNetwork[lines.Length];
42+
43+
for (int i = 0; i < lines.Length; i++)
44+
{
45+
networks[i] = IPNetwork.Parse(lines[i]);
46+
}
47+
48+
return networks;
49+
}
50+
51+
private static async Task<IPNetwork[]?> FetchCloudflareIPs()
52+
{
53+
try
54+
{
55+
using CancellationTokenSource cts = new(TimeSpan.FromSeconds(5)); // Don't want to make application startup slow
56+
var ct = cts.Token;
57+
58+
var v4Task = FetchCloudflareIPs(new Uri("https://www.cloudflare.com/ips-v4"), ct);
59+
var v6Task = FetchCloudflareIPs(new Uri("https://www.cloudflare.com/ips-v6"), ct);
60+
61+
await Task.WhenAll(v4Task, v6Task);
62+
63+
return [.. v4Task.Result, .. v6Task.Result];
64+
}
65+
catch (Exception)
66+
{
67+
return null;
68+
}
69+
}
70+
71+
public static async Task<IPNetwork[]> GetTrustedNetworksAsync(bool fetch = true)
72+
{
73+
IPNetwork[]? cfProxies = null;
74+
75+
if (fetch)
76+
{
77+
cfProxies = await FetchCloudflareIPs();
78+
}
79+
80+
cfProxies ??= CloudflareNetworks;
81+
82+
return [.. PrivateNetworksParsed, .. cfProxies];
83+
}
84+
}

DynamicLinq/Query/DBExpressionBuilder.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ public static Expression<Func<T, bool>> GetFilterExpression<T>(string filterQuer
9595

9696
var completeExpr = ParseFilters(filterQuery)
9797
.Select(filter => CreateMemberCompareExpression(entityType, parameterExpr, filter.MemberName, filter.Operation, filter.Value))
98-
.Aggregate<Expression, Expression?>(null, (prev, next) => prev == null ? next : Expression.And(prev, next));
98+
.Aggregate<Expression, Expression?>(null, (prev, next) => prev is null ? next : Expression.And(prev, next));
9999

100100
return Expression.Lambda<Func<T, bool>>(completeExpr ?? Expression.Constant(true), parameterExpr);
101101
}

DynamicLinq/Query/DBExpressionBuilderUtils.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ public static class DBExpressionBuilderUtils
2424
public static (MemberInfo, Type) GetPropertyOrField(Type type, string propOrFieldName)
2525
{
2626
var memberInfo = type.GetMember(propOrFieldName, BindingFlags.Public | BindingFlags.Instance | BindingFlags.GetProperty | BindingFlags.GetField | BindingFlags.IgnoreCase).SingleOrDefault();
27-
if (memberInfo == null)
27+
if (memberInfo is null)
2828
throw new DBExpressionBuilderException($"'{propOrFieldName}' is not a valid property of type {type.Name}");
2929

3030
var isIgnored = memberInfo.GetCustomAttributes(typeof(IgnoreDataMemberAttribute), true).Any();

DynamicLinq/Query/OrderByQueryBuilder.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@ private static IOrderedQueryable<T> ThenBy<T>(this IOrderedQueryable<T> source,
5858

5959
public static IOrderedQueryable<T> ApplyOrderBy<T>(this IQueryable<T> query, string orderbyQuery) where T : class
6060
{
61+
ArgumentNullException.ThrowIfNull(query);
62+
ArgumentException.ThrowIfNullOrWhiteSpace(orderbyQuery);
63+
6164
var parts = orderbyQuery.Split(',');
6265

6366
var parsed = ParseOrderByPart(parts[0]);

0 commit comments

Comments
 (0)