Skip to content

Commit

Permalink
Merge pull request #128 from TetsuOtter/126-bad-scheme
Browse files Browse the repository at this point in the history
Bad Schemeエラーが表示される問題を修正 (AppLink処理をIOプロジェクトに移動)
  • Loading branch information
TetsuOtter authored May 19, 2024
2 parents 3291dee + 9b349ea commit 9396fad
Show file tree
Hide file tree
Showing 16 changed files with 595 additions and 343 deletions.
File renamed without changes.
File renamed without changes.
File renamed without changes.
80 changes: 80 additions & 0 deletions TRViS.IO.Tests/ResourceInfo/AppLinkInfo.Tests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
namespace TRViS.IO.RequestInfo.Tests;

public class AppLinkInfoTests
{
static readonly Version expectedDefaultVersion = new(1, 0);
const string EMPTY_JSON_BASE64 = "e30K";
[Test]
public void OnlyJsonPath()
{
AppLinkInfo actual = AppLinkInfo.FromAppLink("trvis://app/open/json?path=https://example.com/db.json");
AppLinkInfo expected = new(
AppLinkInfo.FileType.Json,
expectedDefaultVersion,
ResourceUri: new Uri("https://example.com/db.json")
);
Assert.That(actual, Is.EqualTo(expected));
}

[Test]
public void OnlySqlitePath()
{
AppLinkInfo actual = AppLinkInfo.FromAppLink("trvis://app/open/sqlite?path=https://example.com/trvis.db");
AppLinkInfo expected = new(
AppLinkInfo.FileType.Sqlite,
expectedDefaultVersion,
ResourceUri: new Uri("https://example.com/trvis.db")
);
Assert.That(actual, Is.EqualTo(expected));
}

[Test]
public void EmptyLink()
=> Assert.Multiple(() =>
{
Assert.That(() => AppLinkInfo.FromAppLink(""), Throws.Exception.TypeOf<UriFormatException>());
Assert.That(() => AppLinkInfo.FromAppLink("trvis://app/"), Throws.ArgumentException);
Assert.That(() => AppLinkInfo.FromAppLink("trvis://app/open/"), Throws.ArgumentException);
});

[Test]
public void UnknownFileType()
=> Assert.That(() => AppLinkInfo.FromAppLink("trvis://app/open/a"), Throws.ArgumentException);

[Test]
public void WithoutQuery()
=> Assert.That(() => AppLinkInfo.FromAppLink("trvis://app/open/json"), Throws.ArgumentException);

[Test]
public void VersionTest()
{
Assert.That(
AppLinkInfo.FromAppLink($"trvis://app/open/json?ver=&data={EMPTY_JSON_BASE64}").Version,
Is.EqualTo(expectedDefaultVersion),
"version empty => will be default"
);
Assert.That(
AppLinkInfo.FromAppLink($"trvis://app/open/json?ver=0.1&data={EMPTY_JSON_BASE64}").Version,
Is.EqualTo(new Version(0, 1)),
"version 0.1"
);
Assert.That(
() => AppLinkInfo.FromAppLink($"trvis://app/open/json?ver=2.0&data={EMPTY_JSON_BASE64}"),
Throws.ArgumentException,
"Unsupported version"
);
}

[Test]
public void WithoutPathAndData()
=> Assert.That(() => AppLinkInfo.FromAppLink("trvis://app/open/json?path="), Throws.ArgumentException);

[Test]
public void Path_WithoutScheme()
{
string appLink = "trvis://app/open/json?path=/abc/def";
Uri? actual = AppLinkInfo.FromAppLink(appLink).ResourceUri;
Assert.That(actual, Is.Not.Null);
Assert.That(actual?.ToString(), Is.EqualTo("file:///abc/def"));
}
}
File renamed without changes.
File renamed without changes.
File renamed without changes.
158 changes: 158 additions & 0 deletions TRViS.IO/OpenFile.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
using System.Net;
using TRViS.IO.RequestInfo;

namespace TRViS.IO;

