[merge] Merge branch 'dev-abyss'
This commit is contained in:
2
.idea/.idea.Abyss/.idea/dataSources.local.xml
generated
2
.idea/.idea.Abyss/.idea/dataSources.local.xml
generated
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="dataSourceStorageLocal" created-in="RD-252.23892.524">
|
<component name="dataSourceStorageLocal" created-in="RD-252.25557.182">
|
||||||
<data-source name="user" uuid="91acd9d8-5f8b-442f-9d50-17006d4e1ac7">
|
<data-source name="user" uuid="91acd9d8-5f8b-442f-9d50-17006d4e1ac7">
|
||||||
<database-info product="SQLite" version="3.45.1" jdbc-version="4.2" driver-name="SQLite JDBC" driver-version="3.45.1.0" dbms="SQLITE" exact-version="3.45.1" exact-driver-version="3.45">
|
<database-info product="SQLite" version="3.45.1" jdbc-version="4.2" driver-name="SQLite JDBC" driver-version="3.45.1.0" dbms="SQLITE" exact-version="3.45.1" exact-driver-version="3.45">
|
||||||
<identifier-quote-string>"</identifier-quote-string>
|
<identifier-quote-string>"</identifier-quote-string>
|
||||||
|
|||||||
48
.idea/.idea.Abyss/.idea/workspace.xml
generated
48
.idea/.idea.Abyss/.idea/workspace.xml
generated
@@ -10,13 +10,8 @@
|
|||||||
</component>
|
</component>
|
||||||
<component name="ChangeListManager">
|
<component name="ChangeListManager">
|
||||||
<list default="true" id="bf317275-3039-49bb-a475-725a800a0cce" name="Changes" comment="">
|
<list default="true" id="bf317275-3039-49bb-a475-725a800a0cce" name="Changes" comment="">
|
||||||
<change afterPath="$PROJECT_DIR$/Abyss/Components/Static/ControllerExtensions.cs" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/.idea/.idea.Abyss/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/.idea.Abyss/.idea/workspace.xml" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/.idea/.idea.Abyss/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/.idea.Abyss/.idea/workspace.xml" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/Abyss/Components/Controllers/Media/ImageController.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Abyss/Components/Controllers/Media/ImageController.cs" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/Abyss/Components/Services/UserService.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Abyss/Components/Services/UserService.cs" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/Abyss/Components/Controllers/Media/LiveController.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Abyss/Components/Controllers/Media/LiveController.cs" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/Abyss/Components/Controllers/Media/VideoController.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Abyss/Components/Controllers/Media/VideoController.cs" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/Abyss/Components/Controllers/Security/UserController.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Abyss/Components/Controllers/Security/UserController.cs" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/Abyss/Program.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Abyss/Program.cs" afterDir="false" />
|
|
||||||
</list>
|
</list>
|
||||||
<option name="SHOW_DIALOG" value="false" />
|
<option name="SHOW_DIALOG" value="false" />
|
||||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||||
@@ -31,7 +26,9 @@
|
|||||||
</component>
|
</component>
|
||||||
<component name="HighlightingSettingsPerFile">
|
<component name="HighlightingSettingsPerFile">
|
||||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/DecompilerCache/decompiler/011a191356a243438f987de3ec3d6c6230800/04/8419ff35/ServiceProvider.cs" root0="FORCE_HIGHLIGHTING" />
|
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/DecompilerCache/decompiler/011a191356a243438f987de3ec3d6c6230800/04/8419ff35/ServiceProvider.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
|
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/DecompilerCache/decompiler/457530be4752476295767457c3639889d1a000/25/817def70/ConfiguredValueTaskAwaitable`1.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/DecompilerCache/decompiler/457530be4752476295767457c3639889d1a000/4c/4b962087/Monitor.cs" root0="FORCE_HIGHLIGHTING" />
|
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/DecompilerCache/decompiler/457530be4752476295767457c3639889d1a000/4c/4b962087/Monitor.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
|
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/DecompilerCache/decompiler/457530be4752476295767457c3639889d1a000/af/aac0eaa5/ExceptionDispatchInfo.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/DecompilerCache/decompiler/457530be4752476295767457c3639889d1a000/d0/3b166e9e/String.cs" root0="FORCE_HIGHLIGHTING" />
|
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/DecompilerCache/decompiler/457530be4752476295767457c3639889d1a000/d0/3b166e9e/String.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/DecompilerCache/decompiler/5df2accb46d040ccbbbe8331bf4d24b61daa00/df/93debd37/ControllerBase.cs" root0="FORCE_HIGHLIGHTING" />
|
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/DecompilerCache/decompiler/5df2accb46d040ccbbbe8331bf4d24b61daa00/df/93debd37/ControllerBase.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/DecompilerCache/decompiler/7598e47d5cdf4107ba88f8220720fdc89000/a6/79d67871/xxHash128.cs" root0="FORCE_HIGHLIGHTING" />
|
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/DecompilerCache/decompiler/7598e47d5cdf4107ba88f8220720fdc89000/a6/79d67871/xxHash128.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
@@ -40,12 +37,15 @@
|
|||||||
<setting file="file://$PROJECT_DIR$/Abyss/Components/Controllers/Media/LiveController.cs" root0="FORCE_HIGHLIGHTING" />
|
<setting file="file://$PROJECT_DIR$/Abyss/Components/Controllers/Media/LiveController.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
<setting file="file://$PROJECT_DIR$/Abyss/Components/Controllers/Security/UserController.cs" root0="FORCE_HIGHLIGHTING" />
|
<setting file="file://$PROJECT_DIR$/Abyss/Components/Controllers/Security/UserController.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
<setting file="file://$PROJECT_DIR$/Abyss/Components/Controllers/Task/TaskController.cs" root0="FORCE_HIGHLIGHTING" />
|
<setting file="file://$PROJECT_DIR$/Abyss/Components/Controllers/Task/TaskController.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
|
<setting file="file://$PROJECT_DIR$/Abyss/Components/Services/AbyssService.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
<setting file="file://$PROJECT_DIR$/Abyss/Components/Services/ConfigureService.cs" root0="FORCE_HIGHLIGHTING" />
|
<setting file="file://$PROJECT_DIR$/Abyss/Components/Services/ConfigureService.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
<setting file="file://$PROJECT_DIR$/Abyss/Components/Services/ResourceService.cs" root0="FORCE_HIGHLIGHTING" />
|
<setting file="file://$PROJECT_DIR$/Abyss/Components/Services/ResourceService.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
<setting file="file://$PROJECT_DIR$/Abyss/Components/Services/TaskService.cs" root0="FORCE_HIGHLIGHTING" />
|
<setting file="file://$PROJECT_DIR$/Abyss/Components/Services/TaskService.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
<setting file="file://$PROJECT_DIR$/Abyss/Components/Services/UserService.cs" root0="FORCE_HIGHLIGHTING" />
|
<setting file="file://$PROJECT_DIR$/Abyss/Components/Services/UserService.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
<setting file="file://$PROJECT_DIR$/Abyss/Components/Static/Helpers.cs" root0="FORCE_HIGHLIGHTING" />
|
<setting file="file://$PROJECT_DIR$/Abyss/Components/Static/Helpers.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
<setting file="file://$PROJECT_DIR$/Abyss/Components/Static/HttpContextExtensions.cs" root0="FORCE_HIGHLIGHTING" />
|
<setting file="file://$PROJECT_DIR$/Abyss/Components/Tools/AbyssStream.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
|
<setting file="file://$PROJECT_DIR$/Abyss/Components/Tools/HttpHelper.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
|
<setting file="file://$PROJECT_DIR$/Abyss/Components/Tools/HttpReader.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
<setting file="file://$PROJECT_DIR$/Abyss/Model/Bookmark.cs" root0="FORCE_HIGHLIGHTING" />
|
<setting file="file://$PROJECT_DIR$/Abyss/Model/Bookmark.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
<setting file="file://$PROJECT_DIR$/Abyss/Model/ChallengeResponse.cs" root0="FORCE_HIGHLIGHTING" />
|
<setting file="file://$PROJECT_DIR$/Abyss/Model/ChallengeResponse.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
<setting file="file://$PROJECT_DIR$/Abyss/Model/Chip.cs" root0="FORCE_HIGHLIGHTING" />
|
<setting file="file://$PROJECT_DIR$/Abyss/Model/Chip.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
@@ -74,7 +74,7 @@
|
|||||||
</component>
|
</component>
|
||||||
<component name="PropertiesComponent">{
|
<component name="PropertiesComponent">{
|
||||||
"keyToString": {
|
"keyToString": {
|
||||||
".NET Launch Settings Profile.Abyss: http.executor": "Run",
|
".NET Launch Settings Profile.Abyss: http.executor": "Debug",
|
||||||
".NET Launch Settings Profile.Abyss: https.executor": "Debug",
|
".NET Launch Settings Profile.Abyss: https.executor": "Debug",
|
||||||
".NET Project.AbyssCli.executor": "Run",
|
".NET Project.AbyssCli.executor": "Run",
|
||||||
"ASKED_SHARE_PROJECT_CONFIGURATION_FILES": "true",
|
"ASKED_SHARE_PROJECT_CONFIGURATION_FILES": "true",
|
||||||
@@ -85,7 +85,7 @@
|
|||||||
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
|
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
|
||||||
"RunOnceActivity.git.unshallow": "true",
|
"RunOnceActivity.git.unshallow": "true",
|
||||||
"XThreadsFramesViewSplitterKey": "0.30266345",
|
"XThreadsFramesViewSplitterKey": "0.30266345",
|
||||||
"git-widget-placeholder": "main",
|
"git-widget-placeholder": "dev-abyss",
|
||||||
"last_opened_file_path": "/storage/Images/31/summary.json",
|
"last_opened_file_path": "/storage/Images/31/summary.json",
|
||||||
"node.js.detected.package.eslint": "true",
|
"node.js.detected.package.eslint": "true",
|
||||||
"node.js.detected.package.tslint": "true",
|
"node.js.detected.package.tslint": "true",
|
||||||
@@ -152,25 +152,8 @@
|
|||||||
<option name="Build" />
|
<option name="Build" />
|
||||||
</method>
|
</method>
|
||||||
</configuration>
|
</configuration>
|
||||||
<configuration name="Abyss: https" type="LaunchSettings" factoryName=".NET Launch Settings Profile">
|
|
||||||
<option name="LAUNCH_PROFILE_PROJECT_FILE_PATH" value="$PROJECT_DIR$/Abyss/Abyss.csproj" />
|
|
||||||
<option name="LAUNCH_PROFILE_TFM" value="net9.0" />
|
|
||||||
<option name="LAUNCH_PROFILE_NAME" value="https" />
|
|
||||||
<option name="USE_EXTERNAL_CONSOLE" value="0" />
|
|
||||||
<option name="USE_MONO" value="0" />
|
|
||||||
<option name="RUNTIME_ARGUMENTS" value="" />
|
|
||||||
<option name="GENERATE_APPLICATIONHOST_CONFIG" value="1" />
|
|
||||||
<option name="SHOW_IIS_EXPRESS_OUTPUT" value="0" />
|
|
||||||
<option name="SEND_DEBUG_REQUEST" value="1" />
|
|
||||||
<option name="ADDITIONAL_IIS_EXPRESS_ARGUMENTS" value="" />
|
|
||||||
<option name="AUTO_ATTACH_CHILDREN" value="0" />
|
|
||||||
<method v="2">
|
|
||||||
<option name="Build" />
|
|
||||||
</method>
|
|
||||||
</configuration>
|
|
||||||
<list>
|
<list>
|
||||||
<item itemvalue=".NET Launch Settings Profile.Abyss: http" />
|
<item itemvalue=".NET Launch Settings Profile.Abyss: http" />
|
||||||
<item itemvalue=".NET Launch Settings Profile.Abyss: https" />
|
|
||||||
<item itemvalue=".NET Project.AbyssCli" />
|
<item itemvalue=".NET Project.AbyssCli" />
|
||||||
<item itemvalue="Publish to folder.Publish Abyss to folder" />
|
<item itemvalue="Publish to folder.Publish Abyss to folder" />
|
||||||
<item itemvalue="Publish to folder.Publish Abyss to folder x86" />
|
<item itemvalue="Publish to folder.Publish Abyss to folder x86" />
|
||||||
@@ -208,7 +191,16 @@
|
|||||||
<workItem from="1757429030386" duration="20000" />
|
<workItem from="1757429030386" duration="20000" />
|
||||||
<workItem from="1757508119360" duration="1704000" />
|
<workItem from="1757508119360" duration="1704000" />
|
||||||
<workItem from="1757519520290" duration="14000" />
|
<workItem from="1757519520290" duration="14000" />
|
||||||
<workItem from="1757567561745" duration="2019000" />
|
<workItem from="1757567561745" duration="2452000" />
|
||||||
|
<workItem from="1757597908282" duration="9750000" />
|
||||||
|
<workItem from="1757648650473" duration="9000" />
|
||||||
|
<workItem from="1757649246468" duration="4023000" />
|
||||||
|
<workItem from="1757653914660" duration="1923000" />
|
||||||
|
<workItem from="1757680205207" duration="3000" />
|
||||||
|
<workItem from="1757684000965" duration="2511000" />
|
||||||
|
<workItem from="1757687641035" duration="2969000" />
|
||||||
|
<workItem from="1757693751836" duration="667000" />
|
||||||
|
<workItem from="1757694833696" duration="11000" />
|
||||||
</task>
|
</task>
|
||||||
<servers />
|
<servers />
|
||||||
</component>
|
</component>
|
||||||
@@ -228,7 +220,7 @@
|
|||||||
<entry key="branch">
|
<entry key="branch">
|
||||||
<value>
|
<value>
|
||||||
<list>
|
<list>
|
||||||
<option value="dev-task" />
|
<option value="main" />
|
||||||
</list>
|
</list>
|
||||||
</value>
|
</value>
|
||||||
</entry>
|
</entry>
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||||
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AConfiguredValueTaskAwaitable_00601_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F457530be4752476295767457c3639889d1a000_003F25_003F817def70_003FConfiguredValueTaskAwaitable_00601_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AControllerBase_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F5df2accb46d040ccbbbe8331bf4d24b61daa00_003Fdf_003F93debd37_003FControllerBase_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AControllerBase_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F5df2accb46d040ccbbbe8331bf4d24b61daa00_003Fdf_003F93debd37_003FControllerBase_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AExceptionDispatchInfo_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F457530be4752476295767457c3639889d1a000_003Faf_003Faac0eaa5_003FExceptionDispatchInfo_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AKey_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Ff09ccaeb94c34c2299acd3efee0facee1a400_003F81_003F137b58b4_003FKey_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AKey_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Ff09ccaeb94c34c2299acd3efee0facee1a400_003F81_003F137b58b4_003FKey_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AMonitor_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F457530be4752476295767457c3639889d1a000_003F4c_003F4b962087_003FMonitor_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AMonitor_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F457530be4752476295767457c3639889d1a000_003F4c_003F4b962087_003FMonitor_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AServiceProvider_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F011a191356a243438f987de3ec3d6c6230800_003F04_003F8419ff35_003FServiceProvider_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AServiceProvider_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F011a191356a243438f987de3ec3d6c6230800_003F04_003F8419ff35_003FServiceProvider_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AString_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F457530be4752476295767457c3639889d1a000_003Fd0_003F3b166e9e_003FString_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AString_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F457530be4752476295767457c3639889d1a000_003Fd0_003F3b166e9e_003FString_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ATask_00601_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F457530be4752476295767457c3639889d1a000_003F6b_003F2e4babaf_003FTask_00601_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AxxHash128_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F7598e47d5cdf4107ba88f8220720fdc89000_003Fa6_003F79d67871_003FxxHash128_002Ecs/@EntryIndexedValue">ForceIncluded</s:String></wpf:ResourceDictionary>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AxxHash128_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F7598e47d5cdf4107ba88f8220720fdc89000_003Fa6_003F79d67871_003FxxHash128_002Ecs/@EntryIndexedValue">ForceIncluded</s:String></wpf:ResourceDictionary>
|
||||||
190
Abyss/Components/Services/AbyssService.cs
Normal file
190
Abyss/Components/Services/AbyssService.cs
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
using System.Net;
|
||||||
|
using System.Net.Sockets;
|
||||||
|
using System.Text;
|
||||||
|
using Abyss.Components.Tools;
|
||||||
|
|
||||||
|
namespace Abyss.Components.Services;
|
||||||
|
|
||||||
|
public class AbyssService(ILogger<AbyssService> logger, ConfigureService config) : IHostedService, IDisposable
|
||||||
|
{
|
||||||
|
private Task? _executingTask;
|
||||||
|
private CancellationTokenSource? _cts;
|
||||||
|
private readonly TcpListener _listener = new TcpListener(IPAddress.Any, 4096);
|
||||||
|
public readonly int[] AllowedPorts = config.AllowedPorts.Split(' ').Select(int.Parse).ToArray();
|
||||||
|
|
||||||
|
public Task StartAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
_cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||||
|
_executingTask = ExecuteAsync(_cts.Token);
|
||||||
|
return _executingTask.IsCompleted ? _executingTask : Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task UpStreamTunnelAsync(AbyssStream client, NetworkStream upstream, CancellationToken token)
|
||||||
|
{
|
||||||
|
var tunnelUp = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
byte[] buffer = new byte[4096];
|
||||||
|
while (!token.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
int bytesRead = await client.ReadAsync(buffer, 0, buffer.Length, token);
|
||||||
|
if (bytesRead == 0)
|
||||||
|
break;
|
||||||
|
await upstream.WriteAsync(buffer, 0, bytesRead, token);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var tunnelDown = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
byte[] buffer = new byte[4096];
|
||||||
|
while (!token.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
int bytesRead = await upstream.ReadAsync(buffer, 0, buffer.Length, token);
|
||||||
|
if (bytesRead == 0)
|
||||||
|
break;
|
||||||
|
await client.WriteAsync(buffer, 0, bytesRead, token);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await Task.WhenAny(tunnelUp, tunnelDown);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ClientHandlerAsync(TcpClient client, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var stream = await client.GetAbyssStreamAsync(ct: cancellationToken);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var request = HttpHelper.Parse(await HttpReader.ReadHttpMessageAsync(stream, cancellationToken));
|
||||||
|
var port = 80;
|
||||||
|
var sp = request.RequestUri?.ToString().Split(':') ?? [];
|
||||||
|
if (sp.Length == 2)
|
||||||
|
{
|
||||||
|
port = int.Parse(sp[1]);
|
||||||
|
}
|
||||||
|
if (request.Method == "CONNECT")
|
||||||
|
{
|
||||||
|
TcpClient upClient = new TcpClient();
|
||||||
|
await upClient.ConnectAsync("127.0.0.1", port, cancellationToken);
|
||||||
|
|
||||||
|
if (!upClient.Connected)
|
||||||
|
{
|
||||||
|
var err1 = HttpHelper.BuildHttpResponse(
|
||||||
|
504,
|
||||||
|
"Gateway Timeout",
|
||||||
|
new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
["Proxy-Agent"] = "Abyss/0.1",
|
||||||
|
["Content-Length"] = "0"
|
||||||
|
});
|
||||||
|
await stream.WriteAsync(Encoding.UTF8.GetBytes(err1), cancellationToken);
|
||||||
|
throw new Exception("Gateway Timeout");
|
||||||
|
}
|
||||||
|
|
||||||
|
var upstream = upClient.GetStream();
|
||||||
|
var response = HttpHelper.BuildHttpResponse(
|
||||||
|
200,
|
||||||
|
"Connection established",
|
||||||
|
new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
["Proxy-Agent"] = "Abyss/0.1",
|
||||||
|
["Connection"] = "keep-alive"
|
||||||
|
});
|
||||||
|
await stream.WriteAsync(Encoding.UTF8.GetBytes(response), cancellationToken);
|
||||||
|
// Connection established
|
||||||
|
|
||||||
|
logger.LogInformation($"Tunnel for {client.Client.RemoteEndPoint} and upstream {upClient.Client.RemoteEndPoint} created");
|
||||||
|
await UpStreamTunnelAsync(stream, upstream, cancellationToken);
|
||||||
|
logger.LogInformation($"Tunnel for {client.Client.RemoteEndPoint} and upstream {upClient.Client.RemoteEndPoint} will be release");
|
||||||
|
|
||||||
|
upstream.Close();
|
||||||
|
upClient.Close();
|
||||||
|
upClient.Dispose();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
string htmlContent = """
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>405 Method Not Allowed</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Method Not Allowed</h1>
|
||||||
|
<p>The requested HTTP method is not supported by this proxy server.</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
""";
|
||||||
|
byte[] responseBytes = Encoding.UTF8.GetBytes(htmlContent);
|
||||||
|
|
||||||
|
var response = HttpHelper.BuildHttpResponse(
|
||||||
|
405,
|
||||||
|
"Method Not Allowed",
|
||||||
|
new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
["Allow"] = "CONNECT",
|
||||||
|
["Content-Type"] = "text/html; charset=utf-8",
|
||||||
|
["Content-Length"] = responseBytes.Length.ToString()
|
||||||
|
}, htmlContent);
|
||||||
|
|
||||||
|
await stream.WriteAsync(Encoding.UTF8.GetBytes(response), cancellationToken);
|
||||||
|
throw new Exception("Method Not Allowed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
logger.LogError(e.Message);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
stream.Close();
|
||||||
|
client.Close();
|
||||||
|
client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||||
|
{
|
||||||
|
_listener.Start();
|
||||||
|
while (!stoppingToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var c = await _listener.AcceptTcpClientAsync(stoppingToken);
|
||||||
|
_ = Task.Run(() => ClientHandlerAsync(c, stoppingToken), stoppingToken);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "Error occurred in background service");
|
||||||
|
await Task.Delay(TimeSpan.FromSeconds(1), stoppingToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_listener.Stop();
|
||||||
|
logger.LogInformation("TCP listener stopped");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task StopAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (_executingTask == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_cts?.CancelAsync();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
await Task.WhenAny(_executingTask,
|
||||||
|
Task.Delay(Timeout.Infinite, cancellationToken));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_cts?.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ public class ConfigureService
|
|||||||
{
|
{
|
||||||
public string MediaRoot { get; set; } = Environment.GetEnvironmentVariable("MEDIA_ROOT") ?? "/opt";
|
public string MediaRoot { get; set; } = Environment.GetEnvironmentVariable("MEDIA_ROOT") ?? "/opt";
|
||||||
public string DebugMode { get; set; } = Environment.GetEnvironmentVariable("DEBUG_MODE") ?? "Production";
|
public string DebugMode { get; set; } = Environment.GetEnvironmentVariable("DEBUG_MODE") ?? "Production";
|
||||||
|
public string AllowedPorts { get; set; } = Environment.GetEnvironmentVariable("ALLOWED_PORTS") ?? "443"; // Split with ' '
|
||||||
public string Version { get; } = "Alpha v0.1";
|
public string Version { get; } = "Alpha v0.1";
|
||||||
public string UserDatabase { get; set; } = "user.db";
|
public string UserDatabase { get; set; } = "user.db";
|
||||||
public string RaDatabase { get; set; } = "ra.db";
|
public string RaDatabase { get; set; } = "ra.db";
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ public class UserService
|
|||||||
{
|
{
|
||||||
if (_cache.TryGetValue(token, out string? userAndIp))
|
if (_cache.TryGetValue(token, out string? userAndIp))
|
||||||
{
|
{
|
||||||
if (ip != userAndIp?.Split('@')[1])
|
if (ip != userAndIp?.Split('@')[1] && ip != "127.0.0.1")
|
||||||
{
|
{
|
||||||
_logger.LogError($"Token used from another Host: {token}");
|
_logger.LogError($"Token used from another Host: {token}");
|
||||||
Destroy(token);
|
Destroy(token);
|
||||||
|
|||||||
524
Abyss/Components/Tools/AbyssStream.cs
Normal file
524
Abyss/Components/Tools/AbyssStream.cs
Normal file
@@ -0,0 +1,524 @@
|
|||||||
|
// Target: .NET 9
|
||||||
|
// NuGet: NSec.Cryptography (for X25519)
|
||||||
|
// Note: ChaCha20Poly1305 is used from System.Security.Cryptography (available in .NET 7+ / .NET 9)
|
||||||
|
|
||||||
|
using System.Buffers;
|
||||||
|
using System.Buffers.Binary;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
|
||||||
|
using System.Net.Sockets;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
|
||||||
|
using NSec.Cryptography;
|
||||||
|
|
||||||
|
using ChaCha20Poly1305 = System.Security.Cryptography.ChaCha20Poly1305;
|
||||||
|
|
||||||
|
namespace Abyss.Components.Tools
|
||||||
|
{
|
||||||
|
public sealed class AbyssStream : NetworkStream, IDisposable
|
||||||
|
{
|
||||||
|
private const int PublicKeyLength = 32;
|
||||||
|
private const int AeadKeyLen = 32;
|
||||||
|
private const int NonceSaltLen = 4;
|
||||||
|
private const int AeadTagLen = 16;
|
||||||
|
private const int NonceLen = 12; // 4-byte salt + 8-byte counter
|
||||||
|
private const int MaxPlaintextFrame = 64 * 1024; // 64 KiB per frame
|
||||||
|
|
||||||
|
private readonly ChaCha20Poly1305 _aead;
|
||||||
|
private readonly byte[] _sendNonceSalt = new byte[NonceSaltLen];
|
||||||
|
private readonly byte[] _recvNonceSalt = new byte[NonceSaltLen];
|
||||||
|
|
||||||
|
// Counters and locks
|
||||||
|
private ulong _sendCounter;
|
||||||
|
private ulong _recvCounter;
|
||||||
|
private readonly object _sendLock = new();
|
||||||
|
private readonly object _aeadLock = new();
|
||||||
|
|
||||||
|
// Inbound leftover cache (FIFO)
|
||||||
|
private readonly ConcurrentQueue<byte[]> _leftoverQueue = new();
|
||||||
|
private byte[]? _currentLeftoverSegment;
|
||||||
|
private int _currentLeftoverOffset;
|
||||||
|
|
||||||
|
private bool _disposed;
|
||||||
|
|
||||||
|
private AbyssStream(Socket socket, bool ownsSocket, byte[] aeadKey, byte[] sendSalt, byte[] recvSalt)
|
||||||
|
: base(socket, ownsSocket)
|
||||||
|
{
|
||||||
|
if (aeadKey == null || aeadKey.Length != AeadKeyLen) throw new ArgumentException(nameof(aeadKey));
|
||||||
|
if (sendSalt == null || sendSalt.Length != NonceSaltLen) throw new ArgumentException(nameof(sendSalt));
|
||||||
|
if (recvSalt == null || recvSalt.Length != NonceSaltLen) throw new ArgumentException(nameof(recvSalt));
|
||||||
|
|
||||||
|
Array.Copy(sendSalt, 0, _sendNonceSalt, 0, NonceSaltLen);
|
||||||
|
Array.Copy(recvSalt, 0, _recvNonceSalt, 0, NonceSaltLen);
|
||||||
|
|
||||||
|
// ChaCha20Poly1305 is in System.Security.Cryptography in .NET 9
|
||||||
|
_aead = new ChaCha20Poly1305(aeadKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create an AbyssStream over an established TcpClient.
|
||||||
|
/// Handshake: X25519 public exchange (raw) -> shared secret -> HKDF -> AEAD key + saltA + saltB
|
||||||
|
/// send/recv salts are assigned deterministically by lexicographic comparison of raw public keys.
|
||||||
|
/// </summary>
|
||||||
|
public static async Task<AbyssStream> CreateAsync(TcpClient client, byte[]? privateKeyRaw = null, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (client == null) throw new ArgumentNullException(nameof(client));
|
||||||
|
var socket = client.Client ?? throw new ArgumentException("TcpClient has no underlying socket");
|
||||||
|
|
||||||
|
// 1) Prepare local X25519 key (NSec)
|
||||||
|
Key? localKey = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (privateKeyRaw != null)
|
||||||
|
{
|
||||||
|
if (privateKeyRaw.Length != KeyAgreementAlgorithm.X25519.PrivateKeySize)
|
||||||
|
throw new ArgumentException($"privateKeyRaw must be {KeyAgreementAlgorithm.X25519.PrivateKeySize} bytes");
|
||||||
|
localKey = Key.Import(KeyAgreementAlgorithm.X25519, privateKeyRaw, KeyBlobFormat.RawPrivateKey);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var creationParams = new KeyCreationParameters { ExportPolicy = KeyExportPolicies.AllowPlaintextExport };
|
||||||
|
localKey = Key.Create(KeyAgreementAlgorithm.X25519, creationParams);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
localKey?.Dispose();
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
|
||||||
|
var localPublic = localKey.Export(KeyBlobFormat.RawPublicKey);
|
||||||
|
|
||||||
|
// 2) Exchange public keys using raw socket APIs
|
||||||
|
var remotePublic = new byte[PublicKeyLength];
|
||||||
|
|
||||||
|
var sent = 0;
|
||||||
|
while (sent < PublicKeyLength)
|
||||||
|
{
|
||||||
|
var toSend = new ReadOnlyMemory<byte>(localPublic, sent, PublicKeyLength - sent);
|
||||||
|
sent += await socket.SendAsync(toSend, SocketFlags.None, cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
await ReadExactFromSocketAsync(socket, remotePublic, 0, PublicKeyLength, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
// 3) Compute shared secret (X25519)
|
||||||
|
PublicKey remotePub;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
remotePub = PublicKey.Import(KeyAgreementAlgorithm.X25519, remotePublic, KeyBlobFormat.RawPublicKey);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
localKey.Dispose();
|
||||||
|
throw new InvalidOperationException("Failed to import remote public key", ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] aeadKey;
|
||||||
|
byte[] saltA;
|
||||||
|
byte[] saltB;
|
||||||
|
|
||||||
|
using (var shared = KeyAgreementAlgorithm.X25519.Agree(localKey, remotePub))
|
||||||
|
{
|
||||||
|
if (shared == null)
|
||||||
|
throw new InvalidOperationException("Failed to agree remote public key");
|
||||||
|
|
||||||
|
// Derive AEAD key and two independent nonce salts directly from the SharedSecret,
|
||||||
|
// using HKDF-SHA256 within NSec (no raw shared-secret export).
|
||||||
|
aeadKey = KeyDerivationAlgorithm.HkdfSha256.DeriveBytes(
|
||||||
|
shared,
|
||||||
|
salt: null,
|
||||||
|
info: System.Text.Encoding.ASCII.GetBytes("Abyss-AEAD-Key"),
|
||||||
|
count: AeadKeyLen);
|
||||||
|
|
||||||
|
saltA = KeyDerivationAlgorithm.HkdfSha256.DeriveBytes(
|
||||||
|
shared,
|
||||||
|
salt: null,
|
||||||
|
info: System.Text.Encoding.ASCII.GetBytes("Abyss-Nonce-Salt-A"),
|
||||||
|
count: NonceSaltLen);
|
||||||
|
|
||||||
|
saltB = KeyDerivationAlgorithm.HkdfSha256.DeriveBytes(
|
||||||
|
shared,
|
||||||
|
salt: null,
|
||||||
|
info: System.Text.Encoding.ASCII.GetBytes("Abyss-Nonce-Salt-B"),
|
||||||
|
count: NonceSaltLen);
|
||||||
|
}
|
||||||
|
|
||||||
|
// localKey no longer needed
|
||||||
|
localKey.Dispose();
|
||||||
|
|
||||||
|
// Deterministic assignment by lexicographic comparison of raw public keys
|
||||||
|
byte[] sendSalt, recvSalt;
|
||||||
|
int cmp = CompareByteArrayLexicographic(localPublic, remotePublic);
|
||||||
|
if (cmp < 0)
|
||||||
|
{
|
||||||
|
sendSalt = saltA;
|
||||||
|
recvSalt = saltB;
|
||||||
|
}
|
||||||
|
else if (cmp > 0)
|
||||||
|
{
|
||||||
|
sendSalt = saltB;
|
||||||
|
recvSalt = saltA;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// extremely unlikely: identical public keys; fallback
|
||||||
|
sendSalt = saltA;
|
||||||
|
recvSalt = saltB;
|
||||||
|
}
|
||||||
|
|
||||||
|
Array.Clear(localPublic, 0, localPublic.Length);
|
||||||
|
Array.Clear(remotePublic, 0, remotePublic.Length);
|
||||||
|
|
||||||
|
var abyss = new AbyssStream(socket, ownsSocket: true, aeadKey: aeadKey, sendSalt: sendSalt, recvSalt: recvSalt);
|
||||||
|
|
||||||
|
Array.Clear(aeadKey, 0, aeadKey.Length);
|
||||||
|
Array.Clear(saltA, 0, saltA.Length);
|
||||||
|
Array.Clear(saltB, 0, saltB.Length);
|
||||||
|
|
||||||
|
return abyss;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int Read(byte[] buffer, int offset, int count)
|
||||||
|
{
|
||||||
|
return ReadAsync(buffer, offset, count, CancellationToken.None).GetAwaiter().GetResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (buffer == null) throw new ArgumentNullException(nameof(buffer));
|
||||||
|
if (offset < 0 || count < 0 || offset + count > buffer.Length) throw new ArgumentOutOfRangeException();
|
||||||
|
ThrowIfDisposed();
|
||||||
|
|
||||||
|
// Serve leftover first if any (immediately return any available bytes)
|
||||||
|
if (EnsureCurrentLeftoverSegment())
|
||||||
|
{
|
||||||
|
var seg = _currentLeftoverSegment;
|
||||||
|
var avail = seg!.Length - _currentLeftoverOffset;
|
||||||
|
var toCopy = Math.Min(avail, count);
|
||||||
|
Array.Copy(seg, _currentLeftoverOffset, buffer, offset, toCopy);
|
||||||
|
_currentLeftoverOffset += toCopy;
|
||||||
|
if (_currentLeftoverOffset >= seg.Length)
|
||||||
|
{
|
||||||
|
_currentLeftoverSegment = null;
|
||||||
|
_currentLeftoverOffset = 0;
|
||||||
|
}
|
||||||
|
return toCopy;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No leftover -> read exactly one frame and decrypt
|
||||||
|
var plaintext = await ReadOneFrameAndDecryptAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
if (plaintext == null || plaintext.Length == 0)
|
||||||
|
{
|
||||||
|
// EOF
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (plaintext.Length <= count)
|
||||||
|
{
|
||||||
|
Array.Copy(plaintext, 0, buffer, offset, plaintext.Length);
|
||||||
|
return plaintext.Length;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Array.Copy(plaintext, 0, buffer, offset, count);
|
||||||
|
var leftoverLen = plaintext.Length - count;
|
||||||
|
var leftover = new byte[leftoverLen];
|
||||||
|
Array.Copy(plaintext, count, leftover, 0, leftoverLen);
|
||||||
|
_leftoverQueue.Enqueue(leftover);
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<byte[]?> ReadOneFrameAndDecryptAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var header = new byte[4];
|
||||||
|
await ReadExactFromBaseAsync(header, 0, 4, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
var payloadLen = (int)BinaryPrimitives.ReadUInt32BigEndian(header);
|
||||||
|
if (payloadLen > MaxPlaintextFrame) throw new InvalidDataException("payload too big");
|
||||||
|
if (payloadLen < AeadTagLen) throw new InvalidDataException("payload too small");
|
||||||
|
|
||||||
|
var payload = new byte[payloadLen];
|
||||||
|
await ReadExactFromBaseAsync(payload, 0, payloadLen, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
var ciphertextLen = payloadLen - AeadTagLen;
|
||||||
|
var ciphertext = new byte[ciphertextLen];
|
||||||
|
var tag = new byte[AeadTagLen];
|
||||||
|
if (ciphertextLen > 0) Array.Copy(payload, 0, ciphertext, 0, ciphertextLen);
|
||||||
|
Array.Copy(payload, ciphertextLen, tag, 0, AeadTagLen);
|
||||||
|
|
||||||
|
// compute remote nonce using recv counter (no role bit)
|
||||||
|
ulong remoteCounterValue = _recvCounter;
|
||||||
|
_recvCounter++;
|
||||||
|
|
||||||
|
var nonce = new byte[NonceLen];
|
||||||
|
Array.Copy(_recvNonceSalt, 0, nonce, 0, NonceSaltLen);
|
||||||
|
BinaryPrimitives.WriteUInt64BigEndian(nonce.AsSpan(NonceSaltLen), remoteCounterValue);
|
||||||
|
|
||||||
|
var plaintext = new byte[ciphertextLen];
|
||||||
|
try
|
||||||
|
{
|
||||||
|
lock (_aeadLock)
|
||||||
|
{
|
||||||
|
_aead.Decrypt(nonce, ciphertext, tag, plaintext);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (CryptographicException)
|
||||||
|
{
|
||||||
|
Dispose();
|
||||||
|
throw new CryptographicException("AEAD authentication failed; connection closed.");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Array.Clear(nonce, 0, nonce.Length);
|
||||||
|
Array.Clear(payload, 0, payload.Length);
|
||||||
|
Array.Clear(ciphertext, 0, ciphertext.Length);
|
||||||
|
Array.Clear(tag, 0, tag.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
return plaintext;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ReadExactFromBaseAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (count == 0) return;
|
||||||
|
var read = 0;
|
||||||
|
while (read < count)
|
||||||
|
{
|
||||||
|
var n = await base.ReadAsync(buffer, offset + read, count - read, cancellationToken).ConfigureAwait(false);
|
||||||
|
if (n == 0)
|
||||||
|
{
|
||||||
|
if (read == 0)
|
||||||
|
throw new EndOfStreamException("Remote closed connection while reading.");
|
||||||
|
throw new EndOfStreamException("Remote closed connection unexpectedly during read.");
|
||||||
|
}
|
||||||
|
read += n;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task ReadExactFromSocketAsync(Socket socket, byte[] buffer, int offset, int count, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (count == 0) return;
|
||||||
|
var received = 0;
|
||||||
|
while (received < count)
|
||||||
|
{
|
||||||
|
var mem = new Memory<byte>(buffer, offset + received, count - received);
|
||||||
|
var r = await socket.ReceiveAsync(mem, SocketFlags.None, cancellationToken).ConfigureAwait(false);
|
||||||
|
if (r == 0)
|
||||||
|
{
|
||||||
|
if (received == 0)
|
||||||
|
throw new EndOfStreamException("Remote closed connection while reading from socket.");
|
||||||
|
throw new EndOfStreamException("Remote closed connection unexpectedly during socket read.");
|
||||||
|
}
|
||||||
|
received += r;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int CompareByteArrayLexicographic(byte[] a, byte[] b)
|
||||||
|
{
|
||||||
|
if (a == null || b == null) throw new ArgumentNullException();
|
||||||
|
var min = Math.Min(a.Length, b.Length);
|
||||||
|
for (int i = 0; i < min; i++)
|
||||||
|
{
|
||||||
|
if (a[i] < b[i]) return -1;
|
||||||
|
if (a[i] > b[i]) return 1;
|
||||||
|
}
|
||||||
|
if (a.Length < b.Length) return -1;
|
||||||
|
if (a.Length > b.Length) return 1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool EnsureCurrentLeftoverSegment()
|
||||||
|
{
|
||||||
|
if (_currentLeftoverSegment != null && _currentLeftoverOffset < _currentLeftoverSegment.Length) return true;
|
||||||
|
if (_leftoverQueue.TryDequeue(out var next))
|
||||||
|
{
|
||||||
|
_currentLeftoverSegment = next;
|
||||||
|
_currentLeftoverOffset = 0;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Write(byte[] buffer, int offset, int count)
|
||||||
|
{
|
||||||
|
if (buffer == null) throw new ArgumentNullException(nameof(buffer));
|
||||||
|
if (offset < 0 || count < 0 || offset + count > buffer.Length) throw new ArgumentOutOfRangeException();
|
||||||
|
ThrowIfDisposed();
|
||||||
|
WriteAsync(buffer, offset, count, CancellationToken.None).GetAwaiter().GetResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (buffer == null) throw new ArgumentNullException(nameof(buffer));
|
||||||
|
if (offset < 0 || count < 0 || offset + count > buffer.Length) throw new ArgumentOutOfRangeException();
|
||||||
|
ThrowIfDisposed();
|
||||||
|
|
||||||
|
int remaining = count;
|
||||||
|
int idx = offset;
|
||||||
|
|
||||||
|
while (remaining > 0)
|
||||||
|
{
|
||||||
|
var chunk = Math.Min(remaining, MaxPlaintextFrame);
|
||||||
|
var mem = new ReadOnlyMemory<byte>(buffer, idx, chunk);
|
||||||
|
await SendPlaintextChunkAsync(mem, cancellationToken).ConfigureAwait(false);
|
||||||
|
idx += chunk;
|
||||||
|
remaining -= chunk;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Task FlushAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||||
|
|
||||||
|
private async Task SendPlaintextChunkAsync(ReadOnlyMemory<byte> plaintext, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
ThrowIfDisposed();
|
||||||
|
|
||||||
|
var ciphertext = new byte[plaintext.Length];
|
||||||
|
var tag = new byte[AeadTagLen];
|
||||||
|
var nonce = new byte[NonceLen];
|
||||||
|
ulong counterValue;
|
||||||
|
|
||||||
|
lock (_sendLock)
|
||||||
|
{
|
||||||
|
counterValue = _sendCounter;
|
||||||
|
_sendCounter++;
|
||||||
|
}
|
||||||
|
|
||||||
|
Array.Copy(_sendNonceSalt, 0, nonce, 0, NonceSaltLen);
|
||||||
|
BinaryPrimitives.WriteUInt64BigEndian(nonce.AsSpan(NonceSaltLen), counterValue);
|
||||||
|
|
||||||
|
lock (_aeadLock)
|
||||||
|
{
|
||||||
|
_aead.Encrypt(nonce, plaintext.Span, ciphertext, tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
var payloadLen = unchecked((uint)(ciphertext.Length + tag.Length));
|
||||||
|
var header = new byte[4];
|
||||||
|
BinaryPrimitives.WriteUInt32BigEndian(header, payloadLen);
|
||||||
|
|
||||||
|
await base.WriteAsync(header, 0, header.Length, cancellationToken).ConfigureAwait(false);
|
||||||
|
if (ciphertext.Length > 0)
|
||||||
|
await base.WriteAsync(ciphertext, 0, ciphertext.Length, cancellationToken).ConfigureAwait(false);
|
||||||
|
await base.WriteAsync(tag, 0, tag.Length, cancellationToken).ConfigureAwait(false);
|
||||||
|
await base.FlushAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
Array.Clear(nonce, 0, nonce.Length);
|
||||||
|
Array.Clear(tag, 0, tag.Length);
|
||||||
|
Array.Clear(ciphertext, 0, ciphertext.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
if (!_disposed)
|
||||||
|
{
|
||||||
|
if (disposing)
|
||||||
|
{
|
||||||
|
lock (_aeadLock)
|
||||||
|
{
|
||||||
|
_aead.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
while (_leftoverQueue.TryDequeue(out var seg)) Array.Clear(seg, 0, seg.Length);
|
||||||
|
}
|
||||||
|
_disposed = true;
|
||||||
|
}
|
||||||
|
base.Dispose(disposing);
|
||||||
|
}
|
||||||
|
|
||||||
|
void IDisposable.Dispose() => Dispose();
|
||||||
|
|
||||||
|
private void ThrowIfDisposed()
|
||||||
|
{
|
||||||
|
if (_disposed) throw new ObjectDisposedException(nameof(AbyssStream));
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Write(ReadOnlySpan<byte> buffer)
|
||||||
|
{
|
||||||
|
var tmp = ArrayPool<byte>.Shared.Rent(buffer.Length);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
buffer.CopyTo(tmp);
|
||||||
|
Write(tmp, 0, buffer.Length);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
ArrayPool<byte>.Shared.Return(tmp, clearArray: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (MemoryMarshal.TryGetArray(buffer, out ArraySegment<byte> seg))
|
||||||
|
{
|
||||||
|
return new ValueTask(WriteAsync(seg.Array!, seg.Offset, seg.Count, cancellationToken));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return SlowWriteAsync(buffer, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
async ValueTask SlowWriteAsync(ReadOnlyMemory<byte> buf, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var tmp = ArrayPool<byte>.Shared.Rent(buf.Length);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
buf.Span.CopyTo(tmp);
|
||||||
|
await WriteAsync(tmp, 0, buf.Length, ct).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
ArrayPool<byte>.Shared.Return(tmp, clearArray: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int Read(Span<byte> buffer)
|
||||||
|
{
|
||||||
|
var tmp = ArrayPool<byte>.Shared.Rent(buffer.Length);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
int n = Read(tmp, 0, buffer.Length);
|
||||||
|
new ReadOnlySpan<byte>(tmp, 0, n).CopyTo(buffer);
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
ArrayPool<byte>.Shared.Return(tmp, clearArray: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (MemoryMarshal.TryGetArray(buffer, out ArraySegment<byte> seg))
|
||||||
|
{
|
||||||
|
return new ValueTask<int>(ReadAsync(seg.Array!, seg.Offset, seg.Count, cancellationToken));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return SlowReadAsync(buffer, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
async ValueTask<int> SlowReadAsync(Memory<byte> buf, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var tmp = ArrayPool<byte>.Shared.Rent(buf.Length);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
int n = await ReadAsync(tmp, 0, buf.Length, ct).ConfigureAwait(false);
|
||||||
|
new ReadOnlySpan<byte>(tmp, 0, n).CopyTo(buf.Span);
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
ArrayPool<byte>.Shared.Return(tmp, clearArray: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class TcpClientAbyssExtensions
|
||||||
|
{
|
||||||
|
public static Task<AbyssStream> GetAbyssStreamAsync(this TcpClient client, byte[]? privateKeyRaw = null, CancellationToken ct = default)
|
||||||
|
=> AbyssStream.CreateAsync(client, privateKeyRaw, ct);
|
||||||
|
}
|
||||||
|
}
|
||||||
208
Abyss/Components/Tools/HttpHelper.cs
Normal file
208
Abyss/Components/Tools/HttpHelper.cs
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Abyss.Components.Tools;
|
||||||
|
|
||||||
|
public class HttpHelper
|
||||||
|
{
|
||||||
|
private const int MaxHeaderCount = 100;
|
||||||
|
private const int MaxHeaderLineLength = 8192;
|
||||||
|
private const int MaxBodySize = 10 * 1024 * 1024; // 10 MB
|
||||||
|
|
||||||
|
public static string BuildHttpResponse(
|
||||||
|
int statusCode,
|
||||||
|
string statusDescription,
|
||||||
|
Dictionary<string, string>? headers = null,
|
||||||
|
string? body = null,
|
||||||
|
string httpVersion = "HTTP/1.1")
|
||||||
|
{
|
||||||
|
var responseBuilder = new StringBuilder();
|
||||||
|
|
||||||
|
// Sanitize status description (prevent CRLF injection)
|
||||||
|
statusDescription = SanitizeHeaderValue(statusDescription);
|
||||||
|
|
||||||
|
responseBuilder.Append($"{httpVersion} {statusCode} {statusDescription}\r\n");
|
||||||
|
|
||||||
|
headers ??= new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
// Ensure correct Content-Length
|
||||||
|
if (!string.IsNullOrEmpty(body))
|
||||||
|
{
|
||||||
|
int contentLength = Encoding.UTF8.GetByteCount(body);
|
||||||
|
headers["Content-Length"] = contentLength.ToString();
|
||||||
|
if (!headers.ContainsKey("Content-Type"))
|
||||||
|
{
|
||||||
|
headers["Content-Type"] = "text/plain; charset=utf-8";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var header in headers)
|
||||||
|
{
|
||||||
|
string name = SanitizeHeaderName(header.Key);
|
||||||
|
string value = SanitizeHeaderValue(header.Value);
|
||||||
|
responseBuilder.AppendLine($"{name}: {value}");
|
||||||
|
}
|
||||||
|
|
||||||
|
responseBuilder.AppendLine();
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(body))
|
||||||
|
{
|
||||||
|
responseBuilder.Append(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
return responseBuilder.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static HttpRequest Parse(string requestText)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(requestText))
|
||||||
|
throw new ArgumentException("Request text cannot be empty");
|
||||||
|
|
||||||
|
using var reader = new StringReader(requestText);
|
||||||
|
var request = new HttpRequest();
|
||||||
|
|
||||||
|
string requestLine = reader.ReadLine() ?? "";
|
||||||
|
if (string.IsNullOrWhiteSpace(requestLine))
|
||||||
|
throw new FormatException("Invalid HTTP request: missing request line");
|
||||||
|
|
||||||
|
ParseRequestLine(requestLine, request);
|
||||||
|
ParseHeaders(reader, request);
|
||||||
|
ParseBody(reader, request);
|
||||||
|
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ParseRequestLine(string requestLine, HttpRequest request)
|
||||||
|
{
|
||||||
|
var parts = requestLine.Split(' ', 3);
|
||||||
|
if (parts.Length < 3)
|
||||||
|
throw new FormatException("Invalid request line format");
|
||||||
|
|
||||||
|
request.Method = parts[0].Trim();
|
||||||
|
|
||||||
|
if (!Uri.TryCreate(parts[1], UriKind.RelativeOrAbsolute, out var uri))
|
||||||
|
{
|
||||||
|
throw new FormatException("Invalid or unsupported URI");
|
||||||
|
}
|
||||||
|
request.RequestUri = uri;
|
||||||
|
|
||||||
|
request.HttpVersion = parts[2].Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ParseHeaders(StringReader reader, HttpRequest request)
|
||||||
|
{
|
||||||
|
string? line;
|
||||||
|
int headerCount = 0;
|
||||||
|
|
||||||
|
while (!string.IsNullOrEmpty(line = reader.ReadLine()))
|
||||||
|
{
|
||||||
|
if (++headerCount > MaxHeaderCount)
|
||||||
|
throw new InvalidOperationException("Too many headers");
|
||||||
|
|
||||||
|
if (line.Length > MaxHeaderLineLength)
|
||||||
|
throw new InvalidOperationException("Header line too long");
|
||||||
|
|
||||||
|
int colonIndex = line.IndexOf(':');
|
||||||
|
if (colonIndex <= 0)
|
||||||
|
throw new FormatException($"Invalid header format: {line}");
|
||||||
|
|
||||||
|
string headerName = SanitizeHeaderName(line.Substring(0, colonIndex).Trim());
|
||||||
|
string headerValue = SanitizeHeaderValue(line.Substring(colonIndex + 1).Trim());
|
||||||
|
|
||||||
|
if (request.Headers.ContainsKey(headerName))
|
||||||
|
throw new InvalidOperationException($"Duplicate header not allowed: {headerName}");
|
||||||
|
|
||||||
|
request.Headers[headerName] = headerValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ParseBody(StringReader reader, HttpRequest request)
|
||||||
|
{
|
||||||
|
if (request.Headers.TryGetValue("Content-Length", out var contentLengthStr) &&
|
||||||
|
long.TryParse(contentLengthStr, out var contentLength) &&
|
||||||
|
contentLength > 0)
|
||||||
|
{
|
||||||
|
if (contentLength > MaxBodySize)
|
||||||
|
throw new InvalidOperationException("Request body too large");
|
||||||
|
|
||||||
|
var buffer = new char[contentLength];
|
||||||
|
int read = reader.ReadBlock(buffer, 0, (int)contentLength);
|
||||||
|
request.Body = new string(buffer, 0, read);
|
||||||
|
}
|
||||||
|
else if (request.Headers.TryGetValue("Transfer-Encoding", out var encoding) &&
|
||||||
|
encoding.Equals("chunked", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
throw new NotSupportedException("Chunked transfer encoding is not supported");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string SanitizeHeaderName(string name)
|
||||||
|
{
|
||||||
|
if (name.Contains("\r") || name.Contains("\n"))
|
||||||
|
throw new FormatException("Invalid header name");
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string SanitizeHeaderValue(string value)
|
||||||
|
{
|
||||||
|
return value.Replace("\r", "").Replace("\n", "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class HttpRequest
|
||||||
|
{
|
||||||
|
public string Method { get; set; } = "";
|
||||||
|
public Uri? RequestUri { get; set; }
|
||||||
|
public string HttpVersion { get; set; } = "";
|
||||||
|
public Dictionary<string, string> Headers { get; set; }
|
||||||
|
public string Body { get; set; } = "";
|
||||||
|
|
||||||
|
public HttpRequest()
|
||||||
|
{
|
||||||
|
Headers = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get header value by name (case-insensitive)
|
||||||
|
/// </summary>
|
||||||
|
public string? GetHeader(string headerName)
|
||||||
|
{
|
||||||
|
return Headers.TryGetValue(headerName, out var value) ? value : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Check if header exists (case-insensitive)
|
||||||
|
/// </summary>
|
||||||
|
public bool HasHeader(string headerName)
|
||||||
|
{
|
||||||
|
return Headers.ContainsKey(headerName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Convert back to HTTP request string
|
||||||
|
/// </summary>
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
var builder = new StringBuilder();
|
||||||
|
|
||||||
|
// Request line
|
||||||
|
builder.AppendLine($"{Method} {RequestUri} {HttpVersion}");
|
||||||
|
|
||||||
|
// Headers
|
||||||
|
foreach (var header in Headers)
|
||||||
|
{
|
||||||
|
builder.AppendLine($"{header.Key}: {header.Value}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Empty line
|
||||||
|
builder.AppendLine();
|
||||||
|
|
||||||
|
// Body
|
||||||
|
if (!string.IsNullOrEmpty(Body))
|
||||||
|
{
|
||||||
|
builder.Append(Body);
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
277
Abyss/Components/Tools/HttpReader.cs
Normal file
277
Abyss/Components/Tools/HttpReader.cs
Normal file
@@ -0,0 +1,277 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Net.Sockets;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Globalization;
|
||||||
|
|
||||||
|
namespace Abyss.Components.Tools
|
||||||
|
{
|
||||||
|
public static class HttpReader
|
||||||
|
{
|
||||||
|
private const int DefaultBufferSize = 8192;
|
||||||
|
private const int MaxHeaderBytes = 64 * 1024; // 64 KB header max
|
||||||
|
private const long MaxBodyBytes = 10L * 1024 * 1024; // 10 MB body max
|
||||||
|
private const int MaxLineLength = 8 * 1024; // 8 KB per line max
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Read a full HTTP message (headers + body) from a NetworkStream and return as a string.
|
||||||
|
/// This method enforces size limits and parses chunked encoding correctly.
|
||||||
|
/// </summary>
|
||||||
|
public static async Task<string> ReadHttpMessageAsync(AbyssStream stream, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (stream == null) throw new ArgumentNullException(nameof(stream));
|
||||||
|
if (!stream.CanRead) throw new ArgumentException("Stream is not readable", nameof(stream));
|
||||||
|
|
||||||
|
// 1) Read header bytes until CRLFCRLF or header size limit is exceeded
|
||||||
|
var headerBuffer = new MemoryStream();
|
||||||
|
var tmp = new byte[DefaultBufferSize];
|
||||||
|
int headerEndIndex = -1;
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
int n = await stream.ReadAsync(tmp.AsMemory(0, tmp.Length), cancellationToken).ConfigureAwait(false);
|
||||||
|
if (n == 0)
|
||||||
|
throw new IOException("Stream closed before HTTP header was fully read.");
|
||||||
|
|
||||||
|
headerBuffer.Write(tmp, 0, n);
|
||||||
|
|
||||||
|
if (headerBuffer.Length > MaxHeaderBytes)
|
||||||
|
throw new InvalidOperationException("HTTP header exceeds maximum allowed size.");
|
||||||
|
|
||||||
|
// search for CRLFCRLF in the accumulated bytes
|
||||||
|
var bytes = headerBuffer.ToArray();
|
||||||
|
headerEndIndex = IndexOfDoubleCrlf(bytes);
|
||||||
|
if (headerEndIndex >= 0)
|
||||||
|
{
|
||||||
|
// headerEndIndex is the index of the first '\r' of "\r\n\r\n"
|
||||||
|
// stop reading further here; remaining bytes (if any) are part of body initial chunk
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// continue reading
|
||||||
|
}
|
||||||
|
|
||||||
|
var allHeaderBytes = headerBuffer.ToArray();
|
||||||
|
int bodyStartIndex = headerEndIndex + 4;
|
||||||
|
string headerPart = Encoding.ASCII.GetString(allHeaderBytes, 0, headerEndIndex + 4);
|
||||||
|
|
||||||
|
// 2) parse headers to find Content-Length / Transfer-Encoding
|
||||||
|
int contentLength = 0;
|
||||||
|
bool isChunked = false;
|
||||||
|
|
||||||
|
foreach (var line in headerPart.Split(new[] { "\r\n" }, StringSplitOptions.RemoveEmptyEntries))
|
||||||
|
{
|
||||||
|
if (line.StartsWith("Content-Length:", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
var raw = line.Substring("Content-Length:".Length).Trim();
|
||||||
|
if (int.TryParse(raw, NumberStyles.None, CultureInfo.InvariantCulture, out int len))
|
||||||
|
{
|
||||||
|
if (len < 0) throw new FormatException("Negative Content-Length not allowed.");
|
||||||
|
contentLength = len;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new FormatException("Invalid Content-Length value.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (line.StartsWith("Transfer-Encoding:", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
if (line.IndexOf("chunked", StringComparison.OrdinalIgnoreCase) >= 0)
|
||||||
|
{
|
||||||
|
isChunked = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3) Create a buffered reader that first consumes bytes already read after header
|
||||||
|
var initialTail = new ArraySegment<byte>(allHeaderBytes, bodyStartIndex, allHeaderBytes.Length - bodyStartIndex);
|
||||||
|
var reader = new BufferedNetworkReader(stream, initialTail, DefaultBufferSize, cancellationToken);
|
||||||
|
|
||||||
|
// 4) Read body according to encoding
|
||||||
|
byte[] bodyBytes;
|
||||||
|
if (isChunked)
|
||||||
|
{
|
||||||
|
using var bodyMs = new MemoryStream();
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
string sizeLine = await reader.ReadLineAsync(MaxLineLength).ConfigureAwait(false);
|
||||||
|
if (string.IsNullOrWhiteSpace(sizeLine))
|
||||||
|
{
|
||||||
|
// skip empty lines (robustness)
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// chunk-size [; extensions]
|
||||||
|
var semi = sizeLine.IndexOf(';');
|
||||||
|
var sizeToken = semi >= 0 ? sizeLine.Substring(0, semi) : sizeLine;
|
||||||
|
if (!long.TryParse(sizeToken.Trim(), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out long chunkSize))
|
||||||
|
throw new IOException("Invalid chunk size in chunked encoding.");
|
||||||
|
|
||||||
|
if (chunkSize < 0) throw new IOException("Negative chunk size.");
|
||||||
|
|
||||||
|
if (chunkSize == 0)
|
||||||
|
{
|
||||||
|
// read and discard any trailer headers until an empty line
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
var trailerLine = await reader.ReadLineAsync(MaxLineLength).ConfigureAwait(false);
|
||||||
|
if (string.IsNullOrEmpty(trailerLine)) break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chunkSize > MaxBodyBytes || (bodyMs.Length + chunkSize) > MaxBodyBytes)
|
||||||
|
throw new InvalidOperationException("Chunked body exceeds maximum allowed size.");
|
||||||
|
|
||||||
|
await reader.ReadExactAsync(bodyMs, chunkSize).ConfigureAwait(false);
|
||||||
|
|
||||||
|
// after chunk data there must be CRLF; consume it
|
||||||
|
var crlf = await reader.ReadLineAsync(MaxLineLength).ConfigureAwait(false);
|
||||||
|
if (crlf != string.Empty)
|
||||||
|
throw new IOException("Missing CRLF after chunk data.");
|
||||||
|
}
|
||||||
|
|
||||||
|
bodyBytes = bodyMs.ToArray();
|
||||||
|
}
|
||||||
|
else if (contentLength > 0)
|
||||||
|
{
|
||||||
|
if (contentLength > MaxBodyBytes)
|
||||||
|
throw new InvalidOperationException("Content-Length exceeds maximum allowed size.");
|
||||||
|
|
||||||
|
using var bodyMs = new MemoryStream();
|
||||||
|
long remaining = contentLength;
|
||||||
|
// If there were initial tail bytes, BufferedNetworkReader will supply them first
|
||||||
|
await reader.ReadExactAsync(bodyMs, remaining).ConfigureAwait(false);
|
||||||
|
bodyBytes = bodyMs.ToArray();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// no body
|
||||||
|
bodyBytes = Array.Empty<byte>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5) combine headerPart and body decoded as UTF-8 string
|
||||||
|
string bodyPart = Encoding.UTF8.GetString(bodyBytes);
|
||||||
|
return headerPart + bodyPart;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int IndexOfDoubleCrlf(byte[] data)
|
||||||
|
{
|
||||||
|
// find sequence \r\n\r\n
|
||||||
|
for (int i = 0; i + 3 < data.Length; i++)
|
||||||
|
{
|
||||||
|
if (data[i] == 13 && data[i + 1] == 10 && data[i + 2] == 13 && data[i + 3] == 10)
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// BufferedNetworkReader merges an initial buffer (already-read bytes) with later reads from NetworkStream.
|
||||||
|
/// It provides ReadLineAsync and ReadExactAsync semantics used by HTTP parsing.
|
||||||
|
/// </summary>
|
||||||
|
private sealed class BufferedNetworkReader
|
||||||
|
{
|
||||||
|
private readonly AbyssStream _stream;
|
||||||
|
private readonly CancellationToken _cancellation;
|
||||||
|
private readonly int _bufferSize;
|
||||||
|
private byte[] _buffer;
|
||||||
|
private int _offset;
|
||||||
|
private int _count; // valid data range [_offset, _offset + _count)
|
||||||
|
|
||||||
|
public BufferedNetworkReader(AbyssStream stream, ArraySegment<byte> initial, int bufferSize, CancellationToken cancellation)
|
||||||
|
{
|
||||||
|
_stream = stream ?? throw new ArgumentNullException(nameof(stream));
|
||||||
|
_cancellation = cancellation;
|
||||||
|
_bufferSize = Math.Max(512, bufferSize);
|
||||||
|
// initialize buffer and copy initial tail bytes
|
||||||
|
_buffer = new byte[Math.Max(_bufferSize, initial.Count)];
|
||||||
|
Array.Copy(initial.Array ?? Array.Empty<byte>(), initial.Offset, _buffer, 0, initial.Count);
|
||||||
|
_offset = 0;
|
||||||
|
_count = initial.Count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Read a line terminated by CRLF. Returns the line without CRLF.
|
||||||
|
/// Throws if the line length exceeds maxLineLength.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<string> ReadLineAsync(int maxLineLength)
|
||||||
|
{
|
||||||
|
var ms = new MemoryStream();
|
||||||
|
int seen = 0;
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
if (_count == 0)
|
||||||
|
{
|
||||||
|
// refill buffer
|
||||||
|
int n = await _stream.ReadAsync(new Memory<byte>(_buffer, 0, _buffer.Length), _cancellation).ConfigureAwait(false);
|
||||||
|
if (n == 0)
|
||||||
|
throw new IOException("Unexpected end of stream while reading line.");
|
||||||
|
_offset = 0;
|
||||||
|
_count = n;
|
||||||
|
}
|
||||||
|
|
||||||
|
// scan for '\n'
|
||||||
|
int i;
|
||||||
|
for (i = 0; i < _count; i++)
|
||||||
|
{
|
||||||
|
byte b = _buffer[_offset + i];
|
||||||
|
seen++;
|
||||||
|
if (seen > maxLineLength) throw new InvalidOperationException("Line length exceeds maximum allowed.");
|
||||||
|
if (b == (byte)'\n')
|
||||||
|
{
|
||||||
|
// write bytes up to this position
|
||||||
|
ms.Write(_buffer, _offset, i + 1);
|
||||||
|
_offset += i + 1;
|
||||||
|
_count -= i + 1;
|
||||||
|
// convert to string and remove CRLF if present
|
||||||
|
var lineBytes = ms.ToArray();
|
||||||
|
if (lineBytes.Length >= 2 && lineBytes[lineBytes.Length - 2] == (byte)'\r')
|
||||||
|
return Encoding.ASCII.GetString(lineBytes, 0, lineBytes.Length - 2);
|
||||||
|
else if (lineBytes.Length >= 1 && lineBytes[lineBytes.Length - 1] == (byte)'\n')
|
||||||
|
return Encoding.ASCII.GetString(lineBytes, 0, lineBytes.Length - 1);
|
||||||
|
else
|
||||||
|
return Encoding.ASCII.GetString(lineBytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// no newline found in buffer; write all and continue
|
||||||
|
ms.Write(_buffer, _offset, _count);
|
||||||
|
_offset = 0;
|
||||||
|
_count = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Read exactly 'length' bytes and write them to destination stream.
|
||||||
|
/// Throws if stream ends before length bytes are read or size exceeds limits.
|
||||||
|
/// </summary>
|
||||||
|
public async Task ReadExactAsync(Stream destination, long length)
|
||||||
|
{
|
||||||
|
if (length < 0) throw new ArgumentOutOfRangeException(nameof(length));
|
||||||
|
long remaining = length;
|
||||||
|
var tmp = new byte[_bufferSize];
|
||||||
|
|
||||||
|
// first consume from internal buffer
|
||||||
|
if (_count > 0)
|
||||||
|
{
|
||||||
|
int take = (int)Math.Min(_count, remaining);
|
||||||
|
destination.Write(_buffer, _offset, take);
|
||||||
|
_offset += take;
|
||||||
|
_count -= take;
|
||||||
|
remaining -= take;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (remaining > 0)
|
||||||
|
{
|
||||||
|
int toRead = (int)Math.Min(tmp.Length, remaining);
|
||||||
|
int n = await _stream.ReadAsync(tmp.AsMemory(0, toRead), _cancellation).ConfigureAwait(false);
|
||||||
|
if (n == 0) throw new IOException("Unexpected end of stream while reading body.");
|
||||||
|
destination.Write(tmp, 0, n);
|
||||||
|
remaining -= n;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,13 +13,13 @@ public class Program
|
|||||||
|
|
||||||
builder.Services.AddAuthorization();
|
builder.Services.AddAuthorization();
|
||||||
builder.Services.AddMemoryCache();
|
builder.Services.AddMemoryCache();
|
||||||
builder.Services.AddOpenApi();
|
|
||||||
builder.Services.AddControllers();
|
builder.Services.AddControllers();
|
||||||
builder.Services.AddSingleton<ConfigureService>();
|
builder.Services.AddSingleton<ConfigureService>();
|
||||||
builder.Services.AddSingleton<UserService>();
|
builder.Services.AddSingleton<UserService>();
|
||||||
builder.Services.AddSingleton<ResourceService>();
|
builder.Services.AddSingleton<ResourceService>();
|
||||||
builder.Services.AddSingleton<TaskController>();
|
builder.Services.AddSingleton<TaskController>();
|
||||||
builder.Services.AddSingleton<TaskService>();
|
builder.Services.AddSingleton<TaskService>();
|
||||||
|
builder.Services.AddHostedService<AbyssService>();
|
||||||
|
|
||||||
builder.Services.AddRateLimiter(options =>
|
builder.Services.AddRateLimiter(options =>
|
||||||
{
|
{
|
||||||
@@ -42,14 +42,8 @@ public class Program
|
|||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
if (app.Environment.IsDevelopment())
|
|
||||||
{
|
|
||||||
app.MapOpenApi();
|
|
||||||
}
|
|
||||||
|
|
||||||
// app.UseHttpsRedirection();
|
// app.UseHttpsRedirection();
|
||||||
app.UseAuthorization();
|
app.UseAuthorization();
|
||||||
app.MapStaticAssets();
|
|
||||||
app.MapControllers();
|
app.MapControllers();
|
||||||
|
|
||||||
app.UseRateLimiter();
|
app.UseRateLimiter();
|
||||||
|
|||||||
@@ -5,21 +5,11 @@
|
|||||||
"commandName": "Project",
|
"commandName": "Project",
|
||||||
"dotnetRunMessages": true,
|
"dotnetRunMessages": true,
|
||||||
"launchBrowser": false,
|
"launchBrowser": false,
|
||||||
"applicationUrl": "http://192.168.1.244:5198",
|
"applicationUrl": "http://localhost:3000",
|
||||||
"environmentVariables": {
|
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
|
||||||
"MEDIA_ROOT" : "/storage"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"https": {
|
|
||||||
"commandName": "Project",
|
|
||||||
"dotnetRunMessages": true,
|
|
||||||
"launchBrowser": false,
|
|
||||||
"applicationUrl": "https://localhost:7013;http://localhost:5198",
|
|
||||||
"environmentVariables": {
|
"environmentVariables": {
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||||
"MEDIA_ROOT" : "/storage",
|
"MEDIA_ROOT" : "/storage",
|
||||||
"DEBUG_MODE" : "Debug"
|
"ALLOWED_PORTS" : "3000"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user