Skip to content

Commit f515503

Browse files
committed
release v1.3.0
1 parent ef9cd4d commit f515503

49 files changed

Lines changed: 1575 additions & 57 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎Cargo.lock‎

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Cargo.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ members = [
88
]
99

1010
[workspace.package]
11-
version = "1.2.6"
11+
version = "1.3.0"
1212
edition = "2024"
1313

1414
[workspace.dependencies]
Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
1+
using System.Text.Json.Nodes;
2+
3+
namespace FusionFramework;
4+
5+
/// <summary>Built-in security / CORS / cache / request-id middleware (settings-driven).</summary>
6+
public static class BuiltinMiddleware
7+
{
8+
public static IReadOnlyList<FusionMiddleware> FromSettings(FusionSettings settings)
9+
{
10+
var outList = new List<FusionMiddleware>();
11+
var root = AsObject(settings.Get("middleware", new { }));
12+
13+
if (Enabled(root, settings, "security", defaultEnabled: true, out var securityCfg))
14+
outList.Add(SecurityHeaders(securityCfg));
15+
if (Enabled(root, settings, "cors", defaultEnabled: false, out var corsCfg))
16+
outList.Add(Cors(corsCfg));
17+
if (Enabled(root, settings, "cache", defaultEnabled: false, out var cacheCfg))
18+
outList.Add(CacheHeaders(cacheCfg));
19+
if (Enabled(root, settings, "request_id", defaultEnabled: true, out var ridCfg))
20+
outList.Add(RequestId(ridCfg));
21+
22+
return outList;
23+
}
24+
25+
public static FusionMiddleware SecurityHeaders(JsonObject? config = null)
26+
{
27+
var cfg = config ?? new JsonObject();
28+
var headers = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
29+
{
30+
["X-Content-Type-Options"] = Str(cfg, "content_type_options", "x_content_type_options") ?? "nosniff",
31+
["X-Frame-Options"] = Str(cfg, "frame_options", "x_frame_options") ?? "DENY",
32+
["Referrer-Policy"] = Str(cfg, "referrer_policy") ?? "strict-origin-when-cross-origin",
33+
["Permissions-Policy"] = Str(cfg, "permissions_policy")
34+
?? "camera=(), microphone=(), geolocation=(), payment=()",
35+
["X-XSS-Protection"] = Str(cfg, "xss_protection") ?? "0",
36+
["Cross-Origin-Opener-Policy"] = Str(cfg, "coop") ?? "same-origin",
37+
["Cross-Origin-Resource-Policy"] = Str(cfg, "corp") ?? "same-origin",
38+
};
39+
40+
var csp = Str(cfg, "csp", "content_security_policy");
41+
if (!string.IsNullOrEmpty(csp))
42+
headers["Content-Security-Policy"] = csp;
43+
44+
var hsts = cfg["hsts"] as JsonObject ?? new JsonObject();
45+
if (Truthy(hsts["enabled"], false))
46+
{
47+
var maxAge = hsts["max_age"]?.GetValue<long?>() ?? 31536000;
48+
var parts = new List<string> { $"max-age={maxAge}" };
49+
if (Truthy(hsts["include_subdomains"], true)) parts.Add("includeSubDomains");
50+
if (Truthy(hsts["preload"], false)) parts.Add("preload");
51+
headers["Strict-Transport-Security"] = string.Join("; ", parts);
52+
}
53+
54+
if (cfg["headers"] is JsonObject overrides)
55+
{
56+
foreach (var kv in overrides)
57+
{
58+
if (kv.Value is null || kv.Value.GetValueKind() == System.Text.Json.JsonValueKind.Null)
59+
headers.Remove(kv.Key);
60+
else
61+
headers[kv.Key] = kv.Value.ToString();
62+
}
63+
}
64+
65+
return (request, callNext) =>
66+
{
67+
var result = Middleware.ResolveAwaitable(callNext(request));
68+
return Middleware.MergeResponseHeaders(result, headers);
69+
};
70+
}
71+
72+
public static FusionMiddleware Cors(JsonObject? config = null)
73+
{
74+
var cfg = config ?? new JsonObject();
75+
var allowOrigins = AsList(cfg["allow_origins"] ?? cfg["origins"], "*");
76+
var allowMethods = AsList(
77+
cfg["allow_methods"],
78+
"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD");
79+
var allowHeaders = AsList(
80+
cfg["allow_headers"],
81+
"Authorization", "Content-Type", "Accept", "Origin", "X-Request-Id");
82+
var exposeHeaders = AsList(cfg["expose_headers"], "X-Request-Id");
83+
var allowCredentials = Truthy(cfg["allow_credentials"], false);
84+
var maxAge = cfg["max_age"]?.GetValue<long?>() ?? 600;
85+
86+
Dictionary<string, string>? Build(string? origin)
87+
{
88+
if (allowOrigins.Count == 0) return null;
89+
var wildcard = allowOrigins.Contains("*");
90+
string allow;
91+
if (!string.IsNullOrEmpty(origin) && (wildcard || allowOrigins.Contains(origin)))
92+
allow = wildcard && !allowCredentials ? "*" : origin!;
93+
else if (wildcard && !allowCredentials)
94+
allow = "*";
95+
else
96+
return null;
97+
98+
var headers = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
99+
{
100+
["Access-Control-Allow-Origin"] = allow,
101+
["Access-Control-Allow-Methods"] = string.Join(", ", allowMethods),
102+
["Access-Control-Allow-Headers"] = string.Join(", ", allowHeaders),
103+
["Access-Control-Max-Age"] = maxAge.ToString(),
104+
["Vary"] = "Origin",
105+
};
106+
if (exposeHeaders.Count > 0)
107+
headers["Access-Control-Expose-Headers"] = string.Join(", ", exposeHeaders);
108+
if (allowCredentials)
109+
headers["Access-Control-Allow-Credentials"] = "true";
110+
return headers;
111+
}
112+
113+
return (request, callNext) =>
114+
{
115+
request.Headers.TryGetValue("Origin", out var origin);
116+
if (string.IsNullOrEmpty(origin))
117+
{
118+
foreach (var kv in request.Headers)
119+
{
120+
if (kv.Key.Equals("Origin", StringComparison.OrdinalIgnoreCase))
121+
{
122+
origin = kv.Value;
123+
break;
124+
}
125+
}
126+
}
127+
128+
var corsHeaders = Build(origin);
129+
if (request.Method.Equals("OPTIONS", StringComparison.OrdinalIgnoreCase))
130+
{
131+
if (corsHeaders is null)
132+
{
133+
return new Dictionary<string, object?>
134+
{
135+
["status"] = 403,
136+
["body"] = new Dictionary<string, object?> { ["detail"] = "CORS origin not allowed" },
137+
};
138+
}
139+
return new Dictionary<string, object?>
140+
{
141+
["status"] = 204,
142+
["body"] = "",
143+
["headers"] = corsHeaders,
144+
};
145+
}
146+
147+
var result = Middleware.ResolveAwaitable(callNext(request));
148+
return corsHeaders is null ? result : Middleware.MergeResponseHeaders(result, corsHeaders);
149+
};
150+
}
151+
152+
public static FusionMiddleware CacheHeaders(JsonObject? config = null)
153+
{
154+
var cfg = config ?? new JsonObject();
155+
var value = Str(cfg, "default", "cache_control") ?? "no-store";
156+
var extra = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
157+
{
158+
["Cache-Control"] = value,
159+
};
160+
var pragma = Str(cfg, "pragma");
161+
if (!string.IsNullOrEmpty(pragma))
162+
extra["Pragma"] = pragma!;
163+
else if (value == "no-store")
164+
extra["Pragma"] = "no-cache";
165+
166+
return (request, callNext) =>
167+
{
168+
var result = Middleware.ResolveAwaitable(callNext(request));
169+
return Middleware.MergeResponseHeaders(result, extra);
170+
};
171+
}
172+
173+
public static FusionMiddleware RequestId(JsonObject? config = null)
174+
{
175+
var cfg = config ?? new JsonObject();
176+
var headerName = Str(cfg, "header") ?? "X-Request-Id";
177+
var acceptIncoming = Truthy(cfg["incoming"], true);
178+
var stateKey = Str(cfg, "state_key") ?? "request_id";
179+
180+
return (request, callNext) =>
181+
{
182+
string? rid = null;
183+
if (acceptIncoming)
184+
{
185+
foreach (var kv in request.Headers)
186+
{
187+
if (kv.Key.Equals(headerName, StringComparison.OrdinalIgnoreCase))
188+
{
189+
rid = kv.Value;
190+
break;
191+
}
192+
}
193+
}
194+
rid ??= Guid.NewGuid().ToString("N");
195+
Middleware.EnsureState(request)[stateKey] = JsonValue.Create(rid);
196+
197+
var result = Middleware.ResolveAwaitable(callNext(request));
198+
return Middleware.MergeResponseHeaders(result, new Dictionary<string, string>
199+
{
200+
[headerName] = rid,
201+
});
202+
};
203+
}
204+
205+
static bool Enabled(
206+
JsonObject root,
207+
FusionSettings settings,
208+
string name,
209+
bool defaultEnabled,
210+
out JsonObject cfg)
211+
{
212+
cfg = root[name] as JsonObject
213+
?? AsObject(settings.Get(name, new { }))
214+
?? new JsonObject();
215+
if (cfg.ContainsKey("enabled"))
216+
return Truthy(cfg["enabled"], defaultEnabled);
217+
return defaultEnabled;
218+
}
219+
220+
static JsonObject AsObject(JsonNode? value) =>
221+
value as JsonObject ?? new JsonObject();
222+
223+
static string? Str(JsonObject cfg, params string[] keys)
224+
{
225+
foreach (var key in keys)
226+
{
227+
var node = cfg[key];
228+
if (node is null || node.GetValueKind() == System.Text.Json.JsonValueKind.Null)
229+
continue;
230+
var s = node.ToString();
231+
if (!string.IsNullOrWhiteSpace(s)) return s;
232+
}
233+
return null;
234+
}
235+
236+
static bool Truthy(JsonNode? value, bool defaultValue)
237+
{
238+
if (value is null || value.GetValueKind() == System.Text.Json.JsonValueKind.Null)
239+
return defaultValue;
240+
if (value.GetValueKind() == System.Text.Json.JsonValueKind.True) return true;
241+
if (value.GetValueKind() == System.Text.Json.JsonValueKind.False) return false;
242+
if (value is JsonValue v)
243+
{
244+
if (v.TryGetValue<bool>(out var b)) return b;
245+
if (v.TryGetValue<int>(out var n)) return n != 0;
246+
if (v.TryGetValue<string>(out var s))
247+
{
248+
return s.ToLowerInvariant() switch
249+
{
250+
"false" or "0" or "off" or "no" => false,
251+
"true" or "1" or "on" or "yes" => true,
252+
_ => !string.IsNullOrWhiteSpace(s),
253+
};
254+
}
255+
}
256+
return true;
257+
}
258+
259+
static List<string> AsList(JsonNode? value, params string[] fallback)
260+
{
261+
if (value is JsonArray arr)
262+
return arr.Select(n => n?.ToString() ?? "").Where(s => s.Length > 0).ToList();
263+
if (value is JsonValue v && v.TryGetValue<string>(out var s) && !string.IsNullOrWhiteSpace(s))
264+
return s.Split(',').Select(p => p.Trim()).Where(p => p.Length > 0).ToList();
265+
return fallback.ToList();
266+
}
267+
}

