sources/Google.Solutions.IapDesktop.Core/ClientModel/Protocol/AppProtocolConfigurationFile.cs (239 lines of code) (raw):
//
// Copyright 2023 Google LLC
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
//
using Google.Apis.Json;
using Google.Solutions.IapDesktop.Core.ClientModel.Traits;
using Google.Solutions.Platform;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
namespace Google.Solutions.IapDesktop.Core.ClientModel.Protocol
{
/// <summary>
/// Parser for protocol configuration files.
///
/// Example:
///
/// {
/// 'version': 1,
/// 'name': 'telnet',
/// 'condition': 'isLinux()',
/// 'remotePort': 23,
/// 'client': {
/// 'executable': '%SystemRoot%\system32\telnet.exe',
/// 'arguments': '{host} {port}'
/// }
/// }
///
/// The executable can be either the name of a registered app
/// (for ex, "chrome.exe"), or a path to an executable file.
///
/// paths Executable and arguments can contain environment variables,
/// for example:
///
/// %AppData%\.myprofile
///
/// Arguments can contain the following placeholders:
///
/// {port}: the local port to connect to
/// {host}: the locat IP address to connect to
/// {username}: the username to authenticate with (can be empty)
///
/// </summary>
/// <see cref="https://learn.microsoft.com/en-us/windows/win32/shell/app-registration"/>
public static class AppProtocolConfigurationFile
{
/// <summary>
/// File extension for protocol configuration files
/// (IAPC = IAP App Protocol Configuration).
/// </summary>
public const string FileExtension = ".iapc";
//---------------------------------------------------------------------
// Deserialization.
//---------------------------------------------------------------------
private static AppProtocol ReadSection(MainSection section)
{
if (section == null)
{
throw new InvalidAppProtocolException(
"The protocol configuration is empty");
}
else if (
section.SchemaVersion < MainSection.MinSchemaVersion ||
section.SchemaVersion > MainSection.CurrentSchemaVersion)
{
throw new InvalidAppProtocolException(
"The protocol configuration uses an unsupported schema version");
}
return new AppProtocol(
section.ParseName(),
section.ParseCondition(),
section.ParseRemotePort(),
section.ParseLocalEndpoint(),
section.ParseClientSection());
}
public static AppProtocol ReadJson(string json)
{
try
{
return ReadSection(NewtonsoftJsonSerializer
.Instance
.Deserialize<MainSection>(json));
}
catch (JsonException e)
{
throw new InvalidAppProtocolException(
"The protocol configuration contains format errors", e);
}
}
public static Task<AppProtocol> ReadStreamAsync(Stream stream)
{
return Task.Run(() =>
{
try
{
return ReadSection(NewtonsoftJsonSerializer
.Instance
.Deserialize<MainSection>(stream));
}
catch (InvalidAppProtocolException e)
{
throw new InvalidAppProtocolException(
$"The protocol configuration contains format errors", e);
}
catch (JsonException e)
{
throw new InvalidAppProtocolException(
$"The protocol configuration contains format errors", e);
}
});
}
public static Task<AppProtocol> ReadFileAsync(string path)
{
return Task.Run(() =>
{
try
{
using (var stream = File.OpenRead(path))
{
return ReadSection(NewtonsoftJsonSerializer
.Instance
.Deserialize<MainSection>(stream));
}
}
catch (InvalidAppProtocolException e)
{
throw new InvalidAppProtocolException(
$"The protocol configuration file {path} contains format errors", e);
}
catch (JsonException e)
{
throw new InvalidAppProtocolException(
$"The protocol configuration file {path} contains format errors", e);
}
});
}
//---------------------------------------------------------------------
// De/Serialization classes.
//---------------------------------------------------------------------
internal class MainSection
{
internal const ushort MinSchemaVersion = 1;
internal const ushort CurrentSchemaVersion = 1;
/// <summary>
/// Schema version.
/// </summary>
[JsonProperty("version")]
public ushort SchemaVersion { get; set; }
/// <summary>
/// Name of the protocol. The name isn't guaranteed to be unique.
/// </summary>
[JsonProperty("name")]
public string? Name { get; set; }
/// <summary>
/// Conditions for this protocol to be available. Can be
/// an expression of one or more required traits.
///
/// trait1() && trait2() && trait3()
///
/// Currently, only the && operator is available.
/// </summary>
[JsonProperty("condition")]
public string? Condition { get; set; }
/// <summary>
/// Remote port to connect to.
/// </summary>
[JsonProperty("remotePort")]
public string? RemotePort { get; set; }
/// <summary>
/// Optional: Local port.
/// </summary>
[JsonProperty("localPort")]
public string? LocalPort { get; set; }
/// <summary>
/// Optional: Client application to launch.
/// </summary>
[JsonProperty("client")]
public ClientSection? Client { get; set; }
internal string ParseName()
{
if (string.IsNullOrWhiteSpace(this.Name))
{
throw new InvalidAppProtocolException("A name is required");
}
Debug.Assert(this.Name != null);
return this.Name!;
}
internal IEnumerable<ITrait> ParseCondition()
{
if (this.Condition == null)
{
yield break;
}
var clauses = this.Condition
.Replace("&&", "\0")
.Split('\0')
.Select(s => s.Trim())
.Where(s => !string.IsNullOrEmpty(s))
.ToList();
foreach (var clause in clauses)
{
if (InstanceTrait.TryParse(clause, out var instanceTrait))
{
Debug.Assert(instanceTrait != null);
yield return instanceTrait!;
}
else if (WindowsTrait.TryParse(clause, out var windowsTrait))
{
Debug.Assert(windowsTrait != null);
yield return windowsTrait!;
}
else if (LinuxTrait.TryParse(clause, out var linuxTrait))
{
Debug.Assert(linuxTrait != null);
yield return linuxTrait!;
}
else
{
throw new InvalidAppProtocolException(
"The condition contains an unrecognized clause: " + clause);
}
}
}
internal ushort ParseRemotePort()
{
if (ushort.TryParse(this.RemotePort, out var port))
{
return port;
}
else
{
throw new InvalidAppProtocolException("A remote port is required");
}
}
internal IPEndPoint? ParseLocalEndpoint()
{
var localPort = this.LocalPort?.Trim();
if (string.IsNullOrEmpty(localPort))
{
return null;
}
Debug.Assert(localPort != null);
var parts = localPort!.Split(':');
if (parts.Length == 1)
{
//
// Port only.
//
if (ushort.TryParse(parts[0], out var port))
{
return new IPEndPoint(IPAddress.Loopback, port);
}
}
else if (parts.Length == 2)
{
//
// IP:port.
//
if (IPAddress.TryParse(parts[0], out var ip) &&
ushort.TryParse(parts[1], out var port))
{
return new IPEndPoint(ip, port);
}
}
throw new InvalidAppProtocolException(
"The local port must be a number or a IPv4/port tuple in the " +
"format <ip>:<port>.");
}
internal IAppProtocolClient? ParseClientSection()
{
if (this.Client == null ||
string.IsNullOrWhiteSpace(this.Client.Executable))
{
return null;
}
Debug.Assert(this.Client.Executable != null);
if (!UserEnvironment.TryResolveAppPath(
this.Client.Executable!,
out var executablePath))
{
executablePath = UserEnvironment.ExpandEnvironmentStrings(
this.Client.Executable!);
}
Debug.Assert(executablePath != null);
return new AppProtocolClient(
executablePath!,
UserEnvironment.ExpandEnvironmentStrings(this.Client.Arguments));
}
}
internal class ClientSection
{
/// <summary>
/// Path to executable to launch. The path can contain
/// environment variables, for example:
///
/// %ProgramFiles(x86)%\program.exe
///
/// </summary>
[JsonProperty("executable")]
public string? Executable { get; set; }
/// <summary>
/// Optional: Arguments to pass to executable.
/// </summary>
[JsonProperty("arguments")]
public string? Arguments { get; set; }
}
}
public class InvalidAppProtocolException : FormatException
{
public InvalidAppProtocolException(string message) : base(message)
{
}
public InvalidAppProtocolException(string message, Exception innerException)
: base(message, innerException)
{
}
}
}