Skip to content

Commit 0e0d715

Browse files
committed
feat: add initial api
1 parent 1d382c6 commit 0e0d715

9 files changed

Lines changed: 416 additions & 3 deletions

File tree

src/ElectronNET.API/API/Electron.cs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ public static Dock Dock
186186
}
187187

188188
/// <summary>
189-
/// Electeon extensions to the Nodejs process object.
189+
/// Electron extensions to the Nodejs process object.
190190
/// </summary>
191191
public static Process Process
192192
{
@@ -195,5 +195,16 @@ public static Process Process
195195
return Process.Instance;
196196
}
197197
}
198+
199+
/// <summary>
200+
/// Register a custom protocol and intercept existing protocol requests.
201+
/// </summary>
202+
public static Protocol Protocol
203+
{
204+
get
205+
{
206+
return Protocol.Instance;
207+
}
208+
}
198209
}
199210
}
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Text;
4+
using System.Threading.Tasks;
5+
6+
namespace ElectronNET.API
7+
{
8+
9+
public sealed class Protocol : ApiBase
10+
{
11+
protected override SocketTaskEventNameTypes SocketTaskEventNameType => SocketTaskEventNameTypes.DashesLowerFirst;
12+
protected override SocketTaskMessageNameTypes SocketTaskMessageNameType => SocketTaskMessageNameTypes.DashesLowerFirst;
13+
14+
internal Protocol()
15+
{
16+
}
17+
18+
internal static Protocol Instance
19+
{
20+
get
21+
{
22+
if (_protocol == null)
23+
{
24+
lock (_syncRoot)
25+
{
26+
_protocol ??= new Protocol();
27+
}
28+
}
29+
30+
return _protocol;
31+
}
32+
}
33+
34+
private static Protocol _protocol;
35+
36+
private static readonly object _syncRoot = new();
37+
38+
/// <summary>
39+
/// Registers the scheme as standard, secure, bypasses content security policy for resources, allows registering ServiceWorker, supports fetch API, streaming video/audio, and V8 code cache.
40+
/// Specify a privilege with the value of true to enable the capability.
41+
/// </summary>
42+
/// <param name="customSchemes">Custom schemes to be registered with options.</param>
43+
/// <remarks>This method can only be used before the <see cref="App.Ready"/> event of the app module gets emitted and can be called only once.</remarks>
44+
public Task RegisterSchemesAsPrivilegedAsync(params CustomScheme[] customSchemes)
45+
{
46+
var tsc = new TaskCompletionSource();
47+
48+
BridgeConnector.Socket.Once("registerSchemesAsPrivilegedComplete", tsc.SetResult);
49+
BridgeConnector.Socket.Emit("registerSchemesAsPrivileged", customSchemes);
50+
51+
return tsc.Task;
52+
}
53+
54+
/// <summary>
55+
/// Register a protocol handler for scheme. Requests made to URLs with this scheme will delegate to this handler to determine what response should be sent.
56+
/// </summary>
57+
/// <param name="scheme">scheme to handle, for example https or my-app. This is the bit before the : in a URL.</param>
58+
/// <param name="handler">Either a <see cref="Response" /> or a <see cref="Task{Response}"/> can be returned.</param>
59+
public Task HandleAsync(string scheme, Func<Request, Response> handler)
60+
{
61+
if (string.IsNullOrWhiteSpace(scheme))
62+
throw new ArgumentException("Scheme must not be null or empty.", nameof(scheme));
63+
64+
return handler switch
65+
{
66+
null => throw new ArgumentNullException(nameof(handler)),
67+
_ => HandleAsync(scheme, req => Task.FromResult(handler(req)))
68+
};
69+
}
70+
71+
public Task HandleAsync(string scheme, Func<Request, Task<Response>> handler)
72+
{
73+
if (string.IsNullOrWhiteSpace(scheme))
74+
throw new ArgumentException("Scheme must not be null or empty.", nameof(scheme));
75+
76+
if (handler == null)
77+
throw new ArgumentNullException(nameof(handler));
78+
79+
// Tell TS to register protocol.handle for this scheme.
80+
BridgeConnector.Socket.Emit("protocol-handle-register", new
81+
{
82+
scheme
83+
});
84+
85+
// Listen for incoming requests from TS
86+
// Note: If your BridgeConnector has generic On<T>, use that instead of object.
87+
BridgeConnector.Socket.On<Request>("protocol-handle-request", async (request) =>
88+
{
89+
try
90+
{
91+
if (request == null || !string.Equals(request.Scheme, scheme, StringComparison.OrdinalIgnoreCase))
92+
return; // Not our scheme, ignore.
93+
94+
var response = await handler(request).ConfigureAwait(false) ?? new Response
95+
{
96+
Status = 204
97+
};
98+
99+
// Ensure headers dictionary exists
100+
response.Headers ??= new();
101+
102+
// Push ContentType also as header, for TS convenience
103+
if (!string.IsNullOrEmpty(response.ContentType))
104+
{
105+
response.Headers["content-type"] = new[] { response.ContentType! };
106+
}
107+
108+
BridgeConnector.Socket.Emit("protocol-handle-response", new
109+
{
110+
id = request.Id,
111+
status = response.Status,
112+
headers = response.Headers,
113+
body = response.Body != null
114+
? Convert.ToBase64String(response.Body)
115+
: null
116+
});
117+
}
118+
catch (Exception ex)
119+
{
120+
// In case of error, send a 500 back
121+
var errorBody = Encoding.UTF8.GetBytes("Protocol handler error:\n" + ex);
122+
123+
BridgeConnector.Socket.Emit("protocol-handle-response", new
124+
{
125+
id = request.Id,
126+
status = 500,
127+
headers = new
128+
{
129+
// minimal header set
130+
contentType = new[] { "text/plain; charset=utf-8" }
131+
},
132+
body = Convert.ToBase64String(errorBody)
133+
});
134+
}
135+
});
136+
137+
// There’s nothing async to wait for here: registration happens on JS side.
138+
return Task.CompletedTask;
139+
}
140+
}
141+
142+
public sealed class Request
143+
{
144+
public string Id { get; set; } = default!;
145+
public string Scheme { get; set; } = default!;
146+
public string Url { get; set; } = default!;
147+
public string Method { get; set; } = "GET";
148+
149+
// Case-insensitive header name, multiple values per header.
150+
public Dictionary<string, string[]> Headers { get; set; } = new(StringComparer.OrdinalIgnoreCase);
151+
152+
/// <summary>
153+
/// Request body as raw bytes, or null if none.
154+
/// </summary>
155+
public byte[] Body { get; set; }
156+
}
157+
158+
public sealed class Response
159+
{
160+
/// <summary>
161+
/// HTTP-like status code. Default 200.
162+
/// </summary>
163+
public int Status { get; set; } = 200;
164+
165+
/// <summary>
166+
/// Content-Type header value; convenience property.
167+
/// </summary>
168+
public string ContentType { get; set; }
169+
170+
/// <summary>
171+
/// Response headers (without Content-Type).
172+
/// </summary>
173+
public Dictionary<string, string[]> Headers { get; set; } = new(StringComparer.OrdinalIgnoreCase);
174+
175+
/// <summary>
176+
/// Raw response body (may be null for no body).
177+
/// </summary>
178+
public byte[] Body { get; set; }
179+
}
180+
181+
public sealed class CustomScheme
182+
{
183+
public string Scheme { get; set; }
184+
public CustomSchemePrivileges Privileges { get; set; }
185+
186+
}
187+
public sealed class CustomSchemePrivileges
188+
{
189+
public bool? Standard { get; set; }
190+
public bool? Secure { get; set; }
191+
public bool? BypassCSP { get; set; }
192+
public bool? AllowServiceWorkers { get; set; }
193+
public bool? SupportFetchAPI { get; set; }
194+
public bool? CorsEnabled { get; set; }
195+
public bool? Stream { get; set; }
196+
public bool? CodeCache { get; set; }
197+
}
198+
199+
}

src/ElectronNET.Host/api/protocol.js

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

src/ElectronNET.Host/api/protocol.js.map

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

0 commit comments

Comments
 (0)