public class OpenFile(HttpClient httpClient)
{
public delegate Task<bool> CanContinueWhenResourceUriContainsIpDelegate(IPAddress ip, CancellationToken token);
public delegate Task<bool> CanContinueWhenHeadRequestSuccessDelegate(HttpResponseMessage response, CancellationToken token);

public CanContinueWhenHeadRequestSuccessDelegate? CanContinueWhenHeadRequestSuccess { get; set; } = null;
public CanContinueWhenResourceUriContainsIpDelegate? CanContinueWhenResourceUriContainsIp { get; set; } = null;
private readonly HttpClient HttpClient = httpClient;

public Task<ILoader> OpenAppLinkAsync(
string appLink,
CancellationToken token
)
{
AppLinkInfo appLinkInfo = AppLinkInfo.FromAppLink(appLink);
return OpenAppLinkAsync(
appLinkInfo,
token
);
}

public Task<ILoader> OpenAppLinkAsync(
AppLinkInfo appLinkInfo,
CancellationToken token
)
{
try {
if (appLinkInfo.ResourceUri is not null) {
return OpenAppLink_PathTypeAsync(
appLinkInfo,
token
);
}
} catch (Exception e) {
// Contentがセットされている場合は、Contentでの処理を試みる
// (例外が握りつぶされてしまうため、そこは何とかしたい)
if (appLinkInfo.Content is null || appLinkInfo.Content.Length == 0) {
return Task.FromException<ILoader>(e);
}
}

if (appLinkInfo.Content is not null) {
return Task.FromResult(OpenAppLink_DataType(appLinkInfo, token));
}

throw new ArgumentException("ResourceUri and Content are null");
}

private async Task<ILoader> OpenAppLink_PathTypeAsync(
AppLinkInfo appLinkInfo,
CancellationToken token
)
{
if (appLinkInfo.ResourceUri is null) {
throw new ArgumentException("ResourceUri is null");
}

token.ThrowIfCancellationRequested();

Uri uri = appLinkInfo.ResourceUri;
return uri.Scheme switch
{
"file" => appLinkInfo.FileTypeInfo switch
{
AppLinkInfo.FileType.Json => await LoaderJson.InitFromFileAsync(uri.LocalPath, token),
AppLinkInfo.FileType.Sqlite => new LoaderSQL(uri.LocalPath),
_ => throw new ArgumentException("Unknown file type"),
},
"http" or "https" => await OpenAppLink_HttpTypeAsync(
appLinkInfo,
uri,
token
),
_ => throw new ArgumentException("Unknown scheme"),
};
}

private static ILoader OpenAppLink_DataType(
AppLinkInfo appLinkInfo,
CancellationToken token
)
{
if (appLinkInfo.Content is null || appLinkInfo.Content.Length == 0) {
throw new ArgumentException("Content is null or empty");
}

if (appLinkInfo.FileTypeInfo != AppLinkInfo.FileType.Json) {
throw new ArgumentException("This file type is not supported");
}

token.ThrowIfCancellationRequested();

return LoaderJson.InitFromBytes(appLinkInfo.Content);
}

private async Task<ILoader> OpenAppLink_HttpTypeAsync(
AppLinkInfo appLinkInfo,
Uri uri,
CancellationToken token
)
{
if (appLinkInfo.FileTypeInfo != AppLinkInfo.FileType.Json) {
throw new ArgumentException("This file type is not supported");
}

bool isHostIp = uri.HostNameType is UriHostNameType.IPv4 or UriHostNameType.IPv6;
if (isHostIp
&& this.CanContinueWhenResourceUriContainsIp is not null
&& IPAddress.TryParse(uri.Host, out IPAddress? ip)
&& !await this.CanContinueWhenResourceUriContainsIp(ip, token)) {
throw new OperationCanceledException("cancelled by CanContinueWhenResourceUriContainsIp");
}

token.ThrowIfCancellationRequested();

{
using HttpRequestMessage request = new(HttpMethod.Head, uri);
using HttpResponseMessage result = await this.HttpClient.SendAsync(request, token);

if (!result.IsSuccessStatusCode) {
throw new HttpRequestException(
$"HEAD request to ${uri} failed with code: {result.StatusCode}",
inner: null,
statusCode: result.StatusCode
);
}

if (this.CanContinueWhenHeadRequestSuccess is not null
&& !await this.CanContinueWhenHeadRequestSuccess(result, token)) {
throw new OperationCanceledException("cancelled by CanContinueWhenHeadRequestSuccess");
}
}

{
using HttpRequestMessage request = new(HttpMethod.Get, uri);
using HttpResponseMessage result = await this.HttpClient.SendAsync(request, token);
if (!result.IsSuccessStatusCode)
{
throw new HttpRequestException(
$"GET request to ${uri} failed with code: {result.StatusCode}",
inner: null,
statusCode: result.StatusCode
);
}

await using Stream stream = result.Content.ReadAsStream(token);
// メソッドの先頭でJSONかチェックしているため、ここにはJSONしか来ない
ILoader loader = await LoaderJson.InitFromStreamAsync(stream, token);

return loader;
}
}
}
128 changes: 128 additions & 0 deletions TRViS.IO/RequestInfo/AppLinkInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
using System.Collections.Specialized;
using System.Web;

