diff --git a/devolutions-gateway/openapi/doc/index.adoc b/devolutions-gateway/openapi/doc/index.adoc index 405c6a6bf..4442e11f5 100644 --- a/devolutions-gateway/openapi/doc/index.adoc +++ b/devolutions-gateway/openapi/doc/index.adoc @@ -1,6 +1,6 @@ = devolutions-gateway infos@devolutions.net -2025.3.2 +2026.2.4 :toc: left :numbered: :toclevels: 4 @@ -1188,6 +1188,372 @@ ifdef::internal-generation[] endif::internal-generation[] +[.getNetInterfaces] +==== getNetInterfaces + +`GET /jet/net/interfaces` + +Lists Gateway network scan sources. + +===== Description + + + + +// markup not found, no include::{specDir}jet/net/interfaces/GET/spec.adoc[opts=optional] + + + +===== Security + +[cols="2,1,1"] +|=== +| Name | Type | Scheme + +| `netscan_token` +| http +| bearer +|=== + + +===== Return Type + +<> + + +===== Content Type + +* application/json + +===== Responses + +.HTTP Response Codes +[cols="2,3,1"] +|=== +| Code | Message | Datatype + + +| 200 +| Gateway network scan sources +| <> + + +| 400 +| Bad request +| <<>> + + +| 401 +| Invalid or missing authorization token +| <<>> + + +| 403 +| Insufficient permissions +| <<>> + + +| 500 +| Unexpected server error +| <<>> + +|=== + + +ifdef::internal-generation[] +===== Implementation + +// markup not found, no include::{specDir}jet/net/interfaces/GET/implementation.adoc[opts=optional] + + +endif::internal-generation[] + + +[.getNetScan] +==== getNetScan + +`GET /jet/net/scan` + +Stream network scan events over a websocket. + +===== Description + +The endpoint is upgraded from HTTP, so OpenAPI describes the **handshake**: the query parameters that drive the scan (validated before upgrade) and the legacy / v1 event payloads streamed back as JSON text frames. See `NetworkScanResultEvent` for the v1 shape and `LegacyScanEvent` for the legacy shape (selected via `response_format`). + + +// markup not found, no include::{specDir}jet/net/scan/GET/spec.adoc[opts=optional] + + + +===== Security + +[cols="2,1,1"] +|=== +| Name | Type | Scheme + +| `netscan_token` +| http +| bearer +|=== + +===== Parameters + + + + + +====== Query Parameters + +[cols="2,3,1,1,1"] +|=== +|Name| Description| Required| Default| Pattern + +| ping_interval +| Interval in milliseconds (default is 200) +| - +| null +| + +| ping_timeout +| Timeout in milliseconds (default is 500) +| - +| null +| + +| broadcast_timeout +| Timeout in milliseconds (default is 1000) +| - +| null +| + +| port_scan_timeout +| Timeout in milliseconds (default is 1000) +| - +| null +| + +| netbios_timeout +| Timeout in milliseconds (default is 1000) +| - +| null +| + +| netbios_interval +| Interval in milliseconds (default is 200) +| - +| null +| + +| mdns_query_timeout +| The maximum time for each mdns query in milliseconds. (default is 5 * 1000) +| - +| null +| + +| max_wait +| The maximum duration for whole networking scan in milliseconds. Highly suggested! +| - +| null +| + +| range +| The start and end IP address of the range to scan. for example: 10.10.0.0-10.10.0.255 <> +| - +| null +| + +| target +| Explicit single-host targets to scan. Each value must parse as an IPv4 or IPv6 address; invalid values yield a structured `{ error: \"invalid_target\", value: \"\" }` 400 rather than a generic serde rejection at extraction time (mirrors the `range=` / `probe=` validation path). <> +| - +| null +| + +| interface_id +| Gateway network interface IDs to use as scan sources. <> +| - +| null +| + +| probe +| The probes to run. Each value is either `ping`, a port number (`22`), or a named service (`rdp`, `https`, …). Validation is deferred to scan-time so failures can be surfaced as a structured 400 — naming the offending value — instead of a generic serde rejection at extraction time. <> +| - +| null +| + +| enable_ping_start +| **Legacy alias** for `report_ping_start`. Prefer the explicit name in new clients; kept so existing consumers don't break. +| - +| null +| + +| enable_broadcast +| Enable the execution of broadcast scan +| - +| null +| + +| enable_subnet_scan +| Enable the ping scan on subnet +| - +| null +| + +| enable_zeroconf +| Enable ZeroConf/mDNS +| - +| null +| + +| enable_netbios +| Enable NetBIOS name-service queries. Default `true` for backward compatibility. Set `false` (or pair with explicit `target=`) to keep NetBIOS from sweeping the surrounding subnet when the caller only wants results for the targets they listed. +| - +| null +| + +| enable_resolve_dns +| Enable resolve dns +| - +| null +| + +| include_host_results +| Include host-only results. +| - +| null +| + +| report_ping_start +| Emit ping queued/start host results. +| - +| null +| + +| report_ping_success +| Emit ping success host results. +| - +| null +| + +| report_ping_failure +| Emit ping failure host results. +| - +| null +| + +| enable_tcp_probes +| Enable TCP service probes. +| - +| null +| + +| range_interface_policy +| Policy applied when `range=` and `interface_id=` are both provided. Accepted values: `intersect_selected_interfaces` (default) or `allow_cross_interface_range`. Invalid values yield a structured `{ error: \"invalid_range_interface_policy\", value: \"\" }` 400 instead of a generic serde rejection (mirrors the `range=` / `probe=` / `target=` validation path). +| - +| null +| + +| allow_cross_interface_range +| **Legacy alias** for `range_interface_policy=allow_cross_interface_range`. Prefer the structured policy in new clients. +| - +| null +| + +| response_format +| Response shape emitted on the websocket. Accepted values: `legacy` (default) or `network_scan_result_v1`. Invalid values yield a structured `{ error: \"invalid_response_format\", value: \"\" }` 400 instead of a generic serde rejection. +| - +| null +| + +| max_concurrency +| Maximum scanner concurrency. +| - +| null +| + +| max_ping_concurrency +| Maximum ping probe concurrency. +| - +| null +| + +| max_tcp_probe_concurrency +| Maximum TCP probe concurrency. +| - +| null +| + +| enable_failure +| **Legacy alias** for `report_ping_failure`. `enable_failure=true` only opts into ping-failure events; TCP-probe failure events require the explicit `report_tcp_failure=true` and are not affected by this alias. **Behavior change:** historically this single flag controlled both ping-failure and TCP-probe-failure reporting. The two are now split: clients that want the old \"both at once\" semantics must send `enable_failure=true&report_tcp_failure=true` together. The split is intentional — TCP-probe failures are typically high-volume noise that callers were filtering client-side anyway, so the two streams are independently gated. +| - +| null +| + +| report_tcp_failure +| Enable TCP port knocking failure events. +| - +| null +| + +| interface_bind_strict +| When `true`, fail with HTTP 400 if a ping/TCP-probe socket cannot be bound to the planner-selected interface. Default `false` (warn and fall back to default routing). +| - +| null +| + +|=== + + +===== Return Type + + + +- + + +===== Responses + +.HTTP Response Codes +[cols="2,3,1"] +|=== +| Code | Message | Datatype + + +| 101 +| WebSocket upgrade; subsequent text frames carry NetworkScanResultEvent or LegacyScanEvent JSON +| <<>> + + +| 400 +| Invalid query, mixed target/range, oversized range, or selected interface error +| <<>> + + +| 401 +| Invalid or missing authorization token +| <<>> + + +| 403 +| Insufficient permissions +| <<>> + + +| 500 +| Unexpected server error +| <<>> + +|=== + + +ifdef::internal-generation[] +===== Implementation + +// markup not found, no include::{specDir}jet/net/scan/GET/implementation.adoc[opts=optional] + + +endif::internal-generation[] + + [.NetworkMonitoring] === NetworkMonitoring @@ -1884,19 +2250,288 @@ endif::internal-generation[] === Update -[.triggerUpdate] -==== triggerUpdate +[.getUpdateProducts] +==== getUpdateProducts -`POST /jet/update` +`GET /jet/update` -Triggers Devolutions Gateway update process. +Retrieve the currently installed version of each Devolutions product. ===== Description -This is done via updating `Agent/update.json` file, which is then read by Devolutions Agent when changes are detected. If the version written to `update.json` is indeed higher than the currently installed version, Devolutions Agent will proceed with the update process. +Reads `update_status.json`, which is written by the Devolutions Agent on startup and refreshed after every update run. When the file does not exist (agent not installed or is an older version), the endpoint returns HTTP 503 because updater status is unavailable. -// markup not found, no include::{specDir}jet/update/POST/spec.adoc[opts=optional] +// markup not found, no include::{specDir}jet/update/GET/spec.adoc[opts=optional] + + + +===== Security + +[cols="2,1,1"] +|=== +| Name | Type | Scheme + +| `scope_token` +| http +| bearer +|=== + + +===== Return Type + +<> + + +===== Content Type + +* application/json + +===== Responses + +.HTTP Response Codes +[cols="2,3,1"] +|=== +| Code | Message | Datatype + + +| 200 +| Installed product versions +| <> + + +| 401 +| Invalid or missing authorization token +| <<>> + + +| 403 +| Insufficient permissions +| <<>> + + +| 500 +| Failed to read agent status file +| <<>> + + +| 503 +| Agent updater service is unavailable +| <<>> + +|=== + + +ifdef::internal-generation[] +===== Implementation + +// markup not found, no include::{specDir}jet/update/GET/implementation.adoc[opts=optional] + + +endif::internal-generation[] + + +[.getUpdateSchedule] +==== getUpdateSchedule + +`GET /jet/update/schedule` + +Retrieve the current Devolutions Agent auto-update schedule. + +===== Description + +Reads the `Schedule` field from `update_status.json`. When the field is absent the response contains zeroed defaults (`Enabled: false`, interval `0`, window start `0`, no products). + + +// markup not found, no include::{specDir}jet/update/schedule/GET/spec.adoc[opts=optional] + + + +===== Security + +[cols="2,1,1"] +|=== +| Name | Type | Scheme + +| `scope_token` +| http +| bearer +|=== + + +===== Return Type + +<> + + +===== Content Type + +* application/json + +===== Responses + +.HTTP Response Codes +[cols="2,3,1"] +|=== +| Code | Message | Datatype + + +| 200 +| Current auto-update schedule +| <> + + +| 401 +| Invalid or missing authorization token +| <<>> + + +| 403 +| Insufficient permissions +| <<>> + + +| 500 +| Failed to read agent status file +| <<>> + + +| 503 +| Agent updater service is unavailable +| <<>> + +|=== + + +ifdef::internal-generation[] +===== Implementation + +// markup not found, no include::{specDir}jet/update/schedule/GET/implementation.adoc[opts=optional] + + +endif::internal-generation[] + + +[.setUpdateSchedule] +==== setUpdateSchedule + +`POST /jet/update/schedule` + +Set the Devolutions Agent auto-update schedule. + +===== Description + +Writes the `Schedule` field into `update.json`. The agent watches this file and applies the new schedule immediately, then persists it to `agent.json`. All other fields in `update.json` are preserved; the `VersionMinor` field is reset to the minor version this gateway build understands so the agent does not see an unknown future version. + + +// markup not found, no include::{specDir}jet/update/schedule/POST/spec.adoc[opts=optional] + + + +===== Security + +[cols="2,1,1"] +|=== +| Name | Type | Scheme + +| `scope_token` +| http +| bearer +|=== + +===== Parameters + + +====== Body Parameter + +[cols="2,3,1,1,1"] +|=== +|Name| Description| Required| Default| Pattern + +| SetUpdateScheduleRequest +| <> +| X +| +| + +|=== + + + + + +===== Return Type + + +<> + + +===== Content Type + +* application/json + +===== Responses + +.HTTP Response Codes +[cols="2,3,1"] +|=== +| Code | Message | Datatype + + +| 200 +| Auto-update schedule applied +| <> + + +| 400 +| Bad request +| <<>> + + +| 401 +| Invalid or missing authorization token +| <<>> + + +| 403 +| Insufficient permissions +| <<>> + + +| 500 +| Failed to write update manifest +| <<>> + + +| 503 +| Agent updater service is unavailable +| <<>> + +|=== + + +ifdef::internal-generation[] +===== Implementation + +// markup not found, no include::{specDir}jet/update/schedule/POST/implementation.adoc[opts=optional] + + +endif::internal-generation[] + + +[.triggerUpdate] +==== triggerUpdate + +`POST /jet/update` + +Trigger an update for one or more Devolutions products. + +===== Description + +Writes the requested version(s) into `Agent/update.json`, which is watched by Devolutions Agent. When a requested version is higher than the installed version the agent proceeds with the update. **Body form** (preferred): pass a JSON body with a `Products` map. **Query-param form** (legacy, gateway-only): `POST /jet/update?version=latest`. This form updates only the Gateway product and is kept for backward compatibility. Both forms cannot be used simultaneously; doing so returns HTTP 400. + + +// markup not found, no include::{specDir}jet/update/POST/spec.adoc[opts=optional] @@ -1914,6 +2549,19 @@ This is done via updating `Agent/update.json` file, which is then read by Devolu ===== Parameters +====== Body Parameter + +[cols="2,3,1,1,1"] +|=== +|Name| Description| Required| Default| Pattern + +| UpdateRequestSchema +| Products and target versions to update <> +| - +| +| + +|=== @@ -1924,8 +2572,8 @@ This is done via updating `Agent/update.json` file, which is then read by Devolu |Name| Description| Required| Default| Pattern | version -| The version to install; use 'latest' for the latest version, or 'w.x.y.z' for a specific version -| X +| Gateway-only target version; use the request body for multi-product updates +| - | null | @@ -1951,7 +2599,7 @@ This is done via updating `Agent/update.json` file, which is then read by Devolu | 200 -| Update request has been processed successfully +| Update request accepted | <> @@ -1971,7 +2619,7 @@ This is done via updating `Agent/update.json` file, which is then read by Devolu | 500 -| Agent updater service is malfunctioning +| Failed to write update manifest | <<>> @@ -2227,11 +2875,14 @@ endif::internal-generation[] | gateway.recording.delete | gateway.recordings.read | gateway.update +| gateway.update.read | gateway.preflight | gateway.traffic.claim | gateway.traffic.ack | gateway.net.monitor.config | gateway.net.monitor.drain +| gateway.agent.delete +| gateway.agent.read |=== @@ -2704,18 +3355,104 @@ Service configuration diagnostic |=== -[#Heartbeat] -=== _Heartbeat_ - +[#GetUpdateProductsResponse] +=== _GetUpdateProductsResponse_ +Installed version of each product, as reported by Devolutions Agent. -[.fields-Heartbeat] +[.fields-GetUpdateProductsResponse] [cols="2,1,1,2,4,1"] |=== | Field Name| Required| Nullable | Type| Description | Format -| agent_version +| ManifestVersion +| X +| +| String +| Version of the `update_status.json` format in `\"major.minor\"` form (e.g. `\"1.1\"`). +| + +| Products +| +| +| Map of <> +| Map of product name to API-specific product info. +| + +|=== + + + +[#GetUpdateScheduleResponse] +=== _GetUpdateScheduleResponse_ + +Current auto-update schedule for Devolutions Agent. + + +[.fields-GetUpdateScheduleResponse] +[cols="2,1,1,2,4,1"] +|=== +| Field Name| Required| Nullable | Type| Description | Format + +| Enabled +| X +| +| Boolean +| Enable periodic Devolutions Agent self-update checks. +| + +| Interval +| X +| +| Long +| Minimum interval between auto-update checks, in seconds. `0` means check once at `UpdateWindowStart`. +| int64 + +| ManifestVersion +| X +| +| String +| Version of the `update_status.json` format in `\"major.minor\"` form (e.g. `\"1.1\"`). +| + +| Products +| +| +| List of <> +| Products the agent autonomously polls for new versions. +| + +| UpdateWindowEnd +| +| X +| Integer +| End of the maintenance window as seconds past midnight (local time, exclusive). `None` means no upper bound (single check at `UpdateWindowStart`). +| int32 + +| UpdateWindowStart +| X +| +| Integer +| Start of the maintenance window as seconds past midnight (local time). +| int32 + +|=== + + + +[#Heartbeat] +=== _Heartbeat_ + + + + +[.fields-Heartbeat] +[cols="2,1,1,2,4,1"] +|=== +| Field Name| Required| Nullable | Type| Description | Format + +| agent_version | | X | String @@ -2775,6 +3512,27 @@ Service configuration diagnostic +[#HostScanStateDto] +=== _HostScanStateDto_ + + + + + + +[.fields-HostScanStateDto] +[cols="1"] +|=== +| Enum Values + +| queued +| probing +| reachable +| unreachable + +|=== + + [#Identity] === _Identity_ @@ -3143,6 +3901,366 @@ Service configuration diagnostic +[#NetworkInterfaceDto] +=== _NetworkInterfaceDto_ + + + + +[.fields-NetworkInterfaceDto] +[cols="2,1,1,2,4,1"] +|=== +| Field Name| Required| Nullable | Type| Description | Format + +| description +| +| X +| String +| +| + +| id +| X +| +| String +| +| + +| index +| +| X +| Integer +| +| int32 + +| isUp +| +| X +| Boolean +| +| + +| linkType +| +| X +| String +| Coarse link type: `ethernet`, `wifi`, `loopback`, `virtual`, `unknown`. +| + +| macAddress +| +| X +| String +| +| + +| mtu +| +| X +| Integer +| MTU in bytes when known. +| int32 + +| name +| X +| +| String +| +| + +| speedMbps +| +| X +| Long +| Link speed in megabits per second when reported by the OS. +| int64 + +|=== + + + +[#NetworkInterfacesResponse] +=== _NetworkInterfacesResponse_ + + + + +[.fields-NetworkInterfacesResponse] +[cols="2,1,1,2,4,1"] +|=== +| Field Name| Required| Nullable | Type| Description | Format + +| interfaces +| X +| +| List of <> +| +| + +|=== + + + +[#NetworkScanResultEventDto] +=== _NetworkScanResultEventDto_ + + + + +[.fields-NetworkScanResultEventDto] +[cols="2,1,1,2,4,1"] +|=== +| Field Name| Required| Nullable | Type| Description | Format + +| address +| X +| +| String +| +| + +| discoverySource +| X +| +| <> +| +| subnet, broadcast, tcp_probe, gateway, zero_conf, + +| hostName +| +| X +| String +| +| + +| hostScanState +| +| X +| <> +| +| queued, probing, reachable, unreachable, + +| interfaceId +| +| X +| String +| +| + +| interfaceName +| +| X +| String +| +| + +| isReachable +| +| X +| Boolean +| +| + +| kind +| X +| +| <> +| +| host, service, + +| macAddress +| +| X +| String +| +| + +| port +| +| X +| Integer +| +| int32 + +| responseTimeMs +| +| X +| Integer +| +| + +| serviceLabel +| +| X +| String +| +| + +| serviceType +| +| X +| String +| +| + +| source +| X +| +| <> +| +| gateway, + +|=== + + + +[#NetworkScanResultKindDto] +=== _NetworkScanResultKindDto_ + + + + + + +[.fields-NetworkScanResultKindDto] +[cols="1"] +|=== +| Enum Values + +| host +| service + +|=== + + +[#NetworkScanSourceCapabilitiesDto] +=== _NetworkScanSourceCapabilitiesDto_ + + + + +[.fields-NetworkScanSourceCapabilitiesDto] +[cols="2,1,1,2,4,1"] +|=== +| Field Name| Required| Nullable | Type| Description | Format + +| broadcast +| X +| +| Boolean +| +| + +| dnsResolve +| X +| +| Boolean +| +| + +| ipv4 +| X +| +| Boolean +| +| + +| ipv6 +| X +| +| Boolean +| +| + +| subnet +| X +| +| Boolean +| +| + +| tcpProbe +| X +| +| Boolean +| +| + +| zeroConf +| X +| +| Boolean +| +| + +|=== + + + +[#NetworkScanSourceDto] +=== _NetworkScanSourceDto_ + + + + +[.fields-NetworkScanSourceDto] +[cols="2,1,1,2,4,1"] +|=== +| Field Name| Required| Nullable | Type| Description | Format + +| address +| X +| +| String +| +| + +| broadcastAddress +| +| X +| String +| +| + +| capabilities +| X +| +| <> +| +| + +| endAddress +| X +| +| String +| +| + +| interface +| X +| +| <> +| +| + +| prefixLength +| +| X +| Integer +| +| int32 + +| startAddress +| X +| +| String +| +| + +|=== + + + [#PreflightAlertStatus] === _PreflightAlertStatus_ @@ -3179,6 +4297,13 @@ Service configuration diagnostic |=== | Field Name| Required| Nullable | Type| Description | Format +| connection_options +| +| X +| <> +| +| + | host_to_resolve | | X @@ -3198,7 +4323,7 @@ Service configuration diagnostic | | <> | -| get-version, get-agent-version, get-running-session-count, get-recording-storage-health, provision-token, provision-credentials, resolve-host, +| get-version, get-agent-version, get-running-session-count, get-recording-storage-health, provision-token, provision-credentials, provision-connection-options, resolve-host, | proxy_credential | @@ -3218,14 +4343,14 @@ Service configuration diagnostic | | X | Integer -| Minimum persistance duration in seconds for the data provisioned via this operation. Optional parameter for \"provision-token\" and \"provision-credentials\" kinds. +| Minimum persistence duration in seconds for the data provisioned via this operation. Optional parameter for \"provision-token\", \"provision-credentials\" and \"provision-connection-options\" kinds. | int32 | token | | X | String -| The token to be stored on the proxy-side. Required for \"provision-token\" and \"provision-credentials\" kinds. +| The token to be stored on the proxy-side. Required for \"provision-token\", \"provision-credentials\" and \"provision-connection-options\" kinds. | |=== @@ -3251,6 +4376,7 @@ Service configuration diagnostic | get-recording-storage-health | provision-token | provision-credentials +| provision-connection-options | resolve-host |=== @@ -3397,6 +4523,46 @@ Service configuration diagnostic |=== +[#ScanOriginDto] +=== _ScanOriginDto_ + + + + + + +[.fields-ScanOriginDto] +[cols="1"] +|=== +| Enum Values + +| gateway + +|=== + + +[#ScanResultSourceDto] +=== _ScanResultSourceDto_ + + + + + + +[.fields-ScanResultSourceDto] +[cols="1"] +|=== +| Enum Values + +| subnet +| broadcast +| tcp_probe +| gateway +| zero_conf + +|=== + + [#SessionInfo] === _SessionInfo_ @@ -3574,6 +4740,56 @@ This body is returned when the config is successfully set, even if one or all pr +[#SetUpdateScheduleRequest] +=== _SetUpdateScheduleRequest_ + +Desired auto-update schedule to apply to Devolutions Agent. + + +[.fields-SetUpdateScheduleRequest] +[cols="2,1,1,2,4,1"] +|=== +| Field Name| Required| Nullable | Type| Description | Format + +| Enabled +| X +| +| Boolean +| Enable periodic Devolutions Agent self-update checks. +| + +| Interval +| +| +| Long +| Minimum interval between auto-update checks, in seconds. `0` means check once at `UpdateWindowStart` (default). +| int64 + +| Products +| +| +| List of <> +| Products the agent autonomously polls for new versions (default: empty). +| + +| UpdateWindowEnd +| +| X +| Integer +| End of the maintenance window as seconds past midnight in local time, exclusive. `null` (default) means no upper bound - a single check fires at `UpdateWindowStart`. When end < start the window crosses midnight. +| int32 + +| UpdateWindowStart +| +| +| Integer +| Start of the maintenance window as seconds past midnight in local time (default: `7200` = 02:00). +| int32 + +|=== + + + [#SubProvisionerKey] === _SubProvisionerKey_ @@ -3646,6 +4862,28 @@ Subscriber configuration +[#TargetConnectionOptions] +=== _TargetConnectionOptions_ + + + + +[.fields-TargetConnectionOptions] +[cols="2,1,1,2,4,1"] +|=== +| Field Name| Required| Nullable | Type| Description | Format + +| krb_kdc +| +| X +| String +| Kerberos KDC address for the target-side CredSSP connection. Supported schemes are `tcp` and `udp`. +| + +|=== + + + [#TrafficEventResponse] === _TrafficEventResponse_ @@ -3757,3 +4995,50 @@ Subscriber configuration |=== +[#UpdateProductInfo] +=== _UpdateProductInfo_ + +Per-product update information. + + +[.fields-UpdateProductInfo] +[cols="2,1,1,2,4,1"] +|=== +| Field Name| Required| Nullable | Type| Description | Format + +| Version +| X +| +| String +| Requested or installed version: `\"latest\"` or `\"YYYY.M.D\"` / `\"YYYY.M.D.R\"`. +| + +|=== + + + +[#UpdateRequestSchema] +=== _UpdateRequestSchema_ + +OpenAPI schema for the update request body. + +The API accepts an object containing a `Products` map, whose keys are product names +and whose values are update information. + + +[.fields-UpdateRequestSchema] +[cols="2,1,1,2,4,1"] +|=== +| Field Name| Required| Nullable | Type| Description | Format + +| Products +| +| +| Map of <> +| Map of product name to update information. +| + +|=== + + + diff --git a/devolutions-gateway/openapi/dotnet-client/.openapi-generator/FILES b/devolutions-gateway/openapi/dotnet-client/.openapi-generator/FILES index 16fd9a133..90efa2314 100644 --- a/devolutions-gateway/openapi/dotnet-client/.openapi-generator/FILES +++ b/devolutions-gateway/openapi/dotnet-client/.openapi-generator/FILES @@ -19,9 +19,12 @@ docs/DataEncoding.md docs/DeleteManyResult.md docs/DiagnosticsApi.md docs/EventOutcomeResponse.md +docs/GetUpdateProductsResponse.md +docs/GetUpdateScheduleResponse.md docs/HealthApi.md docs/Heartbeat.md docs/HeartbeatApi.md +docs/HostScanStateDto.md docs/Identity.md docs/InterfaceInfo.md docs/JrecApi.md @@ -36,7 +39,13 @@ docs/MonitoringProbeType.md docs/MonitoringProbeTypeOneOf.md docs/MonitorsConfig.md docs/NetApi.md +docs/NetworkInterfaceDto.md +docs/NetworkInterfacesResponse.md docs/NetworkMonitoringApi.md +docs/NetworkScanResultEventDto.md +docs/NetworkScanResultKindDto.md +docs/NetworkScanSourceCapabilitiesDto.md +docs/NetworkScanSourceDto.md docs/PreflightAlertStatus.md docs/PreflightApi.md docs/PreflightOperation.md @@ -44,17 +53,23 @@ docs/PreflightOperationKind.md docs/PreflightOutput.md docs/PreflightOutputKind.md docs/PubKeyFormat.md +docs/ScanOriginDto.md +docs/ScanResultSourceDto.md docs/SessionInfo.md docs/SessionTokenContentType.md docs/SessionTokenSignRequest.md docs/SessionsApi.md docs/SetConfigResponse.md +docs/SetUpdateScheduleRequest.md docs/SubProvisionerKey.md docs/Subscriber.md +docs/TargetConnectionOptions.md docs/TrafficApi.md docs/TrafficEventResponse.md docs/TransportProtocolResponse.md docs/UpdateApi.md +docs/UpdateProductInfo.md +docs/UpdateRequestSchema.md docs/WebAppApi.md src/Devolutions.Gateway.Client/Api/ConfigApi.cs src/Devolutions.Gateway.Client/Api/DiagnosticsApi.cs @@ -104,7 +119,10 @@ src/Devolutions.Gateway.Client/Model/ConnectionMode.cs src/Devolutions.Gateway.Client/Model/DataEncoding.cs src/Devolutions.Gateway.Client/Model/DeleteManyResult.cs src/Devolutions.Gateway.Client/Model/EventOutcomeResponse.cs +src/Devolutions.Gateway.Client/Model/GetUpdateProductsResponse.cs +src/Devolutions.Gateway.Client/Model/GetUpdateScheduleResponse.cs src/Devolutions.Gateway.Client/Model/Heartbeat.cs +src/Devolutions.Gateway.Client/Model/HostScanStateDto.cs src/Devolutions.Gateway.Client/Model/Identity.cs src/Devolutions.Gateway.Client/Model/InterfaceInfo.cs src/Devolutions.Gateway.Client/Model/JrlInfo.cs @@ -116,17 +134,29 @@ src/Devolutions.Gateway.Client/Model/MonitoringLogResponse.cs src/Devolutions.Gateway.Client/Model/MonitoringProbeType.cs src/Devolutions.Gateway.Client/Model/MonitoringProbeTypeOneOf.cs src/Devolutions.Gateway.Client/Model/MonitorsConfig.cs +src/Devolutions.Gateway.Client/Model/NetworkInterfaceDto.cs +src/Devolutions.Gateway.Client/Model/NetworkInterfacesResponse.cs +src/Devolutions.Gateway.Client/Model/NetworkScanResultEventDto.cs +src/Devolutions.Gateway.Client/Model/NetworkScanResultKindDto.cs +src/Devolutions.Gateway.Client/Model/NetworkScanSourceCapabilitiesDto.cs +src/Devolutions.Gateway.Client/Model/NetworkScanSourceDto.cs src/Devolutions.Gateway.Client/Model/PreflightAlertStatus.cs src/Devolutions.Gateway.Client/Model/PreflightOperation.cs src/Devolutions.Gateway.Client/Model/PreflightOperationKind.cs src/Devolutions.Gateway.Client/Model/PreflightOutput.cs src/Devolutions.Gateway.Client/Model/PreflightOutputKind.cs src/Devolutions.Gateway.Client/Model/PubKeyFormat.cs +src/Devolutions.Gateway.Client/Model/ScanOriginDto.cs +src/Devolutions.Gateway.Client/Model/ScanResultSourceDto.cs src/Devolutions.Gateway.Client/Model/SessionInfo.cs src/Devolutions.Gateway.Client/Model/SessionTokenContentType.cs src/Devolutions.Gateway.Client/Model/SessionTokenSignRequest.cs src/Devolutions.Gateway.Client/Model/SetConfigResponse.cs +src/Devolutions.Gateway.Client/Model/SetUpdateScheduleRequest.cs src/Devolutions.Gateway.Client/Model/SubProvisionerKey.cs src/Devolutions.Gateway.Client/Model/Subscriber.cs +src/Devolutions.Gateway.Client/Model/TargetConnectionOptions.cs src/Devolutions.Gateway.Client/Model/TrafficEventResponse.cs src/Devolutions.Gateway.Client/Model/TransportProtocolResponse.cs +src/Devolutions.Gateway.Client/Model/UpdateProductInfo.cs +src/Devolutions.Gateway.Client/Model/UpdateRequestSchema.cs diff --git a/devolutions-gateway/openapi/dotnet-client/README.md b/devolutions-gateway/openapi/dotnet-client/README.md index 8f17bd5a4..b555491ab 100644 --- a/devolutions-gateway/openapi/dotnet-client/README.md +++ b/devolutions-gateway/openapi/dotnet-client/README.md @@ -4,7 +4,7 @@ Protocol-aware fine-grained relay server This C# SDK is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project: -- API version: 2025.3.2 +- API version: 2026.2.4 - SDK version: 2025.12.2 - Generator version: 7.9.0 - Build package: org.openapitools.codegen.languages.CSharpClientCodegen @@ -154,6 +154,8 @@ Class | Method | HTTP request | Description *JrlApi* | [**GetJrlInfo**](docs/JrlApi.md#getjrlinfo) | **GET** /jet/jrl/info | Retrieves current JRL (Json Revocation List) info *JrlApi* | [**UpdateJrl**](docs/JrlApi.md#updatejrl) | **POST** /jet/jrl | Updates JRL (Json Revocation List) using a JRL token *NetApi* | [**GetNetConfig**](docs/NetApi.md#getnetconfig) | **GET** /jet/net/config | Lists network interfaces +*NetApi* | [**GetNetInterfaces**](docs/NetApi.md#getnetinterfaces) | **GET** /jet/net/interfaces | Lists Gateway network scan sources. +*NetApi* | [**GetNetScan**](docs/NetApi.md#getnetscan) | **GET** /jet/net/scan | Stream network scan events over a websocket. *NetworkMonitoringApi* | [**DrainMonitoringLog**](docs/NetworkMonitoringApi.md#drainmonitoringlog) | **POST** /jet/net/monitor/log/drain | Monitors store their results in a temporary log, which is returned here. *NetworkMonitoringApi* | [**SetMonitoringConfig**](docs/NetworkMonitoringApi.md#setmonitoringconfig) | **POST** /jet/net/monitor/config | Replace the current monitoring configuration with the configuration in the request body. *PreflightApi* | [**PostPreflight**](docs/PreflightApi.md#postpreflight) | **POST** /jet/preflight | Performs a batch of preflight operations @@ -161,7 +163,10 @@ Class | Method | HTTP request | Description *SessionsApi* | [**TerminateSession**](docs/SessionsApi.md#terminatesession) | **POST** /jet/session/{id}/terminate | Terminate forcefully a running session *TrafficApi* | [**AckTrafficEvents**](docs/TrafficApi.md#acktrafficevents) | **POST** /jet/traffic/ack | Acknowledge traffic audit events and remove them from the queue *TrafficApi* | [**ClaimTrafficEvents**](docs/TrafficApi.md#claimtrafficevents) | **POST** /jet/traffic/claim | Claim traffic audit events for processing -*UpdateApi* | [**TriggerUpdate**](docs/UpdateApi.md#triggerupdate) | **POST** /jet/update | Triggers Devolutions Gateway update process. +*UpdateApi* | [**GetUpdateProducts**](docs/UpdateApi.md#getupdateproducts) | **GET** /jet/update | Retrieve the currently installed version of each Devolutions product. +*UpdateApi* | [**GetUpdateSchedule**](docs/UpdateApi.md#getupdateschedule) | **GET** /jet/update/schedule | Retrieve the current Devolutions Agent auto-update schedule. +*UpdateApi* | [**SetUpdateSchedule**](docs/UpdateApi.md#setupdateschedule) | **POST** /jet/update/schedule | Set the Devolutions Agent auto-update schedule. +*UpdateApi* | [**TriggerUpdate**](docs/UpdateApi.md#triggerupdate) | **POST** /jet/update | Trigger an update for one or more Devolutions products. *WebAppApi* | [**SignAppToken**](docs/WebAppApi.md#signapptoken) | **POST** /jet/webapp/app-token | Requests a web application token using the configured authorization method *WebAppApi* | [**SignSessionToken**](docs/WebAppApi.md#signsessiontoken) | **POST** /jet/webapp/session-token | Requests a session token using a web application token @@ -185,7 +190,10 @@ Class | Method | HTTP request | Description - [Model.DataEncoding](docs/DataEncoding.md) - [Model.DeleteManyResult](docs/DeleteManyResult.md) - [Model.EventOutcomeResponse](docs/EventOutcomeResponse.md) + - [Model.GetUpdateProductsResponse](docs/GetUpdateProductsResponse.md) + - [Model.GetUpdateScheduleResponse](docs/GetUpdateScheduleResponse.md) - [Model.Heartbeat](docs/Heartbeat.md) + - [Model.HostScanStateDto](docs/HostScanStateDto.md) - [Model.Identity](docs/Identity.md) - [Model.InterfaceInfo](docs/InterfaceInfo.md) - [Model.JrlInfo](docs/JrlInfo.md) @@ -197,20 +205,32 @@ Class | Method | HTTP request | Description - [Model.MonitoringProbeType](docs/MonitoringProbeType.md) - [Model.MonitoringProbeTypeOneOf](docs/MonitoringProbeTypeOneOf.md) - [Model.MonitorsConfig](docs/MonitorsConfig.md) + - [Model.NetworkInterfaceDto](docs/NetworkInterfaceDto.md) + - [Model.NetworkInterfacesResponse](docs/NetworkInterfacesResponse.md) + - [Model.NetworkScanResultEventDto](docs/NetworkScanResultEventDto.md) + - [Model.NetworkScanResultKindDto](docs/NetworkScanResultKindDto.md) + - [Model.NetworkScanSourceCapabilitiesDto](docs/NetworkScanSourceCapabilitiesDto.md) + - [Model.NetworkScanSourceDto](docs/NetworkScanSourceDto.md) - [Model.PreflightAlertStatus](docs/PreflightAlertStatus.md) - [Model.PreflightOperation](docs/PreflightOperation.md) - [Model.PreflightOperationKind](docs/PreflightOperationKind.md) - [Model.PreflightOutput](docs/PreflightOutput.md) - [Model.PreflightOutputKind](docs/PreflightOutputKind.md) - [Model.PubKeyFormat](docs/PubKeyFormat.md) + - [Model.ScanOriginDto](docs/ScanOriginDto.md) + - [Model.ScanResultSourceDto](docs/ScanResultSourceDto.md) - [Model.SessionInfo](docs/SessionInfo.md) - [Model.SessionTokenContentType](docs/SessionTokenContentType.md) - [Model.SessionTokenSignRequest](docs/SessionTokenSignRequest.md) - [Model.SetConfigResponse](docs/SetConfigResponse.md) + - [Model.SetUpdateScheduleRequest](docs/SetUpdateScheduleRequest.md) - [Model.SubProvisionerKey](docs/SubProvisionerKey.md) - [Model.Subscriber](docs/Subscriber.md) + - [Model.TargetConnectionOptions](docs/TargetConnectionOptions.md) - [Model.TrafficEventResponse](docs/TrafficEventResponse.md) - [Model.TransportProtocolResponse](docs/TransportProtocolResponse.md) + - [Model.UpdateProductInfo](docs/UpdateProductInfo.md) + - [Model.UpdateRequestSchema](docs/UpdateRequestSchema.md) diff --git a/devolutions-gateway/openapi/dotnet-client/docs/GetUpdateProductsResponse.md b/devolutions-gateway/openapi/dotnet-client/docs/GetUpdateProductsResponse.md new file mode 100644 index 000000000..915f7d3cf --- /dev/null +++ b/devolutions-gateway/openapi/dotnet-client/docs/GetUpdateProductsResponse.md @@ -0,0 +1,12 @@ +# Devolutions.Gateway.Client.Model.GetUpdateProductsResponse +Installed version of each product, as reported by Devolutions Agent. + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**ManifestVersion** | **string** | Version of the `update_status.json` format in `\"major.minor\"` form (e.g. `\"1.1\"`). | +**Products** | [**Dictionary<string, UpdateProductInfo>**](UpdateProductInfo.md) | Map of product name to API-specific product info. | [optional] + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + diff --git a/devolutions-gateway/openapi/dotnet-client/docs/GetUpdateScheduleResponse.md b/devolutions-gateway/openapi/dotnet-client/docs/GetUpdateScheduleResponse.md new file mode 100644 index 000000000..8402ef8af --- /dev/null +++ b/devolutions-gateway/openapi/dotnet-client/docs/GetUpdateScheduleResponse.md @@ -0,0 +1,16 @@ +# Devolutions.Gateway.Client.Model.GetUpdateScheduleResponse +Current auto-update schedule for Devolutions Agent. + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**Enabled** | **bool** | Enable periodic Devolutions Agent self-update checks. | +**Interval** | **long** | Minimum interval between auto-update checks, in seconds. `0` means check once at `UpdateWindowStart`. | +**ManifestVersion** | **string** | Version of the `update_status.json` format in `\"major.minor\"` form (e.g. `\"1.1\"`). | +**Products** | **List<string>** | Products the agent autonomously polls for new versions. | [optional] +**UpdateWindowEnd** | **int?** | End of the maintenance window as seconds past midnight (local time, exclusive). `None` means no upper bound (single check at `UpdateWindowStart`). | [optional] +**UpdateWindowStart** | **int** | Start of the maintenance window as seconds past midnight (local time). | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + diff --git a/devolutions-gateway/openapi/dotnet-client/docs/HostScanStateDto.md b/devolutions-gateway/openapi/dotnet-client/docs/HostScanStateDto.md new file mode 100644 index 000000000..4e16a06f0 --- /dev/null +++ b/devolutions-gateway/openapi/dotnet-client/docs/HostScanStateDto.md @@ -0,0 +1,9 @@ +# Devolutions.Gateway.Client.Model.HostScanStateDto + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + diff --git a/devolutions-gateway/openapi/dotnet-client/docs/NetApi.md b/devolutions-gateway/openapi/dotnet-client/docs/NetApi.md index 5664a5b20..b3e5663e9 100644 --- a/devolutions-gateway/openapi/dotnet-client/docs/NetApi.md +++ b/devolutions-gateway/openapi/dotnet-client/docs/NetApi.md @@ -5,6 +5,8 @@ All URIs are relative to *http://localhost* | Method | HTTP request | Description | |--------|--------------|-------------| | [**GetNetConfig**](NetApi.md#getnetconfig) | **GET** /jet/net/config | Lists network interfaces | +| [**GetNetInterfaces**](NetApi.md#getnetinterfaces) | **GET** /jet/net/interfaces | Lists Gateway network scan sources. | +| [**GetNetScan**](NetApi.md#getnetscan) | **GET** /jet/net/scan | Stream network scan events over a websocket. | # **GetNetConfig** @@ -101,3 +103,258 @@ This endpoint does not need any parameter. [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + +# **GetNetInterfaces** +> NetworkInterfacesResponse GetNetInterfaces () + +Lists Gateway network scan sources. + +### Example +```csharp +using System.Collections.Generic; +using System.Diagnostics; +using System.Net.Http; +using Devolutions.Gateway.Client.Api; +using Devolutions.Gateway.Client.Client; +using Devolutions.Gateway.Client.Model; + +namespace Example +{ + public class GetNetInterfacesExample + { + public static void Main() + { + Configuration config = new Configuration(); + config.BasePath = "http://localhost"; + // Configure Bearer token for authorization: netscan_token + config.AccessToken = "YOUR_BEARER_TOKEN"; + + // create instances of HttpClient, HttpClientHandler to be reused later with different Api classes + HttpClient httpClient = new HttpClient(); + HttpClientHandler httpClientHandler = new HttpClientHandler(); + var apiInstance = new NetApi(httpClient, config, httpClientHandler); + + try + { + // Lists Gateway network scan sources. + NetworkInterfacesResponse result = apiInstance.GetNetInterfaces(); + Debug.WriteLine(result); + } + catch (ApiException e) + { + Debug.Print("Exception when calling NetApi.GetNetInterfaces: " + e.Message); + Debug.Print("Status Code: " + e.ErrorCode); + Debug.Print(e.StackTrace); + } + } + } +} +``` + +#### Using the GetNetInterfacesWithHttpInfo variant +This returns an ApiResponse object which contains the response data, status code and headers. + +```csharp +try +{ + // Lists Gateway network scan sources. + ApiResponse response = apiInstance.GetNetInterfacesWithHttpInfo(); + Debug.Write("Status Code: " + response.StatusCode); + Debug.Write("Response Headers: " + response.Headers); + Debug.Write("Response Body: " + response.Data); +} +catch (ApiException e) +{ + Debug.Print("Exception when calling NetApi.GetNetInterfacesWithHttpInfo: " + e.Message); + Debug.Print("Status Code: " + e.ErrorCode); + Debug.Print(e.StackTrace); +} +``` + +### Parameters +This endpoint does not need any parameter. +### Return type + +[**NetworkInterfacesResponse**](NetworkInterfacesResponse.md) + +### Authorization + +[netscan_token](../README.md#netscan_token) + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: application/json + + +### HTTP response details +| Status code | Description | Response headers | +|-------------|-------------|------------------| +| **200** | Gateway network scan sources | - | +| **400** | Bad request | - | +| **401** | Invalid or missing authorization token | - | +| **403** | Insufficient permissions | - | +| **500** | Unexpected server error | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + + +# **GetNetScan** +> void GetNetScan (long? pingInterval = null, long? pingTimeout = null, long? broadcastTimeout = null, long? portScanTimeout = null, long? netbiosTimeout = null, long? netbiosInterval = null, long? mdnsQueryTimeout = null, long? maxWait = null, List? range = null, List? target = null, List? interfaceId = null, List? probe = null, bool? enablePingStart = null, bool? enableBroadcast = null, bool? enableSubnetScan = null, bool? enableZeroconf = null, bool? enableNetbios = null, bool? enableResolveDns = null, bool? includeHostResults = null, bool? reportPingStart = null, bool? reportPingSuccess = null, bool? reportPingFailure = null, bool? enableTcpProbes = null, string? rangeInterfacePolicy = null, bool? allowCrossInterfaceRange = null, string? responseFormat = null, int? maxConcurrency = null, int? maxPingConcurrency = null, int? maxTcpProbeConcurrency = null, bool? enableFailure = null, bool? reportTcpFailure = null, bool? interfaceBindStrict = null) + +Stream network scan events over a websocket. + +The endpoint is upgraded from HTTP, so OpenAPI describes the **handshake**: the query parameters that drive the scan (validated before upgrade) and the legacy / v1 event payloads streamed back as JSON text frames. See `NetworkScanResultEvent` for the v1 shape and `LegacyScanEvent` for the legacy shape (selected via `response_format`). + +### Example +```csharp +using System.Collections.Generic; +using System.Diagnostics; +using System.Net.Http; +using Devolutions.Gateway.Client.Api; +using Devolutions.Gateway.Client.Client; +using Devolutions.Gateway.Client.Model; + +namespace Example +{ + public class GetNetScanExample + { + public static void Main() + { + Configuration config = new Configuration(); + config.BasePath = "http://localhost"; + // Configure Bearer token for authorization: netscan_token + config.AccessToken = "YOUR_BEARER_TOKEN"; + + // create instances of HttpClient, HttpClientHandler to be reused later with different Api classes + HttpClient httpClient = new HttpClient(); + HttpClientHandler httpClientHandler = new HttpClientHandler(); + var apiInstance = new NetApi(httpClient, config, httpClientHandler); + var pingInterval = 789L; // long? | Interval in milliseconds (default is 200) (optional) + var pingTimeout = 789L; // long? | Timeout in milliseconds (default is 500) (optional) + var broadcastTimeout = 789L; // long? | Timeout in milliseconds (default is 1000) (optional) + var portScanTimeout = 789L; // long? | Timeout in milliseconds (default is 1000) (optional) + var netbiosTimeout = 789L; // long? | Timeout in milliseconds (default is 1000) (optional) + var netbiosInterval = 789L; // long? | Interval in milliseconds (default is 200) (optional) + var mdnsQueryTimeout = 789L; // long? | The maximum time for each mdns query in milliseconds. (default is 5 * 1000) (optional) + var maxWait = 789L; // long? | The maximum duration for whole networking scan in milliseconds. Highly suggested! (optional) + var range = new List?(); // List? | The start and end IP address of the range to scan. for example: 10.10.0.0-10.10.0.255 (optional) + var target = new List?(); // List? | Explicit single-host targets to scan. Each value must parse as an IPv4 or IPv6 address; invalid values yield a structured `{ error: \"invalid_target\", value: \"\" }` 400 rather than a generic serde rejection at extraction time (mirrors the `range=` / `probe=` validation path). (optional) + var interfaceId = new List?(); // List? | Gateway network interface IDs to use as scan sources. (optional) + var probe = new List?(); // List? | The probes to run. Each value is either `ping`, a port number (`22`), or a named service (`rdp`, `https`, …). Validation is deferred to scan-time so failures can be surfaced as a structured 400 — naming the offending value — instead of a generic serde rejection at extraction time. (optional) + var enablePingStart = true; // bool? | **Legacy alias** for `report_ping_start`. Prefer the explicit name in new clients; kept so existing consumers don't break. (optional) + var enableBroadcast = true; // bool? | Enable the execution of broadcast scan (optional) + var enableSubnetScan = true; // bool? | Enable the ping scan on subnet (optional) + var enableZeroconf = true; // bool? | Enable ZeroConf/mDNS (optional) + var enableNetbios = true; // bool? | Enable NetBIOS name-service queries. Default `true` for backward compatibility. Set `false` (or pair with explicit `target=`) to keep NetBIOS from sweeping the surrounding subnet when the caller only wants results for the targets they listed. (optional) + var enableResolveDns = true; // bool? | Enable resolve dns (optional) + var includeHostResults = true; // bool? | Include host-only results. (optional) + var reportPingStart = true; // bool? | Emit ping queued/start host results. (optional) + var reportPingSuccess = true; // bool? | Emit ping success host results. (optional) + var reportPingFailure = true; // bool? | Emit ping failure host results. (optional) + var enableTcpProbes = true; // bool? | Enable TCP service probes. (optional) + var rangeInterfacePolicy = "rangeInterfacePolicy_example"; // string? | Policy applied when `range=` and `interface_id=` are both provided. Accepted values: `intersect_selected_interfaces` (default) or `allow_cross_interface_range`. Invalid values yield a structured `{ error: \"invalid_range_interface_policy\", value: \"\" }` 400 instead of a generic serde rejection (mirrors the `range=` / `probe=` / `target=` validation path). (optional) + var allowCrossInterfaceRange = true; // bool? | **Legacy alias** for `range_interface_policy=allow_cross_interface_range`. Prefer the structured policy in new clients. (optional) + var responseFormat = "responseFormat_example"; // string? | Response shape emitted on the websocket. Accepted values: `legacy` (default) or `network_scan_result_v1`. Invalid values yield a structured `{ error: \"invalid_response_format\", value: \"\" }` 400 instead of a generic serde rejection. (optional) + var maxConcurrency = 56; // int? | Maximum scanner concurrency. (optional) + var maxPingConcurrency = 56; // int? | Maximum ping probe concurrency. (optional) + var maxTcpProbeConcurrency = 56; // int? | Maximum TCP probe concurrency. (optional) + var enableFailure = true; // bool? | **Legacy alias** for `report_ping_failure`. `enable_failure=true` only opts into ping-failure events; TCP-probe failure events require the explicit `report_tcp_failure=true` and are not affected by this alias. **Behavior change:** historically this single flag controlled both ping-failure and TCP-probe-failure reporting. The two are now split: clients that want the old \"both at once\" semantics must send `enable_failure=true&report_tcp_failure=true` together. The split is intentional — TCP-probe failures are typically high-volume noise that callers were filtering client-side anyway, so the two streams are independently gated. (optional) + var reportTcpFailure = true; // bool? | Enable TCP port knocking failure events. (optional) + var interfaceBindStrict = true; // bool? | When `true`, fail with HTTP 400 if a ping/TCP-probe socket cannot be bound to the planner-selected interface. Default `false` (warn and fall back to default routing). (optional) + + try + { + // Stream network scan events over a websocket. + apiInstance.GetNetScan(pingInterval, pingTimeout, broadcastTimeout, portScanTimeout, netbiosTimeout, netbiosInterval, mdnsQueryTimeout, maxWait, range, target, interfaceId, probe, enablePingStart, enableBroadcast, enableSubnetScan, enableZeroconf, enableNetbios, enableResolveDns, includeHostResults, reportPingStart, reportPingSuccess, reportPingFailure, enableTcpProbes, rangeInterfacePolicy, allowCrossInterfaceRange, responseFormat, maxConcurrency, maxPingConcurrency, maxTcpProbeConcurrency, enableFailure, reportTcpFailure, interfaceBindStrict); + } + catch (ApiException e) + { + Debug.Print("Exception when calling NetApi.GetNetScan: " + e.Message); + Debug.Print("Status Code: " + e.ErrorCode); + Debug.Print(e.StackTrace); + } + } + } +} +``` + +#### Using the GetNetScanWithHttpInfo variant +This returns an ApiResponse object which contains the response data, status code and headers. + +```csharp +try +{ + // Stream network scan events over a websocket. + apiInstance.GetNetScanWithHttpInfo(pingInterval, pingTimeout, broadcastTimeout, portScanTimeout, netbiosTimeout, netbiosInterval, mdnsQueryTimeout, maxWait, range, target, interfaceId, probe, enablePingStart, enableBroadcast, enableSubnetScan, enableZeroconf, enableNetbios, enableResolveDns, includeHostResults, reportPingStart, reportPingSuccess, reportPingFailure, enableTcpProbes, rangeInterfacePolicy, allowCrossInterfaceRange, responseFormat, maxConcurrency, maxPingConcurrency, maxTcpProbeConcurrency, enableFailure, reportTcpFailure, interfaceBindStrict); +} +catch (ApiException e) +{ + Debug.Print("Exception when calling NetApi.GetNetScanWithHttpInfo: " + e.Message); + Debug.Print("Status Code: " + e.ErrorCode); + Debug.Print(e.StackTrace); +} +``` + +### Parameters + +| Name | Type | Description | Notes | +|------|------|-------------|-------| +| **pingInterval** | **long?** | Interval in milliseconds (default is 200) | [optional] | +| **pingTimeout** | **long?** | Timeout in milliseconds (default is 500) | [optional] | +| **broadcastTimeout** | **long?** | Timeout in milliseconds (default is 1000) | [optional] | +| **portScanTimeout** | **long?** | Timeout in milliseconds (default is 1000) | [optional] | +| **netbiosTimeout** | **long?** | Timeout in milliseconds (default is 1000) | [optional] | +| **netbiosInterval** | **long?** | Interval in milliseconds (default is 200) | [optional] | +| **mdnsQueryTimeout** | **long?** | The maximum time for each mdns query in milliseconds. (default is 5 * 1000) | [optional] | +| **maxWait** | **long?** | The maximum duration for whole networking scan in milliseconds. Highly suggested! | [optional] | +| **range** | [**List<string>?**](string.md) | The start and end IP address of the range to scan. for example: 10.10.0.0-10.10.0.255 | [optional] | +| **target** | [**List<string>?**](string.md) | Explicit single-host targets to scan. Each value must parse as an IPv4 or IPv6 address; invalid values yield a structured `{ error: \"invalid_target\", value: \"<raw>\" }` 400 rather than a generic serde rejection at extraction time (mirrors the `range=` / `probe=` validation path). | [optional] | +| **interfaceId** | [**List<string>?**](string.md) | Gateway network interface IDs to use as scan sources. | [optional] | +| **probe** | [**List<string>?**](string.md) | The probes to run. Each value is either `ping`, a port number (`22`), or a named service (`rdp`, `https`, …). Validation is deferred to scan-time so failures can be surfaced as a structured 400 — naming the offending value — instead of a generic serde rejection at extraction time. | [optional] | +| **enablePingStart** | **bool?** | **Legacy alias** for `report_ping_start`. Prefer the explicit name in new clients; kept so existing consumers don't break. | [optional] | +| **enableBroadcast** | **bool?** | Enable the execution of broadcast scan | [optional] | +| **enableSubnetScan** | **bool?** | Enable the ping scan on subnet | [optional] | +| **enableZeroconf** | **bool?** | Enable ZeroConf/mDNS | [optional] | +| **enableNetbios** | **bool?** | Enable NetBIOS name-service queries. Default `true` for backward compatibility. Set `false` (or pair with explicit `target=`) to keep NetBIOS from sweeping the surrounding subnet when the caller only wants results for the targets they listed. | [optional] | +| **enableResolveDns** | **bool?** | Enable resolve dns | [optional] | +| **includeHostResults** | **bool?** | Include host-only results. | [optional] | +| **reportPingStart** | **bool?** | Emit ping queued/start host results. | [optional] | +| **reportPingSuccess** | **bool?** | Emit ping success host results. | [optional] | +| **reportPingFailure** | **bool?** | Emit ping failure host results. | [optional] | +| **enableTcpProbes** | **bool?** | Enable TCP service probes. | [optional] | +| **rangeInterfacePolicy** | **string?** | Policy applied when `range=` and `interface_id=` are both provided. Accepted values: `intersect_selected_interfaces` (default) or `allow_cross_interface_range`. Invalid values yield a structured `{ error: \"invalid_range_interface_policy\", value: \"<raw>\" }` 400 instead of a generic serde rejection (mirrors the `range=` / `probe=` / `target=` validation path). | [optional] | +| **allowCrossInterfaceRange** | **bool?** | **Legacy alias** for `range_interface_policy=allow_cross_interface_range`. Prefer the structured policy in new clients. | [optional] | +| **responseFormat** | **string?** | Response shape emitted on the websocket. Accepted values: `legacy` (default) or `network_scan_result_v1`. Invalid values yield a structured `{ error: \"invalid_response_format\", value: \"<raw>\" }` 400 instead of a generic serde rejection. | [optional] | +| **maxConcurrency** | **int?** | Maximum scanner concurrency. | [optional] | +| **maxPingConcurrency** | **int?** | Maximum ping probe concurrency. | [optional] | +| **maxTcpProbeConcurrency** | **int?** | Maximum TCP probe concurrency. | [optional] | +| **enableFailure** | **bool?** | **Legacy alias** for `report_ping_failure`. `enable_failure=true` only opts into ping-failure events; TCP-probe failure events require the explicit `report_tcp_failure=true` and are not affected by this alias. **Behavior change:** historically this single flag controlled both ping-failure and TCP-probe-failure reporting. The two are now split: clients that want the old \"both at once\" semantics must send `enable_failure=true&report_tcp_failure=true` together. The split is intentional — TCP-probe failures are typically high-volume noise that callers were filtering client-side anyway, so the two streams are independently gated. | [optional] | +| **reportTcpFailure** | **bool?** | Enable TCP port knocking failure events. | [optional] | +| **interfaceBindStrict** | **bool?** | When `true`, fail with HTTP 400 if a ping/TCP-probe socket cannot be bound to the planner-selected interface. Default `false` (warn and fall back to default routing). | [optional] | + +### Return type + +void (empty response body) + +### Authorization + +[netscan_token](../README.md#netscan_token) + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: Not defined + + +### HTTP response details +| Status code | Description | Response headers | +|-------------|-------------|------------------| +| **101** | WebSocket upgrade; subsequent text frames carry NetworkScanResultEvent or LegacyScanEvent JSON | - | +| **400** | Invalid query, mixed target/range, oversized range, or selected interface error | - | +| **401** | Invalid or missing authorization token | - | +| **403** | Insufficient permissions | - | +| **500** | Unexpected server error | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + diff --git a/devolutions-gateway/openapi/dotnet-client/docs/NetworkInterfaceDto.md b/devolutions-gateway/openapi/dotnet-client/docs/NetworkInterfaceDto.md new file mode 100644 index 000000000..43198fc8e --- /dev/null +++ b/devolutions-gateway/openapi/dotnet-client/docs/NetworkInterfaceDto.md @@ -0,0 +1,18 @@ +# Devolutions.Gateway.Client.Model.NetworkInterfaceDto + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**Description** | **string** | | [optional] +**Id** | **string** | | +**Index** | **int?** | | [optional] +**IsUp** | **bool?** | | [optional] +**LinkType** | **string** | Coarse link type: `ethernet`, `wifi`, `loopback`, `virtual`, `unknown`. | [optional] +**MacAddress** | **string** | | [optional] +**Mtu** | **int?** | MTU in bytes when known. | [optional] +**Name** | **string** | | +**SpeedMbps** | **long?** | Link speed in megabits per second when reported by the OS. | [optional] + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + diff --git a/devolutions-gateway/openapi/dotnet-client/docs/NetworkInterfacesResponse.md b/devolutions-gateway/openapi/dotnet-client/docs/NetworkInterfacesResponse.md new file mode 100644 index 000000000..179220e3d --- /dev/null +++ b/devolutions-gateway/openapi/dotnet-client/docs/NetworkInterfacesResponse.md @@ -0,0 +1,10 @@ +# Devolutions.Gateway.Client.Model.NetworkInterfacesResponse + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**Interfaces** | [**List<NetworkScanSourceDto>**](NetworkScanSourceDto.md) | | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + diff --git a/devolutions-gateway/openapi/dotnet-client/docs/NetworkScanResultEventDto.md b/devolutions-gateway/openapi/dotnet-client/docs/NetworkScanResultEventDto.md new file mode 100644 index 000000000..4b24baa49 --- /dev/null +++ b/devolutions-gateway/openapi/dotnet-client/docs/NetworkScanResultEventDto.md @@ -0,0 +1,23 @@ +# Devolutions.Gateway.Client.Model.NetworkScanResultEventDto + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**Address** | **string** | | +**DiscoverySource** | **ScanResultSourceDto** | | +**HostName** | **string** | | [optional] +**HostScanState** | **HostScanStateDto** | | [optional] +**InterfaceId** | **string** | | [optional] +**InterfaceName** | **string** | | [optional] +**IsReachable** | **bool?** | | [optional] +**Kind** | **NetworkScanResultKindDto** | | +**MacAddress** | **string** | | [optional] +**Port** | **int?** | | [optional] +**ResponseTimeMs** | **int?** | | [optional] +**ServiceLabel** | **string** | | [optional] +**ServiceType** | **string** | | [optional] +**Source** | **ScanOriginDto** | | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + diff --git a/devolutions-gateway/openapi/dotnet-client/docs/NetworkScanResultKindDto.md b/devolutions-gateway/openapi/dotnet-client/docs/NetworkScanResultKindDto.md new file mode 100644 index 000000000..d9975c71e --- /dev/null +++ b/devolutions-gateway/openapi/dotnet-client/docs/NetworkScanResultKindDto.md @@ -0,0 +1,9 @@ +# Devolutions.Gateway.Client.Model.NetworkScanResultKindDto + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + diff --git a/devolutions-gateway/openapi/dotnet-client/docs/NetworkScanSourceCapabilitiesDto.md b/devolutions-gateway/openapi/dotnet-client/docs/NetworkScanSourceCapabilitiesDto.md new file mode 100644 index 000000000..72cbe518d --- /dev/null +++ b/devolutions-gateway/openapi/dotnet-client/docs/NetworkScanSourceCapabilitiesDto.md @@ -0,0 +1,16 @@ +# Devolutions.Gateway.Client.Model.NetworkScanSourceCapabilitiesDto + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**Broadcast** | **bool** | | +**DnsResolve** | **bool** | | +**Ipv4** | **bool** | | +**Ipv6** | **bool** | | +**Subnet** | **bool** | | +**TcpProbe** | **bool** | | +**ZeroConf** | **bool** | | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + diff --git a/devolutions-gateway/openapi/dotnet-client/docs/NetworkScanSourceDto.md b/devolutions-gateway/openapi/dotnet-client/docs/NetworkScanSourceDto.md new file mode 100644 index 000000000..712c6ac24 --- /dev/null +++ b/devolutions-gateway/openapi/dotnet-client/docs/NetworkScanSourceDto.md @@ -0,0 +1,16 @@ +# Devolutions.Gateway.Client.Model.NetworkScanSourceDto + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**Address** | **string** | | +**BroadcastAddress** | **string** | | [optional] +**Capabilities** | [**NetworkScanSourceCapabilitiesDto**](NetworkScanSourceCapabilitiesDto.md) | | +**EndAddress** | **string** | | +**Interface** | [**NetworkInterfaceDto**](NetworkInterfaceDto.md) | | +**PrefixLength** | **int?** | | [optional] +**StartAddress** | **string** | | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + diff --git a/devolutions-gateway/openapi/dotnet-client/docs/PreflightOperation.md b/devolutions-gateway/openapi/dotnet-client/docs/PreflightOperation.md index 926754235..02cc704da 100644 --- a/devolutions-gateway/openapi/dotnet-client/docs/PreflightOperation.md +++ b/devolutions-gateway/openapi/dotnet-client/docs/PreflightOperation.md @@ -4,13 +4,14 @@ Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- +**ConnectionOptions** | [**TargetConnectionOptions**](TargetConnectionOptions.md) | | [optional] **HostToResolve** | **string** | The hostname to perform DNS resolution on. Required for \"resolve-host\" kind. | [optional] **Id** | **Guid** | Unique ID identifying the preflight operation. | **Kind** | **PreflightOperationKind** | | **ProxyCredential** | [**AppCredential**](AppCredential.md) | | [optional] **TargetCredential** | [**AppCredential**](AppCredential.md) | | [optional] -**TimeToLive** | **int?** | Minimum persistance duration in seconds for the data provisioned via this operation. Optional parameter for \"provision-token\" and \"provision-credentials\" kinds. | [optional] -**Token** | **string** | The token to be stored on the proxy-side. Required for \"provision-token\" and \"provision-credentials\" kinds. | [optional] +**TimeToLive** | **int?** | Minimum persistence duration in seconds for the data provisioned via this operation. Optional parameter for \"provision-token\", \"provision-credentials\" and \"provision-connection-options\" kinds. | [optional] +**Token** | **string** | The token to be stored on the proxy-side. Required for \"provision-token\", \"provision-credentials\" and \"provision-connection-options\" kinds. | [optional] [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/devolutions-gateway/openapi/dotnet-client/docs/ScanOriginDto.md b/devolutions-gateway/openapi/dotnet-client/docs/ScanOriginDto.md new file mode 100644 index 000000000..8256f5282 --- /dev/null +++ b/devolutions-gateway/openapi/dotnet-client/docs/ScanOriginDto.md @@ -0,0 +1,9 @@ +# Devolutions.Gateway.Client.Model.ScanOriginDto + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + diff --git a/devolutions-gateway/openapi/dotnet-client/docs/ScanResultSourceDto.md b/devolutions-gateway/openapi/dotnet-client/docs/ScanResultSourceDto.md new file mode 100644 index 000000000..09e07d005 --- /dev/null +++ b/devolutions-gateway/openapi/dotnet-client/docs/ScanResultSourceDto.md @@ -0,0 +1,9 @@ +# Devolutions.Gateway.Client.Model.ScanResultSourceDto + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + diff --git a/devolutions-gateway/openapi/dotnet-client/docs/SetUpdateScheduleRequest.md b/devolutions-gateway/openapi/dotnet-client/docs/SetUpdateScheduleRequest.md new file mode 100644 index 000000000..90d7fc8a4 --- /dev/null +++ b/devolutions-gateway/openapi/dotnet-client/docs/SetUpdateScheduleRequest.md @@ -0,0 +1,15 @@ +# Devolutions.Gateway.Client.Model.SetUpdateScheduleRequest +Desired auto-update schedule to apply to Devolutions Agent. + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**Enabled** | **bool** | Enable periodic Devolutions Agent self-update checks. | +**Interval** | **long** | Minimum interval between auto-update checks, in seconds. `0` means check once at `UpdateWindowStart` (default). | [optional] +**Products** | **List<string>** | Products the agent autonomously polls for new versions (default: empty). | [optional] +**UpdateWindowEnd** | **int?** | End of the maintenance window as seconds past midnight in local time, exclusive. `null` (default) means no upper bound - a single check fires at `UpdateWindowStart`. When end < start the window crosses midnight. | [optional] +**UpdateWindowStart** | **int** | Start of the maintenance window as seconds past midnight in local time (default: `7200` = 02:00). | [optional] + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + diff --git a/devolutions-gateway/openapi/dotnet-client/docs/TargetConnectionOptions.md b/devolutions-gateway/openapi/dotnet-client/docs/TargetConnectionOptions.md new file mode 100644 index 000000000..a87c52cb5 --- /dev/null +++ b/devolutions-gateway/openapi/dotnet-client/docs/TargetConnectionOptions.md @@ -0,0 +1,10 @@ +# Devolutions.Gateway.Client.Model.TargetConnectionOptions + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**KrbKdc** | **string** | Kerberos KDC address for the target-side CredSSP connection. Supported schemes are `tcp` and `udp`. | [optional] + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + diff --git a/devolutions-gateway/openapi/dotnet-client/docs/UpdateApi.md b/devolutions-gateway/openapi/dotnet-client/docs/UpdateApi.md index 2cc70c368..ffd85c00a 100644 --- a/devolutions-gateway/openapi/dotnet-client/docs/UpdateApi.md +++ b/devolutions-gateway/openapi/dotnet-client/docs/UpdateApi.md @@ -4,15 +4,315 @@ All URIs are relative to *http://localhost* | Method | HTTP request | Description | |--------|--------------|-------------| -| [**TriggerUpdate**](UpdateApi.md#triggerupdate) | **POST** /jet/update | Triggers Devolutions Gateway update process. | +| [**GetUpdateProducts**](UpdateApi.md#getupdateproducts) | **GET** /jet/update | Retrieve the currently installed version of each Devolutions product. | +| [**GetUpdateSchedule**](UpdateApi.md#getupdateschedule) | **GET** /jet/update/schedule | Retrieve the current Devolutions Agent auto-update schedule. | +| [**SetUpdateSchedule**](UpdateApi.md#setupdateschedule) | **POST** /jet/update/schedule | Set the Devolutions Agent auto-update schedule. | +| [**TriggerUpdate**](UpdateApi.md#triggerupdate) | **POST** /jet/update | Trigger an update for one or more Devolutions products. | + + +# **GetUpdateProducts** +> GetUpdateProductsResponse GetUpdateProducts () + +Retrieve the currently installed version of each Devolutions product. + +Reads `update_status.json`, which is written by the Devolutions Agent on startup and refreshed after every update run. When the file does not exist (agent not installed or is an older version), the endpoint returns HTTP 503 because updater status is unavailable. + +### Example +```csharp +using System.Collections.Generic; +using System.Diagnostics; +using System.Net.Http; +using Devolutions.Gateway.Client.Api; +using Devolutions.Gateway.Client.Client; +using Devolutions.Gateway.Client.Model; + +namespace Example +{ + public class GetUpdateProductsExample + { + public static void Main() + { + Configuration config = new Configuration(); + config.BasePath = "http://localhost"; + // Configure Bearer token for authorization: scope_token + config.AccessToken = "YOUR_BEARER_TOKEN"; + + // create instances of HttpClient, HttpClientHandler to be reused later with different Api classes + HttpClient httpClient = new HttpClient(); + HttpClientHandler httpClientHandler = new HttpClientHandler(); + var apiInstance = new UpdateApi(httpClient, config, httpClientHandler); + + try + { + // Retrieve the currently installed version of each Devolutions product. + GetUpdateProductsResponse result = apiInstance.GetUpdateProducts(); + Debug.WriteLine(result); + } + catch (ApiException e) + { + Debug.Print("Exception when calling UpdateApi.GetUpdateProducts: " + e.Message); + Debug.Print("Status Code: " + e.ErrorCode); + Debug.Print(e.StackTrace); + } + } + } +} +``` + +#### Using the GetUpdateProductsWithHttpInfo variant +This returns an ApiResponse object which contains the response data, status code and headers. + +```csharp +try +{ + // Retrieve the currently installed version of each Devolutions product. + ApiResponse response = apiInstance.GetUpdateProductsWithHttpInfo(); + Debug.Write("Status Code: " + response.StatusCode); + Debug.Write("Response Headers: " + response.Headers); + Debug.Write("Response Body: " + response.Data); +} +catch (ApiException e) +{ + Debug.Print("Exception when calling UpdateApi.GetUpdateProductsWithHttpInfo: " + e.Message); + Debug.Print("Status Code: " + e.ErrorCode); + Debug.Print(e.StackTrace); +} +``` + +### Parameters +This endpoint does not need any parameter. +### Return type + +[**GetUpdateProductsResponse**](GetUpdateProductsResponse.md) + +### Authorization + +[scope_token](../README.md#scope_token) + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: application/json + + +### HTTP response details +| Status code | Description | Response headers | +|-------------|-------------|------------------| +| **200** | Installed product versions | - | +| **401** | Invalid or missing authorization token | - | +| **403** | Insufficient permissions | - | +| **500** | Failed to read agent status file | - | +| **503** | Agent updater service is unavailable | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + + +# **GetUpdateSchedule** +> GetUpdateScheduleResponse GetUpdateSchedule () + +Retrieve the current Devolutions Agent auto-update schedule. + +Reads the `Schedule` field from `update_status.json`. When the field is absent the response contains zeroed defaults (`Enabled: false`, interval `0`, window start `0`, no products). + +### Example +```csharp +using System.Collections.Generic; +using System.Diagnostics; +using System.Net.Http; +using Devolutions.Gateway.Client.Api; +using Devolutions.Gateway.Client.Client; +using Devolutions.Gateway.Client.Model; + +namespace Example +{ + public class GetUpdateScheduleExample + { + public static void Main() + { + Configuration config = new Configuration(); + config.BasePath = "http://localhost"; + // Configure Bearer token for authorization: scope_token + config.AccessToken = "YOUR_BEARER_TOKEN"; + + // create instances of HttpClient, HttpClientHandler to be reused later with different Api classes + HttpClient httpClient = new HttpClient(); + HttpClientHandler httpClientHandler = new HttpClientHandler(); + var apiInstance = new UpdateApi(httpClient, config, httpClientHandler); + + try + { + // Retrieve the current Devolutions Agent auto-update schedule. + GetUpdateScheduleResponse result = apiInstance.GetUpdateSchedule(); + Debug.WriteLine(result); + } + catch (ApiException e) + { + Debug.Print("Exception when calling UpdateApi.GetUpdateSchedule: " + e.Message); + Debug.Print("Status Code: " + e.ErrorCode); + Debug.Print(e.StackTrace); + } + } + } +} +``` + +#### Using the GetUpdateScheduleWithHttpInfo variant +This returns an ApiResponse object which contains the response data, status code and headers. + +```csharp +try +{ + // Retrieve the current Devolutions Agent auto-update schedule. + ApiResponse response = apiInstance.GetUpdateScheduleWithHttpInfo(); + Debug.Write("Status Code: " + response.StatusCode); + Debug.Write("Response Headers: " + response.Headers); + Debug.Write("Response Body: " + response.Data); +} +catch (ApiException e) +{ + Debug.Print("Exception when calling UpdateApi.GetUpdateScheduleWithHttpInfo: " + e.Message); + Debug.Print("Status Code: " + e.ErrorCode); + Debug.Print(e.StackTrace); +} +``` + +### Parameters +This endpoint does not need any parameter. +### Return type + +[**GetUpdateScheduleResponse**](GetUpdateScheduleResponse.md) + +### Authorization + +[scope_token](../README.md#scope_token) + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: application/json + + +### HTTP response details +| Status code | Description | Response headers | +|-------------|-------------|------------------| +| **200** | Current auto-update schedule | - | +| **401** | Invalid or missing authorization token | - | +| **403** | Insufficient permissions | - | +| **500** | Failed to read agent status file | - | +| **503** | Agent updater service is unavailable | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + + +# **SetUpdateSchedule** +> Object SetUpdateSchedule (SetUpdateScheduleRequest setUpdateScheduleRequest) + +Set the Devolutions Agent auto-update schedule. + +Writes the `Schedule` field into `update.json`. The agent watches this file and applies the new schedule immediately, then persists it to `agent.json`. All other fields in `update.json` are preserved; the `VersionMinor` field is reset to the minor version this gateway build understands so the agent does not see an unknown future version. + +### Example +```csharp +using System.Collections.Generic; +using System.Diagnostics; +using System.Net.Http; +using Devolutions.Gateway.Client.Api; +using Devolutions.Gateway.Client.Client; +using Devolutions.Gateway.Client.Model; + +namespace Example +{ + public class SetUpdateScheduleExample + { + public static void Main() + { + Configuration config = new Configuration(); + config.BasePath = "http://localhost"; + // Configure Bearer token for authorization: scope_token + config.AccessToken = "YOUR_BEARER_TOKEN"; + + // create instances of HttpClient, HttpClientHandler to be reused later with different Api classes + HttpClient httpClient = new HttpClient(); + HttpClientHandler httpClientHandler = new HttpClientHandler(); + var apiInstance = new UpdateApi(httpClient, config, httpClientHandler); + var setUpdateScheduleRequest = new SetUpdateScheduleRequest(); // SetUpdateScheduleRequest | + + try + { + // Set the Devolutions Agent auto-update schedule. + Object result = apiInstance.SetUpdateSchedule(setUpdateScheduleRequest); + Debug.WriteLine(result); + } + catch (ApiException e) + { + Debug.Print("Exception when calling UpdateApi.SetUpdateSchedule: " + e.Message); + Debug.Print("Status Code: " + e.ErrorCode); + Debug.Print(e.StackTrace); + } + } + } +} +``` + +#### Using the SetUpdateScheduleWithHttpInfo variant +This returns an ApiResponse object which contains the response data, status code and headers. + +```csharp +try +{ + // Set the Devolutions Agent auto-update schedule. + ApiResponse response = apiInstance.SetUpdateScheduleWithHttpInfo(setUpdateScheduleRequest); + Debug.Write("Status Code: " + response.StatusCode); + Debug.Write("Response Headers: " + response.Headers); + Debug.Write("Response Body: " + response.Data); +} +catch (ApiException e) +{ + Debug.Print("Exception when calling UpdateApi.SetUpdateScheduleWithHttpInfo: " + e.Message); + Debug.Print("Status Code: " + e.ErrorCode); + Debug.Print(e.StackTrace); +} +``` + +### Parameters + +| Name | Type | Description | Notes | +|------|------|-------------|-------| +| **setUpdateScheduleRequest** | [**SetUpdateScheduleRequest**](SetUpdateScheduleRequest.md) | | | + +### Return type + +**Object** + +### Authorization + +[scope_token](../README.md#scope_token) + +### HTTP request headers + + - **Content-Type**: application/json + - **Accept**: application/json + + +### HTTP response details +| Status code | Description | Response headers | +|-------------|-------------|------------------| +| **200** | Auto-update schedule applied | - | +| **400** | Bad request | - | +| **401** | Invalid or missing authorization token | - | +| **403** | Insufficient permissions | - | +| **500** | Failed to write update manifest | - | +| **503** | Agent updater service is unavailable | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) # **TriggerUpdate** -> Object TriggerUpdate (string version) +> Object TriggerUpdate (string? version = null, UpdateRequestSchema? updateRequestSchema = null) -Triggers Devolutions Gateway update process. +Trigger an update for one or more Devolutions products. -This is done via updating `Agent/update.json` file, which is then read by Devolutions Agent when changes are detected. If the version written to `update.json` is indeed higher than the currently installed version, Devolutions Agent will proceed with the update process. +Writes the requested version(s) into `Agent/update.json`, which is watched by Devolutions Agent. When a requested version is higher than the installed version the agent proceeds with the update. **Body form** (preferred): pass a JSON body with a `Products` map. **Query-param form** (legacy, gateway-only): `POST /jet/update?version=latest`. This form updates only the Gateway product and is kept for backward compatibility. Both forms cannot be used simultaneously; doing so returns HTTP 400. ### Example ```csharp @@ -38,12 +338,13 @@ namespace Example HttpClient httpClient = new HttpClient(); HttpClientHandler httpClientHandler = new HttpClientHandler(); var apiInstance = new UpdateApi(httpClient, config, httpClientHandler); - var version = "version_example"; // string | The version to install; use 'latest' for the latest version, or 'w.x.y.z' for a specific version + var version = "version_example"; // string? | Gateway-only target version; use the request body for multi-product updates (optional) + var updateRequestSchema = new UpdateRequestSchema?(); // UpdateRequestSchema? | Products and target versions to update (optional) try { - // Triggers Devolutions Gateway update process. - Object result = apiInstance.TriggerUpdate(version); + // Trigger an update for one or more Devolutions products. + Object result = apiInstance.TriggerUpdate(version, updateRequestSchema); Debug.WriteLine(result); } catch (ApiException e) @@ -63,8 +364,8 @@ This returns an ApiResponse object which contains the response data, status code ```csharp try { - // Triggers Devolutions Gateway update process. - ApiResponse response = apiInstance.TriggerUpdateWithHttpInfo(version); + // Trigger an update for one or more Devolutions products. + ApiResponse response = apiInstance.TriggerUpdateWithHttpInfo(version, updateRequestSchema); Debug.Write("Status Code: " + response.StatusCode); Debug.Write("Response Headers: " + response.Headers); Debug.Write("Response Body: " + response.Data); @@ -81,7 +382,8 @@ catch (ApiException e) | Name | Type | Description | Notes | |------|------|-------------|-------| -| **version** | **string** | The version to install; use 'latest' for the latest version, or 'w.x.y.z' for a specific version | | +| **version** | **string?** | Gateway-only target version; use the request body for multi-product updates | [optional] | +| **updateRequestSchema** | [**UpdateRequestSchema?**](UpdateRequestSchema?.md) | Products and target versions to update | [optional] | ### Return type @@ -93,18 +395,18 @@ catch (ApiException e) ### HTTP request headers - - **Content-Type**: Not defined + - **Content-Type**: application/json - **Accept**: application/json ### HTTP response details | Status code | Description | Response headers | |-------------|-------------|------------------| -| **200** | Update request has been processed successfully | - | +| **200** | Update request accepted | - | | **400** | Bad request | - | | **401** | Invalid or missing authorization token | - | | **403** | Insufficient permissions | - | -| **500** | Agent updater service is malfunctioning | - | +| **500** | Failed to write update manifest | - | | **503** | Agent updater service is unavailable | - | [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) diff --git a/devolutions-gateway/openapi/dotnet-client/docs/UpdateProductInfo.md b/devolutions-gateway/openapi/dotnet-client/docs/UpdateProductInfo.md new file mode 100644 index 000000000..fc61d2909 --- /dev/null +++ b/devolutions-gateway/openapi/dotnet-client/docs/UpdateProductInfo.md @@ -0,0 +1,11 @@ +# Devolutions.Gateway.Client.Model.UpdateProductInfo +Per-product update information. + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**VarVersion** | **string** | Requested or installed version: `\"latest\"` or `\"YYYY.M.D\"` / `\"YYYY.M.D.R\"`. | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + diff --git a/devolutions-gateway/openapi/dotnet-client/docs/UpdateRequestSchema.md b/devolutions-gateway/openapi/dotnet-client/docs/UpdateRequestSchema.md new file mode 100644 index 000000000..161ad7b7b --- /dev/null +++ b/devolutions-gateway/openapi/dotnet-client/docs/UpdateRequestSchema.md @@ -0,0 +1,11 @@ +# Devolutions.Gateway.Client.Model.UpdateRequestSchema +OpenAPI schema for the update request body. The API accepts an object containing a `Products` map, whose keys are product names and whose values are update information. + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**Products** | [**Dictionary<string, UpdateProductInfo>**](UpdateProductInfo.md) | Map of product name to update information. | [optional] + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Api/ConfigApi.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Api/ConfigApi.cs index e83d5a92c..49012f143 100644 --- a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Api/ConfigApi.cs +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Api/ConfigApi.cs @@ -3,7 +3,7 @@ * * Protocol-aware fine-grained relay server * - * The version of the OpenAPI document: 2025.3.2 + * The version of the OpenAPI document: 2026.2.4 * Contact: infos@devolutions.net * Generated by: https://github.com/openapitools/openapi-generator.git */ diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Api/DiagnosticsApi.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Api/DiagnosticsApi.cs index 96d0a7d9d..9d9540715 100644 --- a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Api/DiagnosticsApi.cs +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Api/DiagnosticsApi.cs @@ -3,7 +3,7 @@ * * Protocol-aware fine-grained relay server * - * The version of the OpenAPI document: 2025.3.2 + * The version of the OpenAPI document: 2026.2.4 * Contact: infos@devolutions.net * Generated by: https://github.com/openapitools/openapi-generator.git */ diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Api/HealthApi.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Api/HealthApi.cs index f1d27e6d3..328a0ba4a 100644 --- a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Api/HealthApi.cs +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Api/HealthApi.cs @@ -3,7 +3,7 @@ * * Protocol-aware fine-grained relay server * - * The version of the OpenAPI document: 2025.3.2 + * The version of the OpenAPI document: 2026.2.4 * Contact: infos@devolutions.net * Generated by: https://github.com/openapitools/openapi-generator.git */ diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Api/HeartbeatApi.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Api/HeartbeatApi.cs index 8c4e70250..5a5a67302 100644 --- a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Api/HeartbeatApi.cs +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Api/HeartbeatApi.cs @@ -3,7 +3,7 @@ * * Protocol-aware fine-grained relay server * - * The version of the OpenAPI document: 2025.3.2 + * The version of the OpenAPI document: 2026.2.4 * Contact: infos@devolutions.net * Generated by: https://github.com/openapitools/openapi-generator.git */ diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Api/JrecApi.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Api/JrecApi.cs index cd77b2d8d..dd2fe3342 100644 --- a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Api/JrecApi.cs +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Api/JrecApi.cs @@ -3,7 +3,7 @@ * * Protocol-aware fine-grained relay server * - * The version of the OpenAPI document: 2025.3.2 + * The version of the OpenAPI document: 2026.2.4 * Contact: infos@devolutions.net * Generated by: https://github.com/openapitools/openapi-generator.git */ diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Api/JrlApi.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Api/JrlApi.cs index 81eb23a4c..79c8443e5 100644 --- a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Api/JrlApi.cs +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Api/JrlApi.cs @@ -3,7 +3,7 @@ * * Protocol-aware fine-grained relay server * - * The version of the OpenAPI document: 2025.3.2 + * The version of the OpenAPI document: 2026.2.4 * Contact: infos@devolutions.net * Generated by: https://github.com/openapitools/openapi-generator.git */ diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Api/NetApi.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Api/NetApi.cs index a0ab71e31..265d086e9 100644 --- a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Api/NetApi.cs +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Api/NetApi.cs @@ -3,7 +3,7 @@ * * Protocol-aware fine-grained relay server * - * The version of the OpenAPI document: 2025.3.2 + * The version of the OpenAPI document: 2026.2.4 * Contact: infos@devolutions.net * Generated by: https://github.com/openapitools/openapi-generator.git */ @@ -44,6 +44,105 @@ public interface INetApiSync : IApiAccessor /// Thrown when fails to make API call /// ApiResponse of List<Dictionary<string, List<InterfaceInfo>>> ApiResponse>>> GetNetConfigWithHttpInfo(); + /// + /// Lists Gateway network scan sources. + /// + /// Thrown when fails to make API call + /// NetworkInterfacesResponse + NetworkInterfacesResponse GetNetInterfaces(); + + /// + /// Lists Gateway network scan sources. + /// + /// + /// + /// + /// Thrown when fails to make API call + /// ApiResponse of NetworkInterfacesResponse + ApiResponse GetNetInterfacesWithHttpInfo(); + /// + /// Stream network scan events over a websocket. + /// + /// + /// The endpoint is upgraded from HTTP, so OpenAPI describes the **handshake**: the query parameters that drive the scan (validated before upgrade) and the legacy / v1 event payloads streamed back as JSON text frames. See `NetworkScanResultEvent` for the v1 shape and `LegacyScanEvent` for the legacy shape (selected via `response_format`). + /// + /// Thrown when fails to make API call + /// Interval in milliseconds (default is 200) (optional) + /// Timeout in milliseconds (default is 500) (optional) + /// Timeout in milliseconds (default is 1000) (optional) + /// Timeout in milliseconds (default is 1000) (optional) + /// Timeout in milliseconds (default is 1000) (optional) + /// Interval in milliseconds (default is 200) (optional) + /// The maximum time for each mdns query in milliseconds. (default is 5 * 1000) (optional) + /// The maximum duration for whole networking scan in milliseconds. Highly suggested! (optional) + /// The start and end IP address of the range to scan. for example: 10.10.0.0-10.10.0.255 (optional) + /// Explicit single-host targets to scan. Each value must parse as an IPv4 or IPv6 address; invalid values yield a structured `{ error: \"invalid_target\", value: \"<raw>\" }` 400 rather than a generic serde rejection at extraction time (mirrors the `range=` / `probe=` validation path). (optional) + /// Gateway network interface IDs to use as scan sources. (optional) + /// The probes to run. Each value is either `ping`, a port number (`22`), or a named service (`rdp`, `https`, …). Validation is deferred to scan-time so failures can be surfaced as a structured 400 — naming the offending value — instead of a generic serde rejection at extraction time. (optional) + /// **Legacy alias** for `report_ping_start`. Prefer the explicit name in new clients; kept so existing consumers don't break. (optional) (deprecated) + /// Enable the execution of broadcast scan (optional) + /// Enable the ping scan on subnet (optional) + /// Enable ZeroConf/mDNS (optional) + /// Enable NetBIOS name-service queries. Default `true` for backward compatibility. Set `false` (or pair with explicit `target=`) to keep NetBIOS from sweeping the surrounding subnet when the caller only wants results for the targets they listed. (optional) + /// Enable resolve dns (optional) + /// Include host-only results. (optional) + /// Emit ping queued/start host results. (optional) + /// Emit ping success host results. (optional) + /// Emit ping failure host results. (optional) + /// Enable TCP service probes. (optional) + /// Policy applied when `range=` and `interface_id=` are both provided. Accepted values: `intersect_selected_interfaces` (default) or `allow_cross_interface_range`. Invalid values yield a structured `{ error: \"invalid_range_interface_policy\", value: \"<raw>\" }` 400 instead of a generic serde rejection (mirrors the `range=` / `probe=` / `target=` validation path). (optional) + /// **Legacy alias** for `range_interface_policy=allow_cross_interface_range`. Prefer the structured policy in new clients. (optional) (deprecated) + /// Response shape emitted on the websocket. Accepted values: `legacy` (default) or `network_scan_result_v1`. Invalid values yield a structured `{ error: \"invalid_response_format\", value: \"<raw>\" }` 400 instead of a generic serde rejection. (optional) + /// Maximum scanner concurrency. (optional) + /// Maximum ping probe concurrency. (optional) + /// Maximum TCP probe concurrency. (optional) + /// **Legacy alias** for `report_ping_failure`. `enable_failure=true` only opts into ping-failure events; TCP-probe failure events require the explicit `report_tcp_failure=true` and are not affected by this alias. **Behavior change:** historically this single flag controlled both ping-failure and TCP-probe-failure reporting. The two are now split: clients that want the old \"both at once\" semantics must send `enable_failure=true&report_tcp_failure=true` together. The split is intentional — TCP-probe failures are typically high-volume noise that callers were filtering client-side anyway, so the two streams are independently gated. (optional) (deprecated) + /// Enable TCP port knocking failure events. (optional) + /// When `true`, fail with HTTP 400 if a ping/TCP-probe socket cannot be bound to the planner-selected interface. Default `false` (warn and fall back to default routing). (optional) + /// + void GetNetScan(long? pingInterval = default(long?), long? pingTimeout = default(long?), long? broadcastTimeout = default(long?), long? portScanTimeout = default(long?), long? netbiosTimeout = default(long?), long? netbiosInterval = default(long?), long? mdnsQueryTimeout = default(long?), long? maxWait = default(long?), List? range = default(List?), List? target = default(List?), List? interfaceId = default(List?), List? probe = default(List?), bool? enablePingStart = default(bool?), bool? enableBroadcast = default(bool?), bool? enableSubnetScan = default(bool?), bool? enableZeroconf = default(bool?), bool? enableNetbios = default(bool?), bool? enableResolveDns = default(bool?), bool? includeHostResults = default(bool?), bool? reportPingStart = default(bool?), bool? reportPingSuccess = default(bool?), bool? reportPingFailure = default(bool?), bool? enableTcpProbes = default(bool?), string? rangeInterfacePolicy = default(string?), bool? allowCrossInterfaceRange = default(bool?), string? responseFormat = default(string?), int? maxConcurrency = default(int?), int? maxPingConcurrency = default(int?), int? maxTcpProbeConcurrency = default(int?), bool? enableFailure = default(bool?), bool? reportTcpFailure = default(bool?), bool? interfaceBindStrict = default(bool?)); + + /// + /// Stream network scan events over a websocket. + /// + /// + /// The endpoint is upgraded from HTTP, so OpenAPI describes the **handshake**: the query parameters that drive the scan (validated before upgrade) and the legacy / v1 event payloads streamed back as JSON text frames. See `NetworkScanResultEvent` for the v1 shape and `LegacyScanEvent` for the legacy shape (selected via `response_format`). + /// + /// Thrown when fails to make API call + /// Interval in milliseconds (default is 200) (optional) + /// Timeout in milliseconds (default is 500) (optional) + /// Timeout in milliseconds (default is 1000) (optional) + /// Timeout in milliseconds (default is 1000) (optional) + /// Timeout in milliseconds (default is 1000) (optional) + /// Interval in milliseconds (default is 200) (optional) + /// The maximum time for each mdns query in milliseconds. (default is 5 * 1000) (optional) + /// The maximum duration for whole networking scan in milliseconds. Highly suggested! (optional) + /// The start and end IP address of the range to scan. for example: 10.10.0.0-10.10.0.255 (optional) + /// Explicit single-host targets to scan. Each value must parse as an IPv4 or IPv6 address; invalid values yield a structured `{ error: \"invalid_target\", value: \"<raw>\" }` 400 rather than a generic serde rejection at extraction time (mirrors the `range=` / `probe=` validation path). (optional) + /// Gateway network interface IDs to use as scan sources. (optional) + /// The probes to run. Each value is either `ping`, a port number (`22`), or a named service (`rdp`, `https`, …). Validation is deferred to scan-time so failures can be surfaced as a structured 400 — naming the offending value — instead of a generic serde rejection at extraction time. (optional) + /// **Legacy alias** for `report_ping_start`. Prefer the explicit name in new clients; kept so existing consumers don't break. (optional) (deprecated) + /// Enable the execution of broadcast scan (optional) + /// Enable the ping scan on subnet (optional) + /// Enable ZeroConf/mDNS (optional) + /// Enable NetBIOS name-service queries. Default `true` for backward compatibility. Set `false` (or pair with explicit `target=`) to keep NetBIOS from sweeping the surrounding subnet when the caller only wants results for the targets they listed. (optional) + /// Enable resolve dns (optional) + /// Include host-only results. (optional) + /// Emit ping queued/start host results. (optional) + /// Emit ping success host results. (optional) + /// Emit ping failure host results. (optional) + /// Enable TCP service probes. (optional) + /// Policy applied when `range=` and `interface_id=` are both provided. Accepted values: `intersect_selected_interfaces` (default) or `allow_cross_interface_range`. Invalid values yield a structured `{ error: \"invalid_range_interface_policy\", value: \"<raw>\" }` 400 instead of a generic serde rejection (mirrors the `range=` / `probe=` / `target=` validation path). (optional) + /// **Legacy alias** for `range_interface_policy=allow_cross_interface_range`. Prefer the structured policy in new clients. (optional) (deprecated) + /// Response shape emitted on the websocket. Accepted values: `legacy` (default) or `network_scan_result_v1`. Invalid values yield a structured `{ error: \"invalid_response_format\", value: \"<raw>\" }` 400 instead of a generic serde rejection. (optional) + /// Maximum scanner concurrency. (optional) + /// Maximum ping probe concurrency. (optional) + /// Maximum TCP probe concurrency. (optional) + /// **Legacy alias** for `report_ping_failure`. `enable_failure=true` only opts into ping-failure events; TCP-probe failure events require the explicit `report_tcp_failure=true` and are not affected by this alias. **Behavior change:** historically this single flag controlled both ping-failure and TCP-probe-failure reporting. The two are now split: clients that want the old \"both at once\" semantics must send `enable_failure=true&report_tcp_failure=true` together. The split is intentional — TCP-probe failures are typically high-volume noise that callers were filtering client-side anyway, so the two streams are independently gated. (optional) (deprecated) + /// Enable TCP port knocking failure events. (optional) + /// When `true`, fail with HTTP 400 if a ping/TCP-probe socket cannot be bound to the planner-selected interface. Default `false` (warn and fall back to default routing). (optional) + /// ApiResponse of Object(void) + ApiResponse GetNetScanWithHttpInfo(long? pingInterval = default(long?), long? pingTimeout = default(long?), long? broadcastTimeout = default(long?), long? portScanTimeout = default(long?), long? netbiosTimeout = default(long?), long? netbiosInterval = default(long?), long? mdnsQueryTimeout = default(long?), long? maxWait = default(long?), List? range = default(List?), List? target = default(List?), List? interfaceId = default(List?), List? probe = default(List?), bool? enablePingStart = default(bool?), bool? enableBroadcast = default(bool?), bool? enableSubnetScan = default(bool?), bool? enableZeroconf = default(bool?), bool? enableNetbios = default(bool?), bool? enableResolveDns = default(bool?), bool? includeHostResults = default(bool?), bool? reportPingStart = default(bool?), bool? reportPingSuccess = default(bool?), bool? reportPingFailure = default(bool?), bool? enableTcpProbes = default(bool?), string? rangeInterfacePolicy = default(string?), bool? allowCrossInterfaceRange = default(bool?), string? responseFormat = default(string?), int? maxConcurrency = default(int?), int? maxPingConcurrency = default(int?), int? maxTcpProbeConcurrency = default(int?), bool? enableFailure = default(bool?), bool? reportTcpFailure = default(bool?), bool? interfaceBindStrict = default(bool?)); #endregion Synchronous Operations } @@ -74,6 +173,112 @@ public interface INetApiAsync : IApiAccessor /// Cancellation Token to cancel the request. /// Task of ApiResponse (List<Dictionary<string, List<InterfaceInfo>>>) System.Threading.Tasks.Task>>>> GetNetConfigWithHttpInfoAsync(System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken)); + /// + /// Lists Gateway network scan sources. + /// + /// + /// + /// + /// Thrown when fails to make API call + /// Cancellation Token to cancel the request. + /// Task of NetworkInterfacesResponse + System.Threading.Tasks.Task GetNetInterfacesAsync(System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken)); + + /// + /// Lists Gateway network scan sources. + /// + /// + /// + /// + /// Thrown when fails to make API call + /// Cancellation Token to cancel the request. + /// Task of ApiResponse (NetworkInterfacesResponse) + System.Threading.Tasks.Task> GetNetInterfacesWithHttpInfoAsync(System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken)); + /// + /// Stream network scan events over a websocket. + /// + /// + /// The endpoint is upgraded from HTTP, so OpenAPI describes the **handshake**: the query parameters that drive the scan (validated before upgrade) and the legacy / v1 event payloads streamed back as JSON text frames. See `NetworkScanResultEvent` for the v1 shape and `LegacyScanEvent` for the legacy shape (selected via `response_format`). + /// + /// Thrown when fails to make API call + /// Interval in milliseconds (default is 200) (optional) + /// Timeout in milliseconds (default is 500) (optional) + /// Timeout in milliseconds (default is 1000) (optional) + /// Timeout in milliseconds (default is 1000) (optional) + /// Timeout in milliseconds (default is 1000) (optional) + /// Interval in milliseconds (default is 200) (optional) + /// The maximum time for each mdns query in milliseconds. (default is 5 * 1000) (optional) + /// The maximum duration for whole networking scan in milliseconds. Highly suggested! (optional) + /// The start and end IP address of the range to scan. for example: 10.10.0.0-10.10.0.255 (optional) + /// Explicit single-host targets to scan. Each value must parse as an IPv4 or IPv6 address; invalid values yield a structured `{ error: \"invalid_target\", value: \"<raw>\" }` 400 rather than a generic serde rejection at extraction time (mirrors the `range=` / `probe=` validation path). (optional) + /// Gateway network interface IDs to use as scan sources. (optional) + /// The probes to run. Each value is either `ping`, a port number (`22`), or a named service (`rdp`, `https`, …). Validation is deferred to scan-time so failures can be surfaced as a structured 400 — naming the offending value — instead of a generic serde rejection at extraction time. (optional) + /// **Legacy alias** for `report_ping_start`. Prefer the explicit name in new clients; kept so existing consumers don't break. (optional) (deprecated) + /// Enable the execution of broadcast scan (optional) + /// Enable the ping scan on subnet (optional) + /// Enable ZeroConf/mDNS (optional) + /// Enable NetBIOS name-service queries. Default `true` for backward compatibility. Set `false` (or pair with explicit `target=`) to keep NetBIOS from sweeping the surrounding subnet when the caller only wants results for the targets they listed. (optional) + /// Enable resolve dns (optional) + /// Include host-only results. (optional) + /// Emit ping queued/start host results. (optional) + /// Emit ping success host results. (optional) + /// Emit ping failure host results. (optional) + /// Enable TCP service probes. (optional) + /// Policy applied when `range=` and `interface_id=` are both provided. Accepted values: `intersect_selected_interfaces` (default) or `allow_cross_interface_range`. Invalid values yield a structured `{ error: \"invalid_range_interface_policy\", value: \"<raw>\" }` 400 instead of a generic serde rejection (mirrors the `range=` / `probe=` / `target=` validation path). (optional) + /// **Legacy alias** for `range_interface_policy=allow_cross_interface_range`. Prefer the structured policy in new clients. (optional) (deprecated) + /// Response shape emitted on the websocket. Accepted values: `legacy` (default) or `network_scan_result_v1`. Invalid values yield a structured `{ error: \"invalid_response_format\", value: \"<raw>\" }` 400 instead of a generic serde rejection. (optional) + /// Maximum scanner concurrency. (optional) + /// Maximum ping probe concurrency. (optional) + /// Maximum TCP probe concurrency. (optional) + /// **Legacy alias** for `report_ping_failure`. `enable_failure=true` only opts into ping-failure events; TCP-probe failure events require the explicit `report_tcp_failure=true` and are not affected by this alias. **Behavior change:** historically this single flag controlled both ping-failure and TCP-probe-failure reporting. The two are now split: clients that want the old \"both at once\" semantics must send `enable_failure=true&report_tcp_failure=true` together. The split is intentional — TCP-probe failures are typically high-volume noise that callers were filtering client-side anyway, so the two streams are independently gated. (optional) (deprecated) + /// Enable TCP port knocking failure events. (optional) + /// When `true`, fail with HTTP 400 if a ping/TCP-probe socket cannot be bound to the planner-selected interface. Default `false` (warn and fall back to default routing). (optional) + /// Cancellation Token to cancel the request. + /// Task of void + System.Threading.Tasks.Task GetNetScanAsync(long? pingInterval = default(long?), long? pingTimeout = default(long?), long? broadcastTimeout = default(long?), long? portScanTimeout = default(long?), long? netbiosTimeout = default(long?), long? netbiosInterval = default(long?), long? mdnsQueryTimeout = default(long?), long? maxWait = default(long?), List? range = default(List?), List? target = default(List?), List? interfaceId = default(List?), List? probe = default(List?), bool? enablePingStart = default(bool?), bool? enableBroadcast = default(bool?), bool? enableSubnetScan = default(bool?), bool? enableZeroconf = default(bool?), bool? enableNetbios = default(bool?), bool? enableResolveDns = default(bool?), bool? includeHostResults = default(bool?), bool? reportPingStart = default(bool?), bool? reportPingSuccess = default(bool?), bool? reportPingFailure = default(bool?), bool? enableTcpProbes = default(bool?), string? rangeInterfacePolicy = default(string?), bool? allowCrossInterfaceRange = default(bool?), string? responseFormat = default(string?), int? maxConcurrency = default(int?), int? maxPingConcurrency = default(int?), int? maxTcpProbeConcurrency = default(int?), bool? enableFailure = default(bool?), bool? reportTcpFailure = default(bool?), bool? interfaceBindStrict = default(bool?), System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken)); + + /// + /// Stream network scan events over a websocket. + /// + /// + /// The endpoint is upgraded from HTTP, so OpenAPI describes the **handshake**: the query parameters that drive the scan (validated before upgrade) and the legacy / v1 event payloads streamed back as JSON text frames. See `NetworkScanResultEvent` for the v1 shape and `LegacyScanEvent` for the legacy shape (selected via `response_format`). + /// + /// Thrown when fails to make API call + /// Interval in milliseconds (default is 200) (optional) + /// Timeout in milliseconds (default is 500) (optional) + /// Timeout in milliseconds (default is 1000) (optional) + /// Timeout in milliseconds (default is 1000) (optional) + /// Timeout in milliseconds (default is 1000) (optional) + /// Interval in milliseconds (default is 200) (optional) + /// The maximum time for each mdns query in milliseconds. (default is 5 * 1000) (optional) + /// The maximum duration for whole networking scan in milliseconds. Highly suggested! (optional) + /// The start and end IP address of the range to scan. for example: 10.10.0.0-10.10.0.255 (optional) + /// Explicit single-host targets to scan. Each value must parse as an IPv4 or IPv6 address; invalid values yield a structured `{ error: \"invalid_target\", value: \"<raw>\" }` 400 rather than a generic serde rejection at extraction time (mirrors the `range=` / `probe=` validation path). (optional) + /// Gateway network interface IDs to use as scan sources. (optional) + /// The probes to run. Each value is either `ping`, a port number (`22`), or a named service (`rdp`, `https`, …). Validation is deferred to scan-time so failures can be surfaced as a structured 400 — naming the offending value — instead of a generic serde rejection at extraction time. (optional) + /// **Legacy alias** for `report_ping_start`. Prefer the explicit name in new clients; kept so existing consumers don't break. (optional) (deprecated) + /// Enable the execution of broadcast scan (optional) + /// Enable the ping scan on subnet (optional) + /// Enable ZeroConf/mDNS (optional) + /// Enable NetBIOS name-service queries. Default `true` for backward compatibility. Set `false` (or pair with explicit `target=`) to keep NetBIOS from sweeping the surrounding subnet when the caller only wants results for the targets they listed. (optional) + /// Enable resolve dns (optional) + /// Include host-only results. (optional) + /// Emit ping queued/start host results. (optional) + /// Emit ping success host results. (optional) + /// Emit ping failure host results. (optional) + /// Enable TCP service probes. (optional) + /// Policy applied when `range=` and `interface_id=` are both provided. Accepted values: `intersect_selected_interfaces` (default) or `allow_cross_interface_range`. Invalid values yield a structured `{ error: \"invalid_range_interface_policy\", value: \"<raw>\" }` 400 instead of a generic serde rejection (mirrors the `range=` / `probe=` / `target=` validation path). (optional) + /// **Legacy alias** for `range_interface_policy=allow_cross_interface_range`. Prefer the structured policy in new clients. (optional) (deprecated) + /// Response shape emitted on the websocket. Accepted values: `legacy` (default) or `network_scan_result_v1`. Invalid values yield a structured `{ error: \"invalid_response_format\", value: \"<raw>\" }` 400 instead of a generic serde rejection. (optional) + /// Maximum scanner concurrency. (optional) + /// Maximum ping probe concurrency. (optional) + /// Maximum TCP probe concurrency. (optional) + /// **Legacy alias** for `report_ping_failure`. `enable_failure=true` only opts into ping-failure events; TCP-probe failure events require the explicit `report_tcp_failure=true` and are not affected by this alias. **Behavior change:** historically this single flag controlled both ping-failure and TCP-probe-failure reporting. The two are now split: clients that want the old \"both at once\" semantics must send `enable_failure=true&report_tcp_failure=true` together. The split is intentional — TCP-probe failures are typically high-volume noise that callers were filtering client-side anyway, so the two streams are independently gated. (optional) (deprecated) + /// Enable TCP port knocking failure events. (optional) + /// When `true`, fail with HTTP 400 if a ping/TCP-probe socket cannot be bound to the planner-selected interface. Default `false` (warn and fall back to default routing). (optional) + /// Cancellation Token to cancel the request. + /// Task of ApiResponse + System.Threading.Tasks.Task> GetNetScanWithHttpInfoAsync(long? pingInterval = default(long?), long? pingTimeout = default(long?), long? broadcastTimeout = default(long?), long? portScanTimeout = default(long?), long? netbiosTimeout = default(long?), long? netbiosInterval = default(long?), long? mdnsQueryTimeout = default(long?), long? maxWait = default(long?), List? range = default(List?), List? target = default(List?), List? interfaceId = default(List?), List? probe = default(List?), bool? enablePingStart = default(bool?), bool? enableBroadcast = default(bool?), bool? enableSubnetScan = default(bool?), bool? enableZeroconf = default(bool?), bool? enableNetbios = default(bool?), bool? enableResolveDns = default(bool?), bool? includeHostResults = default(bool?), bool? reportPingStart = default(bool?), bool? reportPingSuccess = default(bool?), bool? reportPingFailure = default(bool?), bool? enableTcpProbes = default(bool?), string? rangeInterfacePolicy = default(string?), bool? allowCrossInterfaceRange = default(bool?), string? responseFormat = default(string?), int? maxConcurrency = default(int?), int? maxPingConcurrency = default(int?), int? maxTcpProbeConcurrency = default(int?), bool? enableFailure = default(bool?), bool? reportTcpFailure = default(bool?), bool? interfaceBindStrict = default(bool?), System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken)); #endregion Asynchronous Operations } @@ -400,5 +605,611 @@ public Devolutions.Gateway.Client.Client.ApiResponse + /// Lists Gateway network scan sources. + /// + /// Thrown when fails to make API call + /// NetworkInterfacesResponse + public NetworkInterfacesResponse GetNetInterfaces() + { + Devolutions.Gateway.Client.Client.ApiResponse localVarResponse = GetNetInterfacesWithHttpInfo(); + return localVarResponse.Data; + } + + /// + /// Lists Gateway network scan sources. + /// + /// Thrown when fails to make API call + /// ApiResponse of NetworkInterfacesResponse + public Devolutions.Gateway.Client.Client.ApiResponse GetNetInterfacesWithHttpInfo() + { + Devolutions.Gateway.Client.Client.RequestOptions localVarRequestOptions = new Devolutions.Gateway.Client.Client.RequestOptions(); + + string[] _contentTypes = new string[] { + }; + + // to determine the Accept header + string[] _accepts = new string[] { + "application/json" + }; + + var localVarContentType = Devolutions.Gateway.Client.Client.ClientUtils.SelectHeaderContentType(_contentTypes); + if (localVarContentType != null) localVarRequestOptions.HeaderParameters.Add("Content-Type", localVarContentType); + + var localVarAccept = Devolutions.Gateway.Client.Client.ClientUtils.SelectHeaderAccept(_accepts); + if (localVarAccept != null) localVarRequestOptions.HeaderParameters.Add("Accept", localVarAccept); + + + // authentication (netscan_token) required + // bearer authentication required + if (!string.IsNullOrEmpty(this.Configuration.AccessToken) && !localVarRequestOptions.HeaderParameters.ContainsKey("Authorization")) + { + localVarRequestOptions.HeaderParameters.Add("Authorization", "Bearer " + this.Configuration.AccessToken); + } + + // make the HTTP request + var localVarResponse = this.Client.Get("/jet/net/interfaces", localVarRequestOptions, this.Configuration); + + if (this.ExceptionFactory != null) + { + Exception _exception = this.ExceptionFactory("GetNetInterfaces", localVarResponse); + if (_exception != null) throw _exception; + } + + return localVarResponse; + } + + /// + /// Lists Gateway network scan sources. + /// + /// Thrown when fails to make API call + /// Cancellation Token to cancel the request. + /// Task of NetworkInterfacesResponse + public async System.Threading.Tasks.Task GetNetInterfacesAsync(System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken)) + { + Devolutions.Gateway.Client.Client.ApiResponse localVarResponse = await GetNetInterfacesWithHttpInfoAsync(cancellationToken).ConfigureAwait(false); + return localVarResponse.Data; + } + + /// + /// Lists Gateway network scan sources. + /// + /// Thrown when fails to make API call + /// Cancellation Token to cancel the request. + /// Task of ApiResponse (NetworkInterfacesResponse) + public async System.Threading.Tasks.Task> GetNetInterfacesWithHttpInfoAsync(System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken)) + { + + Devolutions.Gateway.Client.Client.RequestOptions localVarRequestOptions = new Devolutions.Gateway.Client.Client.RequestOptions(); + + string[] _contentTypes = new string[] { + }; + + // to determine the Accept header + string[] _accepts = new string[] { + "application/json" + }; + + + var localVarContentType = Devolutions.Gateway.Client.Client.ClientUtils.SelectHeaderContentType(_contentTypes); + if (localVarContentType != null) localVarRequestOptions.HeaderParameters.Add("Content-Type", localVarContentType); + + var localVarAccept = Devolutions.Gateway.Client.Client.ClientUtils.SelectHeaderAccept(_accepts); + if (localVarAccept != null) localVarRequestOptions.HeaderParameters.Add("Accept", localVarAccept); + + + // authentication (netscan_token) required + // bearer authentication required + if (!string.IsNullOrEmpty(this.Configuration.AccessToken) && !localVarRequestOptions.HeaderParameters.ContainsKey("Authorization")) + { + localVarRequestOptions.HeaderParameters.Add("Authorization", "Bearer " + this.Configuration.AccessToken); + } + + // make the HTTP request + + var localVarResponse = await this.AsynchronousClient.GetAsync("/jet/net/interfaces", localVarRequestOptions, this.Configuration, cancellationToken).ConfigureAwait(false); + + if (this.ExceptionFactory != null) + { + Exception _exception = this.ExceptionFactory("GetNetInterfaces", localVarResponse); + if (_exception != null) throw _exception; + } + + return localVarResponse; + } + + /// + /// Stream network scan events over a websocket. The endpoint is upgraded from HTTP, so OpenAPI describes the **handshake**: the query parameters that drive the scan (validated before upgrade) and the legacy / v1 event payloads streamed back as JSON text frames. See `NetworkScanResultEvent` for the v1 shape and `LegacyScanEvent` for the legacy shape (selected via `response_format`). + /// + /// Thrown when fails to make API call + /// Interval in milliseconds (default is 200) (optional) + /// Timeout in milliseconds (default is 500) (optional) + /// Timeout in milliseconds (default is 1000) (optional) + /// Timeout in milliseconds (default is 1000) (optional) + /// Timeout in milliseconds (default is 1000) (optional) + /// Interval in milliseconds (default is 200) (optional) + /// The maximum time for each mdns query in milliseconds. (default is 5 * 1000) (optional) + /// The maximum duration for whole networking scan in milliseconds. Highly suggested! (optional) + /// The start and end IP address of the range to scan. for example: 10.10.0.0-10.10.0.255 (optional) + /// Explicit single-host targets to scan. Each value must parse as an IPv4 or IPv6 address; invalid values yield a structured `{ error: \"invalid_target\", value: \"<raw>\" }` 400 rather than a generic serde rejection at extraction time (mirrors the `range=` / `probe=` validation path). (optional) + /// Gateway network interface IDs to use as scan sources. (optional) + /// The probes to run. Each value is either `ping`, a port number (`22`), or a named service (`rdp`, `https`, …). Validation is deferred to scan-time so failures can be surfaced as a structured 400 — naming the offending value — instead of a generic serde rejection at extraction time. (optional) + /// **Legacy alias** for `report_ping_start`. Prefer the explicit name in new clients; kept so existing consumers don't break. (optional) (deprecated) + /// Enable the execution of broadcast scan (optional) + /// Enable the ping scan on subnet (optional) + /// Enable ZeroConf/mDNS (optional) + /// Enable NetBIOS name-service queries. Default `true` for backward compatibility. Set `false` (or pair with explicit `target=`) to keep NetBIOS from sweeping the surrounding subnet when the caller only wants results for the targets they listed. (optional) + /// Enable resolve dns (optional) + /// Include host-only results. (optional) + /// Emit ping queued/start host results. (optional) + /// Emit ping success host results. (optional) + /// Emit ping failure host results. (optional) + /// Enable TCP service probes. (optional) + /// Policy applied when `range=` and `interface_id=` are both provided. Accepted values: `intersect_selected_interfaces` (default) or `allow_cross_interface_range`. Invalid values yield a structured `{ error: \"invalid_range_interface_policy\", value: \"<raw>\" }` 400 instead of a generic serde rejection (mirrors the `range=` / `probe=` / `target=` validation path). (optional) + /// **Legacy alias** for `range_interface_policy=allow_cross_interface_range`. Prefer the structured policy in new clients. (optional) (deprecated) + /// Response shape emitted on the websocket. Accepted values: `legacy` (default) or `network_scan_result_v1`. Invalid values yield a structured `{ error: \"invalid_response_format\", value: \"<raw>\" }` 400 instead of a generic serde rejection. (optional) + /// Maximum scanner concurrency. (optional) + /// Maximum ping probe concurrency. (optional) + /// Maximum TCP probe concurrency. (optional) + /// **Legacy alias** for `report_ping_failure`. `enable_failure=true` only opts into ping-failure events; TCP-probe failure events require the explicit `report_tcp_failure=true` and are not affected by this alias. **Behavior change:** historically this single flag controlled both ping-failure and TCP-probe-failure reporting. The two are now split: clients that want the old \"both at once\" semantics must send `enable_failure=true&report_tcp_failure=true` together. The split is intentional — TCP-probe failures are typically high-volume noise that callers were filtering client-side anyway, so the two streams are independently gated. (optional) (deprecated) + /// Enable TCP port knocking failure events. (optional) + /// When `true`, fail with HTTP 400 if a ping/TCP-probe socket cannot be bound to the planner-selected interface. Default `false` (warn and fall back to default routing). (optional) + /// + public void GetNetScan(long? pingInterval = default(long?), long? pingTimeout = default(long?), long? broadcastTimeout = default(long?), long? portScanTimeout = default(long?), long? netbiosTimeout = default(long?), long? netbiosInterval = default(long?), long? mdnsQueryTimeout = default(long?), long? maxWait = default(long?), List? range = default(List?), List? target = default(List?), List? interfaceId = default(List?), List? probe = default(List?), bool? enablePingStart = default(bool?), bool? enableBroadcast = default(bool?), bool? enableSubnetScan = default(bool?), bool? enableZeroconf = default(bool?), bool? enableNetbios = default(bool?), bool? enableResolveDns = default(bool?), bool? includeHostResults = default(bool?), bool? reportPingStart = default(bool?), bool? reportPingSuccess = default(bool?), bool? reportPingFailure = default(bool?), bool? enableTcpProbes = default(bool?), string? rangeInterfacePolicy = default(string?), bool? allowCrossInterfaceRange = default(bool?), string? responseFormat = default(string?), int? maxConcurrency = default(int?), int? maxPingConcurrency = default(int?), int? maxTcpProbeConcurrency = default(int?), bool? enableFailure = default(bool?), bool? reportTcpFailure = default(bool?), bool? interfaceBindStrict = default(bool?)) + { + GetNetScanWithHttpInfo(pingInterval, pingTimeout, broadcastTimeout, portScanTimeout, netbiosTimeout, netbiosInterval, mdnsQueryTimeout, maxWait, range, target, interfaceId, probe, enablePingStart, enableBroadcast, enableSubnetScan, enableZeroconf, enableNetbios, enableResolveDns, includeHostResults, reportPingStart, reportPingSuccess, reportPingFailure, enableTcpProbes, rangeInterfacePolicy, allowCrossInterfaceRange, responseFormat, maxConcurrency, maxPingConcurrency, maxTcpProbeConcurrency, enableFailure, reportTcpFailure, interfaceBindStrict); + } + + /// + /// Stream network scan events over a websocket. The endpoint is upgraded from HTTP, so OpenAPI describes the **handshake**: the query parameters that drive the scan (validated before upgrade) and the legacy / v1 event payloads streamed back as JSON text frames. See `NetworkScanResultEvent` for the v1 shape and `LegacyScanEvent` for the legacy shape (selected via `response_format`). + /// + /// Thrown when fails to make API call + /// Interval in milliseconds (default is 200) (optional) + /// Timeout in milliseconds (default is 500) (optional) + /// Timeout in milliseconds (default is 1000) (optional) + /// Timeout in milliseconds (default is 1000) (optional) + /// Timeout in milliseconds (default is 1000) (optional) + /// Interval in milliseconds (default is 200) (optional) + /// The maximum time for each mdns query in milliseconds. (default is 5 * 1000) (optional) + /// The maximum duration for whole networking scan in milliseconds. Highly suggested! (optional) + /// The start and end IP address of the range to scan. for example: 10.10.0.0-10.10.0.255 (optional) + /// Explicit single-host targets to scan. Each value must parse as an IPv4 or IPv6 address; invalid values yield a structured `{ error: \"invalid_target\", value: \"<raw>\" }` 400 rather than a generic serde rejection at extraction time (mirrors the `range=` / `probe=` validation path). (optional) + /// Gateway network interface IDs to use as scan sources. (optional) + /// The probes to run. Each value is either `ping`, a port number (`22`), or a named service (`rdp`, `https`, …). Validation is deferred to scan-time so failures can be surfaced as a structured 400 — naming the offending value — instead of a generic serde rejection at extraction time. (optional) + /// **Legacy alias** for `report_ping_start`. Prefer the explicit name in new clients; kept so existing consumers don't break. (optional) (deprecated) + /// Enable the execution of broadcast scan (optional) + /// Enable the ping scan on subnet (optional) + /// Enable ZeroConf/mDNS (optional) + /// Enable NetBIOS name-service queries. Default `true` for backward compatibility. Set `false` (or pair with explicit `target=`) to keep NetBIOS from sweeping the surrounding subnet when the caller only wants results for the targets they listed. (optional) + /// Enable resolve dns (optional) + /// Include host-only results. (optional) + /// Emit ping queued/start host results. (optional) + /// Emit ping success host results. (optional) + /// Emit ping failure host results. (optional) + /// Enable TCP service probes. (optional) + /// Policy applied when `range=` and `interface_id=` are both provided. Accepted values: `intersect_selected_interfaces` (default) or `allow_cross_interface_range`. Invalid values yield a structured `{ error: \"invalid_range_interface_policy\", value: \"<raw>\" }` 400 instead of a generic serde rejection (mirrors the `range=` / `probe=` / `target=` validation path). (optional) + /// **Legacy alias** for `range_interface_policy=allow_cross_interface_range`. Prefer the structured policy in new clients. (optional) (deprecated) + /// Response shape emitted on the websocket. Accepted values: `legacy` (default) or `network_scan_result_v1`. Invalid values yield a structured `{ error: \"invalid_response_format\", value: \"<raw>\" }` 400 instead of a generic serde rejection. (optional) + /// Maximum scanner concurrency. (optional) + /// Maximum ping probe concurrency. (optional) + /// Maximum TCP probe concurrency. (optional) + /// **Legacy alias** for `report_ping_failure`. `enable_failure=true` only opts into ping-failure events; TCP-probe failure events require the explicit `report_tcp_failure=true` and are not affected by this alias. **Behavior change:** historically this single flag controlled both ping-failure and TCP-probe-failure reporting. The two are now split: clients that want the old \"both at once\" semantics must send `enable_failure=true&report_tcp_failure=true` together. The split is intentional — TCP-probe failures are typically high-volume noise that callers were filtering client-side anyway, so the two streams are independently gated. (optional) (deprecated) + /// Enable TCP port knocking failure events. (optional) + /// When `true`, fail with HTTP 400 if a ping/TCP-probe socket cannot be bound to the planner-selected interface. Default `false` (warn and fall back to default routing). (optional) + /// ApiResponse of Object(void) + public Devolutions.Gateway.Client.Client.ApiResponse GetNetScanWithHttpInfo(long? pingInterval = default(long?), long? pingTimeout = default(long?), long? broadcastTimeout = default(long?), long? portScanTimeout = default(long?), long? netbiosTimeout = default(long?), long? netbiosInterval = default(long?), long? mdnsQueryTimeout = default(long?), long? maxWait = default(long?), List? range = default(List?), List? target = default(List?), List? interfaceId = default(List?), List? probe = default(List?), bool? enablePingStart = default(bool?), bool? enableBroadcast = default(bool?), bool? enableSubnetScan = default(bool?), bool? enableZeroconf = default(bool?), bool? enableNetbios = default(bool?), bool? enableResolveDns = default(bool?), bool? includeHostResults = default(bool?), bool? reportPingStart = default(bool?), bool? reportPingSuccess = default(bool?), bool? reportPingFailure = default(bool?), bool? enableTcpProbes = default(bool?), string? rangeInterfacePolicy = default(string?), bool? allowCrossInterfaceRange = default(bool?), string? responseFormat = default(string?), int? maxConcurrency = default(int?), int? maxPingConcurrency = default(int?), int? maxTcpProbeConcurrency = default(int?), bool? enableFailure = default(bool?), bool? reportTcpFailure = default(bool?), bool? interfaceBindStrict = default(bool?)) + { + Devolutions.Gateway.Client.Client.RequestOptions localVarRequestOptions = new Devolutions.Gateway.Client.Client.RequestOptions(); + + string[] _contentTypes = new string[] { + }; + + // to determine the Accept header + string[] _accepts = new string[] { + }; + + var localVarContentType = Devolutions.Gateway.Client.Client.ClientUtils.SelectHeaderContentType(_contentTypes); + if (localVarContentType != null) localVarRequestOptions.HeaderParameters.Add("Content-Type", localVarContentType); + + var localVarAccept = Devolutions.Gateway.Client.Client.ClientUtils.SelectHeaderAccept(_accepts); + if (localVarAccept != null) localVarRequestOptions.HeaderParameters.Add("Accept", localVarAccept); + + if (pingInterval != null) + { + localVarRequestOptions.QueryParameters.Add(Devolutions.Gateway.Client.Client.ClientUtils.ParameterToMultiMap("", "ping_interval", pingInterval)); + } + if (pingTimeout != null) + { + localVarRequestOptions.QueryParameters.Add(Devolutions.Gateway.Client.Client.ClientUtils.ParameterToMultiMap("", "ping_timeout", pingTimeout)); + } + if (broadcastTimeout != null) + { + localVarRequestOptions.QueryParameters.Add(Devolutions.Gateway.Client.Client.ClientUtils.ParameterToMultiMap("", "broadcast_timeout", broadcastTimeout)); + } + if (portScanTimeout != null) + { + localVarRequestOptions.QueryParameters.Add(Devolutions.Gateway.Client.Client.ClientUtils.ParameterToMultiMap("", "port_scan_timeout", portScanTimeout)); + } + if (netbiosTimeout != null) + { + localVarRequestOptions.QueryParameters.Add(Devolutions.Gateway.Client.Client.ClientUtils.ParameterToMultiMap("", "netbios_timeout", netbiosTimeout)); + } + if (netbiosInterval != null) + { + localVarRequestOptions.QueryParameters.Add(Devolutions.Gateway.Client.Client.ClientUtils.ParameterToMultiMap("", "netbios_interval", netbiosInterval)); + } + if (mdnsQueryTimeout != null) + { + localVarRequestOptions.QueryParameters.Add(Devolutions.Gateway.Client.Client.ClientUtils.ParameterToMultiMap("", "mdns_query_timeout", mdnsQueryTimeout)); + } + if (maxWait != null) + { + localVarRequestOptions.QueryParameters.Add(Devolutions.Gateway.Client.Client.ClientUtils.ParameterToMultiMap("", "max_wait", maxWait)); + } + if (range != null) + { + localVarRequestOptions.QueryParameters.Add(Devolutions.Gateway.Client.Client.ClientUtils.ParameterToMultiMap("multi", "range", range)); + } + if (target != null) + { + localVarRequestOptions.QueryParameters.Add(Devolutions.Gateway.Client.Client.ClientUtils.ParameterToMultiMap("multi", "target", target)); + } + if (interfaceId != null) + { + localVarRequestOptions.QueryParameters.Add(Devolutions.Gateway.Client.Client.ClientUtils.ParameterToMultiMap("multi", "interface_id", interfaceId)); + } + if (probe != null) + { + localVarRequestOptions.QueryParameters.Add(Devolutions.Gateway.Client.Client.ClientUtils.ParameterToMultiMap("multi", "probe", probe)); + } + if (enablePingStart != null) + { + localVarRequestOptions.QueryParameters.Add(Devolutions.Gateway.Client.Client.ClientUtils.ParameterToMultiMap("", "enable_ping_start", enablePingStart)); + } + if (enableBroadcast != null) + { + localVarRequestOptions.QueryParameters.Add(Devolutions.Gateway.Client.Client.ClientUtils.ParameterToMultiMap("", "enable_broadcast", enableBroadcast)); + } + if (enableSubnetScan != null) + { + localVarRequestOptions.QueryParameters.Add(Devolutions.Gateway.Client.Client.ClientUtils.ParameterToMultiMap("", "enable_subnet_scan", enableSubnetScan)); + } + if (enableZeroconf != null) + { + localVarRequestOptions.QueryParameters.Add(Devolutions.Gateway.Client.Client.ClientUtils.ParameterToMultiMap("", "enable_zeroconf", enableZeroconf)); + } + if (enableNetbios != null) + { + localVarRequestOptions.QueryParameters.Add(Devolutions.Gateway.Client.Client.ClientUtils.ParameterToMultiMap("", "enable_netbios", enableNetbios)); + } + if (enableResolveDns != null) + { + localVarRequestOptions.QueryParameters.Add(Devolutions.Gateway.Client.Client.ClientUtils.ParameterToMultiMap("", "enable_resolve_dns", enableResolveDns)); + } + if (includeHostResults != null) + { + localVarRequestOptions.QueryParameters.Add(Devolutions.Gateway.Client.Client.ClientUtils.ParameterToMultiMap("", "include_host_results", includeHostResults)); + } + if (reportPingStart != null) + { + localVarRequestOptions.QueryParameters.Add(Devolutions.Gateway.Client.Client.ClientUtils.ParameterToMultiMap("", "report_ping_start", reportPingStart)); + } + if (reportPingSuccess != null) + { + localVarRequestOptions.QueryParameters.Add(Devolutions.Gateway.Client.Client.ClientUtils.ParameterToMultiMap("", "report_ping_success", reportPingSuccess)); + } + if (reportPingFailure != null) + { + localVarRequestOptions.QueryParameters.Add(Devolutions.Gateway.Client.Client.ClientUtils.ParameterToMultiMap("", "report_ping_failure", reportPingFailure)); + } + if (enableTcpProbes != null) + { + localVarRequestOptions.QueryParameters.Add(Devolutions.Gateway.Client.Client.ClientUtils.ParameterToMultiMap("", "enable_tcp_probes", enableTcpProbes)); + } + if (rangeInterfacePolicy != null) + { + localVarRequestOptions.QueryParameters.Add(Devolutions.Gateway.Client.Client.ClientUtils.ParameterToMultiMap("", "range_interface_policy", rangeInterfacePolicy)); + } + if (allowCrossInterfaceRange != null) + { + localVarRequestOptions.QueryParameters.Add(Devolutions.Gateway.Client.Client.ClientUtils.ParameterToMultiMap("", "allow_cross_interface_range", allowCrossInterfaceRange)); + } + if (responseFormat != null) + { + localVarRequestOptions.QueryParameters.Add(Devolutions.Gateway.Client.Client.ClientUtils.ParameterToMultiMap("", "response_format", responseFormat)); + } + if (maxConcurrency != null) + { + localVarRequestOptions.QueryParameters.Add(Devolutions.Gateway.Client.Client.ClientUtils.ParameterToMultiMap("", "max_concurrency", maxConcurrency)); + } + if (maxPingConcurrency != null) + { + localVarRequestOptions.QueryParameters.Add(Devolutions.Gateway.Client.Client.ClientUtils.ParameterToMultiMap("", "max_ping_concurrency", maxPingConcurrency)); + } + if (maxTcpProbeConcurrency != null) + { + localVarRequestOptions.QueryParameters.Add(Devolutions.Gateway.Client.Client.ClientUtils.ParameterToMultiMap("", "max_tcp_probe_concurrency", maxTcpProbeConcurrency)); + } + if (enableFailure != null) + { + localVarRequestOptions.QueryParameters.Add(Devolutions.Gateway.Client.Client.ClientUtils.ParameterToMultiMap("", "enable_failure", enableFailure)); + } + if (reportTcpFailure != null) + { + localVarRequestOptions.QueryParameters.Add(Devolutions.Gateway.Client.Client.ClientUtils.ParameterToMultiMap("", "report_tcp_failure", reportTcpFailure)); + } + if (interfaceBindStrict != null) + { + localVarRequestOptions.QueryParameters.Add(Devolutions.Gateway.Client.Client.ClientUtils.ParameterToMultiMap("", "interface_bind_strict", interfaceBindStrict)); + } + + // authentication (netscan_token) required + // bearer authentication required + if (!string.IsNullOrEmpty(this.Configuration.AccessToken) && !localVarRequestOptions.HeaderParameters.ContainsKey("Authorization")) + { + localVarRequestOptions.HeaderParameters.Add("Authorization", "Bearer " + this.Configuration.AccessToken); + } + + // make the HTTP request + var localVarResponse = this.Client.Get("/jet/net/scan", localVarRequestOptions, this.Configuration); + + if (this.ExceptionFactory != null) + { + Exception _exception = this.ExceptionFactory("GetNetScan", localVarResponse); + if (_exception != null) throw _exception; + } + + return localVarResponse; + } + + /// + /// Stream network scan events over a websocket. The endpoint is upgraded from HTTP, so OpenAPI describes the **handshake**: the query parameters that drive the scan (validated before upgrade) and the legacy / v1 event payloads streamed back as JSON text frames. See `NetworkScanResultEvent` for the v1 shape and `LegacyScanEvent` for the legacy shape (selected via `response_format`). + /// + /// Thrown when fails to make API call + /// Interval in milliseconds (default is 200) (optional) + /// Timeout in milliseconds (default is 500) (optional) + /// Timeout in milliseconds (default is 1000) (optional) + /// Timeout in milliseconds (default is 1000) (optional) + /// Timeout in milliseconds (default is 1000) (optional) + /// Interval in milliseconds (default is 200) (optional) + /// The maximum time for each mdns query in milliseconds. (default is 5 * 1000) (optional) + /// The maximum duration for whole networking scan in milliseconds. Highly suggested! (optional) + /// The start and end IP address of the range to scan. for example: 10.10.0.0-10.10.0.255 (optional) + /// Explicit single-host targets to scan. Each value must parse as an IPv4 or IPv6 address; invalid values yield a structured `{ error: \"invalid_target\", value: \"<raw>\" }` 400 rather than a generic serde rejection at extraction time (mirrors the `range=` / `probe=` validation path). (optional) + /// Gateway network interface IDs to use as scan sources. (optional) + /// The probes to run. Each value is either `ping`, a port number (`22`), or a named service (`rdp`, `https`, …). Validation is deferred to scan-time so failures can be surfaced as a structured 400 — naming the offending value — instead of a generic serde rejection at extraction time. (optional) + /// **Legacy alias** for `report_ping_start`. Prefer the explicit name in new clients; kept so existing consumers don't break. (optional) (deprecated) + /// Enable the execution of broadcast scan (optional) + /// Enable the ping scan on subnet (optional) + /// Enable ZeroConf/mDNS (optional) + /// Enable NetBIOS name-service queries. Default `true` for backward compatibility. Set `false` (or pair with explicit `target=`) to keep NetBIOS from sweeping the surrounding subnet when the caller only wants results for the targets they listed. (optional) + /// Enable resolve dns (optional) + /// Include host-only results. (optional) + /// Emit ping queued/start host results. (optional) + /// Emit ping success host results. (optional) + /// Emit ping failure host results. (optional) + /// Enable TCP service probes. (optional) + /// Policy applied when `range=` and `interface_id=` are both provided. Accepted values: `intersect_selected_interfaces` (default) or `allow_cross_interface_range`. Invalid values yield a structured `{ error: \"invalid_range_interface_policy\", value: \"<raw>\" }` 400 instead of a generic serde rejection (mirrors the `range=` / `probe=` / `target=` validation path). (optional) + /// **Legacy alias** for `range_interface_policy=allow_cross_interface_range`. Prefer the structured policy in new clients. (optional) (deprecated) + /// Response shape emitted on the websocket. Accepted values: `legacy` (default) or `network_scan_result_v1`. Invalid values yield a structured `{ error: \"invalid_response_format\", value: \"<raw>\" }` 400 instead of a generic serde rejection. (optional) + /// Maximum scanner concurrency. (optional) + /// Maximum ping probe concurrency. (optional) + /// Maximum TCP probe concurrency. (optional) + /// **Legacy alias** for `report_ping_failure`. `enable_failure=true` only opts into ping-failure events; TCP-probe failure events require the explicit `report_tcp_failure=true` and are not affected by this alias. **Behavior change:** historically this single flag controlled both ping-failure and TCP-probe-failure reporting. The two are now split: clients that want the old \"both at once\" semantics must send `enable_failure=true&report_tcp_failure=true` together. The split is intentional — TCP-probe failures are typically high-volume noise that callers were filtering client-side anyway, so the two streams are independently gated. (optional) (deprecated) + /// Enable TCP port knocking failure events. (optional) + /// When `true`, fail with HTTP 400 if a ping/TCP-probe socket cannot be bound to the planner-selected interface. Default `false` (warn and fall back to default routing). (optional) + /// Cancellation Token to cancel the request. + /// Task of void + public async System.Threading.Tasks.Task GetNetScanAsync(long? pingInterval = default(long?), long? pingTimeout = default(long?), long? broadcastTimeout = default(long?), long? portScanTimeout = default(long?), long? netbiosTimeout = default(long?), long? netbiosInterval = default(long?), long? mdnsQueryTimeout = default(long?), long? maxWait = default(long?), List? range = default(List?), List? target = default(List?), List? interfaceId = default(List?), List? probe = default(List?), bool? enablePingStart = default(bool?), bool? enableBroadcast = default(bool?), bool? enableSubnetScan = default(bool?), bool? enableZeroconf = default(bool?), bool? enableNetbios = default(bool?), bool? enableResolveDns = default(bool?), bool? includeHostResults = default(bool?), bool? reportPingStart = default(bool?), bool? reportPingSuccess = default(bool?), bool? reportPingFailure = default(bool?), bool? enableTcpProbes = default(bool?), string? rangeInterfacePolicy = default(string?), bool? allowCrossInterfaceRange = default(bool?), string? responseFormat = default(string?), int? maxConcurrency = default(int?), int? maxPingConcurrency = default(int?), int? maxTcpProbeConcurrency = default(int?), bool? enableFailure = default(bool?), bool? reportTcpFailure = default(bool?), bool? interfaceBindStrict = default(bool?), System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken)) + { + await GetNetScanWithHttpInfoAsync(pingInterval, pingTimeout, broadcastTimeout, portScanTimeout, netbiosTimeout, netbiosInterval, mdnsQueryTimeout, maxWait, range, target, interfaceId, probe, enablePingStart, enableBroadcast, enableSubnetScan, enableZeroconf, enableNetbios, enableResolveDns, includeHostResults, reportPingStart, reportPingSuccess, reportPingFailure, enableTcpProbes, rangeInterfacePolicy, allowCrossInterfaceRange, responseFormat, maxConcurrency, maxPingConcurrency, maxTcpProbeConcurrency, enableFailure, reportTcpFailure, interfaceBindStrict, cancellationToken).ConfigureAwait(false); + } + + /// + /// Stream network scan events over a websocket. The endpoint is upgraded from HTTP, so OpenAPI describes the **handshake**: the query parameters that drive the scan (validated before upgrade) and the legacy / v1 event payloads streamed back as JSON text frames. See `NetworkScanResultEvent` for the v1 shape and `LegacyScanEvent` for the legacy shape (selected via `response_format`). + /// + /// Thrown when fails to make API call + /// Interval in milliseconds (default is 200) (optional) + /// Timeout in milliseconds (default is 500) (optional) + /// Timeout in milliseconds (default is 1000) (optional) + /// Timeout in milliseconds (default is 1000) (optional) + /// Timeout in milliseconds (default is 1000) (optional) + /// Interval in milliseconds (default is 200) (optional) + /// The maximum time for each mdns query in milliseconds. (default is 5 * 1000) (optional) + /// The maximum duration for whole networking scan in milliseconds. Highly suggested! (optional) + /// The start and end IP address of the range to scan. for example: 10.10.0.0-10.10.0.255 (optional) + /// Explicit single-host targets to scan. Each value must parse as an IPv4 or IPv6 address; invalid values yield a structured `{ error: \"invalid_target\", value: \"<raw>\" }` 400 rather than a generic serde rejection at extraction time (mirrors the `range=` / `probe=` validation path). (optional) + /// Gateway network interface IDs to use as scan sources. (optional) + /// The probes to run. Each value is either `ping`, a port number (`22`), or a named service (`rdp`, `https`, …). Validation is deferred to scan-time so failures can be surfaced as a structured 400 — naming the offending value — instead of a generic serde rejection at extraction time. (optional) + /// **Legacy alias** for `report_ping_start`. Prefer the explicit name in new clients; kept so existing consumers don't break. (optional) (deprecated) + /// Enable the execution of broadcast scan (optional) + /// Enable the ping scan on subnet (optional) + /// Enable ZeroConf/mDNS (optional) + /// Enable NetBIOS name-service queries. Default `true` for backward compatibility. Set `false` (or pair with explicit `target=`) to keep NetBIOS from sweeping the surrounding subnet when the caller only wants results for the targets they listed. (optional) + /// Enable resolve dns (optional) + /// Include host-only results. (optional) + /// Emit ping queued/start host results. (optional) + /// Emit ping success host results. (optional) + /// Emit ping failure host results. (optional) + /// Enable TCP service probes. (optional) + /// Policy applied when `range=` and `interface_id=` are both provided. Accepted values: `intersect_selected_interfaces` (default) or `allow_cross_interface_range`. Invalid values yield a structured `{ error: \"invalid_range_interface_policy\", value: \"<raw>\" }` 400 instead of a generic serde rejection (mirrors the `range=` / `probe=` / `target=` validation path). (optional) + /// **Legacy alias** for `range_interface_policy=allow_cross_interface_range`. Prefer the structured policy in new clients. (optional) (deprecated) + /// Response shape emitted on the websocket. Accepted values: `legacy` (default) or `network_scan_result_v1`. Invalid values yield a structured `{ error: \"invalid_response_format\", value: \"<raw>\" }` 400 instead of a generic serde rejection. (optional) + /// Maximum scanner concurrency. (optional) + /// Maximum ping probe concurrency. (optional) + /// Maximum TCP probe concurrency. (optional) + /// **Legacy alias** for `report_ping_failure`. `enable_failure=true` only opts into ping-failure events; TCP-probe failure events require the explicit `report_tcp_failure=true` and are not affected by this alias. **Behavior change:** historically this single flag controlled both ping-failure and TCP-probe-failure reporting. The two are now split: clients that want the old \"both at once\" semantics must send `enable_failure=true&report_tcp_failure=true` together. The split is intentional — TCP-probe failures are typically high-volume noise that callers were filtering client-side anyway, so the two streams are independently gated. (optional) (deprecated) + /// Enable TCP port knocking failure events. (optional) + /// When `true`, fail with HTTP 400 if a ping/TCP-probe socket cannot be bound to the planner-selected interface. Default `false` (warn and fall back to default routing). (optional) + /// Cancellation Token to cancel the request. + /// Task of ApiResponse + public async System.Threading.Tasks.Task> GetNetScanWithHttpInfoAsync(long? pingInterval = default(long?), long? pingTimeout = default(long?), long? broadcastTimeout = default(long?), long? portScanTimeout = default(long?), long? netbiosTimeout = default(long?), long? netbiosInterval = default(long?), long? mdnsQueryTimeout = default(long?), long? maxWait = default(long?), List? range = default(List?), List? target = default(List?), List? interfaceId = default(List?), List? probe = default(List?), bool? enablePingStart = default(bool?), bool? enableBroadcast = default(bool?), bool? enableSubnetScan = default(bool?), bool? enableZeroconf = default(bool?), bool? enableNetbios = default(bool?), bool? enableResolveDns = default(bool?), bool? includeHostResults = default(bool?), bool? reportPingStart = default(bool?), bool? reportPingSuccess = default(bool?), bool? reportPingFailure = default(bool?), bool? enableTcpProbes = default(bool?), string? rangeInterfacePolicy = default(string?), bool? allowCrossInterfaceRange = default(bool?), string? responseFormat = default(string?), int? maxConcurrency = default(int?), int? maxPingConcurrency = default(int?), int? maxTcpProbeConcurrency = default(int?), bool? enableFailure = default(bool?), bool? reportTcpFailure = default(bool?), bool? interfaceBindStrict = default(bool?), System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken)) + { + + Devolutions.Gateway.Client.Client.RequestOptions localVarRequestOptions = new Devolutions.Gateway.Client.Client.RequestOptions(); + + string[] _contentTypes = new string[] { + }; + + // to determine the Accept header + string[] _accepts = new string[] { + }; + + + var localVarContentType = Devolutions.Gateway.Client.Client.ClientUtils.SelectHeaderContentType(_contentTypes); + if (localVarContentType != null) localVarRequestOptions.HeaderParameters.Add("Content-Type", localVarContentType); + + var localVarAccept = Devolutions.Gateway.Client.Client.ClientUtils.SelectHeaderAccept(_accepts); + if (localVarAccept != null) localVarRequestOptions.HeaderParameters.Add("Accept", localVarAccept); + + if (pingInterval != null) + { + localVarRequestOptions.QueryParameters.Add(Devolutions.Gateway.Client.Client.ClientUtils.ParameterToMultiMap("", "ping_interval", pingInterval)); + } + if (pingTimeout != null) + { + localVarRequestOptions.QueryParameters.Add(Devolutions.Gateway.Client.Client.ClientUtils.ParameterToMultiMap("", "ping_timeout", pingTimeout)); + } + if (broadcastTimeout != null) + { + localVarRequestOptions.QueryParameters.Add(Devolutions.Gateway.Client.Client.ClientUtils.ParameterToMultiMap("", "broadcast_timeout", broadcastTimeout)); + } + if (portScanTimeout != null) + { + localVarRequestOptions.QueryParameters.Add(Devolutions.Gateway.Client.Client.ClientUtils.ParameterToMultiMap("", "port_scan_timeout", portScanTimeout)); + } + if (netbiosTimeout != null) + { + localVarRequestOptions.QueryParameters.Add(Devolutions.Gateway.Client.Client.ClientUtils.ParameterToMultiMap("", "netbios_timeout", netbiosTimeout)); + } + if (netbiosInterval != null) + { + localVarRequestOptions.QueryParameters.Add(Devolutions.Gateway.Client.Client.ClientUtils.ParameterToMultiMap("", "netbios_interval", netbiosInterval)); + } + if (mdnsQueryTimeout != null) + { + localVarRequestOptions.QueryParameters.Add(Devolutions.Gateway.Client.Client.ClientUtils.ParameterToMultiMap("", "mdns_query_timeout", mdnsQueryTimeout)); + } + if (maxWait != null) + { + localVarRequestOptions.QueryParameters.Add(Devolutions.Gateway.Client.Client.ClientUtils.ParameterToMultiMap("", "max_wait", maxWait)); + } + if (range != null) + { + localVarRequestOptions.QueryParameters.Add(Devolutions.Gateway.Client.Client.ClientUtils.ParameterToMultiMap("multi", "range", range)); + } + if (target != null) + { + localVarRequestOptions.QueryParameters.Add(Devolutions.Gateway.Client.Client.ClientUtils.ParameterToMultiMap("multi", "target", target)); + } + if (interfaceId != null) + { + localVarRequestOptions.QueryParameters.Add(Devolutions.Gateway.Client.Client.ClientUtils.ParameterToMultiMap("multi", "interface_id", interfaceId)); + } + if (probe != null) + { + localVarRequestOptions.QueryParameters.Add(Devolutions.Gateway.Client.Client.ClientUtils.ParameterToMultiMap("multi", "probe", probe)); + } + if (enablePingStart != null) + { + localVarRequestOptions.QueryParameters.Add(Devolutions.Gateway.Client.Client.ClientUtils.ParameterToMultiMap("", "enable_ping_start", enablePingStart)); + } + if (enableBroadcast != null) + { + localVarRequestOptions.QueryParameters.Add(Devolutions.Gateway.Client.Client.ClientUtils.ParameterToMultiMap("", "enable_broadcast", enableBroadcast)); + } + if (enableSubnetScan != null) + { + localVarRequestOptions.QueryParameters.Add(Devolutions.Gateway.Client.Client.ClientUtils.ParameterToMultiMap("", "enable_subnet_scan", enableSubnetScan)); + } + if (enableZeroconf != null) + { + localVarRequestOptions.QueryParameters.Add(Devolutions.Gateway.Client.Client.ClientUtils.ParameterToMultiMap("", "enable_zeroconf", enableZeroconf)); + } + if (enableNetbios != null) + { + localVarRequestOptions.QueryParameters.Add(Devolutions.Gateway.Client.Client.ClientUtils.ParameterToMultiMap("", "enable_netbios", enableNetbios)); + } + if (enableResolveDns != null) + { + localVarRequestOptions.QueryParameters.Add(Devolutions.Gateway.Client.Client.ClientUtils.ParameterToMultiMap("", "enable_resolve_dns", enableResolveDns)); + } + if (includeHostResults != null) + { + localVarRequestOptions.QueryParameters.Add(Devolutions.Gateway.Client.Client.ClientUtils.ParameterToMultiMap("", "include_host_results", includeHostResults)); + } + if (reportPingStart != null) + { + localVarRequestOptions.QueryParameters.Add(Devolutions.Gateway.Client.Client.ClientUtils.ParameterToMultiMap("", "report_ping_start", reportPingStart)); + } + if (reportPingSuccess != null) + { + localVarRequestOptions.QueryParameters.Add(Devolutions.Gateway.Client.Client.ClientUtils.ParameterToMultiMap("", "report_ping_success", reportPingSuccess)); + } + if (reportPingFailure != null) + { + localVarRequestOptions.QueryParameters.Add(Devolutions.Gateway.Client.Client.ClientUtils.ParameterToMultiMap("", "report_ping_failure", reportPingFailure)); + } + if (enableTcpProbes != null) + { + localVarRequestOptions.QueryParameters.Add(Devolutions.Gateway.Client.Client.ClientUtils.ParameterToMultiMap("", "enable_tcp_probes", enableTcpProbes)); + } + if (rangeInterfacePolicy != null) + { + localVarRequestOptions.QueryParameters.Add(Devolutions.Gateway.Client.Client.ClientUtils.ParameterToMultiMap("", "range_interface_policy", rangeInterfacePolicy)); + } + if (allowCrossInterfaceRange != null) + { + localVarRequestOptions.QueryParameters.Add(Devolutions.Gateway.Client.Client.ClientUtils.ParameterToMultiMap("", "allow_cross_interface_range", allowCrossInterfaceRange)); + } + if (responseFormat != null) + { + localVarRequestOptions.QueryParameters.Add(Devolutions.Gateway.Client.Client.ClientUtils.ParameterToMultiMap("", "response_format", responseFormat)); + } + if (maxConcurrency != null) + { + localVarRequestOptions.QueryParameters.Add(Devolutions.Gateway.Client.Client.ClientUtils.ParameterToMultiMap("", "max_concurrency", maxConcurrency)); + } + if (maxPingConcurrency != null) + { + localVarRequestOptions.QueryParameters.Add(Devolutions.Gateway.Client.Client.ClientUtils.ParameterToMultiMap("", "max_ping_concurrency", maxPingConcurrency)); + } + if (maxTcpProbeConcurrency != null) + { + localVarRequestOptions.QueryParameters.Add(Devolutions.Gateway.Client.Client.ClientUtils.ParameterToMultiMap("", "max_tcp_probe_concurrency", maxTcpProbeConcurrency)); + } + if (enableFailure != null) + { + localVarRequestOptions.QueryParameters.Add(Devolutions.Gateway.Client.Client.ClientUtils.ParameterToMultiMap("", "enable_failure", enableFailure)); + } + if (reportTcpFailure != null) + { + localVarRequestOptions.QueryParameters.Add(Devolutions.Gateway.Client.Client.ClientUtils.ParameterToMultiMap("", "report_tcp_failure", reportTcpFailure)); + } + if (interfaceBindStrict != null) + { + localVarRequestOptions.QueryParameters.Add(Devolutions.Gateway.Client.Client.ClientUtils.ParameterToMultiMap("", "interface_bind_strict", interfaceBindStrict)); + } + + // authentication (netscan_token) required + // bearer authentication required + if (!string.IsNullOrEmpty(this.Configuration.AccessToken) && !localVarRequestOptions.HeaderParameters.ContainsKey("Authorization")) + { + localVarRequestOptions.HeaderParameters.Add("Authorization", "Bearer " + this.Configuration.AccessToken); + } + + // make the HTTP request + + var localVarResponse = await this.AsynchronousClient.GetAsync("/jet/net/scan", localVarRequestOptions, this.Configuration, cancellationToken).ConfigureAwait(false); + + if (this.ExceptionFactory != null) + { + Exception _exception = this.ExceptionFactory("GetNetScan", localVarResponse); + if (_exception != null) throw _exception; + } + + return localVarResponse; + } + } } diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Api/NetworkMonitoringApi.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Api/NetworkMonitoringApi.cs index 659e5ee61..cb7415dc3 100644 --- a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Api/NetworkMonitoringApi.cs +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Api/NetworkMonitoringApi.cs @@ -3,7 +3,7 @@ * * Protocol-aware fine-grained relay server * - * The version of the OpenAPI document: 2025.3.2 + * The version of the OpenAPI document: 2026.2.4 * Contact: infos@devolutions.net * Generated by: https://github.com/openapitools/openapi-generator.git */ diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Api/PreflightApi.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Api/PreflightApi.cs index ddedb993c..03f20799b 100644 --- a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Api/PreflightApi.cs +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Api/PreflightApi.cs @@ -3,7 +3,7 @@ * * Protocol-aware fine-grained relay server * - * The version of the OpenAPI document: 2025.3.2 + * The version of the OpenAPI document: 2026.2.4 * Contact: infos@devolutions.net * Generated by: https://github.com/openapitools/openapi-generator.git */ diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Api/SessionsApi.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Api/SessionsApi.cs index 687defe3c..f08d09dc1 100644 --- a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Api/SessionsApi.cs +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Api/SessionsApi.cs @@ -3,7 +3,7 @@ * * Protocol-aware fine-grained relay server * - * The version of the OpenAPI document: 2025.3.2 + * The version of the OpenAPI document: 2026.2.4 * Contact: infos@devolutions.net * Generated by: https://github.com/openapitools/openapi-generator.git */ diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Api/TrafficApi.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Api/TrafficApi.cs index c337afbd8..127ae622a 100644 --- a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Api/TrafficApi.cs +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Api/TrafficApi.cs @@ -3,7 +3,7 @@ * * Protocol-aware fine-grained relay server * - * The version of the OpenAPI document: 2025.3.2 + * The version of the OpenAPI document: 2026.2.4 * Contact: infos@devolutions.net * Generated by: https://github.com/openapitools/openapi-generator.git */ diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Api/UpdateApi.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Api/UpdateApi.cs index 933ab6963..0eb72c187 100644 --- a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Api/UpdateApi.cs +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Api/UpdateApi.cs @@ -3,7 +3,7 @@ * * Protocol-aware fine-grained relay server * - * The version of the OpenAPI document: 2025.3.2 + * The version of the OpenAPI document: 2026.2.4 * Contact: infos@devolutions.net * Generated by: https://github.com/openapitools/openapi-generator.git */ @@ -17,6 +17,7 @@ using System.Net.Http; using System.Net.Mime; using Devolutions.Gateway.Client.Client; +using Devolutions.Gateway.Client.Model; namespace Devolutions.Gateway.Client.Api { @@ -28,26 +29,87 @@ public interface IUpdateApiSync : IApiAccessor { #region Synchronous Operations /// - /// Triggers Devolutions Gateway update process. + /// Retrieve the currently installed version of each Devolutions product. /// /// - /// This is done via updating `Agent/update.json` file, which is then read by Devolutions Agent when changes are detected. If the version written to `update.json` is indeed higher than the currently installed version, Devolutions Agent will proceed with the update process. + /// Reads `update_status.json`, which is written by the Devolutions Agent on startup and refreshed after every update run. When the file does not exist (agent not installed or is an older version), the endpoint returns HTTP 503 because updater status is unavailable. /// /// Thrown when fails to make API call - /// The version to install; use 'latest' for the latest version, or 'w.x.y.z' for a specific version + /// GetUpdateProductsResponse + GetUpdateProductsResponse GetUpdateProducts(); + + /// + /// Retrieve the currently installed version of each Devolutions product. + /// + /// + /// Reads `update_status.json`, which is written by the Devolutions Agent on startup and refreshed after every update run. When the file does not exist (agent not installed or is an older version), the endpoint returns HTTP 503 because updater status is unavailable. + /// + /// Thrown when fails to make API call + /// ApiResponse of GetUpdateProductsResponse + ApiResponse GetUpdateProductsWithHttpInfo(); + /// + /// Retrieve the current Devolutions Agent auto-update schedule. + /// + /// + /// Reads the `Schedule` field from `update_status.json`. When the field is absent the response contains zeroed defaults (`Enabled: false`, interval `0`, window start `0`, no products). + /// + /// Thrown when fails to make API call + /// GetUpdateScheduleResponse + GetUpdateScheduleResponse GetUpdateSchedule(); + + /// + /// Retrieve the current Devolutions Agent auto-update schedule. + /// + /// + /// Reads the `Schedule` field from `update_status.json`. When the field is absent the response contains zeroed defaults (`Enabled: false`, interval `0`, window start `0`, no products). + /// + /// Thrown when fails to make API call + /// ApiResponse of GetUpdateScheduleResponse + ApiResponse GetUpdateScheduleWithHttpInfo(); + /// + /// Set the Devolutions Agent auto-update schedule. + /// + /// + /// Writes the `Schedule` field into `update.json`. The agent watches this file and applies the new schedule immediately, then persists it to `agent.json`. All other fields in `update.json` are preserved; the `VersionMinor` field is reset to the minor version this gateway build understands so the agent does not see an unknown future version. + /// + /// Thrown when fails to make API call + /// /// Object - Object TriggerUpdate(string version); + Object SetUpdateSchedule(SetUpdateScheduleRequest setUpdateScheduleRequest); /// - /// Triggers Devolutions Gateway update process. + /// Set the Devolutions Agent auto-update schedule. /// /// - /// This is done via updating `Agent/update.json` file, which is then read by Devolutions Agent when changes are detected. If the version written to `update.json` is indeed higher than the currently installed version, Devolutions Agent will proceed with the update process. + /// Writes the `Schedule` field into `update.json`. The agent watches this file and applies the new schedule immediately, then persists it to `agent.json`. All other fields in `update.json` are preserved; the `VersionMinor` field is reset to the minor version this gateway build understands so the agent does not see an unknown future version. /// /// Thrown when fails to make API call - /// The version to install; use 'latest' for the latest version, or 'w.x.y.z' for a specific version + /// /// ApiResponse of Object - ApiResponse TriggerUpdateWithHttpInfo(string version); + ApiResponse SetUpdateScheduleWithHttpInfo(SetUpdateScheduleRequest setUpdateScheduleRequest); + /// + /// Trigger an update for one or more Devolutions products. + /// + /// + /// Writes the requested version(s) into `Agent/update.json`, which is watched by Devolutions Agent. When a requested version is higher than the installed version the agent proceeds with the update. **Body form** (preferred): pass a JSON body with a `Products` map. **Query-param form** (legacy, gateway-only): `POST /jet/update?version=latest`. This form updates only the Gateway product and is kept for backward compatibility. Both forms cannot be used simultaneously; doing so returns HTTP 400. + /// + /// Thrown when fails to make API call + /// Gateway-only target version; use the request body for multi-product updates (optional) (deprecated) + /// Products and target versions to update (optional) + /// Object + Object TriggerUpdate(string? version = default(string?), UpdateRequestSchema? updateRequestSchema = default(UpdateRequestSchema?)); + + /// + /// Trigger an update for one or more Devolutions products. + /// + /// + /// Writes the requested version(s) into `Agent/update.json`, which is watched by Devolutions Agent. When a requested version is higher than the installed version the agent proceeds with the update. **Body form** (preferred): pass a JSON body with a `Products` map. **Query-param form** (legacy, gateway-only): `POST /jet/update?version=latest`. This form updates only the Gateway product and is kept for backward compatibility. Both forms cannot be used simultaneously; doing so returns HTTP 400. + /// + /// Thrown when fails to make API call + /// Gateway-only target version; use the request body for multi-product updates (optional) (deprecated) + /// Products and target versions to update (optional) + /// ApiResponse of Object + ApiResponse TriggerUpdateWithHttpInfo(string? version = default(string?), UpdateRequestSchema? updateRequestSchema = default(UpdateRequestSchema?)); #endregion Synchronous Operations } @@ -58,28 +120,95 @@ public interface IUpdateApiAsync : IApiAccessor { #region Asynchronous Operations /// - /// Triggers Devolutions Gateway update process. + /// Retrieve the currently installed version of each Devolutions product. + /// + /// + /// Reads `update_status.json`, which is written by the Devolutions Agent on startup and refreshed after every update run. When the file does not exist (agent not installed or is an older version), the endpoint returns HTTP 503 because updater status is unavailable. + /// + /// Thrown when fails to make API call + /// Cancellation Token to cancel the request. + /// Task of GetUpdateProductsResponse + System.Threading.Tasks.Task GetUpdateProductsAsync(System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken)); + + /// + /// Retrieve the currently installed version of each Devolutions product. + /// + /// + /// Reads `update_status.json`, which is written by the Devolutions Agent on startup and refreshed after every update run. When the file does not exist (agent not installed or is an older version), the endpoint returns HTTP 503 because updater status is unavailable. + /// + /// Thrown when fails to make API call + /// Cancellation Token to cancel the request. + /// Task of ApiResponse (GetUpdateProductsResponse) + System.Threading.Tasks.Task> GetUpdateProductsWithHttpInfoAsync(System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken)); + /// + /// Retrieve the current Devolutions Agent auto-update schedule. + /// + /// + /// Reads the `Schedule` field from `update_status.json`. When the field is absent the response contains zeroed defaults (`Enabled: false`, interval `0`, window start `0`, no products). + /// + /// Thrown when fails to make API call + /// Cancellation Token to cancel the request. + /// Task of GetUpdateScheduleResponse + System.Threading.Tasks.Task GetUpdateScheduleAsync(System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken)); + + /// + /// Retrieve the current Devolutions Agent auto-update schedule. + /// + /// + /// Reads the `Schedule` field from `update_status.json`. When the field is absent the response contains zeroed defaults (`Enabled: false`, interval `0`, window start `0`, no products). + /// + /// Thrown when fails to make API call + /// Cancellation Token to cancel the request. + /// Task of ApiResponse (GetUpdateScheduleResponse) + System.Threading.Tasks.Task> GetUpdateScheduleWithHttpInfoAsync(System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken)); + /// + /// Set the Devolutions Agent auto-update schedule. /// /// - /// This is done via updating `Agent/update.json` file, which is then read by Devolutions Agent when changes are detected. If the version written to `update.json` is indeed higher than the currently installed version, Devolutions Agent will proceed with the update process. + /// Writes the `Schedule` field into `update.json`. The agent watches this file and applies the new schedule immediately, then persists it to `agent.json`. All other fields in `update.json` are preserved; the `VersionMinor` field is reset to the minor version this gateway build understands so the agent does not see an unknown future version. /// /// Thrown when fails to make API call - /// The version to install; use 'latest' for the latest version, or 'w.x.y.z' for a specific version + /// /// Cancellation Token to cancel the request. /// Task of Object - System.Threading.Tasks.Task TriggerUpdateAsync(string version, System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken)); + System.Threading.Tasks.Task SetUpdateScheduleAsync(SetUpdateScheduleRequest setUpdateScheduleRequest, System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken)); /// - /// Triggers Devolutions Gateway update process. + /// Set the Devolutions Agent auto-update schedule. /// /// - /// This is done via updating `Agent/update.json` file, which is then read by Devolutions Agent when changes are detected. If the version written to `update.json` is indeed higher than the currently installed version, Devolutions Agent will proceed with the update process. + /// Writes the `Schedule` field into `update.json`. The agent watches this file and applies the new schedule immediately, then persists it to `agent.json`. All other fields in `update.json` are preserved; the `VersionMinor` field is reset to the minor version this gateway build understands so the agent does not see an unknown future version. /// /// Thrown when fails to make API call - /// The version to install; use 'latest' for the latest version, or 'w.x.y.z' for a specific version + /// /// Cancellation Token to cancel the request. /// Task of ApiResponse (Object) - System.Threading.Tasks.Task> TriggerUpdateWithHttpInfoAsync(string version, System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken)); + System.Threading.Tasks.Task> SetUpdateScheduleWithHttpInfoAsync(SetUpdateScheduleRequest setUpdateScheduleRequest, System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken)); + /// + /// Trigger an update for one or more Devolutions products. + /// + /// + /// Writes the requested version(s) into `Agent/update.json`, which is watched by Devolutions Agent. When a requested version is higher than the installed version the agent proceeds with the update. **Body form** (preferred): pass a JSON body with a `Products` map. **Query-param form** (legacy, gateway-only): `POST /jet/update?version=latest`. This form updates only the Gateway product and is kept for backward compatibility. Both forms cannot be used simultaneously; doing so returns HTTP 400. + /// + /// Thrown when fails to make API call + /// Gateway-only target version; use the request body for multi-product updates (optional) (deprecated) + /// Products and target versions to update (optional) + /// Cancellation Token to cancel the request. + /// Task of Object + System.Threading.Tasks.Task TriggerUpdateAsync(string? version = default(string?), UpdateRequestSchema? updateRequestSchema = default(UpdateRequestSchema?), System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken)); + + /// + /// Trigger an update for one or more Devolutions products. + /// + /// + /// Writes the requested version(s) into `Agent/update.json`, which is watched by Devolutions Agent. When a requested version is higher than the installed version the agent proceeds with the update. **Body form** (preferred): pass a JSON body with a `Products` map. **Query-param form** (legacy, gateway-only): `POST /jet/update?version=latest`. This form updates only the Gateway product and is kept for backward compatibility. Both forms cannot be used simultaneously; doing so returns HTTP 400. + /// + /// Thrown when fails to make API call + /// Gateway-only target version; use the request body for multi-product updates (optional) (deprecated) + /// Products and target versions to update (optional) + /// Cancellation Token to cancel the request. + /// Task of ApiResponse (Object) + System.Threading.Tasks.Task> TriggerUpdateWithHttpInfoAsync(string? version = default(string?), UpdateRequestSchema? updateRequestSchema = default(UpdateRequestSchema?), System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken)); #endregion Asynchronous Operations } @@ -294,32 +423,259 @@ public Devolutions.Gateway.Client.Client.ExceptionFactory ExceptionFactory } /// - /// Triggers Devolutions Gateway update process. This is done via updating `Agent/update.json` file, which is then read by Devolutions Agent when changes are detected. If the version written to `update.json` is indeed higher than the currently installed version, Devolutions Agent will proceed with the update process. + /// Retrieve the currently installed version of each Devolutions product. Reads `update_status.json`, which is written by the Devolutions Agent on startup and refreshed after every update run. When the file does not exist (agent not installed or is an older version), the endpoint returns HTTP 503 because updater status is unavailable. + /// + /// Thrown when fails to make API call + /// GetUpdateProductsResponse + public GetUpdateProductsResponse GetUpdateProducts() + { + Devolutions.Gateway.Client.Client.ApiResponse localVarResponse = GetUpdateProductsWithHttpInfo(); + return localVarResponse.Data; + } + + /// + /// Retrieve the currently installed version of each Devolutions product. Reads `update_status.json`, which is written by the Devolutions Agent on startup and refreshed after every update run. When the file does not exist (agent not installed or is an older version), the endpoint returns HTTP 503 because updater status is unavailable. + /// + /// Thrown when fails to make API call + /// ApiResponse of GetUpdateProductsResponse + public Devolutions.Gateway.Client.Client.ApiResponse GetUpdateProductsWithHttpInfo() + { + Devolutions.Gateway.Client.Client.RequestOptions localVarRequestOptions = new Devolutions.Gateway.Client.Client.RequestOptions(); + + string[] _contentTypes = new string[] { + }; + + // to determine the Accept header + string[] _accepts = new string[] { + "application/json" + }; + + var localVarContentType = Devolutions.Gateway.Client.Client.ClientUtils.SelectHeaderContentType(_contentTypes); + if (localVarContentType != null) localVarRequestOptions.HeaderParameters.Add("Content-Type", localVarContentType); + + var localVarAccept = Devolutions.Gateway.Client.Client.ClientUtils.SelectHeaderAccept(_accepts); + if (localVarAccept != null) localVarRequestOptions.HeaderParameters.Add("Accept", localVarAccept); + + + // authentication (scope_token) required + // bearer authentication required + if (!string.IsNullOrEmpty(this.Configuration.AccessToken) && !localVarRequestOptions.HeaderParameters.ContainsKey("Authorization")) + { + localVarRequestOptions.HeaderParameters.Add("Authorization", "Bearer " + this.Configuration.AccessToken); + } + + // make the HTTP request + var localVarResponse = this.Client.Get("/jet/update", localVarRequestOptions, this.Configuration); + + if (this.ExceptionFactory != null) + { + Exception _exception = this.ExceptionFactory("GetUpdateProducts", localVarResponse); + if (_exception != null) throw _exception; + } + + return localVarResponse; + } + + /// + /// Retrieve the currently installed version of each Devolutions product. Reads `update_status.json`, which is written by the Devolutions Agent on startup and refreshed after every update run. When the file does not exist (agent not installed or is an older version), the endpoint returns HTTP 503 because updater status is unavailable. + /// + /// Thrown when fails to make API call + /// Cancellation Token to cancel the request. + /// Task of GetUpdateProductsResponse + public async System.Threading.Tasks.Task GetUpdateProductsAsync(System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken)) + { + Devolutions.Gateway.Client.Client.ApiResponse localVarResponse = await GetUpdateProductsWithHttpInfoAsync(cancellationToken).ConfigureAwait(false); + return localVarResponse.Data; + } + + /// + /// Retrieve the currently installed version of each Devolutions product. Reads `update_status.json`, which is written by the Devolutions Agent on startup and refreshed after every update run. When the file does not exist (agent not installed or is an older version), the endpoint returns HTTP 503 because updater status is unavailable. + /// + /// Thrown when fails to make API call + /// Cancellation Token to cancel the request. + /// Task of ApiResponse (GetUpdateProductsResponse) + public async System.Threading.Tasks.Task> GetUpdateProductsWithHttpInfoAsync(System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken)) + { + + Devolutions.Gateway.Client.Client.RequestOptions localVarRequestOptions = new Devolutions.Gateway.Client.Client.RequestOptions(); + + string[] _contentTypes = new string[] { + }; + + // to determine the Accept header + string[] _accepts = new string[] { + "application/json" + }; + + + var localVarContentType = Devolutions.Gateway.Client.Client.ClientUtils.SelectHeaderContentType(_contentTypes); + if (localVarContentType != null) localVarRequestOptions.HeaderParameters.Add("Content-Type", localVarContentType); + + var localVarAccept = Devolutions.Gateway.Client.Client.ClientUtils.SelectHeaderAccept(_accepts); + if (localVarAccept != null) localVarRequestOptions.HeaderParameters.Add("Accept", localVarAccept); + + + // authentication (scope_token) required + // bearer authentication required + if (!string.IsNullOrEmpty(this.Configuration.AccessToken) && !localVarRequestOptions.HeaderParameters.ContainsKey("Authorization")) + { + localVarRequestOptions.HeaderParameters.Add("Authorization", "Bearer " + this.Configuration.AccessToken); + } + + // make the HTTP request + + var localVarResponse = await this.AsynchronousClient.GetAsync("/jet/update", localVarRequestOptions, this.Configuration, cancellationToken).ConfigureAwait(false); + + if (this.ExceptionFactory != null) + { + Exception _exception = this.ExceptionFactory("GetUpdateProducts", localVarResponse); + if (_exception != null) throw _exception; + } + + return localVarResponse; + } + + /// + /// Retrieve the current Devolutions Agent auto-update schedule. Reads the `Schedule` field from `update_status.json`. When the field is absent the response contains zeroed defaults (`Enabled: false`, interval `0`, window start `0`, no products). /// /// Thrown when fails to make API call - /// The version to install; use 'latest' for the latest version, or 'w.x.y.z' for a specific version + /// GetUpdateScheduleResponse + public GetUpdateScheduleResponse GetUpdateSchedule() + { + Devolutions.Gateway.Client.Client.ApiResponse localVarResponse = GetUpdateScheduleWithHttpInfo(); + return localVarResponse.Data; + } + + /// + /// Retrieve the current Devolutions Agent auto-update schedule. Reads the `Schedule` field from `update_status.json`. When the field is absent the response contains zeroed defaults (`Enabled: false`, interval `0`, window start `0`, no products). + /// + /// Thrown when fails to make API call + /// ApiResponse of GetUpdateScheduleResponse + public Devolutions.Gateway.Client.Client.ApiResponse GetUpdateScheduleWithHttpInfo() + { + Devolutions.Gateway.Client.Client.RequestOptions localVarRequestOptions = new Devolutions.Gateway.Client.Client.RequestOptions(); + + string[] _contentTypes = new string[] { + }; + + // to determine the Accept header + string[] _accepts = new string[] { + "application/json" + }; + + var localVarContentType = Devolutions.Gateway.Client.Client.ClientUtils.SelectHeaderContentType(_contentTypes); + if (localVarContentType != null) localVarRequestOptions.HeaderParameters.Add("Content-Type", localVarContentType); + + var localVarAccept = Devolutions.Gateway.Client.Client.ClientUtils.SelectHeaderAccept(_accepts); + if (localVarAccept != null) localVarRequestOptions.HeaderParameters.Add("Accept", localVarAccept); + + + // authentication (scope_token) required + // bearer authentication required + if (!string.IsNullOrEmpty(this.Configuration.AccessToken) && !localVarRequestOptions.HeaderParameters.ContainsKey("Authorization")) + { + localVarRequestOptions.HeaderParameters.Add("Authorization", "Bearer " + this.Configuration.AccessToken); + } + + // make the HTTP request + var localVarResponse = this.Client.Get("/jet/update/schedule", localVarRequestOptions, this.Configuration); + + if (this.ExceptionFactory != null) + { + Exception _exception = this.ExceptionFactory("GetUpdateSchedule", localVarResponse); + if (_exception != null) throw _exception; + } + + return localVarResponse; + } + + /// + /// Retrieve the current Devolutions Agent auto-update schedule. Reads the `Schedule` field from `update_status.json`. When the field is absent the response contains zeroed defaults (`Enabled: false`, interval `0`, window start `0`, no products). + /// + /// Thrown when fails to make API call + /// Cancellation Token to cancel the request. + /// Task of GetUpdateScheduleResponse + public async System.Threading.Tasks.Task GetUpdateScheduleAsync(System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken)) + { + Devolutions.Gateway.Client.Client.ApiResponse localVarResponse = await GetUpdateScheduleWithHttpInfoAsync(cancellationToken).ConfigureAwait(false); + return localVarResponse.Data; + } + + /// + /// Retrieve the current Devolutions Agent auto-update schedule. Reads the `Schedule` field from `update_status.json`. When the field is absent the response contains zeroed defaults (`Enabled: false`, interval `0`, window start `0`, no products). + /// + /// Thrown when fails to make API call + /// Cancellation Token to cancel the request. + /// Task of ApiResponse (GetUpdateScheduleResponse) + public async System.Threading.Tasks.Task> GetUpdateScheduleWithHttpInfoAsync(System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken)) + { + + Devolutions.Gateway.Client.Client.RequestOptions localVarRequestOptions = new Devolutions.Gateway.Client.Client.RequestOptions(); + + string[] _contentTypes = new string[] { + }; + + // to determine the Accept header + string[] _accepts = new string[] { + "application/json" + }; + + + var localVarContentType = Devolutions.Gateway.Client.Client.ClientUtils.SelectHeaderContentType(_contentTypes); + if (localVarContentType != null) localVarRequestOptions.HeaderParameters.Add("Content-Type", localVarContentType); + + var localVarAccept = Devolutions.Gateway.Client.Client.ClientUtils.SelectHeaderAccept(_accepts); + if (localVarAccept != null) localVarRequestOptions.HeaderParameters.Add("Accept", localVarAccept); + + + // authentication (scope_token) required + // bearer authentication required + if (!string.IsNullOrEmpty(this.Configuration.AccessToken) && !localVarRequestOptions.HeaderParameters.ContainsKey("Authorization")) + { + localVarRequestOptions.HeaderParameters.Add("Authorization", "Bearer " + this.Configuration.AccessToken); + } + + // make the HTTP request + + var localVarResponse = await this.AsynchronousClient.GetAsync("/jet/update/schedule", localVarRequestOptions, this.Configuration, cancellationToken).ConfigureAwait(false); + + if (this.ExceptionFactory != null) + { + Exception _exception = this.ExceptionFactory("GetUpdateSchedule", localVarResponse); + if (_exception != null) throw _exception; + } + + return localVarResponse; + } + + /// + /// Set the Devolutions Agent auto-update schedule. Writes the `Schedule` field into `update.json`. The agent watches this file and applies the new schedule immediately, then persists it to `agent.json`. All other fields in `update.json` are preserved; the `VersionMinor` field is reset to the minor version this gateway build understands so the agent does not see an unknown future version. + /// + /// Thrown when fails to make API call + /// /// Object - public Object TriggerUpdate(string version) + public Object SetUpdateSchedule(SetUpdateScheduleRequest setUpdateScheduleRequest) { - Devolutions.Gateway.Client.Client.ApiResponse localVarResponse = TriggerUpdateWithHttpInfo(version); + Devolutions.Gateway.Client.Client.ApiResponse localVarResponse = SetUpdateScheduleWithHttpInfo(setUpdateScheduleRequest); return localVarResponse.Data; } /// - /// Triggers Devolutions Gateway update process. This is done via updating `Agent/update.json` file, which is then read by Devolutions Agent when changes are detected. If the version written to `update.json` is indeed higher than the currently installed version, Devolutions Agent will proceed with the update process. + /// Set the Devolutions Agent auto-update schedule. Writes the `Schedule` field into `update.json`. The agent watches this file and applies the new schedule immediately, then persists it to `agent.json`. All other fields in `update.json` are preserved; the `VersionMinor` field is reset to the minor version this gateway build understands so the agent does not see an unknown future version. /// /// Thrown when fails to make API call - /// The version to install; use 'latest' for the latest version, or 'w.x.y.z' for a specific version + /// /// ApiResponse of Object - public Devolutions.Gateway.Client.Client.ApiResponse TriggerUpdateWithHttpInfo(string version) + public Devolutions.Gateway.Client.Client.ApiResponse SetUpdateScheduleWithHttpInfo(SetUpdateScheduleRequest setUpdateScheduleRequest) { - // verify the required parameter 'version' is set - if (version == null) - throw new Devolutions.Gateway.Client.Client.ApiException(400, "Missing required parameter 'version' when calling UpdateApi->TriggerUpdate"); + // verify the required parameter 'setUpdateScheduleRequest' is set + if (setUpdateScheduleRequest == null) + throw new Devolutions.Gateway.Client.Client.ApiException(400, "Missing required parameter 'setUpdateScheduleRequest' when calling UpdateApi->SetUpdateSchedule"); Devolutions.Gateway.Client.Client.RequestOptions localVarRequestOptions = new Devolutions.Gateway.Client.Client.RequestOptions(); string[] _contentTypes = new string[] { + "application/json" }; // to determine the Accept header @@ -333,7 +689,138 @@ public Devolutions.Gateway.Client.Client.ApiResponse TriggerUpdateWithHt var localVarAccept = Devolutions.Gateway.Client.Client.ClientUtils.SelectHeaderAccept(_accepts); if (localVarAccept != null) localVarRequestOptions.HeaderParameters.Add("Accept", localVarAccept); - localVarRequestOptions.QueryParameters.Add(Devolutions.Gateway.Client.Client.ClientUtils.ParameterToMultiMap("", "version", version)); + localVarRequestOptions.Data = setUpdateScheduleRequest; + + // authentication (scope_token) required + // bearer authentication required + if (!string.IsNullOrEmpty(this.Configuration.AccessToken) && !localVarRequestOptions.HeaderParameters.ContainsKey("Authorization")) + { + localVarRequestOptions.HeaderParameters.Add("Authorization", "Bearer " + this.Configuration.AccessToken); + } + + // make the HTTP request + var localVarResponse = this.Client.Post("/jet/update/schedule", localVarRequestOptions, this.Configuration); + + if (this.ExceptionFactory != null) + { + Exception _exception = this.ExceptionFactory("SetUpdateSchedule", localVarResponse); + if (_exception != null) throw _exception; + } + + return localVarResponse; + } + + /// + /// Set the Devolutions Agent auto-update schedule. Writes the `Schedule` field into `update.json`. The agent watches this file and applies the new schedule immediately, then persists it to `agent.json`. All other fields in `update.json` are preserved; the `VersionMinor` field is reset to the minor version this gateway build understands so the agent does not see an unknown future version. + /// + /// Thrown when fails to make API call + /// + /// Cancellation Token to cancel the request. + /// Task of Object + public async System.Threading.Tasks.Task SetUpdateScheduleAsync(SetUpdateScheduleRequest setUpdateScheduleRequest, System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken)) + { + Devolutions.Gateway.Client.Client.ApiResponse localVarResponse = await SetUpdateScheduleWithHttpInfoAsync(setUpdateScheduleRequest, cancellationToken).ConfigureAwait(false); + return localVarResponse.Data; + } + + /// + /// Set the Devolutions Agent auto-update schedule. Writes the `Schedule` field into `update.json`. The agent watches this file and applies the new schedule immediately, then persists it to `agent.json`. All other fields in `update.json` are preserved; the `VersionMinor` field is reset to the minor version this gateway build understands so the agent does not see an unknown future version. + /// + /// Thrown when fails to make API call + /// + /// Cancellation Token to cancel the request. + /// Task of ApiResponse (Object) + public async System.Threading.Tasks.Task> SetUpdateScheduleWithHttpInfoAsync(SetUpdateScheduleRequest setUpdateScheduleRequest, System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken)) + { + // verify the required parameter 'setUpdateScheduleRequest' is set + if (setUpdateScheduleRequest == null) + throw new Devolutions.Gateway.Client.Client.ApiException(400, "Missing required parameter 'setUpdateScheduleRequest' when calling UpdateApi->SetUpdateSchedule"); + + + Devolutions.Gateway.Client.Client.RequestOptions localVarRequestOptions = new Devolutions.Gateway.Client.Client.RequestOptions(); + + string[] _contentTypes = new string[] { + "application/json" + }; + + // to determine the Accept header + string[] _accepts = new string[] { + "application/json" + }; + + + var localVarContentType = Devolutions.Gateway.Client.Client.ClientUtils.SelectHeaderContentType(_contentTypes); + if (localVarContentType != null) localVarRequestOptions.HeaderParameters.Add("Content-Type", localVarContentType); + + var localVarAccept = Devolutions.Gateway.Client.Client.ClientUtils.SelectHeaderAccept(_accepts); + if (localVarAccept != null) localVarRequestOptions.HeaderParameters.Add("Accept", localVarAccept); + + localVarRequestOptions.Data = setUpdateScheduleRequest; + + // authentication (scope_token) required + // bearer authentication required + if (!string.IsNullOrEmpty(this.Configuration.AccessToken) && !localVarRequestOptions.HeaderParameters.ContainsKey("Authorization")) + { + localVarRequestOptions.HeaderParameters.Add("Authorization", "Bearer " + this.Configuration.AccessToken); + } + + // make the HTTP request + + var localVarResponse = await this.AsynchronousClient.PostAsync("/jet/update/schedule", localVarRequestOptions, this.Configuration, cancellationToken).ConfigureAwait(false); + + if (this.ExceptionFactory != null) + { + Exception _exception = this.ExceptionFactory("SetUpdateSchedule", localVarResponse); + if (_exception != null) throw _exception; + } + + return localVarResponse; + } + + /// + /// Trigger an update for one or more Devolutions products. Writes the requested version(s) into `Agent/update.json`, which is watched by Devolutions Agent. When a requested version is higher than the installed version the agent proceeds with the update. **Body form** (preferred): pass a JSON body with a `Products` map. **Query-param form** (legacy, gateway-only): `POST /jet/update?version=latest`. This form updates only the Gateway product and is kept for backward compatibility. Both forms cannot be used simultaneously; doing so returns HTTP 400. + /// + /// Thrown when fails to make API call + /// Gateway-only target version; use the request body for multi-product updates (optional) (deprecated) + /// Products and target versions to update (optional) + /// Object + public Object TriggerUpdate(string? version = default(string?), UpdateRequestSchema? updateRequestSchema = default(UpdateRequestSchema?)) + { + Devolutions.Gateway.Client.Client.ApiResponse localVarResponse = TriggerUpdateWithHttpInfo(version, updateRequestSchema); + return localVarResponse.Data; + } + + /// + /// Trigger an update for one or more Devolutions products. Writes the requested version(s) into `Agent/update.json`, which is watched by Devolutions Agent. When a requested version is higher than the installed version the agent proceeds with the update. **Body form** (preferred): pass a JSON body with a `Products` map. **Query-param form** (legacy, gateway-only): `POST /jet/update?version=latest`. This form updates only the Gateway product and is kept for backward compatibility. Both forms cannot be used simultaneously; doing so returns HTTP 400. + /// + /// Thrown when fails to make API call + /// Gateway-only target version; use the request body for multi-product updates (optional) (deprecated) + /// Products and target versions to update (optional) + /// ApiResponse of Object + public Devolutions.Gateway.Client.Client.ApiResponse TriggerUpdateWithHttpInfo(string? version = default(string?), UpdateRequestSchema? updateRequestSchema = default(UpdateRequestSchema?)) + { + Devolutions.Gateway.Client.Client.RequestOptions localVarRequestOptions = new Devolutions.Gateway.Client.Client.RequestOptions(); + + string[] _contentTypes = new string[] { + "application/json" + }; + + // to determine the Accept header + string[] _accepts = new string[] { + "application/json" + }; + + var localVarContentType = Devolutions.Gateway.Client.Client.ClientUtils.SelectHeaderContentType(_contentTypes); + if (localVarContentType != null) localVarRequestOptions.HeaderParameters.Add("Content-Type", localVarContentType); + + var localVarAccept = Devolutions.Gateway.Client.Client.ClientUtils.SelectHeaderAccept(_accepts); + if (localVarAccept != null) localVarRequestOptions.HeaderParameters.Add("Accept", localVarAccept); + + if (version != null) + { + localVarRequestOptions.QueryParameters.Add(Devolutions.Gateway.Client.Client.ClientUtils.ParameterToMultiMap("", "version", version)); + } + localVarRequestOptions.Data = updateRequestSchema; // authentication (scope_token) required // bearer authentication required @@ -355,35 +842,34 @@ public Devolutions.Gateway.Client.Client.ApiResponse TriggerUpdateWithHt } /// - /// Triggers Devolutions Gateway update process. This is done via updating `Agent/update.json` file, which is then read by Devolutions Agent when changes are detected. If the version written to `update.json` is indeed higher than the currently installed version, Devolutions Agent will proceed with the update process. + /// Trigger an update for one or more Devolutions products. Writes the requested version(s) into `Agent/update.json`, which is watched by Devolutions Agent. When a requested version is higher than the installed version the agent proceeds with the update. **Body form** (preferred): pass a JSON body with a `Products` map. **Query-param form** (legacy, gateway-only): `POST /jet/update?version=latest`. This form updates only the Gateway product and is kept for backward compatibility. Both forms cannot be used simultaneously; doing so returns HTTP 400. /// /// Thrown when fails to make API call - /// The version to install; use 'latest' for the latest version, or 'w.x.y.z' for a specific version + /// Gateway-only target version; use the request body for multi-product updates (optional) (deprecated) + /// Products and target versions to update (optional) /// Cancellation Token to cancel the request. /// Task of Object - public async System.Threading.Tasks.Task TriggerUpdateAsync(string version, System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken)) + public async System.Threading.Tasks.Task TriggerUpdateAsync(string? version = default(string?), UpdateRequestSchema? updateRequestSchema = default(UpdateRequestSchema?), System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken)) { - Devolutions.Gateway.Client.Client.ApiResponse localVarResponse = await TriggerUpdateWithHttpInfoAsync(version, cancellationToken).ConfigureAwait(false); + Devolutions.Gateway.Client.Client.ApiResponse localVarResponse = await TriggerUpdateWithHttpInfoAsync(version, updateRequestSchema, cancellationToken).ConfigureAwait(false); return localVarResponse.Data; } /// - /// Triggers Devolutions Gateway update process. This is done via updating `Agent/update.json` file, which is then read by Devolutions Agent when changes are detected. If the version written to `update.json` is indeed higher than the currently installed version, Devolutions Agent will proceed with the update process. + /// Trigger an update for one or more Devolutions products. Writes the requested version(s) into `Agent/update.json`, which is watched by Devolutions Agent. When a requested version is higher than the installed version the agent proceeds with the update. **Body form** (preferred): pass a JSON body with a `Products` map. **Query-param form** (legacy, gateway-only): `POST /jet/update?version=latest`. This form updates only the Gateway product and is kept for backward compatibility. Both forms cannot be used simultaneously; doing so returns HTTP 400. /// /// Thrown when fails to make API call - /// The version to install; use 'latest' for the latest version, or 'w.x.y.z' for a specific version + /// Gateway-only target version; use the request body for multi-product updates (optional) (deprecated) + /// Products and target versions to update (optional) /// Cancellation Token to cancel the request. /// Task of ApiResponse (Object) - public async System.Threading.Tasks.Task> TriggerUpdateWithHttpInfoAsync(string version, System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken)) + public async System.Threading.Tasks.Task> TriggerUpdateWithHttpInfoAsync(string? version = default(string?), UpdateRequestSchema? updateRequestSchema = default(UpdateRequestSchema?), System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken)) { - // verify the required parameter 'version' is set - if (version == null) - throw new Devolutions.Gateway.Client.Client.ApiException(400, "Missing required parameter 'version' when calling UpdateApi->TriggerUpdate"); - Devolutions.Gateway.Client.Client.RequestOptions localVarRequestOptions = new Devolutions.Gateway.Client.Client.RequestOptions(); string[] _contentTypes = new string[] { + "application/json" }; // to determine the Accept header @@ -398,7 +884,11 @@ public Devolutions.Gateway.Client.Client.ApiResponse TriggerUpdateWithHt var localVarAccept = Devolutions.Gateway.Client.Client.ClientUtils.SelectHeaderAccept(_accepts); if (localVarAccept != null) localVarRequestOptions.HeaderParameters.Add("Accept", localVarAccept); - localVarRequestOptions.QueryParameters.Add(Devolutions.Gateway.Client.Client.ClientUtils.ParameterToMultiMap("", "version", version)); + if (version != null) + { + localVarRequestOptions.QueryParameters.Add(Devolutions.Gateway.Client.Client.ClientUtils.ParameterToMultiMap("", "version", version)); + } + localVarRequestOptions.Data = updateRequestSchema; // authentication (scope_token) required // bearer authentication required diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Api/WebAppApi.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Api/WebAppApi.cs index 6750931d2..ae2be8539 100644 --- a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Api/WebAppApi.cs +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Api/WebAppApi.cs @@ -3,7 +3,7 @@ * * Protocol-aware fine-grained relay server * - * The version of the OpenAPI document: 2025.3.2 + * The version of the OpenAPI document: 2026.2.4 * Contact: infos@devolutions.net * Generated by: https://github.com/openapitools/openapi-generator.git */ diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Client/ApiClient.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Client/ApiClient.cs index 4b285a4a3..4d77365cc 100644 --- a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Client/ApiClient.cs +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Client/ApiClient.cs @@ -3,7 +3,7 @@ * * Protocol-aware fine-grained relay server * - * The version of the OpenAPI document: 2025.3.2 + * The version of the OpenAPI document: 2026.2.4 * Contact: infos@devolutions.net * Generated by: https://github.com/openapitools/openapi-generator.git */ diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Client/ApiException.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Client/ApiException.cs index 4594b482a..19461fa46 100644 --- a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Client/ApiException.cs +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Client/ApiException.cs @@ -3,7 +3,7 @@ * * Protocol-aware fine-grained relay server * - * The version of the OpenAPI document: 2025.3.2 + * The version of the OpenAPI document: 2026.2.4 * Contact: infos@devolutions.net * Generated by: https://github.com/openapitools/openapi-generator.git */ diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Client/ApiResponse.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Client/ApiResponse.cs index b5b8e4753..955894c8f 100644 --- a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Client/ApiResponse.cs +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Client/ApiResponse.cs @@ -3,7 +3,7 @@ * * Protocol-aware fine-grained relay server * - * The version of the OpenAPI document: 2025.3.2 + * The version of the OpenAPI document: 2026.2.4 * Contact: infos@devolutions.net * Generated by: https://github.com/openapitools/openapi-generator.git */ diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Client/ClientUtils.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Client/ClientUtils.cs index 05b5ee056..89ef69ff9 100644 --- a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Client/ClientUtils.cs +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Client/ClientUtils.cs @@ -3,7 +3,7 @@ * * Protocol-aware fine-grained relay server * - * The version of the OpenAPI document: 2025.3.2 + * The version of the OpenAPI document: 2026.2.4 * Contact: infos@devolutions.net * Generated by: https://github.com/openapitools/openapi-generator.git */ diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Client/Configuration.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Client/Configuration.cs index 8a1e61c53..29990bcfa 100644 --- a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Client/Configuration.cs +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Client/Configuration.cs @@ -3,7 +3,7 @@ * * Protocol-aware fine-grained relay server * - * The version of the OpenAPI document: 2025.3.2 + * The version of the OpenAPI document: 2026.2.4 * Contact: infos@devolutions.net * Generated by: https://github.com/openapitools/openapi-generator.git */ @@ -540,7 +540,7 @@ public static string ToDebugReport() string report = "C# SDK (Devolutions.Gateway.Client) Debug Report:\n"; report += " OS: " + System.Environment.OSVersion + "\n"; report += " .NET Framework Version: " + System.Environment.Version + "\n"; - report += " Version of the API: 2025.3.2\n"; + report += " Version of the API: 2026.2.4\n"; report += " SDK Package Version: 2025.12.2\n"; return report; diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Client/ExceptionFactory.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Client/ExceptionFactory.cs index a1bf2ea63..1e2698d5a 100644 --- a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Client/ExceptionFactory.cs +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Client/ExceptionFactory.cs @@ -3,7 +3,7 @@ * * Protocol-aware fine-grained relay server * - * The version of the OpenAPI document: 2025.3.2 + * The version of the OpenAPI document: 2026.2.4 * Contact: infos@devolutions.net * Generated by: https://github.com/openapitools/openapi-generator.git */ diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Client/FileParameter.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Client/FileParameter.cs index 281f6945d..f557e590d 100644 --- a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Client/FileParameter.cs +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Client/FileParameter.cs @@ -3,7 +3,7 @@ * * Protocol-aware fine-grained relay server * - * The version of the OpenAPI document: 2025.3.2 + * The version of the OpenAPI document: 2026.2.4 * Contact: infos@devolutions.net * Generated by: https://github.com/openapitools/openapi-generator.git */ diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Client/GlobalConfiguration.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Client/GlobalConfiguration.cs index 2e90e183e..06ba08891 100644 --- a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Client/GlobalConfiguration.cs +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Client/GlobalConfiguration.cs @@ -3,7 +3,7 @@ * * Protocol-aware fine-grained relay server * - * The version of the OpenAPI document: 2025.3.2 + * The version of the OpenAPI document: 2026.2.4 * Contact: infos@devolutions.net * Generated by: https://github.com/openapitools/openapi-generator.git */ diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Client/IApiAccessor.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Client/IApiAccessor.cs index 55006b14e..de69db1b1 100644 --- a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Client/IApiAccessor.cs +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Client/IApiAccessor.cs @@ -3,7 +3,7 @@ * * Protocol-aware fine-grained relay server * - * The version of the OpenAPI document: 2025.3.2 + * The version of the OpenAPI document: 2026.2.4 * Contact: infos@devolutions.net * Generated by: https://github.com/openapitools/openapi-generator.git */ diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Client/IAsynchronousClient.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Client/IAsynchronousClient.cs index 09a6cf32c..81c81f40c 100644 --- a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Client/IAsynchronousClient.cs +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Client/IAsynchronousClient.cs @@ -3,7 +3,7 @@ * * Protocol-aware fine-grained relay server * - * The version of the OpenAPI document: 2025.3.2 + * The version of the OpenAPI document: 2026.2.4 * Contact: infos@devolutions.net * Generated by: https://github.com/openapitools/openapi-generator.git */ diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Client/IReadableConfiguration.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Client/IReadableConfiguration.cs index 8b917ec9f..5ad5279b4 100644 --- a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Client/IReadableConfiguration.cs +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Client/IReadableConfiguration.cs @@ -3,7 +3,7 @@ * * Protocol-aware fine-grained relay server * - * The version of the OpenAPI document: 2025.3.2 + * The version of the OpenAPI document: 2026.2.4 * Contact: infos@devolutions.net * Generated by: https://github.com/openapitools/openapi-generator.git */ diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Client/ISynchronousClient.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Client/ISynchronousClient.cs index 1b5d4eec2..24fa46df0 100644 --- a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Client/ISynchronousClient.cs +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Client/ISynchronousClient.cs @@ -3,7 +3,7 @@ * * Protocol-aware fine-grained relay server * - * The version of the OpenAPI document: 2025.3.2 + * The version of the OpenAPI document: 2026.2.4 * Contact: infos@devolutions.net * Generated by: https://github.com/openapitools/openapi-generator.git */ diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Client/Multimap.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Client/Multimap.cs index 75f5900c5..6c6c6dd47 100644 --- a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Client/Multimap.cs +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Client/Multimap.cs @@ -3,7 +3,7 @@ * * Protocol-aware fine-grained relay server * - * The version of the OpenAPI document: 2025.3.2 + * The version of the OpenAPI document: 2026.2.4 * Contact: infos@devolutions.net * Generated by: https://github.com/openapitools/openapi-generator.git */ diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Client/OpenAPIDateConverter.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Client/OpenAPIDateConverter.cs index 51d33afb5..a2c6d86ca 100644 --- a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Client/OpenAPIDateConverter.cs +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Client/OpenAPIDateConverter.cs @@ -3,7 +3,7 @@ * * Protocol-aware fine-grained relay server * - * The version of the OpenAPI document: 2025.3.2 + * The version of the OpenAPI document: 2026.2.4 * Contact: infos@devolutions.net * Generated by: https://github.com/openapitools/openapi-generator.git */ diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Client/RequestOptions.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Client/RequestOptions.cs index c3311adf2..3d250a880 100644 --- a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Client/RequestOptions.cs +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Client/RequestOptions.cs @@ -3,7 +3,7 @@ * * Protocol-aware fine-grained relay server * - * The version of the OpenAPI document: 2025.3.2 + * The version of the OpenAPI document: 2026.2.4 * Contact: infos@devolutions.net * Generated by: https://github.com/openapitools/openapi-generator.git */ diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Client/RetryConfiguration.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Client/RetryConfiguration.cs index ee5537544..5a7dad315 100644 --- a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Client/RetryConfiguration.cs +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Client/RetryConfiguration.cs @@ -3,7 +3,7 @@ * * Protocol-aware fine-grained relay server * - * The version of the OpenAPI document: 2025.3.2 + * The version of the OpenAPI document: 2026.2.4 * Contact: infos@devolutions.net * Generated by: https://github.com/openapitools/openapi-generator.git */ diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Client/WebRequestPathBuilder.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Client/WebRequestPathBuilder.cs index acd6b7337..0246ad865 100644 --- a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Client/WebRequestPathBuilder.cs +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Client/WebRequestPathBuilder.cs @@ -3,7 +3,7 @@ * * Protocol-aware fine-grained relay server * - * The version of the OpenAPI document: 2025.3.2 + * The version of the OpenAPI document: 2026.2.4 * Contact: infos@devolutions.net * Generated by: https://github.com/openapitools/openapi-generator.git */ diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/AbstractOpenAPISchema.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/AbstractOpenAPISchema.cs index cc66daaab..f704f6a04 100644 --- a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/AbstractOpenAPISchema.cs +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/AbstractOpenAPISchema.cs @@ -3,7 +3,7 @@ * * Protocol-aware fine-grained relay server * - * The version of the OpenAPI document: 2025.3.2 + * The version of the OpenAPI document: 2026.2.4 * Contact: infos@devolutions.net * Generated by: https://github.com/openapitools/openapi-generator.git */ diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/AccessScope.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/AccessScope.cs index 700fea365..98c7ac937 100644 --- a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/AccessScope.cs +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/AccessScope.cs @@ -3,7 +3,7 @@ * * Protocol-aware fine-grained relay server * - * The version of the OpenAPI document: 2025.3.2 + * The version of the OpenAPI document: 2026.2.4 * Contact: infos@devolutions.net * Generated by: https://github.com/openapitools/openapi-generator.git */ @@ -99,35 +99,53 @@ public enum AccessScope [EnumMember(Value = "gateway.update")] GatewayUpdate = 11, + /// + /// Enum GatewayUpdateRead for value: gateway.update.read + /// + [EnumMember(Value = "gateway.update.read")] + GatewayUpdateRead = 12, + /// /// Enum GatewayPreflight for value: gateway.preflight /// [EnumMember(Value = "gateway.preflight")] - GatewayPreflight = 12, + GatewayPreflight = 13, /// /// Enum GatewayTrafficClaim for value: gateway.traffic.claim /// [EnumMember(Value = "gateway.traffic.claim")] - GatewayTrafficClaim = 13, + GatewayTrafficClaim = 14, /// /// Enum GatewayTrafficAck for value: gateway.traffic.ack /// [EnumMember(Value = "gateway.traffic.ack")] - GatewayTrafficAck = 14, + GatewayTrafficAck = 15, /// /// Enum GatewayNetMonitorConfig for value: gateway.net.monitor.config /// [EnumMember(Value = "gateway.net.monitor.config")] - GatewayNetMonitorConfig = 15, + GatewayNetMonitorConfig = 16, /// /// Enum GatewayNetMonitorDrain for value: gateway.net.monitor.drain /// [EnumMember(Value = "gateway.net.monitor.drain")] - GatewayNetMonitorDrain = 16 + GatewayNetMonitorDrain = 17, + + /// + /// Enum GatewayAgentDelete for value: gateway.agent.delete + /// + [EnumMember(Value = "gateway.agent.delete")] + GatewayAgentDelete = 18, + + /// + /// Enum GatewayAgentRead for value: gateway.agent.read + /// + [EnumMember(Value = "gateway.agent.read")] + GatewayAgentRead = 19 } public static class AccessScopeExtensions @@ -161,6 +179,8 @@ public static string ToValue(this AccessScope variant) return "gateway.recordings.read"; case AccessScope.GatewayUpdate: return "gateway.update"; + case AccessScope.GatewayUpdateRead: + return "gateway.update.read"; case AccessScope.GatewayPreflight: return "gateway.preflight"; case AccessScope.GatewayTrafficClaim: @@ -171,6 +191,10 @@ public static string ToValue(this AccessScope variant) return "gateway.net.monitor.config"; case AccessScope.GatewayNetMonitorDrain: return "gateway.net.monitor.drain"; + case AccessScope.GatewayAgentDelete: + return "gateway.agent.delete"; + case AccessScope.GatewayAgentRead: + return "gateway.agent.read"; default: throw new ArgumentOutOfRangeException(nameof(variant), $"Unexpected variant: {variant}"); } diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/AckRequest.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/AckRequest.cs index 9aeb82538..453b72fea 100644 --- a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/AckRequest.cs +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/AckRequest.cs @@ -3,7 +3,7 @@ * * Protocol-aware fine-grained relay server * - * The version of the OpenAPI document: 2025.3.2 + * The version of the OpenAPI document: 2026.2.4 * Contact: infos@devolutions.net * Generated by: https://github.com/openapitools/openapi-generator.git */ diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/AckResponse.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/AckResponse.cs index c061175b4..217abd069 100644 --- a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/AckResponse.cs +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/AckResponse.cs @@ -3,7 +3,7 @@ * * Protocol-aware fine-grained relay server * - * The version of the OpenAPI document: 2025.3.2 + * The version of the OpenAPI document: 2026.2.4 * Contact: infos@devolutions.net * Generated by: https://github.com/openapitools/openapi-generator.git */ diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/AddressFamily.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/AddressFamily.cs index 3e40c5716..d288af632 100644 --- a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/AddressFamily.cs +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/AddressFamily.cs @@ -3,7 +3,7 @@ * * Protocol-aware fine-grained relay server * - * The version of the OpenAPI document: 2025.3.2 + * The version of the OpenAPI document: 2026.2.4 * Contact: infos@devolutions.net * Generated by: https://github.com/openapitools/openapi-generator.git */ diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/AppCredential.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/AppCredential.cs index 0def8adc9..37d90237b 100644 --- a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/AppCredential.cs +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/AppCredential.cs @@ -3,7 +3,7 @@ * * Protocol-aware fine-grained relay server * - * The version of the OpenAPI document: 2025.3.2 + * The version of the OpenAPI document: 2026.2.4 * Contact: infos@devolutions.net * Generated by: https://github.com/openapitools/openapi-generator.git */ diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/AppCredentialKind.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/AppCredentialKind.cs index 8d7c4dab6..c6bf83bbf 100644 --- a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/AppCredentialKind.cs +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/AppCredentialKind.cs @@ -3,7 +3,7 @@ * * Protocol-aware fine-grained relay server * - * The version of the OpenAPI document: 2025.3.2 + * The version of the OpenAPI document: 2026.2.4 * Contact: infos@devolutions.net * Generated by: https://github.com/openapitools/openapi-generator.git */ diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/AppTokenContentType.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/AppTokenContentType.cs index 24ac9b6ae..50fcc4962 100644 --- a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/AppTokenContentType.cs +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/AppTokenContentType.cs @@ -3,7 +3,7 @@ * * Protocol-aware fine-grained relay server * - * The version of the OpenAPI document: 2025.3.2 + * The version of the OpenAPI document: 2026.2.4 * Contact: infos@devolutions.net * Generated by: https://github.com/openapitools/openapi-generator.git */ diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/AppTokenSignRequest.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/AppTokenSignRequest.cs index 768c0e2cb..fb54ffa0d 100644 --- a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/AppTokenSignRequest.cs +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/AppTokenSignRequest.cs @@ -3,7 +3,7 @@ * * Protocol-aware fine-grained relay server * - * The version of the OpenAPI document: 2025.3.2 + * The version of the OpenAPI document: 2026.2.4 * Contact: infos@devolutions.net * Generated by: https://github.com/openapitools/openapi-generator.git */ diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/ClaimedTrafficEvent.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/ClaimedTrafficEvent.cs index b8572a9fd..a69d86bda 100644 --- a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/ClaimedTrafficEvent.cs +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/ClaimedTrafficEvent.cs @@ -3,7 +3,7 @@ * * Protocol-aware fine-grained relay server * - * The version of the OpenAPI document: 2025.3.2 + * The version of the OpenAPI document: 2026.2.4 * Contact: infos@devolutions.net * Generated by: https://github.com/openapitools/openapi-generator.git */ diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/ClockDiagnostic.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/ClockDiagnostic.cs index 0cbe1c572..119d37def 100644 --- a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/ClockDiagnostic.cs +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/ClockDiagnostic.cs @@ -3,7 +3,7 @@ * * Protocol-aware fine-grained relay server * - * The version of the OpenAPI document: 2025.3.2 + * The version of the OpenAPI document: 2026.2.4 * Contact: infos@devolutions.net * Generated by: https://github.com/openapitools/openapi-generator.git */ diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/ConfigDiagnostic.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/ConfigDiagnostic.cs index ffd26e8cb..b5c6c4a98 100644 --- a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/ConfigDiagnostic.cs +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/ConfigDiagnostic.cs @@ -3,7 +3,7 @@ * * Protocol-aware fine-grained relay server * - * The version of the OpenAPI document: 2025.3.2 + * The version of the OpenAPI document: 2026.2.4 * Contact: infos@devolutions.net * Generated by: https://github.com/openapitools/openapi-generator.git */ diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/ConfigPatch.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/ConfigPatch.cs index 78e0b848e..b23cdace1 100644 --- a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/ConfigPatch.cs +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/ConfigPatch.cs @@ -3,7 +3,7 @@ * * Protocol-aware fine-grained relay server * - * The version of the OpenAPI document: 2025.3.2 + * The version of the OpenAPI document: 2026.2.4 * Contact: infos@devolutions.net * Generated by: https://github.com/openapitools/openapi-generator.git */ diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/ConnectionMode.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/ConnectionMode.cs index 48afa473c..1b02f8383 100644 --- a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/ConnectionMode.cs +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/ConnectionMode.cs @@ -3,7 +3,7 @@ * * Protocol-aware fine-grained relay server * - * The version of the OpenAPI document: 2025.3.2 + * The version of the OpenAPI document: 2026.2.4 * Contact: infos@devolutions.net * Generated by: https://github.com/openapitools/openapi-generator.git */ diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/DataEncoding.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/DataEncoding.cs index fd94ffdc5..43551f3c4 100644 --- a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/DataEncoding.cs +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/DataEncoding.cs @@ -3,7 +3,7 @@ * * Protocol-aware fine-grained relay server * - * The version of the OpenAPI document: 2025.3.2 + * The version of the OpenAPI document: 2026.2.4 * Contact: infos@devolutions.net * Generated by: https://github.com/openapitools/openapi-generator.git */ diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/DeleteManyResult.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/DeleteManyResult.cs index b03a73230..d0c3420bc 100644 --- a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/DeleteManyResult.cs +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/DeleteManyResult.cs @@ -3,7 +3,7 @@ * * Protocol-aware fine-grained relay server * - * The version of the OpenAPI document: 2025.3.2 + * The version of the OpenAPI document: 2026.2.4 * Contact: infos@devolutions.net * Generated by: https://github.com/openapitools/openapi-generator.git */ diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/EventOutcomeResponse.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/EventOutcomeResponse.cs index b72752912..25dd7a7c9 100644 --- a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/EventOutcomeResponse.cs +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/EventOutcomeResponse.cs @@ -3,7 +3,7 @@ * * Protocol-aware fine-grained relay server * - * The version of the OpenAPI document: 2025.3.2 + * The version of the OpenAPI document: 2026.2.4 * Contact: infos@devolutions.net * Generated by: https://github.com/openapitools/openapi-generator.git */ diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/GetUpdateProductsResponse.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/GetUpdateProductsResponse.cs new file mode 100644 index 000000000..e45b5346a --- /dev/null +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/GetUpdateProductsResponse.cs @@ -0,0 +1,105 @@ +/* + * devolutions-gateway + * + * Protocol-aware fine-grained relay server + * + * The version of the OpenAPI document: 2026.2.4 + * Contact: infos@devolutions.net + * Generated by: https://github.com/openapitools/openapi-generator.git + */ + + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.IO; +using System.Runtime.Serialization; +using System.Text; +using System.Text.RegularExpressions; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Newtonsoft.Json.Linq; +using System.ComponentModel.DataAnnotations; +using FileParameter = Devolutions.Gateway.Client.Client.FileParameter; +using OpenAPIDateConverter = Devolutions.Gateway.Client.Client.OpenAPIDateConverter; + +namespace Devolutions.Gateway.Client.Model +{ + /// + /// Installed version of each product, as reported by Devolutions Agent. + /// + [DataContract(Name = "GetUpdateProductsResponse")] + public partial class GetUpdateProductsResponse : IValidatableObject + { + /// + /// Initializes a new instance of the class. + /// + [JsonConstructorAttribute] + protected GetUpdateProductsResponse() { } + /// + /// Initializes a new instance of the class. + /// + /// Version of the `update_status.json` format in `\"major.minor\"` form (e.g. `\"1.1\"`). (required). + /// Map of product name to API-specific product info.. + public GetUpdateProductsResponse(string manifestVersion = default(string), Dictionary products = default(Dictionary)) + { + // to ensure "manifestVersion" is required (not null) + if (manifestVersion == null) + { + throw new ArgumentNullException("manifestVersion is a required property for GetUpdateProductsResponse and cannot be null"); + } + this.ManifestVersion = manifestVersion; + this.Products = products; + } + + /// + /// Version of the `update_status.json` format in `\"major.minor\"` form (e.g. `\"1.1\"`). + /// + /// Version of the `update_status.json` format in `\"major.minor\"` form (e.g. `\"1.1\"`). + [DataMember(Name = "ManifestVersion", IsRequired = true, EmitDefaultValue = true)] + public string ManifestVersion { get; set; } + + /// + /// Map of product name to API-specific product info. + /// + /// Map of product name to API-specific product info. + [DataMember(Name = "Products", EmitDefaultValue = false)] + public Dictionary Products { get; set; } + + /// + /// Returns the string presentation of the object + /// + /// String presentation of the object + public override string ToString() + { + StringBuilder sb = new StringBuilder(); + sb.Append("class GetUpdateProductsResponse {\n"); + sb.Append(" ManifestVersion: ").Append(ManifestVersion).Append("\n"); + sb.Append(" Products: ").Append(Products).Append("\n"); + sb.Append("}\n"); + return sb.ToString(); + } + + /// + /// Returns the JSON string presentation of the object + /// + /// JSON string presentation of the object + public virtual string ToJson() + { + return Newtonsoft.Json.JsonConvert.SerializeObject(this, Newtonsoft.Json.Formatting.Indented); + } + + /// + /// To validate all properties of the instance + /// + /// Validation context + /// Validation Result + IEnumerable IValidatableObject.Validate(ValidationContext validationContext) + { + yield break; + } + } + +} diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/GetUpdateScheduleResponse.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/GetUpdateScheduleResponse.cs new file mode 100644 index 000000000..214610d2c --- /dev/null +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/GetUpdateScheduleResponse.cs @@ -0,0 +1,163 @@ +/* + * devolutions-gateway + * + * Protocol-aware fine-grained relay server + * + * The version of the OpenAPI document: 2026.2.4 + * Contact: infos@devolutions.net + * Generated by: https://github.com/openapitools/openapi-generator.git + */ + + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.IO; +using System.Runtime.Serialization; +using System.Text; +using System.Text.RegularExpressions; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Newtonsoft.Json.Linq; +using System.ComponentModel.DataAnnotations; +using FileParameter = Devolutions.Gateway.Client.Client.FileParameter; +using OpenAPIDateConverter = Devolutions.Gateway.Client.Client.OpenAPIDateConverter; + +namespace Devolutions.Gateway.Client.Model +{ + /// + /// Current auto-update schedule for Devolutions Agent. + /// + [DataContract(Name = "GetUpdateScheduleResponse")] + public partial class GetUpdateScheduleResponse : IValidatableObject + { + /// + /// Initializes a new instance of the class. + /// + [JsonConstructorAttribute] + protected GetUpdateScheduleResponse() { } + /// + /// Initializes a new instance of the class. + /// + /// Enable periodic Devolutions Agent self-update checks. (required). + /// Minimum interval between auto-update checks, in seconds. `0` means check once at `UpdateWindowStart`. (required). + /// Version of the `update_status.json` format in `\"major.minor\"` form (e.g. `\"1.1\"`). (required). + /// Products the agent autonomously polls for new versions.. + /// End of the maintenance window as seconds past midnight (local time, exclusive). `None` means no upper bound (single check at `UpdateWindowStart`).. + /// Start of the maintenance window as seconds past midnight (local time). (required). + public GetUpdateScheduleResponse(bool enabled = default(bool), long interval = default(long), string manifestVersion = default(string), List products = default(List), int? updateWindowEnd = default(int?), int updateWindowStart = default(int)) + { + this.Enabled = enabled; + this.Interval = interval; + // to ensure "manifestVersion" is required (not null) + if (manifestVersion == null) + { + throw new ArgumentNullException("manifestVersion is a required property for GetUpdateScheduleResponse and cannot be null"); + } + this.ManifestVersion = manifestVersion; + this.UpdateWindowStart = updateWindowStart; + this.Products = products; + this.UpdateWindowEnd = updateWindowEnd; + } + + /// + /// Enable periodic Devolutions Agent self-update checks. + /// + /// Enable periodic Devolutions Agent self-update checks. + [DataMember(Name = "Enabled", IsRequired = true, EmitDefaultValue = true)] + public bool Enabled { get; set; } + + /// + /// Minimum interval between auto-update checks, in seconds. `0` means check once at `UpdateWindowStart`. + /// + /// Minimum interval between auto-update checks, in seconds. `0` means check once at `UpdateWindowStart`. + [DataMember(Name = "Interval", IsRequired = true, EmitDefaultValue = true)] + public long Interval { get; set; } + + /// + /// Version of the `update_status.json` format in `\"major.minor\"` form (e.g. `\"1.1\"`). + /// + /// Version of the `update_status.json` format in `\"major.minor\"` form (e.g. `\"1.1\"`). + [DataMember(Name = "ManifestVersion", IsRequired = true, EmitDefaultValue = true)] + public string ManifestVersion { get; set; } + + /// + /// Products the agent autonomously polls for new versions. + /// + /// Products the agent autonomously polls for new versions. + [DataMember(Name = "Products", EmitDefaultValue = false)] + public List Products { get; set; } + + /// + /// End of the maintenance window as seconds past midnight (local time, exclusive). `None` means no upper bound (single check at `UpdateWindowStart`). + /// + /// End of the maintenance window as seconds past midnight (local time, exclusive). `None` means no upper bound (single check at `UpdateWindowStart`). + [DataMember(Name = "UpdateWindowEnd", EmitDefaultValue = true)] + public int? UpdateWindowEnd { get; set; } + + /// + /// Start of the maintenance window as seconds past midnight (local time). + /// + /// Start of the maintenance window as seconds past midnight (local time). + [DataMember(Name = "UpdateWindowStart", IsRequired = true, EmitDefaultValue = true)] + public int UpdateWindowStart { get; set; } + + /// + /// Returns the string presentation of the object + /// + /// String presentation of the object + public override string ToString() + { + StringBuilder sb = new StringBuilder(); + sb.Append("class GetUpdateScheduleResponse {\n"); + sb.Append(" Enabled: ").Append(Enabled).Append("\n"); + sb.Append(" Interval: ").Append(Interval).Append("\n"); + sb.Append(" ManifestVersion: ").Append(ManifestVersion).Append("\n"); + sb.Append(" Products: ").Append(Products).Append("\n"); + sb.Append(" UpdateWindowEnd: ").Append(UpdateWindowEnd).Append("\n"); + sb.Append(" UpdateWindowStart: ").Append(UpdateWindowStart).Append("\n"); + sb.Append("}\n"); + return sb.ToString(); + } + + /// + /// Returns the JSON string presentation of the object + /// + /// JSON string presentation of the object + public virtual string ToJson() + { + return Newtonsoft.Json.JsonConvert.SerializeObject(this, Newtonsoft.Json.Formatting.Indented); + } + + /// + /// To validate all properties of the instance + /// + /// Validation context + /// Validation Result + IEnumerable IValidatableObject.Validate(ValidationContext validationContext) + { + // Interval (long) minimum + if (this.Interval < (long)0) + { + yield return new ValidationResult("Invalid value for Interval, must be a value greater than or equal to 0.", new [] { "Interval" }); + } + + // UpdateWindowEnd (int?) minimum + if (this.UpdateWindowEnd < (int?)0) + { + yield return new ValidationResult("Invalid value for UpdateWindowEnd, must be a value greater than or equal to 0.", new [] { "UpdateWindowEnd" }); + } + + // UpdateWindowStart (int) minimum + if (this.UpdateWindowStart < (int)0) + { + yield return new ValidationResult("Invalid value for UpdateWindowStart, must be a value greater than or equal to 0.", new [] { "UpdateWindowStart" }); + } + + yield break; + } + } + +} diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/Heartbeat.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/Heartbeat.cs index 3cee0d048..70e8d5fce 100644 --- a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/Heartbeat.cs +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/Heartbeat.cs @@ -3,7 +3,7 @@ * * Protocol-aware fine-grained relay server * - * The version of the OpenAPI document: 2025.3.2 + * The version of the OpenAPI document: 2026.2.4 * Contact: infos@devolutions.net * Generated by: https://github.com/openapitools/openapi-generator.git */ diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/HostScanStateDto.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/HostScanStateDto.cs new file mode 100644 index 000000000..a8773544a --- /dev/null +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/HostScanStateDto.cs @@ -0,0 +1,84 @@ +/* + * devolutions-gateway + * + * Protocol-aware fine-grained relay server + * + * The version of the OpenAPI document: 2026.2.4 + * Contact: infos@devolutions.net + * Generated by: https://github.com/openapitools/openapi-generator.git + */ + + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.IO; +using System.Runtime.Serialization; +using System.Text; +using System.Text.RegularExpressions; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Newtonsoft.Json.Linq; +using System.ComponentModel.DataAnnotations; +using FileParameter = Devolutions.Gateway.Client.Client.FileParameter; +using OpenAPIDateConverter = Devolutions.Gateway.Client.Client.OpenAPIDateConverter; + +namespace Devolutions.Gateway.Client.Model +{ + /// + /// Defines HostScanStateDto + /// + [JsonConverter(typeof(StringEnumConverter))] + public enum HostScanStateDto + { + /// + /// Enum Queued for value: queued + /// + [EnumMember(Value = "queued")] + Queued = 1, + + /// + /// Enum Probing for value: probing + /// + [EnumMember(Value = "probing")] + Probing = 2, + + /// + /// Enum Reachable for value: reachable + /// + [EnumMember(Value = "reachable")] + Reachable = 3, + + /// + /// Enum Unreachable for value: unreachable + /// + [EnumMember(Value = "unreachable")] + Unreachable = 4 + } + + public static class HostScanStateDtoExtensions + { + /// + /// Returns the value as string for a given variant + /// + public static string ToValue(this HostScanStateDto variant) + { + switch (variant) + { + case HostScanStateDto.Queued: + return "queued"; + case HostScanStateDto.Probing: + return "probing"; + case HostScanStateDto.Reachable: + return "reachable"; + case HostScanStateDto.Unreachable: + return "unreachable"; + default: + throw new ArgumentOutOfRangeException(nameof(variant), $"Unexpected variant: {variant}"); + } + } + } + +} diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/Identity.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/Identity.cs index 69740ca33..f27bb51dc 100644 --- a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/Identity.cs +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/Identity.cs @@ -3,7 +3,7 @@ * * Protocol-aware fine-grained relay server * - * The version of the OpenAPI document: 2025.3.2 + * The version of the OpenAPI document: 2026.2.4 * Contact: infos@devolutions.net * Generated by: https://github.com/openapitools/openapi-generator.git */ diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/InterfaceInfo.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/InterfaceInfo.cs index c24d33afb..9acb50e90 100644 --- a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/InterfaceInfo.cs +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/InterfaceInfo.cs @@ -3,7 +3,7 @@ * * Protocol-aware fine-grained relay server * - * The version of the OpenAPI document: 2025.3.2 + * The version of the OpenAPI document: 2026.2.4 * Contact: infos@devolutions.net * Generated by: https://github.com/openapitools/openapi-generator.git */ diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/JrlInfo.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/JrlInfo.cs index a4acf996f..44ae29e6b 100644 --- a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/JrlInfo.cs +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/JrlInfo.cs @@ -3,7 +3,7 @@ * * Protocol-aware fine-grained relay server * - * The version of the OpenAPI document: 2025.3.2 + * The version of the OpenAPI document: 2026.2.4 * Contact: infos@devolutions.net * Generated by: https://github.com/openapitools/openapi-generator.git */ diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/ListenerUrls.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/ListenerUrls.cs index 1823d9300..ccd051a68 100644 --- a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/ListenerUrls.cs +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/ListenerUrls.cs @@ -3,7 +3,7 @@ * * Protocol-aware fine-grained relay server * - * The version of the OpenAPI document: 2025.3.2 + * The version of the OpenAPI document: 2026.2.4 * Contact: infos@devolutions.net * Generated by: https://github.com/openapitools/openapi-generator.git */ diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/MonitorDefinition.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/MonitorDefinition.cs index c763d1bf4..1c5ced8de 100644 --- a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/MonitorDefinition.cs +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/MonitorDefinition.cs @@ -3,7 +3,7 @@ * * Protocol-aware fine-grained relay server * - * The version of the OpenAPI document: 2025.3.2 + * The version of the OpenAPI document: 2026.2.4 * Contact: infos@devolutions.net * Generated by: https://github.com/openapitools/openapi-generator.git */ diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/MonitorDefinitionProbeTypeError.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/MonitorDefinitionProbeTypeError.cs index f11b7c806..e6319f7fb 100644 --- a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/MonitorDefinitionProbeTypeError.cs +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/MonitorDefinitionProbeTypeError.cs @@ -3,7 +3,7 @@ * * Protocol-aware fine-grained relay server * - * The version of the OpenAPI document: 2025.3.2 + * The version of the OpenAPI document: 2026.2.4 * Contact: infos@devolutions.net * Generated by: https://github.com/openapitools/openapi-generator.git */ diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/MonitorResult.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/MonitorResult.cs index ad308e4af..3d326db43 100644 --- a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/MonitorResult.cs +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/MonitorResult.cs @@ -3,7 +3,7 @@ * * Protocol-aware fine-grained relay server * - * The version of the OpenAPI document: 2025.3.2 + * The version of the OpenAPI document: 2026.2.4 * Contact: infos@devolutions.net * Generated by: https://github.com/openapitools/openapi-generator.git */ diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/MonitoringLogResponse.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/MonitoringLogResponse.cs index 6a4fd51f9..5178361b7 100644 --- a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/MonitoringLogResponse.cs +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/MonitoringLogResponse.cs @@ -3,7 +3,7 @@ * * Protocol-aware fine-grained relay server * - * The version of the OpenAPI document: 2025.3.2 + * The version of the OpenAPI document: 2026.2.4 * Contact: infos@devolutions.net * Generated by: https://github.com/openapitools/openapi-generator.git */ diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/MonitoringProbeType.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/MonitoringProbeType.cs index eea997e69..d5940114c 100644 --- a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/MonitoringProbeType.cs +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/MonitoringProbeType.cs @@ -3,7 +3,7 @@ * * Protocol-aware fine-grained relay server * - * The version of the OpenAPI document: 2025.3.2 + * The version of the OpenAPI document: 2026.2.4 * Contact: infos@devolutions.net * Generated by: https://github.com/openapitools/openapi-generator.git */ diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/MonitoringProbeTypeOneOf.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/MonitoringProbeTypeOneOf.cs index ec1b3110c..c54f19afd 100644 --- a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/MonitoringProbeTypeOneOf.cs +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/MonitoringProbeTypeOneOf.cs @@ -3,7 +3,7 @@ * * Protocol-aware fine-grained relay server * - * The version of the OpenAPI document: 2025.3.2 + * The version of the OpenAPI document: 2026.2.4 * Contact: infos@devolutions.net * Generated by: https://github.com/openapitools/openapi-generator.git */ diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/MonitorsConfig.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/MonitorsConfig.cs index 11d43526b..e2023b36e 100644 --- a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/MonitorsConfig.cs +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/MonitorsConfig.cs @@ -3,7 +3,7 @@ * * Protocol-aware fine-grained relay server * - * The version of the OpenAPI document: 2025.3.2 + * The version of the OpenAPI document: 2026.2.4 * Contact: infos@devolutions.net * Generated by: https://github.com/openapitools/openapi-generator.git */ diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/NetworkInterfaceDto.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/NetworkInterfaceDto.cs new file mode 100644 index 000000000..82dd1c5ee --- /dev/null +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/NetworkInterfaceDto.cs @@ -0,0 +1,192 @@ +/* + * devolutions-gateway + * + * Protocol-aware fine-grained relay server + * + * The version of the OpenAPI document: 2026.2.4 + * Contact: infos@devolutions.net + * Generated by: https://github.com/openapitools/openapi-generator.git + */ + + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.IO; +using System.Runtime.Serialization; +using System.Text; +using System.Text.RegularExpressions; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Newtonsoft.Json.Linq; +using System.ComponentModel.DataAnnotations; +using FileParameter = Devolutions.Gateway.Client.Client.FileParameter; +using OpenAPIDateConverter = Devolutions.Gateway.Client.Client.OpenAPIDateConverter; + +namespace Devolutions.Gateway.Client.Model +{ + /// + /// NetworkInterfaceDto + /// + [DataContract(Name = "NetworkInterfaceDto")] + public partial class NetworkInterfaceDto : IValidatableObject + { + /// + /// Initializes a new instance of the class. + /// + [JsonConstructorAttribute] + protected NetworkInterfaceDto() { } + /// + /// Initializes a new instance of the class. + /// + /// description. + /// id (required). + /// index. + /// isUp. + /// Coarse link type: `ethernet`, `wifi`, `loopback`, `virtual`, `unknown`.. + /// macAddress. + /// MTU in bytes when known.. + /// name (required). + /// Link speed in megabits per second when reported by the OS.. + public NetworkInterfaceDto(string description = default(string), string id = default(string), int? index = default(int?), bool? isUp = default(bool?), string linkType = default(string), string macAddress = default(string), int? mtu = default(int?), string name = default(string), long? speedMbps = default(long?)) + { + // to ensure "id" is required (not null) + if (id == null) + { + throw new ArgumentNullException("id is a required property for NetworkInterfaceDto and cannot be null"); + } + this.Id = id; + // to ensure "name" is required (not null) + if (name == null) + { + throw new ArgumentNullException("name is a required property for NetworkInterfaceDto and cannot be null"); + } + this.Name = name; + this.Description = description; + this.Index = index; + this.IsUp = isUp; + this.LinkType = linkType; + this.MacAddress = macAddress; + this.Mtu = mtu; + this.SpeedMbps = speedMbps; + } + + /// + /// Gets or Sets Description + /// + [DataMember(Name = "description", EmitDefaultValue = true)] + public string Description { get; set; } + + /// + /// Gets or Sets Id + /// + [DataMember(Name = "id", IsRequired = true, EmitDefaultValue = true)] + public string Id { get; set; } + + /// + /// Gets or Sets Index + /// + [DataMember(Name = "index", EmitDefaultValue = true)] + public int? Index { get; set; } + + /// + /// Gets or Sets IsUp + /// + [DataMember(Name = "isUp", EmitDefaultValue = true)] + public bool? IsUp { get; set; } + + /// + /// Coarse link type: `ethernet`, `wifi`, `loopback`, `virtual`, `unknown`. + /// + /// Coarse link type: `ethernet`, `wifi`, `loopback`, `virtual`, `unknown`. + [DataMember(Name = "linkType", EmitDefaultValue = true)] + public string LinkType { get; set; } + + /// + /// Gets or Sets MacAddress + /// + [DataMember(Name = "macAddress", EmitDefaultValue = true)] + public string MacAddress { get; set; } + + /// + /// MTU in bytes when known. + /// + /// MTU in bytes when known. + [DataMember(Name = "mtu", EmitDefaultValue = true)] + public int? Mtu { get; set; } + + /// + /// Gets or Sets Name + /// + [DataMember(Name = "name", IsRequired = true, EmitDefaultValue = true)] + public string Name { get; set; } + + /// + /// Link speed in megabits per second when reported by the OS. + /// + /// Link speed in megabits per second when reported by the OS. + [DataMember(Name = "speedMbps", EmitDefaultValue = true)] + public long? SpeedMbps { get; set; } + + /// + /// Returns the string presentation of the object + /// + /// String presentation of the object + public override string ToString() + { + StringBuilder sb = new StringBuilder(); + sb.Append("class NetworkInterfaceDto {\n"); + sb.Append(" Description: ").Append(Description).Append("\n"); + sb.Append(" Id: ").Append(Id).Append("\n"); + sb.Append(" Index: ").Append(Index).Append("\n"); + sb.Append(" IsUp: ").Append(IsUp).Append("\n"); + sb.Append(" LinkType: ").Append(LinkType).Append("\n"); + sb.Append(" MacAddress: ").Append(MacAddress).Append("\n"); + sb.Append(" Mtu: ").Append(Mtu).Append("\n"); + sb.Append(" Name: ").Append(Name).Append("\n"); + sb.Append(" SpeedMbps: ").Append(SpeedMbps).Append("\n"); + sb.Append("}\n"); + return sb.ToString(); + } + + /// + /// Returns the JSON string presentation of the object + /// + /// JSON string presentation of the object + public virtual string ToJson() + { + return Newtonsoft.Json.JsonConvert.SerializeObject(this, Newtonsoft.Json.Formatting.Indented); + } + + /// + /// To validate all properties of the instance + /// + /// Validation context + /// Validation Result + IEnumerable IValidatableObject.Validate(ValidationContext validationContext) + { + // Index (int?) minimum + if (this.Index < (int?)0) + { + yield return new ValidationResult("Invalid value for Index, must be a value greater than or equal to 0.", new [] { "Index" }); + } + + // Mtu (int?) minimum + if (this.Mtu < (int?)0) + { + yield return new ValidationResult("Invalid value for Mtu, must be a value greater than or equal to 0.", new [] { "Mtu" }); + } + + // SpeedMbps (long?) minimum + if (this.SpeedMbps < (long?)0) + { + yield return new ValidationResult("Invalid value for SpeedMbps, must be a value greater than or equal to 0.", new [] { "SpeedMbps" }); + } + + yield break; + } + } + +} diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/NetworkInterfacesResponse.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/NetworkInterfacesResponse.cs new file mode 100644 index 000000000..35cdcf5e7 --- /dev/null +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/NetworkInterfacesResponse.cs @@ -0,0 +1,94 @@ +/* + * devolutions-gateway + * + * Protocol-aware fine-grained relay server + * + * The version of the OpenAPI document: 2026.2.4 + * Contact: infos@devolutions.net + * Generated by: https://github.com/openapitools/openapi-generator.git + */ + + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.IO; +using System.Runtime.Serialization; +using System.Text; +using System.Text.RegularExpressions; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Newtonsoft.Json.Linq; +using System.ComponentModel.DataAnnotations; +using FileParameter = Devolutions.Gateway.Client.Client.FileParameter; +using OpenAPIDateConverter = Devolutions.Gateway.Client.Client.OpenAPIDateConverter; + +namespace Devolutions.Gateway.Client.Model +{ + /// + /// NetworkInterfacesResponse + /// + [DataContract(Name = "NetworkInterfacesResponse")] + public partial class NetworkInterfacesResponse : IValidatableObject + { + /// + /// Initializes a new instance of the class. + /// + [JsonConstructorAttribute] + protected NetworkInterfacesResponse() { } + /// + /// Initializes a new instance of the class. + /// + /// interfaces (required). + public NetworkInterfacesResponse(List interfaces = default(List)) + { + // to ensure "interfaces" is required (not null) + if (interfaces == null) + { + throw new ArgumentNullException("interfaces is a required property for NetworkInterfacesResponse and cannot be null"); + } + this.Interfaces = interfaces; + } + + /// + /// Gets or Sets Interfaces + /// + [DataMember(Name = "interfaces", IsRequired = true, EmitDefaultValue = true)] + public List Interfaces { get; set; } + + /// + /// Returns the string presentation of the object + /// + /// String presentation of the object + public override string ToString() + { + StringBuilder sb = new StringBuilder(); + sb.Append("class NetworkInterfacesResponse {\n"); + sb.Append(" Interfaces: ").Append(Interfaces).Append("\n"); + sb.Append("}\n"); + return sb.ToString(); + } + + /// + /// Returns the JSON string presentation of the object + /// + /// JSON string presentation of the object + public virtual string ToJson() + { + return Newtonsoft.Json.JsonConvert.SerializeObject(this, Newtonsoft.Json.Formatting.Indented); + } + + /// + /// To validate all properties of the instance + /// + /// Validation context + /// Validation Result + IEnumerable IValidatableObject.Validate(ValidationContext validationContext) + { + yield break; + } + } + +} diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/NetworkScanResultEventDto.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/NetworkScanResultEventDto.cs new file mode 100644 index 000000000..b55c29445 --- /dev/null +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/NetworkScanResultEventDto.cs @@ -0,0 +1,223 @@ +/* + * devolutions-gateway + * + * Protocol-aware fine-grained relay server + * + * The version of the OpenAPI document: 2026.2.4 + * Contact: infos@devolutions.net + * Generated by: https://github.com/openapitools/openapi-generator.git + */ + + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.IO; +using System.Runtime.Serialization; +using System.Text; +using System.Text.RegularExpressions; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Newtonsoft.Json.Linq; +using System.ComponentModel.DataAnnotations; +using FileParameter = Devolutions.Gateway.Client.Client.FileParameter; +using OpenAPIDateConverter = Devolutions.Gateway.Client.Client.OpenAPIDateConverter; + +namespace Devolutions.Gateway.Client.Model +{ + /// + /// NetworkScanResultEventDto + /// + [DataContract(Name = "NetworkScanResultEventDto")] + public partial class NetworkScanResultEventDto : IValidatableObject + { + + /// + /// Gets or Sets DiscoverySource + /// + [DataMember(Name = "discoverySource", IsRequired = true, EmitDefaultValue = true)] + public ScanResultSourceDto DiscoverySource { get; set; } + + /// + /// Gets or Sets HostScanState + /// + [DataMember(Name = "hostScanState", EmitDefaultValue = true)] + public HostScanStateDto? HostScanState { get; set; } + + /// + /// Gets or Sets Kind + /// + [DataMember(Name = "kind", IsRequired = true, EmitDefaultValue = true)] + public NetworkScanResultKindDto Kind { get; set; } + + /// + /// Gets or Sets Source + /// + [DataMember(Name = "source", IsRequired = true, EmitDefaultValue = true)] + public ScanOriginDto Source { get; set; } + /// + /// Initializes a new instance of the class. + /// + [JsonConstructorAttribute] + protected NetworkScanResultEventDto() { } + /// + /// Initializes a new instance of the class. + /// + /// address (required). + /// discoverySource (required). + /// hostName. + /// hostScanState. + /// interfaceId. + /// interfaceName. + /// isReachable. + /// kind (required). + /// macAddress. + /// port. + /// responseTimeMs. + /// serviceLabel. + /// serviceType. + /// source (required). + public NetworkScanResultEventDto(string address = default(string), ScanResultSourceDto discoverySource = default(ScanResultSourceDto), string hostName = default(string), HostScanStateDto? hostScanState = default(HostScanStateDto?), string interfaceId = default(string), string interfaceName = default(string), bool? isReachable = default(bool?), NetworkScanResultKindDto kind = default(NetworkScanResultKindDto), string macAddress = default(string), int? port = default(int?), int? responseTimeMs = default(int?), string serviceLabel = default(string), string serviceType = default(string), ScanOriginDto source = default(ScanOriginDto)) + { + // to ensure "address" is required (not null) + if (address == null) + { + throw new ArgumentNullException("address is a required property for NetworkScanResultEventDto and cannot be null"); + } + this.Address = address; + this.DiscoverySource = discoverySource; + this.Kind = kind; + this.Source = source; + this.HostName = hostName; + this.HostScanState = hostScanState; + this.InterfaceId = interfaceId; + this.InterfaceName = interfaceName; + this.IsReachable = isReachable; + this.MacAddress = macAddress; + this.Port = port; + this.ResponseTimeMs = responseTimeMs; + this.ServiceLabel = serviceLabel; + this.ServiceType = serviceType; + } + + /// + /// Gets or Sets Address + /// + [DataMember(Name = "address", IsRequired = true, EmitDefaultValue = true)] + public string Address { get; set; } + + /// + /// Gets or Sets HostName + /// + [DataMember(Name = "hostName", EmitDefaultValue = true)] + public string HostName { get; set; } + + /// + /// Gets or Sets InterfaceId + /// + [DataMember(Name = "interfaceId", EmitDefaultValue = true)] + public string InterfaceId { get; set; } + + /// + /// Gets or Sets InterfaceName + /// + [DataMember(Name = "interfaceName", EmitDefaultValue = true)] + public string InterfaceName { get; set; } + + /// + /// Gets or Sets IsReachable + /// + [DataMember(Name = "isReachable", EmitDefaultValue = true)] + public bool? IsReachable { get; set; } + + /// + /// Gets or Sets MacAddress + /// + [DataMember(Name = "macAddress", EmitDefaultValue = true)] + public string MacAddress { get; set; } + + /// + /// Gets or Sets Port + /// + [DataMember(Name = "port", EmitDefaultValue = true)] + public int? Port { get; set; } + + /// + /// Gets or Sets ResponseTimeMs + /// + [DataMember(Name = "responseTimeMs", EmitDefaultValue = true)] + public int? ResponseTimeMs { get; set; } + + /// + /// Gets or Sets ServiceLabel + /// + [DataMember(Name = "serviceLabel", EmitDefaultValue = true)] + public string ServiceLabel { get; set; } + + /// + /// Gets or Sets ServiceType + /// + [DataMember(Name = "serviceType", EmitDefaultValue = true)] + public string ServiceType { get; set; } + + /// + /// Returns the string presentation of the object + /// + /// String presentation of the object + public override string ToString() + { + StringBuilder sb = new StringBuilder(); + sb.Append("class NetworkScanResultEventDto {\n"); + sb.Append(" Address: ").Append(Address).Append("\n"); + sb.Append(" DiscoverySource: ").Append(DiscoverySource).Append("\n"); + sb.Append(" HostName: ").Append(HostName).Append("\n"); + sb.Append(" HostScanState: ").Append(HostScanState).Append("\n"); + sb.Append(" InterfaceId: ").Append(InterfaceId).Append("\n"); + sb.Append(" InterfaceName: ").Append(InterfaceName).Append("\n"); + sb.Append(" IsReachable: ").Append(IsReachable).Append("\n"); + sb.Append(" Kind: ").Append(Kind).Append("\n"); + sb.Append(" MacAddress: ").Append(MacAddress).Append("\n"); + sb.Append(" Port: ").Append(Port).Append("\n"); + sb.Append(" ResponseTimeMs: ").Append(ResponseTimeMs).Append("\n"); + sb.Append(" ServiceLabel: ").Append(ServiceLabel).Append("\n"); + sb.Append(" ServiceType: ").Append(ServiceType).Append("\n"); + sb.Append(" Source: ").Append(Source).Append("\n"); + sb.Append("}\n"); + return sb.ToString(); + } + + /// + /// Returns the JSON string presentation of the object + /// + /// JSON string presentation of the object + public virtual string ToJson() + { + return Newtonsoft.Json.JsonConvert.SerializeObject(this, Newtonsoft.Json.Formatting.Indented); + } + + /// + /// To validate all properties of the instance + /// + /// Validation context + /// Validation Result + IEnumerable IValidatableObject.Validate(ValidationContext validationContext) + { + // Port (int?) minimum + if (this.Port < (int?)0) + { + yield return new ValidationResult("Invalid value for Port, must be a value greater than or equal to 0.", new [] { "Port" }); + } + + // ResponseTimeMs (int?) minimum + if (this.ResponseTimeMs < (int?)0) + { + yield return new ValidationResult("Invalid value for ResponseTimeMs, must be a value greater than or equal to 0.", new [] { "ResponseTimeMs" }); + } + + yield break; + } + } + +} diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/NetworkScanResultKindDto.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/NetworkScanResultKindDto.cs new file mode 100644 index 000000000..a06d6f44a --- /dev/null +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/NetworkScanResultKindDto.cs @@ -0,0 +1,68 @@ +/* + * devolutions-gateway + * + * Protocol-aware fine-grained relay server + * + * The version of the OpenAPI document: 2026.2.4 + * Contact: infos@devolutions.net + * Generated by: https://github.com/openapitools/openapi-generator.git + */ + + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.IO; +using System.Runtime.Serialization; +using System.Text; +using System.Text.RegularExpressions; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Newtonsoft.Json.Linq; +using System.ComponentModel.DataAnnotations; +using FileParameter = Devolutions.Gateway.Client.Client.FileParameter; +using OpenAPIDateConverter = Devolutions.Gateway.Client.Client.OpenAPIDateConverter; + +namespace Devolutions.Gateway.Client.Model +{ + /// + /// Defines NetworkScanResultKindDto + /// + [JsonConverter(typeof(StringEnumConverter))] + public enum NetworkScanResultKindDto + { + /// + /// Enum Host for value: host + /// + [EnumMember(Value = "host")] + Host = 1, + + /// + /// Enum Service for value: service + /// + [EnumMember(Value = "service")] + Service = 2 + } + + public static class NetworkScanResultKindDtoExtensions + { + /// + /// Returns the value as string for a given variant + /// + public static string ToValue(this NetworkScanResultKindDto variant) + { + switch (variant) + { + case NetworkScanResultKindDto.Host: + return "host"; + case NetworkScanResultKindDto.Service: + return "service"; + default: + throw new ArgumentOutOfRangeException(nameof(variant), $"Unexpected variant: {variant}"); + } + } + } + +} diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/NetworkScanSourceCapabilitiesDto.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/NetworkScanSourceCapabilitiesDto.cs new file mode 100644 index 000000000..5a67d2b4f --- /dev/null +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/NetworkScanSourceCapabilitiesDto.cs @@ -0,0 +1,143 @@ +/* + * devolutions-gateway + * + * Protocol-aware fine-grained relay server + * + * The version of the OpenAPI document: 2026.2.4 + * Contact: infos@devolutions.net + * Generated by: https://github.com/openapitools/openapi-generator.git + */ + + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.IO; +using System.Runtime.Serialization; +using System.Text; +using System.Text.RegularExpressions; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Newtonsoft.Json.Linq; +using System.ComponentModel.DataAnnotations; +using FileParameter = Devolutions.Gateway.Client.Client.FileParameter; +using OpenAPIDateConverter = Devolutions.Gateway.Client.Client.OpenAPIDateConverter; + +namespace Devolutions.Gateway.Client.Model +{ + /// + /// NetworkScanSourceCapabilitiesDto + /// + [DataContract(Name = "NetworkScanSourceCapabilitiesDto")] + public partial class NetworkScanSourceCapabilitiesDto : IValidatableObject + { + /// + /// Initializes a new instance of the class. + /// + [JsonConstructorAttribute] + protected NetworkScanSourceCapabilitiesDto() { } + /// + /// Initializes a new instance of the class. + /// + /// broadcast (required). + /// dnsResolve (required). + /// ipv4 (required). + /// ipv6 (required). + /// subnet (required). + /// tcpProbe (required). + /// zeroConf (required). + public NetworkScanSourceCapabilitiesDto(bool broadcast = default(bool), bool dnsResolve = default(bool), bool ipv4 = default(bool), bool ipv6 = default(bool), bool subnet = default(bool), bool tcpProbe = default(bool), bool zeroConf = default(bool)) + { + this.Broadcast = broadcast; + this.DnsResolve = dnsResolve; + this.Ipv4 = ipv4; + this.Ipv6 = ipv6; + this.Subnet = subnet; + this.TcpProbe = tcpProbe; + this.ZeroConf = zeroConf; + } + + /// + /// Gets or Sets Broadcast + /// + [DataMember(Name = "broadcast", IsRequired = true, EmitDefaultValue = true)] + public bool Broadcast { get; set; } + + /// + /// Gets or Sets DnsResolve + /// + [DataMember(Name = "dnsResolve", IsRequired = true, EmitDefaultValue = true)] + public bool DnsResolve { get; set; } + + /// + /// Gets or Sets Ipv4 + /// + [DataMember(Name = "ipv4", IsRequired = true, EmitDefaultValue = true)] + public bool Ipv4 { get; set; } + + /// + /// Gets or Sets Ipv6 + /// + [DataMember(Name = "ipv6", IsRequired = true, EmitDefaultValue = true)] + public bool Ipv6 { get; set; } + + /// + /// Gets or Sets Subnet + /// + [DataMember(Name = "subnet", IsRequired = true, EmitDefaultValue = true)] + public bool Subnet { get; set; } + + /// + /// Gets or Sets TcpProbe + /// + [DataMember(Name = "tcpProbe", IsRequired = true, EmitDefaultValue = true)] + public bool TcpProbe { get; set; } + + /// + /// Gets or Sets ZeroConf + /// + [DataMember(Name = "zeroConf", IsRequired = true, EmitDefaultValue = true)] + public bool ZeroConf { get; set; } + + /// + /// Returns the string presentation of the object + /// + /// String presentation of the object + public override string ToString() + { + StringBuilder sb = new StringBuilder(); + sb.Append("class NetworkScanSourceCapabilitiesDto {\n"); + sb.Append(" Broadcast: ").Append(Broadcast).Append("\n"); + sb.Append(" DnsResolve: ").Append(DnsResolve).Append("\n"); + sb.Append(" Ipv4: ").Append(Ipv4).Append("\n"); + sb.Append(" Ipv6: ").Append(Ipv6).Append("\n"); + sb.Append(" Subnet: ").Append(Subnet).Append("\n"); + sb.Append(" TcpProbe: ").Append(TcpProbe).Append("\n"); + sb.Append(" ZeroConf: ").Append(ZeroConf).Append("\n"); + sb.Append("}\n"); + return sb.ToString(); + } + + /// + /// Returns the JSON string presentation of the object + /// + /// JSON string presentation of the object + public virtual string ToJson() + { + return Newtonsoft.Json.JsonConvert.SerializeObject(this, Newtonsoft.Json.Formatting.Indented); + } + + /// + /// To validate all properties of the instance + /// + /// Validation context + /// Validation Result + IEnumerable IValidatableObject.Validate(ValidationContext validationContext) + { + yield break; + } + } + +} diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/NetworkScanSourceDto.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/NetworkScanSourceDto.cs new file mode 100644 index 000000000..5c10a9c6d --- /dev/null +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/NetworkScanSourceDto.cs @@ -0,0 +1,174 @@ +/* + * devolutions-gateway + * + * Protocol-aware fine-grained relay server + * + * The version of the OpenAPI document: 2026.2.4 + * Contact: infos@devolutions.net + * Generated by: https://github.com/openapitools/openapi-generator.git + */ + + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.IO; +using System.Runtime.Serialization; +using System.Text; +using System.Text.RegularExpressions; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Newtonsoft.Json.Linq; +using System.ComponentModel.DataAnnotations; +using FileParameter = Devolutions.Gateway.Client.Client.FileParameter; +using OpenAPIDateConverter = Devolutions.Gateway.Client.Client.OpenAPIDateConverter; + +namespace Devolutions.Gateway.Client.Model +{ + /// + /// NetworkScanSourceDto + /// + [DataContract(Name = "NetworkScanSourceDto")] + public partial class NetworkScanSourceDto : IValidatableObject + { + /// + /// Initializes a new instance of the class. + /// + [JsonConstructorAttribute] + protected NetworkScanSourceDto() { } + /// + /// Initializes a new instance of the class. + /// + /// address (required). + /// broadcastAddress. + /// capabilities (required). + /// endAddress (required). + /// varInterface (required). + /// prefixLength. + /// startAddress (required). + public NetworkScanSourceDto(string address = default(string), string broadcastAddress = default(string), NetworkScanSourceCapabilitiesDto capabilities = default(NetworkScanSourceCapabilitiesDto), string endAddress = default(string), NetworkInterfaceDto varInterface = default(NetworkInterfaceDto), int? prefixLength = default(int?), string startAddress = default(string)) + { + // to ensure "address" is required (not null) + if (address == null) + { + throw new ArgumentNullException("address is a required property for NetworkScanSourceDto and cannot be null"); + } + this.Address = address; + // to ensure "capabilities" is required (not null) + if (capabilities == null) + { + throw new ArgumentNullException("capabilities is a required property for NetworkScanSourceDto and cannot be null"); + } + this.Capabilities = capabilities; + // to ensure "endAddress" is required (not null) + if (endAddress == null) + { + throw new ArgumentNullException("endAddress is a required property for NetworkScanSourceDto and cannot be null"); + } + this.EndAddress = endAddress; + // to ensure "varInterface" is required (not null) + if (varInterface == null) + { + throw new ArgumentNullException("varInterface is a required property for NetworkScanSourceDto and cannot be null"); + } + this.Interface = varInterface; + // to ensure "startAddress" is required (not null) + if (startAddress == null) + { + throw new ArgumentNullException("startAddress is a required property for NetworkScanSourceDto and cannot be null"); + } + this.StartAddress = startAddress; + this.BroadcastAddress = broadcastAddress; + this.PrefixLength = prefixLength; + } + + /// + /// Gets or Sets Address + /// + [DataMember(Name = "address", IsRequired = true, EmitDefaultValue = true)] + public string Address { get; set; } + + /// + /// Gets or Sets BroadcastAddress + /// + [DataMember(Name = "broadcastAddress", EmitDefaultValue = true)] + public string BroadcastAddress { get; set; } + + /// + /// Gets or Sets Capabilities + /// + [DataMember(Name = "capabilities", IsRequired = true, EmitDefaultValue = true)] + public NetworkScanSourceCapabilitiesDto Capabilities { get; set; } + + /// + /// Gets or Sets EndAddress + /// + [DataMember(Name = "endAddress", IsRequired = true, EmitDefaultValue = true)] + public string EndAddress { get; set; } + + /// + /// Gets or Sets Interface + /// + [DataMember(Name = "interface", IsRequired = true, EmitDefaultValue = true)] + public NetworkInterfaceDto Interface { get; set; } + + /// + /// Gets or Sets PrefixLength + /// + [DataMember(Name = "prefixLength", EmitDefaultValue = true)] + public int? PrefixLength { get; set; } + + /// + /// Gets or Sets StartAddress + /// + [DataMember(Name = "startAddress", IsRequired = true, EmitDefaultValue = true)] + public string StartAddress { get; set; } + + /// + /// Returns the string presentation of the object + /// + /// String presentation of the object + public override string ToString() + { + StringBuilder sb = new StringBuilder(); + sb.Append("class NetworkScanSourceDto {\n"); + sb.Append(" Address: ").Append(Address).Append("\n"); + sb.Append(" BroadcastAddress: ").Append(BroadcastAddress).Append("\n"); + sb.Append(" Capabilities: ").Append(Capabilities).Append("\n"); + sb.Append(" EndAddress: ").Append(EndAddress).Append("\n"); + sb.Append(" Interface: ").Append(Interface).Append("\n"); + sb.Append(" PrefixLength: ").Append(PrefixLength).Append("\n"); + sb.Append(" StartAddress: ").Append(StartAddress).Append("\n"); + sb.Append("}\n"); + return sb.ToString(); + } + + /// + /// Returns the JSON string presentation of the object + /// + /// JSON string presentation of the object + public virtual string ToJson() + { + return Newtonsoft.Json.JsonConvert.SerializeObject(this, Newtonsoft.Json.Formatting.Indented); + } + + /// + /// To validate all properties of the instance + /// + /// Validation context + /// Validation Result + IEnumerable IValidatableObject.Validate(ValidationContext validationContext) + { + // PrefixLength (int?) minimum + if (this.PrefixLength < (int?)0) + { + yield return new ValidationResult("Invalid value for PrefixLength, must be a value greater than or equal to 0.", new [] { "PrefixLength" }); + } + + yield break; + } + } + +} diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/PreflightAlertStatus.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/PreflightAlertStatus.cs index 24da53a6e..f943d4fb6 100644 --- a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/PreflightAlertStatus.cs +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/PreflightAlertStatus.cs @@ -3,7 +3,7 @@ * * Protocol-aware fine-grained relay server * - * The version of the OpenAPI document: 2025.3.2 + * The version of the OpenAPI document: 2026.2.4 * Contact: infos@devolutions.net * Generated by: https://github.com/openapitools/openapi-generator.git */ diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/PreflightOperation.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/PreflightOperation.cs index f2d1e9b56..52005cd09 100644 --- a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/PreflightOperation.cs +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/PreflightOperation.cs @@ -3,7 +3,7 @@ * * Protocol-aware fine-grained relay server * - * The version of the OpenAPI document: 2025.3.2 + * The version of the OpenAPI document: 2026.2.4 * Contact: infos@devolutions.net * Generated by: https://github.com/openapitools/openapi-generator.git */ @@ -47,17 +47,19 @@ protected PreflightOperation() { } /// /// Initializes a new instance of the class. /// + /// connectionOptions. /// The hostname to perform DNS resolution on. Required for \"resolve-host\" kind.. /// Unique ID identifying the preflight operation. (required). /// kind (required). /// proxyCredential. /// targetCredential. - /// Minimum persistance duration in seconds for the data provisioned via this operation. Optional parameter for \"provision-token\" and \"provision-credentials\" kinds.. - /// The token to be stored on the proxy-side. Required for \"provision-token\" and \"provision-credentials\" kinds.. - public PreflightOperation(string hostToResolve = default(string), Guid id = default(Guid), PreflightOperationKind kind = default(PreflightOperationKind), AppCredential proxyCredential = default(AppCredential), AppCredential targetCredential = default(AppCredential), int? timeToLive = default(int?), string token = default(string)) + /// Minimum persistence duration in seconds for the data provisioned via this operation. Optional parameter for \"provision-token\", \"provision-credentials\" and \"provision-connection-options\" kinds.. + /// The token to be stored on the proxy-side. Required for \"provision-token\", \"provision-credentials\" and \"provision-connection-options\" kinds.. + public PreflightOperation(TargetConnectionOptions connectionOptions = default(TargetConnectionOptions), string hostToResolve = default(string), Guid id = default(Guid), PreflightOperationKind kind = default(PreflightOperationKind), AppCredential proxyCredential = default(AppCredential), AppCredential targetCredential = default(AppCredential), int? timeToLive = default(int?), string token = default(string)) { this.Id = id; this.Kind = kind; + this.ConnectionOptions = connectionOptions; this.HostToResolve = hostToResolve; this.ProxyCredential = proxyCredential; this.TargetCredential = targetCredential; @@ -65,6 +67,12 @@ protected PreflightOperation() { } this.Token = token; } + /// + /// Gets or Sets ConnectionOptions + /// + [DataMember(Name = "connection_options", EmitDefaultValue = true)] + public TargetConnectionOptions ConnectionOptions { get; set; } + /// /// The hostname to perform DNS resolution on. Required for \"resolve-host\" kind. /// @@ -92,16 +100,16 @@ protected PreflightOperation() { } public AppCredential TargetCredential { get; set; } /// - /// Minimum persistance duration in seconds for the data provisioned via this operation. Optional parameter for \"provision-token\" and \"provision-credentials\" kinds. + /// Minimum persistence duration in seconds for the data provisioned via this operation. Optional parameter for \"provision-token\", \"provision-credentials\" and \"provision-connection-options\" kinds. /// - /// Minimum persistance duration in seconds for the data provisioned via this operation. Optional parameter for \"provision-token\" and \"provision-credentials\" kinds. + /// Minimum persistence duration in seconds for the data provisioned via this operation. Optional parameter for \"provision-token\", \"provision-credentials\" and \"provision-connection-options\" kinds. [DataMember(Name = "time_to_live", EmitDefaultValue = true)] public int? TimeToLive { get; set; } /// - /// The token to be stored on the proxy-side. Required for \"provision-token\" and \"provision-credentials\" kinds. + /// The token to be stored on the proxy-side. Required for \"provision-token\", \"provision-credentials\" and \"provision-connection-options\" kinds. /// - /// The token to be stored on the proxy-side. Required for \"provision-token\" and \"provision-credentials\" kinds. + /// The token to be stored on the proxy-side. Required for \"provision-token\", \"provision-credentials\" and \"provision-connection-options\" kinds. [DataMember(Name = "token", EmitDefaultValue = true)] public string Token { get; set; } @@ -113,6 +121,7 @@ public override string ToString() { StringBuilder sb = new StringBuilder(); sb.Append("class PreflightOperation {\n"); + sb.Append(" ConnectionOptions: ").Append(ConnectionOptions).Append("\n"); sb.Append(" HostToResolve: ").Append(HostToResolve).Append("\n"); sb.Append(" Id: ").Append(Id).Append("\n"); sb.Append(" Kind: ").Append(Kind).Append("\n"); diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/PreflightOperationKind.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/PreflightOperationKind.cs index 51da44a10..bfcdf43e4 100644 --- a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/PreflightOperationKind.cs +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/PreflightOperationKind.cs @@ -3,7 +3,7 @@ * * Protocol-aware fine-grained relay server * - * The version of the OpenAPI document: 2025.3.2 + * The version of the OpenAPI document: 2026.2.4 * Contact: infos@devolutions.net * Generated by: https://github.com/openapitools/openapi-generator.git */ @@ -69,11 +69,17 @@ public enum PreflightOperationKind [EnumMember(Value = "provision-credentials")] ProvisionCredentials = 6, + /// + /// Enum ProvisionConnectionOptions for value: provision-connection-options + /// + [EnumMember(Value = "provision-connection-options")] + ProvisionConnectionOptions = 7, + /// /// Enum ResolveHost for value: resolve-host /// [EnumMember(Value = "resolve-host")] - ResolveHost = 7 + ResolveHost = 8 } public static class PreflightOperationKindExtensions @@ -97,6 +103,8 @@ public static string ToValue(this PreflightOperationKind variant) return "provision-token"; case PreflightOperationKind.ProvisionCredentials: return "provision-credentials"; + case PreflightOperationKind.ProvisionConnectionOptions: + return "provision-connection-options"; case PreflightOperationKind.ResolveHost: return "resolve-host"; default: diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/PreflightOutput.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/PreflightOutput.cs index e60158a9e..35b5cd057 100644 --- a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/PreflightOutput.cs +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/PreflightOutput.cs @@ -3,7 +3,7 @@ * * Protocol-aware fine-grained relay server * - * The version of the OpenAPI document: 2025.3.2 + * The version of the OpenAPI document: 2026.2.4 * Contact: infos@devolutions.net * Generated by: https://github.com/openapitools/openapi-generator.git */ diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/PreflightOutputKind.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/PreflightOutputKind.cs index 872ede506..b05ad143e 100644 --- a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/PreflightOutputKind.cs +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/PreflightOutputKind.cs @@ -3,7 +3,7 @@ * * Protocol-aware fine-grained relay server * - * The version of the OpenAPI document: 2025.3.2 + * The version of the OpenAPI document: 2026.2.4 * Contact: infos@devolutions.net * Generated by: https://github.com/openapitools/openapi-generator.git */ diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/PubKeyFormat.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/PubKeyFormat.cs index a714fd97e..805d381c3 100644 --- a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/PubKeyFormat.cs +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/PubKeyFormat.cs @@ -3,7 +3,7 @@ * * Protocol-aware fine-grained relay server * - * The version of the OpenAPI document: 2025.3.2 + * The version of the OpenAPI document: 2026.2.4 * Contact: infos@devolutions.net * Generated by: https://github.com/openapitools/openapi-generator.git */ diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/ScanOriginDto.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/ScanOriginDto.cs new file mode 100644 index 000000000..a47ef1ba3 --- /dev/null +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/ScanOriginDto.cs @@ -0,0 +1,60 @@ +/* + * devolutions-gateway + * + * Protocol-aware fine-grained relay server + * + * The version of the OpenAPI document: 2026.2.4 + * Contact: infos@devolutions.net + * Generated by: https://github.com/openapitools/openapi-generator.git + */ + + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.IO; +using System.Runtime.Serialization; +using System.Text; +using System.Text.RegularExpressions; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Newtonsoft.Json.Linq; +using System.ComponentModel.DataAnnotations; +using FileParameter = Devolutions.Gateway.Client.Client.FileParameter; +using OpenAPIDateConverter = Devolutions.Gateway.Client.Client.OpenAPIDateConverter; + +namespace Devolutions.Gateway.Client.Model +{ + /// + /// Defines ScanOriginDto + /// + [JsonConverter(typeof(StringEnumConverter))] + public enum ScanOriginDto + { + /// + /// Enum Gateway for value: gateway + /// + [EnumMember(Value = "gateway")] + Gateway = 1 + } + + public static class ScanOriginDtoExtensions + { + /// + /// Returns the value as string for a given variant + /// + public static string ToValue(this ScanOriginDto variant) + { + switch (variant) + { + case ScanOriginDto.Gateway: + return "gateway"; + default: + throw new ArgumentOutOfRangeException(nameof(variant), $"Unexpected variant: {variant}"); + } + } + } + +} diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/ScanResultSourceDto.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/ScanResultSourceDto.cs new file mode 100644 index 000000000..a98ab0a98 --- /dev/null +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/ScanResultSourceDto.cs @@ -0,0 +1,92 @@ +/* + * devolutions-gateway + * + * Protocol-aware fine-grained relay server + * + * The version of the OpenAPI document: 2026.2.4 + * Contact: infos@devolutions.net + * Generated by: https://github.com/openapitools/openapi-generator.git + */ + + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.IO; +using System.Runtime.Serialization; +using System.Text; +using System.Text.RegularExpressions; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Newtonsoft.Json.Linq; +using System.ComponentModel.DataAnnotations; +using FileParameter = Devolutions.Gateway.Client.Client.FileParameter; +using OpenAPIDateConverter = Devolutions.Gateway.Client.Client.OpenAPIDateConverter; + +namespace Devolutions.Gateway.Client.Model +{ + /// + /// Defines ScanResultSourceDto + /// + [JsonConverter(typeof(StringEnumConverter))] + public enum ScanResultSourceDto + { + /// + /// Enum Subnet for value: subnet + /// + [EnumMember(Value = "subnet")] + Subnet = 1, + + /// + /// Enum Broadcast for value: broadcast + /// + [EnumMember(Value = "broadcast")] + Broadcast = 2, + + /// + /// Enum TcpProbe for value: tcp_probe + /// + [EnumMember(Value = "tcp_probe")] + TcpProbe = 3, + + /// + /// Enum Gateway for value: gateway + /// + [EnumMember(Value = "gateway")] + Gateway = 4, + + /// + /// Enum ZeroConf for value: zero_conf + /// + [EnumMember(Value = "zero_conf")] + ZeroConf = 5 + } + + public static class ScanResultSourceDtoExtensions + { + /// + /// Returns the value as string for a given variant + /// + public static string ToValue(this ScanResultSourceDto variant) + { + switch (variant) + { + case ScanResultSourceDto.Subnet: + return "subnet"; + case ScanResultSourceDto.Broadcast: + return "broadcast"; + case ScanResultSourceDto.TcpProbe: + return "tcp_probe"; + case ScanResultSourceDto.Gateway: + return "gateway"; + case ScanResultSourceDto.ZeroConf: + return "zero_conf"; + default: + throw new ArgumentOutOfRangeException(nameof(variant), $"Unexpected variant: {variant}"); + } + } + } + +} diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/SessionInfo.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/SessionInfo.cs index 2dd24b0c0..a63021619 100644 --- a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/SessionInfo.cs +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/SessionInfo.cs @@ -3,7 +3,7 @@ * * Protocol-aware fine-grained relay server * - * The version of the OpenAPI document: 2025.3.2 + * The version of the OpenAPI document: 2026.2.4 * Contact: infos@devolutions.net * Generated by: https://github.com/openapitools/openapi-generator.git */ diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/SessionTokenContentType.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/SessionTokenContentType.cs index 8d485f3f5..20b9a1773 100644 --- a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/SessionTokenContentType.cs +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/SessionTokenContentType.cs @@ -3,7 +3,7 @@ * * Protocol-aware fine-grained relay server * - * The version of the OpenAPI document: 2025.3.2 + * The version of the OpenAPI document: 2026.2.4 * Contact: infos@devolutions.net * Generated by: https://github.com/openapitools/openapi-generator.git */ diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/SessionTokenSignRequest.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/SessionTokenSignRequest.cs index b40a10e51..611233691 100644 --- a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/SessionTokenSignRequest.cs +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/SessionTokenSignRequest.cs @@ -3,7 +3,7 @@ * * Protocol-aware fine-grained relay server * - * The version of the OpenAPI document: 2025.3.2 + * The version of the OpenAPI document: 2026.2.4 * Contact: infos@devolutions.net * Generated by: https://github.com/openapitools/openapi-generator.git */ diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/SetConfigResponse.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/SetConfigResponse.cs index 1bafc62b1..1914cf188 100644 --- a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/SetConfigResponse.cs +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/SetConfigResponse.cs @@ -3,7 +3,7 @@ * * Protocol-aware fine-grained relay server * - * The version of the OpenAPI document: 2025.3.2 + * The version of the OpenAPI document: 2026.2.4 * Contact: infos@devolutions.net * Generated by: https://github.com/openapitools/openapi-generator.git */ diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/SetUpdateScheduleRequest.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/SetUpdateScheduleRequest.cs new file mode 100644 index 000000000..e12658b41 --- /dev/null +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/SetUpdateScheduleRequest.cs @@ -0,0 +1,148 @@ +/* + * devolutions-gateway + * + * Protocol-aware fine-grained relay server + * + * The version of the OpenAPI document: 2026.2.4 + * Contact: infos@devolutions.net + * Generated by: https://github.com/openapitools/openapi-generator.git + */ + + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.IO; +using System.Runtime.Serialization; +using System.Text; +using System.Text.RegularExpressions; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Newtonsoft.Json.Linq; +using System.ComponentModel.DataAnnotations; +using FileParameter = Devolutions.Gateway.Client.Client.FileParameter; +using OpenAPIDateConverter = Devolutions.Gateway.Client.Client.OpenAPIDateConverter; + +namespace Devolutions.Gateway.Client.Model +{ + /// + /// Desired auto-update schedule to apply to Devolutions Agent. + /// + [DataContract(Name = "SetUpdateScheduleRequest")] + public partial class SetUpdateScheduleRequest : IValidatableObject + { + /// + /// Initializes a new instance of the class. + /// + [JsonConstructorAttribute] + protected SetUpdateScheduleRequest() { } + /// + /// Initializes a new instance of the class. + /// + /// Enable periodic Devolutions Agent self-update checks. (required). + /// Minimum interval between auto-update checks, in seconds. `0` means check once at `UpdateWindowStart` (default).. + /// Products the agent autonomously polls for new versions (default: empty).. + /// End of the maintenance window as seconds past midnight in local time, exclusive. `null` (default) means no upper bound - a single check fires at `UpdateWindowStart`. When end < start the window crosses midnight.. + /// Start of the maintenance window as seconds past midnight in local time (default: `7200` = 02:00).. + public SetUpdateScheduleRequest(bool enabled = default(bool), long interval = default(long), List products = default(List), int? updateWindowEnd = default(int?), int updateWindowStart = default(int)) + { + this.Enabled = enabled; + this.Interval = interval; + this.Products = products; + this.UpdateWindowEnd = updateWindowEnd; + this.UpdateWindowStart = updateWindowStart; + } + + /// + /// Enable periodic Devolutions Agent self-update checks. + /// + /// Enable periodic Devolutions Agent self-update checks. + [DataMember(Name = "Enabled", IsRequired = true, EmitDefaultValue = true)] + public bool Enabled { get; set; } + + /// + /// Minimum interval between auto-update checks, in seconds. `0` means check once at `UpdateWindowStart` (default). + /// + /// Minimum interval between auto-update checks, in seconds. `0` means check once at `UpdateWindowStart` (default). + [DataMember(Name = "Interval", EmitDefaultValue = false)] + public long Interval { get; set; } + + /// + /// Products the agent autonomously polls for new versions (default: empty). + /// + /// Products the agent autonomously polls for new versions (default: empty). + [DataMember(Name = "Products", EmitDefaultValue = false)] + public List Products { get; set; } + + /// + /// End of the maintenance window as seconds past midnight in local time, exclusive. `null` (default) means no upper bound - a single check fires at `UpdateWindowStart`. When end < start the window crosses midnight. + /// + /// End of the maintenance window as seconds past midnight in local time, exclusive. `null` (default) means no upper bound - a single check fires at `UpdateWindowStart`. When end < start the window crosses midnight. + [DataMember(Name = "UpdateWindowEnd", EmitDefaultValue = true)] + public int? UpdateWindowEnd { get; set; } + + /// + /// Start of the maintenance window as seconds past midnight in local time (default: `7200` = 02:00). + /// + /// Start of the maintenance window as seconds past midnight in local time (default: `7200` = 02:00). + [DataMember(Name = "UpdateWindowStart", EmitDefaultValue = false)] + public int UpdateWindowStart { get; set; } + + /// + /// Returns the string presentation of the object + /// + /// String presentation of the object + public override string ToString() + { + StringBuilder sb = new StringBuilder(); + sb.Append("class SetUpdateScheduleRequest {\n"); + sb.Append(" Enabled: ").Append(Enabled).Append("\n"); + sb.Append(" Interval: ").Append(Interval).Append("\n"); + sb.Append(" Products: ").Append(Products).Append("\n"); + sb.Append(" UpdateWindowEnd: ").Append(UpdateWindowEnd).Append("\n"); + sb.Append(" UpdateWindowStart: ").Append(UpdateWindowStart).Append("\n"); + sb.Append("}\n"); + return sb.ToString(); + } + + /// + /// Returns the JSON string presentation of the object + /// + /// JSON string presentation of the object + public virtual string ToJson() + { + return Newtonsoft.Json.JsonConvert.SerializeObject(this, Newtonsoft.Json.Formatting.Indented); + } + + /// + /// To validate all properties of the instance + /// + /// Validation context + /// Validation Result + IEnumerable IValidatableObject.Validate(ValidationContext validationContext) + { + // Interval (long) minimum + if (this.Interval < (long)0) + { + yield return new ValidationResult("Invalid value for Interval, must be a value greater than or equal to 0.", new [] { "Interval" }); + } + + // UpdateWindowEnd (int?) minimum + if (this.UpdateWindowEnd < (int?)0) + { + yield return new ValidationResult("Invalid value for UpdateWindowEnd, must be a value greater than or equal to 0.", new [] { "UpdateWindowEnd" }); + } + + // UpdateWindowStart (int) minimum + if (this.UpdateWindowStart < (int)0) + { + yield return new ValidationResult("Invalid value for UpdateWindowStart, must be a value greater than or equal to 0.", new [] { "UpdateWindowStart" }); + } + + yield break; + } + } + +} diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/SubProvisionerKey.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/SubProvisionerKey.cs index e51daafcc..404ab0fed 100644 --- a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/SubProvisionerKey.cs +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/SubProvisionerKey.cs @@ -3,7 +3,7 @@ * * Protocol-aware fine-grained relay server * - * The version of the OpenAPI document: 2025.3.2 + * The version of the OpenAPI document: 2026.2.4 * Contact: infos@devolutions.net * Generated by: https://github.com/openapitools/openapi-generator.git */ diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/Subscriber.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/Subscriber.cs index 48f0e746d..ac570f147 100644 --- a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/Subscriber.cs +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/Subscriber.cs @@ -3,7 +3,7 @@ * * Protocol-aware fine-grained relay server * - * The version of the OpenAPI document: 2025.3.2 + * The version of the OpenAPI document: 2026.2.4 * Contact: infos@devolutions.net * Generated by: https://github.com/openapitools/openapi-generator.git */ diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/TargetConnectionOptions.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/TargetConnectionOptions.cs new file mode 100644 index 000000000..a1f32b05b --- /dev/null +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/TargetConnectionOptions.cs @@ -0,0 +1,85 @@ +/* + * devolutions-gateway + * + * Protocol-aware fine-grained relay server + * + * The version of the OpenAPI document: 2026.2.4 + * Contact: infos@devolutions.net + * Generated by: https://github.com/openapitools/openapi-generator.git + */ + + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.IO; +using System.Runtime.Serialization; +using System.Text; +using System.Text.RegularExpressions; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Newtonsoft.Json.Linq; +using System.ComponentModel.DataAnnotations; +using FileParameter = Devolutions.Gateway.Client.Client.FileParameter; +using OpenAPIDateConverter = Devolutions.Gateway.Client.Client.OpenAPIDateConverter; + +namespace Devolutions.Gateway.Client.Model +{ + /// + /// TargetConnectionOptions + /// + [DataContract(Name = "TargetConnectionOptions")] + public partial class TargetConnectionOptions : IValidatableObject + { + /// + /// Initializes a new instance of the class. + /// + /// Kerberos KDC address for the target-side CredSSP connection. Supported schemes are `tcp` and `udp`.. + public TargetConnectionOptions(string krbKdc = default(string)) + { + this.KrbKdc = krbKdc; + } + + /// + /// Kerberos KDC address for the target-side CredSSP connection. Supported schemes are `tcp` and `udp`. + /// + /// Kerberos KDC address for the target-side CredSSP connection. Supported schemes are `tcp` and `udp`. + [DataMember(Name = "krb_kdc", EmitDefaultValue = true)] + public string KrbKdc { get; set; } + + /// + /// Returns the string presentation of the object + /// + /// String presentation of the object + public override string ToString() + { + StringBuilder sb = new StringBuilder(); + sb.Append("class TargetConnectionOptions {\n"); + sb.Append(" KrbKdc: ").Append(KrbKdc).Append("\n"); + sb.Append("}\n"); + return sb.ToString(); + } + + /// + /// Returns the JSON string presentation of the object + /// + /// JSON string presentation of the object + public virtual string ToJson() + { + return Newtonsoft.Json.JsonConvert.SerializeObject(this, Newtonsoft.Json.Formatting.Indented); + } + + /// + /// To validate all properties of the instance + /// + /// Validation context + /// Validation Result + IEnumerable IValidatableObject.Validate(ValidationContext validationContext) + { + yield break; + } + } + +} diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/TrafficEventResponse.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/TrafficEventResponse.cs index baf34a61c..317a12c35 100644 --- a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/TrafficEventResponse.cs +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/TrafficEventResponse.cs @@ -3,7 +3,7 @@ * * Protocol-aware fine-grained relay server * - * The version of the OpenAPI document: 2025.3.2 + * The version of the OpenAPI document: 2026.2.4 * Contact: infos@devolutions.net * Generated by: https://github.com/openapitools/openapi-generator.git */ diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/TransportProtocolResponse.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/TransportProtocolResponse.cs index 79ef69d5e..dc275b1a8 100644 --- a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/TransportProtocolResponse.cs +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/TransportProtocolResponse.cs @@ -3,7 +3,7 @@ * * Protocol-aware fine-grained relay server * - * The version of the OpenAPI document: 2025.3.2 + * The version of the OpenAPI document: 2026.2.4 * Contact: infos@devolutions.net * Generated by: https://github.com/openapitools/openapi-generator.git */ diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/UpdateProductInfo.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/UpdateProductInfo.cs new file mode 100644 index 000000000..30008605c --- /dev/null +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/UpdateProductInfo.cs @@ -0,0 +1,95 @@ +/* + * devolutions-gateway + * + * Protocol-aware fine-grained relay server + * + * The version of the OpenAPI document: 2026.2.4 + * Contact: infos@devolutions.net + * Generated by: https://github.com/openapitools/openapi-generator.git + */ + + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.IO; +using System.Runtime.Serialization; +using System.Text; +using System.Text.RegularExpressions; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Newtonsoft.Json.Linq; +using System.ComponentModel.DataAnnotations; +using FileParameter = Devolutions.Gateway.Client.Client.FileParameter; +using OpenAPIDateConverter = Devolutions.Gateway.Client.Client.OpenAPIDateConverter; + +namespace Devolutions.Gateway.Client.Model +{ + /// + /// Per-product update information. + /// + [DataContract(Name = "UpdateProductInfo")] + public partial class UpdateProductInfo : IValidatableObject + { + /// + /// Initializes a new instance of the class. + /// + [JsonConstructorAttribute] + protected UpdateProductInfo() { } + /// + /// Initializes a new instance of the class. + /// + /// Requested or installed version: `\"latest\"` or `\"YYYY.M.D\"` / `\"YYYY.M.D.R\"`. (required). + public UpdateProductInfo(string varVersion = default(string)) + { + // to ensure "varVersion" is required (not null) + if (varVersion == null) + { + throw new ArgumentNullException("varVersion is a required property for UpdateProductInfo and cannot be null"); + } + this.VarVersion = varVersion; + } + + /// + /// Requested or installed version: `\"latest\"` or `\"YYYY.M.D\"` / `\"YYYY.M.D.R\"`. + /// + /// Requested or installed version: `\"latest\"` or `\"YYYY.M.D\"` / `\"YYYY.M.D.R\"`. + [DataMember(Name = "Version", IsRequired = true, EmitDefaultValue = true)] + public string VarVersion { get; set; } + + /// + /// Returns the string presentation of the object + /// + /// String presentation of the object + public override string ToString() + { + StringBuilder sb = new StringBuilder(); + sb.Append("class UpdateProductInfo {\n"); + sb.Append(" VarVersion: ").Append(VarVersion).Append("\n"); + sb.Append("}\n"); + return sb.ToString(); + } + + /// + /// Returns the JSON string presentation of the object + /// + /// JSON string presentation of the object + public virtual string ToJson() + { + return Newtonsoft.Json.JsonConvert.SerializeObject(this, Newtonsoft.Json.Formatting.Indented); + } + + /// + /// To validate all properties of the instance + /// + /// Validation context + /// Validation Result + IEnumerable IValidatableObject.Validate(ValidationContext validationContext) + { + yield break; + } + } + +} diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/UpdateRequestSchema.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/UpdateRequestSchema.cs new file mode 100644 index 000000000..8adad30c1 --- /dev/null +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/UpdateRequestSchema.cs @@ -0,0 +1,85 @@ +/* + * devolutions-gateway + * + * Protocol-aware fine-grained relay server + * + * The version of the OpenAPI document: 2026.2.4 + * Contact: infos@devolutions.net + * Generated by: https://github.com/openapitools/openapi-generator.git + */ + + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.IO; +using System.Runtime.Serialization; +using System.Text; +using System.Text.RegularExpressions; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Newtonsoft.Json.Linq; +using System.ComponentModel.DataAnnotations; +using FileParameter = Devolutions.Gateway.Client.Client.FileParameter; +using OpenAPIDateConverter = Devolutions.Gateway.Client.Client.OpenAPIDateConverter; + +namespace Devolutions.Gateway.Client.Model +{ + /// + /// OpenAPI schema for the update request body. The API accepts an object containing a `Products` map, whose keys are product names and whose values are update information. + /// + [DataContract(Name = "UpdateRequestSchema")] + public partial class UpdateRequestSchema : IValidatableObject + { + /// + /// Initializes a new instance of the class. + /// + /// Map of product name to update information.. + public UpdateRequestSchema(Dictionary products = default(Dictionary)) + { + this.Products = products; + } + + /// + /// Map of product name to update information. + /// + /// Map of product name to update information. + [DataMember(Name = "Products", EmitDefaultValue = false)] + public Dictionary Products { get; set; } + + /// + /// Returns the string presentation of the object + /// + /// String presentation of the object + public override string ToString() + { + StringBuilder sb = new StringBuilder(); + sb.Append("class UpdateRequestSchema {\n"); + sb.Append(" Products: ").Append(Products).Append("\n"); + sb.Append("}\n"); + return sb.ToString(); + } + + /// + /// Returns the JSON string presentation of the object + /// + /// JSON string presentation of the object + public virtual string ToJson() + { + return Newtonsoft.Json.JsonConvert.SerializeObject(this, Newtonsoft.Json.Formatting.Indented); + } + + /// + /// To validate all properties of the instance + /// + /// Validation context + /// Validation Result + IEnumerable IValidatableObject.Validate(ValidationContext validationContext) + { + yield break; + } + } + +} diff --git a/devolutions-gateway/openapi/dotnet-subscriber/src/Devolutions.Gateway.Subscriber/Attributes/ValidateModelStateAttribute.cs b/devolutions-gateway/openapi/dotnet-subscriber/src/Devolutions.Gateway.Subscriber/Attributes/ValidateModelStateAttribute.cs index ab8664636..27433372d 100644 --- a/devolutions-gateway/openapi/dotnet-subscriber/src/Devolutions.Gateway.Subscriber/Attributes/ValidateModelStateAttribute.cs +++ b/devolutions-gateway/openapi/dotnet-subscriber/src/Devolutions.Gateway.Subscriber/Attributes/ValidateModelStateAttribute.cs @@ -3,7 +3,7 @@ * * API a service must implement in order to receive Devolutions Gateway notifications * - * The version of the OpenAPI document: 2025.3.2 + * The version of the OpenAPI document: 2026.2.4 * Contact: infos@devolutions.net * Generated by: https://openapi-generator.tech */ diff --git a/devolutions-gateway/openapi/dotnet-subscriber/src/Devolutions.Gateway.Subscriber/Authentication/ApiAuthentication.cs b/devolutions-gateway/openapi/dotnet-subscriber/src/Devolutions.Gateway.Subscriber/Authentication/ApiAuthentication.cs index c5c9f6eff..71ae543b6 100644 --- a/devolutions-gateway/openapi/dotnet-subscriber/src/Devolutions.Gateway.Subscriber/Authentication/ApiAuthentication.cs +++ b/devolutions-gateway/openapi/dotnet-subscriber/src/Devolutions.Gateway.Subscriber/Authentication/ApiAuthentication.cs @@ -3,7 +3,7 @@ * * API a service must implement in order to receive Devolutions Gateway notifications * - * The version of the OpenAPI document: 2025.3.2 + * The version of the OpenAPI document: 2026.2.4 * Contact: infos@devolutions.net * Generated by: https://openapi-generator.tech */ diff --git a/devolutions-gateway/openapi/dotnet-subscriber/src/Devolutions.Gateway.Subscriber/Controllers/SubscriberApi.cs b/devolutions-gateway/openapi/dotnet-subscriber/src/Devolutions.Gateway.Subscriber/Controllers/SubscriberApi.cs index a4dbe8666..c22598601 100644 --- a/devolutions-gateway/openapi/dotnet-subscriber/src/Devolutions.Gateway.Subscriber/Controllers/SubscriberApi.cs +++ b/devolutions-gateway/openapi/dotnet-subscriber/src/Devolutions.Gateway.Subscriber/Controllers/SubscriberApi.cs @@ -3,7 +3,7 @@ * * API a service must implement in order to receive Devolutions Gateway notifications * - * The version of the OpenAPI document: 2025.3.2 + * The version of the OpenAPI document: 2026.2.4 * Contact: infos@devolutions.net * Generated by: https://openapi-generator.tech */ diff --git a/devolutions-gateway/openapi/dotnet-subscriber/src/Devolutions.Gateway.Subscriber/Converters/CustomEnumConverter.cs b/devolutions-gateway/openapi/dotnet-subscriber/src/Devolutions.Gateway.Subscriber/Converters/CustomEnumConverter.cs index 811441e40..f70017e6f 100644 --- a/devolutions-gateway/openapi/dotnet-subscriber/src/Devolutions.Gateway.Subscriber/Converters/CustomEnumConverter.cs +++ b/devolutions-gateway/openapi/dotnet-subscriber/src/Devolutions.Gateway.Subscriber/Converters/CustomEnumConverter.cs @@ -3,7 +3,7 @@ * * API a service must implement in order to receive Devolutions Gateway notifications * - * The version of the OpenAPI document: 2025.3.2 + * The version of the OpenAPI document: 2026.2.4 * Contact: infos@devolutions.net * Generated by: https://openapi-generator.tech */ diff --git a/devolutions-gateway/openapi/dotnet-subscriber/src/Devolutions.Gateway.Subscriber/Devolutions.Gateway.Subscriber.csproj b/devolutions-gateway/openapi/dotnet-subscriber/src/Devolutions.Gateway.Subscriber/Devolutions.Gateway.Subscriber.csproj index 183bb5590..ba24e79f7 100644 --- a/devolutions-gateway/openapi/dotnet-subscriber/src/Devolutions.Gateway.Subscriber/Devolutions.Gateway.Subscriber.csproj +++ b/devolutions-gateway/openapi/dotnet-subscriber/src/Devolutions.Gateway.Subscriber/Devolutions.Gateway.Subscriber.csproj @@ -3,7 +3,7 @@ C# interface to implement Devolutions Gateway Subscriber API No Copyright Devolutions Inc. - net10.0 + net6.0 true true 2023.2.22.0 diff --git a/devolutions-gateway/openapi/dotnet-subscriber/src/Devolutions.Gateway.Subscriber/Formatters/InputFormatterStream.cs b/devolutions-gateway/openapi/dotnet-subscriber/src/Devolutions.Gateway.Subscriber/Formatters/InputFormatterStream.cs index 90272ba19..763f1b0f3 100644 --- a/devolutions-gateway/openapi/dotnet-subscriber/src/Devolutions.Gateway.Subscriber/Formatters/InputFormatterStream.cs +++ b/devolutions-gateway/openapi/dotnet-subscriber/src/Devolutions.Gateway.Subscriber/Formatters/InputFormatterStream.cs @@ -3,7 +3,7 @@ * * API a service must implement in order to receive Devolutions Gateway notifications * - * The version of the OpenAPI document: 2025.3.2 + * The version of the OpenAPI document: 2026.2.4 * Contact: infos@devolutions.net * Generated by: https://openapi-generator.tech */ diff --git a/devolutions-gateway/openapi/dotnet-subscriber/src/Devolutions.Gateway.Subscriber/Models/SubscriberMessage.cs b/devolutions-gateway/openapi/dotnet-subscriber/src/Devolutions.Gateway.Subscriber/Models/SubscriberMessage.cs index 3c782ca6b..869acc3ec 100644 --- a/devolutions-gateway/openapi/dotnet-subscriber/src/Devolutions.Gateway.Subscriber/Models/SubscriberMessage.cs +++ b/devolutions-gateway/openapi/dotnet-subscriber/src/Devolutions.Gateway.Subscriber/Models/SubscriberMessage.cs @@ -3,7 +3,7 @@ * * API a service must implement in order to receive Devolutions Gateway notifications * - * The version of the OpenAPI document: 2025.3.2 + * The version of the OpenAPI document: 2026.2.4 * Contact: infos@devolutions.net * Generated by: https://openapi-generator.tech */ diff --git a/devolutions-gateway/openapi/dotnet-subscriber/src/Devolutions.Gateway.Subscriber/Models/SubscriberMessageKind.cs b/devolutions-gateway/openapi/dotnet-subscriber/src/Devolutions.Gateway.Subscriber/Models/SubscriberMessageKind.cs index 1ef9ff3be..c1ca4cafb 100644 --- a/devolutions-gateway/openapi/dotnet-subscriber/src/Devolutions.Gateway.Subscriber/Models/SubscriberMessageKind.cs +++ b/devolutions-gateway/openapi/dotnet-subscriber/src/Devolutions.Gateway.Subscriber/Models/SubscriberMessageKind.cs @@ -3,7 +3,7 @@ * * API a service must implement in order to receive Devolutions Gateway notifications * - * The version of the OpenAPI document: 2025.3.2 + * The version of the OpenAPI document: 2026.2.4 * Contact: infos@devolutions.net * Generated by: https://openapi-generator.tech */ diff --git a/devolutions-gateway/openapi/dotnet-subscriber/src/Devolutions.Gateway.Subscriber/Models/SubscriberSessionInfo.cs b/devolutions-gateway/openapi/dotnet-subscriber/src/Devolutions.Gateway.Subscriber/Models/SubscriberSessionInfo.cs index bdfff0e35..532d5ef72 100644 --- a/devolutions-gateway/openapi/dotnet-subscriber/src/Devolutions.Gateway.Subscriber/Models/SubscriberSessionInfo.cs +++ b/devolutions-gateway/openapi/dotnet-subscriber/src/Devolutions.Gateway.Subscriber/Models/SubscriberSessionInfo.cs @@ -3,7 +3,7 @@ * * API a service must implement in order to receive Devolutions Gateway notifications * - * The version of the OpenAPI document: 2025.3.2 + * The version of the OpenAPI document: 2026.2.4 * Contact: infos@devolutions.net * Generated by: https://openapi-generator.tech */ diff --git a/devolutions-gateway/openapi/dotnet-subscriber/src/Devolutions.Gateway.Subscriber/OpenApi/TypeExtensions.cs b/devolutions-gateway/openapi/dotnet-subscriber/src/Devolutions.Gateway.Subscriber/OpenApi/TypeExtensions.cs index 386f4a040..28fd38278 100644 --- a/devolutions-gateway/openapi/dotnet-subscriber/src/Devolutions.Gateway.Subscriber/OpenApi/TypeExtensions.cs +++ b/devolutions-gateway/openapi/dotnet-subscriber/src/Devolutions.Gateway.Subscriber/OpenApi/TypeExtensions.cs @@ -3,7 +3,7 @@ * * API a service must implement in order to receive Devolutions Gateway notifications * - * The version of the OpenAPI document: 2025.3.2 + * The version of the OpenAPI document: 2026.2.4 * Contact: infos@devolutions.net * Generated by: https://openapi-generator.tech */ diff --git a/devolutions-gateway/openapi/gateway-api.yaml b/devolutions-gateway/openapi/gateway-api.yaml index c64ef1c77..3bb69ca72 100644 --- a/devolutions-gateway/openapi/gateway-api.yaml +++ b/devolutions-gateway/openapi/gateway-api.yaml @@ -1772,6 +1772,10 @@ components: - id - kind properties: + connection_options: + allOf: + - $ref: '#/components/schemas/TargetConnectionOptions' + nullable: true host_to_resolve: type: string description: |- @@ -1799,7 +1803,7 @@ components: description: |- Minimum persistence duration in seconds for the data provisioned via this operation. - Optional parameter for "provision-token" and "provision-credentials" kinds. + Optional parameter for "provision-token", "provision-credentials" and "provision-connection-options" kinds. nullable: true minimum: 0 token: @@ -1807,7 +1811,7 @@ components: description: |- The token to be stored on the proxy-side. - Required for "provision-token" and "provision-credentials" kinds. + Required for "provision-token", "provision-credentials" and "provision-connection-options" kinds. nullable: true PreflightOperationKind: type: string @@ -1818,6 +1822,7 @@ components: - get-recording-storage-health - provision-token - provision-credentials + - provision-connection-options - resolve-host PreflightOutput: type: object @@ -2105,6 +2110,16 @@ components: Url: type: string description: HTTP URL where notification messages are to be sent + TargetConnectionOptions: + type: object + properties: + krb_kdc: + type: string + description: |- + Kerberos KDC address for the target-side CredSSP connection. + + Supported schemes are `tcp` and `udp`. + nullable: true TrafficEventResponse: type: object required: diff --git a/devolutions-gateway/openapi/subscriber-api.yaml b/devolutions-gateway/openapi/subscriber-api.yaml index 28e9e798b..398f36702 100644 --- a/devolutions-gateway/openapi/subscriber-api.yaml +++ b/devolutions-gateway/openapi/subscriber-api.yaml @@ -7,7 +7,7 @@ info: email: infos@devolutions.net license: name: MIT/Apache-2.0 - version: 2026.1.2 + version: 2026.2.4 paths: /dgw/subscriber: post: diff --git a/devolutions-gateway/openapi/ts-angular-client/.openapi-generator/FILES b/devolutions-gateway/openapi/ts-angular-client/.openapi-generator/FILES index 2f0c2d88c..8a388ef76 100644 --- a/devolutions-gateway/openapi/ts-angular-client/.openapi-generator/FILES +++ b/devolutions-gateway/openapi/ts-angular-client/.openapi-generator/FILES @@ -34,7 +34,10 @@ model/connectionMode.ts model/dataEncoding.ts model/deleteManyResult.ts model/eventOutcomeResponse.ts +model/getUpdateProductsResponse.ts +model/getUpdateScheduleResponse.ts model/heartbeat.ts +model/hostScanStateDto.ts model/identity.ts model/interfaceInfo.ts model/jrlInfo.ts @@ -47,20 +50,32 @@ model/monitoringLogResponse.ts model/monitoringProbeType.ts model/monitoringProbeTypeOneOf.ts model/monitorsConfig.ts +model/networkInterfaceDto.ts +model/networkInterfacesResponse.ts +model/networkScanResultEventDto.ts +model/networkScanResultKindDto.ts +model/networkScanSourceCapabilitiesDto.ts +model/networkScanSourceDto.ts model/preflightAlertStatus.ts model/preflightOperation.ts model/preflightOperationKind.ts model/preflightOutput.ts model/preflightOutputKind.ts model/pubKeyFormat.ts +model/scanOriginDto.ts +model/scanResultSourceDto.ts model/sessionInfo.ts model/sessionTokenContentType.ts model/sessionTokenSignRequest.ts model/setConfigResponse.ts +model/setUpdateScheduleRequest.ts model/subProvisionerKey.ts model/subscriber.ts +model/targetConnectionOptions.ts model/trafficEventResponse.ts model/transportProtocolResponse.ts +model/updateProductInfo.ts +model/updateRequestSchema.ts ng-package.json package.json param.ts diff --git a/devolutions-gateway/openapi/ts-angular-client/README.md b/devolutions-gateway/openapi/ts-angular-client/README.md index b2c73d3a3..eb9e333c5 100644 --- a/devolutions-gateway/openapi/ts-angular-client/README.md +++ b/devolutions-gateway/openapi/ts-angular-client/README.md @@ -2,7 +2,7 @@ Protocol-aware fine-grained relay server -The version of the OpenAPI document: 2025.3.2 +The version of the OpenAPI document: 2026.2.4 ### Building diff --git a/devolutions-gateway/openapi/ts-angular-client/api/net.service.ts b/devolutions-gateway/openapi/ts-angular-client/api/net.service.ts index a4d2e8b7e..f34155875 100644 --- a/devolutions-gateway/openapi/ts-angular-client/api/net.service.ts +++ b/devolutions-gateway/openapi/ts-angular-client/api/net.service.ts @@ -18,6 +18,8 @@ import { Observable } from 'rxjs'; // @ts-ignore import { InterfaceInfo } from '../model/interfaceInfo'; +// @ts-ignore +import { NetworkInterfacesResponse } from '../model/networkInterfacesResponse'; // @ts-ignore import { BASE_PATH, COLLECTION_FORMATS } from '../variables'; @@ -157,4 +159,309 @@ export class NetService { ); } + /** + * Lists Gateway network scan sources. + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public getNetInterfaces(observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable; + public getNetInterfaces(observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public getNetInterfaces(observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public getNetInterfaces(observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable { + + let localVarHeaders = this.defaultHeaders; + + let localVarCredential: string | undefined; + // authentication (netscan_token) required + localVarCredential = this.configuration.lookupCredential('netscan_token'); + if (localVarCredential) { + localVarHeaders = localVarHeaders.set('Authorization', 'Bearer ' + localVarCredential); + } + + let localVarHttpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept; + if (localVarHttpHeaderAcceptSelected === undefined) { + // to determine the Accept header + const httpHeaderAccepts: string[] = [ + 'application/json' + ]; + localVarHttpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts); + } + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + let localVarHttpContext: HttpContext | undefined = options && options.context; + if (localVarHttpContext === undefined) { + localVarHttpContext = new HttpContext(); + } + + let localVarTransferCache: boolean | undefined = options && options.transferCache; + if (localVarTransferCache === undefined) { + localVarTransferCache = true; + } + + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/jet/net/interfaces`; + return this.httpClient.request('get', `${this.configuration.basePath}${localVarPath}`, + { + context: localVarHttpContext, + responseType: responseType_, + withCredentials: this.configuration.withCredentials, + headers: localVarHeaders, + observe: observe, + transferCache: localVarTransferCache, + reportProgress: reportProgress + } + ); + } + + /** + * Stream network scan events over a websocket. + * The endpoint is upgraded from HTTP, so OpenAPI describes the **handshake**: the query parameters that drive the scan (validated before upgrade) and the legacy / v1 event payloads streamed back as JSON text frames. See `NetworkScanResultEvent` for the v1 shape and `LegacyScanEvent` for the legacy shape (selected via `response_format`). + * @param pingInterval Interval in milliseconds (default is 200) + * @param pingTimeout Timeout in milliseconds (default is 500) + * @param broadcastTimeout Timeout in milliseconds (default is 1000) + * @param portScanTimeout Timeout in milliseconds (default is 1000) + * @param netbiosTimeout Timeout in milliseconds (default is 1000) + * @param netbiosInterval Interval in milliseconds (default is 200) + * @param mdnsQueryTimeout The maximum time for each mdns query in milliseconds. (default is 5 * 1000) + * @param maxWait The maximum duration for whole networking scan in milliseconds. Highly suggested! + * @param range The start and end IP address of the range to scan. for example: 10.10.0.0-10.10.0.255 + * @param target Explicit single-host targets to scan. Each value must parse as an IPv4 or IPv6 address; invalid values yield a structured `{ error: \"invalid_target\", value: \"<raw>\" }` 400 rather than a generic serde rejection at extraction time (mirrors the `range=` / `probe=` validation path). + * @param interfaceId Gateway network interface IDs to use as scan sources. + * @param probe The probes to run. Each value is either `ping`, a port number (`22`), or a named service (`rdp`, `https`, …). Validation is deferred to scan-time so failures can be surfaced as a structured 400 — naming the offending value — instead of a generic serde rejection at extraction time. + * @param enablePingStart **Legacy alias** for `report_ping_start`. Prefer the explicit name in new clients; kept so existing consumers don\'t break. + * @param enableBroadcast Enable the execution of broadcast scan + * @param enableSubnetScan Enable the ping scan on subnet + * @param enableZeroconf Enable ZeroConf/mDNS + * @param enableNetbios Enable NetBIOS name-service queries. Default `true` for backward compatibility. Set `false` (or pair with explicit `target=`) to keep NetBIOS from sweeping the surrounding subnet when the caller only wants results for the targets they listed. + * @param enableResolveDns Enable resolve dns + * @param includeHostResults Include host-only results. + * @param reportPingStart Emit ping queued/start host results. + * @param reportPingSuccess Emit ping success host results. + * @param reportPingFailure Emit ping failure host results. + * @param enableTcpProbes Enable TCP service probes. + * @param rangeInterfacePolicy Policy applied when `range=` and `interface_id=` are both provided. Accepted values: `intersect_selected_interfaces` (default) or `allow_cross_interface_range`. Invalid values yield a structured `{ error: \"invalid_range_interface_policy\", value: \"<raw>\" }` 400 instead of a generic serde rejection (mirrors the `range=` / `probe=` / `target=` validation path). + * @param allowCrossInterfaceRange **Legacy alias** for `range_interface_policy=allow_cross_interface_range`. Prefer the structured policy in new clients. + * @param responseFormat Response shape emitted on the websocket. Accepted values: `legacy` (default) or `network_scan_result_v1`. Invalid values yield a structured `{ error: \"invalid_response_format\", value: \"<raw>\" }` 400 instead of a generic serde rejection. + * @param maxConcurrency Maximum scanner concurrency. + * @param maxPingConcurrency Maximum ping probe concurrency. + * @param maxTcpProbeConcurrency Maximum TCP probe concurrency. + * @param enableFailure **Legacy alias** for `report_ping_failure`. `enable_failure=true` only opts into ping-failure events; TCP-probe failure events require the explicit `report_tcp_failure=true` and are not affected by this alias. **Behavior change:** historically this single flag controlled both ping-failure and TCP-probe-failure reporting. The two are now split: clients that want the old \"both at once\" semantics must send `enable_failure=true&report_tcp_failure=true` together. The split is intentional — TCP-probe failures are typically high-volume noise that callers were filtering client-side anyway, so the two streams are independently gated. + * @param reportTcpFailure Enable TCP port knocking failure events. + * @param interfaceBindStrict When `true`, fail with HTTP 400 if a ping/TCP-probe socket cannot be bound to the planner-selected interface. Default `false` (warn and fall back to default routing). + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public getNetScan(pingInterval?: number, pingTimeout?: number, broadcastTimeout?: number, portScanTimeout?: number, netbiosTimeout?: number, netbiosInterval?: number, mdnsQueryTimeout?: number, maxWait?: number, range?: Array, target?: Array, interfaceId?: Array, probe?: Array, enablePingStart?: boolean, enableBroadcast?: boolean, enableSubnetScan?: boolean, enableZeroconf?: boolean, enableNetbios?: boolean, enableResolveDns?: boolean, includeHostResults?: boolean, reportPingStart?: boolean, reportPingSuccess?: boolean, reportPingFailure?: boolean, enableTcpProbes?: boolean, rangeInterfacePolicy?: string, allowCrossInterfaceRange?: boolean, responseFormat?: string, maxConcurrency?: number, maxPingConcurrency?: number, maxTcpProbeConcurrency?: number, enableFailure?: boolean, reportTcpFailure?: boolean, interfaceBindStrict?: boolean, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: undefined, context?: HttpContext, transferCache?: boolean}): Observable; + public getNetScan(pingInterval?: number, pingTimeout?: number, broadcastTimeout?: number, portScanTimeout?: number, netbiosTimeout?: number, netbiosInterval?: number, mdnsQueryTimeout?: number, maxWait?: number, range?: Array, target?: Array, interfaceId?: Array, probe?: Array, enablePingStart?: boolean, enableBroadcast?: boolean, enableSubnetScan?: boolean, enableZeroconf?: boolean, enableNetbios?: boolean, enableResolveDns?: boolean, includeHostResults?: boolean, reportPingStart?: boolean, reportPingSuccess?: boolean, reportPingFailure?: boolean, enableTcpProbes?: boolean, rangeInterfacePolicy?: string, allowCrossInterfaceRange?: boolean, responseFormat?: string, maxConcurrency?: number, maxPingConcurrency?: number, maxTcpProbeConcurrency?: number, enableFailure?: boolean, reportTcpFailure?: boolean, interfaceBindStrict?: boolean, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: undefined, context?: HttpContext, transferCache?: boolean}): Observable>; + public getNetScan(pingInterval?: number, pingTimeout?: number, broadcastTimeout?: number, portScanTimeout?: number, netbiosTimeout?: number, netbiosInterval?: number, mdnsQueryTimeout?: number, maxWait?: number, range?: Array, target?: Array, interfaceId?: Array, probe?: Array, enablePingStart?: boolean, enableBroadcast?: boolean, enableSubnetScan?: boolean, enableZeroconf?: boolean, enableNetbios?: boolean, enableResolveDns?: boolean, includeHostResults?: boolean, reportPingStart?: boolean, reportPingSuccess?: boolean, reportPingFailure?: boolean, enableTcpProbes?: boolean, rangeInterfacePolicy?: string, allowCrossInterfaceRange?: boolean, responseFormat?: string, maxConcurrency?: number, maxPingConcurrency?: number, maxTcpProbeConcurrency?: number, enableFailure?: boolean, reportTcpFailure?: boolean, interfaceBindStrict?: boolean, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: undefined, context?: HttpContext, transferCache?: boolean}): Observable>; + public getNetScan(pingInterval?: number, pingTimeout?: number, broadcastTimeout?: number, portScanTimeout?: number, netbiosTimeout?: number, netbiosInterval?: number, mdnsQueryTimeout?: number, maxWait?: number, range?: Array, target?: Array, interfaceId?: Array, probe?: Array, enablePingStart?: boolean, enableBroadcast?: boolean, enableSubnetScan?: boolean, enableZeroconf?: boolean, enableNetbios?: boolean, enableResolveDns?: boolean, includeHostResults?: boolean, reportPingStart?: boolean, reportPingSuccess?: boolean, reportPingFailure?: boolean, enableTcpProbes?: boolean, rangeInterfacePolicy?: string, allowCrossInterfaceRange?: boolean, responseFormat?: string, maxConcurrency?: number, maxPingConcurrency?: number, maxTcpProbeConcurrency?: number, enableFailure?: boolean, reportTcpFailure?: boolean, interfaceBindStrict?: boolean, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: undefined, context?: HttpContext, transferCache?: boolean}): Observable { + + let localVarQueryParameters = new HttpParams({encoder: this.encoder}); + if (pingInterval !== undefined && pingInterval !== null) { + localVarQueryParameters = this.addToHttpParams(localVarQueryParameters, + pingInterval, 'ping_interval'); + } + if (pingTimeout !== undefined && pingTimeout !== null) { + localVarQueryParameters = this.addToHttpParams(localVarQueryParameters, + pingTimeout, 'ping_timeout'); + } + if (broadcastTimeout !== undefined && broadcastTimeout !== null) { + localVarQueryParameters = this.addToHttpParams(localVarQueryParameters, + broadcastTimeout, 'broadcast_timeout'); + } + if (portScanTimeout !== undefined && portScanTimeout !== null) { + localVarQueryParameters = this.addToHttpParams(localVarQueryParameters, + portScanTimeout, 'port_scan_timeout'); + } + if (netbiosTimeout !== undefined && netbiosTimeout !== null) { + localVarQueryParameters = this.addToHttpParams(localVarQueryParameters, + netbiosTimeout, 'netbios_timeout'); + } + if (netbiosInterval !== undefined && netbiosInterval !== null) { + localVarQueryParameters = this.addToHttpParams(localVarQueryParameters, + netbiosInterval, 'netbios_interval'); + } + if (mdnsQueryTimeout !== undefined && mdnsQueryTimeout !== null) { + localVarQueryParameters = this.addToHttpParams(localVarQueryParameters, + mdnsQueryTimeout, 'mdns_query_timeout'); + } + if (maxWait !== undefined && maxWait !== null) { + localVarQueryParameters = this.addToHttpParams(localVarQueryParameters, + maxWait, 'max_wait'); + } + if (range) { + range.forEach((element) => { + localVarQueryParameters = this.addToHttpParams(localVarQueryParameters, + element, 'range'); + }) + } + if (target) { + target.forEach((element) => { + localVarQueryParameters = this.addToHttpParams(localVarQueryParameters, + element, 'target'); + }) + } + if (interfaceId) { + interfaceId.forEach((element) => { + localVarQueryParameters = this.addToHttpParams(localVarQueryParameters, + element, 'interface_id'); + }) + } + if (probe) { + probe.forEach((element) => { + localVarQueryParameters = this.addToHttpParams(localVarQueryParameters, + element, 'probe'); + }) + } + if (enablePingStart !== undefined && enablePingStart !== null) { + localVarQueryParameters = this.addToHttpParams(localVarQueryParameters, + enablePingStart, 'enable_ping_start'); + } + if (enableBroadcast !== undefined && enableBroadcast !== null) { + localVarQueryParameters = this.addToHttpParams(localVarQueryParameters, + enableBroadcast, 'enable_broadcast'); + } + if (enableSubnetScan !== undefined && enableSubnetScan !== null) { + localVarQueryParameters = this.addToHttpParams(localVarQueryParameters, + enableSubnetScan, 'enable_subnet_scan'); + } + if (enableZeroconf !== undefined && enableZeroconf !== null) { + localVarQueryParameters = this.addToHttpParams(localVarQueryParameters, + enableZeroconf, 'enable_zeroconf'); + } + if (enableNetbios !== undefined && enableNetbios !== null) { + localVarQueryParameters = this.addToHttpParams(localVarQueryParameters, + enableNetbios, 'enable_netbios'); + } + if (enableResolveDns !== undefined && enableResolveDns !== null) { + localVarQueryParameters = this.addToHttpParams(localVarQueryParameters, + enableResolveDns, 'enable_resolve_dns'); + } + if (includeHostResults !== undefined && includeHostResults !== null) { + localVarQueryParameters = this.addToHttpParams(localVarQueryParameters, + includeHostResults, 'include_host_results'); + } + if (reportPingStart !== undefined && reportPingStart !== null) { + localVarQueryParameters = this.addToHttpParams(localVarQueryParameters, + reportPingStart, 'report_ping_start'); + } + if (reportPingSuccess !== undefined && reportPingSuccess !== null) { + localVarQueryParameters = this.addToHttpParams(localVarQueryParameters, + reportPingSuccess, 'report_ping_success'); + } + if (reportPingFailure !== undefined && reportPingFailure !== null) { + localVarQueryParameters = this.addToHttpParams(localVarQueryParameters, + reportPingFailure, 'report_ping_failure'); + } + if (enableTcpProbes !== undefined && enableTcpProbes !== null) { + localVarQueryParameters = this.addToHttpParams(localVarQueryParameters, + enableTcpProbes, 'enable_tcp_probes'); + } + if (rangeInterfacePolicy !== undefined && rangeInterfacePolicy !== null) { + localVarQueryParameters = this.addToHttpParams(localVarQueryParameters, + rangeInterfacePolicy, 'range_interface_policy'); + } + if (allowCrossInterfaceRange !== undefined && allowCrossInterfaceRange !== null) { + localVarQueryParameters = this.addToHttpParams(localVarQueryParameters, + allowCrossInterfaceRange, 'allow_cross_interface_range'); + } + if (responseFormat !== undefined && responseFormat !== null) { + localVarQueryParameters = this.addToHttpParams(localVarQueryParameters, + responseFormat, 'response_format'); + } + if (maxConcurrency !== undefined && maxConcurrency !== null) { + localVarQueryParameters = this.addToHttpParams(localVarQueryParameters, + maxConcurrency, 'max_concurrency'); + } + if (maxPingConcurrency !== undefined && maxPingConcurrency !== null) { + localVarQueryParameters = this.addToHttpParams(localVarQueryParameters, + maxPingConcurrency, 'max_ping_concurrency'); + } + if (maxTcpProbeConcurrency !== undefined && maxTcpProbeConcurrency !== null) { + localVarQueryParameters = this.addToHttpParams(localVarQueryParameters, + maxTcpProbeConcurrency, 'max_tcp_probe_concurrency'); + } + if (enableFailure !== undefined && enableFailure !== null) { + localVarQueryParameters = this.addToHttpParams(localVarQueryParameters, + enableFailure, 'enable_failure'); + } + if (reportTcpFailure !== undefined && reportTcpFailure !== null) { + localVarQueryParameters = this.addToHttpParams(localVarQueryParameters, + reportTcpFailure, 'report_tcp_failure'); + } + if (interfaceBindStrict !== undefined && interfaceBindStrict !== null) { + localVarQueryParameters = this.addToHttpParams(localVarQueryParameters, + interfaceBindStrict, 'interface_bind_strict'); + } + + let localVarHeaders = this.defaultHeaders; + + let localVarCredential: string | undefined; + // authentication (netscan_token) required + localVarCredential = this.configuration.lookupCredential('netscan_token'); + if (localVarCredential) { + localVarHeaders = localVarHeaders.set('Authorization', 'Bearer ' + localVarCredential); + } + + let localVarHttpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept; + if (localVarHttpHeaderAcceptSelected === undefined) { + // to determine the Accept header + const httpHeaderAccepts: string[] = [ + ]; + localVarHttpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts); + } + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + let localVarHttpContext: HttpContext | undefined = options && options.context; + if (localVarHttpContext === undefined) { + localVarHttpContext = new HttpContext(); + } + + let localVarTransferCache: boolean | undefined = options && options.transferCache; + if (localVarTransferCache === undefined) { + localVarTransferCache = true; + } + + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/jet/net/scan`; + return this.httpClient.request('get', `${this.configuration.basePath}${localVarPath}`, + { + context: localVarHttpContext, + params: localVarQueryParameters, + responseType: responseType_, + withCredentials: this.configuration.withCredentials, + headers: localVarHeaders, + observe: observe, + transferCache: localVarTransferCache, + reportProgress: reportProgress + } + ); + } + } diff --git a/devolutions-gateway/openapi/ts-angular-client/api/update.service.ts b/devolutions-gateway/openapi/ts-angular-client/api/update.service.ts index 996357fd7..446252e56 100644 --- a/devolutions-gateway/openapi/ts-angular-client/api/update.service.ts +++ b/devolutions-gateway/openapi/ts-angular-client/api/update.service.ts @@ -16,6 +16,14 @@ import { HttpClient, HttpHeaders, HttpParams, import { CustomHttpParameterCodec } from '../encoder'; import { Observable } from 'rxjs'; +// @ts-ignore +import { GetUpdateProductsResponse } from '../model/getUpdateProductsResponse'; +// @ts-ignore +import { GetUpdateScheduleResponse } from '../model/getUpdateScheduleResponse'; +// @ts-ignore +import { SetUpdateScheduleRequest } from '../model/setUpdateScheduleRequest'; +// @ts-ignore +import { UpdateRequestSchema } from '../model/updateRequestSchema'; // @ts-ignore import { BASE_PATH, COLLECTION_FORMATS } from '../variables'; @@ -89,20 +97,236 @@ export class UpdateService { } /** - * Triggers Devolutions Gateway update process. - * This is done via updating `Agent/update.json` file, which is then read by Devolutions Agent when changes are detected. If the version written to `update.json` is indeed higher than the currently installed version, Devolutions Agent will proceed with the update process. - * @param version The version to install; use \'latest\' for the latest version, or \'w.x.y.z\' for a specific version + * Retrieve the currently installed version of each Devolutions product. + * Reads `update_status.json`, which is written by the Devolutions Agent on startup and refreshed after every update run. When the file does not exist (agent not installed or is an older version), the endpoint returns HTTP 503 because updater status is unavailable. + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public getUpdateProducts(observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable; + public getUpdateProducts(observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public getUpdateProducts(observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public getUpdateProducts(observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable { + + let localVarHeaders = this.defaultHeaders; + + let localVarCredential: string | undefined; + // authentication (scope_token) required + localVarCredential = this.configuration.lookupCredential('scope_token'); + if (localVarCredential) { + localVarHeaders = localVarHeaders.set('Authorization', 'Bearer ' + localVarCredential); + } + + let localVarHttpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept; + if (localVarHttpHeaderAcceptSelected === undefined) { + // to determine the Accept header + const httpHeaderAccepts: string[] = [ + 'application/json' + ]; + localVarHttpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts); + } + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + let localVarHttpContext: HttpContext | undefined = options && options.context; + if (localVarHttpContext === undefined) { + localVarHttpContext = new HttpContext(); + } + + let localVarTransferCache: boolean | undefined = options && options.transferCache; + if (localVarTransferCache === undefined) { + localVarTransferCache = true; + } + + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/jet/update`; + return this.httpClient.request('get', `${this.configuration.basePath}${localVarPath}`, + { + context: localVarHttpContext, + responseType: responseType_, + withCredentials: this.configuration.withCredentials, + headers: localVarHeaders, + observe: observe, + transferCache: localVarTransferCache, + reportProgress: reportProgress + } + ); + } + + /** + * Retrieve the current Devolutions Agent auto-update schedule. + * Reads the `Schedule` field from `update_status.json`. When the field is absent the response contains zeroed defaults (`Enabled: false`, interval `0`, window start `0`, no products). + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public getUpdateSchedule(observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable; + public getUpdateSchedule(observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public getUpdateSchedule(observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public getUpdateSchedule(observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable { + + let localVarHeaders = this.defaultHeaders; + + let localVarCredential: string | undefined; + // authentication (scope_token) required + localVarCredential = this.configuration.lookupCredential('scope_token'); + if (localVarCredential) { + localVarHeaders = localVarHeaders.set('Authorization', 'Bearer ' + localVarCredential); + } + + let localVarHttpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept; + if (localVarHttpHeaderAcceptSelected === undefined) { + // to determine the Accept header + const httpHeaderAccepts: string[] = [ + 'application/json' + ]; + localVarHttpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts); + } + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + let localVarHttpContext: HttpContext | undefined = options && options.context; + if (localVarHttpContext === undefined) { + localVarHttpContext = new HttpContext(); + } + + let localVarTransferCache: boolean | undefined = options && options.transferCache; + if (localVarTransferCache === undefined) { + localVarTransferCache = true; + } + + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/jet/update/schedule`; + return this.httpClient.request('get', `${this.configuration.basePath}${localVarPath}`, + { + context: localVarHttpContext, + responseType: responseType_, + withCredentials: this.configuration.withCredentials, + headers: localVarHeaders, + observe: observe, + transferCache: localVarTransferCache, + reportProgress: reportProgress + } + ); + } + + /** + * Set the Devolutions Agent auto-update schedule. + * Writes the `Schedule` field into `update.json`. The agent watches this file and applies the new schedule immediately, then persists it to `agent.json`. All other fields in `update.json` are preserved; the `VersionMinor` field is reset to the minor version this gateway build understands so the agent does not see an unknown future version. + * @param setUpdateScheduleRequest * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. * @param reportProgress flag to report request and response progress. */ - public triggerUpdate(version: string, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable; - public triggerUpdate(version: string, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; - public triggerUpdate(version: string, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; - public triggerUpdate(version: string, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable { - if (version === null || version === undefined) { - throw new Error('Required parameter version was null or undefined when calling triggerUpdate.'); + public setUpdateSchedule(setUpdateScheduleRequest: SetUpdateScheduleRequest, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable; + public setUpdateSchedule(setUpdateScheduleRequest: SetUpdateScheduleRequest, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public setUpdateSchedule(setUpdateScheduleRequest: SetUpdateScheduleRequest, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public setUpdateSchedule(setUpdateScheduleRequest: SetUpdateScheduleRequest, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable { + if (setUpdateScheduleRequest === null || setUpdateScheduleRequest === undefined) { + throw new Error('Required parameter setUpdateScheduleRequest was null or undefined when calling setUpdateSchedule.'); + } + + let localVarHeaders = this.defaultHeaders; + + let localVarCredential: string | undefined; + // authentication (scope_token) required + localVarCredential = this.configuration.lookupCredential('scope_token'); + if (localVarCredential) { + localVarHeaders = localVarHeaders.set('Authorization', 'Bearer ' + localVarCredential); } + let localVarHttpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept; + if (localVarHttpHeaderAcceptSelected === undefined) { + // to determine the Accept header + const httpHeaderAccepts: string[] = [ + 'application/json' + ]; + localVarHttpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts); + } + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + let localVarHttpContext: HttpContext | undefined = options && options.context; + if (localVarHttpContext === undefined) { + localVarHttpContext = new HttpContext(); + } + + let localVarTransferCache: boolean | undefined = options && options.transferCache; + if (localVarTransferCache === undefined) { + localVarTransferCache = true; + } + + + // to determine the Content-Type header + const consumes: string[] = [ + 'application/json' + ]; + const httpContentTypeSelected: string | undefined = this.configuration.selectHeaderContentType(consumes); + if (httpContentTypeSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Content-Type', httpContentTypeSelected); + } + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/jet/update/schedule`; + return this.httpClient.request('post', `${this.configuration.basePath}${localVarPath}`, + { + context: localVarHttpContext, + body: setUpdateScheduleRequest, + responseType: responseType_, + withCredentials: this.configuration.withCredentials, + headers: localVarHeaders, + observe: observe, + transferCache: localVarTransferCache, + reportProgress: reportProgress + } + ); + } + + /** + * Trigger an update for one or more Devolutions products. + * Writes the requested version(s) into `Agent/update.json`, which is watched by Devolutions Agent. When a requested version is higher than the installed version the agent proceeds with the update. **Body form** (preferred): pass a JSON body with a `Products` map. **Query-param form** (legacy, gateway-only): `POST /jet/update?version=latest`. This form updates only the Gateway product and is kept for backward compatibility. Both forms cannot be used simultaneously; doing so returns HTTP 400. + * @param version Gateway-only target version; use the request body for multi-product updates + * @param updateRequestSchema Products and target versions to update + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public triggerUpdate(version?: string, updateRequestSchema?: UpdateRequestSchema, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable; + public triggerUpdate(version?: string, updateRequestSchema?: UpdateRequestSchema, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public triggerUpdate(version?: string, updateRequestSchema?: UpdateRequestSchema, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public triggerUpdate(version?: string, updateRequestSchema?: UpdateRequestSchema, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable { + let localVarQueryParameters = new HttpParams({encoder: this.encoder}); if (version !== undefined && version !== null) { localVarQueryParameters = this.addToHttpParams(localVarQueryParameters, @@ -141,6 +365,15 @@ export class UpdateService { } + // to determine the Content-Type header + const consumes: string[] = [ + 'application/json' + ]; + const httpContentTypeSelected: string | undefined = this.configuration.selectHeaderContentType(consumes); + if (httpContentTypeSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Content-Type', httpContentTypeSelected); + } + let responseType_: 'text' | 'json' | 'blob' = 'json'; if (localVarHttpHeaderAcceptSelected) { if (localVarHttpHeaderAcceptSelected.startsWith('text')) { @@ -156,6 +389,7 @@ export class UpdateService { return this.httpClient.request('post', `${this.configuration.basePath}${localVarPath}`, { context: localVarHttpContext, + body: updateRequestSchema, params: localVarQueryParameters, responseType: responseType_, withCredentials: this.configuration.withCredentials, diff --git a/devolutions-gateway/openapi/ts-angular-client/model/accessScope.ts b/devolutions-gateway/openapi/ts-angular-client/model/accessScope.ts index 982b4fd4a..0827c35b7 100644 --- a/devolutions-gateway/openapi/ts-angular-client/model/accessScope.ts +++ b/devolutions-gateway/openapi/ts-angular-client/model/accessScope.ts @@ -9,7 +9,7 @@ */ -export type AccessScope = '*' | 'gateway.sessions.read' | 'gateway.session.terminate' | 'gateway.associations.read' | 'gateway.diagnostics.read' | 'gateway.jrl.read' | 'gateway.config.write' | 'gateway.heartbeat.read' | 'gateway.recording.delete' | 'gateway.recordings.read' | 'gateway.update' | 'gateway.preflight' | 'gateway.traffic.claim' | 'gateway.traffic.ack' | 'gateway.net.monitor.config' | 'gateway.net.monitor.drain'; +export type AccessScope = '*' | 'gateway.sessions.read' | 'gateway.session.terminate' | 'gateway.associations.read' | 'gateway.diagnostics.read' | 'gateway.jrl.read' | 'gateway.config.write' | 'gateway.heartbeat.read' | 'gateway.recording.delete' | 'gateway.recordings.read' | 'gateway.update' | 'gateway.update.read' | 'gateway.preflight' | 'gateway.traffic.claim' | 'gateway.traffic.ack' | 'gateway.net.monitor.config' | 'gateway.net.monitor.drain' | 'gateway.agent.delete' | 'gateway.agent.read'; export const AccessScope = { Star: '*' as AccessScope, @@ -23,10 +23,13 @@ export const AccessScope = { GatewayRecordingDelete: 'gateway.recording.delete' as AccessScope, GatewayRecordingsRead: 'gateway.recordings.read' as AccessScope, GatewayUpdate: 'gateway.update' as AccessScope, + GatewayUpdateRead: 'gateway.update.read' as AccessScope, GatewayPreflight: 'gateway.preflight' as AccessScope, GatewayTrafficClaim: 'gateway.traffic.claim' as AccessScope, GatewayTrafficAck: 'gateway.traffic.ack' as AccessScope, GatewayNetMonitorConfig: 'gateway.net.monitor.config' as AccessScope, - GatewayNetMonitorDrain: 'gateway.net.monitor.drain' as AccessScope + GatewayNetMonitorDrain: 'gateway.net.monitor.drain' as AccessScope, + GatewayAgentDelete: 'gateway.agent.delete' as AccessScope, + GatewayAgentRead: 'gateway.agent.read' as AccessScope }; diff --git a/devolutions-gateway/openapi/ts-angular-client/model/getUpdateProductsResponse.ts b/devolutions-gateway/openapi/ts-angular-client/model/getUpdateProductsResponse.ts new file mode 100644 index 000000000..ee9be047e --- /dev/null +++ b/devolutions-gateway/openapi/ts-angular-client/model/getUpdateProductsResponse.ts @@ -0,0 +1,26 @@ +/** + * devolutions-gateway + * + * Contact: infos@devolutions.net + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +import { UpdateProductInfo } from './updateProductInfo'; + + +/** + * Installed version of each product, as reported by Devolutions Agent. + */ +export interface GetUpdateProductsResponse { + /** + * Version of the `update_status.json` format in `\"major.minor\"` form (e.g. `\"1.1\"`). + */ + ManifestVersion: string; + /** + * Map of product name to API-specific product info. + */ + Products?: { [key: string]: UpdateProductInfo; }; +} + diff --git a/devolutions-gateway/openapi/ts-angular-client/model/getUpdateScheduleResponse.ts b/devolutions-gateway/openapi/ts-angular-client/model/getUpdateScheduleResponse.ts new file mode 100644 index 000000000..e83fc6ebf --- /dev/null +++ b/devolutions-gateway/openapi/ts-angular-client/model/getUpdateScheduleResponse.ts @@ -0,0 +1,41 @@ +/** + * devolutions-gateway + * + * Contact: infos@devolutions.net + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +/** + * Current auto-update schedule for Devolutions Agent. + */ +export interface GetUpdateScheduleResponse { + /** + * Enable periodic Devolutions Agent self-update checks. + */ + Enabled: boolean; + /** + * Minimum interval between auto-update checks, in seconds. `0` means check once at `UpdateWindowStart`. + */ + Interval: number; + /** + * Version of the `update_status.json` format in `\"major.minor\"` form (e.g. `\"1.1\"`). + */ + ManifestVersion: string; + /** + * Products the agent autonomously polls for new versions. + */ + Products?: Array; + /** + * End of the maintenance window as seconds past midnight (local time, exclusive). `None` means no upper bound (single check at `UpdateWindowStart`). + */ + UpdateWindowEnd?: number | null; + /** + * Start of the maintenance window as seconds past midnight (local time). + */ + UpdateWindowStart: number; +} + diff --git a/devolutions-gateway/openapi/ts-angular-client/model/hostScanStateDto.ts b/devolutions-gateway/openapi/ts-angular-client/model/hostScanStateDto.ts new file mode 100644 index 000000000..5e168fb29 --- /dev/null +++ b/devolutions-gateway/openapi/ts-angular-client/model/hostScanStateDto.ts @@ -0,0 +1,20 @@ +/** + * devolutions-gateway + * + * Contact: infos@devolutions.net + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +export type HostScanStateDto = 'queued' | 'probing' | 'reachable' | 'unreachable'; + +export const HostScanStateDto = { + Queued: 'queued' as HostScanStateDto, + Probing: 'probing' as HostScanStateDto, + Reachable: 'reachable' as HostScanStateDto, + Unreachable: 'unreachable' as HostScanStateDto +}; + diff --git a/devolutions-gateway/openapi/ts-angular-client/model/models.ts b/devolutions-gateway/openapi/ts-angular-client/model/models.ts index 652b6b61f..f7c62b504 100644 --- a/devolutions-gateway/openapi/ts-angular-client/model/models.ts +++ b/devolutions-gateway/openapi/ts-angular-client/model/models.ts @@ -14,7 +14,10 @@ export * from './connectionMode'; export * from './dataEncoding'; export * from './deleteManyResult'; export * from './eventOutcomeResponse'; +export * from './getUpdateProductsResponse'; +export * from './getUpdateScheduleResponse'; export * from './heartbeat'; +export * from './hostScanStateDto'; export * from './identity'; export * from './interfaceInfo'; export * from './jrlInfo'; @@ -26,17 +29,29 @@ export * from './monitoringLogResponse'; export * from './monitoringProbeType'; export * from './monitoringProbeTypeOneOf'; export * from './monitorsConfig'; +export * from './networkInterfaceDto'; +export * from './networkInterfacesResponse'; +export * from './networkScanResultEventDto'; +export * from './networkScanResultKindDto'; +export * from './networkScanSourceCapabilitiesDto'; +export * from './networkScanSourceDto'; export * from './preflightAlertStatus'; export * from './preflightOperation'; export * from './preflightOperationKind'; export * from './preflightOutput'; export * from './preflightOutputKind'; export * from './pubKeyFormat'; +export * from './scanOriginDto'; +export * from './scanResultSourceDto'; export * from './sessionInfo'; export * from './sessionTokenContentType'; export * from './sessionTokenSignRequest'; export * from './setConfigResponse'; +export * from './setUpdateScheduleRequest'; export * from './subProvisionerKey'; export * from './subscriber'; +export * from './targetConnectionOptions'; export * from './trafficEventResponse'; export * from './transportProtocolResponse'; +export * from './updateProductInfo'; +export * from './updateRequestSchema'; diff --git a/devolutions-gateway/openapi/ts-angular-client/model/networkInterfaceDto.ts b/devolutions-gateway/openapi/ts-angular-client/model/networkInterfaceDto.ts new file mode 100644 index 000000000..aa43757eb --- /dev/null +++ b/devolutions-gateway/openapi/ts-angular-client/model/networkInterfaceDto.ts @@ -0,0 +1,32 @@ +/** + * devolutions-gateway + * + * Contact: infos@devolutions.net + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +export interface NetworkInterfaceDto { + description?: string | null; + id: string; + index?: number | null; + isUp?: boolean | null; + /** + * Coarse link type: `ethernet`, `wifi`, `loopback`, `virtual`, `unknown`. + */ + linkType?: string | null; + macAddress?: string | null; + /** + * MTU in bytes when known. + */ + mtu?: number | null; + name: string; + /** + * Link speed in megabits per second when reported by the OS. + */ + speedMbps?: number | null; +} + diff --git a/devolutions-gateway/openapi/ts-angular-client/model/networkInterfacesResponse.ts b/devolutions-gateway/openapi/ts-angular-client/model/networkInterfacesResponse.ts new file mode 100644 index 000000000..cb55fe021 --- /dev/null +++ b/devolutions-gateway/openapi/ts-angular-client/model/networkInterfacesResponse.ts @@ -0,0 +1,16 @@ +/** + * devolutions-gateway + * + * Contact: infos@devolutions.net + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +import { NetworkScanSourceDto } from './networkScanSourceDto'; + + +export interface NetworkInterfacesResponse { + interfaces: Array; +} + diff --git a/devolutions-gateway/openapi/ts-angular-client/model/networkScanResultEventDto.ts b/devolutions-gateway/openapi/ts-angular-client/model/networkScanResultEventDto.ts new file mode 100644 index 000000000..cd2796570 --- /dev/null +++ b/devolutions-gateway/openapi/ts-angular-client/model/networkScanResultEventDto.ts @@ -0,0 +1,35 @@ +/** + * devolutions-gateway + * + * Contact: infos@devolutions.net + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +import { HostScanStateDto } from './hostScanStateDto'; +import { ScanOriginDto } from './scanOriginDto'; +import { ScanResultSourceDto } from './scanResultSourceDto'; +import { NetworkScanResultKindDto } from './networkScanResultKindDto'; + + +export interface NetworkScanResultEventDto { + address: string; + discoverySource: ScanResultSourceDto; + hostName?: string | null; + hostScanState?: HostScanStateDto | null; + interfaceId?: string | null; + interfaceName?: string | null; + isReachable?: boolean | null; + kind: NetworkScanResultKindDto; + macAddress?: string | null; + port?: number | null; + responseTimeMs?: number | null; + serviceLabel?: string | null; + serviceType?: string | null; + source: ScanOriginDto; +} +export namespace NetworkScanResultEventDto { +} + + diff --git a/devolutions-gateway/openapi/ts-angular-client/model/networkScanResultKindDto.ts b/devolutions-gateway/openapi/ts-angular-client/model/networkScanResultKindDto.ts new file mode 100644 index 000000000..c81eecbf1 --- /dev/null +++ b/devolutions-gateway/openapi/ts-angular-client/model/networkScanResultKindDto.ts @@ -0,0 +1,18 @@ +/** + * devolutions-gateway + * + * Contact: infos@devolutions.net + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +export type NetworkScanResultKindDto = 'host' | 'service'; + +export const NetworkScanResultKindDto = { + Host: 'host' as NetworkScanResultKindDto, + Service: 'service' as NetworkScanResultKindDto +}; + diff --git a/devolutions-gateway/openapi/ts-angular-client/model/networkScanSourceCapabilitiesDto.ts b/devolutions-gateway/openapi/ts-angular-client/model/networkScanSourceCapabilitiesDto.ts new file mode 100644 index 000000000..60ece37ac --- /dev/null +++ b/devolutions-gateway/openapi/ts-angular-client/model/networkScanSourceCapabilitiesDto.ts @@ -0,0 +1,21 @@ +/** + * devolutions-gateway + * + * Contact: infos@devolutions.net + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +export interface NetworkScanSourceCapabilitiesDto { + broadcast: boolean; + dnsResolve: boolean; + ipv4: boolean; + ipv6: boolean; + subnet: boolean; + tcpProbe: boolean; + zeroConf: boolean; +} + diff --git a/devolutions-gateway/openapi/ts-angular-client/model/networkScanSourceDto.ts b/devolutions-gateway/openapi/ts-angular-client/model/networkScanSourceDto.ts new file mode 100644 index 000000000..934bc9f49 --- /dev/null +++ b/devolutions-gateway/openapi/ts-angular-client/model/networkScanSourceDto.ts @@ -0,0 +1,23 @@ +/** + * devolutions-gateway + * + * Contact: infos@devolutions.net + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +import { NetworkInterfaceDto } from './networkInterfaceDto'; +import { NetworkScanSourceCapabilitiesDto } from './networkScanSourceCapabilitiesDto'; + + +export interface NetworkScanSourceDto { + address: string; + broadcastAddress?: string | null; + capabilities: NetworkScanSourceCapabilitiesDto; + endAddress: string; + 'interface': NetworkInterfaceDto; + prefixLength?: number | null; + startAddress: string; +} + diff --git a/devolutions-gateway/openapi/ts-angular-client/model/preflightOperation.ts b/devolutions-gateway/openapi/ts-angular-client/model/preflightOperation.ts index 440e61c4a..a2b87f800 100644 --- a/devolutions-gateway/openapi/ts-angular-client/model/preflightOperation.ts +++ b/devolutions-gateway/openapi/ts-angular-client/model/preflightOperation.ts @@ -7,11 +7,13 @@ * https://openapi-generator.tech * Do not edit the class manually. */ +import { TargetConnectionOptions } from './targetConnectionOptions'; import { AppCredential } from './appCredential'; import { PreflightOperationKind } from './preflightOperationKind'; export interface PreflightOperation { + connection_options?: TargetConnectionOptions | null; /** * The hostname to perform DNS resolution on. Required for \"resolve-host\" kind. */ @@ -24,11 +26,11 @@ export interface PreflightOperation { proxy_credential?: AppCredential | null; target_credential?: AppCredential | null; /** - * Minimum persistance duration in seconds for the data provisioned via this operation. Optional parameter for \"provision-token\" and \"provision-credentials\" kinds. + * Minimum persistence duration in seconds for the data provisioned via this operation. Optional parameter for \"provision-token\", \"provision-credentials\" and \"provision-connection-options\" kinds. */ time_to_live?: number | null; /** - * The token to be stored on the proxy-side. Required for \"provision-token\" and \"provision-credentials\" kinds. + * The token to be stored on the proxy-side. Required for \"provision-token\", \"provision-credentials\" and \"provision-connection-options\" kinds. */ token?: string | null; } diff --git a/devolutions-gateway/openapi/ts-angular-client/model/preflightOperationKind.ts b/devolutions-gateway/openapi/ts-angular-client/model/preflightOperationKind.ts index 235e78a37..506dfbcde 100644 --- a/devolutions-gateway/openapi/ts-angular-client/model/preflightOperationKind.ts +++ b/devolutions-gateway/openapi/ts-angular-client/model/preflightOperationKind.ts @@ -9,7 +9,7 @@ */ -export type PreflightOperationKind = 'get-version' | 'get-agent-version' | 'get-running-session-count' | 'get-recording-storage-health' | 'provision-token' | 'provision-credentials' | 'resolve-host'; +export type PreflightOperationKind = 'get-version' | 'get-agent-version' | 'get-running-session-count' | 'get-recording-storage-health' | 'provision-token' | 'provision-credentials' | 'provision-connection-options' | 'resolve-host'; export const PreflightOperationKind = { GetVersion: 'get-version' as PreflightOperationKind, @@ -18,6 +18,7 @@ export const PreflightOperationKind = { GetRecordingStorageHealth: 'get-recording-storage-health' as PreflightOperationKind, ProvisionToken: 'provision-token' as PreflightOperationKind, ProvisionCredentials: 'provision-credentials' as PreflightOperationKind, + ProvisionConnectionOptions: 'provision-connection-options' as PreflightOperationKind, ResolveHost: 'resolve-host' as PreflightOperationKind }; diff --git a/devolutions-gateway/openapi/ts-angular-client/model/scanOriginDto.ts b/devolutions-gateway/openapi/ts-angular-client/model/scanOriginDto.ts new file mode 100644 index 000000000..79b6b8d4c --- /dev/null +++ b/devolutions-gateway/openapi/ts-angular-client/model/scanOriginDto.ts @@ -0,0 +1,17 @@ +/** + * devolutions-gateway + * + * Contact: infos@devolutions.net + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +export type ScanOriginDto = 'gateway'; + +export const ScanOriginDto = { + Gateway: 'gateway' as ScanOriginDto +}; + diff --git a/devolutions-gateway/openapi/ts-angular-client/model/scanResultSourceDto.ts b/devolutions-gateway/openapi/ts-angular-client/model/scanResultSourceDto.ts new file mode 100644 index 000000000..420ce5f56 --- /dev/null +++ b/devolutions-gateway/openapi/ts-angular-client/model/scanResultSourceDto.ts @@ -0,0 +1,21 @@ +/** + * devolutions-gateway + * + * Contact: infos@devolutions.net + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +export type ScanResultSourceDto = 'subnet' | 'broadcast' | 'tcp_probe' | 'gateway' | 'zero_conf'; + +export const ScanResultSourceDto = { + Subnet: 'subnet' as ScanResultSourceDto, + Broadcast: 'broadcast' as ScanResultSourceDto, + TcpProbe: 'tcp_probe' as ScanResultSourceDto, + Gateway: 'gateway' as ScanResultSourceDto, + ZeroConf: 'zero_conf' as ScanResultSourceDto +}; + diff --git a/devolutions-gateway/openapi/ts-angular-client/model/setUpdateScheduleRequest.ts b/devolutions-gateway/openapi/ts-angular-client/model/setUpdateScheduleRequest.ts new file mode 100644 index 000000000..49e2ffaa9 --- /dev/null +++ b/devolutions-gateway/openapi/ts-angular-client/model/setUpdateScheduleRequest.ts @@ -0,0 +1,37 @@ +/** + * devolutions-gateway + * + * Contact: infos@devolutions.net + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +/** + * Desired auto-update schedule to apply to Devolutions Agent. + */ +export interface SetUpdateScheduleRequest { + /** + * Enable periodic Devolutions Agent self-update checks. + */ + Enabled: boolean; + /** + * Minimum interval between auto-update checks, in seconds. `0` means check once at `UpdateWindowStart` (default). + */ + Interval?: number; + /** + * Products the agent autonomously polls for new versions (default: empty). + */ + Products?: Array; + /** + * End of the maintenance window as seconds past midnight in local time, exclusive. `null` (default) means no upper bound - a single check fires at `UpdateWindowStart`. When end < start the window crosses midnight. + */ + UpdateWindowEnd?: number | null; + /** + * Start of the maintenance window as seconds past midnight in local time (default: `7200` = 02:00). + */ + UpdateWindowStart?: number; +} + diff --git a/devolutions-gateway/openapi/ts-angular-client/model/targetConnectionOptions.ts b/devolutions-gateway/openapi/ts-angular-client/model/targetConnectionOptions.ts new file mode 100644 index 000000000..7596f7508 --- /dev/null +++ b/devolutions-gateway/openapi/ts-angular-client/model/targetConnectionOptions.ts @@ -0,0 +1,18 @@ +/** + * devolutions-gateway + * + * Contact: infos@devolutions.net + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +export interface TargetConnectionOptions { + /** + * Kerberos KDC address for the target-side CredSSP connection. Supported schemes are `tcp` and `udp`. + */ + krb_kdc?: string | null; +} + diff --git a/devolutions-gateway/openapi/ts-angular-client/model/updateProductInfo.ts b/devolutions-gateway/openapi/ts-angular-client/model/updateProductInfo.ts new file mode 100644 index 000000000..bde42e7b6 --- /dev/null +++ b/devolutions-gateway/openapi/ts-angular-client/model/updateProductInfo.ts @@ -0,0 +1,21 @@ +/** + * devolutions-gateway + * + * Contact: infos@devolutions.net + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +/** + * Per-product update information. + */ +export interface UpdateProductInfo { + /** + * Requested or installed version: `\"latest\"` or `\"YYYY.M.D\"` / `\"YYYY.M.D.R\"`. + */ + Version: string; +} + diff --git a/devolutions-gateway/openapi/ts-angular-client/model/updateRequestSchema.ts b/devolutions-gateway/openapi/ts-angular-client/model/updateRequestSchema.ts new file mode 100644 index 000000000..2994294f2 --- /dev/null +++ b/devolutions-gateway/openapi/ts-angular-client/model/updateRequestSchema.ts @@ -0,0 +1,22 @@ +/** + * devolutions-gateway + * + * Contact: infos@devolutions.net + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +import { UpdateProductInfo } from './updateProductInfo'; + + +/** + * OpenAPI schema for the update request body. The API accepts an object containing a `Products` map, whose keys are product names and whose values are update information. + */ +export interface UpdateRequestSchema { + /** + * Map of product name to update information. + */ + Products?: { [key: string]: UpdateProductInfo; }; +} + diff --git a/devolutions-gateway/src/api/kdc_proxy.rs b/devolutions-gateway/src/api/kdc_proxy.rs index 865fc369f..988cb83b4 100644 --- a/devolutions-gateway/src/api/kdc_proxy.rs +++ b/devolutions-gateway/src/api/kdc_proxy.rs @@ -5,10 +5,7 @@ use picky_krb::messages::KdcProxyMessage; use uuid::Uuid; use crate::DgwState; -use crate::credential_injection_kdc::{ - CredentialInjectionKdcInterception, CredentialInjectionKdcRequest, CredentialInjectionKdcResolveError, - kdc_proxy_message_realm, -}; +use crate::credential_injection::{SyntheticKdcInterception, kdc_proxy_message_realm}; use crate::extract::KdcToken; use crate::http::HttpError; use crate::kdc_connector::KdcConnector; @@ -22,7 +19,7 @@ pub fn make_router(state: DgwState) -> Router { async fn kdc_proxy( State(DgwState { conf_handle, - credentials, + synthetic_kdc_registry, agent_tunnel_handle, .. }): State, @@ -47,7 +44,9 @@ async fn kdc_proxy( KdcDestination::Inject { jti } => { enforce_credential_injection_enabled(jti, conf.debug.enable_unstable)?; - let kdc = credentials.kdc_for(jti).map_err(credential_injection_resolve_error)?; + let kdc = synthetic_kdc_registry + .get(jti) + .ok_or_else(|| HttpError::bad_request().msg("credential-injection state is not available"))?; debug!( jti = %kdc.jti(), @@ -55,16 +54,14 @@ async fn kdc_proxy( ); match kdc - .handle_kdc_proxy_request(CredentialInjectionKdcRequest::from_token(kdc_proxy_message)) + .handle_kdc_proxy_message(kdc_proxy_message) .map_err(HttpError::internal().err())? { - CredentialInjectionKdcInterception::Intercepted(reply) => Ok(reply), - CredentialInjectionKdcInterception::NotInjectionRealm(mismatch) => { - Err(HttpError::bad_request() - .with_msg("requested domain is not allowed") - .err()(mismatch)) - } - CredentialInjectionKdcInterception::NotInjectionRequest => { + SyntheticKdcInterception::Intercepted(reply) => Ok(reply), + SyntheticKdcInterception::NotInjectionRealm(mismatch) => Err(HttpError::bad_request() + .with_msg("requested domain is not allowed") + .err()(mismatch)), + SyntheticKdcInterception::NotInjectionRequest => { Err(HttpError::internal().msg("credential-injection KDC did not handle the KDC proxy request")) } } @@ -92,17 +89,6 @@ async fn kdc_proxy( } } -fn credential_injection_resolve_error(error: CredentialInjectionKdcResolveError) -> HttpError { - match error { - CredentialInjectionKdcResolveError::BuildKdcConfig { .. } => HttpError::internal() - .with_msg("credential-injection KDC could not be initialized") - .build(error), - _ => HttpError::bad_request() - .with_msg("credential-injection state is not available") - .build(error), - } -} - // Forwards the request to the real KDC indicated by the token (or by the debug override) and // returns the response wrapped as a `KdcProxyMessage`. // diff --git a/devolutions-gateway/src/api/preflight.rs b/devolutions-gateway/src/api/preflight.rs index 95216cc25..0fb83e546 100644 --- a/devolutions-gateway/src/api/preflight.rs +++ b/devolutions-gateway/src/api/preflight.rs @@ -11,10 +11,9 @@ use uuid::Uuid; use crate::DgwState; use crate::config::Conf; -use crate::credential::InsertError; -use crate::credential_injection_kdc::CredentialService; use crate::extract::PreflightScope; use crate::http::HttpError; +use crate::provisioning::ProvisioningStore; use crate::session::SessionMessageSender; const OP_GET_VERSION: &str = "get-version"; @@ -23,6 +22,7 @@ const OP_GET_RUNNING_SESSION_COUNT: &str = "get-running-session-count"; const OP_GET_RECORDING_STORAGE_HEALTH: &str = "get-recording-storage-health"; const OP_PROVISION_TOKEN: &str = "provision-token"; const OP_PROVISION_CREDENTIALS: &str = "provision-credentials"; +const OP_PROVISION_CONNECTION_OPTIONS: &str = "provision-connection-options"; const OP_RESOLVE_HOST: &str = "resolve-host"; const DEFAULT_TTL: Duration = Duration::minutes(15); @@ -46,7 +46,14 @@ struct ProvisionTokenParams { struct ProvisionCredentialsParams { token: String, #[serde(flatten)] - mapping: crate::credential::CleartextAppCredentialMapping, + credentials: crate::credential::CleartextAppCredentials, + time_to_live: Option, +} + +#[derive(Debug, Deserialize)] +struct ProvisionConnectionOptionsParams { + token: String, + connection_options: crate::target_connection_options::TargetConnectionOptions, time_to_live: Option, } @@ -196,7 +203,7 @@ pub(super) async fn post_preflight( State(DgwState { conf_handle, sessions, - credentials, + provisioning, .. }): State, _scope: PreflightScope, @@ -223,13 +230,13 @@ pub(super) async fn post_preflight( let outputs = outputs.clone(); let conf = conf_handle.get_conf(); let sessions = sessions.clone(); - let credentials = credentials.clone(); + let provisioning = provisioning.clone(); async move { let operation_id = operation.id; trace!(%operation.id, "Process preflight operation"); - if let Err(error) = handle_operation(operation, &outputs, &conf, &sessions, &credentials).await { + if let Err(error) = handle_operation(operation, &outputs, &conf, &sessions, &provisioning).await { outputs.push(PreflightOutput { operation_id, kind: PreflightOutputKind::Alert { @@ -256,7 +263,7 @@ async fn handle_operation( outputs: &Outputs, conf: &Conf, sessions: &SessionMessageSender, - credentials: &CredentialService, + provisioning: &ProvisioningStore, ) -> Result<(), PreflightError> { match operation.kind.as_str() { OP_GET_VERSION => outputs.push(PreflightOutput { @@ -309,76 +316,94 @@ async fn handle_operation( }, }); } - OP_PROVISION_TOKEN | OP_PROVISION_CREDENTIALS => { - let is_provision_credentials = operation.kind.as_str() == OP_PROVISION_CREDENTIALS; - let (token, time_to_live, mapping) = if operation.kind.as_str() == OP_PROVISION_TOKEN { - let ProvisionTokenParams { token, time_to_live } = - from_params(operation.params).map_err(PreflightError::invalid_params)?; - (token, time_to_live, None) - } else { - let ProvisionCredentialsParams { - token, - mapping, - time_to_live, - } = from_params(operation.params).map_err(PreflightError::invalid_params)?; - (token, time_to_live, Some(mapping)) - }; - - let time_to_live = time_to_live - .map(i64::from) - .map(Duration::seconds) - .unwrap_or(DEFAULT_TTL); - - if time_to_live > MAX_TTL { - return Err(PreflightError { - status: PreflightAlertStatus::InvalidParams, - message: format!( - "provided time_to_live ({time_to_live}) is exceeding the maximum TTL duration ({MAX_TTL})" - ), - }); - } + OP_PROVISION_TOKEN => { + let ProvisionTokenParams { token, time_to_live } = + from_params(operation.params).map_err(PreflightError::invalid_params)?; + validate_time_to_live(time_to_live)?; + + // provision-token stores nothing: the current model has no credential-less entry, and a + // token-only entry never affected connection handling. It is kept only so older callers + // that still send it get a well-formed token validated and acknowledged. + crate::token::extract_jti(&token).map_err(|error| { + PreflightError::new(PreflightAlertStatus::InvalidParams, format!("invalid token: {error:#}")) + })?; - // Provision-credentials tokens must be valid association tokens with the credential - // injection shape (JTI + dst_hst + no dst_alt). Fail-fast at preflight so the request - // never reaches the credential store with malformed input. - if is_provision_credentials { - crate::token::validate_credential_injection_association_token(&token) - .inspect_err(|error| { - warn!( - %operation.id, - error = format!("{error:#}"), - "Credential-injection token is not valid" - ) - }) - .map_err(|error| { - PreflightError::new( - PreflightAlertStatus::InvalidParams, - format!("invalid credential-injection token: {error:#}"), - ) - })?; - } + outputs.push(PreflightOutput { + operation_id: operation.id, + kind: PreflightOutputKind::Ack, + }); + } + OP_PROVISION_CREDENTIALS => { + let ProvisionCredentialsParams { + token, + credentials, + time_to_live, + } = from_params(operation.params).map_err(PreflightError::invalid_params)?; + let time_to_live = validate_time_to_live(time_to_live)?; + + let token_data = crate::token::validate_credential_injection_association_token(&token) + .inspect_err(|error| { + warn!( + %operation.id, + error = format!("{error:#}"), + "Credential-injection token is not valid" + ) + }) + .map_err(|error| { + PreflightError::new( + PreflightAlertStatus::InvalidParams, + format!("invalid credential-injection token: {error:#}"), + ) + })?; - let previous_entry = credentials - .insert(token, mapping, time_to_live) + let replaced = provisioning + .insert_credentials(token_data, credentials, time_to_live) .inspect_err(|error| warn!(%operation.id, error = format!("{error:#}"), "Failed to insert credentials")) - .map_err(|error| match error { - InsertError::InvalidToken(error) => { - PreflightError::new(PreflightAlertStatus::InvalidParams, format!("invalid token: {error:#}")) - } - InsertError::Internal(_) => PreflightError::new( + .map_err(|_| { + PreflightError::new( PreflightAlertStatus::InternalServerError, "an internal error occurred".to_owned(), - ), + ) })?; - // `CredentialService::insert` already drops the cached Kerberos session for a - // replaced entry, so no explicit invalidation is needed here. - if previous_entry.is_some() { + if replaced { outputs.push(PreflightOutput { operation_id: operation.id, kind: PreflightOutputKind::Alert { status: PreflightAlertStatus::Info, - message: "an existing credential entry was replaced".to_owned(), + message: "existing provisioned credentials were replaced".to_owned(), + }, + }); + } + + outputs.push(PreflightOutput { + operation_id: operation.id, + kind: PreflightOutputKind::Ack, + }); + } + OP_PROVISION_CONNECTION_OPTIONS => { + let ProvisionConnectionOptionsParams { + token, + connection_options, + time_to_live, + } = from_params(operation.params).map_err(PreflightError::invalid_params)?; + let time_to_live = validate_time_to_live(time_to_live)?; + + // Connection options are generic routing metadata, not credential-injection state, so + // they only need a JTI to key by — not the full credential-injection token shape that + // provision-credentials requires. + let jti = crate::token::extract_jti(&token).map_err(|error| { + PreflightError::new(PreflightAlertStatus::InvalidParams, format!("invalid token: {error:#}")) + })?; + + let replaced = provisioning.insert_connection_options(jti, connection_options, time_to_live); + + if replaced { + outputs.push(PreflightOutput { + operation_id: operation.id, + kind: PreflightOutputKind::Alert { + status: PreflightAlertStatus::Info, + message: "existing provisioned connection options were replaced".to_owned(), }, }); } @@ -426,6 +451,22 @@ fn from_params(params: serde_json::Map) -> Result { + let time_to_live = time_to_live + .map(i64::from) + .map(Duration::seconds) + .unwrap_or(DEFAULT_TTL); + + if time_to_live > MAX_TTL { + return Err(PreflightError::new( + PreflightAlertStatus::InvalidParams, + format!("provided time_to_live ({time_to_live}) is exceeding the maximum TTL duration ({MAX_TTL})"), + )); + } + + Ok(time_to_live) +} + /// Redacts sensitive fields in JSON values. /// /// This function recursively traverses a JSON value and replaces any field diff --git a/devolutions-gateway/src/api/rdp.rs b/devolutions-gateway/src/api/rdp.rs index b3d45dbcb..5a93dcead 100644 --- a/devolutions-gateway/src/api/rdp.rs +++ b/devolutions-gateway/src/api/rdp.rs @@ -25,7 +25,8 @@ pub async fn handler( subscriber_tx, recordings, shutdown_signal, - credentials, + synthetic_kdc_registry, + provisioning, agent_tunnel_handle, .. }): State, @@ -46,7 +47,8 @@ pub async fn handler( subscriber_tx, recordings.active_recordings, source_addr, - credentials, + synthetic_kdc_registry, + provisioning, agent_tunnel_handle, ) .instrument(span) @@ -66,7 +68,8 @@ async fn handle_socket( subscriber_tx: SubscriberSender, active_recordings: Arc, source_addr: SocketAddr, - credentials: crate::credential_injection_kdc::CredentialService, + synthetic_kdc_registry: crate::credential_injection::SyntheticKdcRegistry, + provisioning: crate::provisioning::ProvisioningStore, agent_tunnel_handle: Option>, ) { let (stream, close_handle) = crate::ws::handle( @@ -84,7 +87,8 @@ async fn handle_socket( sessions, subscriber_tx, &active_recordings, - &credentials, + &synthetic_kdc_registry, + &provisioning, agent_tunnel_handle, ) .await; diff --git a/devolutions-gateway/src/credential/crypto.rs b/devolutions-gateway/src/credential/crypto.rs index 2f1a84d70..380d13ff6 100644 --- a/devolutions-gateway/src/credential/crypto.rs +++ b/devolutions-gateway/src/credential/crypto.rs @@ -1,6 +1,6 @@ //! In-memory credential encryption using ChaCha20-Poly1305. //! -//! This module provides encryption-at-rest for passwords stored in the credential store. +//! This module provides encryption-at-rest for passwords managed by the credential service. //! A randomly generated 256-bit master key is held in a [`ProtectedBytes<32>`] allocation backed by `secure-memory`, which applies //! the best available OS hardening (mlock, guard pages, core-dump exclusion) and always zeroizes on drop. //! diff --git a/devolutions-gateway/src/credential/mod.rs b/devolutions-gateway/src/credential/mod.rs index 166be9412..598be6b45 100644 --- a/devolutions-gateway/src/credential/mod.rs +++ b/devolutions-gateway/src/credential/mod.rs @@ -3,41 +3,10 @@ mod crypto; #[rustfmt::skip] pub use crypto::EncryptedPassword; -use std::collections::HashMap; -use std::fmt; -use std::sync::Arc; - -use anyhow::Context; -use async_trait::async_trait; -use devolutions_gateway_task::{ShutdownSignal, Task}; -use parking_lot::Mutex; use secrecy::ExposeSecret as _; -use uuid::Uuid; use self::crypto::MASTER_KEY; -/// Error returned by [`CredentialStoreHandle::insert`]. -#[derive(Debug)] -pub enum InsertError { - /// The provided token is invalid (e.g., missing or malformed JTI). - /// - /// This is a client-side error: the caller supplied bad input. - InvalidToken(anyhow::Error), - /// An internal error occurred (e.g., encryption failure). - Internal(anyhow::Error), -} - -impl fmt::Display for InsertError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::InvalidToken(e) => e.fmt(f), - Self::Internal(e) => e.fmt(f), - } - } -} - -impl std::error::Error for InsertError {} - /// Credential at the application protocol level #[derive(Debug, Clone)] pub enum AppCredential { @@ -61,17 +30,16 @@ impl AppCredential { } } -/// Application protocol level credential mapping +/// Application protocol level credentials. #[derive(Debug, Clone)] -pub struct AppCredentialMapping { +pub struct AppCredentials { pub proxy: AppCredential, pub target: AppCredential, } /// Cleartext credential received from the API, used for deserialization only. /// -/// Passwords are encrypted and stored as [`AppCredential`] inside the credential store. -/// This type is never stored directly — hand it to [`CredentialStoreHandle::insert`]. +/// Passwords are encrypted and stored as [`AppCredential`] by the credential service. #[derive(Debug, Deserialize)] #[serde(tag = "kind")] pub enum CleartextAppCredential { @@ -96,141 +64,20 @@ impl CleartextAppCredential { } } -/// Cleartext credential mapping received from the API, used for deserialization only. -/// -/// Passwords are encrypted on write. Hand this directly to [`CredentialStoreHandle::insert`]. +/// Cleartext credentials received from the API, used for deserialization only. #[derive(Debug, Deserialize)] -pub struct CleartextAppCredentialMapping { +pub struct CleartextAppCredentials { #[serde(rename = "proxy_credential")] pub proxy: CleartextAppCredential, #[serde(rename = "target_credential")] pub target: CleartextAppCredential, } -impl CleartextAppCredentialMapping { - fn encrypt(self) -> anyhow::Result { - Ok(AppCredentialMapping { +impl CleartextAppCredentials { + pub(crate) fn encrypt(self) -> anyhow::Result { + Ok(AppCredentials { proxy: self.proxy.encrypt()?, target: self.target.encrypt()?, }) } } - -#[derive(Debug, Clone)] -pub struct CredentialStoreHandle(Arc>); - -impl Default for CredentialStoreHandle { - fn default() -> Self { - Self::new() - } -} - -impl CredentialStoreHandle { - pub fn new() -> Self { - Self(Arc::new(Mutex::new(CredentialStore::new()))) - } - - pub fn insert( - &self, - token: String, - mapping: Option, - time_to_live: time::Duration, - ) -> Result, InsertError> { - let mapping = mapping - .map(CleartextAppCredentialMapping::encrypt) - .transpose() - .map_err(InsertError::Internal)?; - self.0.lock().insert(token, mapping, time_to_live) - } - - pub fn get(&self, token_id: Uuid) -> Option { - self.0.lock().get(token_id) - } -} - -#[derive(Debug)] -struct CredentialStore { - entries: HashMap, -} - -#[derive(Debug)] -pub struct CredentialEntry { - pub token: String, - pub mapping: Option, - pub expires_at: time::OffsetDateTime, -} - -pub type ArcCredentialEntry = Arc; - -impl CredentialStore { - fn new() -> Self { - Self { - entries: HashMap::new(), - } - } - - fn insert( - &mut self, - token: String, - mapping: Option, - time_to_live: time::Duration, - ) -> Result, InsertError> { - let jti = crate::token::extract_jti(&token) - .context("failed to extract token ID") - .map_err(InsertError::InvalidToken)?; - - let entry = CredentialEntry { - token, - mapping, - expires_at: time::OffsetDateTime::now_utc() + time_to_live, - }; - - let previous_entry = self.entries.insert(jti, Arc::new(entry)); - - Ok(previous_entry) - } - - fn get(&self, token_id: Uuid) -> Option { - self.entries.get(&token_id).map(Arc::clone) - } -} - -pub struct CleanupTask { - pub handle: CredentialStoreHandle, -} - -#[async_trait] -impl Task for CleanupTask { - type Output = anyhow::Result<()>; - - const NAME: &'static str = "credential store cleanup"; - - async fn run(self, shutdown_signal: ShutdownSignal) -> Self::Output { - cleanup_task(self.handle, shutdown_signal).await; - Ok(()) - } -} - -#[instrument(skip_all)] -async fn cleanup_task(handle: CredentialStoreHandle, mut shutdown_signal: ShutdownSignal) { - use tokio::time::{Duration, sleep}; - - const TASK_INTERVAL: Duration = Duration::from_secs(60 * 15); // 15 minutes - - debug!("Task started"); - - loop { - tokio::select! { - _ = sleep(TASK_INTERVAL) => {} - _ = shutdown_signal.wait() => { - break; - } - } - - let now = time::OffsetDateTime::now_utc(); - - handle.0.lock().entries.retain(|_, src| now < src.expires_at); - } - - debug!("Task terminated"); -} diff --git a/devolutions-gateway/src/credential_injection.rs b/devolutions-gateway/src/credential_injection.rs new file mode 100644 index 000000000..0f66aa73d --- /dev/null +++ b/devolutions-gateway/src/credential_injection.rs @@ -0,0 +1,736 @@ +//! Proxy-based credential injection using Kerberos or NTLM. +//! +//! This module selects the authentication protocol and owns the credentials used by the proxy. +//! For Kerberos, it also owns per-session fake-KDC material, the registry of live sessions, +//! KDC proxy handling, and in-process KDC requests from the server-side CredSSP acceptor. + +use std::collections::HashMap; +use std::fmt; +use std::net::SocketAddr; +use std::sync::Arc; +use std::time::Duration; + +use anyhow::Context as _; +use chacha20poly1305::aead::OsRng; +use chacha20poly1305::aead::rand_core::RngCore as _; +use ironrdp_connector::sspi; +use ironrdp_connector::sspi::generator::NetworkRequest; +use parking_lot::Mutex; +use picky_krb::messages::KdcProxyMessage; +use secrecy::{ExposeSecret as _, SecretBox, SecretString}; +use url::Url; +use uuid::Uuid; + +use crate::credential::{AppCredential, AppCredentials}; +use crate::provisioning::ProvisionedConnection; +use crate::target_addr::TargetAddr; + +// The reserved `.invalid` TLD (RFC 6761) lets sspi-rs CredSSP server emit "KDC requests" that +// never leave the process: `intercept_network_request` recognises this hostname and dispatches +// the message into the in-process `kdc` server below. +// +// TODO(sspi-rs#664): replace this URL-trampoline with a pluggable KDC dispatcher trait once +// sspi-rs ships the API — see https://github.com/Devolutions/sspi-rs/issues/664. +const IN_PROCESS_KDC_HOST: &str = "cred.invalid"; + +pub(crate) enum CredentialInjection { + // The registration proves the synthetic KDC is published in the registry: holding a + // `Kerberos` value means the KDC is live for the client's KKDCP lookups. It unpublishes on + // drop, so it rides inside the value that the RDP proxy owns for the whole session. + Kerberos( + KerberosCredentialInjection, + #[expect( + dead_code, + reason = "held only for its RAII Drop, which unpublishes the KDC at session end" + )] + SyntheticKdcRegistration, + ), + Ntlm(NtlmCredentialInjection), +} + +pub(crate) struct KerberosCredentialInjection { + jti: Uuid, + credentials: AppCredentials, + target_kdc: TargetAddr, + synthetic_kdc: Arc, +} + +impl KerberosCredentialInjection { + pub(crate) fn synthetic_kdc(&self) -> Arc { + Arc::clone(&self.synthetic_kdc) + } +} + +pub(crate) struct NtlmCredentialInjection { + jti: Uuid, + credentials: AppCredentials, +} + +pub(crate) struct SyntheticKdcSession { + jti: Uuid, + target_hostname: String, + realm: String, + acceptor_principal_name: String, + acceptor_password: SecretString, + acceptor_long_term_key: SecretBox>, + // The KDC crate models users with plaintext passwords, so this object owns those secrets + // for the lifetime of the credential-injection KDC. Keep Debug redacted. + kdc_config: kdc::config::KerberosServer, +} + +impl fmt::Debug for SyntheticKdcSession { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("SyntheticKdcSession") + .field("jti", &self.jti) + .field("target_hostname", &self.target_hostname) + .field("realm", &self.realm) + .field("kdc_config", &"") + .finish() + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct RealmMismatch { + pub(crate) expected: String, + pub(crate) actual: String, +} + +impl fmt::Display for RealmMismatch { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "expected: {}, got: {}", self.expected, self.actual) + } +} + +impl std::error::Error for RealmMismatch {} + +#[derive(Debug)] +pub(crate) enum SyntheticKdcInterception { + Intercepted(Vec), + NotInjectionRequest, + NotInjectionRealm(RealmMismatch), +} + +impl fmt::Debug for CredentialInjection { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Kerberos(injection, _) => f + .debug_struct("CredentialInjection::Kerberos") + .field("jti", &injection.jti) + .field("target_hostname", &injection.synthetic_kdc.target_hostname) + .field("realm", &injection.synthetic_kdc.realm) + .field("kdc_config", &"") + .finish(), + Self::Ntlm(injection) => f + .debug_struct("CredentialInjection::Ntlm") + .field("jti", &injection.jti) + .finish(), + } + } +} + +pub(crate) enum CredentialInjectionInitializationResult { + Kerberos(KerberosCredentialInjection), + Ntlm(NtlmCredentialInjection), +} + +impl CredentialInjectionInitializationResult { + pub(crate) fn maybe_register_synthetic_kdc(self, registry: &SyntheticKdcRegistry) -> CredentialInjection { + match self { + Self::Kerberos(injection) => { + // The registration rides inside the returned CredentialInjection (owned by the RDP + // proxy), so the synthetic KDC stays published for the whole session and the + // client's KKDCP lookups resolve; it unpublishes on drop when the session ends. + let registration = registry.register(injection.synthetic_kdc()); + debug!( + jti = %injection.jti, + "registered synthetic KDC for credential-injection session" + ); + CredentialInjection::Kerberos(injection, registration) + } + Self::Ntlm(injection) => CredentialInjection::Ntlm(injection), + } + } +} + +impl CredentialInjection { + pub(crate) fn from_provisioned( + jti: Uuid, + provisioned_connection: ProvisionedConnection, + target_hostname: &str, + kerberos_enabled: bool, + ) -> anyhow::Result { + anyhow::ensure!( + target_hostname.eq_ignore_ascii_case(&provisioned_connection.target_hostname), + "credential-injection target mismatch" + ); + + let ProvisionedConnection { + credentials, + connection_options, + target_hostname, + } = provisioned_connection; + + let uses_kerberos = if kerberos_enabled { + let target_username = sspi::Username::parse(app_credential_username(&credentials.target)) + .context("invalid target credential username")?; + target_username.domain_name().is_some() + } else { + false + }; + + if uses_kerberos { + let target_kdc = connection_options + .as_ref() + .and_then(crate::target_connection_options::TargetConnectionOptions::krb_kdc) + .cloned() + .context("Kerberos credential injection requires target connection option krb_kdc")?; + let synthetic_kdc = SyntheticKdcSession::new(jti, target_hostname, &credentials.proxy)?; + + Ok(CredentialInjectionInitializationResult::Kerberos( + KerberosCredentialInjection { + jti, + credentials, + target_kdc, + synthetic_kdc: Arc::new(synthetic_kdc), + }, + )) + } else { + Ok(CredentialInjectionInitializationResult::Ntlm(NtlmCredentialInjection { + jti, + credentials, + })) + } + } + + pub(crate) fn jti(&self) -> Uuid { + match self { + Self::Kerberos(injection, _) => injection.jti, + Self::Ntlm(injection) => injection.jti, + } + } + + pub(crate) fn proxy_credential(&self) -> &AppCredential { + match self { + Self::Kerberos(injection, _) => &injection.credentials.proxy, + Self::Ntlm(injection) => &injection.credentials.proxy, + } + } + + pub(crate) fn target_credential(&self) -> &AppCredential { + match self { + Self::Kerberos(injection, _) => &injection.credentials.target, + Self::Ntlm(injection) => &injection.credentials.target, + } + } + + pub(crate) fn kerberos_configs( + &self, + client_addr: SocketAddr, + gateway_hostname: &str, + ) -> anyhow::Result> { + match self { + Self::Kerberos(injection, _) => Ok(Some(( + injection.synthetic_kdc.server_kerberos_config(client_addr)?, + ironrdp_connector::credssp::KerberosConfig { + kdc_proxy_url: Some( + Url::try_from(&injection.target_kdc).context("convert target KDC address to URL")?, + ), + hostname: gateway_hostname.to_owned(), + }, + ))), + Self::Ntlm(_) => Ok(None), + } + } + + pub(crate) fn intercept_network_request( + &self, + request: &NetworkRequest, + ) -> anyhow::Result { + match self { + Self::Kerberos(injection, _) => injection.synthetic_kdc.intercept_network_request(request), + Self::Ntlm(_) => Ok(SyntheticKdcInterception::NotInjectionRequest), + } + } +} + +impl SyntheticKdcSession { + fn new(jti: Uuid, target_hostname: String, proxy_credential: &AppCredential) -> anyhow::Result { + let proxy_username = app_credential_username(proxy_credential); + let realm = proxy_username + .split_once('@') + .map(|(_, realm)| realm) + .filter(|realm| !realm.is_empty()) + .map(str::to_owned) + .unwrap_or_else(|| synthetic_realm(jti)); + let krbtgt_key = SecretBox::new(Box::new(random_32_bytes())); + let acceptor_principal_name = "jet".to_owned(); + let acceptor_password = SecretString::from(hex::encode(random_32_bytes())); + let acceptor_long_term_key = SecretBox::new(Box::new(random_32_bytes())); + let kdc_config = build_kdc_config( + &realm, + &krbtgt_key, + &acceptor_principal_name, + &acceptor_password, + &acceptor_long_term_key, + proxy_credential, + )?; + + Ok(Self { + jti, + target_hostname, + realm, + acceptor_principal_name, + acceptor_password, + acceptor_long_term_key, + kdc_config, + }) + } + + pub(crate) fn jti(&self) -> Uuid { + self.jti + } + + fn server_kerberos_config(&self, client_addr: SocketAddr) -> anyhow::Result { + let user = sspi::CredentialsBuffers::AuthIdentity(sspi::AuthIdentityBuffers::from_utf8( + &self.acceptor_principal_name, + &self.realm, + self.acceptor_password.expose_secret(), + )); + + let kdc_url = self.in_process_kdc_url()?; + + // The SPN that the client puts on its AP-REQ ticket is the one for the target RDP + // server (`TERMSRV/`). Gateway-as-CredSSP-server is impersonating that target, + // so ServerProperties must claim the same SPN or sspi-rs rejects the ticket. + Ok(sspi::KerberosServerConfig { + kerberos_config: sspi::KerberosConfig { + kdc_url: Some(kdc_url), + client_computer_name: client_addr.to_string(), + }, + server_properties: sspi::kerberos::ServerProperties::new( + &["TERMSRV", &self.target_hostname], + Some(user), + Duration::from_secs(300), + Some(sspi::Secret::new(self.acceptor_long_term_key.expose_secret().clone())), + )?, + }) + } + + pub(crate) fn intercept_network_request( + &self, + request: &NetworkRequest, + ) -> anyhow::Result { + if request.url.host_str() != Some(IN_PROCESS_KDC_HOST) { + return Ok(SyntheticKdcInterception::NotInjectionRequest); + } + + let url_jti = request + .url + .path() + .trim_start_matches('/') + .parse::() + .context("malformed in-process KDC URL")?; + anyhow::ensure!( + url_jti == self.jti(), + "in-process KDC URL JTI does not match current CredSSP session", + ); + + debug!( + jti = %self.jti(), + scheme = %request.url.scheme(), + "Credential-injection KDC intercepted in-process request" + ); + + let kdc_message = KdcProxyMessage::from_raw(&request.data).context("malformed in-process KDC proxy payload")?; + self.handle_kdc_proxy_message(kdc_message) + } + + pub(crate) fn handle_kdc_proxy_message( + &self, + message: KdcProxyMessage, + ) -> anyhow::Result { + let request_realm = self.resolve_message_realm(&message); + debug!( + jti = %self.jti(), + resolved_realm = %request_realm, + "Credential-injection KDC realm resolved" + ); + + if let Some(mismatch) = realm_mismatch(&self.realm, &request_realm) { + return Ok(SyntheticKdcInterception::NotInjectionRealm(mismatch)); + } + + let reply = self.handle_message(message)?; + Ok(SyntheticKdcInterception::Intercepted(reply)) + } + + fn in_process_kdc_url(&self) -> anyhow::Result { + Url::parse(&format!("http://{}/{}", IN_PROCESS_KDC_HOST, self.jti())).context("build in-process KDC URL") + } + + fn resolve_message_realm(&self, kdc_proxy_message: &KdcProxyMessage) -> String { + kdc_proxy_message_realm(kdc_proxy_message).unwrap_or_else(|| self.realm.clone()) + } + + fn handle_message(&self, kdc_proxy_message: KdcProxyMessage) -> anyhow::Result> { + let reply = kdc::handle_kdc_proxy_message(kdc_proxy_message, &self.kdc_config, &self.target_hostname) + .context("handle credential-injection KDC message")?; + + reply.to_vec().context("encode credential-injection KDC reply") + } +} + +fn app_credential_username(credential: &AppCredential) -> &str { + match credential { + AppCredential::UsernamePassword { username, password: _ } => username, + } +} + +pub(crate) fn kdc_proxy_message_realm(kdc_proxy_message: &KdcProxyMessage) -> Option { + kdc_proxy_message + .target_domain + .0 + .as_ref() + .map(|realm| realm.0.to_string()) + .filter(|realm| !realm.is_empty()) +} + +fn realm_mismatch(expected: &str, actual: &str) -> Option { + if expected.eq_ignore_ascii_case(actual) { + return None; + } + + Some(RealmMismatch { + expected: expected.to_owned(), + actual: actual.to_owned(), + }) +} + +fn build_kdc_config( + realm: &str, + krbtgt_key: &SecretBox>, + acceptor_principal_name: &str, + acceptor_password: &SecretString, + acceptor_long_term_key: &SecretBox>, + proxy_credential: &AppCredential, +) -> anyhow::Result { + let (proxy_user_name, proxy_password) = proxy_credential.decrypt_password()?; + let proxy_user_name = principal_for_realm(&proxy_user_name, realm); + let acceptor_principal_name = principal_for_realm(acceptor_principal_name, realm); + + let acceptor_password = acceptor_password.expose_secret().to_owned(); + Ok(kdc::config::KerberosServer { + realm: realm.to_owned(), + users: vec![ + kdc::config::DomainUser { + username: proxy_user_name.clone(), + password: proxy_password.expose_secret().to_owned(), + salt: kerberos_salt(realm, &proxy_user_name), + }, + kdc::config::DomainUser { + username: acceptor_principal_name.clone(), + password: acceptor_password.clone(), + salt: kerberos_salt(realm, &acceptor_principal_name), + }, + ], + max_time_skew: 300, + krbtgt_key: krbtgt_key.expose_secret().clone(), + ticket_decryption_key: Some(acceptor_long_term_key.expose_secret().clone()), + service_user: Some(kdc::config::DomainUser { + username: acceptor_principal_name.clone(), + password: acceptor_password, + salt: kerberos_salt(realm, &acceptor_principal_name), + }), + }) +} + +fn principal_for_realm(user_name: &str, realm: &str) -> String { + if user_name.contains('@') { + user_name.to_owned() + } else { + format!("{user_name}@{realm}") + } +} + +fn kerberos_salt(realm: &str, principal: &str) -> String { + let local_name = principal.split('@').next().unwrap_or(principal); + format!("{}{local_name}", realm.to_ascii_uppercase()) +} + +fn synthetic_realm(jti: Uuid) -> String { + format!("CRED-{}.INVALID", jti.simple()).to_ascii_uppercase() +} + +fn random_32_bytes() -> Vec { + let mut bytes = vec![0u8; 32]; + OsRng.fill_bytes(&mut bytes); + bytes +} + +/// Indexes Kerberos sessions created by active RDP credential-injection connections. +#[derive(Debug, Clone)] +pub struct SyntheticKdcRegistry { + sessions: Arc>>>, +} + +#[must_use = "dropping the registration unpublishes the synthetic KDC"] +pub(crate) struct SyntheticKdcRegistration { + jti: Uuid, + session: Arc, + registry: SyntheticKdcRegistry, +} + +impl Drop for SyntheticKdcRegistration { + fn drop(&mut self) { + // Only retract our own publication: a superseding reconnect may already own this JTI. + let mut sessions = self.registry.sessions.lock(); + if sessions + .get(&self.jti) + .is_some_and(|current| Arc::ptr_eq(current, &self.session)) + { + sessions.remove(&self.jti); + } + } +} + +impl Default for SyntheticKdcRegistry { + fn default() -> Self { + Self::new() + } +} + +impl SyntheticKdcRegistry { + pub fn new() -> Self { + Self { + sessions: Arc::new(Mutex::new(HashMap::new())), + } + } + + pub(crate) fn register(&self, session: Arc) -> SyntheticKdcRegistration { + let jti = session.jti(); + // Replace rather than reject: a fast RDP reconnect can register before the previous + // connection's registration has dropped. The newest connection owns the JTI. + self.sessions.lock().insert(jti, Arc::clone(&session)); + SyntheticKdcRegistration { + jti, + session, + registry: self.clone(), + } + } + + pub(crate) fn get(&self, jti: Uuid) -> Option> { + self.sessions.lock().get(&jti).cloned() + } +} + +#[cfg(test)] +mod tests { + use ironrdp_connector::sspi::network_client::NetworkProtocol; + use secrecy::SecretString; + + use super::*; + use crate::credential::{CleartextAppCredential, CleartextAppCredentials}; + use crate::target_connection_options::TargetConnectionOptions; + + fn app_credentials(proxy_username: &str, target_username: &str) -> AppCredentials { + CleartextAppCredentials { + proxy: CleartextAppCredential::UsernamePassword { + username: proxy_username.to_owned(), + password: SecretString::from("pwd"), + }, + target: CleartextAppCredential::UsernamePassword { + username: target_username.to_owned(), + password: SecretString::from("pwd"), + }, + } + .encrypt() + .expect("credentials encrypt") + } + + fn provisioned(target_username: &str, krb_kdc: Option) -> ProvisionedConnection { + ProvisionedConnection { + credentials: app_credentials("proxy@example.invalid", target_username), + connection_options: krb_kdc.map(|kdc| TargetConnectionOptions::new(Some(kdc)).expect("connection options")), + target_hostname: "target.example".to_owned(), + } + } + + fn target_kdc() -> TargetAddr { + TargetAddr::parse("tcp://kdc.example.invalid:88", Some(88)).expect("KDC address parses") + } + + fn session(jti: Uuid) -> SyntheticKdcSession { + SyntheticKdcSession::new( + jti, + "target.example".to_owned(), + &app_credentials("proxy@example.invalid", "target").proxy, + ) + .expect("synthetic KDC session") + } + + fn network_request(url: &str) -> NetworkRequest { + NetworkRequest { + protocol: NetworkProtocol::Http, + url: Url::parse(url).expect("test URL parses"), + data: Vec::new(), + } + } + + #[test] + fn proxy_user_at_realm_is_used_as_realm() { + assert_eq!(session(Uuid::new_v4()).realm, "example.invalid"); + } + + #[test] + fn bare_proxy_username_yields_synthetic_realm() { + let jti = Uuid::new_v4(); + let session = SyntheticKdcSession::new( + jti, + "target.example".to_owned(), + &app_credentials("just-a-uuid", "target").proxy, + ) + .expect("synthetic KDC session"); + assert_eq!(session.realm, synthetic_realm(jti)); + } + + #[test] + fn from_provisioned_selects_kerberos_for_domain_target_with_kdc() { + let kdc = target_kdc(); + let injection = CredentialInjection::from_provisioned( + Uuid::new_v4(), + provisioned("administrator@example.invalid", Some(kdc.clone())), + "target.example", + true, + ) + .expect("Kerberos injection"); + + match injection { + CredentialInjectionInitializationResult::Kerberos(injection) => assert_eq!(injection.target_kdc, kdc), + CredentialInjectionInitializationResult::Ntlm(_) => panic!("expected Kerberos injection"), + } + } + + #[test] + fn from_provisioned_hard_errors_when_kerberos_target_has_no_kdc() { + let error = CredentialInjection::from_provisioned( + Uuid::new_v4(), + provisioned("administrator@example.invalid", None), + "target.example", + true, + ) + .err() + .expect("missing KDC must abort, never fall back to NTLM"); + assert!(format!("{error:#}").contains("requires target connection option krb_kdc")); + } + + #[test] + fn from_provisioned_selects_ntlm_for_domainless_target() { + let injection = CredentialInjection::from_provisioned( + Uuid::new_v4(), + provisioned("Administrator", None), + "target.example", + true, + ) + .expect("NTLM injection"); + assert!(matches!(injection, CredentialInjectionInitializationResult::Ntlm(_))); + } + + #[test] + fn from_provisioned_selects_ntlm_when_kerberos_disabled() { + let injection = CredentialInjection::from_provisioned( + Uuid::new_v4(), + provisioned("administrator@example.invalid", Some(target_kdc())), + "target.example", + false, + ) + .expect("NTLM injection"); + assert!(matches!(injection, CredentialInjectionInitializationResult::Ntlm(_))); + } + + #[test] + fn from_provisioned_rejects_target_hostname_mismatch() { + let error = CredentialInjection::from_provisioned( + Uuid::new_v4(), + provisioned("Administrator", None), + "other.example", + true, + ) + .err() + .expect("target hostname mismatch must fail closed"); + assert!(format!("{error:#}").contains("target mismatch")); + } + + #[test] + fn registered_session_is_retrievable() { + let registry = SyntheticKdcRegistry::new(); + let jti = Uuid::new_v4(); + let session = Arc::new(session(jti)); + let _registration = registry.register(Arc::clone(&session)); + assert!(Arc::ptr_eq(®istry.get(jti).expect("registered"), &session)); + } + + #[test] + fn register_replaces_and_guarded_drop_keeps_successor() { + let registry = SyntheticKdcRegistry::new(); + let jti = Uuid::new_v4(); + // Two fresh sessions under one JTI model a reconnect that re-derives its own material. + let first = Arc::new(session(jti)); + let second = Arc::new(session(jti)); + + let first_registration = registry.register(Arc::clone(&first)); + let second_registration = registry.register(Arc::clone(&second)); + + // The reconnect supersedes the previous publication. + assert!(Arc::ptr_eq(®istry.get(jti).expect("registered"), &second)); + + // The superseded connection's late teardown must not evict the successor. + drop(first_registration); + assert!(Arc::ptr_eq(®istry.get(jti).expect("still registered"), &second)); + + // Dropping the current registration retracts it. + drop(second_registration); + assert!(registry.get(jti).is_none()); + } + + #[test] + fn intercept_ignores_non_loopback_host() { + let result = session(Uuid::new_v4()) + .intercept_network_request(&network_request("http://kdc.real.example/path")) + .expect("non-loopback request dispatches"); + assert!(matches!(result, SyntheticKdcInterception::NotInjectionRequest)); + } + + #[test] + fn intercept_rejects_malformed_url_path() { + let error = session(Uuid::new_v4()) + .intercept_network_request(&network_request("http://cred.invalid/not-a-uuid")) + .expect_err("non-UUID path must fail"); + assert!(format!("{error:#}").contains("malformed in-process KDC URL")); + } + + #[test] + fn intercept_rejects_mismatched_jti() { + let session = session(Uuid::new_v4()); + let error = session + .intercept_network_request(&network_request(&format!("http://cred.invalid/{}", Uuid::new_v4()))) + .expect_err("JTI mismatch must fail"); + assert!(format!("{error:#}").contains("does not match current CredSSP session")); + } + + #[test] + fn intercept_accepts_matching_url_path_before_payload_decode() { + let jti = Uuid::new_v4(); + let error = session(jti) + .intercept_network_request(&network_request(&format!("http://cred.invalid/{jti}"))) + .expect_err("empty KDC payload must fail after URL/JTI validation"); + assert!(format!("{error:#}").contains("malformed in-process KDC proxy payload")); + } + + #[test] + fn realm_mismatch_reports_case_insensitively() { + assert!(realm_mismatch("AD.EXAMPLE", "ad.example").is_none()); + let mismatch = realm_mismatch("cred.invalid", "evil.example").expect("different realms mismatch"); + assert_eq!(mismatch.expected, "cred.invalid"); + assert_eq!(mismatch.actual, "evil.example"); + } +} diff --git a/devolutions-gateway/src/credential_injection_kdc.rs b/devolutions-gateway/src/credential_injection_kdc.rs deleted file mode 100644 index b789c2b74..000000000 --- a/devolutions-gateway/src/credential_injection_kdc.rs +++ /dev/null @@ -1,1027 +0,0 @@ -//! In-memory Kerberos KDC used by proxy-based credential injection. -//! -//! This module owns the Kerberos side of credential injection end-to-end: -//! per-session fake-KDC material, the session store, KDC proxy handling, and the -//! in-process KDC requests emitted by the server-side CredSSP acceptor. -//! Callers should only decide whether credential injection applies; once it does, this -//! component owns the Kerberos-specific behavior. - -use std::collections::HashMap; -use std::fmt; -use std::net::SocketAddr; -use std::sync::Arc; -use std::time::Duration; - -use anyhow::Context as _; -use async_trait::async_trait; -use chacha20poly1305::aead::OsRng; -use chacha20poly1305::aead::rand_core::RngCore as _; -use devolutions_gateway_task::{ShutdownSignal, Task}; -use ironrdp_connector::sspi; -use ironrdp_connector::sspi::generator::NetworkRequest; -use parking_lot::Mutex; -use picky_krb::messages::KdcProxyMessage; -use secrecy::{ExposeSecret as _, SecretBox, SecretString}; -use thiserror::Error; -use url::Url; -use uuid::Uuid; - -use crate::credential::{AppCredential, AppCredentialMapping, ArcCredentialEntry, CredentialStoreHandle}; - -// The reserved `.invalid` TLD (RFC 6761) lets sspi-rs CredSSP server emit "KDC requests" that -// never leave the process: `intercept_network_request` recognises this hostname and dispatches -// the message into the in-process `kdc` server below. -// -// TODO(sspi-rs#664): replace this URL-trampoline with a pluggable KDC dispatcher trait once -// sspi-rs ships the API — see https://github.com/Devolutions/sspi-rs/issues/664. -const IN_PROCESS_KDC_HOST: &str = "cred.invalid"; - -pub(crate) struct CredentialInjectionKdc { - jti: Uuid, - raw_token: String, - credential_mapping: AppCredentialMapping, - target_hostname: String, - session: Arc, - // The KDC crate models users with plaintext passwords, so this object owns those secrets - // for the lifetime of the credential-injection KDC. Keep Debug redacted. - kdc_config: kdc::config::KerberosServer, -} - -#[derive(Debug, Error)] -pub(crate) enum CredentialInjectionKdcResolveError { - #[error("credential-injection state is not available for {jti}")] - MissingCredential { jti: Uuid }, - #[error("credential-injection state for {jti} has expired")] - ExpiredCredential { jti: Uuid }, - #[error("credential-injection state is not available for {jti}")] - NonInjectionCredential { jti: Uuid }, - #[error("association token for {jti} is not valid for credential injection")] - InvalidAssociationToken { - jti: Uuid, - #[source] - source: anyhow::Error, - }, - #[error("credential-injection KDC config could not be initialized for {jti}")] - BuildKdcConfig { - jti: Uuid, - #[source] - source: anyhow::Error, - }, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub(crate) struct RealmMismatch { - pub(crate) expected: String, - pub(crate) actual: String, -} - -impl fmt::Display for RealmMismatch { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "expected: {}, got: {}", self.expected, self.actual) - } -} - -impl std::error::Error for RealmMismatch {} - -#[derive(Debug)] -pub(crate) enum CredentialInjectionKdcInterception { - Intercepted(Vec), - NotInjectionRequest, - NotInjectionRealm(RealmMismatch), -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) enum CredentialInjectionClientAcceptorProtocol { - Kerberos, - Ntlm, -} - -pub(crate) struct CredentialInjectionKdcRequest { - message: KdcProxyMessage, -} - -impl CredentialInjectionKdcRequest { - pub(crate) fn from_token(message: KdcProxyMessage) -> Self { - Self { message } - } - - fn in_process(message: KdcProxyMessage) -> Self { - Self { message } - } -} - -impl fmt::Debug for CredentialInjectionKdc { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("CredentialInjectionKdc") - .field("jti", &self.jti) - .field("target_hostname", &self.target_hostname) - .field("realm", &self.session.realm) - .field("kdc_config", &"") - .finish() - } -} - -impl CredentialInjectionKdc { - fn from_parts( - jti: Uuid, - credential_entry: ArcCredentialEntry, - target_hostname: String, - session: Arc, - ) -> anyhow::Result { - let mapping = credential_entry - .mapping - .as_ref() - .context("credential entry has no credential-injection mapping")?; - anyhow::ensure!( - jti == session.jti, - "credential entry JTI does not match credential-injection KDC session JTI", - ); - - let kdc_config = build_kdc_config(&session, &mapping.proxy)?; - - Ok(Self { - jti, - raw_token: credential_entry.token.clone(), - credential_mapping: mapping.clone(), - target_hostname, - session, - kdc_config, - }) - } - - pub(crate) fn jti(&self) -> Uuid { - self.jti - } - - pub(crate) fn raw_token(&self) -> &str { - &self.raw_token - } - - pub(crate) fn proxy_credential(&self) -> &AppCredential { - &self.credential_mapping.proxy - } - - pub(crate) fn target_credential(&self) -> &AppCredential { - &self.credential_mapping.target - } - - /// Selects the CredSSP acceptor backend Gateway should present to the RDP client. - /// - /// The acceptor side must mirror the target-side auth package. - /// Domainless target credentials cannot acquire Kerberos tickets. - /// Enabling the Kerberos acceptor for those sessions would make incoming NTLMSSP tokens fail in Kerberos parsing. - pub(crate) fn client_acceptor_protocol(&self) -> anyhow::Result { - let target_username = sspi::Username::parse(app_credential_username(self.target_credential())) - .context("invalid target credential username")?; - - if target_username.domain_name().is_some() { - Ok(CredentialInjectionClientAcceptorProtocol::Kerberos) - } else { - Ok(CredentialInjectionClientAcceptorProtocol::Ntlm) - } - } - - pub(crate) fn server_kerberos_config(&self, client_addr: SocketAddr) -> anyhow::Result { - let user = sspi::CredentialsBuffers::AuthIdentity(sspi::AuthIdentityBuffers::from_utf8( - &self.session.acceptor.principal_name, - &self.session.realm, - self.session.acceptor.password.expose_secret(), - )); - - let kdc_url = self.in_process_kdc_url()?; - - // The SPN that the client puts on its AP-REQ ticket is the one for the target RDP - // server (`TERMSRV/`). Gateway-as-CredSSP-server is impersonating that target, - // so ServerProperties must claim the same SPN or sspi-rs rejects the ticket. - Ok(sspi::KerberosServerConfig { - kerberos_config: sspi::KerberosConfig { - kdc_url: Some(kdc_url), - client_computer_name: client_addr.to_string(), - }, - server_properties: sspi::kerberos::ServerProperties::new( - &["TERMSRV", &self.target_hostname], - Some(user), - Duration::from_secs(300), - Some(sspi::Secret::new( - self.session.acceptor.long_term_key.expose_secret().clone(), - )), - )?, - }) - } - - pub(crate) fn intercept_network_request( - &self, - request: &NetworkRequest, - ) -> anyhow::Result { - if request.url.host_str() != Some(IN_PROCESS_KDC_HOST) { - return Ok(CredentialInjectionKdcInterception::NotInjectionRequest); - } - - let url_jti = request - .url - .path() - .trim_start_matches('/') - .parse::() - .context("malformed in-process KDC URL")?; - anyhow::ensure!( - url_jti == self.jti, - "in-process KDC URL JTI does not match current CredSSP session", - ); - - debug!( - jti = %self.jti, - scheme = %request.url.scheme(), - "Credential-injection KDC intercepted in-process request" - ); - - let kdc_message = KdcProxyMessage::from_raw(&request.data).context("malformed in-process KDC proxy payload")?; - self.handle_kdc_proxy_request(CredentialInjectionKdcRequest::in_process(kdc_message)) - } - - pub(crate) fn handle_kdc_proxy_request( - &self, - request: CredentialInjectionKdcRequest, - ) -> anyhow::Result { - let request_realm = self.resolve_message_realm(&request.message); - debug!( - jti = %self.jti, - resolved_realm = %request_realm, - "Credential-injection KDC realm resolved" - ); - - if let Some(mismatch) = realm_mismatch(&self.session.realm, &request_realm) { - return Ok(CredentialInjectionKdcInterception::NotInjectionRealm(mismatch)); - } - - let reply = self.handle_message(request.message)?; - Ok(CredentialInjectionKdcInterception::Intercepted(reply)) - } - - fn in_process_kdc_url(&self) -> anyhow::Result { - Url::parse(&format!("http://{}/{}", IN_PROCESS_KDC_HOST, self.jti)).context("build in-process KDC URL") - } - - fn resolve_message_realm(&self, kdc_proxy_message: &KdcProxyMessage) -> String { - kdc_proxy_message_realm(kdc_proxy_message).unwrap_or_else(|| self.session.realm.clone()) - } - - fn handle_message(&self, kdc_proxy_message: KdcProxyMessage) -> anyhow::Result> { - let reply = kdc::handle_kdc_proxy_message(kdc_proxy_message, &self.kdc_config, &self.target_hostname) - .context("handle credential-injection KDC message")?; - - reply.to_vec().context("encode credential-injection KDC reply") - } -} - -fn app_credential_username(credential: &AppCredential) -> &str { - match credential { - AppCredential::UsernamePassword { username, password: _ } => username, - } -} - -pub(crate) fn kdc_proxy_message_realm(kdc_proxy_message: &KdcProxyMessage) -> Option { - kdc_proxy_message - .target_domain - .0 - .as_ref() - .map(|realm| realm.0.to_string()) - .filter(|realm| !realm.is_empty()) -} - -fn realm_mismatch(expected: &str, actual: &str) -> Option { - if expected.eq_ignore_ascii_case(actual) { - return None; - } - - Some(RealmMismatch { - expected: expected.to_owned(), - actual: actual.to_owned(), - }) -} - -/// Per-session Kerberos material for proxy-based credential injection. -/// -/// The key material and the acceptor PA-ENC-TIMESTAMP password are wrapped in [`SecretBox`] / -/// [`SecretString`] so they cannot be accidentally written to logs through structured tracing. -/// Access requires an explicit `expose_secret()` call, which is greppable and reviewable. -struct CredentialInjectionKdcSession { - jti: Uuid, - realm: String, - kdc: CredentialInjectionKdcState, - acceptor: CredentialInjectionAcceptorState, -} - -struct CredentialInjectionKdcState { - krbtgt_key: SecretBox>, -} - -struct CredentialInjectionAcceptorState { - principal_name: String, - password: SecretString, - long_term_key: SecretBox>, -} - -impl fmt::Debug for CredentialInjectionKdcSession { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("CredentialInjectionKdcSession") - .field("jti", &self.jti) - .field("realm", &self.realm) - .field("kdc", &self.kdc) - .field("acceptor", &self.acceptor) - .finish() - } -} - -impl fmt::Debug for CredentialInjectionKdcState { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("CredentialInjectionKdcState") - .field("krbtgt_key", &"<32 bytes redacted>") - .finish() - } -} - -impl fmt::Debug for CredentialInjectionAcceptorState { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("CredentialInjectionAcceptorState") - .field("principal_name", &self.principal_name) - .field("password", &"") - .field("long_term_key", &"<32 bytes redacted>") - .finish() - } -} - -/// Derive per-session Kerberos material from the proxy username and the association token's JTI. -/// -/// The proxy username's optional `@realm` suffix selects the realm DVLS supplied; otherwise -/// fall back to a per-session synthetic realm derived from the JTI. The two sides agree -/// because DVLS derives the synthetic value the same way. -fn derive_credential_injection_kdc_session(proxy_username: &str, jti: Uuid) -> CredentialInjectionKdcSession { - let realm = proxy_username - .split_once('@') - .map(|(_, realm)| realm) - .filter(|realm| !realm.is_empty()) - .map(str::to_owned) - .unwrap_or_else(|| synthetic_realm(jti)); - - CredentialInjectionKdcSession { - jti, - realm, - kdc: CredentialInjectionKdcState { - krbtgt_key: SecretBox::new(Box::new(random_32_bytes())), - }, - acceptor: CredentialInjectionAcceptorState { - principal_name: "jet".to_owned(), - password: SecretString::from(hex::encode(random_32_bytes())), - long_term_key: SecretBox::new(Box::new(random_32_bytes())), - }, - } -} - -fn build_kdc_config( - session: &CredentialInjectionKdcSession, - proxy_credential: &AppCredential, -) -> anyhow::Result { - let realm = &session.realm; - let (proxy_user_name, proxy_password) = proxy_credential.decrypt_password()?; - let proxy_user_name = principal_for_realm(&proxy_user_name, realm); - let acceptor_principal_name = principal_for_realm(&session.acceptor.principal_name, realm); - - let acceptor_password = session.acceptor.password.expose_secret().to_owned(); - Ok(kdc::config::KerberosServer { - realm: realm.to_owned(), - users: vec![ - kdc::config::DomainUser { - username: proxy_user_name.clone(), - password: proxy_password.expose_secret().to_owned(), - salt: kerberos_salt(realm, &proxy_user_name), - }, - kdc::config::DomainUser { - username: acceptor_principal_name.clone(), - password: acceptor_password.clone(), - salt: kerberos_salt(realm, &acceptor_principal_name), - }, - ], - max_time_skew: 300, - krbtgt_key: session.kdc.krbtgt_key.expose_secret().clone(), - ticket_decryption_key: Some(session.acceptor.long_term_key.expose_secret().clone()), - service_user: Some(kdc::config::DomainUser { - username: acceptor_principal_name.clone(), - password: acceptor_password, - salt: kerberos_salt(realm, &acceptor_principal_name), - }), - }) -} - -fn principal_for_realm(user_name: &str, realm: &str) -> String { - if user_name.contains('@') { - user_name.to_owned() - } else { - format!("{user_name}@{realm}") - } -} - -fn kerberos_salt(realm: &str, principal: &str) -> String { - let local_name = principal.split('@').next().unwrap_or(principal); - format!("{}{local_name}", realm.to_ascii_uppercase()) -} - -fn synthetic_realm(jti: Uuid) -> String { - format!("CRED-{}.INVALID", jti.simple()).to_ascii_uppercase() -} - -fn random_32_bytes() -> Vec { - let mut bytes = vec![0u8; 32]; - OsRng.fill_bytes(&mut bytes); - bytes -} - -/// One-stop service for credential storage and credential-injection KDC state. -/// -/// Wraps the protocol-neutral [`CredentialStoreHandle`] and adds a Kerberos session cache keyed by -/// association-token JTI. The credential store remains the single source of truth for entry -/// lifetime; the session cache piggybacks on it (Arc-cloned credentials at lookup time, with stale -/// sessions evicted on insert-replacement and by a periodic sweep). -/// -/// All credential reads/writes — provision-credentials, RDP mode detection, KDC dispatch — go -/// through this service, so callers see one handle instead of coordinating a store and a registry. -#[derive(Debug, Clone)] -pub struct CredentialService { - credentials: CredentialStoreHandle, - sessions: Arc>>>, -} - -impl Default for CredentialService { - fn default() -> Self { - Self::new() - } -} - -impl CredentialService { - pub fn new() -> Self { - Self { - credentials: CredentialStoreHandle::new(), - sessions: Arc::new(Mutex::new(HashMap::new())), - } - } - - /// Insert (or replace) a credential entry keyed by the token's JTI. - /// - /// Any previously-cached Kerberos session for the same JTI is dropped: it was derived from - /// the prior provisioning and is no longer valid for the new entry. We invalidate even when - /// `CredentialStoreHandle::insert` reports no replacement, because the prior entry may have - /// already been evicted by `credential::CleanupTask` while its session cache entry was still - /// awaiting the next `sweep_orphans` tick — without an unconditional drop here, a fresh - /// provisioning under the same JTI would reuse stale key material. - pub fn insert( - &self, - token: String, - mapping: Option, - time_to_live: time::Duration, - ) -> Result, crate::credential::InsertError> { - // Snapshot the JTI from the new token so we can invalidate the matching session entry - // regardless of whether the credential store reports a replacement. `CredentialStore::insert` - // re-extracts internally; both calls go through the same code path, so an invalid token - // here will surface as the same `InvalidToken` error downstream. - let jti = crate::token::extract_jti(&token) - .context("failed to extract token ID") - .map_err(crate::credential::InsertError::InvalidToken)?; - let previous = self.credentials.insert(token, mapping, time_to_live)?; - self.sessions.lock().remove(&jti); - Ok(previous) - } - - /// Look up a credential entry by its association-token JTI. - pub fn get(&self, jti: Uuid) -> Option { - self.credentials.get(jti) - } - - /// Borrow the inner [`CredentialStoreHandle`] for plumbing that genuinely needs the - /// protocol-neutral primitive (e.g. wiring the background expiry task). - pub fn credential_store(&self) -> &CredentialStoreHandle { - &self.credentials - } - - /// Resolve the credential-injection KDC bound to the given association-token JTI. - /// - /// Returns the per-call KDC view; the underlying Kerberos session (krbtgt key, acceptor - /// long-term key, acceptor password) is cached so the in-process KDC and the CredSSP acceptor - /// see identical key material for the lifetime of the provisioned credentials. - pub(crate) fn kdc_for(&self, jti: Uuid) -> Result { - let credential_entry = self.credentials.get(jti).ok_or_else(|| { - warn!(%jti, "KDC token references missing credential-injection state"); - CredentialInjectionKdcResolveError::MissingCredential { jti } - })?; - - // `CredentialStoreHandle::get` does not enforce expiry — entries are evicted asynchronously - // by the credential cleanup task. Treat a stale entry as already gone so we never build a - // KDC against expired credentials. - if time::OffsetDateTime::now_utc() >= credential_entry.expires_at { - warn!(%jti, "KDC token references expired credential-injection state"); - self.sessions.lock().remove(&jti); - return Err(CredentialInjectionKdcResolveError::ExpiredCredential { jti }); - } - - let mapping = credential_entry.mapping.as_ref().ok_or_else(|| { - warn!(%jti, "KDC token references non-injection credential state"); - CredentialInjectionKdcResolveError::NonInjectionCredential { jti } - })?; - - let target_hostname = crate::token::extract_credential_injection_target_hostname(&credential_entry.token) - .map_err(|source| { - warn!( - %jti, - error = format!("{source:#}"), - "KDC token references invalid credential-injection association token" - ); - CredentialInjectionKdcResolveError::InvalidAssociationToken { jti, source } - })?; - - let proxy_username = app_credential_username(&mapping.proxy).to_owned(); - // Atomic get-or-insert: holds the lock long enough to guarantee a single Arc - // wins for this JTI even under concurrent `kdc_for` calls. The derivation is fast (a few - // hundred bytes of OsRng) so doing it under the lock is acceptable. - let session = { - let mut sessions = self.sessions.lock(); - let session = sessions - .entry(jti) - .or_insert_with(|| Arc::new(derive_credential_injection_kdc_session(&proxy_username, jti))); - Arc::clone(session) - }; - - CredentialInjectionKdc::from_parts(jti, credential_entry, target_hostname, session) - .map_err(|source| CredentialInjectionKdcResolveError::BuildKdcConfig { jti, source }) - } - - fn sweep_orphans(&self) { - let stale_jtis: Vec = { - let sessions = self.sessions.lock(); - sessions - .keys() - .copied() - .filter(|jti| self.credentials.get(*jti).is_none()) - .collect() - }; - - if stale_jtis.is_empty() { - return; - } - - let mut sessions = self.sessions.lock(); - for jti in stale_jtis { - sessions.remove(&jti); - } - } -} - -pub struct CleanupTask { - pub service: CredentialService, -} - -#[async_trait] -impl Task for CleanupTask { - type Output = anyhow::Result<()>; - - const NAME: &'static str = "credential injection kdc cleanup"; - - async fn run(self, shutdown_signal: ShutdownSignal) -> Self::Output { - cleanup_task(self.service, shutdown_signal).await; - Ok(()) - } -} - -#[instrument(skip_all)] -async fn cleanup_task(service: CredentialService, mut shutdown_signal: ShutdownSignal) { - use tokio::time::{Duration, sleep}; - - const TASK_INTERVAL: Duration = Duration::from_secs(60 * 15); // 15 minutes - - debug!("Task started"); - - loop { - tokio::select! { - _ = sleep(TASK_INTERVAL) => {} - _ = shutdown_signal.wait() => { - break; - } - } - - service.sweep_orphans(); - } - - debug!("Task terminated"); -} - -#[cfg(test)] -mod tests { - use base64::Engine as _; - use ironrdp_connector::sspi::network_client::NetworkProtocol; - use secrecy::SecretString; - - use super::*; - use crate::credential::{CleartextAppCredential, CleartextAppCredentialMapping}; - - fn cleartext_mapping_with_target_username(target_username: &str) -> CleartextAppCredentialMapping { - CleartextAppCredentialMapping { - proxy: CleartextAppCredential::UsernamePassword { - username: "proxy@example.invalid".to_owned(), - password: SecretString::from("pwd"), - }, - target: CleartextAppCredential::UsernamePassword { - username: target_username.to_owned(), - password: SecretString::from("pwd"), - }, - } - } - - fn unsigned_jws(payload: serde_json::Value) -> String { - let engine = base64::engine::general_purpose::URL_SAFE_NO_PAD; - let header = engine.encode(r#"{"alg":"RS256"}"#); - let payload = engine.encode(serde_json::to_vec(&payload).expect("payload serializes")); - let signature = engine.encode(b"signature"); - format!("{header}.{payload}.{signature}") - } - - fn association_token(jti: Uuid) -> String { - unsigned_jws(serde_json::json!({ - "jti": jti, - "dst_hst": "target.example:3389" - })) - } - - fn dummy_entry_with_target_username(jti: Uuid, target_username: &str) -> ArcCredentialEntry { - let store = CredentialStoreHandle::new(); - store - .insert( - association_token(jti), - Some(cleartext_mapping_with_target_username(target_username)), - time::Duration::minutes(5), - ) - .expect("credential entry inserts"); - - store.get(jti).expect("credential entry is indexed by JTI") - } - - fn dummy_entry(jti: Uuid) -> ArcCredentialEntry { - dummy_entry_with_target_username(jti, "target") - } - - fn dummy_kdc(jti: Uuid) -> CredentialInjectionKdc { - let entry = dummy_entry(jti); - let session = Arc::new(derive_credential_injection_kdc_session("proxy@example.invalid", jti)); - CredentialInjectionKdc::from_parts(jti, entry, "target.example".to_owned(), session) - .expect("valid credential-injection KDC") - } - - fn dummy_kdc_with_target_username(jti: Uuid, target_username: &str) -> CredentialInjectionKdc { - let entry = dummy_entry_with_target_username(jti, target_username); - let session = Arc::new(derive_credential_injection_kdc_session("proxy@example.invalid", jti)); - CredentialInjectionKdc::from_parts(jti, entry, "target.example".to_owned(), session) - .expect("valid credential-injection KDC") - } - - fn network_request(url: &str) -> NetworkRequest { - NetworkRequest { - protocol: NetworkProtocol::Http, - url: Url::parse(url).expect("test URL parses"), - data: Vec::new(), - } - } - - #[test] - fn proxy_user_at_realm_is_used_as_realm() { - let session = derive_credential_injection_kdc_session("proxy@example.invalid", Uuid::new_v4()); - assert_eq!(session.realm, "example.invalid"); - } - - #[test] - fn bare_proxy_username_yields_synthetic_realm() { - let jti = Uuid::new_v4(); - let session = derive_credential_injection_kdc_session("just-a-uuid", jti); - assert_eq!(session.realm, synthetic_realm(jti)); - assert!(!session.realm.is_empty()); - } - - #[test] - fn service_kdc_for_rejects_expired_credential_entry() { - let service = CredentialService::new(); - let jti = Uuid::new_v4(); - - // Negative TTL: entry is born already expired. `CredentialStoreHandle::get` does not - // filter on expiry, so the service's own check is what guarantees we never build a KDC - // over stale credentials. - service - .insert( - association_token(jti), - Some(cleartext_mapping_with_target_username("target")), - time::Duration::seconds(-1), - ) - .expect("credential entry inserts"); - - assert!( - matches!( - service.kdc_for(jti), - Err(CredentialInjectionKdcResolveError::ExpiredCredential { .. }) - ), - "expired credentials must not yield a KDC" - ); - } - - #[test] - fn service_kdc_for_returns_same_session_under_concurrent_calls() { - let service = CredentialService::new(); - let jti = Uuid::new_v4(); - - service - .insert( - association_token(jti), - Some(cleartext_mapping_with_target_username("target")), - time::Duration::minutes(5), - ) - .expect("credential entry inserts"); - - let first = service.kdc_for(jti).expect("first call resolves"); - let second = service.kdc_for(jti).expect("second call resolves"); - - // The Kerberos session is the piece that must be stable across calls; the per-call KDC - // view rebuilds the rest. Compare via the long-term acceptor key as a session-identity - // probe. - let first_key = first.session.acceptor.long_term_key.expose_secret().clone(); - let second_key = second.session.acceptor.long_term_key.expose_secret().clone(); - assert_eq!( - first_key, second_key, - "concurrent kdc_for must share one cached session per JTI" - ); - } - - #[test] - fn service_insert_drops_stale_session_even_without_credential_replacement() { - let service = CredentialService::new(); - let jti = Uuid::new_v4(); - - // Simulate the race called out by Codex: a previous provisioning's session is still - // cached, but the credential entry has already been evicted (e.g. by - // `credential::cleanup_task`) and `sweep_orphans` has not run yet. A fresh provisioning - // under the same JTI must drop the stale session regardless of whether - // `CredentialStoreHandle::insert` reports a replacement, otherwise the next `kdc_for` - // would reuse the old key material. - let stale_session = Arc::new(derive_credential_injection_kdc_session("proxy@example.invalid", jti)); - service.sessions.lock().insert(jti, Arc::clone(&stale_session)); - - let previous = service - .insert( - association_token(jti), - Some(cleartext_mapping_with_target_username("target")), - time::Duration::minutes(5), - ) - .expect("credential entry inserts"); - assert!(previous.is_none(), "test precondition: no credential replacement"); - - assert!( - !service.sessions.lock().contains_key(&jti), - "insert must drop stale session even when no credential replacement occurred" - ); - } - - #[test] - fn service_insert_replacement_drops_cached_kerberos_material() { - let service = CredentialService::new(); - let jti = Uuid::new_v4(); - - service - .insert( - association_token(jti), - Some(cleartext_mapping_with_target_username("target")), - time::Duration::minutes(5), - ) - .expect("credential entry inserts"); - - let first = service.kdc_for(jti).expect("first call resolves"); - let first_key = first.session.acceptor.long_term_key.expose_secret().clone(); - - // Re-insert under the same JTI: the cached session for the previous entry must be evicted - // automatically, otherwise the new KDC would carry stale key material that the freshly - // provisioned credentials no longer match. - service - .insert( - association_token(jti), - Some(cleartext_mapping_with_target_username("target")), - time::Duration::minutes(5), - ) - .expect("credential entry re-inserts"); - - let second = service.kdc_for(jti).expect("second call resolves with fresh session"); - let second_key = second.session.acceptor.long_term_key.expose_secret().clone(); - - assert_ne!( - first_key, second_key, - "insert-replacement must force a fresh session derivation" - ); - } - - #[test] - fn service_sweep_orphans_drops_sessions_with_no_credential_entry() { - let service = CredentialService::new(); - let jti = Uuid::new_v4(); - - service - .insert( - association_token(jti), - Some(cleartext_mapping_with_target_username("target")), - time::Duration::minutes(5), - ) - .expect("credential entry inserts"); - - service.kdc_for(jti).expect("kdc_for populates session cache"); - assert!(service.sessions.lock().contains_key(&jti), "session cached"); - - // Simulate credential store eviction: build a parallel service whose credential store is - // empty but whose session cache is shared with the original. A more faithful test would - // drive `credential::cleanup_task` to expire the entry, but it sleeps for 15 minutes - // between ticks. Swapping the inner store is the deterministic equivalent. - let orphaned_service = CredentialService { - credentials: CredentialStoreHandle::new(), - sessions: Arc::clone(&service.sessions), - }; - - orphaned_service.sweep_orphans(); - assert!( - !orphaned_service.sessions.lock().contains_key(&jti), - "sweep must drop sessions whose JTI is no longer in credential_store" - ); - } - - #[test] - fn client_acceptor_protocol_is_ntlm_for_domainless_target_credential() { - let kdc = dummy_kdc_with_target_username(Uuid::new_v4(), "Administrator"); - - assert_eq!( - kdc.client_acceptor_protocol().expect("protocol selected"), - CredentialInjectionClientAcceptorProtocol::Ntlm - ); - } - - #[test] - fn client_acceptor_protocol_is_kerberos_for_upn_target_credential() { - let kdc = dummy_kdc_with_target_username(Uuid::new_v4(), "administrator@example.invalid"); - - assert_eq!( - kdc.client_acceptor_protocol().expect("protocol selected"), - CredentialInjectionClientAcceptorProtocol::Kerberos - ); - } - - #[test] - fn client_acceptor_protocol_is_kerberos_for_downlevel_target_credential() { - let kdc = dummy_kdc_with_target_username(Uuid::new_v4(), "EXAMPLE\\Administrator"); - - assert_eq!( - kdc.client_acceptor_protocol().expect("protocol selected"), - CredentialInjectionClientAcceptorProtocol::Kerberos - ); - } - - #[test] - fn from_parts_rejects_mismatched_entry_and_session_jti() { - let entry_jti = Uuid::new_v4(); - let session_jti = Uuid::new_v4(); - assert_ne!(entry_jti, session_jti); - - let entry = dummy_entry(entry_jti); - let session = Arc::new(derive_credential_injection_kdc_session( - "proxy@example.invalid", - session_jti, - )); - - let err = CredentialInjectionKdc::from_parts(entry_jti, entry, "target.example".to_owned(), session) - .expect_err("mismatched entry/session JTI must fail closed"); - let msg = format!("{err:#}"); - assert!( - msg.contains("credential entry JTI does not match credential-injection KDC session JTI"), - "actual: {msg}" - ); - } - - #[test] - fn service_kdc_for_rejects_unknown_jti() { - let service = CredentialService::new(); - - assert!( - matches!( - service.kdc_for(Uuid::new_v4()), - Err(CredentialInjectionKdcResolveError::MissingCredential { .. }) - ), - "KDC tokens with jet_cred_id must not fall back to real-KDC forwarding" - ); - } - - #[test] - fn service_kdc_for_rejects_non_injection_entry() { - let service = CredentialService::new(); - let jti = Uuid::new_v4(); - - service - .insert(association_token(jti), None, time::Duration::minutes(5)) - .expect("provision-token entry inserts"); - - assert!( - matches!( - service.kdc_for(jti), - Err(CredentialInjectionKdcResolveError::NonInjectionCredential { .. }) - ), - "KDC tokens with jet_cred_id must require provision-credentials state" - ); - } - - #[test] - fn service_kdc_for_lazily_extracts_target_hostname_from_entry_token() { - let service = CredentialService::new(); - let jti = Uuid::new_v4(); - - service - .insert( - association_token(jti), - Some(cleartext_mapping_with_target_username("target")), - time::Duration::minutes(5), - ) - .expect("credential entry inserts"); - - let kdc = service.kdc_for(jti).expect("credential-injection KDC resolves"); - - assert_eq!(kdc.target_hostname, "target.example"); - } - - #[test] - fn intercept_ignores_non_loopback_host() { - let jti = Uuid::new_v4(); - let kdc = dummy_kdc(jti); - - let request = network_request("http://kdc.real.example/path"); - let result = kdc - .intercept_network_request(&request) - .expect("non-loopback request dispatches"); - - assert!(matches!( - result, - CredentialInjectionKdcInterception::NotInjectionRequest - )); - } - - #[test] - fn intercept_rejects_malformed_url_path() { - let jti = Uuid::new_v4(); - let kdc = dummy_kdc(jti); - - let request = network_request("http://cred.invalid/not-a-uuid"); - let err = kdc - .intercept_network_request(&request) - .expect_err("non-UUID path must fail"); - let msg = format!("{err:#}"); - assert!(msg.contains("malformed in-process KDC URL"), "actual: {msg}"); - } - - #[test] - fn intercept_rejects_mismatched_jti() { - let entry_jti = Uuid::new_v4(); - let other_jti = Uuid::new_v4(); - assert_ne!(entry_jti, other_jti); - - let kdc = dummy_kdc(entry_jti); - - let request = network_request(&format!("http://cred.invalid/{}", other_jti)); - let err = kdc - .intercept_network_request(&request) - .expect_err("JTI mismatch must fail"); - let msg = format!("{err:#}"); - assert!(msg.contains("does not match current CredSSP session"), "actual: {msg}"); - } - - #[test] - fn intercept_accepts_matching_url_path_before_payload_decode() { - let jti = Uuid::new_v4(); - let kdc = dummy_kdc(jti); - - let request = network_request(&format!("http://cred.invalid/{jti}")); - let err = kdc - .intercept_network_request(&request) - .expect_err("empty KDC payload must fail after URL/JTI validation"); - let msg = format!("{err:#}"); - assert!(msg.contains("malformed in-process KDC proxy payload"), "actual: {msg}"); - } - - #[test] - fn realm_mismatch_is_reported_as_not_injection_realm() { - let mismatch = - realm_mismatch("cred-session.invalid", "evil.example").expect("different realms produce a mismatch"); - assert_eq!(mismatch.expected, "cred-session.invalid"); - assert_eq!(mismatch.actual, "evil.example"); - } - - #[test] - fn missing_kdc_proxy_envelope_realm_falls_back_to_session_realm() { - let jti = Uuid::new_v4(); - let kdc = dummy_kdc(jti); - let message = KdcProxyMessage::from_raw_kerb_message(&[]).expect("KDC proxy wrapper builds"); - - assert_eq!(kdc.resolve_message_realm(&message), "example.invalid"); - } -} diff --git a/devolutions-gateway/src/generic_client.rs b/devolutions-gateway/src/generic_client.rs index dfc31df7f..17d552da1 100644 --- a/devolutions-gateway/src/generic_client.rs +++ b/devolutions-gateway/src/generic_client.rs @@ -8,13 +8,14 @@ use tracing::field; use typed_builder::TypedBuilder; use crate::config::Conf; -use crate::credential_injection_kdc::CredentialService; +use crate::credential_injection::{CredentialInjection, SyntheticKdcRegistry}; +use crate::provisioning::ProvisioningStore; use crate::proxy::Proxy; use crate::rdp_pcb::{extract_association_claims, read_pcb}; use crate::recording::ActiveRecordings; use crate::session::{ConnectionModeDetails, DisconnectInterest, SessionInfo, SessionMessageSender}; use crate::subscriber::SubscriberSender; -use crate::token::{self, ConnectionMode, CurrentJrl, RecordingPolicy, TokenCache}; +use crate::token::{self, ConnectionMode, CurrentJrl, Protocol, RecordingPolicy, TokenCache}; use crate::upstream::{self, ConnectedUpstream}; #[derive(TypedBuilder)] @@ -27,7 +28,8 @@ pub struct GenericClient { sessions: SessionMessageSender, subscriber_tx: SubscriberSender, active_recordings: Arc, - credentials: CredentialService, + synthetic_kdc_registry: SyntheticKdcRegistry, + provisioning: ProvisioningStore, #[builder(default)] agent_tunnel_handle: Option>, } @@ -51,7 +53,8 @@ where sessions, subscriber_tx, active_recordings, - credentials, + synthetic_kdc_registry, + provisioning, agent_tunnel_handle, } = self; @@ -128,7 +131,7 @@ where span.record("target", selected_target.to_string()); - let is_rdp = claims.jet_ap == token::ApplicationProtocol::Known(token::Protocol::Rdp); + let is_rdp = matches!(claims.jet_ap, token::ApplicationProtocol::Known(Protocol::Rdp)); let info = SessionInfo::builder() .id(claims.jet_aid) @@ -143,23 +146,19 @@ where let disconnect_interest = DisconnectInterest::from_reconnection_policy(claims.jet_reuse); - // We support proxy-based credential injection for RDP. - // If a credential mapping has been pushed, we automatically switch to this mode. - // Otherwise, we continue the generic procedure. - // - // RdpProxy is generic over the server stream, so credential injection works - // regardless of whether the upstream is direct TCP or tunnelled via an agent. - // The credential store is keyed on the association token's JTI, so a direct - // lookup by `claims.jti` is the primary path. - if is_rdp - && let Some(entry) = credentials.get(claims.jti) - && entry.mapping.is_some() - { - anyhow::ensure!(token == entry.token, "token mismatch"); - let credential_injection_kdc = credentials.kdc_for(claims.jti)?; + if is_rdp && let Some(provisioned_connection) = provisioning.get(claims.jti) { + let credential_injection = CredentialInjection::from_provisioned( + claims.jti, + provisioned_connection, + selected_target.host(), + conf.debug.enable_unstable && conf.debug.kerberos_credential_injection, + )?; + + let credential_injection = + credential_injection.maybe_register_synthetic_kdc(&synthetic_kdc_registry); info!( - jti = %credential_injection_kdc.jti(), + jti = %credential_injection.jti(), "RDP-TLS forwarding with credential injection" ); @@ -169,21 +168,24 @@ where agent_tunnel_handle.clone(), ); - // NOTE: In the future, we could imagine performing proxy-based recording as well using RdpProxy. - return crate::rdp_proxy::RdpProxy::builder() + let credssp_session = crate::rdp_proxy::CredsspSession::builder() .conf(conf) .session_info(info) .client_addr(client_addr) - .client_stream(client_stream) .server_addr(server_addr) - .server_stream(server_stream) + .credential_injection(credential_injection) .sessions(sessions) .subscriber_tx(subscriber_tx) - .credential_injection_kdc(credential_injection_kdc) - .client_stream_leftover_bytes(leftover_bytes) .server_dns_name(selected_target.host().to_owned()) .disconnect_interest(disconnect_interest) .kdc_connector(kdc_connector) + .build(); + + return crate::rdp_proxy::RdpProxy::builder() + .session(credssp_session) + .client_stream(client_stream) + .server_stream(server_stream) + .client_stream_leftover_bytes(leftover_bytes) .build() .run() .await diff --git a/devolutions-gateway/src/kdc_connector.rs b/devolutions-gateway/src/kdc_connector.rs index 7b5cd1eca..f1840b643 100644 --- a/devolutions-gateway/src/kdc_connector.rs +++ b/devolutions-gateway/src/kdc_connector.rs @@ -261,7 +261,7 @@ impl KdcConnector { /// goes away entirely. pub async fn send_network_request(&self, request: &NetworkRequest) -> anyhow::Result> { match request.url.scheme() { - "tcp" | "udp" => { + scheme if crate::target_connection_options::is_supported_krb_kdc_scheme(scheme) => { let target_addr = TargetAddr::parse(request.url.as_str(), Some(88))?; self.send(&target_addr, &request.data) diff --git a/devolutions-gateway/src/lib.rs b/devolutions-gateway/src/lib.rs index a55556b59..6752ddab0 100644 --- a/devolutions-gateway/src/lib.rs +++ b/devolutions-gateway/src/lib.rs @@ -17,7 +17,7 @@ pub mod api; pub mod cli; pub mod config; pub mod credential; -pub mod credential_injection_kdc; +pub mod credential_injection; pub mod extract; pub mod generic_client; pub mod http; @@ -30,6 +30,7 @@ pub mod log; pub mod middleware; pub mod ngrok; pub mod plugin_manager; +pub mod provisioning; pub mod proxy; pub mod rd_clean_path; pub mod rdp_pcb; @@ -39,6 +40,7 @@ pub mod session; pub mod streaming; pub mod subscriber; pub mod target_addr; +pub(crate) mod target_connection_options; pub mod tls; pub mod token; pub mod traffic_audit; @@ -61,7 +63,8 @@ pub struct DgwState { pub shutdown_signal: devolutions_gateway_task::ShutdownSignal, pub recordings: recording::RecordingMessageSender, pub job_queue_handle: job_queue::JobQueueHandle, - pub credentials: credential_injection_kdc::CredentialService, + pub provisioning: provisioning::ProvisioningStore, + pub synthetic_kdc_registry: credential_injection::SyntheticKdcRegistry, pub monitoring_state: Arc, pub traffic_audit_handle: traffic_audit::TrafficAuditHandle, pub agent_tunnel_handle: Option>, @@ -89,7 +92,8 @@ impl DgwState { let (shutdown_handle, shutdown_signal) = devolutions_gateway_task::ShutdownHandle::new(); let (job_queue_handle, job_queue_rx) = job_queue::JobQueueHandle::new(); let (traffic_audit_handle, traffic_audit_rx) = traffic_audit::TrafficAuditHandle::new(); - let credentials = credential_injection_kdc::CredentialService::new(); + let provisioning = provisioning::ProvisioningStore::new(); + let synthetic_kdc_registry = credential_injection::SyntheticKdcRegistry::new(); let monitoring_state = Arc::new(network_monitor::State::new(Arc::new(MockMonitorsCache))?); let state = Self { @@ -102,7 +106,8 @@ impl DgwState { recordings: recording_manager_handle, job_queue_handle, traffic_audit_handle, - credentials, + provisioning, + synthetic_kdc_registry, monitoring_state, agent_tunnel_handle: None, }; diff --git a/devolutions-gateway/src/listener.rs b/devolutions-gateway/src/listener.rs index 5e23f5f8a..5e972019a 100644 --- a/devolutions-gateway/src/listener.rs +++ b/devolutions-gateway/src/listener.rs @@ -158,7 +158,8 @@ async fn handle_tcp_peer(stream: TcpStream, state: DgwState, peer_addr: SocketAd .sessions(state.sessions) .subscriber_tx(state.subscriber_tx) .active_recordings(state.recordings.active_recordings) - .credentials(state.credentials) + .synthetic_kdc_registry(state.synthetic_kdc_registry) + .provisioning(state.provisioning) .agent_tunnel_handle(state.agent_tunnel_handle) .build() .serve() diff --git a/devolutions-gateway/src/ngrok.rs b/devolutions-gateway/src/ngrok.rs index 9e2e846bd..644f6630c 100644 --- a/devolutions-gateway/src/ngrok.rs +++ b/devolutions-gateway/src/ngrok.rs @@ -237,7 +237,8 @@ async fn run_tcp_tunnel(mut tunnel: ngrok::tunnel::TcpTunnel, state: DgwState) { .sessions(state.sessions) .subscriber_tx(state.subscriber_tx) .active_recordings(state.recordings.active_recordings) - .credentials(state.credentials) + .synthetic_kdc_registry(state.synthetic_kdc_registry) + .provisioning(state.provisioning) .agent_tunnel_handle(state.agent_tunnel_handle) .build() .serve() diff --git a/devolutions-gateway/src/openapi.rs b/devolutions-gateway/src/openapi.rs index 1e8b3e212..8b0b72d8d 100644 --- a/devolutions-gateway/src/openapi.rs +++ b/devolutions-gateway/src/openapi.rs @@ -68,6 +68,7 @@ use crate::config::dto::{DataEncoding, PubKeyFormat, Subscriber}; PreflightOperationKind, AppCredential, AppCredentialKind, + TargetConnectionOptions, PreflightOutput, PreflightOutputKind, PreflightAlertStatus, @@ -372,7 +373,7 @@ struct PreflightOperation { kind: PreflightOperationKind, /// The token to be stored on the proxy-side. /// - /// Required for "provision-token" and "provision-credentials" kinds. + /// Required for "provision-token", "provision-credentials" and "provision-connection-options" kinds. token: Option, /// The credential to use to authorize the client at the proxy-level. /// @@ -382,16 +383,29 @@ struct PreflightOperation { /// /// Required for "provision-credentials" kind. target_credential: Option, + /// Options used by the Gateway when connecting to the target. + /// + /// Required for "provision-connection-options" kind. + connection_options: Option, /// The hostname to perform DNS resolution on. /// /// Required for "resolve-host" kind. host_to_resolve: Option, /// Minimum persistence duration in seconds for the data provisioned via this operation. /// - /// Optional parameter for "provision-token" and "provision-credentials" kinds. + /// Optional parameter for "provision-token", "provision-credentials" and "provision-connection-options" kinds. time_to_live: Option, } +#[allow(unused)] +#[derive(Deserialize, utoipa::ToSchema)] +struct TargetConnectionOptions { + /// Kerberos KDC address for the target-side CredSSP connection. + /// + /// Supported schemes are `tcp` and `udp`. + krb_kdc: Option, +} + #[derive(Deserialize, utoipa::ToSchema)] enum PreflightOperationKind { #[serde(rename = "get-version")] @@ -406,6 +420,8 @@ enum PreflightOperationKind { ProvisionToken, #[serde(rename = "provision-credentials")] ProvisionCredentials, + #[serde(rename = "provision-connection-options")] + ProvisionConnectionOptions, #[serde(rename = "resolve-host")] ResolveHost, } diff --git a/devolutions-gateway/src/provisioning.rs b/devolutions-gateway/src/provisioning.rs new file mode 100644 index 000000000..81a0a9e86 --- /dev/null +++ b/devolutions-gateway/src/provisioning.rs @@ -0,0 +1,237 @@ +use std::collections::HashMap; +use std::sync::Arc; + +use anyhow::Context as _; +use async_trait::async_trait; +use devolutions_gateway_task::{ShutdownSignal, Task}; +use parking_lot::Mutex; +use uuid::Uuid; + +use crate::credential::{AppCredentials, CleartextAppCredentials}; +use crate::target_connection_options::TargetConnectionOptions; + +/// A combined, point-in-time view of everything provisioned for a session, assembled on read from +/// the two independent stores. +/// +/// Credentials are always present; connection options are optional and may be absent — never +/// provisioned, or expired before the credentials half. +#[derive(Debug, Clone)] +pub(crate) struct ProvisionedConnection { + pub(crate) credentials: AppCredentials, + pub(crate) connection_options: Option, + pub(crate) target_hostname: String, +} + +#[derive(Debug, Clone)] +struct CredentialsEntry { + credentials: AppCredentials, + target_hostname: String, + expires_at: time::OffsetDateTime, +} + +#[derive(Debug, Clone)] +struct ConnectionOptionsEntry { + connection_options: TargetConnectionOptions, + expires_at: time::OffsetDateTime, +} + +/// Two independent token-keyed stores that together provision a session. +/// +/// The credentials store is the encryption boundary: cleartext mappings are encrypted on the way +/// in, so entries only ever hold encrypted material and the master key never leaves the credential +/// module's dependency graph. The connection-options store holds plaintext routing metadata only +/// and has no crypto dependency at all — keeping the two apart is what preserves that property. +/// +/// Both are keyed by the association-token JTI, but the two halves are provisioned by separate +/// preflight operations and may arrive, expire, or be replaced independently. +#[derive(Debug, Clone)] +pub struct ProvisioningStore { + credentials: Arc>>, + connection_options: Arc>>, +} + +impl Default for ProvisioningStore { + fn default() -> Self { + Self::new() + } +} + +impl ProvisioningStore { + pub fn new() -> Self { + Self { + credentials: Arc::new(Mutex::new(HashMap::new())), + connection_options: Arc::new(Mutex::new(HashMap::new())), + } + } + + pub(crate) fn insert_credentials( + &self, + token_data: crate::token::CredentialInjectionTokenData, + credentials: CleartextAppCredentials, + time_to_live: time::Duration, + ) -> anyhow::Result { + let credentials = credentials.encrypt().context("encrypt provisioned credentials")?; + let entry = CredentialsEntry { + credentials, + target_hostname: token_data.target_hostname, + expires_at: time::OffsetDateTime::now_utc() + time_to_live, + }; + + Ok(self.credentials.lock().insert(token_data.jti, entry).is_some()) + } + + pub(crate) fn insert_connection_options( + &self, + jti: Uuid, + connection_options: TargetConnectionOptions, + time_to_live: time::Duration, + ) -> bool { + let entry = ConnectionOptionsEntry { + connection_options, + expires_at: time::OffsetDateTime::now_utc() + time_to_live, + }; + + self.connection_options.lock().insert(jti, entry).is_some() + } + + /// Assemble the provisioned view for a session. + /// + /// Returns `None` unless the credentials half is present and live; folds in the connection-options + /// half when it too is present. Entries live until their TTL, not until first use: a jti names a + /// session, and the token layer allows several connections per session (reconnects), each of which + /// needs the same materials. Either half is treated as absent once expired; the cleanup task + /// reclaims them. + pub(crate) fn get(&self, jti: Uuid) -> Option { + let now = time::OffsetDateTime::now_utc(); + + let (credentials, target_hostname) = { + let entries = self.credentials.lock(); + let entry = entries.get(&jti)?; + if now >= entry.expires_at { + warn!(%jti, "Provisioned credentials expired before the connection arrived"); + return None; + } + (entry.credentials.clone(), entry.target_hostname.clone()) + }; + + let connection_options = self.get_live_connection_options(jti, now); + + Some(ProvisionedConnection { + credentials, + connection_options, + target_hostname, + }) + } + + fn get_live_connection_options(&self, jti: Uuid, now: time::OffsetDateTime) -> Option { + let entries = self.connection_options.lock(); + let entry = entries.get(&jti)?; + if now >= entry.expires_at { + warn!(%jti, "Provisioned connection options expired before the connection arrived"); + return None; + } + Some(entry.connection_options.clone()) + } + + pub fn cleanup_task(&self) -> impl Task> + 'static + use<> { + CleanupTask { + credentials: Arc::clone(&self.credentials), + connection_options: Arc::clone(&self.connection_options), + } + } +} + +struct CleanupTask { + credentials: Arc>>, + connection_options: Arc>>, +} + +#[async_trait] +impl Task for CleanupTask { + type Output = anyhow::Result<()>; + + const NAME: &'static str = "provisioning cleanup"; + + async fn run(self, shutdown_signal: ShutdownSignal) -> Self::Output { + cleanup_task(self.credentials, self.connection_options, shutdown_signal).await; + Ok(()) + } +} + +#[instrument(skip_all)] +async fn cleanup_task( + credentials: Arc>>, + connection_options: Arc>>, + mut shutdown_signal: ShutdownSignal, +) { + use tokio::time::{Duration, sleep}; + + const TASK_INTERVAL: Duration = Duration::from_secs(60 * 15); + + debug!("Task started"); + + loop { + tokio::select! { + _ = sleep(TASK_INTERVAL) => {} + _ = shutdown_signal.wait() => { + break; + } + } + + let now = time::OffsetDateTime::now_utc(); + credentials.lock().retain(|_, entry| now < entry.expires_at); + connection_options.lock().retain(|_, entry| now < entry.expires_at); + } + + debug!("Task terminated"); +} + +#[cfg(test)] +mod tests { + use secrecy::SecretString; + use uuid::Uuid; + + use super::*; + use crate::credential::CleartextAppCredential; + use crate::token::CredentialInjectionTokenData; + + fn credentials() -> CleartextAppCredentials { + CleartextAppCredentials { + proxy: CleartextAppCredential::UsernamePassword { + username: "proxy".to_owned(), + password: SecretString::from("pwd"), + }, + target: CleartextAppCredential::UsernamePassword { + username: "target".to_owned(), + password: SecretString::from("pwd"), + }, + } + } + + fn token_data(jti: Uuid) -> CredentialInjectionTokenData { + CredentialInjectionTokenData { + jti, + target_hostname: "target.example".to_owned(), + } + } + + #[test] + fn get_returns_a_live_entry() { + let store = ProvisioningStore::new(); + let jti = Uuid::new_v4(); + store + .insert_credentials(token_data(jti), credentials(), time::Duration::minutes(5)) + .expect("entry inserts"); + assert_eq!(store.get(jti).expect("live entry").target_hostname, "target.example"); + } + + #[test] + fn get_treats_an_expired_entry_as_absent() { + let store = ProvisioningStore::new(); + let jti = Uuid::new_v4(); + store + .insert_credentials(token_data(jti), credentials(), time::Duration::seconds(-1)) + .expect("entry inserts"); + assert!(store.get(jti).is_none(), "an expired entry must read as absent"); + } +} diff --git a/devolutions-gateway/src/rd_clean_path.rs b/devolutions-gateway/src/rd_clean_path.rs index d36ebabe4..589be13fb 100644 --- a/devolutions-gateway/src/rd_clean_path.rs +++ b/devolutions-gateway/src/rd_clean_path.rs @@ -11,7 +11,8 @@ use tokio::io::{AsyncRead, AsyncReadExt as _, AsyncWrite, AsyncWriteExt as _}; use tracing::field; use crate::config::Conf; -use crate::credential_injection_kdc::{CredentialInjectionKdc, CredentialService}; +use crate::credential_injection::{CredentialInjection, SyntheticKdcRegistry}; +use crate::provisioning::ProvisioningStore; use crate::proxy::Proxy; use crate::recording::ActiveRecordings; use crate::session::{ConnectionModeDetails, DisconnectInterest, DisconnectedInfo, SessionInfo, SessionMessageSender}; @@ -313,13 +314,11 @@ async fn handle_with_credential_injection( mut client_stream: impl AsyncRead + AsyncWrite + Unpin + Send, client_addr: SocketAddr, conf: Arc, - token_cache: &TokenCache, - jrl: &CurrentJrl, sessions: SessionMessageSender, subscriber_tx: SubscriberSender, - active_recordings: &ActiveRecordings, + auth: CleanPathAuth, cleanpath_pdu: RDCleanPathPdu, - credential_injection_kdc: CredentialInjectionKdc, + credential_injection: CredentialInjection, agent_tunnel_handle: Option>, ) -> anyhow::Result<()> { let tls_conf = conf.credssp_tls.get().context("CredSSP TLS configuration")?; @@ -350,18 +349,7 @@ async fn handle_with_credential_injection( ) }; - // Authorize and connect to the RDP server. - let CleanPathAuth { claims } = authorize_cleanpath( - &cleanpath_pdu, - client_addr, - &conf, - token_cache, - jrl, - active_recordings, - &sessions, - ) - .await - .context("RDCleanPath authorization failed")?; + let CleanPathAuth { claims } = auth; let ConnectedRdpServer { tls_stream: server_stream, @@ -414,71 +402,9 @@ async fn handle_with_credential_injection( send_clean_path_response(&mut client_stream, &rd_clean_path_rsp).await?; debug!("RDCleanPath response sent, now performing CredSSP MITM"); - // -- Perform the CredSSP authentication with the client (acting as a server) and the server (acting as a client) -- // - - let mut client_framed = ironrdp_tokio::MovableTokioFramed::new(client_stream); - let mut server_framed = ironrdp_tokio::MovableTokioFramed::new(server_stream); - - let krb_configs = crate::rdp_proxy::credential_injection_kerberos_configs( - &conf, - client_addr, - &gateway_hostname, - &credential_injection_kdc, - )?; - let kdc_connector = crate::kdc_connector::KdcConnector::new(claims.jet_aid, claims.jet_agent_id, agent_tunnel_handle.clone()); - let client_credssp_fut = crate::rdp_proxy::perform_credssp_as_server( - &mut client_framed, - client_addr.ip(), - gateway_public_key, - client_security_protocol, - credential_injection_kdc.proxy_credential(), - krb_configs.server, - &credential_injection_kdc, - &kdc_connector, - ); - - let server_credssp_fut = crate::rdp_proxy::perform_credssp_as_client( - &mut server_framed, - destination.host().to_owned(), - server_public_key, - server_security_protocol, - credential_injection_kdc.target_credential(), - krb_configs.client, - &kdc_connector, - ); - - let (client_credssp_res, server_credssp_res) = tokio::join!(client_credssp_fut, server_credssp_fut); - client_credssp_res.context("CredSSP with client")?; - server_credssp_res.context("CredSSP with server")?; - - debug!("CredSSP MITM completed successfully"); - - // -- Intercept the Connect Confirm PDU, to override the server_security_protocol field -- // - - crate::rdp_proxy::intercept_connect_confirm(&mut client_framed, &mut server_framed, server_security_protocol) - .await?; - - let (mut client_stream, client_leftover) = client_framed.into_inner(); - let (mut server_stream, server_leftover) = server_framed.into_inner(); - - // -- At this point, proceed to the usual two-way forwarding -- // - - info!("RDP-TLS forwarding (credential injection)"); - - client_stream - .write_all(&server_leftover) - .await - .context("write server leftover to client")?; - - server_stream - .write_all(&client_leftover) - .await - .context("write client leftover to server")?; - - // Build SessionInfo for forwarding let info = SessionInfo::builder() .id(claims.jet_aid) .application_protocol(claims.jet_ap) @@ -492,21 +418,29 @@ async fn handle_with_credential_injection( let disconnect_interest = DisconnectInterest::from_reconnection_policy(claims.jet_reuse); - // Plain forwarding for now - Proxy::builder() + let credssp_session = crate::rdp_proxy::CredsspSession::builder() .conf(conf) .session_info(info) - .address_a(client_addr) - .transport_a(client_stream) - .address_b(server_addr) - .transport_b(server_stream) + .client_addr(client_addr) + .server_addr(server_addr) + .credential_injection(credential_injection) .sessions(sessions) .subscriber_tx(subscriber_tx) + .server_dns_name(destination.host().to_owned()) .disconnect_interest(disconnect_interest) - .build() - .select_dissector_and_forward() - .await - .context("proxy failed") + .kdc_connector(kdc_connector) + .build(); + + let prepared = crate::rdp_proxy::PreparedCredssp::builder() + .client_stream(client_stream) + .server_stream(server_stream) + .gateway_public_key(gateway_public_key) + .server_public_key(server_public_key) + .client_security_protocol(client_security_protocol) + .server_security_protocol(server_security_protocol) + .build(); + + credssp_session.run(prepared).await } #[expect(clippy::too_many_arguments)] @@ -520,7 +454,8 @@ pub async fn handle( sessions: SessionMessageSender, subscriber_tx: SubscriberSender, active_recordings: &ActiveRecordings, - credentials: &CredentialService, + synthetic_kdc_registry: &SyntheticKdcRegistry, + provisioning: &ProvisioningStore, agent_tunnel_handle: Option>, ) -> anyhow::Result<()> { // Special handshake of our RDP extension @@ -531,23 +466,44 @@ pub async fn handle( .await .context("couldn't read cleanpath PDU")?; - // Early credential detection: check if we should use RdpProxy instead. - let token = cleanpath_pdu - .proxy_auth - .as_deref() - .context("missing token in RDCleanPath PDU")?; - - // If a credential mapping has been pushed, we automatically switch to - // proxy-based credential injection mode. Otherwise, we continue the usual - // clean path procedure. The credential store is keyed on the association token's JTI. - if let Some(jti) = crate::token::extract_jti(token).ok() - && let Some(entry) = credentials.get(jti) - && entry.mapping.is_some() + let auth = match authorize_cleanpath( + &cleanpath_pdu, + client_addr, + &conf, + token_cache, + jrl, + active_recordings, + &sessions, + ) + .await { - let credential_injection_kdc = credentials.kdc_for(jti)?; - anyhow::ensure!(token == credential_injection_kdc.raw_token(), "token mismatch"); + Ok(auth) => auth, + Err(error) => { + let response = RDCleanPathPdu::from(&error); + send_clean_path_response(&mut client_stream, &response).await?; + return anyhow::Error::new(error) + .context("RDCleanPath authorization failed") + .pipe(Err); + } + }; + + let target_hostname = match &auth.claims.jet_cm { + crate::token::ConnectionMode::Fwd { targets } => targets.first().host().to_owned(), + crate::token::ConnectionMode::Rdv => anyhow::bail!("authorize_cleanpath rejects rendezvous mode"), + }; + + if let Some(provisioned_connection) = provisioning.get(auth.claims.jti) { + let credential_injection = CredentialInjection::from_provisioned( + auth.claims.jti, + provisioned_connection, + &target_hostname, + conf.debug.enable_unstable && conf.debug.kerberos_credential_injection, + )?; + + let credential_injection = credential_injection.maybe_register_synthetic_kdc(synthetic_kdc_registry); + debug!( - jti = %credential_injection_kdc.jti(), + jti = %credential_injection.jti(), "Switching to RdpProxy for credential injection (WebSocket)" ); @@ -555,13 +511,11 @@ pub async fn handle( client_stream, client_addr, conf, - token_cache, - jrl, sessions, subscriber_tx, - active_recordings, + auth, cleanpath_pdu, - credential_injection_kdc, + credential_injection, agent_tunnel_handle.clone(), ) .await; @@ -569,25 +523,8 @@ pub async fn handle( trace!("Processing RDCleanPath"); - let (auth, connected) = match async { - let auth = authorize_cleanpath( - &cleanpath_pdu, - client_addr, - &conf, - token_cache, - jrl, - active_recordings, - &sessions, - ) - .await?; - - let connected = connect_rdp_server(&auth.claims, cleanpath_pdu, agent_tunnel_handle.as_ref()).await?; - - Ok::<_, CleanPathError>((auth, connected)) - } - .await - { - Ok(result) => result, + let connected = match connect_rdp_server(&auth.claims, cleanpath_pdu, agent_tunnel_handle.as_ref()).await { + Ok(connected) => connected, Err(error) => { let response = RDCleanPathPdu::from(&error); send_clean_path_response(&mut client_stream, &response).await?; diff --git a/devolutions-gateway/src/rdp_proxy.rs b/devolutions-gateway/src/rdp_proxy.rs deleted file mode 100644 index 856210fa1..000000000 --- a/devolutions-gateway/src/rdp_proxy.rs +++ /dev/null @@ -1,689 +0,0 @@ -use std::net::{IpAddr, SocketAddr}; -use std::sync::Arc; - -use anyhow::Context as _; -use ironrdp_acceptor::credssp::CredsspProcessGenerator as CredsspServerProcessGenerator; -use ironrdp_connector::credssp::CredsspProcessGenerator as CredsspClientProcessGenerator; -use ironrdp_connector::sspi; -use ironrdp_connector::sspi::generator::GeneratorState; -use ironrdp_pdu::{mcs, nego, x224}; -use secrecy::ExposeSecret as _; -use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt}; -use typed_builder::TypedBuilder; - -use crate::config::Conf; -use crate::credential::AppCredential; -use crate::credential_injection_kdc::{ - CredentialInjectionClientAcceptorProtocol, CredentialInjectionKdc, CredentialInjectionKdcInterception, -}; -use crate::kdc_connector::KdcConnector; -use crate::proxy::Proxy; -use crate::session::{DisconnectInterest, SessionInfo, SessionMessageSender}; -use crate::subscriber::SubscriberSender; - -#[derive(TypedBuilder)] -pub struct RdpProxy { - conf: Arc, - session_info: SessionInfo, - client_stream: C, - client_addr: SocketAddr, - server_stream: S, - server_addr: SocketAddr, - credential_injection_kdc: CredentialInjectionKdc, - client_stream_leftover_bytes: bytes::BytesMut, - sessions: SessionMessageSender, - subscriber_tx: SubscriberSender, - server_dns_name: String, - disconnect_interest: Option, - /// Outbound dispatcher for CredSSP-originated KDC traffic. Encapsulates whether KDC - /// requests should attempt agent-tunnel routing (and any `jet_agent_id` pin from the - /// parent association token) or always go direct. - kdc_connector: KdcConnector, -} - -impl RdpProxy -where - A: AsyncWrite + AsyncRead + Unpin + Send, - B: AsyncWrite + AsyncRead + Unpin + Send, -{ - pub async fn run(self) -> anyhow::Result<()> { - handle(self).await - } -} - -#[instrument("rdp_proxy", skip_all, fields(session_id = proxy.session_info.id.to_string(), target = proxy.server_addr.to_string()))] -async fn handle(proxy: RdpProxy) -> anyhow::Result<()> -where - C: AsyncRead + AsyncWrite + Unpin + Send, - S: AsyncRead + AsyncWrite + Unpin + Send, -{ - let RdpProxy { - conf, - session_info, - client_stream, - client_addr, - server_stream, - server_addr, - credential_injection_kdc, - client_stream_leftover_bytes, - sessions, - subscriber_tx, - server_dns_name, - disconnect_interest, - kdc_connector, - } = proxy; - - let tls_conf = conf.credssp_tls.get().context("CredSSP TLS configuration")?; - let gateway_hostname = conf.hostname.clone(); - - // -- Retrieve the Gateway TLS public key that must be used for client-proxy CredSSP later on -- // - - let gateway_cert_chain_handle = tokio::spawn(crate::tls::get_cert_chain_for_acceptor_cached( - gateway_hostname.clone(), - tls_conf.acceptor.clone(), - )); - - // -- Dual handshake with the client and the server until the TLS security upgrade -- // - - let mut client_framed = - ironrdp_tokio::MovableTokioFramed::new_with_leftover(client_stream, client_stream_leftover_bytes); - let mut server_framed = ironrdp_tokio::MovableTokioFramed::new(server_stream); - - let handshake_result = dual_handshake_until_tls_upgrade( - &mut client_framed, - &mut server_framed, - credential_injection_kdc.target_credential(), - ) - .await?; - - let client_stream = client_framed.into_inner_no_leftover(); - let server_stream = server_framed.into_inner_no_leftover(); - - // -- Perform the TLS upgrading for both the client and the server, effectively acting as a man-in-the-middle -- // - - let client_tls_upgrade_fut = tls_conf.acceptor.accept(client_stream); - let server_tls_upgrade_fut = crate::tls::dangerous_connect(server_dns_name.clone(), server_stream); - - let (client_stream, server_stream) = tokio::join!(client_tls_upgrade_fut, server_tls_upgrade_fut); - - let client_stream = client_stream.context("TLS upgrade with client failed")?; - let server_stream = server_stream.context("TLS upgrade with server failed")?; - - let server_public_key = - crate::tls::extract_stream_peer_public_key(&server_stream).context("extract target server TLS public key")?; - - let gateway_cert_chain = gateway_cert_chain_handle.await??; - let gateway_public_key = crate::tls::extract_public_key(gateway_cert_chain.first().context("no leaf")?) - .context("extract Gateway public key")?; - - // -- Perform the CredSSP authentication with the client (acting as a server) and the server (acting as a client) -- // - - let mut client_framed = ironrdp_tokio::MovableTokioFramed::new(client_stream); - let mut server_framed = ironrdp_tokio::MovableTokioFramed::new(server_stream); - - let krb_configs = - credential_injection_kerberos_configs(&conf, client_addr, &gateway_hostname, &credential_injection_kdc)?; - - let client_credssp_fut = perform_credssp_as_server( - &mut client_framed, - client_addr.ip(), - gateway_public_key, - handshake_result.client_security_protocol, - credential_injection_kdc.proxy_credential(), - krb_configs.server, - &credential_injection_kdc, - &kdc_connector, - ); - - let server_credssp_fut = perform_credssp_as_client( - &mut server_framed, - server_dns_name, - server_public_key, - handshake_result.server_security_protocol, - credential_injection_kdc.target_credential(), - krb_configs.client, - &kdc_connector, - ); - - let (client_credssp_res, server_credssp_res) = tokio::join!(client_credssp_fut, server_credssp_fut); - client_credssp_res.context("CredSSP with client")?; - server_credssp_res.context("CredSSP with server")?; - - // -- Intercept the Connect Confirm PDU, to override the server_security_protocol field -- // - - intercept_connect_confirm( - &mut client_framed, - &mut server_framed, - handshake_result.server_security_protocol, - ) - .await?; - - let (mut client_stream, client_leftover) = client_framed.into_inner(); - let (mut server_stream, server_leftover) = server_framed.into_inner(); - - // -- At this point, proceed to the usual two-way forwarding -- // - - info!("RDP-TLS forwarding (credential injection)"); - - client_stream - .write_all(&server_leftover) - .await - .context("write server leftover to client")?; - - server_stream - .write_all(&client_leftover) - .await - .context("write client leftover to server")?; - - Proxy::builder() - .conf(conf) - .session_info(session_info) - .address_a(client_addr) - .transport_a(client_stream) - .address_b(server_addr) - .transport_b(server_stream) - .sessions(sessions) - .subscriber_tx(subscriber_tx) - .disconnect_interest(disconnect_interest) - .build() - .select_dissector_and_forward() - .await - .context("RDP-TLS traffic proxying failed")?; - - Ok(()) -} - -#[derive(Debug)] -struct HandshakeResult { - client_security_protocol: nego::SecurityProtocol, - server_security_protocol: nego::SecurityProtocol, -} - -#[instrument(level = "debug", ret, skip_all)] -pub(crate) async fn intercept_connect_confirm( - client_framed: &mut ironrdp_tokio::MovableTokioFramed, - server_framed: &mut ironrdp_tokio::MovableTokioFramed, - server_security_protocol: nego::SecurityProtocol, -) -> anyhow::Result<()> -where - C: AsyncWrite + AsyncRead + Unpin + Send, - S: AsyncWrite + AsyncRead + Unpin + Send, -{ - let (_, received_frame) = client_framed - .read_pdu() - .await - .context("read MCS Connect Initial from client")?; - let received_connect_initial: x224::X224> = - ironrdp_core::decode(&received_frame).context("decode PDU from client")?; - let mut received_connect_initial: mcs::ConnectInitial = - ironrdp_core::decode(&received_connect_initial.0.data).context("decode Connect Initial PDU")?; - trace!(message = ?received_connect_initial, "Received Connect Initial PDU from client"); - - let mut gcc_blocks = received_connect_initial.conference_create_request.into_gcc_blocks(); - gcc_blocks.core.optional_data.server_selected_protocol = Some(server_security_protocol); - // Update the conference request with modified gcc_blocks. - received_connect_initial.conference_create_request = ironrdp_pdu::gcc::ConferenceCreateRequest::new(gcc_blocks)?; - trace!(message = ?received_connect_initial, "Send Connection Request PDU to server"); - let x224_msg_buf = ironrdp_core::encode_vec(&received_connect_initial)?; - let pdu = x224::X224Data { - data: std::borrow::Cow::Owned(x224_msg_buf), - }; - send_pdu(server_framed, &x224::X224(pdu)) - .await - .context("send connection request to server")?; - - Ok(()) -} - -#[instrument(name = "dual_handshake", level = "debug", ret, skip_all)] -async fn dual_handshake_until_tls_upgrade( - client_framed: &mut ironrdp_tokio::MovableTokioFramed, - server_framed: &mut ironrdp_tokio::MovableTokioFramed, - target_credential: &AppCredential, -) -> anyhow::Result -where - C: AsyncWrite + AsyncRead + Unpin + Send, - S: AsyncWrite + AsyncRead + Unpin + Send, -{ - let (_, received_frame) = client_framed.read_pdu().await.context("read PDU from client")?; - let received_connection_request: x224::X224 = - ironrdp_core::decode(&received_frame).context("decode PDU from client")?; - trace!(message = ?received_connection_request, "Received Connection Request PDU from client"); - - // Choose the security protocol to use with the client. - let received_connection_request_protocol = received_connection_request.0.protocol; - let client_security_protocol = if received_connection_request_protocol.contains(nego::SecurityProtocol::HYBRID_EX) { - nego::SecurityProtocol::HYBRID_EX - } else if received_connection_request - .0 - .protocol - .contains(nego::SecurityProtocol::HYBRID) - { - nego::SecurityProtocol::HYBRID - } else { - anyhow::bail!( - "client does not support CredSSP (received {})", - received_connection_request.0.protocol - ) - }; - - let connection_request_to_send = nego::ConnectionRequest { - nego_data: match target_credential { - AppCredential::UsernamePassword { username, .. } => { - Some(nego::NegoRequestData::cookie(username.to_owned())) - } - }, - flags: received_connection_request.0.flags, - // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/902b090b-9cb3-4efc-92bf-ee13373371e3 - // - // The spec states that `PROTOCOL_SSL` "SHOULD" also be set when using `PROTOCOL_HYBRID`: - // - // > PROTOCOL_HYBRID (0x00000002) - // > Credential Security Support Provider protocol (CredSSP) (section 5.4.5.2). - // > If this flag is set, then the PROTOCOL_SSL (0x00000001) flag SHOULD also be set - // > because Transport Layer Security (TLS) is a subset of CredSSP. - // - // However, in practice `mstsc` is picky about these flags: it expects the - // SupportedProtocol bits in the ConnectionRequestPDU that reach the target - // server to match what the client originally sent. If the proxy modifies - // them (for example, forcing HYBRID | HYBRID_EX and/or clearing SSL), - // the connection can fail with an authentication error (Code: 0x609). - // - // We therefore *do not* synthesize a new protocol bitmask here anymore. - // Instead, we forward the client's SupportedProtocol flags as-is and - // enforce our policy by validating them: if HYBRID / HYBRID_EX are not - // present (i.e. NLA is not negotiated), we fail the connection rather - // than trying to "fix" the flags ourselves. - // - // See also: https://serverfault.com/a/720161 - protocol: received_connection_request_protocol, - }; - trace!(?connection_request_to_send, "Send Connection Request PDU to server"); - send_pdu(server_framed, &x224::X224(connection_request_to_send)) - .await - .context("send connection request to server")?; - - let (_, received_frame) = server_framed.read_pdu().await.context("read PDU from server")?; - let received_connection_confirm: x224::X224 = - ironrdp_core::decode(&received_frame).context("decode PDU from server")?; - trace!(message = ?received_connection_confirm, "Received Connection Confirm PDU from server"); - - let (connection_confirm_to_send, handshake_result) = match &received_connection_confirm.0 { - nego::ConnectionConfirm::Response { - flags, - protocol: server_security_protocol, - } => { - debug!(?server_security_protocol, ?flags, "Server confirmed connection"); - - let result = if !server_security_protocol - .intersects(nego::SecurityProtocol::HYBRID | nego::SecurityProtocol::HYBRID_EX) - { - Err(anyhow::anyhow!( - "server selected security protocol {server_security_protocol}, which is not supported for credential injection" - )) - } else { - Ok(HandshakeResult { - client_security_protocol, - server_security_protocol: *server_security_protocol, - }) - }; - - ( - x224::X224(nego::ConnectionConfirm::Response { - flags: *flags, - protocol: client_security_protocol, - }), - result, - ) - } - nego::ConnectionConfirm::Failure { code } => ( - x224::X224(received_connection_confirm.0.clone()), - Err(anyhow::anyhow!("RDP session initiation failed with code {code}")), - ), - }; - - trace!(?connection_confirm_to_send, "Send Connection Request PDU to client"); - send_pdu(client_framed, &connection_confirm_to_send) - .await - .context("send connection confirm to client")?; - - handshake_result -} - -/// Kerberos configs for the two CredSSP legs of a credential-injection session. -/// -/// `server` drives the client-facing acceptor (Gateway-as-server); `client` drives the -/// target-facing leg (Gateway-as-client). `None` on a leg means that leg authenticates over NTLM. -pub(crate) struct CredentialInjectionKerberosConfigs { - pub server: Option, - pub client: Option, -} - -/// Whether a credential-injection session speaks Kerberos (vs NTLM). Decided once so both CredSSP -/// legs agree — sspi's acceptor and initiator must speak the same package or the handshake fails -/// reading one as the other. Kerberos needs the experimental opt-in AND a domain-qualified target -/// (a domainless account can't get a ticket). -fn injection_uses_kerberos( - enable_unstable: bool, - kerberos_credential_injection: bool, - protocol: CredentialInjectionClientAcceptorProtocol, -) -> bool { - enable_unstable - && kerberos_credential_injection - && matches!(protocol, CredentialInjectionClientAcceptorProtocol::Kerberos) -} - -/// Build the Kerberos config for both CredSSP legs from the single [`injection_uses_kerberos`] -/// decision. Everything else is NTLM on both legs. -pub(crate) fn credential_injection_kerberos_configs( - conf: &Conf, - client_addr: SocketAddr, - gateway_hostname: &str, - credential_injection_kdc: &CredentialInjectionKdc, -) -> anyhow::Result { - let protocol = credential_injection_kdc.client_acceptor_protocol()?; - - if !injection_uses_kerberos( - conf.debug.enable_unstable, - conf.debug.kerberos_credential_injection, - protocol, - ) { - return Ok(CredentialInjectionKerberosConfigs { - server: None, - client: None, - }); - } - - Ok(CredentialInjectionKerberosConfigs { - server: Some(credential_injection_kdc.server_kerberos_config(client_addr)?), - client: Some(ironrdp_connector::credssp::KerberosConfig { - // TODO: Provision the target KDC through connection options after the store is generalized. - // See https://github.com/Devolutions/devolutions-gateway/pull/1862#pullrequestreview-4774565673. - kdc_proxy_url: None, - hostname: gateway_hostname.to_owned(), - }), - }) -} - -#[instrument(name = "server_credssp", level = "debug", ret, skip_all)] -pub(crate) async fn perform_credssp_as_client( - framed: &mut ironrdp_tokio::Framed, - server_name: String, - server_public_key: Vec, - security_protocol: nego::SecurityProtocol, - credentials: &AppCredential, - kerberos_config: Option, - kdc_connector: &KdcConnector, -) -> anyhow::Result<()> -where - S: ironrdp_tokio::FramedRead + ironrdp_tokio::FramedWrite, -{ - use ironrdp_tokio::FramedWrite as _; - - // Decrypt password into short-lived buffer. - let (username, decrypted_password) = credentials - .decrypt_password() - .context("failed to decrypt credentials")?; - - let credentials = ironrdp_connector::Credentials::UsernamePassword { - username, - password: decrypted_password.expose_secret().to_owned(), - }; - // decrypted_password drops here, zeroizing its buffer; note: a copy of the plaintext - // remains in `credentials` above, which is a regular String (downstream API limitation). - - let (mut sequence, mut ts_request) = ironrdp_connector::credssp::CredsspSequence::init( - credentials, - None, - security_protocol, - ironrdp_connector::ServerName::new(server_name.clone()), - server_public_key, - kerberos_config, - )?; - - let mut buf = ironrdp_pdu::WriteBuf::new(); - - loop { - let client_state = { - let mut generator = sequence.process_ts_request(ts_request); - resolve_client_generator(&mut generator, kdc_connector).await? - }; // drop generator - - buf.clear(); - let written = sequence.handle_process_result(client_state, &mut buf)?; - - if let Some(response_len) = written.size() { - let response = &buf[..response_len]; - framed - .write_all(response) - .await - .map_err(|e| ironrdp_connector::custom_err!("write all", e))?; - } - - let Some(next_pdu_hint) = sequence.next_pdu_hint() else { - break; - }; - - let pdu = framed.read_by_hint(next_pdu_hint).await.context("read frame by hint")?; - - if let Some(next_request) = sequence.decode_server_message(&pdu)? { - ts_request = next_request; - } else { - break; - } - } - - Ok(()) -} - -async fn resolve_server_generator( - generator: &mut CredsspServerProcessGenerator<'_>, - credential_injection_kdc: &CredentialInjectionKdc, - kdc_connector: &KdcConnector, -) -> Result { - let mut state = generator.start(); - - loop { - match state { - GeneratorState::Suspended(request) => { - let response = match credential_injection_kdc.intercept_network_request(&request) { - Ok(CredentialInjectionKdcInterception::Intercepted(response)) => Ok(response), - Ok(CredentialInjectionKdcInterception::NotInjectionRequest) => { - kdc_connector.send_network_request(&request).await - } - Ok(CredentialInjectionKdcInterception::NotInjectionRealm(mismatch)) => Err(anyhow::anyhow!( - "kdc request realm does not match credential-injection session realm: {mismatch}" - )), - Err(error) => Err(error), - } - .map_err(|err| sspi::credssp::ServerError { - ts_request: None, - error: sspi::Error::new(sspi::ErrorKind::InternalError, err), - })?; - - state = generator.resume(Ok(response)); - } - GeneratorState::Completed(client_state) => { - break client_state; - } - } - } -} - -async fn resolve_client_generator( - generator: &mut CredsspClientProcessGenerator<'_>, - kdc_connector: &KdcConnector, -) -> anyhow::Result { - let mut state = generator.start(); - - loop { - match state { - GeneratorState::Suspended(request) => { - let response = kdc_connector.send_network_request(&request).await?; - state = generator.resume(Ok(response)); - } - GeneratorState::Completed(client_state) => { - break Ok(client_state.map_err(|e| { - ironrdp_connector::ConnectorError::new("CredSSP", ironrdp_connector::ConnectorErrorKind::Credssp(e)) - })?); - } - }; - } -} - -#[expect(clippy::too_many_arguments)] -#[instrument(name = "client_credssp", level = "debug", ret, skip_all)] -pub(crate) async fn perform_credssp_as_server( - framed: &mut ironrdp_tokio::Framed, - client_addr: IpAddr, - gateway_public_key: Vec, - security_protocol: nego::SecurityProtocol, - credentials: &AppCredential, - kerberos_server_config: Option, - credential_injection_kdc: &CredentialInjectionKdc, - kdc_connector: &KdcConnector, -) -> anyhow::Result<()> -where - S: ironrdp_tokio::FramedRead + ironrdp_tokio::FramedWrite, -{ - use ironrdp_connector::sspi::credssp::EarlyUserAuthResult; - use ironrdp_tokio::FramedWrite as _; - - let mut buf = ironrdp_pdu::WriteBuf::new(); - - // Are we supposed to use the actual computer name of the client? - // But this does not seem to matter so far, so we stringify the IP address of the client instead. - let client_computer_name = ironrdp_connector::ServerName::new(client_addr.to_string()); - - let result = credssp_loop( - framed, - &mut buf, - client_computer_name, - gateway_public_key, - credentials, - kerberos_server_config, - credential_injection_kdc, - kdc_connector, - ) - .await; - - if security_protocol.intersects(nego::SecurityProtocol::HYBRID_EX) { - trace!(?result, "HYBRID_EX"); - - let result = if result.is_ok() { - EarlyUserAuthResult::Success - } else { - EarlyUserAuthResult::AccessDenied - }; - - buf.clear(); - result.to_buffer(&mut buf).context("write early user auth result")?; - let response = &buf[..result.buffer_len()]; - framed.write_all(response).await.context("write_all")?; - } - - return result; - - async fn credssp_loop( - framed: &mut ironrdp_tokio::Framed, - buf: &mut ironrdp_pdu::WriteBuf, - client_computer_name: ironrdp_connector::ServerName, - public_key: Vec, - credentials: &AppCredential, - kerberos_server_config: Option, - credential_injection_kdc: &CredentialInjectionKdc, - kdc_connector: &KdcConnector, - ) -> anyhow::Result<()> - where - S: ironrdp_tokio::FramedRead + ironrdp_tokio::FramedWrite, - { - // Decrypt password into short-lived buffer. - let (username, decrypted_password) = credentials - .decrypt_password() - .context("failed to decrypt credentials")?; - - let username = sspi::Username::parse(&username).context("invalid username")?; - - let identity = sspi::AuthIdentity { - username, - password: decrypted_password.expose_secret().to_owned().into(), - }; - // decrypted_password drops here, zeroizing its buffer; note: a copy of the plaintext - // remains in `identity` above (downstream API limitation). - - let mut sequence = ironrdp_acceptor::credssp::CredsspSequence::init( - &identity, - client_computer_name, - public_key, - kerberos_server_config, - )?; - - loop { - let Some(next_pdu_hint) = sequence.next_pdu_hint()? else { - break; - }; - - let pdu = framed - .read_by_hint(next_pdu_hint) - .await - .map_err(|e| ironrdp_connector::custom_err!("read frame by hint", e))?; - - let Some(ts_request) = sequence.decode_client_message(&pdu)? else { - break; - }; - - let result = { - let mut generator = sequence.process_ts_request(ts_request); - resolve_server_generator(&mut generator, credential_injection_kdc, kdc_connector).await - }; // drop generator - - buf.clear(); - let written = sequence.handle_process_result(result, buf)?; - - if let Some(response_len) = written.size() { - let response = &buf[..response_len]; - framed - .write_all(response) - .await - .map_err(|e| ironrdp_connector::custom_err!("write all", e))?; - } - } - - Ok(()) - } -} - -async fn send_pdu(framed: &mut ironrdp_tokio::MovableTokioFramed, pdu: &P) -> anyhow::Result<()> -where - S: AsyncWrite + Unpin + Send, - P: ironrdp_core::Encode, -{ - use ironrdp_tokio::FramedWrite as _; - - let payload = ironrdp_core::encode_vec(pdu).context("failed to encode PDU")?; - framed.write_all(&payload).await.context("failed to write PDU")?; - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::*; - - // The two CredSSP legs are built from this single decision, so agreement is guaranteed by - // construction. These cases pin the decision itself (the bug was the two legs deciding - // independently): Kerberos requires BOTH opt-in flags AND a domain-qualified target. - #[test] - fn injection_uses_kerberos_requires_optin_and_domain_qualified_target() { - use CredentialInjectionClientAcceptorProtocol::{Kerberos, Ntlm}; - - assert!(injection_uses_kerberos(true, true, Kerberos)); - - // Either opt-in off => NTLM, even for a Kerberos-capable target. - assert!(!injection_uses_kerberos(false, true, Kerberos)); - assert!(!injection_uses_kerberos(true, false, Kerberos)); - - // Domainless target can't get a ticket => NTLM regardless of the flags. - assert!(!injection_uses_kerberos(true, true, Ntlm)); - assert!(!injection_uses_kerberos(false, false, Ntlm)); - } -} diff --git a/devolutions-gateway/src/rdp_proxy/credssp.rs b/devolutions-gateway/src/rdp_proxy/credssp.rs new file mode 100644 index 000000000..8909f187d --- /dev/null +++ b/devolutions-gateway/src/rdp_proxy/credssp.rs @@ -0,0 +1,430 @@ +use std::net::SocketAddr; +use std::sync::Arc; + +use anyhow::Context as _; +use ironrdp_acceptor::credssp::CredsspProcessGenerator as CredsspServerProcessGenerator; +use ironrdp_connector::credssp::CredsspProcessGenerator as CredsspClientProcessGenerator; +use ironrdp_connector::sspi; +use ironrdp_connector::sspi::generator::GeneratorState; +use ironrdp_pdu::{mcs, nego, x224}; +use secrecy::ExposeSecret as _; +use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt as _}; +use typed_builder::TypedBuilder; + +use super::send_pdu; +use crate::config::Conf; +use crate::credential::AppCredential; +use crate::credential_injection::{CredentialInjection, SyntheticKdcInterception}; +use crate::kdc_connector::KdcConnector; +use crate::proxy::Proxy; +use crate::session::{DisconnectInterest, SessionInfo, SessionMessageSender}; +use crate::subscriber::SubscriberSender; + +#[derive(TypedBuilder)] +pub(crate) struct CredsspSession { + conf: Arc, + session_info: SessionInfo, + client_addr: SocketAddr, + server_addr: SocketAddr, + credential_injection: CredentialInjection, + sessions: SessionMessageSender, + subscriber_tx: SubscriberSender, + server_dns_name: String, + disconnect_interest: Option, + kdc_connector: KdcConnector, +} + +#[derive(TypedBuilder)] +pub(crate) struct PreparedCredssp { + client_stream: C, + server_stream: S, + gateway_public_key: Vec, + server_public_key: Vec, + client_security_protocol: nego::SecurityProtocol, + server_security_protocol: nego::SecurityProtocol, +} + +impl CredsspSession { + pub(super) fn conf(&self) -> &Conf { + &self.conf + } + + pub(super) fn server_dns_name(&self) -> &str { + &self.server_dns_name + } + + pub(super) fn target_credential(&self) -> &AppCredential { + self.credential_injection.target_credential() + } + + pub(crate) async fn run(self, prepared: PreparedCredssp) -> anyhow::Result<()> + where + C: AsyncRead + AsyncWrite + Unpin + Send, + S: AsyncRead + AsyncWrite + Unpin + Send, + { + let Self { + conf, + session_info, + client_addr, + server_addr, + credential_injection, + sessions, + subscriber_tx, + server_dns_name, + disconnect_interest, + kdc_connector, + } = self; + let PreparedCredssp { + client_stream, + server_stream, + gateway_public_key, + server_public_key, + client_security_protocol, + server_security_protocol, + } = prepared; + + let mut client_framed = ironrdp_tokio::MovableTokioFramed::new(client_stream); + let mut server_framed = ironrdp_tokio::MovableTokioFramed::new(server_stream); + + let (server_kerberos_config, client_kerberos_config) = credential_injection + .kerberos_configs(client_addr, &conf.hostname)? + .unzip(); + + let client_credssp_fut = perform_credssp_as_server( + &mut client_framed, + client_addr.ip(), + gateway_public_key, + client_security_protocol, + credential_injection.proxy_credential(), + server_kerberos_config, + &credential_injection, + &kdc_connector, + ); + + let server_credssp_fut = perform_credssp_as_client( + &mut server_framed, + server_dns_name, + server_public_key, + server_security_protocol, + credential_injection.target_credential(), + client_kerberos_config, + &kdc_connector, + ); + + let (client_credssp_res, server_credssp_res) = tokio::join!(client_credssp_fut, server_credssp_fut); + client_credssp_res.context("CredSSP with client")?; + server_credssp_res.context("CredSSP with server")?; + + intercept_connect_confirm(&mut client_framed, &mut server_framed, server_security_protocol).await?; + + let (mut client_stream, client_leftover) = client_framed.into_inner(); + let (mut server_stream, server_leftover) = server_framed.into_inner(); + + info!("RDP-TLS forwarding (credential injection)"); + + client_stream + .write_all(&server_leftover) + .await + .context("write server leftover to client")?; + + server_stream + .write_all(&client_leftover) + .await + .context("write client leftover to server")?; + + Proxy::builder() + .conf(conf) + .session_info(session_info) + .address_a(client_addr) + .transport_a(client_stream) + .address_b(server_addr) + .transport_b(server_stream) + .sessions(sessions) + .subscriber_tx(subscriber_tx) + .disconnect_interest(disconnect_interest) + .build() + .select_dissector_and_forward() + .await + .context("RDP-TLS traffic proxying failed") + } +} + +#[instrument(level = "debug", ret, skip_all)] +async fn intercept_connect_confirm( + client_framed: &mut ironrdp_tokio::MovableTokioFramed, + server_framed: &mut ironrdp_tokio::MovableTokioFramed, + server_security_protocol: nego::SecurityProtocol, +) -> anyhow::Result<()> +where + C: AsyncWrite + AsyncRead + Unpin + Send, + S: AsyncWrite + AsyncRead + Unpin + Send, +{ + let (_, received_frame) = client_framed + .read_pdu() + .await + .context("read MCS Connect Initial from client")?; + let received_connect_initial: x224::X224> = + ironrdp_core::decode(&received_frame).context("decode PDU from client")?; + let mut received_connect_initial: mcs::ConnectInitial = + ironrdp_core::decode(&received_connect_initial.0.data).context("decode Connect Initial PDU")?; + trace!(message = ?received_connect_initial, "Received Connect Initial PDU from client"); + + let mut gcc_blocks = received_connect_initial.conference_create_request.into_gcc_blocks(); + gcc_blocks.core.optional_data.server_selected_protocol = Some(server_security_protocol); + received_connect_initial.conference_create_request = ironrdp_pdu::gcc::ConferenceCreateRequest::new(gcc_blocks)?; + trace!(message = ?received_connect_initial, "Send Connection Request PDU to server"); + let x224_msg_buf = ironrdp_core::encode_vec(&received_connect_initial)?; + let pdu = x224::X224Data { + data: std::borrow::Cow::Owned(x224_msg_buf), + }; + send_pdu(server_framed, &x224::X224(pdu)) + .await + .context("send connection request to server")?; + + Ok(()) +} + +#[instrument(name = "server_credssp", level = "debug", ret, skip_all)] +async fn perform_credssp_as_client( + framed: &mut ironrdp_tokio::Framed, + server_name: String, + server_public_key: Vec, + security_protocol: nego::SecurityProtocol, + credentials: &AppCredential, + kerberos_config: Option, + kdc_connector: &KdcConnector, +) -> anyhow::Result<()> +where + S: ironrdp_tokio::FramedRead + ironrdp_tokio::FramedWrite, +{ + use ironrdp_tokio::FramedWrite as _; + + let (username, decrypted_password) = credentials + .decrypt_password() + .context("failed to decrypt credentials")?; + + let credentials = ironrdp_connector::Credentials::UsernamePassword { + username, + password: decrypted_password.expose_secret().to_owned(), + }; + + let (mut sequence, mut ts_request) = ironrdp_connector::credssp::CredsspSequence::init( + credentials, + None, + security_protocol, + ironrdp_connector::ServerName::new(server_name.clone()), + server_public_key, + kerberos_config, + )?; + + let mut buf = ironrdp_pdu::WriteBuf::new(); + + loop { + let client_state = { + let mut generator = sequence.process_ts_request(ts_request); + resolve_client_generator(&mut generator, kdc_connector).await? + }; + + buf.clear(); + let written = sequence.handle_process_result(client_state, &mut buf)?; + + if let Some(response_len) = written.size() { + let response = &buf[..response_len]; + framed + .write_all(response) + .await + .map_err(|error| ironrdp_connector::custom_err!("write all", error))?; + } + + let Some(next_pdu_hint) = sequence.next_pdu_hint() else { + break; + }; + + let pdu = framed.read_by_hint(next_pdu_hint).await.context("read frame by hint")?; + + if let Some(next_request) = sequence.decode_server_message(&pdu)? { + ts_request = next_request; + } else { + break; + } + } + + Ok(()) +} + +async fn resolve_server_generator( + generator: &mut CredsspServerProcessGenerator<'_>, + credential_injection: &CredentialInjection, + kdc_connector: &KdcConnector, +) -> Result { + let mut state = generator.start(); + + loop { + match state { + GeneratorState::Suspended(request) => { + let response = match credential_injection.intercept_network_request(&request) { + Ok(SyntheticKdcInterception::Intercepted(response)) => Ok(response), + Ok(SyntheticKdcInterception::NotInjectionRealm(mismatch)) => Err(anyhow::anyhow!( + "kdc request realm does not match credential-injection session realm: {mismatch}" + )), + Ok(SyntheticKdcInterception::NotInjectionRequest) => { + kdc_connector.send_network_request(&request).await + } + Err(error) => Err(error), + } + .map_err(|error| sspi::credssp::ServerError { + ts_request: None, + error: sspi::Error::new(sspi::ErrorKind::InternalError, error), + })?; + + state = generator.resume(Ok(response)); + } + GeneratorState::Completed(client_state) => { + break client_state; + } + } + } +} + +async fn resolve_client_generator( + generator: &mut CredsspClientProcessGenerator<'_>, + kdc_connector: &KdcConnector, +) -> anyhow::Result { + let mut state = generator.start(); + + loop { + match state { + GeneratorState::Suspended(request) => { + let response = kdc_connector.send_network_request(&request).await?; + state = generator.resume(Ok(response)); + } + GeneratorState::Completed(client_state) => { + break Ok(client_state.map_err(|error| { + ironrdp_connector::ConnectorError::new( + "CredSSP", + ironrdp_connector::ConnectorErrorKind::Credssp(error), + ) + })?); + } + }; + } +} + +#[expect(clippy::too_many_arguments)] +#[instrument(name = "client_credssp", level = "debug", ret, skip_all)] +async fn perform_credssp_as_server( + framed: &mut ironrdp_tokio::Framed, + client_addr: std::net::IpAddr, + gateway_public_key: Vec, + security_protocol: nego::SecurityProtocol, + credentials: &AppCredential, + kerberos_server_config: Option, + credential_injection: &CredentialInjection, + kdc_connector: &KdcConnector, +) -> anyhow::Result<()> +where + S: ironrdp_tokio::FramedRead + ironrdp_tokio::FramedWrite, +{ + use ironrdp_connector::sspi::credssp::EarlyUserAuthResult; + use ironrdp_tokio::FramedWrite as _; + + let mut buf = ironrdp_pdu::WriteBuf::new(); + let client_computer_name = ironrdp_connector::ServerName::new(client_addr.to_string()); + + let result = credssp_loop( + framed, + &mut buf, + client_computer_name, + gateway_public_key, + credentials, + kerberos_server_config, + credential_injection, + kdc_connector, + ) + .await; + + if security_protocol.intersects(nego::SecurityProtocol::HYBRID_EX) { + trace!(?result, "HYBRID_EX"); + + let result = if result.is_ok() { + EarlyUserAuthResult::Success + } else { + EarlyUserAuthResult::AccessDenied + }; + + buf.clear(); + result.to_buffer(&mut buf).context("write early user auth result")?; + let response = &buf[..result.buffer_len()]; + framed.write_all(response).await.context("write_all")?; + } + + result +} + +#[expect(clippy::too_many_arguments)] +async fn credssp_loop( + framed: &mut ironrdp_tokio::Framed, + buf: &mut ironrdp_pdu::WriteBuf, + client_computer_name: ironrdp_connector::ServerName, + public_key: Vec, + credentials: &AppCredential, + kerberos_server_config: Option, + credential_injection: &CredentialInjection, + kdc_connector: &KdcConnector, +) -> anyhow::Result<()> +where + S: ironrdp_tokio::FramedRead + ironrdp_tokio::FramedWrite, +{ + use ironrdp_tokio::FramedWrite as _; + + let (username, decrypted_password) = credentials + .decrypt_password() + .context("failed to decrypt credentials")?; + + let username = sspi::Username::parse(&username).context("invalid username")?; + + let identity = sspi::AuthIdentity { + username, + password: decrypted_password.expose_secret().to_owned().into(), + }; + + let mut sequence = ironrdp_acceptor::credssp::CredsspSequence::init( + &identity, + client_computer_name, + public_key, + kerberos_server_config, + )?; + + loop { + let Some(next_pdu_hint) = sequence.next_pdu_hint()? else { + break; + }; + + let pdu = framed + .read_by_hint(next_pdu_hint) + .await + .map_err(|error| ironrdp_connector::custom_err!("read frame by hint", error))?; + + let Some(ts_request) = sequence.decode_client_message(&pdu)? else { + break; + }; + + let result = { + let mut generator = sequence.process_ts_request(ts_request); + resolve_server_generator(&mut generator, credential_injection, kdc_connector).await + }; + + buf.clear(); + let written = sequence.handle_process_result(result, buf)?; + + if let Some(response_len) = written.size() { + let response = &buf[..response_len]; + framed + .write_all(response) + .await + .map_err(|error| ironrdp_connector::custom_err!("write all", error))?; + } + } + + Ok(()) +} diff --git a/devolutions-gateway/src/rdp_proxy/mod.rs b/devolutions-gateway/src/rdp_proxy/mod.rs new file mode 100644 index 000000000..73395efce --- /dev/null +++ b/devolutions-gateway/src/rdp_proxy/mod.rs @@ -0,0 +1,224 @@ +mod credssp; + +use anyhow::Context as _; +pub(crate) use credssp::{CredsspSession, PreparedCredssp}; +use ironrdp_pdu::{nego, x224}; +use tokio::io::{AsyncRead, AsyncWrite}; +use typed_builder::TypedBuilder; + +use crate::credential::AppCredential; + +#[derive(TypedBuilder)] +pub(crate) struct RdpProxy { + session: CredsspSession, + client_stream: C, + server_stream: S, + client_stream_leftover_bytes: bytes::BytesMut, +} + +impl RdpProxy +where + A: AsyncWrite + AsyncRead + Unpin + Send, + B: AsyncWrite + AsyncRead + Unpin + Send, +{ + pub(crate) async fn run(self) -> anyhow::Result<()> { + handle(self).await + } +} + +#[instrument("rdp_proxy", skip_all)] +async fn handle(proxy: RdpProxy) -> anyhow::Result<()> +where + C: AsyncRead + AsyncWrite + Unpin + Send, + S: AsyncRead + AsyncWrite + Unpin + Send, +{ + let RdpProxy { + session, + client_stream, + server_stream, + client_stream_leftover_bytes, + } = proxy; + + let tls_conf = session.conf().credssp_tls.get().context("CredSSP TLS configuration")?; + let gateway_hostname = session.conf().hostname.clone(); + + // -- Retrieve the Gateway TLS public key that must be used for client-proxy CredSSP later on -- // + + let gateway_cert_chain_handle = tokio::spawn(crate::tls::get_cert_chain_for_acceptor_cached( + gateway_hostname.clone(), + tls_conf.acceptor.clone(), + )); + + // -- Dual handshake with the client and the server until the TLS security upgrade -- // + + let mut client_framed = + ironrdp_tokio::MovableTokioFramed::new_with_leftover(client_stream, client_stream_leftover_bytes); + let mut server_framed = ironrdp_tokio::MovableTokioFramed::new(server_stream); + + let handshake_result = + dual_handshake_until_tls_upgrade(&mut client_framed, &mut server_framed, session.target_credential()).await?; + + let client_stream = client_framed.into_inner_no_leftover(); + let server_stream = server_framed.into_inner_no_leftover(); + + // -- Perform the TLS upgrading for both the client and the server, effectively acting as a man-in-the-middle -- // + + let client_tls_upgrade_fut = tls_conf.acceptor.accept(client_stream); + let server_tls_upgrade_fut = crate::tls::dangerous_connect(session.server_dns_name().to_owned(), server_stream); + + let (client_stream, server_stream) = tokio::join!(client_tls_upgrade_fut, server_tls_upgrade_fut); + + let client_stream = client_stream.context("TLS upgrade with client failed")?; + let server_stream = server_stream.context("TLS upgrade with server failed")?; + + let server_public_key = + crate::tls::extract_stream_peer_public_key(&server_stream).context("extract target server TLS public key")?; + + let gateway_cert_chain = gateway_cert_chain_handle.await??; + let gateway_public_key = crate::tls::extract_public_key(gateway_cert_chain.first().context("no leaf")?) + .context("extract Gateway public key")?; + + let prepared = PreparedCredssp::builder() + .client_stream(client_stream) + .server_stream(server_stream) + .gateway_public_key(gateway_public_key) + .server_public_key(server_public_key) + .client_security_protocol(handshake_result.client_security_protocol) + .server_security_protocol(handshake_result.server_security_protocol) + .build(); + + session.run(prepared).await +} + +#[derive(Debug)] +struct HandshakeResult { + client_security_protocol: nego::SecurityProtocol, + server_security_protocol: nego::SecurityProtocol, +} + +#[instrument(name = "dual_handshake", level = "debug", ret, skip_all)] +async fn dual_handshake_until_tls_upgrade( + client_framed: &mut ironrdp_tokio::MovableTokioFramed, + server_framed: &mut ironrdp_tokio::MovableTokioFramed, + target_credential: &AppCredential, +) -> anyhow::Result +where + C: AsyncWrite + AsyncRead + Unpin + Send, + S: AsyncWrite + AsyncRead + Unpin + Send, +{ + let (_, received_frame) = client_framed.read_pdu().await.context("read PDU from client")?; + let received_connection_request: x224::X224 = + ironrdp_core::decode(&received_frame).context("decode PDU from client")?; + trace!(message = ?received_connection_request, "Received Connection Request PDU from client"); + + // Choose the security protocol to use with the client. + let received_connection_request_protocol = received_connection_request.0.protocol; + let client_security_protocol = if received_connection_request_protocol.contains(nego::SecurityProtocol::HYBRID_EX) { + nego::SecurityProtocol::HYBRID_EX + } else if received_connection_request + .0 + .protocol + .contains(nego::SecurityProtocol::HYBRID) + { + nego::SecurityProtocol::HYBRID + } else { + anyhow::bail!( + "client does not support CredSSP (received {})", + received_connection_request.0.protocol + ) + }; + + let connection_request_to_send = nego::ConnectionRequest { + nego_data: match target_credential { + AppCredential::UsernamePassword { username, .. } => { + Some(nego::NegoRequestData::cookie(username.to_owned())) + } + }, + flags: received_connection_request.0.flags, + // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/902b090b-9cb3-4efc-92bf-ee13373371e3 + // + // The spec states that `PROTOCOL_SSL` "SHOULD" also be set when using `PROTOCOL_HYBRID`: + // + // > PROTOCOL_HYBRID (0x00000002) + // > Credential Security Support Provider protocol (CredSSP) (section 5.4.5.2). + // > If this flag is set, then the PROTOCOL_SSL (0x00000001) flag SHOULD also be set + // > because Transport Layer Security (TLS) is a subset of CredSSP. + // + // However, in practice `mstsc` is picky about these flags: it expects the + // SupportedProtocol bits in the ConnectionRequestPDU that reach the target + // server to match what the client originally sent. If the proxy modifies + // them (for example, forcing HYBRID | HYBRID_EX and/or clearing SSL), + // the connection can fail with an authentication error (Code: 0x609). + // + // We therefore *do not* synthesize a new protocol bitmask here anymore. + // Instead, we forward the client's SupportedProtocol flags as-is and + // enforce our policy by validating them: if HYBRID / HYBRID_EX are not + // present (i.e. NLA is not negotiated), we fail the connection rather + // than trying to "fix" the flags ourselves. + // + // See also: https://serverfault.com/a/720161 + protocol: received_connection_request_protocol, + }; + trace!(?connection_request_to_send, "Send Connection Request PDU to server"); + send_pdu(server_framed, &x224::X224(connection_request_to_send)) + .await + .context("send connection request to server")?; + + let (_, received_frame) = server_framed.read_pdu().await.context("read PDU from server")?; + let received_connection_confirm: x224::X224 = + ironrdp_core::decode(&received_frame).context("decode PDU from server")?; + trace!(message = ?received_connection_confirm, "Received Connection Confirm PDU from server"); + + let (connection_confirm_to_send, handshake_result) = match &received_connection_confirm.0 { + nego::ConnectionConfirm::Response { + flags, + protocol: server_security_protocol, + } => { + debug!(?server_security_protocol, ?flags, "Server confirmed connection"); + + let result = if !server_security_protocol + .intersects(nego::SecurityProtocol::HYBRID | nego::SecurityProtocol::HYBRID_EX) + { + Err(anyhow::anyhow!( + "server selected security protocol {server_security_protocol}, which is not supported for credential injection" + )) + } else { + Ok(HandshakeResult { + client_security_protocol, + server_security_protocol: *server_security_protocol, + }) + }; + + ( + x224::X224(nego::ConnectionConfirm::Response { + flags: *flags, + protocol: client_security_protocol, + }), + result, + ) + } + nego::ConnectionConfirm::Failure { code } => ( + x224::X224(received_connection_confirm.0.clone()), + Err(anyhow::anyhow!("RDP session initiation failed with code {code}")), + ), + }; + + trace!(?connection_confirm_to_send, "Send Connection Request PDU to client"); + send_pdu(client_framed, &connection_confirm_to_send) + .await + .context("send connection confirm to client")?; + + handshake_result +} + +async fn send_pdu(framed: &mut ironrdp_tokio::MovableTokioFramed, pdu: &P) -> anyhow::Result<()> +where + S: AsyncWrite + Unpin + Send, + P: ironrdp_core::Encode, +{ + use ironrdp_tokio::FramedWrite as _; + + let payload = ironrdp_core::encode_vec(pdu).context("failed to encode PDU")?; + framed.write_all(&payload).await.context("failed to write PDU")?; + Ok(()) +} diff --git a/devolutions-gateway/src/service.rs b/devolutions-gateway/src/service.rs index e847e1d94..1f0175260 100644 --- a/devolutions-gateway/src/service.rs +++ b/devolutions-gateway/src/service.rs @@ -267,7 +267,8 @@ async fn spawn_tasks(conf_handle: ConfHandle) -> anyhow::Result { .await .context("failed to initialize traffic audit manager")?; - let credentials = devolutions_gateway::credential_injection_kdc::CredentialService::new(); + let provisioning = devolutions_gateway::provisioning::ProvisioningStore::new(); + let synthetic_kdc_registry = devolutions_gateway::credential_injection::SyntheticKdcRegistry::new(); let filesystem_monitor_config_cache = devolutions_gateway::api::monitoring::FilesystemConfigCache::new( config::get_data_dir().join("monitors_cache.json"), @@ -315,7 +316,8 @@ async fn spawn_tasks(conf_handle: ConfHandle) -> anyhow::Result { shutdown_signal: tasks.shutdown_signal.clone(), recordings: recording_manager_handle.clone(), job_queue_handle: job_queue_ctx.job_queue_handle.clone(), - credentials: credentials.clone(), + provisioning: provisioning.clone(), + synthetic_kdc_registry: synthetic_kdc_registry.clone(), monitoring_state, traffic_audit_handle: traffic_audit_task.handle(), agent_tunnel_handle, @@ -350,11 +352,7 @@ async fn spawn_tasks(conf_handle: ConfHandle) -> anyhow::Result { tasks.register(devolutions_gateway::token::CleanupTask { token_cache }); - tasks.register(devolutions_gateway::credential::CleanupTask { - handle: credentials.credential_store().clone(), - }); - - tasks.register(devolutions_gateway::credential_injection_kdc::CleanupTask { service: credentials }); + tasks.register(provisioning.cleanup_task()); tasks.register(devolutions_log::LogDeleterTask::::new( conf.log_file.clone(), diff --git a/devolutions-gateway/src/target_addr.rs b/devolutions-gateway/src/target_addr.rs index 7e533a4fa..14b6ca064 100644 --- a/devolutions-gateway/src/target_addr.rs +++ b/devolutions-gateway/src/target_addr.rs @@ -216,6 +216,14 @@ impl TryFrom for TargetAddr { } } +impl TryFrom<&TargetAddr> for url::Url { + type Error = url::ParseError; + + fn try_from(target: &TargetAddr) -> Result { + url::Url::parse(target.as_str()) + } +} + impl fmt::Display for TargetAddr { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.serialization) diff --git a/devolutions-gateway/src/target_connection_options.rs b/devolutions-gateway/src/target_connection_options.rs new file mode 100644 index 000000000..7f5fea145 --- /dev/null +++ b/devolutions-gateway/src/target_connection_options.rs @@ -0,0 +1,108 @@ +use crate::target_addr::TargetAddr; + +/// How the Gateway's internal client should reach the target, provisioned alongside the credentials. +/// +/// The KDC address is fully validated at construction — supported scheme, a host, and a parseable +/// URL — so the rest of the code can trust `krb_kdc` without re-checking it or failing late when a +/// session starts. +#[derive(Debug, Clone, Deserialize)] +#[serde(try_from = "RawTargetConnectionOptions")] +pub(crate) struct TargetConnectionOptions { + krb_kdc: Option, +} + +impl TargetConnectionOptions { + pub(crate) fn new(krb_kdc: Option) -> Result { + if let Some(krb_kdc) = &krb_kdc { + if !is_supported_krb_kdc_scheme(krb_kdc.scheme()) { + return Err(InvalidKdcAddr::UnsupportedScheme(krb_kdc.scheme().to_owned())); + } + if krb_kdc.host().is_empty() { + return Err(InvalidKdcAddr::MissingHost(krb_kdc.as_str().to_owned())); + } + // The target-side CredSSP leg turns this into a URL. Reject a value that won't parse here, + // so provisioning fails fast instead of at session start. + url::Url::try_from(krb_kdc).map_err(|_| InvalidKdcAddr::NotAUrl(krb_kdc.as_str().to_owned()))?; + } + Ok(Self { krb_kdc }) + } + + pub(crate) fn krb_kdc(&self) -> Option<&TargetAddr> { + self.krb_kdc.as_ref() + } +} + +pub(crate) fn is_supported_krb_kdc_scheme(scheme: &str) -> bool { + matches!(scheme, "tcp" | "udp") +} + +#[derive(Debug, thiserror::Error)] +pub(crate) enum InvalidKdcAddr { + #[error("unsupported KDC protocol: {0}")] + UnsupportedScheme(String), + #[error("KDC address is missing a host: {0}")] + MissingHost(String), + #[error("KDC address is not a valid URL: {0}")] + NotAUrl(String), +} + +#[derive(Deserialize)] +struct RawTargetConnectionOptions { + #[serde(default)] + krb_kdc: Option, +} + +impl TryFrom for TargetConnectionOptions { + type Error = InvalidKdcAddr; + + fn try_from(raw: RawTargetConnectionOptions) -> Result { + Self::new(raw.krb_kdc) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn accepts_supported_kdc_protocols() { + for krb_kdc in ["tcp://dc.example.com:88", "udp://dc.example.com:88"] { + let options: TargetConnectionOptions = serde_json::from_value(serde_json::json!({ + "krb_kdc": krb_kdc, + })) + .expect("supported KDC protocol should deserialize"); + + assert_eq!( + options.krb_kdc().expect("KDC address should be present").as_str(), + krb_kdc + ); + } + } + + #[test] + fn rejects_unsupported_kdc_protocol() { + let error = serde_json::from_value::(serde_json::json!({ + "krb_kdc": "https://dc.example.com:443", + })) + .expect_err("unsupported KDC protocol should be rejected"); + + assert!(error.to_string().contains("unsupported KDC protocol: https")); + } + + #[test] + fn rejects_kdc_without_a_host() { + assert!( + serde_json::from_value::(serde_json::json!({ + "krb_kdc": "tcp://:88", + })) + .is_err(), + "a host-less KDC address must be rejected at provisioning time" + ); + } + + #[test] + fn new_rejects_unsupported_scheme_for_in_crate_callers() { + let krb_kdc = TargetAddr::parse("https://dc.example.com:443", Some(443)).expect("addr parses"); + assert!(TargetConnectionOptions::new(Some(krb_kdc)).is_err()); + } +} diff --git a/devolutions-gateway/src/token.rs b/devolutions-gateway/src/token.rs index a3f6e0e7f..7a95c45d4 100644 --- a/devolutions-gateway/src/token.rs +++ b/devolutions-gateway/src/token.rs @@ -1313,15 +1313,21 @@ pub fn extract_dst_alt(token: &str) -> anyhow::Result> { .collect() } -/// Validate the association-token claims required by credential injection. +pub(crate) struct CredentialInjectionTokenData { + pub(crate) jti: Uuid, + pub(crate) target_hostname: String, +} + +/// Parse the association-token data required by credential injection. /// -/// This is intentionally a token-layer shape check only. -/// The credential-injection KDC still lazily extracts the target hostname from the original token -/// when it builds its per-session state. -pub fn validate_credential_injection_association_token(token: &str) -> anyhow::Result { +/// This is intentionally a token-layer shape check only; normal connection authorization still +/// validates the token before consuming the provisioned data. +pub(crate) fn validate_credential_injection_association_token( + token: &str, +) -> anyhow::Result { let jti = extract_jti(token).context("read jti from association token")?; - extract_credential_injection_target_hostname(token)?; - Ok(jti) + let target_hostname = extract_credential_injection_target_hostname(token)?; + Ok(CredentialInjectionTokenData { jti, target_hostname }) } /// Extract the target hostname used by credential injection from an association token. diff --git a/devolutions-gateway/tests/preflight.rs b/devolutions-gateway/tests/preflight.rs index 8246696ac..522cb32eb 100644 --- a/devolutions-gateway/tests/preflight.rs +++ b/devolutions-gateway/tests/preflight.rs @@ -231,62 +231,48 @@ async fn test_provision_credentials_rejects_missing_target_hostname() -> anyhow: } #[tokio::test] -async fn test_provision_token_overwrite_alert() -> anyhow::Result<()> { +async fn test_provision_invalid_params() -> anyhow::Result<()> { let _guard = init_logger(); let (app, _state, _handles) = make_router()?; let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiI1ZTNlODMzZi04NGM3LTQ1NDEtYjY3Ni1hY2MzMjk5ZTM5YjgifQ.1qECGlrW7y9HWFArc6GPHLGTOY7PhAvzKJ5XMRBg4k4"; - let op_id1 = Uuid::new_v4(); - let op_id2 = Uuid::new_v4(); - - let op1 = json!([{ - "id": op_id1, - "kind": "provision-token", - "token": token, - }]); - - app.clone().oneshot(preflight_request(op1)?).await?; + let op_id = Uuid::new_v4(); - let op2 = json!([{ - "id": op_id2, - "kind": "provision-token", + let op = json!([{ + "id": op_id, + "kind": "provision-credentials", "token": token, + "proxy_credendial": { "kind": "unknown" }, + "target_credential": { "kind": "username-password", "username": "u", "password": "p" }, }]); - let response = app.oneshot(preflight_request(op2)?).await?; + let request = preflight_request(op)?; + let response = app.oneshot(request).await?; let body = response.into_body().collect().await?.to_bytes(); let body: serde_json::Value = serde_json::from_slice(&body)?; - assert_eq!(body.as_array().expect("an array").len(), 2); + assert_eq!(body.as_array().expect("an array").len(), 1); assert_eq!(body[0]["kind"], "alert"); - assert!(body[0]["alert_message"].as_str().unwrap().contains("replaced")); - assert_eq!(body[1]["kind"], "ack"); + assert_eq!(body[0]["alert_status"], "invalid-parameters"); Ok(()) } #[tokio::test] -async fn test_provision_invalid_params() -> anyhow::Result<()> { +async fn test_provision_token_rejects_garbage_token() -> anyhow::Result<()> { let _guard = init_logger(); let (app, _state, _handles) = make_router()?; - let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiI1ZTNlODMzZi04NGM3LTQ1NDEtYjY3Ni1hY2MzMjk5ZTM5YjgifQ.1qECGlrW7y9HWFArc6GPHLGTOY7PhAvzKJ5XMRBg4k4"; - - let op_id = Uuid::new_v4(); - let op = json!([{ - "id": op_id, - "kind": "provision-credentials", - "token": token, - "proxy_credendial": { "kind": "unknown" }, - "target_credential": { "kind": "username-password", "username": "u", "password": "p" }, + "id": Uuid::new_v4(), + "kind": "provision-token", + "token": "not-a-jwt", }]); - let request = preflight_request(op)?; - let response = app.oneshot(request).await?; + let response = app.oneshot(preflight_request(op)?).await?; let body = response.into_body().collect().await?.to_bytes(); let body: serde_json::Value = serde_json::from_slice(&body)?;