18using System.Collections.Generic;
24using System.Text.Json;
25using System.Threading.Tasks;
27using System.Windows.Forms;
28using System.Xml.Serialization;
31using YamlDotNet.Serialization;
36 internal class RESTHelper
38 internal int StatusCode {
get;
set; }
40 private RESTClient _restClient;
44 private HttpClient _httpClient {
get;
set; } =
new HttpClient(
new HttpClientHandler { UseCookies =
false });
46 private readonly
string _browserExtensionApiBase;
47 internal static Dictionary<ApiEndpoint, string> Endpoints {
get;
set; } =
new Dictionary<ApiEndpoint, string>
49 [ApiEndpoint.Back] =
"back",
50 [ApiEndpoint.CaptureVisibleTab] =
"capture-visible-tab",
51 [ApiEndpoint.CastDesktop] =
"cast-desktop",
52 [ApiEndpoint.CastTab] =
"cast-tab",
53 [ApiEndpoint.CheckNetworkIdle] =
"check-network-idle",
54 [ApiEndpoint.CheckStatus] =
"status",
55 [ApiEndpoint.ClearReferrer] =
"clear-referrer",
56 [ApiEndpoint.CloseBrowser] =
"close-browser",
57 [ApiEndpoint.CloseTab] =
"close-tab",
58 [ApiEndpoint.DeleteStorage] =
"delete-storage",
59 [ApiEndpoint.DragAndDrop] =
"drag-and-drop",
60 [ApiEndpoint.Evaluate] =
"evaluate",
61 [ApiEndpoint.EvaluateAll] =
"evaluate-all",
62 [ApiEndpoint.EvaluatePersistent] =
"evaluate-persistent",
63 [ApiEndpoint.EvaluateAllPersistent] =
"evaluate-all-persistent",
64 [ApiEndpoint.ExecuteJavaScript] =
"execute-javascript",
65 [ApiEndpoint.Fetch] =
"fetch",
66 [ApiEndpoint.FillInAppend] =
"fill-in-append",
67 [ApiEndpoint.FillInInsert] =
"fill-in-insert",
68 [ApiEndpoint.FillInOverwrite] =
"fill-in-overwrite",
69 [ApiEndpoint.FireChangeEvent] =
"fire-change-event",
70 [ApiEndpoint.Focus] =
"focus",
71 [ApiEndpoint.Forward] =
"forward",
72 [ApiEndpoint.FullScreen] =
"fullscreen",
73 [ApiEndpoint.GetAttribute] =
"get-attribute",
74 [ApiEndpoint.GetBoundingClientRect] =
"get-bounding-client-rect",
75 [ApiEndpoint.ElementFromPoint] =
"element-from-point",
76 [ApiEndpoint.GetContentAndCss] =
"get-content-and-css",
77 [ApiEndpoint.GetCssAttributes] =
"get-css-attributes",
78 [ApiEndpoint.GetCurrentUrl] =
"get-current-url",
79 [ApiEndpoint.GetDomAttributes] =
"get-dom-attributes",
80 [ApiEndpoint.GetDomProperties] =
"get-dom-properties",
81 [ApiEndpoint.GetElementAttributeHash] =
"get-element-attribute-hash",
82 [ApiEndpoint.GetPageSource] =
"get-page-source",
83 [ApiEndpoint.GetParentNode] =
"get-parent-node",
84 [ApiEndpoint.GetReadyStatus] =
"get-ready-status",
85 [ApiEndpoint.GetShadowRoot] =
"get-shadow-root",
86 [ApiEndpoint.GetStorage] =
"get-storage",
87 [ApiEndpoint.GetLanguages] =
"get-languages",
88 [ApiEndpoint.CaptureCalls] =
"capture-calls",
89 [ApiEndpoint.GetCapturedCalls] =
"get-captured-calls",
90 [ApiEndpoint.GetUserAgent] =
"get-useragent",
91 [ApiEndpoint.GoTo] =
"goto",
92 [ApiEndpoint.GoToTab] =
"goto-tab",
93 [ApiEndpoint.GetWindowRectangle] =
"get-window-rectangle",
94 [ApiEndpoint.HideElement] =
"hide-element",
95 [ApiEndpoint.Hover] =
"hover",
96 [ApiEndpoint.InjectScript] =
"inject-script",
97 [ApiEndpoint.ClearInjectedScripts] =
"clear-injected-scripts",
98 [ApiEndpoint.IsClickable] =
"is-clickable",
99 [ApiEndpoint.IsDisplayed] =
"is-displayed",
100 [ApiEndpoint.IsEnabled] =
"is-enabled",
101 [ApiEndpoint.IsEndOfPage] =
"is-end-of-page",
102 [ApiEndpoint.IsVisibleInViewport] =
"is-visible-in-viewport",
103 [ApiEndpoint.LeftClick] =
"left-click",
104 [ApiEndpoint.LeftClickAndDownload] =
"left-click-download",
105 [ApiEndpoint.LeftDoubleClick] =
"left-double-click",
106 [ApiEndpoint.Maximize] =
"maximize",
107 [ApiEndpoint.MiddleClick] =
"middle-click",
108 [ApiEndpoint.Minimize] =
"minimize",
109 [ApiEndpoint.MoveTo] =
"move-to",
110 [ApiEndpoint.NewTab] =
"new-tab",
111 [ApiEndpoint.NextTab] =
"next-tab",
112 [ApiEndpoint.NextWindow] =
"next-window",
113 [ApiEndpoint.Normal] =
"normal",
114 [ApiEndpoint.OverrideReferrer] =
"override-referrer",
115 [ApiEndpoint.PageDown] =
"page-down",
116 [ApiEndpoint.PageEnd] =
"page-end",
117 [ApiEndpoint.PageTop] =
"page-top",
118 [ApiEndpoint.PageUp] =
"page-up",
119 [ApiEndpoint.PressModifierKey] =
"press-modifier-key",
120 [ApiEndpoint.PreviousTab] =
"previous-tab",
121 [ApiEndpoint.PreviousWindow] =
"previous-window",
122 [ApiEndpoint.QuerySelector] =
"query-selector",
123 [ApiEndpoint.QuerySelectors] =
"query-selectors",
124 [ApiEndpoint.QueryPersistentSelector] =
"query-persistent-selector",
125 [ApiEndpoint.QueryPersistentSelectors] =
"query-persistent-selectors",
126 [ApiEndpoint.Refresh] =
"refresh",
127 [ApiEndpoint.ReleaseModifierKey] =
"release-modifier-key",
128 [ApiEndpoint.Restore] =
"restore",
129 [ApiEndpoint.RightClick] =
"right-click",
130 [ApiEndpoint.RightClickAndDownload] =
"right-click-download",
131 [ApiEndpoint.ScrollElement] =
"scroll-element",
132 [ApiEndpoint.ScrollIntoView] =
"scroll-into-view",
133 [ApiEndpoint.ScrollWindowByHorizontal] =
"scroll-window",
134 [ApiEndpoint.ScrollWindowByVertical] =
"scroll-window",
135 [ApiEndpoint.ScrollWindow] =
"scroll-window",
136 [ApiEndpoint.SelectClick] =
"select-click",
137 [ApiEndpoint.SendKey] =
"send-key",
138 [ApiEndpoint.SendString] =
"send-string",
139 [ApiEndpoint.SetAttribute] =
"set-attribute",
140 [ApiEndpoint.SetDownloadFilename] =
"set-download-filename",
141 [ApiEndpoint.SetRange] =
"set-range",
142 [ApiEndpoint.SetStorage] =
"set-storage",
143 [ApiEndpoint.SetUserAgent] =
"set-useragent",
144 [ApiEndpoint.SetValueFromElement] =
"set-value-from-element",
145 [ApiEndpoint.StealthOverrideReferrer] =
"stealth-override-referrer",
146 [ApiEndpoint.StopCasting] =
"stop-casting",
147 [ApiEndpoint.SubmitForm] =
"submit-form",
148 [ApiEndpoint.SwitchToDefaultContent] =
"switch-to-default-content",
149 [ApiEndpoint.SwitchToElement] =
"switch-to-element",
150 [ApiEndpoint.SwitchToFrame] =
"switch-to-frame",
151 [ApiEndpoint.SwitchToShadowRoot] =
"switch-to-shadow-root",
152 [ApiEndpoint.TabCount] =
"tab-count",
153 [ApiEndpoint.TopBrowser] =
"top-browser",
154 [ApiEndpoint.MoveToPoint] =
"move-to-point",
155 [ApiEndpoint.ClickPoint] =
"click-point",
156 [ApiEndpoint.TrustedLeftClick] =
"trusted-left-click",
157 [ApiEndpoint.Upload] =
"upload",
158 [ApiEndpoint.WindowInnerHeight] =
"window-inner-height",
159 [ApiEndpoint.WindowInnerWidth] =
"window-inner-width",
160 [ApiEndpoint.WindowOuterHeight] =
"window-outer-height",
161 [ApiEndpoint.WindowOuterWidth] =
"window-outer-width",
162 [ApiEndpoint.WindowPageOffsetX] =
"window-page-xoffset",
163 [ApiEndpoint.WindowPageOffsetY] =
"window-page-yoffset",
164 [ApiEndpoint.WindowScreenLeft] =
"window-screen-left",
165 [ApiEndpoint.WindowScreenTop] =
"window-screen-top",
166 [ApiEndpoint.OpenWindow] =
"open-window",
167 [ApiEndpoint.GoToWindow] =
"goto-window",
168 [ApiEndpoint.CloseWindow] =
"close-window",
169 [ApiEndpoint.GetCurrentWindow] =
"get-current-window",
171 [ApiEndpoint.GetWorkflow] =
"get-workflow",
172 [ApiEndpoint.GetSettings] =
"get-settings",
173 [ApiEndpoint.GetGpalSettings] =
"get-gpal-settings",
174 [ApiEndpoint.GetBrowserSettings] =
"get-browser-settings",
175 #endregion GPAL STUDIO
178 public RESTHelper(
string browserExtensionApiBase, RESTClient client, TimeSpan? httpTimeout =
null)
180 if (browserExtensionApiBase ==
null)
182 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
183 "Browser extension API base cannot be null.",
184 null, GPALObjectType.None);
186 _browserExtensionApiBase = browserExtensionApiBase;
189 _restClient = client;
192 public string GetEndpointUrl(ApiEndpoint endpoint)
194 return $
"{_browserExtensionApiBase}{Endpoints[endpoint]}";
197 internal async Task<string> Call(ApiEndpoint endpoint,
object parameters =
null,
string httpMethod =
"GET", ContentEncoding contentEncoding = ContentEncoding.Json)
202 string url = GetEndpointUrl(endpoint);
204 string result = await InternalCall(url, parameters, httpMethod, contentEncoding).ConfigureAwait(
false);
209 internal async Task<string> Call(
string endpoint,
object parameters =
null,
string httpMethod =
"GET", ContentEncoding contentEncoding = ContentEncoding.Json)
214 string url =
true == endpoint?.StartsWith(
"http", StringComparison.OrdinalIgnoreCase)
216 : $
"{_browserExtensionApiBase}{endpoint}";
218 string result = await InternalCall(url, parameters, httpMethod, contentEncoding).ConfigureAwait(
false);
223 internal async Task<T> CallAndDeserialize<T>(ApiEndpoint endpoint,
object parameters =
null,
string httpMethod =
"GET", ContentEncoding contentEncoding = ContentEncoding.Json)
226 string jsonResponse = await Call(endpoint, parameters, httpMethod, contentEncoding).ConfigureAwait(
false);
227 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $
"[{endpoint}] result: [{jsonResponse}]");
229 return string.IsNullOrEmpty(jsonResponse)
231 : JsonConvert.DeserializeObject<T>(jsonResponse);
240 private static string DecodeJsonString(
string response)
242 string retVal = response;
244 while (
false ==
string.IsNullOrEmpty(retVal)
245 &&
true == retVal.StartsWith(
"\"") &&
true == retVal.EndsWith(
"\""))
249 string peeled = JsonConvert.DeserializeObject<
string>(retVal);
252 if (peeled == retVal)
257 catch (Newtonsoft.Json.JsonException)
266 internal async Task<T> CallAndDeserialize<T>(
string endpoint,
object parameters =
null,
string httpMethod =
"GET", ContentEncoding contentEncoding = ContentEncoding.Json)
268 string jsonResponse = await Call(endpoint, parameters, httpMethod, contentEncoding).ConfigureAwait(
false);
273 jsonResponse = DecodeJsonString(jsonResponse);
275 if (typeof(GPALElement) == typeof(T) || typeof(List<GPALElement>) == typeof(T))
277 var settings =
new JsonSerializerSettings
279 Converters =
new List<JsonConverter> {
new GPALElementConverter(),
new PointConverter(),
new SizeConverter() },
280 NullValueHandling = NullValueHandling.Ignore
284 return JsonConvert.DeserializeObject<T>(jsonResponse, settings);
288 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $
"Exception calling [{endpoint}]",
null, GPALObjectType.None, ex);
291 else if(
string.IsNullOrEmpty(jsonResponse))
293 if (endpoint.Equals(Endpoints[ApiEndpoint.IsEndOfPage]) &&
294 typeof(T) == typeof(
bool))
296 return (T)(object)
true;
302 if (typeof(T) == typeof(
bool))
303 if (
"OK".Equals(jsonResponse) ||
"true".Equals(jsonResponse))
304 return (T)(object)
true;
305 else if (
"NOTOK".Equals(jsonResponse) ||
"false".Equals(jsonResponse))
306 return (T)(object)
false;
309 var msg = jsonResponse.Replace(
"\\",
"");
310 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $
"[{msg}]", parameters, GPALObjectType.Other);
311 return (T)(object)
false;
316 if (endpoint.Equals(Endpoints[ApiEndpoint.GetCssAttributes]) &&
"[]".Equals(jsonResponse?.Trim()))
321 if (
null != jsonResponse)
322 return JsonConvert.DeserializeObject<T>(jsonResponse);
326 catch (Newtonsoft.Json.JsonException ex)
328 GPAL.PublishSimpleEvent(GPALEventType.WARNING,
329 $
"Could not deserialize [{jsonResponse?.Substring(0, Math.Min(80, jsonResponse?.Length ?? 0))}] as [{typeof(T).Name}]",
330 null, GPALObjectType.None, ex);
335 private static T HandleUnsupportedContentType<T>(
string contentType,
string response)
337 GPAL.PublishSimpleEvent(
338 GPALEventType.WARNING,
339 $
"Unsupported format: [{contentType}]. Unable to cast to [{typeof(T)}]. Returning raw response.",
341 GPALObjectType.Other);
347 private static readonly
string[] contentHeaders =
349 "content-type",
"content-length",
"content-encoding",
"content-language",
"content-location",
350 "content-md5",
"content-range",
"content-disposition",
"expires",
"last-modified",
"allow"
353 private static bool IsContentHeader(
string name)
355 return Array.Exists(contentHeaders, content => content.Equals(name, StringComparison.OrdinalIgnoreCase));
367 private static string AppendQueryString(
string route,
object parameters)
371 if (parameters is
string written)
373 query = written.TrimStart(
'?',
'&');
377 IEnumerable<KeyValuePair<string, object>> pairs;
379 if (parameters is IDictionary<string, object> dictionary)
382 pairs = parameters.GetType().GetProperties()
383 .Select(property =>
new KeyValuePair<string, object>(property.Name, property.GetValue(parameters)));
385 query =
string.Join(
"&", pairs
386 .Where(pair =>
null != pair.Value)
387 .Select(pair => $
"{HttpUtility.UrlEncode(pair.Key)}={HttpUtility.UrlEncode(pair.Value.ToString())}"));
390 string retVal = route;
392 if (
false ==
string.IsNullOrEmpty(query))
393 retVal =
true == route.Contains(
"?") ? $
"{route}&{query}" : $
"{route}?{query}";
398 internal async Task<string> InternalCall(
string route,
object parameters =
null,
string httpMethod =
"GET", ContentEncoding contentEncoding = ContentEncoding.Json)
400 string upperMethod = httpMethod.ToUpperInvariant();
406 if ((
"GET" == upperMethod ||
"HEAD" == upperMethod) &&
null != parameters)
408 route = AppendQueryString(route, parameters);
413 async Task<string> TryOnce()
415 using var cts =
new System.Threading.CancellationTokenSource(TimeSpan.FromSeconds(60));
416 HttpResponseMessage httpResponse =
new HttpResponseMessage();
417 string responseBody =
"";
421 if (httpMethod.ToUpper() ==
"POST" &&
422 route.EndsWith(
"upload", StringComparison.OrdinalIgnoreCase))
424 var paramsDict = parameters as IDictionary<string, object>
425 ?? parameters?.GetType().GetProperties()
426 .ToDictionary(p => p.Name, p => p.GetValue(parameters));
428 paramsDict.TryGetValue(
"uploadPath", out var uploadPathObj);
429 string uploadPath = uploadPathObj?.ToString();
431 paramsDict.TryGetValue(
"elementId", out var elementIdObj);
432 string elementId = elementIdObj?.ToString();
434 if (!File.Exists(uploadPath))
436 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
437 $
"upload file not found: [{uploadPath}]",
null, GPALObjectType.None);
441 byte[] fileBytes = File.ReadAllBytes(uploadPath);
442 string base64Data = Convert.ToBase64String(fileBytes);
443 string mimeType = MimeMapping.GetMimeMapping(uploadPath);
453 string jsonBody = System.Text.Json.JsonSerializer.Serialize(payload);
454 HttpContent content =
new StringContent(jsonBody, Encoding.UTF8,
"application/json");
456 var attachRequest =
new HttpRequestMessage(HttpMethod.Post, route);
457 attachRequest.Content = content;
459 httpResponse = await _httpClient.SendAsync(attachRequest, cts.Token).ConfigureAwait(
false);
463 var request =
new HttpRequestMessage(
new HttpMethod(httpMethod.ToUpperInvariant()), route);
465 request.Headers.Accept.Add(
new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue(
"application/json"));
467 foreach (var header
in _restClient._headers)
472 if (
false == IsContentHeader(header.Key) && !request.Headers.Contains(header.Key))
474 request.Headers.TryAddWithoutValidation(header.Key, header.Value);
478 if (parameters !=
null)
484 string contentType = RESTClient.GetContentType(contentEncoding);
486 foreach (var header
in _restClient._headers)
487 if (
true ==
"Content-Type".Equals(header.Key, StringComparison.OrdinalIgnoreCase)
488 &&
false ==
string.IsNullOrWhiteSpace(header.Value))
489 contentType = header.Value.Split(
';')[0].Trim();
491 object parms = parameters;
493 string parmsEncodingType = StringFormatDetector.DetectFormat(parameters.ToString());
495 if (
"none".Equals(parmsEncodingType) &&
"application/json".Equals(contentType))
497 parms = System.Text.Json.JsonSerializer.Serialize(parameters);
499 else if (
"none".Equals(parmsEncodingType) &&
"application/x-www-form-urlencoded".Equals(contentType))
501 if (!(parameters is
string))
502 parms = HttpUtility.UrlEncode(parameters.ToString());
505 string bodyText = parms?.ToString() ??
"";
506 request.Content =
new StringContent(bodyText, System.Text.Encoding.UTF8, contentType);
509 httpResponse = await _httpClient.SendAsync(request, cts.Token).ConfigureAwait(
false);
512 StatusCode = (int)httpResponse.StatusCode;
514 responseBody = await httpResponse.Content.ReadAsStringAsync().ConfigureAwait(
false);
519 const int maxRawLogChars = 200;
520 string rawForLog =
string.IsNullOrEmpty(responseBody) || responseBody.Length <= maxRawLogChars
522 : responseBody.Substring(0, maxRawLogChars) + $
"...[truncated, {responseBody.Length} chars total]";
524 GPAL.PublishSimpleEvent(GPALEventType.DEEPDEBUG, $
"raw result[{route}][{rawForLog}]",
this, GPALObjectType.RestHelper);
526 string upperMethod = httpMethod.ToUpperInvariant();
527 if (
string.IsNullOrEmpty(responseBody) &&
528 (upperMethod ==
"OPTIONS" || upperMethod ==
"HEAD"))
530 var headerDict =
new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
532 foreach (var h
in httpResponse.Headers)
533 headerDict[h.Key] =
string.Join(
", ", h.Value);
535 foreach (var h
in httpResponse.Content.Headers)
536 if (!headerDict.ContainsKey(h.Key))
537 headerDict[h.Key] =
string.Join(
", ", h.Value);
539 responseBody = System.Text.Json.JsonSerializer.Serialize(headerDict,
540 new JsonSerializerOptions { WriteIndented =
true });
543 if (
false == httpResponse.IsSuccessStatusCode)
545 GPAL.PublishSimpleEvent(GPALEventType.WARNING,
546 $
"Request [{route}] failed with status code: [{StatusCode}] [{httpResponse.ReasonPhrase}]",
547 responseBody, GPALObjectType.None);
554 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION,
555 $
"Error in TryOnce for [{httpMethod}] [{route}]",
null, GPALObjectType.RestClient, ex);
563 return await TryOnce().ConfigureAwait(
false);
569 await Task.Delay(2_000).ConfigureAwait(
false);
570 return await TryOnce().ConfigureAwait(
false);
574 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION,
575 $
"Unable to communicate with: [{route}]",
576 null, GPALObjectType.None, ex);
581 if (
false ==
string.IsNullOrEmpty(_browserExtensionApiBase)
582 &&
true == route.StartsWith(_browserExtensionApiBase, StringComparison.OrdinalIgnoreCase))
583 throw new GPALException($
"{GPAL.MyMethodName()}: The browser at [{_browserExtensionApiBase}] did not answer [{route}] on either attempt, so there is nothing to drive");
589 public async Task<bool> CheckAPIStatus()
591 string route = $
"{_browserExtensionApiBase}{Endpoints[ApiEndpoint.CheckStatus]}";
592 using (HttpClient client =
new HttpClient())
596 HttpResponseMessage response = await client.GetAsync(route).ConfigureAwait(
false);
597 string responseBody = await response.Content.ReadAsStringAsync().ConfigureAwait(
false);
599 return responseBody.Equals(
"Server is running");
608 public static class StringFormatDetector
610 public static string DetectFormat(
string input)
612 if (
string.IsNullOrWhiteSpace(input))
616 var trimmed = input.Trim();
623 if (IsUrlEncoded(trimmed))
629 internal static bool IsJson(
string input)
631 if ((input.StartsWith(
"{") && input.EndsWith(
"}")) ||
632 (input.StartsWith(
"[") && input.EndsWith(
"]")))
636 using var doc = JsonDocument.Parse(input);
647 private static bool IsUrlEncoded(
string input)
651 var decoded = HttpUtility.UrlDecode(input);
652 var reencoded = HttpUtility.UrlEncode(decoded);
653 return string.Equals(input, reencoded, StringComparison.Ordinal);