‎bindings/csharp/FusionFramework/FusionApp.cs‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,6 @@ public FusionApp(FusionSettings? settings = null)
2020

2121
settings ??= SettingsStore.Current;
2222
Native.fusion_app_set_settings(_app, settings.Handle);
23-
// Default: advertise Fusion to clients / Wappalyzer-style detectors.
24-
_middleware.Add(Middleware.FrameworkHeaders());
2523
}
2624

2725
public FusionApp Use(FusionMiddleware middleware)
@@ -90,6 +88,7 @@ public void Mount()
9088

9189
// Final guard: never serialize a raw Task as the response body.
9290
result = Middleware.ResolveAwaitable(result);
91+
result = HeaderOps.ApplyMethodHeaderOps(s.Handler, result);
9392
var json = JsonUtil.SerializeResponse(result);
9493
return Native.fusion_string_dup(json);
9594
}

‎bindings/csharp/FusionFramework/FusionFramework.csproj‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
<RootNamespace>FusionFramework</RootNamespace>
88
<AssemblyName>FusionFramework</AssemblyName>
99
<PackageId>Fusion-Framework</PackageId>
10-
<Version>1.2.6</Version>
10+
<Version>1.3.0</Version>
1111
<Authors>CipherUnits</Authors>
1212
<Company>CipherUnits</Company>
1313
<Description>Fusion Framework managed bindings (C#) over fusion-ffi / fusion-core</Description>

‎bindings/csharp/FusionFramework/Header.cs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ public static Dictionary<string, string> Fingerprint()
9090
{
9191
["X-Powered-By"] = "Fusion Framework",
9292
["X-Framework"] = "Fusion",
93-
["X-Fusion-Version"] = "1.2.6",
93+
["X-Fusion-Version"] = "1.3.0",
9494
};
9595
}
9696
}

0 commit comments

Comments
 (0)