namespace TRViS.IO.RequestInfo;

public record AppLinkInfo(
AppLinkInfo.FileType FileTypeInfo,
Version Version,
AppLinkInfo.CompressionType CompressionTypeInfo = AppLinkInfo.CompressionType.None,
AppLinkInfo.EncryptionType EncryptionTypeInfo = AppLinkInfo.EncryptionType.None,
Uri? ResourceUri = null,
byte[]? Content = null,
byte[]? DecryptionKey = null,
Uri? RealtimeServiceUri = null,
string? RealtimeServiceToken = null,
Version? RealtimeServiceVersion = null
)
{
public enum FileType
{
Sqlite,
Json,
};

public enum CompressionType
{
None,
Gzip,
};

public enum EncryptionType
{
None,
};

static readonly Version supportedMaxVersion = new(1, 0);
const string OPEN_FILE_JSON = "/open/json";
const string OPEN_FILE_SQLITE = "/open/sqlite";

public static AppLinkInfo FromAppLink(
string appLink
)
=> AppLinkInfo.FromAppLink(new Uri(appLink));

public static AppLinkInfo FromAppLink(
Uri uri
)
{
// Scheme部分はチェックしない
if (uri.Host != "app")
{
throw new ArgumentException("host is not `app`");
}

string path = uri.LocalPath;
AppLinkInfo.FileType? fileType = path switch
{
OPEN_FILE_JSON => AppLinkInfo.FileType.Json,
OPEN_FILE_SQLITE => AppLinkInfo.FileType.Sqlite,
_ => null,
};
if (fileType is null)
{
throw new ArgumentException("Unknown file type");
}

if (string.IsNullOrEmpty(uri.Query))
{
throw new ArgumentException("Query is empty");
}

NameValueCollection queryParams = HttpUtility.ParseQueryString(uri.Query);
string? versionQuery = queryParams["ver"];
Version version = string.IsNullOrEmpty(versionQuery) ? new(1,0) : new(versionQuery);
if (supportedMaxVersion < version) {
throw new ArgumentException("Unsupported version");
}

AppLinkInfo.CompressionType compressionType = queryParams["cmp"] switch
{
null or "" or "none" => AppLinkInfo.CompressionType.None,
"gzip" => AppLinkInfo.CompressionType.Gzip,
_ => throw new ArgumentException("Unknown compression type"),
};

AppLinkInfo.EncryptionType encryptionType = queryParams["enc"] switch
{
null or "" or "none" => AppLinkInfo.EncryptionType.None,
_ => throw new ArgumentException("Unknown encryption type"),
};

string? resourceUriQuery = queryParams["path"];
string? dataQuery = queryParams["data"];
string? decryptionKeyQuery = queryParams["key"];
if (encryptionType != AppLinkInfo.EncryptionType.None && string.IsNullOrEmpty(decryptionKeyQuery))
{
throw new ArgumentException("DecryptionKey is required when EncryptionType is not None");
}

if (string.IsNullOrEmpty(resourceUriQuery) && string.IsNullOrEmpty(dataQuery))
{
throw new ArgumentException("At least one of ResourceUri or Data must be set");
}

string? realtimeServiceUriQuery = queryParams["rts"];
string? realtimeServiceToken = queryParams["rtk"];
string? realtimeServiceVersionQuery = queryParams["rtv"];
Version? realtimeServiceVersion = string.IsNullOrEmpty(realtimeServiceVersionQuery) ? null : new(realtimeServiceVersionQuery);

Uri? resourceUri = string.IsNullOrEmpty(resourceUriQuery) ? null : new Uri(resourceUriQuery);
byte[]? content = string.IsNullOrEmpty(dataQuery) ? null : Utils.UrlSafeBase64Decode(dataQuery);
byte[]? decryptionKey = string.IsNullOrEmpty(decryptionKeyQuery) ? null : Utils.UrlSafeBase64Decode(decryptionKeyQuery);
Uri? realtimeServiceUri = string.IsNullOrEmpty(realtimeServiceUriQuery) ? null : new Uri(realtimeServiceUriQuery);

return new AppLinkInfo(
fileType.Value,
version,
compressionType,
encryptionType,
resourceUri,
content,
decryptionKey,
realtimeServiceUri,
realtimeServiceToken,
realtimeServiceVersion
);
}
}
1 change: 1 addition & 0 deletions TRViS.IO/TRViS.IO.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<LangVersion>12</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
Expand Down
Loading

0 comments on commit 9396fad

Please sign in to comment.