-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathRagnorokClientExtensions.cs
254 lines (212 loc) · 12 KB
/
RagnorokClientExtensions.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
using Ragnarok.AgentApi.Exceptions;
using Ragnarok.AgentApi.Models;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace Ragnarok.AgentApi.Extensions
{
/// <summary>
/// Extension methods for <see cref="RagnarokClient"/>
/// </summary>
public static class RagnorokClientExtensions
{
/// <summary>
/// Start ngrok and open a tunnel
/// </summary>
/// <param name="client">An instance of <see cref="RagnarokClient"/></param>
/// <param name="authToken">Authorization token to register in ngrok.yml</param>
/// <param name="cancellationToken">Propagates notification that operations should be canceled</param>
/// <returns><see cref="TunnelDetail"/></returns>
/// <remarks>
/// <see cref="RagnarokClient.InitializeAsync"/> will be called if not previously executed
/// </remarks>
public static async Task<TunnelDetail> ConnectAsync(this RagnarokClient client, string authToken = null, CancellationToken cancellationToken = default)
=> await ConnectAsync(client, new TunnelDefinition(), authToken, cancellationToken);
/// <summary>
/// Start ngrok and open a tunnel
/// </summary>
/// <param name="client">An instance of <see cref="RagnarokClient"/></param>
/// <param name="port">Local port number to forward traffic</param>
/// <param name="authToken">Authorization token to register in ngrok.yml</param>
/// <param name="cancellationToken">Propagates notification that operations should be canceled</param>
/// <returns><see cref="TunnelDetail"/></returns>
/// <remarks>
/// <see cref="RagnarokClient.InitializeAsync"/> will be called if not previously executed
/// </remarks>
public static async Task<TunnelDetail> ConnectAsync(this RagnarokClient client, int port, string authToken = null, CancellationToken cancellationToken = default)
=> await ConnectAsync(client, option => option.Address = port.ToString(), authToken, cancellationToken);
/// <summary>
/// Start ngrok and open a tunnel
/// </summary>
/// <param name="client">An instance of <see cref="RagnarokClient"/></param>
/// <param name="tunnelName">Named tunnel configured in ngrok.yml</param>
/// <param name="authToken">Authorization token to register in ngrok.yml</param>
/// <param name="cancellationToken">Propagates notification that operations should be canceled</param>
/// <returns><see cref="TunnelDetail"/></returns>
/// <remarks>
/// The name provided must exist in the ngrok configuration file. <br/>
/// <see cref="RagnarokClient.InitializeAsync"/> will be called if not previously executed
/// </remarks>
public static async Task<TunnelDetail> ConnectAsync(this RagnarokClient client, string tunnelName,
string authToken = null, CancellationToken cancellationToken = default)
{
if (client.Config.Tunnels is null)
throw new NgrokConfigurationException($"No configured tunnels exist");
var definition = client.Config.Tunnels.FirstOrDefault(x => x.Key.Equals(tunnelName, StringComparison.OrdinalIgnoreCase));
if (definition.Equals(default(KeyValuePair<string, TunnelDefinition>)))
throw new NgrokConfigurationException($"Could not find config for named tunnel {tunnelName}");
definition.Value.Name = definition.Key;
return await ConnectAsync(client, definition.Value, authToken, cancellationToken);
}
/// <summary>
/// Start ngrok and open a tunnel
/// </summary>
/// <param name="client">An instance of <see cref="RagnarokClient"/></param>
/// <param name="details"><see cref="TunnelDetail"/> to replicate</param>
/// <param name="authToken">Authorization token to register in ngrok.yml</param>
/// <param name="cancellationToken">Propagates notification that operations should be canceled</param>
/// <remarks>
/// Creates a new tunnel based on properties defined in the provided <see cref="TunnelDetail"/> <br/>
/// <see cref="RagnarokClient.InitializeAsync"/> will be called if not previously executed
/// </remarks>
public static async Task<TunnelDetail> ConnectAsync(this RagnarokClient client, TunnelDetail details,
string authToken = null, CancellationToken cancellationToken = default)
{
return await ConnectAsync(client, options =>
{
options.Name = details.Name;
options.Protocol = details.Protocol;
options.Address = details.Config.Address;
options.Scheme = details.Protocol == TunnelProtocol.http ? Scheme.http : Scheme.https;
},
authToken: authToken,
cancellationToken: cancellationToken);
}
/// <summary>
/// Start ngrok and open a tunnel
/// </summary>
/// <param name="client">An instance of <see cref="RagnarokClient"/></param>
/// <param name="options">Options used to define the tunnel to be created</param>
/// <param name="authToken">Authorization token to register in ngrok.yml</param>
/// <param name="cancellationToken">Propagates notification that operations should be canceled</param>
/// <returns><see cref="TunnelDetail"/></returns>
/// <remarks>
/// <see cref="RagnarokClient.InitializeAsync"/> will be called if not previously executed
/// </remarks>
public static async Task<TunnelDetail> ConnectAsync(this RagnarokClient client, Action<TunnelDefinition> options,
string authToken = null, CancellationToken cancellationToken = default)
{
var definition = new TunnelDefinition();
options?.Invoke(definition);
return await ConnectAsync(client, definition, authToken, cancellationToken);
}
/// <summary>
/// Start ngrok and open a tunnel
/// </summary>
/// <param name="client">An instance of <see cref="RagnarokClient"/></param>
/// <param name="options">Options used to define the tunnel to be created</param>
/// <param name="authToken">Authorization token to register in ngrok.yml</param>
/// <param name="cancellationToken">Propagates notification that operations should be canceled</param>
/// <returns><see cref="TunnelDetail"/></returns>
/// <remarks>
/// <see cref="RagnarokClient.InitializeAsync"/> will be called if not previously executed
/// </remarks>
public static async Task<TunnelDetail> ConnectAsync(this RagnarokClient client, TunnelDefinition options,
string authToken = null, CancellationToken cancellationToken = default)
{
if (!string.IsNullOrWhiteSpace(authToken)) await client.RegisterAuthTokenAsync(authToken);
if (!client.Ngrok.IsActive) await client.InitializeAsync();
options ??= new TunnelDefinition();
return await client.StartTunnelAsync(options, cancellationToken);
}
/// <summary>
/// Close an open tunnel
/// </summary>
/// <param name="client">An instance of <see cref="RagnarokClient"/></param>
/// <param name="url">The <see cref="TunnelDetail.PublicURL"/> of the tunnel to stop</param>
/// <param name="cancellationToken">Propagates notification that operations should be canceled</param>
/// <remarks>
/// If <paramref name="url"/> is not provided, all tunnels will be stopped
/// </remarks>
public static async Task<bool> DisconnectAsync(this RagnarokClient client, string url = null, CancellationToken cancellationToken = default)
{
if (!client.Ngrok.IsActive) return false;
var tunnels = await client.ListTunnelsAsync(cancellationToken);
if (string.IsNullOrWhiteSpace(url)) return await client.DisconnectAllAsync(tunnels, cancellationToken: cancellationToken);
var detail = tunnels.FirstOrDefault(x => x.PublicURL.Equals(url, StringComparison.OrdinalIgnoreCase));
if (detail == null) return false;
await client.StopTunnelAsync(detail.Name, cancellationToken);
return true;
}
/// <summary>
/// Close all specified open tunnels
/// </summary>
/// <param name="client">An instance of <see cref="RagnarokClient"/></param>
/// <param name="tunnels">List of <see cref="TunnelDetail"/>s to be stopped</param>
/// <param name="cancellationToken">Propagates notification that operations should be canceled</param>
/// <returns></returns>
private static async Task<bool> DisconnectAllAsync(this RagnarokClient client, IEnumerable<TunnelDetail> tunnels, CancellationToken cancellationToken = default)
{
if (!tunnels.Any()) return false;
foreach (var tunnel in tunnels)
await client.StopTunnelAsync(tunnel.Name, cancellationToken);
return true;
}
/// <summary>
/// Add/Modify the authtoken property in the ngrok configuration file
/// </summary>
/// <param name="client">An instance of <see cref="RagnarokClient"/></param>
/// <param name="authToken">ngrok authtoken <see href="https://dashboard.ngrok.com/get-started/your-authtoken"/></param>
/// <param name="throwOnError">Should exceptions be thrown or suppressed</param>
/// <returns></returns>
public static async Task<bool> RegisterAuthTokenAsync(this RagnarokClient client, string authToken, bool throwOnError = true)
{
var processData = new StringBuilder();
var startInfo = new ProcessStartInfo()
{
CreateNoWindow = true,
RedirectStandardError = true,
RedirectStandardOutput = true,
WindowStyle = ProcessWindowStyle.Hidden,
FileName = client.Options.NgrokExecutablePath,
Arguments = $"add-authtoken {authToken} --config={client.Options.NgrokConfigPath}"
};
var process = new Process() { StartInfo = startInfo };
process.ErrorDataReceived += (sender, e) =>
{
if (!string.IsNullOrWhiteSpace(e.Data)) processData.AppendLine(e.Data);
};
process.OutputDataReceived += (sender, e) =>
{
if (!string.IsNullOrWhiteSpace(e.Data)) processData.AppendLine(e.Data);
};
process.Start();
process.BeginErrorReadLine();
process.BeginOutputReadLine();
await process.WaitForExitAsync();
var data = processData.ToString();
if (string.IsNullOrWhiteSpace(data)) return false;
var error = data.Contains("ERROR:");
if (throwOnError && error) throw new Exception(data);
else if (error) return false;
return true;
}
internal static async Task<bool> WaitForEventAsync(this RagnarokClient client, string eventName, TimeSpan timeout)
{
var type = client.GetType();
var eventInfo = type.GetEvent(eventName);
var delay = Task.Delay(timeout);
var promise = new TaskCompletionSource<bool>();
EventHandler handler = (object sender, EventArgs args) => promise.SetResult(true);
eventInfo.AddEventHandler(client, handler);
var tsk = await Task.WhenAny(promise.Task, delay);
if (tsk.Equals(delay)) promise.SetResult(false);
eventInfo.RemoveEventHandler(client, handler);
return await promise.Task;
}
}
}