-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #128 from TetsuOtter/126-bad-scheme
Bad Schemeエラーが表示される問題を修正 (AppLink処理をIOプロジェクトに移動)
- Loading branch information
Showing
16 changed files
with
595 additions
and
343 deletions.
There are no files selected for viewing
File renamed without changes.
File renamed without changes.
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.