├── .editorconfig ├── .github ├── dependabot.yml └── workflows │ ├── iPanel-build.yml │ └── iPanel.Tests-run-tests.yml ├── .gitignore ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── .wakatime-project ├── LICENSE ├── README.md ├── iPanel.Tests ├── Api │ ├── CommonTests.cs │ ├── LoginTests.cs │ ├── LogoutTests.cs │ └── UserManagementTests.cs ├── InstanceTests.cs ├── ParseInputTests.cs ├── SettingTests.cs ├── Utils.cs └── iPanel.Tests.csproj ├── iPanel.code-workspace ├── iPanel.sln └── iPanel ├── App.cs ├── AppBuilder.cs ├── Constant.cs ├── Core ├── Interaction │ ├── Command.cs │ ├── CommandDescriptionAttribute.cs │ ├── CommandUsageAttribute.cs │ ├── Commands │ │ ├── ClearScreenCommand.cs │ │ ├── ExitCommand.cs │ │ ├── ListConnectionCommand.cs │ │ ├── UserCommand.cs │ │ └── VersionCommand.cs │ └── InputReader.cs ├── Models │ ├── Client │ │ ├── Client.cs │ │ ├── ConsoleListener.cs │ │ ├── Info │ │ │ ├── InstanceInfo.cs │ │ │ ├── ServerInfo.cs │ │ │ └── SystemInfo.cs │ │ ├── Instance.cs │ │ └── Metadata.cs │ ├── Exceptions │ │ ├── PacketException.cs │ │ └── SettingsException.cs │ ├── Packets │ │ ├── ApiPacket.cs │ │ ├── Datas │ │ │ ├── DirInfo.cs │ │ │ ├── Result.cs │ │ │ ├── Status.cs │ │ │ └── VerifyBody.cs │ │ ├── Event │ │ │ ├── InvalidDataPacket.cs │ │ │ ├── InvalidParamPacket.cs │ │ │ ├── ResultTypes.cs │ │ │ └── VerifyResultPacket.cs │ │ ├── Sender.cs │ │ ├── WsPacket.cs │ │ ├── WsReceivedPacket.cs │ │ └── WsSentPacket.cs │ ├── Request.cs │ ├── Settings │ │ ├── CertificateSettings.cs │ │ ├── Setting.cs │ │ └── WebServerSetting.cs │ └── Users │ │ ├── PermissionLevel.cs │ │ ├── User.cs │ │ └── UserWithoutPwd.cs ├── Server │ ├── Api │ │ ├── ApiHelper.cs │ │ ├── ApiMap.Instances.cs │ │ ├── ApiMap.Main.cs │ │ └── ApiMap.Users.cs │ ├── HttpServer.cs │ ├── IPBannerModule.cs │ ├── SessionKeyConstants.cs │ └── WebSocket │ │ ├── BroadcastWsModule.cs │ │ ├── DebugWsModule.cs │ │ ├── Handlers │ │ ├── BroadcastHandler.cs │ │ ├── HandlerAttribute.cs │ │ ├── HandlerBase.cs │ │ ├── RequestsFactory.cs │ │ └── ReturnHandler.cs │ │ └── InstanceWsModule.cs └── Service │ └── UserManager.cs ├── Program.cs ├── Sources ├── logo.ico └── logo.png ├── Utils ├── AssemblyInfo.cs ├── CommandLineHelper.cs ├── EncodingsMap.cs ├── Encryption.cs ├── Extensions │ └── WebSocketExtension.cs ├── Json │ ├── JsonNodeExtension.cs │ └── JsonSerializerOptionsFactory.cs ├── ResourceFileManager.cs ├── SimpleLogger.cs └── SimpleLoggerProvider.cs └── iPanel.csproj /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | # All files 4 | [*] 5 | indent_style = space 6 | 7 | # Xml files 8 | [*.xml] 9 | indent_size = 2 10 | 11 | # C# files 12 | [*.cs] 13 | 14 | #### Core EditorConfig Options #### 15 | 16 | # Indentation and spacing 17 | indent_size = 4 18 | tab_width = 4 19 | 20 | # New line preferences 21 | end_of_line = crlf 22 | insert_final_newline = true 23 | 24 | #### .NET Coding Conventions #### 25 | [*.{cs,vb}] 26 | 27 | # Organize usings 28 | dotnet_separate_import_directive_groups = true 29 | dotnet_sort_system_directives_first = true 30 | file_header_template = unset 31 | 32 | # this. and Me. preferences 33 | dotnet_style_qualification_for_event = false:silent 34 | dotnet_style_qualification_for_field = false:silent 35 | dotnet_style_qualification_for_method = false:silent 36 | dotnet_style_qualification_for_property = false:silent 37 | 38 | # Language keywords vs BCL types preferences 39 | dotnet_style_predefined_type_for_locals_parameters_members = true:silent 40 | dotnet_style_predefined_type_for_member_access = true:silent 41 | 42 | # Parentheses preferences 43 | dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent 44 | dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent 45 | dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent 46 | dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent 47 | 48 | # Modifier preferences 49 | dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent 50 | 51 | # Expression-level preferences 52 | dotnet_style_coalesce_expression = true:suggestion 53 | dotnet_style_collection_initializer = true:suggestion 54 | dotnet_style_explicit_tuple_names = true:suggestion 55 | dotnet_style_null_propagation = true:suggestion 56 | dotnet_style_object_initializer = true:suggestion 57 | dotnet_style_operator_placement_when_wrapping = beginning_of_line 58 | dotnet_style_prefer_auto_properties = true:suggestion 59 | dotnet_style_prefer_compound_assignment = true:suggestion 60 | dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion 61 | dotnet_style_prefer_conditional_expression_over_return = true:suggestion 62 | dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion 63 | dotnet_style_prefer_inferred_tuple_names = true:suggestion 64 | dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion 65 | dotnet_style_prefer_simplified_boolean_expressions = true:suggestion 66 | dotnet_style_prefer_simplified_interpolation = true:suggestion 67 | 68 | # Field preferences 69 | dotnet_style_readonly_field = true:warning 70 | 71 | # Parameter preferences 72 | dotnet_code_quality_unused_parameters = all:suggestion 73 | 74 | # Suppression preferences 75 | dotnet_remove_unnecessary_suppression_exclusions = none 76 | 77 | #### C# Coding Conventions #### 78 | [*.cs] 79 | 80 | # var preferences 81 | csharp_style_var_elsewhere = false:silent 82 | csharp_style_var_for_built_in_types = false:silent 83 | csharp_style_var_when_type_is_apparent = false:silent 84 | 85 | # Expression-bodied members 86 | csharp_style_expression_bodied_accessors = true:silent 87 | csharp_style_expression_bodied_constructors = false:silent 88 | csharp_style_expression_bodied_indexers = true:silent 89 | csharp_style_expression_bodied_lambdas = true:suggestion 90 | csharp_style_expression_bodied_local_functions = false:silent 91 | csharp_style_expression_bodied_methods = false:silent 92 | csharp_style_expression_bodied_operators = false:silent 93 | csharp_style_expression_bodied_properties = true:silent 94 | 95 | # Pattern matching preferences 96 | csharp_style_pattern_matching_over_as_with_null_check = true:suggestion 97 | csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion 98 | csharp_style_prefer_not_pattern = true:suggestion 99 | csharp_style_prefer_pattern_matching = true:silent 100 | csharp_style_prefer_switch_expression = true:suggestion 101 | 102 | # Null-checking preferences 103 | csharp_style_conditional_delegate_call = true:suggestion 104 | 105 | # Modifier preferences 106 | csharp_prefer_static_local_function = true:warning 107 | csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:silent 108 | 109 | # Code-block preferences 110 | csharp_prefer_braces = true:silent 111 | csharp_prefer_simple_using_statement = true:suggestion 112 | 113 | # Expression-level preferences 114 | csharp_prefer_simple_default_expression = true:suggestion 115 | csharp_style_deconstructed_variable_declaration = true:suggestion 116 | csharp_style_inlined_variable_declaration = true:suggestion 117 | csharp_style_pattern_local_over_anonymous_function = true:suggestion 118 | csharp_style_prefer_index_operator = true:suggestion 119 | csharp_style_prefer_range_operator = true:suggestion 120 | csharp_style_throw_expression = true:suggestion 121 | csharp_style_unused_value_assignment_preference = discard_variable:suggestion 122 | csharp_style_unused_value_expression_statement_preference = discard_variable:silent 123 | 124 | # 'using' directive preferences 125 | csharp_using_directive_placement = outside_namespace:silent 126 | 127 | #### C# Formatting Rules #### 128 | 129 | # New line preferences 130 | csharp_new_line_before_catch = true 131 | csharp_new_line_before_else = true 132 | csharp_new_line_before_finally = true 133 | csharp_new_line_before_members_in_anonymous_types = true 134 | csharp_new_line_before_members_in_object_initializers = true 135 | csharp_new_line_before_open_brace = all 136 | csharp_new_line_between_query_expression_clauses = true 137 | 138 | # Indentation preferences 139 | csharp_indent_block_contents = true 140 | csharp_indent_braces = false 141 | csharp_indent_case_contents = true 142 | csharp_indent_case_contents_when_block = true 143 | csharp_indent_labels = one_less_than_current 144 | csharp_indent_switch_labels = true 145 | 146 | # Space preferences 147 | csharp_space_after_cast = false 148 | csharp_space_after_colon_in_inheritance_clause = true 149 | csharp_space_after_comma = true 150 | csharp_space_after_dot = false 151 | csharp_space_after_keywords_in_control_flow_statements = true 152 | csharp_space_after_semicolon_in_for_statement = true 153 | csharp_space_around_binary_operators = before_and_after 154 | csharp_space_around_declaration_statements = false 155 | csharp_space_before_colon_in_inheritance_clause = true 156 | csharp_space_before_comma = false 157 | csharp_space_before_dot = false 158 | csharp_space_before_open_square_brackets = false 159 | csharp_space_before_semicolon_in_for_statement = false 160 | csharp_space_between_empty_square_brackets = false 161 | csharp_space_between_method_call_empty_parameter_list_parentheses = false 162 | csharp_space_between_method_call_name_and_opening_parenthesis = false 163 | csharp_space_between_method_call_parameter_list_parentheses = false 164 | csharp_space_between_method_declaration_empty_parameter_list_parentheses = false 165 | csharp_space_between_method_declaration_name_and_open_parenthesis = false 166 | csharp_space_between_method_declaration_parameter_list_parentheses = false 167 | csharp_space_between_parentheses = false 168 | csharp_space_between_square_brackets = false 169 | 170 | # Wrapping preferences 171 | csharp_preserve_single_line_blocks = true 172 | csharp_preserve_single_line_statements = true 173 | 174 | #### Naming styles #### 175 | [*.{cs,vb}] 176 | 177 | # Naming rules 178 | 179 | dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.severity = suggestion 180 | dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.symbols = types_and_namespaces 181 | dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.style = pascalcase 182 | 183 | dotnet_naming_rule.interfaces_should_be_ipascalcase.severity = suggestion 184 | dotnet_naming_rule.interfaces_should_be_ipascalcase.symbols = interfaces 185 | dotnet_naming_rule.interfaces_should_be_ipascalcase.style = ipascalcase 186 | 187 | dotnet_naming_rule.type_parameters_should_be_tpascalcase.severity = suggestion 188 | dotnet_naming_rule.type_parameters_should_be_tpascalcase.symbols = type_parameters 189 | dotnet_naming_rule.type_parameters_should_be_tpascalcase.style = tpascalcase 190 | 191 | dotnet_naming_rule.methods_should_be_pascalcase.severity = suggestion 192 | dotnet_naming_rule.methods_should_be_pascalcase.symbols = methods 193 | dotnet_naming_rule.methods_should_be_pascalcase.style = pascalcase 194 | 195 | dotnet_naming_rule.properties_should_be_pascalcase.severity = suggestion 196 | dotnet_naming_rule.properties_should_be_pascalcase.symbols = properties 197 | dotnet_naming_rule.properties_should_be_pascalcase.style = pascalcase 198 | 199 | dotnet_naming_rule.events_should_be_pascalcase.severity = suggestion 200 | dotnet_naming_rule.events_should_be_pascalcase.symbols = events 201 | dotnet_naming_rule.events_should_be_pascalcase.style = pascalcase 202 | 203 | dotnet_naming_rule.local_variables_should_be_camelcase.severity = suggestion 204 | dotnet_naming_rule.local_variables_should_be_camelcase.symbols = local_variables 205 | dotnet_naming_rule.local_variables_should_be_camelcase.style = camelcase 206 | 207 | dotnet_naming_rule.local_constants_should_be_camelcase.severity = suggestion 208 | dotnet_naming_rule.local_constants_should_be_camelcase.symbols = local_constants 209 | dotnet_naming_rule.local_constants_should_be_camelcase.style = camelcase 210 | 211 | dotnet_naming_rule.parameters_should_be_camelcase.severity = suggestion 212 | dotnet_naming_rule.parameters_should_be_camelcase.symbols = parameters 213 | dotnet_naming_rule.parameters_should_be_camelcase.style = camelcase 214 | 215 | dotnet_naming_rule.public_fields_should_be_pascalcase.severity = suggestion 216 | dotnet_naming_rule.public_fields_should_be_pascalcase.symbols = public_fields 217 | dotnet_naming_rule.public_fields_should_be_pascalcase.style = pascalcase 218 | 219 | dotnet_naming_rule.private_fields_should_be__camelcase.severity = suggestion 220 | dotnet_naming_rule.private_fields_should_be__camelcase.symbols = private_fields 221 | dotnet_naming_rule.private_fields_should_be__camelcase.style = _camelcase 222 | 223 | dotnet_naming_rule.private_static_fields_should_be_s_camelcase.severity = suggestion 224 | dotnet_naming_rule.private_static_fields_should_be_s_camelcase.symbols = private_static_fields 225 | dotnet_naming_rule.private_static_fields_should_be_s_camelcase.style = s_camelcase 226 | 227 | dotnet_naming_rule.public_constant_fields_should_be_pascalcase.severity = suggestion 228 | dotnet_naming_rule.public_constant_fields_should_be_pascalcase.symbols = public_constant_fields 229 | dotnet_naming_rule.public_constant_fields_should_be_pascalcase.style = pascalcase 230 | 231 | dotnet_naming_rule.private_constant_fields_should_be_pascalcase.severity = suggestion 232 | dotnet_naming_rule.private_constant_fields_should_be_pascalcase.symbols = private_constant_fields 233 | dotnet_naming_rule.private_constant_fields_should_be_pascalcase.style = pascalcase 234 | 235 | dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.severity = suggestion 236 | dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.symbols = public_static_readonly_fields 237 | dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.style = pascalcase 238 | 239 | dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.severity = suggestion 240 | dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.symbols = private_static_readonly_fields 241 | dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.style = pascalcase 242 | 243 | dotnet_naming_rule.enums_should_be_pascalcase.severity = suggestion 244 | dotnet_naming_rule.enums_should_be_pascalcase.symbols = enums 245 | dotnet_naming_rule.enums_should_be_pascalcase.style = pascalcase 246 | 247 | dotnet_naming_rule.local_functions_should_be_pascalcase.severity = suggestion 248 | dotnet_naming_rule.local_functions_should_be_pascalcase.symbols = local_functions 249 | dotnet_naming_rule.local_functions_should_be_pascalcase.style = pascalcase 250 | 251 | dotnet_naming_rule.non_field_members_should_be_pascalcase.severity = suggestion 252 | dotnet_naming_rule.non_field_members_should_be_pascalcase.symbols = non_field_members 253 | dotnet_naming_rule.non_field_members_should_be_pascalcase.style = pascalcase 254 | 255 | # Symbol specifications 256 | 257 | dotnet_naming_symbols.interfaces.applicable_kinds = interface 258 | dotnet_naming_symbols.interfaces.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 259 | dotnet_naming_symbols.interfaces.required_modifiers = 260 | 261 | dotnet_naming_symbols.enums.applicable_kinds = enum 262 | dotnet_naming_symbols.enums.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 263 | dotnet_naming_symbols.enums.required_modifiers = 264 | 265 | dotnet_naming_symbols.events.applicable_kinds = event 266 | dotnet_naming_symbols.events.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 267 | dotnet_naming_symbols.events.required_modifiers = 268 | 269 | dotnet_naming_symbols.methods.applicable_kinds = method 270 | dotnet_naming_symbols.methods.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 271 | dotnet_naming_symbols.methods.required_modifiers = 272 | 273 | dotnet_naming_symbols.properties.applicable_kinds = property 274 | dotnet_naming_symbols.properties.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 275 | dotnet_naming_symbols.properties.required_modifiers = 276 | 277 | dotnet_naming_symbols.public_fields.applicable_kinds = field 278 | dotnet_naming_symbols.public_fields.applicable_accessibilities = public, internal 279 | dotnet_naming_symbols.public_fields.required_modifiers = 280 | 281 | dotnet_naming_symbols.private_fields.applicable_kinds = field 282 | dotnet_naming_symbols.private_fields.applicable_accessibilities = private, protected, protected_internal, private_protected 283 | dotnet_naming_symbols.private_fields.required_modifiers = 284 | 285 | dotnet_naming_symbols.private_static_fields.applicable_kinds = field 286 | dotnet_naming_symbols.private_static_fields.applicable_accessibilities = private, protected, protected_internal, private_protected 287 | dotnet_naming_symbols.private_static_fields.required_modifiers = static 288 | 289 | dotnet_naming_symbols.types_and_namespaces.applicable_kinds = namespace, class, struct, interface, enum 290 | dotnet_naming_symbols.types_and_namespaces.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 291 | dotnet_naming_symbols.types_and_namespaces.required_modifiers = 292 | 293 | dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method 294 | dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 295 | dotnet_naming_symbols.non_field_members.required_modifiers = 296 | 297 | dotnet_naming_symbols.type_parameters.applicable_kinds = namespace 298 | dotnet_naming_symbols.type_parameters.applicable_accessibilities = * 299 | dotnet_naming_symbols.type_parameters.required_modifiers = 300 | 301 | dotnet_naming_symbols.private_constant_fields.applicable_kinds = field 302 | dotnet_naming_symbols.private_constant_fields.applicable_accessibilities = private, protected, protected_internal, private_protected 303 | dotnet_naming_symbols.private_constant_fields.required_modifiers = const 304 | 305 | dotnet_naming_symbols.local_variables.applicable_kinds = local 306 | dotnet_naming_symbols.local_variables.applicable_accessibilities = local 307 | dotnet_naming_symbols.local_variables.required_modifiers = 308 | 309 | dotnet_naming_symbols.local_constants.applicable_kinds = local 310 | dotnet_naming_symbols.local_constants.applicable_accessibilities = local 311 | dotnet_naming_symbols.local_constants.required_modifiers = const 312 | 313 | dotnet_naming_symbols.parameters.applicable_kinds = parameter 314 | dotnet_naming_symbols.parameters.applicable_accessibilities = * 315 | dotnet_naming_symbols.parameters.required_modifiers = 316 | 317 | dotnet_naming_symbols.public_constant_fields.applicable_kinds = field 318 | dotnet_naming_symbols.public_constant_fields.applicable_accessibilities = public, internal 319 | dotnet_naming_symbols.public_constant_fields.required_modifiers = const 320 | 321 | dotnet_naming_symbols.public_static_readonly_fields.applicable_kinds = field 322 | dotnet_naming_symbols.public_static_readonly_fields.applicable_accessibilities = public, internal 323 | dotnet_naming_symbols.public_static_readonly_fields.required_modifiers = readonly, static 324 | 325 | dotnet_naming_symbols.private_static_readonly_fields.applicable_kinds = field 326 | dotnet_naming_symbols.private_static_readonly_fields.applicable_accessibilities = private, protected, protected_internal, private_protected 327 | dotnet_naming_symbols.private_static_readonly_fields.required_modifiers = readonly, static 328 | 329 | dotnet_naming_symbols.local_functions.applicable_kinds = local_function 330 | dotnet_naming_symbols.local_functions.applicable_accessibilities = * 331 | dotnet_naming_symbols.local_functions.required_modifiers = 332 | 333 | # Naming styles 334 | 335 | dotnet_naming_style.pascalcase.required_prefix = 336 | dotnet_naming_style.pascalcase.required_suffix = 337 | dotnet_naming_style.pascalcase.word_separator = 338 | dotnet_naming_style.pascalcase.capitalization = pascal_case 339 | 340 | dotnet_naming_style.ipascalcase.required_prefix = I 341 | dotnet_naming_style.ipascalcase.required_suffix = 342 | dotnet_naming_style.ipascalcase.word_separator = 343 | dotnet_naming_style.ipascalcase.capitalization = pascal_case 344 | 345 | dotnet_naming_style.tpascalcase.required_prefix = T 346 | dotnet_naming_style.tpascalcase.required_suffix = 347 | dotnet_naming_style.tpascalcase.word_separator = 348 | dotnet_naming_style.tpascalcase.capitalization = pascal_case 349 | 350 | dotnet_naming_style._camelcase.required_prefix = _ 351 | dotnet_naming_style._camelcase.required_suffix = 352 | dotnet_naming_style._camelcase.word_separator = 353 | dotnet_naming_style._camelcase.capitalization = camel_case 354 | 355 | dotnet_naming_style.camelcase.required_prefix = 356 | dotnet_naming_style.camelcase.required_suffix = 357 | dotnet_naming_style.camelcase.word_separator = 358 | dotnet_naming_style.camelcase.capitalization = camel_case 359 | 360 | dotnet_naming_style.s_camelcase.required_prefix = s_ 361 | dotnet_naming_style.s_camelcase.required_suffix = 362 | dotnet_naming_style.s_camelcase.word_separator = 363 | dotnet_naming_style.s_camelcase.capitalization = camel_case 364 | 365 | [*.{cs,vb}] 366 | 367 | # 由于此调用不会等待,因此在此调用完成之前将会继续执行当前方法。请考虑将 "await" 运算符应用于调用结果 368 | dotnet_diagnostic.CS4014.severity = none 369 | 370 | # 命名规则冲突: 这些字必须以大写字符开头 371 | dotnet_diagnostic.IDE1006.severity = none 372 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "nuget" 9 | directory: "iPanel" 10 | schedule: 11 | interval: "weekly" 12 | labels: 13 | - "dependency" 14 | -------------------------------------------------------------------------------- /.github/workflows/iPanel-build.yml: -------------------------------------------------------------------------------- 1 | name: iPanel Build 2 | 3 | on: 4 | push: 5 | paths: 6 | - "**.cs" 7 | pull_request: 8 | paths: 9 | - "**.cs" 10 | 11 | workflow_dispatch: 12 | 13 | permissions: 14 | id-token: write 15 | contents: write 16 | checks: write 17 | 18 | jobs: 19 | build: 20 | runs-on: windows-latest 21 | 22 | strategy: 23 | matrix: 24 | runtimeIdentifier: [ win-x64, win-x86, linux-x64, linux-arm, linux-arm64, osx-x64, osx-arm64 ] 25 | 26 | steps: 27 | - name: Checkout repo 28 | uses: actions/checkout@v4 29 | 30 | - name: Download artifact 31 | uses: Legit-Labs/action-download-artifact@v2 32 | continue-on-error: true 33 | id: download 34 | with: 35 | repo: "iPanelDev/WebConsole" 36 | workflow: "deploy.yml" 37 | workflow_conclusion: success 38 | github_token: ${{ secrets.GITHUB_TOKEN }} 39 | event: push 40 | name: "github-pages" 41 | path: ${{ github.workspace }} 42 | check_artifacts: true 43 | 44 | - name: Extract files 45 | if: ${{ steps.download.outcome == 'success' }} 46 | continue-on-error: true 47 | shell: powershell 48 | run: | 49 | cd "${{ github.workspace }}" 50 | 7z x -odist -y artifact.tar 51 | 7z a -y webconsole.zip ./dist/* 52 | copy webconsole.zip iPanel/Sources/webconsole.zip 53 | 54 | - name: Install .NET 55 | uses: actions/setup-dotnet@v3 56 | with: 57 | dotnet-version: 7.0.x 58 | 59 | - name: Build iPanel 60 | shell: powershell 61 | run: | 62 | dotnet publish iPanel/iPanel.csproj --no-self-contained -p:PublishSingleFile=true -p:IncludeContentInSingleFile=true -p:RuntimeIdentifier=${{ matrix.runtimeIdentifier }} -f net6.0 63 | 64 | - name: Upload binary files(${{ matrix.runtimeIdentifier }}) 65 | uses: actions/upload-artifact@v3 66 | with: 67 | name: iPanel_${{ matrix.runtimeIdentifier }} 68 | path: iPanel/bin/Debug/net6.0/${{ matrix.runtimeIdentifier }}/publish 69 | -------------------------------------------------------------------------------- /.github/workflows/iPanel.Tests-run-tests.yml: -------------------------------------------------------------------------------- 1 | name: iPanel.Tests Run tests 2 | 3 | on: 4 | push: 5 | paths: 6 | - "**.cs" 7 | pull_request: 8 | paths: 9 | - "**.cs" 10 | 11 | permissions: 12 | id-token: write 13 | contents: write 14 | checks: write 15 | 16 | jobs: 17 | test: 18 | runs-on: windows-latest 19 | steps: 20 | - name: Checkout repo 21 | uses: actions/checkout@v4 22 | 23 | - name: Run tests 24 | run: | 25 | cd ./iPanel.Tests 26 | dotnet test 27 | cd ../ 28 | 29 | - name: Test Report 30 | uses: dorny/test-reporter@v1 31 | if: success() || failure() 32 | with: 33 | name: Tests Result 34 | path: ./iPanel.Tests/TestResults/iPanel.Tests.trx 35 | reporter: dotnet-trx 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | obj/ 3 | .vs/ 4 | 5 | node_modules 6 | *.mcdr 7 | __pycache__/** 8 | *.pyc 9 | 10 | LocalTest/ 11 | TestResults/ 12 | webconsole.zip -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": ".NET Core Attach", 6 | "type": "coreclr", 7 | "request": "attach" 8 | }, 9 | { 10 | "name": ".NET Core Launch", 11 | "type": "coreclr", 12 | "request": "launch", 13 | "program": "${workspaceFolder}/iPanel/bin/Debug/net6.0/iPanel.dll", 14 | "args": [], 15 | "cwd": "${workspaceFolder}", 16 | "stopAtEntry": false, 17 | "console": "externalTerminal", 18 | "requireExactSource": false 19 | } 20 | ] 21 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.exclude": { 3 | "**/obj/**": true, 4 | "**/bin/**": true, 5 | }, 6 | "editor.bracketPairColorization.enabled": false 7 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "command": "dotnet", 7 | "type": "shell", 8 | "args": [ 9 | "build", 10 | "./iPanel", 11 | "/property:GenerateFullPaths=true", 12 | "/consoleloggerparameters:NoSummary", 13 | "/p:WarningLevel=0" 14 | ], 15 | "group": "build", 16 | "presentation": { 17 | "reveal": "silent" 18 | }, 19 | "problemMatcher": "$msCompile" 20 | } 21 | ] 22 | } -------------------------------------------------------------------------------- /.wakatime-project: -------------------------------------------------------------------------------- 1 | iPanel -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # iPanel 4 | 5 | 网页版控制台后端 6 | 7 | [![GitHub bulid](https://img.shields.io/github/actions/workflow/status/iPanelDev/iPanel/ci.yml?branch=main&color=blue)](https://github.com/iPanelDev/iPanel/actions/workflows/iPanel-build.yml) 8 | ![GitHub all releases](https://img.shields.io/github/downloads/iPanelDev/iPanel/total?color=blue) 9 | [![GitHub repo file count](https://img.shields.io/github/languages/code-size/iPanelDev/iPanel)](https://github.com/iPanelDev/iPanel) 10 | [![wakatime](https://wakatime.com/badge/user/724e95cb-6b0f-48fb-9f96-915cce8cc845/project/77afa545-87b2-4608-8df2-2e2355550d67.svg)](https://wakatime.com/badge/user/724e95cb-6b0f-48fb-9f96-915cce8cc845/project/77afa545-87b2-4608-8df2-2e2355550d67) 11 | ![net6.0](https://img.shields.io/badge/NET-6.0-512BD4) 12 | 13 | ## 相关链接 14 | 15 | - [文档](https://ipaneldev.github.io/) 16 | - [前端仓库](https://github.com/iPanelDev/WebConsole) 17 | 18 | ## 编译 19 | 20 | - NET SDK 6.0+ 21 | 22 | ```sh 23 | dotnet build 24 | ``` 25 | 26 | ## 测试 27 | 28 | ```sh 29 | dotnet test 30 | ``` 31 | -------------------------------------------------------------------------------- /iPanel.Tests/Api/CommonTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using System.Net.Http; 4 | using System.Net.Http.Json; 5 | using System.Text.Json; 6 | using System.Text.Json.Nodes; 7 | using System.Threading.Tasks; 8 | 9 | using iPanel.Core.Models.Packets; 10 | using iPanel.Utils.Json; 11 | 12 | using Microsoft.Extensions.Hosting; 13 | 14 | using Xunit; 15 | 16 | namespace iPanel.Tests.Api; 17 | 18 | [Collection("IPANEL")] 19 | public class CommonTests : IDisposable 20 | { 21 | private const string _base = "http://127.0.0.1:30000/"; 22 | private readonly IHost _host; 23 | private readonly HttpClient _httpClient; 24 | 25 | public CommonTests() 26 | { 27 | _host = new AppBuilder(new() { InstancePassword = "🥵" }).Build(); 28 | _host.StartAsync(); 29 | _httpClient = new() { BaseAddress = new(_base) }; 30 | } 31 | 32 | public void Dispose() 33 | { 34 | _host.StopAsync(); 35 | _host.Dispose(); 36 | _httpClient.Dispose(); 37 | GC.SuppressFinalize(this); 38 | } 39 | 40 | [Fact] 41 | public async Task ShouldBeAbleToVisitRoot() 42 | { 43 | var response = await _httpClient.GetAsync("/api"); 44 | Assert.True(response.IsSuccessStatusCode); 45 | Assert.False(string.IsNullOrEmpty(await response.Content.ReadAsStringAsync())); 46 | } 47 | 48 | [Fact] 49 | public async Task ShouldBeAbleToGetVersion() 50 | { 51 | var response = await _httpClient.GetAsync("/api/version"); 52 | Assert.True(response.IsSuccessStatusCode); 53 | 54 | var result = await response.Content.ReadFromJsonAsync( 55 | JsonSerializerOptionsFactory.CamelCase 56 | ); 57 | Assert.Equal(Constant.Version, result?.Data?.ToString()); 58 | } 59 | 60 | [Fact] 61 | public async Task ShouldBeAbleToGetStatus() 62 | { 63 | var response = await _httpClient.GetAsync("/api/users/@self/status"); 64 | Assert.True(response.IsSuccessStatusCode); 65 | 66 | var result = await response.Content.ReadFromJsonAsync( 67 | JsonSerializerOptionsFactory.CamelCase 68 | ); 69 | Assert.Equal(JsonValueKind.False, result?["data"]?["logined"]?.GetValueKind()); 70 | } 71 | 72 | [Fact] 73 | public async Task ShouldBeAbleToGet400() 74 | { 75 | var response = await _httpClient.SendAsync( 76 | new(HttpMethod.Get, "/api/instances/114514/subscribe") 77 | ); 78 | Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); 79 | } 80 | 81 | [Theory] 82 | [InlineData("/api/users")] 83 | [InlineData("/api/users/@self")] 84 | [InlineData("/api/users/114514")] 85 | [InlineData("/api/instances")] 86 | [InlineData("/api/instances/114514")] 87 | [InlineData("/api/instances/114514/subscribe?connectionId=114154")] 88 | [InlineData("/api/instances/114514/start")] 89 | [InlineData("/api/instances/114514/stop")] 90 | [InlineData("/api/instances/114514/kill")] 91 | public async Task ShouldBeAbleToGet401(string url) 92 | { 93 | var response = await _httpClient.SendAsync(new(HttpMethod.Get, url)); 94 | Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); 95 | } 96 | 97 | [Theory] 98 | [InlineData("/api/users/@self/login")] 99 | [InlineData("/api/instances/114514/input")] 100 | public async Task ShouldBeAbleToGet405(string url) 101 | { 102 | var response = await _httpClient.SendAsync(new(HttpMethod.Get, url)); 103 | Assert.Equal(HttpStatusCode.MethodNotAllowed, response.StatusCode); 104 | 105 | response = await _httpClient.SendAsync(new(HttpMethod.Delete, url)); 106 | Assert.Equal(HttpStatusCode.MethodNotAllowed, response.StatusCode); 107 | 108 | response = await _httpClient.SendAsync(new(HttpMethod.Put, url)); 109 | Assert.Equal(HttpStatusCode.MethodNotAllowed, response.StatusCode); 110 | 111 | response = await _httpClient.SendAsync(new(HttpMethod.Head, url)); 112 | Assert.Equal(HttpStatusCode.MethodNotAllowed, response.StatusCode); 113 | } 114 | 115 | [Fact] 116 | public async Task ShouldBeAbleToGet404() 117 | { 118 | var response = await _httpClient.SendAsync(new(HttpMethod.Get, "/api/114514")); 119 | Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /iPanel.Tests/Api/LoginTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using System.Net.Http; 4 | using System.Net.Http.Json; 5 | using System.Text.Json.Nodes; 6 | using System.Threading.Tasks; 7 | 8 | using iPanel.Core.Models.Packets; 9 | using iPanel.Core.Models.Packets.Data; 10 | using iPanel.Core.Models.Users; 11 | using iPanel.Core.Service; 12 | using iPanel.Utils; 13 | using iPanel.Utils.Json; 14 | 15 | using Microsoft.Extensions.DependencyInjection; 16 | using Microsoft.Extensions.Hosting; 17 | 18 | using Xunit; 19 | 20 | namespace iPanel.Tests.Api; 21 | 22 | [Collection("IPANEL")] 23 | public class LoginTests : IDisposable 24 | { 25 | private const string _base = "http://127.0.0.1:30000/"; 26 | private readonly IHost _host; 27 | private IServiceProvider Services => _host.Services; 28 | private UserManager UserManager => Services.GetRequiredService(); 29 | 30 | public LoginTests() 31 | { 32 | _host = new AppBuilder(new() { InstancePassword = "🥵" }).Build(); 33 | _host.Start(); 34 | UserManager.Clear(); 35 | UserManager.Add( 36 | "Administrator", 37 | new() { Password = "123456", Level = PermissionLevel.Administrator } 38 | ); 39 | UserManager.Add("Guest", new() { Password = "123456", Level = PermissionLevel.Guest }); 40 | } 41 | 42 | public void Dispose() 43 | { 44 | _host.StopAsync(); 45 | _host.Dispose(); 46 | GC.SuppressFinalize(this); 47 | } 48 | 49 | [Fact] 50 | public async Task ShouldBeAbleToLogin() 51 | { 52 | using var httpClient = new HttpClient { BaseAddress = new(_base) }; 53 | 54 | var dateTime = DateTime.Now.ToString("o"); 55 | var response = await httpClient.SendAsync( 56 | new(HttpMethod.Post, "/api/users/@self/login") 57 | { 58 | Content = Utils.CreateContent( 59 | new VerifyBody 60 | { 61 | MD5 = Encryption.GetMD5($"{dateTime}.Administrator.123456"), 62 | Time = dateTime.ToString(), 63 | UserName = "Administrator" 64 | }, 65 | "application/json" 66 | ) 67 | } 68 | ); 69 | 70 | var body = await response.Content.ReadFromJsonAsync>( 71 | JsonSerializerOptionsFactory.CamelCase 72 | ); 73 | 74 | Assert.True(response.IsSuccessStatusCode); 75 | Assert.NotNull(body); 76 | Assert.Null(body?.ErrorMsg); 77 | Assert.True(body?.Data?.Logined); 78 | Assert.NotNull(body?.Data?.User); 79 | Assert.Equal(PermissionLevel.Administrator, body?.Data?.User?.Level); 80 | } 81 | 82 | [Fact] 83 | public async Task ShouldNotBeAbleToLoginWithWrongPwd() 84 | { 85 | using var httpClient = new HttpClient { BaseAddress = new(_base) }; 86 | 87 | var dateTime = DateTime.Now.ToString("o"); 88 | var response = await httpClient.SendAsync( 89 | new(HttpMethod.Post, "/api/users/@self/login") 90 | { 91 | Content = Utils.CreateContent( 92 | new VerifyBody 93 | { 94 | MD5 = Encryption.GetMD5($"{dateTime}.Administrator.114514"), 95 | Time = dateTime.ToString(), 96 | UserName = "Administrator" 97 | }, 98 | "application/json" 99 | ) 100 | } 101 | ); 102 | 103 | var body = await response.Content.ReadFromJsonAsync>( 104 | JsonSerializerOptionsFactory.CamelCase 105 | ); 106 | 107 | Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode); 108 | Assert.NotNull(body); 109 | Assert.Equal("用户名或密码错误", body?.ErrorMsg); 110 | Assert.Null(body?.Data); 111 | } 112 | 113 | [Fact] 114 | public async Task ShouldNotBeAbleToLoginAsInvalidUser() 115 | { 116 | using var httpClient = new HttpClient { BaseAddress = new(_base) }; 117 | 118 | var dateTime = DateTime.Now.ToString("o"); 119 | var response = await httpClient.SendAsync( 120 | new(HttpMethod.Post, "/api/users/@self/login") 121 | { 122 | Content = Utils.CreateContent( 123 | new VerifyBody 124 | { 125 | MD5 = Encryption.GetMD5($"{dateTime}.abc.114514"), 126 | Time = dateTime.ToString(), 127 | UserName = "abc" 128 | }, 129 | "application/json" 130 | ) 131 | } 132 | ); 133 | 134 | var body = await response.Content.ReadFromJsonAsync>( 135 | JsonSerializerOptionsFactory.CamelCase 136 | ); 137 | 138 | Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode); 139 | Assert.NotNull(body); 140 | Assert.Equal("用户名或密码错误", body?.ErrorMsg); 141 | Assert.Null(body?.Data); 142 | } 143 | 144 | [Theory] 145 | [InlineData("Get")] 146 | [InlineData("Delete")] 147 | [InlineData("Head")] 148 | [InlineData("Put")] 149 | [InlineData("Patch")] 150 | public async Task ShouldNotBeAbleToLoginWithOtherMethods(string method) 151 | { 152 | using var httpClient = new HttpClient { BaseAddress = new(_base) }; 153 | var httpMethod = method switch 154 | { 155 | "Get" => HttpMethod.Get, 156 | "Delete" => HttpMethod.Delete, 157 | "Head" => HttpMethod.Head, 158 | "Put" => HttpMethod.Put, 159 | "Patch" => HttpMethod.Patch, 160 | _ => throw new ArgumentOutOfRangeException(nameof(method)) 161 | }; 162 | 163 | var dateTime = DateTime.Now.ToString("o"); 164 | var response = await httpClient.SendAsync( 165 | new(httpMethod, "/api/users/@self/login") 166 | { 167 | Content = Utils.CreateContent( 168 | new VerifyBody 169 | { 170 | MD5 = Encryption.GetMD5($"{dateTime}.Administrator.123456"), 171 | Time = dateTime.ToString(), 172 | UserName = "Administrator" 173 | }, 174 | "application/json" 175 | ) 176 | } 177 | ); 178 | 179 | Assert.Equal(HttpStatusCode.MethodNotAllowed, response.StatusCode); 180 | } 181 | 182 | [Fact] 183 | public async Task ShouldNotBeAbleToLoginWithoutContentType() 184 | { 185 | using var httpClient = new HttpClient { BaseAddress = new(_base) }; 186 | 187 | var dateTime = DateTime.Now.ToString("o"); 188 | var response = await httpClient.SendAsync( 189 | new(HttpMethod.Post, "/api/users/@self/login") 190 | { 191 | Content = Utils.CreateContent( 192 | new VerifyBody 193 | { 194 | MD5 = Encryption.GetMD5($"{dateTime}.Administrator.123456"), 195 | Time = dateTime.ToString(), 196 | UserName = "Administrator" 197 | } 198 | ) 199 | } 200 | ); 201 | 202 | Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); 203 | Assert.Equal( 204 | "不支持的\"ContentType\"", 205 | ( 206 | await response.Content.ReadFromJsonAsync( 207 | JsonSerializerOptionsFactory.CamelCase 208 | ) 209 | )?.ErrorMsg 210 | ); 211 | } 212 | 213 | [Fact] 214 | public async Task ShouldNotBeAbleToLoginWithoutTime() 215 | { 216 | using var httpClient = new HttpClient { BaseAddress = new(_base) }; 217 | 218 | var dateTime = DateTime.Now.ToString("o"); 219 | var response = await httpClient.SendAsync( 220 | new(HttpMethod.Post, "/api/users/@self/login") 221 | { 222 | Content = Utils.CreateContent( 223 | new VerifyBody 224 | { 225 | MD5 = Encryption.GetMD5($"{dateTime}.Administrator.123456"), 226 | UserName = "Administrator" 227 | }, 228 | "application/json" 229 | ) 230 | } 231 | ); 232 | 233 | Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); 234 | Assert.Equal( 235 | "\"time\"无效", 236 | ( 237 | await response.Content.ReadFromJsonAsync( 238 | JsonSerializerOptionsFactory.CamelCase 239 | ) 240 | )?.ErrorMsg 241 | ); 242 | } 243 | 244 | [Fact] 245 | public async Task ShouldNotBeAbleToLoginWithoutWrongTime() 246 | { 247 | using var httpClient = new HttpClient { BaseAddress = new(_base) }; 248 | 249 | var dateTime = DateTime.Now.ToString("o"); 250 | var response = await httpClient.SendAsync( 251 | new(HttpMethod.Post, "/api/users/@self/login") 252 | { 253 | Content = Utils.CreateContent( 254 | new VerifyBody 255 | { 256 | MD5 = Encryption.GetMD5($"{dateTime}.Administrator.123456"), 257 | UserName = "Administrator", 258 | Time = "1sfnrugeigheiggtnnfwenrnjwrnjr" 259 | }, 260 | "application/json" 261 | ) 262 | } 263 | ); 264 | 265 | Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); 266 | Assert.Equal( 267 | "\"time\"无效", 268 | ( 269 | await response.Content.ReadFromJsonAsync( 270 | JsonSerializerOptionsFactory.CamelCase 271 | ) 272 | )?.ErrorMsg 273 | ); 274 | } 275 | 276 | [Fact] 277 | public async Task ShouldNotBeAbleToLoginWithoutExpiredTime() 278 | { 279 | using var httpClient = new HttpClient { BaseAddress = new(_base) }; 280 | 281 | var dateTime = DateTime.Now.AddSeconds(-20).ToString("o"); 282 | var response = await httpClient.SendAsync( 283 | new(HttpMethod.Post, "/api/users/@self/login") 284 | { 285 | Content = Utils.CreateContent( 286 | new VerifyBody 287 | { 288 | MD5 = Encryption.GetMD5($"{dateTime}.Administrator.123456"), 289 | UserName = "Administrator", 290 | Time = dateTime 291 | }, 292 | "application/json" 293 | ) 294 | } 295 | ); 296 | 297 | Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); 298 | Assert.Equal( 299 | "\"time\"已过期", 300 | ( 301 | await response.Content.ReadFromJsonAsync( 302 | JsonSerializerOptionsFactory.CamelCase 303 | ) 304 | )?.ErrorMsg 305 | ); 306 | } 307 | 308 | [Fact] 309 | public async Task ShouldNotBeAbleToLoginAsGuest() 310 | { 311 | using var httpClient = new HttpClient { BaseAddress = new(_base) }; 312 | 313 | var dateTime = DateTime.Now.ToString("o"); 314 | var response = await httpClient.SendAsync( 315 | new(HttpMethod.Post, "/api/users/@self/login") 316 | { 317 | Content = Utils.CreateContent( 318 | new VerifyBody 319 | { 320 | MD5 = Encryption.GetMD5($"{dateTime}.Guest.123456"), 321 | UserName = "Guest", 322 | Time = dateTime 323 | }, 324 | "application/json" 325 | ) 326 | } 327 | ); 328 | 329 | Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode); 330 | Assert.Equal( 331 | "用户无效", 332 | ( 333 | await response.Content.ReadFromJsonAsync( 334 | JsonSerializerOptionsFactory.CamelCase 335 | ) 336 | )?.ErrorMsg 337 | ); 338 | } 339 | } 340 | -------------------------------------------------------------------------------- /iPanel.Tests/Api/LogoutTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Http; 3 | using System.Net.Http.Json; 4 | using System.Text.Json; 5 | using System.Text.Json.Nodes; 6 | using System.Threading.Tasks; 7 | 8 | using iPanel.Core.Models.Packets; 9 | using iPanel.Core.Models.Packets.Data; 10 | using iPanel.Core.Models.Users; 11 | using iPanel.Core.Service; 12 | using iPanel.Utils; 13 | using iPanel.Utils.Json; 14 | 15 | using Microsoft.Extensions.DependencyInjection; 16 | using Microsoft.Extensions.Hosting; 17 | 18 | using Xunit; 19 | 20 | namespace iPanel.Tests.Api; 21 | 22 | [Collection("IPANEL")] 23 | public class LogoutTests : IDisposable 24 | { 25 | private const string _base = "http://127.0.0.1:30000/"; 26 | private readonly IHost _host; 27 | private IServiceProvider Services => _host.Services; 28 | private UserManager UserManager => Services.GetRequiredService(); 29 | 30 | public LogoutTests() 31 | { 32 | _host = new AppBuilder(new() { InstancePassword = "🥵" }).Build(); 33 | _host.Start(); 34 | UserManager.Clear(); 35 | UserManager.Add( 36 | "Administrator", 37 | new() { Password = "123456", Level = PermissionLevel.Administrator } 38 | ); 39 | } 40 | 41 | public void Dispose() 42 | { 43 | _host.StopAsync(); 44 | _host.Dispose(); 45 | GC.SuppressFinalize(this); 46 | } 47 | 48 | [Fact] 49 | public async Task ShouldBeAbleToLogout() 50 | { 51 | using var httpClient = new HttpClient { BaseAddress = new(_base) }; 52 | 53 | var dateTime = DateTime.Now.ToString("o"); 54 | var response = await httpClient.SendAsync( 55 | new(HttpMethod.Post, "/api/users/@self/login") 56 | { 57 | Content = Utils.CreateContent( 58 | new VerifyBody 59 | { 60 | MD5 = Encryption.GetMD5($"{dateTime}.Administrator.123456"), 61 | Time = dateTime.ToString(), 62 | UserName = "Administrator" 63 | }, 64 | "application/json" 65 | ) 66 | } 67 | ); 68 | var body = await response.Content.ReadFromJsonAsync( 69 | JsonSerializerOptionsFactory.CamelCase 70 | ); 71 | 72 | Assert.True(response.IsSuccessStatusCode); 73 | Assert.NotNull(body); 74 | 75 | response = await httpClient.SendAsync(new(HttpMethod.Get, "/api/users/@self/status")); 76 | var result = await response.Content.ReadFromJsonAsync( 77 | JsonSerializerOptionsFactory.CamelCase 78 | ); 79 | Assert.Equal(JsonValueKind.True, result?["data"]?["logined"]?.GetValueKind()); 80 | 81 | response = await httpClient.SendAsync(new(HttpMethod.Get, "/api/users/@self/logout")); 82 | body = await response.Content.ReadFromJsonAsync( 83 | JsonSerializerOptionsFactory.CamelCase 84 | ); 85 | 86 | Assert.True(response.IsSuccessStatusCode); 87 | Assert.NotNull(body); 88 | Assert.Null(body?.Data); 89 | Assert.Null(body?.ErrorMsg); 90 | 91 | response = await httpClient.SendAsync(new(HttpMethod.Get, "/api/users/@self/status")); 92 | result = await response.Content.ReadFromJsonAsync( 93 | JsonSerializerOptionsFactory.CamelCase 94 | ); 95 | Assert.Equal(JsonValueKind.False, result?["data"]?["logined"]?.GetValueKind()); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /iPanel.Tests/Api/UserManagementTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net; 5 | using System.Net.Http; 6 | using System.Net.Http.Json; 7 | using System.Threading.Tasks; 8 | 9 | using iPanel.Core.Models.Packets; 10 | using iPanel.Core.Models.Packets.Data; 11 | using iPanel.Core.Models.Users; 12 | using iPanel.Core.Service; 13 | using iPanel.Utils; 14 | using iPanel.Utils.Json; 15 | 16 | using Microsoft.Extensions.DependencyInjection; 17 | using Microsoft.Extensions.Hosting; 18 | 19 | using Xunit; 20 | 21 | namespace iPanel.Tests.Api; 22 | 23 | [Collection("IPANEL")] 24 | public class UserManagementTests : IDisposable 25 | { 26 | private const string _base = "http://127.0.0.1:30000/"; 27 | private readonly IHost _host; 28 | private IServiceProvider Services => _host.Services; 29 | private UserManager UserManager => Services.GetRequiredService(); 30 | 31 | public UserManagementTests() 32 | { 33 | _host = new AppBuilder(new() { InstancePassword = "🥵" }).Build(); 34 | _host.Start(); 35 | UserManager.Clear(); 36 | UserManager.Add( 37 | "Administrator", 38 | new() { Password = "123456", Level = PermissionLevel.Administrator } 39 | ); 40 | UserManager.Add( 41 | "Assistant", 42 | new() { Password = "123456", Level = PermissionLevel.Assistant } 43 | ); 44 | UserManager.Add( 45 | "ReadOnly", 46 | new() { Password = "123456", Level = PermissionLevel.ReadOnly } 47 | ); 48 | UserManager.Add("Guest", new() { Password = "123456", Level = PermissionLevel.Guest }); 49 | } 50 | 51 | public void Dispose() 52 | { 53 | _host.StopAsync(); 54 | _host.Dispose(); 55 | GC.SuppressFinalize(this); 56 | } 57 | 58 | public async Task Login( 59 | HttpClient httpClient, 60 | PermissionLevel permissionLevel, 61 | string? password = null 62 | ) 63 | { 64 | var dateTime = DateTime.Now.ToString("o"); 65 | var response = await httpClient.SendAsync( 66 | new(HttpMethod.Post, "/api/users/@self/login") 67 | { 68 | Content = Utils.CreateContent( 69 | new VerifyBody 70 | { 71 | MD5 = Encryption.GetMD5( 72 | $"{dateTime}.{permissionLevel}.{password ?? "123456"}" 73 | ), 74 | Time = dateTime.ToString(), 75 | UserName = permissionLevel.ToString() 76 | }, 77 | "application/json" 78 | ) 79 | } 80 | ); 81 | 82 | var body = await response.Content.ReadFromJsonAsync>( 83 | JsonSerializerOptionsFactory.CamelCase 84 | ); 85 | 86 | Assert.True(response.IsSuccessStatusCode); 87 | Assert.NotNull(body); 88 | Assert.Null(body?.ErrorMsg); 89 | Assert.True(body?.Data?.Logined); 90 | Assert.NotNull(body?.Data?.User); 91 | } 92 | 93 | [Fact] 94 | public async Task ShouldNotBeAbleToGetAllUsersWhenNotLoginYet() 95 | { 96 | using var httpClient = new HttpClient { BaseAddress = new(_base) }; 97 | var response = await httpClient.SendAsync(new(HttpMethod.Get, "/api/users")); 98 | 99 | var body = await response.Content.ReadFromJsonAsync( 100 | JsonSerializerOptionsFactory.CamelCase 101 | ); 102 | 103 | Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); 104 | Assert.NotNull(body); 105 | Assert.Null(body?.Data); 106 | } 107 | 108 | [Fact] 109 | public async Task ShouldNotBeAbleToRemoveSpecificUserWhenNotLoginYet() 110 | { 111 | using var httpClient = new HttpClient { BaseAddress = new(_base) }; 112 | var response = await httpClient.SendAsync(new(HttpMethod.Delete, "/api/users/Guest")); 113 | 114 | var body = await response.Content.ReadFromJsonAsync( 115 | JsonSerializerOptionsFactory.CamelCase 116 | ); 117 | 118 | Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); 119 | Assert.NotNull(body); 120 | Assert.Null(body?.Data); 121 | } 122 | 123 | [Fact] 124 | public async Task ShouldNotBeAbleToGetSpecificUserWhenNotLoginYet() 125 | { 126 | using var httpClient = new HttpClient { BaseAddress = new(_base) }; 127 | var response = await httpClient.SendAsync(new(HttpMethod.Get, "/api/users/Guest")); 128 | 129 | var body = await response.Content.ReadFromJsonAsync( 130 | JsonSerializerOptionsFactory.CamelCase 131 | ); 132 | 133 | Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); 134 | Assert.NotNull(body); 135 | Assert.Null(body?.Data); 136 | } 137 | 138 | [Fact] 139 | public async Task ShouldNotBeAbleToCreateSpecificUserWhenNotLoginYet() 140 | { 141 | using var httpClient = new HttpClient { BaseAddress = new(_base) }; 142 | var response = await httpClient.SendAsync( 143 | new(HttpMethod.Post, "/api/users/Guest") 144 | { 145 | Content = Utils.CreateContent(new User { Password = "123456" }, "application/json") 146 | } 147 | ); 148 | 149 | var body = await response.Content.ReadFromJsonAsync( 150 | JsonSerializerOptionsFactory.CamelCase 151 | ); 152 | 153 | Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); 154 | Assert.NotNull(body); 155 | Assert.Null(body?.Data); 156 | } 157 | 158 | [Fact] 159 | public async Task ShouldNotBeAbleToEditSpecificUserWhenNotLoginYet() 160 | { 161 | using var httpClient = new HttpClient { BaseAddress = new(_base) }; 162 | var response = await httpClient.SendAsync( 163 | new(HttpMethod.Put, "/api/users/Guest") 164 | { 165 | Content = Utils.CreateContent(new User { Password = "123456" }, "application/json") 166 | } 167 | ); 168 | 169 | var body = await response.Content.ReadFromJsonAsync( 170 | JsonSerializerOptionsFactory.CamelCase 171 | ); 172 | 173 | Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); 174 | Assert.NotNull(body); 175 | Assert.Null(body?.Data); 176 | } 177 | 178 | [Fact] 179 | public async Task ShouldNotBeAbleToGetSpecificUser() 180 | { 181 | using var httpClient = new HttpClient { BaseAddress = new(_base) }; 182 | var response = await httpClient.SendAsync(new(HttpMethod.Get, "/api/users/Guest")); 183 | 184 | var body = await response.Content.ReadFromJsonAsync( 185 | JsonSerializerOptionsFactory.CamelCase 186 | ); 187 | 188 | Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); 189 | Assert.NotNull(body); 190 | Assert.Null(body?.Data); 191 | } 192 | 193 | [Theory] 194 | [InlineData(PermissionLevel.Assistant)] 195 | [InlineData(PermissionLevel.ReadOnly)] 196 | public async Task ShouldNotBeAbleToGetAllUsersWhenLoginNotAsAdministrator( 197 | PermissionLevel permissionLevel 198 | ) 199 | { 200 | using var httpClient = new HttpClient { BaseAddress = new(_base) }; 201 | await Login(httpClient, permissionLevel); 202 | 203 | var response = await httpClient.SendAsync(new(HttpMethod.Get, "/api/users")); 204 | var body = await response.Content.ReadFromJsonAsync( 205 | JsonSerializerOptionsFactory.CamelCase 206 | ); 207 | 208 | Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode); 209 | Assert.NotNull(body); 210 | Assert.Null(body?.Data); 211 | } 212 | 213 | [Theory] 214 | [InlineData(PermissionLevel.Assistant)] 215 | [InlineData(PermissionLevel.ReadOnly)] 216 | public async Task ShouldNotBeAbleToRemoveSpecificUserWhenLoginNotAsAdministrator( 217 | PermissionLevel permissionLevel 218 | ) 219 | { 220 | using var httpClient = new HttpClient { BaseAddress = new(_base) }; 221 | await Login(httpClient, permissionLevel); 222 | 223 | var response = await httpClient.SendAsync(new(HttpMethod.Delete, "/api/users/Guest")); 224 | var body = await response.Content.ReadFromJsonAsync( 225 | JsonSerializerOptionsFactory.CamelCase 226 | ); 227 | 228 | Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode); 229 | Assert.NotNull(body); 230 | Assert.Null(body?.Data); 231 | } 232 | 233 | [Theory] 234 | [InlineData(PermissionLevel.Assistant)] 235 | [InlineData(PermissionLevel.ReadOnly)] 236 | public async Task ShouldNotBeAbleToGetSpecificUserWhenLoginNotAsAdministrator( 237 | PermissionLevel permissionLevel 238 | ) 239 | { 240 | using var httpClient = new HttpClient { BaseAddress = new(_base) }; 241 | await Login(httpClient, permissionLevel); 242 | var response = await httpClient.SendAsync(new(HttpMethod.Get, "/api/users/Guest")); 243 | 244 | var body = await response.Content.ReadFromJsonAsync( 245 | JsonSerializerOptionsFactory.CamelCase 246 | ); 247 | 248 | Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode); 249 | Assert.NotNull(body); 250 | Assert.Null(body?.Data); 251 | } 252 | 253 | [Theory] 254 | [InlineData(PermissionLevel.Assistant)] 255 | [InlineData(PermissionLevel.ReadOnly)] 256 | public async Task ShouldNotBeAbleToCreateSpecificUserWhenLoginNotAsAdministrator( 257 | PermissionLevel permissionLevel 258 | ) 259 | { 260 | using var httpClient = new HttpClient { BaseAddress = new(_base) }; 261 | await Login(httpClient, permissionLevel); 262 | 263 | var response = await httpClient.SendAsync( 264 | new(HttpMethod.Post, "/api/users/Guest") 265 | { 266 | Content = Utils.CreateContent(new User { Password = "123456" }, "application/json") 267 | } 268 | ); 269 | var body = await response.Content.ReadFromJsonAsync( 270 | JsonSerializerOptionsFactory.CamelCase 271 | ); 272 | 273 | Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode); 274 | Assert.NotNull(body); 275 | Assert.Null(body?.Data); 276 | } 277 | 278 | [Theory] 279 | [InlineData(PermissionLevel.Assistant)] 280 | [InlineData(PermissionLevel.ReadOnly)] 281 | public async Task ShouldNotBeAbleToEditSpecificUserWhenLoginNotAsAdministrator( 282 | PermissionLevel permissionLevel 283 | ) 284 | { 285 | using var httpClient = new HttpClient { BaseAddress = new(_base) }; 286 | await Login(httpClient, permissionLevel); 287 | 288 | var response = await httpClient.SendAsync( 289 | new(HttpMethod.Put, "/api/users/Guest") 290 | { 291 | Content = Utils.CreateContent(new User { Password = "123456" }, "application/json") 292 | } 293 | ); 294 | var body = await response.Content.ReadFromJsonAsync( 295 | JsonSerializerOptionsFactory.CamelCase 296 | ); 297 | 298 | Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode); 299 | Assert.NotNull(body); 300 | Assert.Null(body?.Data); 301 | } 302 | 303 | [Fact] 304 | public async Task ShouldBeAbleToGetAllUsers() 305 | { 306 | using var httpClient = new HttpClient { BaseAddress = new(_base) }; 307 | await Login(httpClient, PermissionLevel.Administrator); 308 | 309 | var response = await httpClient.SendAsync(new(HttpMethod.Get, "/api/users")); 310 | var body = await response.Content.ReadFromJsonAsync>>( 311 | JsonSerializerOptionsFactory.CamelCase 312 | ); 313 | 314 | Assert.True(response.IsSuccessStatusCode); 315 | Assert.NotNull(body); 316 | Assert.Equal(4, body?.Data?.Count); 317 | Assert.Null(body?.Data?.FirstOrDefault().Value.Password); 318 | Assert.True(body?.Data?.ContainsKey("Administrator")); 319 | Assert.True(body?.Data?.ContainsKey("ReadOnly")); 320 | Assert.True(body?.Data?.ContainsKey("Assistant")); 321 | Assert.True(body?.Data?.ContainsKey("Guest")); 322 | } 323 | 324 | [Fact] 325 | public async Task ShouldBeAbleToGetSpecificUser() 326 | { 327 | using var httpClient = new HttpClient { BaseAddress = new(_base) }; 328 | await Login(httpClient, PermissionLevel.Administrator); 329 | 330 | var response = await httpClient.SendAsync(new(HttpMethod.Get, "/api/users/Guest")); 331 | var body = await response.Content.ReadFromJsonAsync>( 332 | JsonSerializerOptionsFactory.CamelCase 333 | ); 334 | 335 | Assert.True(response.IsSuccessStatusCode); 336 | Assert.NotNull(body); 337 | Assert.Equal(PermissionLevel.Guest, body?.Data?.Level); 338 | Assert.Null(body?.Data?.Password); 339 | } 340 | 341 | [Fact] 342 | public async Task ShouldBeAbleToCreateSpecificUser() 343 | { 344 | using var httpClient = new HttpClient { BaseAddress = new(_base) }; 345 | await Login(httpClient, PermissionLevel.Administrator); 346 | 347 | var response = await httpClient.SendAsync( 348 | new(HttpMethod.Post, "/api/users/Guest1") 349 | { 350 | Content = Utils.CreateContent(new User { Password = "123456" }, "application/json") 351 | } 352 | ); 353 | var body = await response.Content.ReadFromJsonAsync( 354 | JsonSerializerOptionsFactory.CamelCase 355 | ); 356 | 357 | Assert.True(response.IsSuccessStatusCode); 358 | Assert.NotNull(body); 359 | Assert.Null(body?.Data); 360 | } 361 | 362 | [Fact] 363 | public async Task ShouldBeAbleToRemoveSpecificUser() 364 | { 365 | using var httpClient = new HttpClient { BaseAddress = new(_base) }; 366 | await Login(httpClient, PermissionLevel.Administrator); 367 | 368 | var response = await httpClient.SendAsync(new(HttpMethod.Delete, "/api/users/Guest")); 369 | var body = await response.Content.ReadFromJsonAsync( 370 | JsonSerializerOptionsFactory.CamelCase 371 | ); 372 | 373 | Assert.True(response.IsSuccessStatusCode); 374 | Assert.NotNull(body); 375 | Assert.Null(body?.Data); 376 | 377 | response = await httpClient.SendAsync(new(HttpMethod.Get, "/api/users/Guest")); 378 | body = await response.Content.ReadFromJsonAsync( 379 | JsonSerializerOptionsFactory.CamelCase 380 | ); 381 | 382 | Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); 383 | Assert.NotNull(body); 384 | Assert.Null(body?.Data); 385 | Assert.Equal("用户不存在", body?.ErrorMsg); 386 | } 387 | 388 | [Fact] 389 | public async Task ShouldBeAbleToEditSpecificUser() 390 | { 391 | var httpClient = new HttpClient { BaseAddress = new(_base) }; 392 | await Login(httpClient, PermissionLevel.Administrator); 393 | 394 | var response = await httpClient.SendAsync( 395 | new(HttpMethod.Put, "/api/users/ReadOnly") 396 | { 397 | Content = Utils.CreateContent( 398 | new User { Password = "1234567", Level = PermissionLevel.ReadOnly }, 399 | "application/json" 400 | ) 401 | } 402 | ); 403 | var body = await response.Content.ReadFromJsonAsync( 404 | JsonSerializerOptionsFactory.CamelCase 405 | ); 406 | 407 | Assert.True(response.IsSuccessStatusCode); 408 | 409 | httpClient = new HttpClient { BaseAddress = new(_base) }; 410 | await Login(httpClient, PermissionLevel.ReadOnly, "1234567"); 411 | } 412 | } 413 | -------------------------------------------------------------------------------- /iPanel.Tests/InstanceTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.Json; 3 | 4 | using iPanel.Core.Models.Packets; 5 | using iPanel.Core.Models.Packets.Data; 6 | using iPanel.Utils; 7 | using iPanel.Utils.Json; 8 | 9 | using Microsoft.Extensions.Hosting; 10 | 11 | using WebSocket4Net; 12 | 13 | using Xunit; 14 | 15 | namespace iPanel.Tests; 16 | 17 | [Collection("IPANEL")] 18 | public class InstanceTests : IDisposable 19 | { 20 | private readonly IHost _host; 21 | 22 | private const string _wsUrl = "ws://127.0.0.1:30000/ws/instance"; 23 | private const string _password = "114514"; 24 | 25 | public InstanceTests() 26 | { 27 | _host = new AppBuilder(new() { InstancePassword = _password }).Build(); 28 | _host.StartAsync(); 29 | } 30 | 31 | public void Dispose() 32 | { 33 | _host.StopAsync(); 34 | _host.Dispose(); 35 | GC.SuppressFinalize(this); 36 | } 37 | 38 | [Fact] 39 | public void ShouldBeAbleToConnect() 40 | { 41 | var ws = new WebSocket(_wsUrl); 42 | ws.Opened += (_, _) => Assert.True(true); 43 | ws.Opened += (_, _) => ws.Close(); 44 | ws.Open(); 45 | } 46 | 47 | [Fact] 48 | public void ShouldBeAbleToVerify() 49 | { 50 | var ws = new WebSocket(_wsUrl); 51 | var dateTime = DateTime.Now.ToString("o"); 52 | ws.Opened += (_, _) => 53 | { 54 | ws.Send( 55 | new WsSentPacket( 56 | "request", 57 | "verify", 58 | new VerifyBody 59 | { 60 | Time = dateTime, 61 | MD5 = Encryption.GetMD5($"{dateTime}.{_password}") 62 | } 63 | ) 64 | ); 65 | }; 66 | ws.MessageReceived += (_, e) => 67 | { 68 | var packet = JsonSerializer.Deserialize( 69 | e.Message, 70 | JsonSerializerOptionsFactory.CamelCase 71 | ); 72 | 73 | Assert.True(packet?.Type == "event"); 74 | Assert.True(packet?.SubType == "verify_result"); 75 | Assert.True(packet?.Data?.GetValueKind() == JsonValueKind.Object); 76 | Assert.True(packet?.Data?["success"]?.GetValueKind() == JsonValueKind.True); 77 | ws.Close(); 78 | }; 79 | ws.Open(); 80 | } 81 | 82 | [Fact] 83 | public void ShouldBeNotAbleToVerifyWithoutMD5() 84 | { 85 | var ws = new WebSocket(_wsUrl); 86 | var dateTime = DateTime.Now.ToString("o"); 87 | ws.Opened += (_, _) => 88 | { 89 | ws.Send(new WsSentPacket("request", "verify", new VerifyBody { Time = dateTime, })); 90 | }; 91 | ws.MessageReceived += (_, e) => 92 | { 93 | var packet = JsonSerializer.Deserialize( 94 | e.Message, 95 | JsonSerializerOptionsFactory.CamelCase 96 | ); 97 | 98 | Assert.True(packet?.Type == "event"); 99 | Assert.True(packet?.SubType == "verify_result"); 100 | Assert.True(packet?.Data?.GetValueKind() == JsonValueKind.Object); 101 | Assert.True(packet?.Data?["success"]?.GetValueKind() == JsonValueKind.False); 102 | ws.Close(); 103 | }; 104 | ws.Open(); 105 | } 106 | 107 | [Fact] 108 | public void ShouldBeNotAbleToVerifyWithoutTime() 109 | { 110 | var ws = new WebSocket(_wsUrl); 111 | var dateTime = DateTime.Now.ToString("o"); 112 | ws.Opened += (_, _) => 113 | { 114 | ws.Send( 115 | new WsSentPacket( 116 | "request", 117 | "verify", 118 | new VerifyBody { MD5 = Encryption.GetMD5($"{dateTime}.{_password}") } 119 | ) 120 | ); 121 | }; 122 | ws.MessageReceived += (_, e) => 123 | { 124 | var packet = JsonSerializer.Deserialize( 125 | e.Message, 126 | JsonSerializerOptionsFactory.CamelCase 127 | ); 128 | 129 | Assert.True(packet?.Type == "event"); 130 | Assert.True(packet?.SubType == "verify_result"); 131 | Assert.True(packet?.Data?.GetValueKind() == JsonValueKind.Object); 132 | Assert.True(packet?.Data?["success"]?.GetValueKind() == JsonValueKind.False); 133 | ws.Close(); 134 | }; 135 | ws.Open(); 136 | } 137 | 138 | [Fact(Timeout = 7500)] 139 | public void ShouldBeClosedDueToTimeout() 140 | { 141 | var ws = new WebSocket(_wsUrl); 142 | ws.MessageReceived += (_, e) => 143 | { 144 | var packet = JsonSerializer.Deserialize( 145 | e.Message, 146 | JsonSerializerOptionsFactory.CamelCase 147 | ); 148 | 149 | Assert.True(packet?.Type == "event"); 150 | Assert.True(packet?.SubType == "verify_result"); 151 | Assert.True(packet?.Data?.GetValueKind() == JsonValueKind.Object); 152 | Assert.True(packet?.Data?["success"]?.GetValueKind() == JsonValueKind.False); 153 | ws.Close(); 154 | }; 155 | ws.Open(); 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /iPanel.Tests/ParseInputTests.cs: -------------------------------------------------------------------------------- 1 | using iPanel.Core.Interaction; 2 | 3 | using Xunit; 4 | 5 | namespace iPanel.Tests; 6 | 7 | public class ParseInputTests 8 | { 9 | [Theory] 10 | [InlineData("a", "a")] 11 | [InlineData("a b", "a", "b")] 12 | [InlineData("a b", "a", "b")] 13 | [InlineData("a b ", "a", "b")] 14 | [InlineData("a b ", "a", "b")] 15 | public void ShouldSplitBySpace(string input, params string[] expected) 16 | { 17 | Assert.True(InputReader.Parse(input, out var result)); 18 | Assert.Equal(expected, result); 19 | } 20 | 21 | [Theory] 22 | [InlineData("test \"")] 23 | [InlineData("test \"aa")] 24 | [InlineData("test aa\"")] 25 | public void ShouldReturnFalseWhenColonsAreNotClosed(string input) 26 | { 27 | Assert.False(InputReader.Parse(input, out _)); 28 | } 29 | 30 | [Theory] 31 | [InlineData("test \"\"", "test")] 32 | [InlineData("test \"abc\"", "test", "abc")] 33 | [InlineData("test \"a b c\"", "test", "a b c")] 34 | public void ShouldReturnTrueWhenColonsAreClosed(string input, params string[] expected) 35 | { 36 | Assert.True(InputReader.Parse(input, out var result)); 37 | Assert.Equal(expected, result); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /iPanel.Tests/SettingTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using iPanel.Core.Models.Exceptions; 4 | using iPanel.Core.Models.Settings; 5 | 6 | using Xunit; 7 | 8 | namespace iPanel.Tests; 9 | 10 | public class SettingTests 11 | { 12 | [Fact] 13 | public void SettingsShouldThrowIfInvalid() 14 | { 15 | Assert.Throws(() => new Setting().Check()); 16 | 17 | Assert.Throws( 18 | () => new Setting { InstancePassword = "6", WebServer = null!, }.Check() 19 | ); 20 | 21 | Assert.Throws( 22 | () => 23 | new Setting 24 | { 25 | InstancePassword = "6", 26 | WebServer = new() { Certificate = null! }, 27 | }.Check() 28 | ); 29 | 30 | Assert.Throws( 31 | () => 32 | new Setting 33 | { 34 | InstancePassword = "6", 35 | WebServer = new() { Directory = string.Empty }, 36 | }.Check() 37 | ); 38 | Assert.Throws( 39 | () => 40 | new Setting 41 | { 42 | InstancePassword = "6", 43 | WebServer = new() { Directory = null! }, 44 | }.Check() 45 | ); 46 | 47 | Assert.Throws( 48 | () => 49 | new Setting 50 | { 51 | InstancePassword = "6", 52 | WebServer = new() { Page404 = string.Empty }, 53 | }.Check() 54 | ); 55 | Assert.Throws( 56 | () => 57 | new Setting 58 | { 59 | InstancePassword = "6", 60 | WebServer = new() { Page404 = null! }, 61 | }.Check() 62 | ); 63 | 64 | Assert.Throws( 65 | () => 66 | new Setting 67 | { 68 | InstancePassword = "6", 69 | WebServer = new() { UrlPrefixes = Array.Empty() }, 70 | }.Check() 71 | ); 72 | Assert.Throws( 73 | () => 74 | new Setting 75 | { 76 | InstancePassword = "6", 77 | WebServer = new() { UrlPrefixes = null! }, 78 | }.Check() 79 | ); 80 | 81 | Assert.Throws(() => new Setting { InstancePassword = null!, }.Check()); 82 | 83 | Assert.Throws( 84 | () => 85 | new Setting 86 | { 87 | InstancePassword = "6", 88 | WebServer = new() { MaxRequestsPerSecond = -1 } 89 | }.Check() 90 | ); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /iPanel.Tests/Utils.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http; 2 | using System.Text.Json; 3 | using System.Text.Json.Serialization; 4 | 5 | using iPanel.Utils.Json; 6 | 7 | namespace iPanel.Tests; 8 | 9 | public static class Utils 10 | { 11 | public static HttpContent CreateContent(T obj, string? contentType = null) 12 | { 13 | var content = new StringContent( 14 | JsonSerializer.Serialize( 15 | obj, 16 | options: new(JsonSerializerOptionsFactory.CamelCase) 17 | { 18 | DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault 19 | } 20 | ) 21 | ) 22 | { 23 | Headers = { ContentType = string.IsNullOrEmpty(contentType) ? null : new(contentType) } 24 | }; 25 | 26 | return content; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /iPanel.Tests/iPanel.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | runtime; build; native; contentfiles; analyzers; buildtransitive 15 | all 16 | 17 | 18 | runtime; build; native; contentfiles; analyzers; buildtransitive 19 | all 20 | 21 | 22 | 23 | 24 | 25 | 26 | trx%3bLogFileName=$(MSBuildProjectName).trx 27 | $(MSBuildThisFileDirectory)/TestResults/ 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /iPanel.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "." 5 | } 6 | ], 7 | "extensions": { 8 | "recommendations": [ 9 | "ms-dotnettools.vscode-dotnet-runtime", 10 | "ms-dotnettools.csharp", 11 | "ms-dotnettools.vscodeintellicode-csharp", 12 | "k--kato.docomment", 13 | "Fudge.auto-using", 14 | "adrianwilczynski.namespace", 15 | "kreativ-software.csharpextensions" 16 | ], 17 | }, 18 | "settings": { 19 | "workbench.colorTheme": "Visual Studio 2019 Dark", 20 | "workbench.iconTheme": "vscode-icons", 21 | "dotnet.defaultSolution": "iPanel.sln" 22 | } 23 | } -------------------------------------------------------------------------------- /iPanel.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.2.32526.322 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "iPanel", "iPanel\iPanel.csproj", "{F47C93EF-F309-4A45-9603-7C8DF6573FE5}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "iPanel.Tests", "iPanel.Tests\iPanel.Tests.csproj", "{CA09EE4B-C745-4CD3-B20C-346A5F1C7081}" 9 | EndProject 10 | Global 11 | EndGlobal 12 | -------------------------------------------------------------------------------- /iPanel/App.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | 5 | using iPanel.Core.Interaction; 6 | using iPanel.Core.Models.Settings; 7 | using iPanel.Core.Server; 8 | using iPanel.Core.Service; 9 | using iPanel.Utils; 10 | 11 | using Microsoft.Extensions.DependencyInjection; 12 | using Microsoft.Extensions.Hosting; 13 | using Microsoft.Extensions.Logging; 14 | 15 | namespace iPanel; 16 | 17 | public class App : IHost 18 | { 19 | private readonly IHost _host; 20 | public IServiceProvider Services => _host.Services; 21 | private ILogger Logger => Services.GetRequiredService>(); 22 | private HttpServer HttpServer => Services.GetRequiredService(); 23 | private Setting Setting => Services.GetRequiredService(); 24 | private UserManager UserManager => Services.GetRequiredService(); 25 | private InputReader InputReader => Services.GetRequiredService(); 26 | private ResourceFileManager ResourceFileManager => 27 | Services.GetRequiredService(); 28 | private readonly CancellationTokenSource _cancellationTokenSource; 29 | 30 | public App(IHost host) 31 | { 32 | _host = host; 33 | _cancellationTokenSource = new(); 34 | Setting.Check(); 35 | Logger.LogInformation("{}", Constant.Logo); 36 | 37 | SimpleLogger.StaticLogLevel = Setting.Debug 38 | ? Swan.Logging.LogLevel.Debug 39 | : Swan.Logging.LogLevel.Info; 40 | Console.CancelKeyPress += (_, _) => StopAsync(); 41 | 42 | ResourceFileManager.Release(); 43 | } 44 | 45 | public void Dispose() 46 | { 47 | _cancellationTokenSource.Dispose(); 48 | GC.SuppressFinalize(this); 49 | } 50 | 51 | public Task StartAsync(CancellationToken cancellationToken = default) 52 | { 53 | UserManager.Read(); 54 | Logger.LogInformation("启动完毕"); 55 | InputReader.Start(cancellationToken); 56 | HttpServer.Start(cancellationToken); 57 | 58 | Logger.LogInformation("讨论区/公告/Bug反馈:{}", "https://github.com/orgs/iPanelDev/discussions"); 59 | Logger.LogInformation("文档:{}", "https://ipaneldev.github.io/"); 60 | Logger.LogInformation("GitHub仓库:{}", "https://github.com/iPanelDev/iPanel"); 61 | 62 | return Task.CompletedTask; 63 | } 64 | 65 | public Task StopAsync(CancellationToken cancellationToken = default) 66 | { 67 | HttpServer.Dispose(); 68 | _cancellationTokenSource.Cancel(); 69 | Logger.LogInformation("Goodbye."); 70 | return Task.CompletedTask; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /iPanel/AppBuilder.cs: -------------------------------------------------------------------------------- 1 | using iPanel.Core.Interaction; 2 | using iPanel.Core.Models.Settings; 3 | using iPanel.Core.Server; 4 | using iPanel.Core.Server.WebSocket; 5 | using iPanel.Core.Service; 6 | using iPanel.Utils; 7 | 8 | using Microsoft.Extensions.DependencyInjection; 9 | using Microsoft.Extensions.Hosting; 10 | using Microsoft.Extensions.Logging; 11 | 12 | namespace iPanel; 13 | 14 | public class AppBuilder 15 | { 16 | private IServiceCollection Services => _hostAppBuilder.Services; 17 | 18 | private readonly SimpleLoggerProvider _loggerProvider; 19 | private readonly HostApplicationBuilder _hostAppBuilder; 20 | 21 | public AppBuilder(Setting setting) 22 | { 23 | _loggerProvider = new(); 24 | _hostAppBuilder = new HostApplicationBuilder(); 25 | _hostAppBuilder.Logging.ClearProviders(); 26 | _hostAppBuilder.Logging.AddProvider(_loggerProvider); 27 | 28 | Services.AddSingleton(setting); 29 | Services.AddSingleton(); 30 | Services.AddSingleton(); 31 | Services.AddSingleton(); 32 | Services.AddSingleton(); 33 | Services.AddSingleton(); 34 | Services.AddSingleton(); 35 | Services.AddSingleton(); 36 | Services.AddSingleton(); 37 | } 38 | 39 | public App Build() => new(_hostAppBuilder.Build()); 40 | } 41 | -------------------------------------------------------------------------------- /iPanel/Constant.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | 3 | namespace iPanel; 4 | 5 | public static class Constant 6 | { 7 | public static readonly string Version = Assembly 8 | .GetExecutingAssembly() 9 | .GetName() 10 | .Version!.ToString(); 11 | 12 | public const string Logo = 13 | @" _ ____ _ 14 | (_) _ \ __ _ _ __ ___| | 15 | | | |_) / _` | '_ \ / _ \ | 16 | | | __/ (_| | | | | __/ | 17 | |_|_| \__,_|_| |_|\___|_| 18 | "; 19 | } 20 | -------------------------------------------------------------------------------- /iPanel/Core/Interaction/Command.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using Microsoft.Extensions.Hosting; 4 | 5 | namespace iPanel.Core.Interaction; 6 | 7 | public abstract class Command 8 | { 9 | protected readonly IHost _host; 10 | protected IServiceProvider Services => _host.Services; 11 | 12 | protected Command(IHost host) 13 | { 14 | _host = host; 15 | } 16 | 17 | public abstract void Parse(string[] args); 18 | } 19 | -------------------------------------------------------------------------------- /iPanel/Core/Interaction/CommandDescriptionAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace iPanel.Core.Interaction; 4 | 5 | [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] 6 | public class CommandDescriptionAttribute : Attribute 7 | { 8 | public CommandDescriptionAttribute(string rootCommnad, string description) 9 | { 10 | RootCommand = rootCommnad; 11 | Description = description; 12 | } 13 | 14 | public string RootCommand { get; } 15 | 16 | public string Description { get; } 17 | 18 | public int Priority { get; init; } 19 | } 20 | -------------------------------------------------------------------------------- /iPanel/Core/Interaction/CommandUsageAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace iPanel.Core.Interaction; 4 | 5 | [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] 6 | public class CommandUsageAttribute : Attribute 7 | { 8 | public CommandUsageAttribute(string example, string description) 9 | { 10 | Example = example; 11 | Description = description; 12 | } 13 | 14 | public string Example { get; } 15 | 16 | public string Description { get; } 17 | } 18 | -------------------------------------------------------------------------------- /iPanel/Core/Interaction/Commands/ClearScreenCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using Microsoft.Extensions.Hosting; 4 | 5 | namespace iPanel.Core.Interaction.Commands; 6 | 7 | [CommandDescription("cls", "清屏")] 8 | public class ClearScreenCommand : Command 9 | { 10 | public ClearScreenCommand(IHost host) 11 | : base(host) { } 12 | 13 | public override void Parse(string[] args) 14 | { 15 | Console.Clear(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /iPanel/Core/Interaction/Commands/ExitCommand.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Hosting; 2 | 3 | namespace iPanel.Core.Interaction.Commands; 4 | 5 | [CommandDescription("exit", "关闭并退出", Priority = -114514)] 6 | public class ExitCommand : Command 7 | { 8 | public ExitCommand(IHost host) 9 | : base(host) { } 10 | 11 | public override void Parse(string[] args) 12 | { 13 | _host.StopAsync(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /iPanel/Core/Interaction/Commands/ListConnectionCommand.cs: -------------------------------------------------------------------------------- 1 | using iPanel.Core.Server.WebSocket; 2 | 3 | using Microsoft.Extensions.DependencyInjection; 4 | using Microsoft.Extensions.Hosting; 5 | using Microsoft.Extensions.Logging; 6 | 7 | using Spectre.Console; 8 | 9 | namespace iPanel.Core.Interaction.Commands; 10 | 11 | [CommandDescription("list", "列出所有实例", Priority = 3)] 12 | public class ListConnectionCommand : Command 13 | { 14 | public ListConnectionCommand(IHost host) 15 | : base(host) { } 16 | 17 | private ILogger Logger => 18 | Services.GetRequiredService>(); 19 | 20 | private InstanceWsModule InstanceWsModule => Services.GetRequiredService(); 21 | 22 | public override void Parse(string[] args) 23 | { 24 | Logger.LogInformation("当前有{}个实例在线", InstanceWsModule.Instances.Count); 25 | 26 | var table = new Table().AddColumns("地址", "自定义名称", "实例信息").RoundedBorder(); 27 | 28 | table.Columns[0].Centered(); 29 | table.Columns[1].Centered(); 30 | table.Columns[2].Centered(); 31 | 32 | lock (InstanceWsModule.Instances) 33 | foreach (var kv in InstanceWsModule.Instances) 34 | { 35 | table.AddRow( 36 | (kv.Value.Address ?? string.Empty).EscapeMarkup(), 37 | (kv.Value.CustomName ?? string.Empty).EscapeMarkup(), 38 | $"{kv.Value.Metadata?.Name ?? "未知名称"}({kv.Value.Metadata?.Version ?? "?"})".EscapeMarkup() 39 | ); 40 | } 41 | 42 | AnsiConsole.Write(table); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /iPanel/Core/Interaction/Commands/UserCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text.RegularExpressions; 5 | 6 | using iPanel.Core.Models.Users; 7 | using iPanel.Core.Service; 8 | 9 | using Microsoft.Extensions.DependencyInjection; 10 | using Microsoft.Extensions.Hosting; 11 | using Microsoft.Extensions.Logging; 12 | 13 | using Spectre.Console; 14 | 15 | namespace iPanel.Core.Interaction.Commands; 16 | 17 | [CommandDescription("user", "管理用户", Priority = 114514)] 18 | [CommandUsage("user create/remove/edit", "操作用户(此功能需要可交互的终端)")] 19 | [CommandUsage("user list", "列出所有用户")] 20 | [CommandUsage( 21 | "user create [description:string?]", 22 | "创建用户" 23 | )] 24 | [CommandUsage( 25 | "user edit [description:string?]", 26 | "编辑指定用户" 27 | )] 28 | [CommandUsage("user remove ", "删除指定用户")] 29 | public class UserCommand : Command 30 | { 31 | public UserCommand(IHost host) 32 | : base(host) { } 33 | 34 | private UserManager UserManager => Services.GetRequiredService(); 35 | 36 | private ILogger Logger => 37 | Services.GetRequiredService>(); 38 | 39 | private static readonly Dictionary _levelDescription = 40 | new() 41 | { 42 | { PermissionLevel.Guest, "游客: 禁止登录" }, 43 | { PermissionLevel.ReadOnly, "只读: 仅可查看" }, 44 | { PermissionLevel.Assistant, "助手: 允许控制服务器" }, 45 | { PermissionLevel.Administrator, "管理员: 允许控制服务器、新建修改删除用户" } 46 | }; 47 | 48 | public override void Parse(string[] args) 49 | { 50 | if (args.Length < 2) 51 | { 52 | Logger.LogError("语法错误:缺少子命令(可用值:create/remove/edit/list)"); 53 | return; 54 | } 55 | 56 | switch (args[1]) 57 | { 58 | case "list": 59 | var table = new Table() 60 | .AddColumns("用户名", "用户等级", "上一次登录时间", "最近登录IP", "描述") 61 | .RoundedBorder(); 62 | 63 | table.Columns[0].Centered(); 64 | table.Columns[1].Centered(); 65 | table.Columns[2].Centered(); 66 | table.Columns[3].Centered(); 67 | table.Columns[4].Centered(); 68 | lock (UserManager.Users) 69 | { 70 | Logger.LogInformation("当前共有{}个用户", UserManager.Users.Count); 71 | foreach (var kv in UserManager.Users) 72 | table.AddRow( 73 | kv.Key, 74 | UserManager.LevelNames[kv.Value.Level], 75 | kv.Value.LastLoginTime?.ToString() ?? string.Empty, 76 | (kv.Value.IPAddresses.FirstOrDefault() ?? string.Empty).EscapeMarkup(), 77 | kv.Value.Description.EscapeMarkup() 78 | ); 79 | AnsiConsole.Write(table); 80 | } 81 | break; 82 | 83 | case "create": 84 | if (args.Length == 2) 85 | Create(); 86 | else if (args.Length == 5 || args.Length == 6) 87 | Create(args); 88 | else 89 | Logger.LogError( 90 | "语法错误:参数数量不正确。正确格式:\"user create [description:string?]\"" 91 | ); 92 | break; 93 | 94 | case "edit": 95 | if (args.Length == 2) 96 | Edit(); 97 | else if (args.Length == 5 || args.Length == 6) 98 | Edit(args); 99 | else 100 | Logger.LogError( 101 | "语法错误:参数数量不正确。正确格式:\"user edit [description:string?]\"" 102 | ); 103 | break; 104 | 105 | case "remove": 106 | if (args.Length == 2) 107 | Remove(); 108 | else if (UserManager.Remove(args[2])) 109 | Logger.LogInformation("删除成功"); 110 | else 111 | Logger.LogError("删除失败:用户不存在"); 112 | 113 | break; 114 | 115 | default: 116 | Logger.LogError("语法错误:未知的子命令(可用值:create/remove/edit/list)"); 117 | break; 118 | } 119 | } 120 | 121 | private void Create() 122 | { 123 | if (!AnsiConsole.Profile.Capabilities.Interactive) 124 | { 125 | Logger.LogError( 126 | "当前终端不可交互。请使用\"user create [description:string?]\"" 127 | ); 128 | return; 129 | } 130 | 131 | var name = InputNewUserName(); 132 | var user = new User() 133 | { 134 | Password = InputPassword(), 135 | Level = SelectPermissionLevel(), 136 | Description = InputDescription(), 137 | }; 138 | 139 | if (!UserManager.Add(name, user)) 140 | Logger.LogError("创建失败"); 141 | else 142 | { 143 | Logger.LogInformation("创建成功"); 144 | AnsiConsole.Write( 145 | new Table() 146 | .AddColumns( 147 | new TableColumn("用户名") { Alignment = Justify.Center }, 148 | new(Markup.Escape(name)) { Alignment = Justify.Center } 149 | ) 150 | .AddRow("权限等级", UserManager.LevelNames[user.Level]) 151 | .AddRow("密码", string.Empty.PadRight(user.Password!.Length, '*')) 152 | .AddRow("描述", user.Description ?? string.Empty) 153 | .RoundedBorder() 154 | ); 155 | } 156 | } 157 | 158 | private void Create(string[] args) 159 | { 160 | if (!Regex.IsMatch(args[2], @"^[^\s\\""'@]{3,}$")) 161 | Logger.LogError("创建失败:用户名过短或含有特殊字符(\"'\\@)或空格"); 162 | else if (args[3].Length <= 6) 163 | Logger.LogError("创建失败:密码长度至少为6"); 164 | else if ( 165 | UserManager.Add( 166 | args[2], 167 | new() 168 | { 169 | Password = args[3], 170 | Level = Enum.TryParse(args[4], true, out PermissionLevel permissionLevel) 171 | ? permissionLevel 172 | : throw new ArgumentException( 173 | "无效的枚举值(可用值:Guest|ReadOnly|Assistant|Administrator)", 174 | nameof(args) 175 | ), 176 | Description = args.Length == 6 ? args[5] : string.Empty 177 | } 178 | ) 179 | ) 180 | Logger.LogInformation("创建成功"); 181 | else 182 | Logger.LogError("创建失败:用户名重复"); 183 | } 184 | 185 | private void Edit() 186 | { 187 | if (!AnsiConsole.Profile.Capabilities.Interactive) 188 | { 189 | Logger.LogError( 190 | "当前终端不可交互。请使用\"user edit [description:string?]\"" 191 | ); 192 | return; 193 | } 194 | 195 | var kv = SelectUser(true); 196 | kv.Value.Password = InputPassword(kv.Value.Password); 197 | kv.Value.Level = SelectPermissionLevel(); 198 | kv.Value.Description = InputDescription(kv.Value.Description); 199 | 200 | UserManager.Save(); 201 | Logger.LogInformation("编辑成功"); 202 | } 203 | 204 | private void Edit(string[] args) 205 | { 206 | if (!UserManager.Users.TryGetValue(args[2], out User? user)) 207 | Logger.LogError("修改失败:用户不存在"); 208 | else if (args[3].Length <= 6) 209 | Logger.LogError("修改失败:密码长度至少为6"); 210 | else 211 | { 212 | user.Password = args[3]; 213 | user.Level = Enum.TryParse(args[4], true, out PermissionLevel permissionLevel) 214 | ? permissionLevel 215 | : throw new ArgumentException( 216 | "无效的枚举值(可用值:Guest|ReadOnly|Assistant|Administrator)", 217 | nameof(args) 218 | ); 219 | user.Description = args.Length == 6 ? args[5] : string.Empty; 220 | UserManager.Save(); 221 | Logger.LogInformation("修改成功"); 222 | } 223 | } 224 | 225 | private void Remove() 226 | { 227 | if (!AnsiConsole.Profile.Capabilities.Interactive) 228 | { 229 | Logger.LogError("当前终端不可交互。请使用\"user remove \""); 230 | return; 231 | } 232 | 233 | var kv = SelectUser(true); 234 | UserManager.Remove(kv.Key); 235 | UserManager.Save(); 236 | Logger.LogInformation("删除成功"); 237 | } 238 | 239 | private KeyValuePair SelectUser(bool edit) 240 | { 241 | WriteDivider($"选择要{(edit ? "编辑" : "删除")}的用户"); 242 | AnsiConsole.MarkupLine("▪ 使用键盘的 [green]<↑>[/] 和 [green]<↓>[/] 进行选择"); 243 | AnsiConsole.MarkupLine("▪ 使用键盘的 [green][/] 或 [green][/] 进行确认"); 244 | AnsiConsole.WriteLine(); 245 | 246 | var keyValuePair = AnsiConsole.Prompt( 247 | new SelectionPrompt>() 248 | .AddChoices(UserManager.Users.ToArray()) 249 | .UseConverter( 250 | (kv) => 251 | $"{Markup.Escape(kv.Key)} [gray]({UserManager.LevelNames[kv.Value.Level]})[/]" 252 | ) 253 | ); 254 | 255 | AnsiConsole.MarkupLine( 256 | $"[blue]{Markup.Escape(keyValuePair.Key)} ({UserManager.LevelNames[keyValuePair.Value.Level]})[/]" 257 | ); 258 | return keyValuePair; 259 | } 260 | 261 | private static void WriteDivider(string text) 262 | { 263 | AnsiConsole.WriteLine(); 264 | AnsiConsole.Write(new Rule($"[white]{text}[/]").RuleStyle("grey").LeftJustified()); 265 | } 266 | 267 | private string InputNewUserName() 268 | { 269 | WriteDivider("用户名"); 270 | AnsiConsole.MarkupLine("▪ 长度应大于3"); 271 | AnsiConsole.MarkupLine("▪ 不含有空格[gray]([underline] [/])[/]"); 272 | AnsiConsole.MarkupLine("▪ 不含有特殊字符[gray]([underline]\"'\\@[/])[/]"); 273 | AnsiConsole.MarkupLine("▪ 不与已有的用户名重复"); 274 | AnsiConsole.WriteLine(); 275 | 276 | return AnsiConsole.Prompt( 277 | new TextPrompt(">").Validate( 278 | (line) => 279 | line.Length >= 3 280 | && Regex.IsMatch(line, @"^[^\s\\""'@]+$") 281 | && !UserManager.Users.ContainsKey(line), 282 | "[red]用户名不合上述要求[/]" 283 | ) 284 | ); 285 | } 286 | 287 | private static string InputPassword(string? defaultPwd = null) 288 | { 289 | WriteDivider("输入密码"); 290 | AnsiConsole.MarkupLine("▪ 长度应大于6"); 291 | AnsiConsole.MarkupLine("▪ 不含有空格[gray]([underline] [/])[/]"); 292 | AnsiConsole.MarkupLine("▪ 不含有敏感字符[gray]([underline]\"'\\[/])[/]"); 293 | AnsiConsole.MarkupLine("▪ 不建议于其他密码相同[gray](如服务器连接密码、QQ或微信密码)[/]"); 294 | AnsiConsole.MarkupLine("▪ 推荐大小写字母数字结合"); 295 | 296 | var prompt = new TextPrompt(">"); 297 | 298 | if (!string.IsNullOrEmpty(defaultPwd)) 299 | { 300 | prompt.DefaultValue(defaultPwd).HideDefaultValue(); 301 | AnsiConsole.MarkupLine("▪ 你可以使用键盘的 [green][/] 选择跳过"); 302 | } 303 | 304 | AnsiConsole.WriteLine(); 305 | prompt 306 | .Secret() 307 | .Validate( 308 | (line) => line.Length >= 6 && Regex.IsMatch(line, @"^[^\s\\""']+$"), 309 | "[red]密码不合上述要求[/]" 310 | ); 311 | 312 | return AnsiConsole.Prompt(prompt); 313 | } 314 | 315 | private static PermissionLevel SelectPermissionLevel() 316 | { 317 | WriteDivider("用户权限"); 318 | AnsiConsole.MarkupLine("▪ 使用键盘的 [green]<↑>[/] 和 [green]<↓>[/] 进行选择"); 319 | AnsiConsole.MarkupLine("▪ 使用键盘的 [green][/] 或 [green][/] 进行确认"); 320 | AnsiConsole.WriteLine(); 321 | 322 | var level = AnsiConsole.Prompt( 323 | new SelectionPrompt() 324 | .AddChoices(_levelDescription.Keys) 325 | .UseConverter((level) => _levelDescription[level]) 326 | ); 327 | AnsiConsole.MarkupLine("[blue]{0}[/]", _levelDescription[level]); 328 | return level; 329 | } 330 | 331 | private static string InputDescription(string? defaultValue = null) 332 | { 333 | WriteDivider("描述(可选)"); 334 | AnsiConsole.MarkupLine("▪ 你可以使用键盘的 [green][/] 选择跳过"); 335 | AnsiConsole.WriteLine(); 336 | 337 | var prompt = new TextPrompt(string.Empty); 338 | prompt.AllowEmpty(); 339 | prompt.HideDefaultValue(); 340 | 341 | if (!string.IsNullOrEmpty(defaultValue)) 342 | { 343 | prompt.DefaultValue(defaultValue); 344 | } 345 | return AnsiConsole.Prompt(prompt); 346 | } 347 | } 348 | -------------------------------------------------------------------------------- /iPanel/Core/Interaction/Commands/VersionCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Reflection; 4 | 5 | using iPanel.Utils; 6 | 7 | using Microsoft.Extensions.DependencyInjection; 8 | using Microsoft.Extensions.Hosting; 9 | using Microsoft.Extensions.Logging; 10 | 11 | using Spectre.Console; 12 | 13 | namespace iPanel.Core.Interaction.Commands; 14 | 15 | [CommandDescription("version", "显示详细的版本和版权信息", Priority = -1)] 16 | public class VersionCommand : Command 17 | { 18 | public VersionCommand(IHost host) 19 | : base(host) { } 20 | 21 | private ILogger Logger => 22 | Services.GetRequiredService>(); 23 | 24 | public override void Parse(string[] args) 25 | { 26 | Logger.LogInformation("Copyright (C) 2023 iPanelDev. All rights reserved."); 27 | var versionTable = new Table() 28 | .RoundedBorder() 29 | .AddColumns( 30 | new TableColumn("名称") { Alignment = Justify.Center }, 31 | new(Assembly.GetExecutingAssembly().GetName().Name ?? string.Empty) 32 | { 33 | Alignment = Justify.Center 34 | } 35 | ) 36 | .AddRow( 37 | "版本", 38 | Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? string.Empty 39 | ); 40 | var commandlineArgs = Environment.GetCommandLineArgs(); 41 | if (commandlineArgs.Length > 0 && File.Exists(commandlineArgs[0])) 42 | versionTable 43 | .AddRow("文件名", Path.GetFileName(commandlineArgs[0]).EscapeMarkup()) 44 | .AddRow("MD5", Encryption.GetMD5(File.ReadAllBytes(commandlineArgs[0]))) 45 | .AddRow("创建时间", File.GetCreationTime(commandlineArgs[0]).ToString("o")) 46 | .AddRow("修改时间", File.GetLastWriteTime(commandlineArgs[0]).ToString("o")); 47 | 48 | AnsiConsole.Write(versionTable); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /iPanel/Core/Interaction/InputReader.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics.CodeAnalysis; 4 | using System.Linq; 5 | using System.Reflection; 6 | using System.Text; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | 10 | using Microsoft.Extensions.DependencyInjection; 11 | using Microsoft.Extensions.Hosting; 12 | using Microsoft.Extensions.Logging; 13 | 14 | namespace iPanel.Core.Interaction; 15 | 16 | public class InputReader 17 | { 18 | private readonly IReadOnlyDictionary _commandParser; 19 | 20 | private readonly string _allCommands; 21 | private static readonly char[] _separator = new char[] { '\x20' }; 22 | private readonly IHost _host; 23 | private IServiceProvider Services => _host.Services; 24 | 25 | private ILogger Logger => Services.GetRequiredService>(); 26 | 27 | public InputReader(IHost host) 28 | { 29 | _host = host; 30 | 31 | var stringBuilder = new StringBuilder(); 32 | var attributes = new List<(CommandDescriptionAttribute, CommandUsageAttribute[])>(); 33 | var dict = new Dictionary(); 34 | 35 | stringBuilder.AppendLine($"iPanel {Constant.Version}"); 36 | foreach (var type in Assembly.GetExecutingAssembly().GetTypes()) 37 | { 38 | var attribute = type.GetCustomAttribute(); 39 | if (attribute is null) 40 | continue; 41 | 42 | var handler = (Command?)Activator.CreateInstance(type, _host); 43 | 44 | if (handler is null) 45 | continue; 46 | 47 | dict[attribute.RootCommand] = handler; 48 | attributes.Add( 49 | (attribute, type.GetCustomAttributes().ToArray()) 50 | ); 51 | } 52 | 53 | attributes.Sort((a, b) => b.Item1.Priority - a.Item1.Priority); 54 | foreach (var attributePair in attributes) 55 | { 56 | stringBuilder.AppendLine( 57 | $"▪ {attributePair.Item1.RootCommand} {attributePair.Item1.Description}" 58 | ); 59 | 60 | foreach (var usage in attributePair.Item2) 61 | stringBuilder.AppendLine($" ▪ {usage.Example} {usage.Description}"); 62 | } 63 | 64 | _allCommands = stringBuilder.ToString(); 65 | _commandParser = dict; 66 | } 67 | 68 | public void Start(CancellationToken cancellationToken) 69 | { 70 | Task.Run(() => ReadLine(cancellationToken), cancellationToken); 71 | } 72 | 73 | private void ReadLine(CancellationToken cancellationToken) 74 | { 75 | while (!cancellationToken.IsCancellationRequested) 76 | { 77 | var input = Console.ReadLine(); 78 | 79 | if (input is null) 80 | continue; 81 | 82 | if (Parse(input.Trim().TrimStart('/'), out var args)) 83 | Handle(args); 84 | else 85 | Logger.LogError("语法错误:含有未闭合的冒号(\")。若要作为参数的一部分传输,请使用\\\"进行转义"); 86 | } 87 | } 88 | 89 | public static bool Parse(string line, [NotNullWhen(true)] out List? result) 90 | { 91 | var args = new List(); 92 | 93 | if (!line.Contains('"') || !line.Contains(' ')) 94 | args.AddRange(line.Split(_separator, options: StringSplitOptions.RemoveEmptyEntries)); 95 | else 96 | { 97 | var inColon = false; 98 | var colonIndex = -1; 99 | 100 | var stringBuilder = new StringBuilder(); 101 | for (int i = 0; i < line.Length; i++) 102 | { 103 | var c = line[i]; 104 | 105 | switch (c) 106 | { 107 | case '\x20': 108 | if (!inColon) 109 | { 110 | args.Add(stringBuilder.ToString()); 111 | stringBuilder.Clear(); 112 | } 113 | else 114 | stringBuilder.Append(c); 115 | break; 116 | 117 | case '"': 118 | if (inColon && colonIndex > 0) 119 | colonIndex = -1; 120 | 121 | if (i > 1 && line[i - 1] != '\\') 122 | { 123 | inColon = !inColon; 124 | colonIndex = i; 125 | } 126 | else 127 | { 128 | stringBuilder.Remove(stringBuilder.Length - 1, 1); 129 | stringBuilder.Append(c); 130 | } 131 | 132 | break; 133 | 134 | default: 135 | stringBuilder.Append(c); 136 | break; 137 | } 138 | } 139 | 140 | if (inColon) 141 | { 142 | result = null; 143 | return false; 144 | } 145 | if (stringBuilder.Length != 0) 146 | args.Add(stringBuilder.ToString()); 147 | } 148 | result = args; 149 | return true; 150 | } 151 | 152 | private void Handle(List args) 153 | { 154 | if (args.Count == 0) 155 | { 156 | Logger.LogError("未知命令。请使用\"help\"查看所有命令"); 157 | return; 158 | } 159 | 160 | if (args[0] == "help" || args[0] == "?") 161 | Logger.LogInformation("{}", _allCommands); 162 | else if (!_commandParser.TryGetValue(args[0], out var parser)) 163 | Logger.LogError("未知命令。请使用\"help\"查看所有命令"); 164 | else 165 | try 166 | { 167 | parser.Parse(args.ToArray()); 168 | } 169 | catch (Exception e) 170 | { 171 | Logger.LogError(e, "解析命令时出现异常"); 172 | } 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /iPanel/Core/Models/Client/Client.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | using System.Threading.Tasks; 3 | 4 | using EmbedIO.WebSockets; 5 | 6 | using iPanel.Utils.Extensions; 7 | 8 | namespace iPanel.Core.Models.Client; 9 | 10 | public abstract class Client 11 | { 12 | [JsonIgnore] 13 | public IWebSocketContext Context { get; init; } = null!; 14 | 15 | public string? Address => Context.RemoteEndPoint.ToString(); 16 | 17 | public async Task SendAsync(string text) => await Context.SendAsync(text); 18 | } 19 | -------------------------------------------------------------------------------- /iPanel/Core/Models/Client/ConsoleListener.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | using EmbedIO.WebSockets; 4 | 5 | using iPanel.Core.Models.Users; 6 | using iPanel.Core.Server; 7 | 8 | namespace iPanel.Core.Models.Client; 9 | 10 | public class ConsoleListener : Client 11 | { 12 | [JsonIgnore] 13 | public User User; 14 | 15 | public string? InstanceIdSubscribed { get; set; } 16 | 17 | [JsonIgnore] 18 | public string ConnectionId { get; set; } 19 | 20 | public ConsoleListener(User user, IWebSocketContext context, string connectionId) 21 | { 22 | User = user; 23 | Context = context; 24 | ConnectionId = connectionId; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /iPanel/Core/Models/Client/Info/InstanceInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace iPanel.Core.Models.Client.Infos; 5 | 6 | public class InstanceInfo 7 | { 8 | public DateTime UpdateTime { get; } = DateTime.Now; 9 | 10 | [JsonRequired] 11 | public SystemInfo System { get; init; } = new(); 12 | 13 | [JsonRequired] 14 | public ServerInfo Server { get; init; } = new(); 15 | } 16 | -------------------------------------------------------------------------------- /iPanel/Core/Models/Client/Info/ServerInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace iPanel.Core.Models.Client.Infos; 4 | 5 | public class ServerInfo 6 | { 7 | public string? Filename { get; init; } 8 | 9 | [JsonRequired] 10 | public bool Status { get; init; } 11 | 12 | public string? RunTime { get; init; } 13 | 14 | public double Usage { get; init; } 15 | 16 | public int Capacity { get; set; } 17 | 18 | public int OnlinePlayers { get; set; } 19 | 20 | public string? Version { get; set; } 21 | } 22 | -------------------------------------------------------------------------------- /iPanel/Core/Models/Client/Info/SystemInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace iPanel.Core.Models.Client.Infos; 4 | 5 | public class SystemInfo 6 | { 7 | public string? OS { get; init; } 8 | 9 | public string? CPUName { get; init; } 10 | 11 | [JsonPropertyName("totalRam")] 12 | public double TotalRAM { get; init; } 13 | 14 | [JsonPropertyName("freeRam")] 15 | public double FreeRAM { get; init; } 16 | 17 | public double RAMUsage => TotalRAM == 0 ? 0 : (1 - FreeRAM / TotalRAM) * 100; 18 | 19 | public double CPUUsage { get; init; } 20 | } 21 | -------------------------------------------------------------------------------- /iPanel/Core/Models/Client/Instance.cs: -------------------------------------------------------------------------------- 1 | using iPanel.Core.Models.Client.Infos; 2 | 3 | namespace iPanel.Core.Models.Client; 4 | 5 | public class Instance : Client 6 | { 7 | public InstanceInfo Info { get; set; } = new(); 8 | 9 | public string? CustomName { get; set; } 10 | 11 | public string InstanceId { get; set; } 12 | 13 | public Metadata Metadata { get; set; } 14 | 15 | public Instance(string instanceId) 16 | { 17 | InstanceId = instanceId; 18 | Metadata ??= new(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /iPanel/Core/Models/Client/Metadata.cs: -------------------------------------------------------------------------------- 1 | namespace iPanel.Core.Models.Client; 2 | 3 | public class Metadata 4 | { 5 | public string? Version { get; init; } 6 | 7 | public string? Name { get; init; } 8 | 9 | public string? Environment { get; init; } 10 | } 11 | -------------------------------------------------------------------------------- /iPanel/Core/Models/Exceptions/PacketException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace iPanel.Core.Models.Exceptions; 4 | 5 | public class PacketException : Exception 6 | { 7 | public PacketException(string? message) 8 | : base(message) { } 9 | } 10 | -------------------------------------------------------------------------------- /iPanel/Core/Models/Exceptions/SettingsException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace iPanel.Core.Models.Exceptions; 4 | 5 | public class SettingsException : Exception 6 | { 7 | public SettingsException(string? message) 8 | : base(message) { } 9 | } 10 | -------------------------------------------------------------------------------- /iPanel/Core/Models/Packets/ApiPacket.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace iPanel.Core.Models.Packets; 4 | 5 | public class ApiPacket 6 | where T : notnull 7 | { 8 | public int Code { get; init; } = 200; 9 | 10 | public string? ErrorMsg { get; init; } 11 | 12 | public T? Data { get; init; } 13 | 14 | public DateTime Time { get; } = DateTime.Now; 15 | } 16 | 17 | public class ApiPacket : ApiPacket { } 18 | -------------------------------------------------------------------------------- /iPanel/Core/Models/Packets/Datas/DirInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | 4 | namespace iPanel.Core.Models.Packets.Data; 5 | 6 | public class DirInfo 7 | { 8 | public FileItem[] Items { get; init; } 9 | 10 | public bool IsExist { get; init; } 11 | 12 | public string? Path { get; init; } 13 | 14 | public DirInfo() 15 | { 16 | Items = 17 | Items?.Where((item) => item?.Type == "file" || item?.Type == "dir").ToArray() 18 | ?? Array.Empty(); 19 | } 20 | 21 | public class FileItem 22 | { 23 | public string? Type { get; init; } 24 | 25 | public string? Path { get; init; } 26 | 27 | public string? Name { get; init; } 28 | 29 | public long? Size { get; init; } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /iPanel/Core/Models/Packets/Datas/Result.cs: -------------------------------------------------------------------------------- 1 | using iPanel.Core.Models.Packets.Event; 2 | 3 | namespace iPanel.Core.Models.Packets.Data; 4 | 5 | public class Result 6 | { 7 | public bool? Success { get; init; } 8 | 9 | public int? Code { get; init; } 10 | 11 | public string? Reason { get; init; } 12 | 13 | public Result(string? reason) 14 | { 15 | Reason = reason; 16 | Success = null; 17 | } 18 | 19 | public Result(string? reason, bool success) 20 | { 21 | Reason = reason; 22 | Success = success; 23 | } 24 | 25 | public Result(ResultTypes resultTypes) 26 | { 27 | Reason = resultTypes.ToString(); 28 | Code = (int)resultTypes; 29 | Success = resultTypes == ResultTypes.Success; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /iPanel/Core/Models/Packets/Datas/Status.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using iPanel.Core.Models.Users; 4 | 5 | namespace iPanel.Core.Models.Packets.Data; 6 | 7 | public class Status 8 | { 9 | public bool Logined { get; init; } 10 | 11 | public TimeSpan SessionDuration { get; init; } 12 | 13 | public UserWithoutPwd? User { get; init; } 14 | } 15 | -------------------------------------------------------------------------------- /iPanel/Core/Models/Packets/Datas/VerifyBody.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | using iPanel.Core.Models.Client; 4 | 5 | namespace iPanel.Core.Models.Packets.Data; 6 | 7 | public class VerifyBody 8 | { 9 | [JsonPropertyName("md5")] 10 | [JsonRequired] 11 | public string MD5 { get; init; } = string.Empty; 12 | 13 | [JsonRequired] 14 | public string Time { get; init; } = string.Empty; 15 | 16 | public string? CustomName { get; init; } 17 | 18 | public string? InstanceId { get; init; } 19 | 20 | public Metadata? Metadata { get; init; } 21 | 22 | public string? UserName { get; init; } 23 | } 24 | -------------------------------------------------------------------------------- /iPanel/Core/Models/Packets/Event/InvalidDataPacket.cs: -------------------------------------------------------------------------------- 1 | using iPanel.Core.Models.Packets.Data; 2 | 3 | namespace iPanel.Core.Models.Packets.Event; 4 | 5 | public class InvalidDataPacket : WsSentPacket 6 | { 7 | public InvalidDataPacket(string reason) 8 | : base("event", "invalid_data", new Result(reason)) { } 9 | } 10 | -------------------------------------------------------------------------------- /iPanel/Core/Models/Packets/Event/InvalidParamPacket.cs: -------------------------------------------------------------------------------- 1 | using iPanel.Core.Models.Packets.Data; 2 | 3 | namespace iPanel.Core.Models.Packets.Event; 4 | 5 | public class InvalidParamPacket : WsSentPacket 6 | { 7 | public InvalidParamPacket(string reason) 8 | : base("event", "invalid_param", new Result(reason)) { } 9 | } 10 | -------------------------------------------------------------------------------- /iPanel/Core/Models/Packets/Event/ResultTypes.cs: -------------------------------------------------------------------------------- 1 | namespace iPanel.Core.Models.Packets.Event; 2 | 3 | public enum ResultTypes 4 | { 5 | Unknown = -1, 6 | Success, 7 | DataAnomaly, 8 | DuplicateInstanceID, 9 | EmptyUserName, 10 | ErrorWhenGettingPacketContent, 11 | FailToVerify, 12 | IncorrectUserNameOrPassword, 13 | IncorrectClientType, 14 | IncorrectInstanceID, 15 | InternalDataError, 16 | InvalidArgs, 17 | InvalidConsole, 18 | InvalidSubscription, 19 | InvalidUser, 20 | LostArgs, 21 | NotVerifyYet, 22 | PermissionDenied, 23 | TimeoutInVerification, 24 | Timeout 25 | } 26 | -------------------------------------------------------------------------------- /iPanel/Core/Models/Packets/Event/VerifyResultPacket.cs: -------------------------------------------------------------------------------- 1 | using iPanel.Core.Models.Packets.Data; 2 | 3 | namespace iPanel.Core.Models.Packets.Event; 4 | 5 | public class VerifyResultPacket : WsSentPacket 6 | { 7 | public VerifyResultPacket(bool success, string? reason = null) 8 | : base("event", "verify_result", new Result(reason, success)) { } 9 | } 10 | -------------------------------------------------------------------------------- /iPanel/Core/Models/Packets/Sender.cs: -------------------------------------------------------------------------------- 1 | using iPanel.Core.Models.Client; 2 | 3 | namespace iPanel.Core.Models.Packets; 4 | 5 | public class Sender 6 | { 7 | public string? Name { get; init; } 8 | 9 | public string Type { get; init; } 10 | 11 | public string? Address { get; init; } 12 | 13 | public Metadata? Metadata { get; init; } 14 | 15 | public string? InstanceId { get; init; } 16 | 17 | protected Sender() 18 | { 19 | Type = "unknown"; 20 | } 21 | 22 | public Sender(string name, string type, string? address, Metadata metadata) 23 | { 24 | Name = name; 25 | Type = type; 26 | Address = address; 27 | Metadata = metadata; 28 | } 29 | 30 | public static Sender From(Instance instance) => 31 | new() 32 | { 33 | Name = instance.CustomName, 34 | Type = "instance", 35 | Address = instance.Address, 36 | InstanceId = instance.InstanceId, 37 | Metadata = instance.Metadata 38 | }; 39 | 40 | public static Sender CreateUserSender(string? userName) => 41 | new() { Type = "user", Name = userName }; 42 | } 43 | -------------------------------------------------------------------------------- /iPanel/Core/Models/Packets/WsPacket.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.Serialization; 3 | 4 | using iPanel.Utils.Json; 5 | 6 | namespace iPanel.Core.Models.Packets; 7 | 8 | public abstract class WsPacket 9 | { 10 | [JsonRequired] 11 | public string Type { get; init; } = string.Empty; 12 | 13 | [JsonRequired] 14 | public string SubType { get; init; } = string.Empty; 15 | 16 | public T? Data { get; init; } 17 | 18 | public Sender? Sender { get; init; } 19 | 20 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] 21 | public string? RequestId { get; init; } 22 | 23 | public override string ToString() => 24 | JsonSerializer.Serialize(this, JsonSerializerOptionsFactory.CamelCase); 25 | 26 | public static implicit operator string(WsPacket packet) => packet.ToString(); 27 | } 28 | -------------------------------------------------------------------------------- /iPanel/Core/Models/Packets/WsReceivedPacket.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Nodes; 2 | 3 | namespace iPanel.Core.Models.Packets; 4 | 5 | public sealed class WsReceivedPacket : WsPacket { } 6 | -------------------------------------------------------------------------------- /iPanel/Core/Models/Packets/WsSentPacket.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace iPanel.Core.Models.Packets; 4 | 5 | public class WsSentPacket : WsPacket 6 | { 7 | public DateTime Time { get; } = DateTime.Now; 8 | 9 | private static readonly Sender _selfSender = 10 | new( 11 | $"iPanel", 12 | "host", 13 | null, 14 | new() 15 | { 16 | Version = Constant.Version, 17 | Environment = Environment.Version.ToString(), 18 | Name = "iPanel" 19 | } 20 | ); 21 | 22 | public WsSentPacket() { } 23 | 24 | public WsSentPacket(string type, string sub_type, object? data = null) 25 | : this(type, sub_type, data, _selfSender) { } 26 | 27 | public WsSentPacket(string type, string sub_type, object? data, Sender sender) 28 | { 29 | Type = type; 30 | SubType = sub_type; 31 | Data = data; 32 | Sender = sender; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /iPanel/Core/Models/Request.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.Json.Nodes; 3 | 4 | namespace iPanel.Core.Models; 5 | 6 | public class Request 7 | { 8 | public string InstanceId { get; set; } 9 | 10 | public DateTime StartTime { get; } = DateTime.Now; 11 | 12 | public JsonNode? Data { get; set; } 13 | 14 | public bool HasReceived { get; set; } 15 | 16 | public Request(string instanceId) 17 | { 18 | InstanceId = instanceId; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /iPanel/Core/Models/Settings/CertificateSettings.cs: -------------------------------------------------------------------------------- 1 | namespace iPanel.Core.Models.Settings; 2 | 3 | public class CertificateSettings 4 | { 5 | public bool Enable { get; init; } 6 | 7 | public bool AutoRegisterCertificate { get; init; } 8 | 9 | public bool AutoLoadCertificate { get; init; } 10 | 11 | public string? Path { get; init; } 12 | 13 | public string? Password { get; init; } 14 | } 15 | -------------------------------------------------------------------------------- /iPanel/Core/Models/Settings/Setting.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using iPanel.Core.Models.Exceptions; 4 | 5 | using Swan.Logging; 6 | 7 | namespace iPanel.Core.Models.Settings; 8 | 9 | public class Setting 10 | { 11 | public bool Debug { get; init; } 12 | 13 | public string InstancePassword { get; init; } = string.Empty; 14 | 15 | public WebServerSetting WebServer { get; init; } = new(); 16 | 17 | public void Check() 18 | { 19 | if (WebServer is null) 20 | Throw($"{nameof(WebServer)}数据异常"); 21 | 22 | if (string.IsNullOrEmpty(InstancePassword)) 23 | Throw($"{nameof(InstancePassword)}为空"); 24 | 25 | if (WebServer!.UrlPrefixes is null) 26 | Throw($"{nameof(WebServer.UrlPrefixes)}为null"); 27 | 28 | if (WebServer.UrlPrefixes!.Length == 0) 29 | Throw($"{nameof(WebServer.UrlPrefixes)}为空。你至少应该设置一个"); 30 | 31 | if (string.IsNullOrEmpty(WebServer.Directory)) 32 | Throw($"{nameof(WebServer.Directory)}为空"); 33 | 34 | if (string.IsNullOrEmpty(WebServer.Page404)) 35 | Throw($"{nameof(WebServer.Page404)}为空"); 36 | 37 | if (WebServer.MaxRequestsPerSecond <= 0) 38 | Throw($"{nameof(WebServer.MaxRequestsPerSecond)}超出范围"); 39 | 40 | if (WebServer.Certificate is null) 41 | Throw($"{nameof(WebServer.Certificate)}为null"); 42 | } 43 | 44 | private static void Throw(string message) => throw new SettingsException(message); 45 | } 46 | -------------------------------------------------------------------------------- /iPanel/Core/Models/Settings/WebServerSetting.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace iPanel.Core.Models.Settings; 4 | 5 | public class WebServerSetting 6 | { 7 | public string[] UrlPrefixes { get; init; } = { "http://127.0.0.1:30000" }; 8 | 9 | public string Directory { get; init; } = "dist"; 10 | 11 | public bool DisableFilesHotUpdate { get; init; } = true; 12 | 13 | public string Page404 { get; init; } = "index.html"; 14 | 15 | public bool AllowCrossOrigin { get; init; } 16 | 17 | public int MaxRequestsPerSecond { get; init; } = 50; 18 | 19 | public string[] WhiteList { get; init; } = Array.Empty(); 20 | 21 | public CertificateSettings Certificate { get; init; } = new(); 22 | } 23 | -------------------------------------------------------------------------------- /iPanel/Core/Models/Users/PermissionLevel.cs: -------------------------------------------------------------------------------- 1 | namespace iPanel.Core.Models.Users; 2 | 3 | public enum PermissionLevel 4 | { 5 | Guest, 6 | 7 | ReadOnly, 8 | 9 | Assistant, 10 | 11 | Administrator, 12 | } 13 | -------------------------------------------------------------------------------- /iPanel/Core/Models/Users/User.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace iPanel.Core.Models.Users; 5 | 6 | public class User : UserWithoutPwd 7 | { 8 | public string? Password { get; set; } 9 | 10 | public User() 11 | { 12 | IPAddresses ??= new(); 13 | Instances ??= Array.Empty(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /iPanel/Core/Models/Users/UserWithoutPwd.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace iPanel.Core.Models.Users; 5 | 6 | public class UserWithoutPwd 7 | { 8 | public DateTime? LastLoginTime { get; set; } 9 | 10 | public PermissionLevel Level { get; set; } 11 | 12 | public string[] Instances { get; set; } = Array.Empty(); 13 | 14 | public string Description { get; set; } = string.Empty; 15 | 16 | public List IPAddresses { get; set; } = new(); 17 | } 18 | -------------------------------------------------------------------------------- /iPanel/Core/Server/Api/ApiHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Net; 4 | using System.Text.Json; 5 | using System.Text.RegularExpressions; 6 | using System.Threading.Tasks; 7 | 8 | using EmbedIO; 9 | using EmbedIO.Sessions; 10 | 11 | using iPanel.Core.Models.Packets; 12 | using iPanel.Core.Models.Users; 13 | using iPanel.Utils; 14 | using iPanel.Utils.Json; 15 | 16 | namespace iPanel.Core.Server.Api; 17 | 18 | public static class ApiHelper 19 | { 20 | public static async Task ConvertRequestTo(this IHttpContext httpContext) 21 | where T : notnull 22 | { 23 | if (httpContext.Request.HttpVerb is not HttpVerbs.Get or HttpVerbs.Head) 24 | { 25 | if (httpContext.Request.ContentType != "application/json") 26 | throw HttpException.BadRequest("不支持的\"ContentType\""); 27 | 28 | try 29 | { 30 | return JsonSerializer.Deserialize( 31 | await httpContext.GetRequestBodyAsStringAsync(), 32 | JsonSerializerOptionsFactory.CamelCase 33 | ); 34 | } 35 | catch (Exception e) 36 | { 37 | throw HttpException.BadRequest(e.Message); 38 | } 39 | } 40 | 41 | throw HttpException.MethodNotAllowed(); 42 | } 43 | 44 | public static bool IsLogined(this IHttpContext httpContext) => 45 | httpContext.Session.TryGetValue(SessionKeyConstants.User, out object? value) 46 | && value is User user 47 | && user is not null 48 | && user.Level != PermissionLevel.Guest; 49 | 50 | public static UserWithoutPwd EnsureLogined(this IHttpContext httpContext) 51 | { 52 | if (!httpContext.IsLogined()) 53 | throw HttpException.Unauthorized(); 54 | 55 | return (httpContext.Session[SessionKeyConstants.User] as User)!; 56 | } 57 | 58 | public static void EnsureLevel(this IHttpContext httpContext, PermissionLevel permissionLevel) 59 | { 60 | if ( 61 | !httpContext.Session.TryGetValue(SessionKeyConstants.User, out User? user) 62 | || user is null 63 | || user.Level == PermissionLevel.Guest 64 | ) 65 | throw HttpException.Unauthorized(); 66 | 67 | if (user.Level < permissionLevel) 68 | throw HttpException.Forbidden("权限不足"); 69 | } 70 | 71 | public static void EnsureAccess( 72 | this IHttpContext httpContext, 73 | string instanceId, 74 | bool strict = true 75 | ) 76 | { 77 | if ( 78 | !httpContext.Session.TryGetValue(SessionKeyConstants.User, out User? user) 79 | || user is null 80 | || user.Level == PermissionLevel.Guest 81 | ) 82 | throw HttpException.Unauthorized(); 83 | 84 | if ( 85 | user.Level != PermissionLevel.Administrator 86 | && (!user.Instances.Contains(instanceId) || user.Level != PermissionLevel.Assistant) 87 | && ( 88 | !user.Instances.Contains(instanceId) 89 | || user.Level != PermissionLevel.ReadOnly 90 | || strict 91 | ) 92 | ) 93 | throw HttpException.Forbidden("权限不足"); 94 | } 95 | 96 | private static async Task SendJsonAsync(this IHttpContext httpContext, ApiPacket packet) 97 | where T : notnull 98 | { 99 | httpContext.Response.StatusCode = packet.Code; 100 | await httpContext.SendStringAsync( 101 | JsonSerializer.Serialize(packet, JsonSerializerOptionsFactory.CamelCase), 102 | "text/json", 103 | EncodingsMap.UTF8 104 | ); 105 | 106 | httpContext.SetHandled(); 107 | } 108 | 109 | private static async Task SendPacketAsync(this IHttpContext httpContext, ApiPacket packet) => 110 | await SendJsonAsync(httpContext, packet); 111 | 112 | public static async Task SendPacketAsync( 113 | this IHttpContext httpContext, 114 | T? data = default, 115 | HttpStatusCode statusCode = HttpStatusCode.OK 116 | ) 117 | where T : notnull => 118 | await SendJsonAsync( 119 | httpContext, 120 | new ApiPacket() { Data = data, Code = (int)statusCode } 121 | ); 122 | 123 | public static async Task SendPacketAsync( 124 | this IHttpContext httpContext, 125 | HttpStatusCode statusCode = HttpStatusCode.OK 126 | ) => await SendPacketAsync(httpContext, new ApiPacket() { Code = (int)statusCode }); 127 | 128 | public static async Task HandleHttpException(IHttpContext context, IHttpException exception) 129 | { 130 | var httpStatusCode = (HttpStatusCode)exception.StatusCode; 131 | await context.SendPacketAsync( 132 | new ApiPacket 133 | { 134 | ErrorMsg = 135 | exception.Message 136 | ?? Regex.Replace( 137 | httpStatusCode.ToString(), 138 | @"^[A-Z]", 139 | (c) => c.Value.ToLower() 140 | ), 141 | Code = exception.StatusCode 142 | } 143 | ); 144 | } 145 | 146 | public static async Task HandleException(IHttpContext context, Exception e) 147 | { 148 | if (context.IsLogined()) 149 | await context.SendPacketAsync(new ApiPacket { ErrorMsg = $"{e.GetType()}", Code = 500 }); 150 | else 151 | await context.SendPacketAsync( 152 | new ApiPacket { ErrorMsg = $"{e.GetType()}:{e.Message}", Code = 500 } 153 | ); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /iPanel/Core/Server/Api/ApiMap.Instances.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Net; 4 | using System.Threading.Tasks; 5 | 6 | using EmbedIO; 7 | using EmbedIO.Routing; 8 | using EmbedIO.WebApi; 9 | 10 | using iPanel.Core.Models.Client; 11 | using iPanel.Core.Models.Packets; 12 | using iPanel.Core.Models.Users; 13 | 14 | namespace iPanel.Core.Server.Api; 15 | 16 | public partial class ApiMap 17 | { 18 | [Route(HttpVerbs.Get, "/instances")] 19 | public async Task GetAllInstances() 20 | { 21 | var user = HttpContext.EnsureLogined(); 22 | await HttpContext.SendPacketAsync( 23 | InstanceWsModule.Instances.Values 24 | .Where( 25 | (instance) => 26 | user.Level == PermissionLevel.Administrator 27 | || ( 28 | user.Level == PermissionLevel.Assistant 29 | || user.Level == PermissionLevel.ReadOnly 30 | ) && user.Instances.Contains(instance.InstanceId) 31 | ) 32 | .ToArray() 33 | ); 34 | } 35 | 36 | [Route(HttpVerbs.Get, "/instances/{instanceId}")] 37 | public async Task GetInstance(string instanceId) 38 | { 39 | HttpContext.EnsureAccess(instanceId, false); 40 | 41 | if ( 42 | InstanceWsModule.Instances.TryGetValue(instanceId, out Instance? instance) 43 | && instance is not null 44 | ) 45 | await HttpContext.SendPacketAsync(instance); 46 | else 47 | throw HttpException.NotFound("实例不存在"); 48 | } 49 | 50 | [Route(HttpVerbs.Get, "/instances/{instanceId}/subscribe")] 51 | public async Task SubscribeInstance(string instanceId, [QueryField(true)] string connectionId) 52 | { 53 | HttpContext.EnsureAccess(instanceId, false); 54 | 55 | if (!BroadcastWsModule.Listeners.TryGetValue(connectionId, out var listener)) 56 | throw HttpException.BadRequest("未连接到广播WebSocket服务器"); 57 | 58 | if (InstanceWsModule.Instances.ContainsKey(instanceId)) 59 | { 60 | listener.InstanceIdSubscribed = instanceId; 61 | await HttpContext.SendPacketAsync(); 62 | } 63 | else 64 | throw HttpException.NotFound("实例不存在"); 65 | } 66 | 67 | [Route(HttpVerbs.Get, "/instances/{instanceId}/start")] 68 | public async Task CallInstanceStart(string instanceId) 69 | { 70 | HttpContext.EnsureAccess(instanceId); 71 | 72 | if ( 73 | InstanceWsModule.Instances.TryGetValue(instanceId, out Instance? instance) 74 | && instance is not null 75 | ) 76 | { 77 | await instance.SendAsync( 78 | new WsSentPacket( 79 | "request", 80 | "server_start", 81 | null, 82 | Sender.CreateUserSender( 83 | HttpContext.Session[SessionKeyConstants.UserName]?.ToString() 84 | ) 85 | ) 86 | ); 87 | await HttpContext.SendPacketAsync(HttpStatusCode.Accepted); 88 | } 89 | else 90 | throw HttpException.NotFound("实例不存在"); 91 | } 92 | 93 | [Route(HttpVerbs.Get, "/instances/{instanceId}/stop")] 94 | public async Task CallInstanceStop(string instanceId) 95 | { 96 | HttpContext.EnsureAccess(instanceId); 97 | 98 | if ( 99 | InstanceWsModule.Instances.TryGetValue(instanceId, out Instance? instance) 100 | && instance is not null 101 | ) 102 | { 103 | await instance.SendAsync( 104 | new WsSentPacket( 105 | "request", 106 | "server_stop", 107 | null, 108 | Sender.CreateUserSender( 109 | HttpContext.Session[SessionKeyConstants.UserName]?.ToString() 110 | ) 111 | ) 112 | ); 113 | await HttpContext.SendPacketAsync(HttpStatusCode.Accepted); 114 | } 115 | else 116 | throw HttpException.NotFound("实例不存在"); 117 | } 118 | 119 | [Route(HttpVerbs.Get, "/instances/{instanceId}/kill")] 120 | public async Task CallInstanceKill(string instanceId) 121 | { 122 | HttpContext.EnsureAccess(instanceId); 123 | 124 | if ( 125 | InstanceWsModule.Instances.TryGetValue(instanceId, out Instance? instance) 126 | && instance is not null 127 | ) 128 | { 129 | await instance.SendAsync( 130 | new WsSentPacket( 131 | "request", 132 | "server_kill", 133 | null, 134 | Sender.CreateUserSender( 135 | HttpContext.Session[SessionKeyConstants.UserName]?.ToString() 136 | ) 137 | ) 138 | ); 139 | await HttpContext.SendPacketAsync(HttpStatusCode.Accepted); 140 | } 141 | else 142 | throw HttpException.NotFound("实例不存在"); 143 | } 144 | 145 | [Route(HttpVerbs.Post, "/instances/{instanceId}/input")] 146 | public async Task CallInstanceInput(string instanceId) 147 | { 148 | HttpContext.EnsureAccess(instanceId); 149 | 150 | var inputs = await HttpContext.ConvertRequestTo(); 151 | if (inputs is null || inputs.Length == 0) 152 | throw HttpException.BadRequest("缺少输入数据"); 153 | 154 | if ( 155 | InstanceWsModule.Instances.TryGetValue(instanceId, out Instance? instance) 156 | && instance is not null 157 | ) 158 | { 159 | await instance.SendAsync( 160 | new WsSentPacket( 161 | "request", 162 | "server_input", 163 | inputs, 164 | Sender.CreateUserSender( 165 | HttpContext.Session[SessionKeyConstants.UserName]?.ToString() 166 | ) 167 | ) 168 | ); 169 | await HttpContext.SendPacketAsync(HttpStatusCode.Accepted); 170 | } 171 | else 172 | throw HttpException.NotFound("实例不存在"); 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /iPanel/Core/Server/Api/ApiMap.Main.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using System.Threading.Tasks; 4 | 5 | using EmbedIO; 6 | using EmbedIO.Routing; 7 | using EmbedIO.WebApi; 8 | 9 | using iPanel.Core.Server.WebSocket; 10 | using iPanel.Core.Service; 11 | 12 | using Microsoft.Extensions.DependencyInjection; 13 | using Microsoft.Extensions.Hosting; 14 | using Microsoft.Extensions.Logging; 15 | 16 | namespace iPanel.Core.Server.Api; 17 | 18 | public partial class ApiMap : WebApiController 19 | { 20 | private readonly IHost _host; 21 | private IServiceProvider Services => _host.Services; 22 | private ILogger Logger => Services.GetRequiredService>(); 23 | private UserManager UserManager => Services.GetRequiredService(); 24 | private InstanceWsModule InstanceWsModule => Services.GetRequiredService(); 25 | private BroadcastWsModule BroadcastWsModule => Services.GetRequiredService(); 26 | 27 | public ApiMap(IHost host) 28 | { 29 | _host = host; 30 | } 31 | 32 | [Route(HttpVerbs.Get, "/")] 33 | public async Task Root() 34 | { 35 | await HttpContext.SendPacketAsync("Hello world. :)"); 36 | } 37 | 38 | [Route(HttpVerbs.Get, "/version")] 39 | public async Task GetVersion() 40 | { 41 | await HttpContext.SendPacketAsync(Constant.Version); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /iPanel/Core/Server/Api/ApiMap.Users.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net; 5 | using System.Threading.Tasks; 6 | 7 | using EmbedIO; 8 | using EmbedIO.Routing; 9 | using EmbedIO.Sessions; 10 | using EmbedIO.Utilities; 11 | 12 | using iPanel.Core.Models.Packets.Data; 13 | using iPanel.Core.Models.Users; 14 | using iPanel.Core.Service; 15 | using iPanel.Utils; 16 | 17 | using Microsoft.Extensions.Logging; 18 | 19 | namespace iPanel.Core.Server.Api; 20 | 21 | public partial class ApiMap 22 | { 23 | [Route(HttpVerbs.Get, "/users/@self/status")] 24 | public async Task Status() 25 | { 26 | if (HttpContext.IsLogined()) 27 | { 28 | await HttpContext.SendPacketAsync( 29 | new Status 30 | { 31 | Logined = true, 32 | SessionDuration = HttpContext.Session.Duration, 33 | User = (HttpContext.Session[SessionKeyConstants.User] as User)!, 34 | } 35 | ); 36 | return; 37 | } 38 | 39 | await HttpContext.SendPacketAsync(new Status { Logined = false }); 40 | } 41 | 42 | [Route(HttpVerbs.Post, "/users/@self/login")] 43 | public async Task Login() 44 | { 45 | if (HttpContext.IsLogined()) 46 | { 47 | await HttpContext.SendPacketAsync( 48 | new Status 49 | { 50 | Logined = true, 51 | SessionDuration = HttpContext.Session.Duration, 52 | User = (HttpContext.Session[SessionKeyConstants.User] as User)!, 53 | } 54 | ); 55 | return; 56 | } 57 | 58 | var verifyBody = 59 | await HttpContext.ConvertRequestTo() ?? throw HttpException.BadRequest(); 60 | 61 | if (string.IsNullOrEmpty(verifyBody.UserName)) 62 | throw HttpException.BadRequest("用户名为空"); 63 | 64 | if ( 65 | string.IsNullOrEmpty(verifyBody.Time) 66 | || !DateTime.TryParse(verifyBody.Time, out DateTime dateTime) 67 | ) 68 | throw HttpException.BadRequest("\"time\"无效"); 69 | 70 | var span = dateTime - DateTime.Now; 71 | if (span.TotalSeconds < -10 || span.TotalMinutes > 10) 72 | throw HttpException.BadRequest("\"time\"已过期"); 73 | 74 | if ( 75 | !UserManager.Users.TryGetValue(verifyBody.UserName!, out User? user) 76 | || verifyBody.MD5 77 | != Encryption.GetMD5($"{verifyBody.Time}.{verifyBody.UserName}.{user.Password}") 78 | ) 79 | throw HttpException.Forbidden("用户名或密码错误"); 80 | 81 | if (user.Level == PermissionLevel.Guest) 82 | throw HttpException.Forbidden("用户无效"); 83 | 84 | user.LastLoginTime = DateTime.Now; 85 | 86 | var address = HttpContext.RemoteEndPoint.Address.ToString(); 87 | user.IPAddresses.Remove(address); 88 | user.IPAddresses.Insert(0, address); 89 | if (user.IPAddresses.Count > 10) 90 | user.IPAddresses.RemoveRange(10, user.IPAddresses.Count - 10); 91 | 92 | HttpContext.Session[SessionKeyConstants.User] = user; 93 | HttpContext.Session[SessionKeyConstants.UserName] = verifyBody.UserName; 94 | 95 | Logger.LogInformation("[{}] 登录成功", HttpContext.Id); 96 | 97 | await HttpContext.SendPacketAsync( 98 | new Status 99 | { 100 | Logined = true, 101 | SessionDuration = HttpContext.Session.Duration, 102 | User = (HttpContext.Session[SessionKeyConstants.User] as User)!, 103 | } 104 | ); 105 | } 106 | 107 | [Route(HttpVerbs.Get, "/users/@self/logout")] 108 | public async Task Logout() 109 | { 110 | HttpContext.EnsureLogined(); 111 | HttpContext.Session.Delete(); 112 | await HttpContext.SendPacketAsync(); 113 | Logger.LogInformation("[{}] 退出成功", HttpContext.Id); 114 | } 115 | 116 | [Route(HttpVerbs.Get, "/users")] 117 | public async Task ListUsers() 118 | { 119 | HttpContext.EnsureLevel(PermissionLevel.Administrator); 120 | 121 | await HttpContext.SendPacketAsync( 122 | UserManager.Users 123 | .Select((kv) => new KeyValuePair(kv.Key, kv.Value)) 124 | .ToDictionary((kv) => kv.Key, (kv) => kv.Value) 125 | ); 126 | } 127 | 128 | [Route(HttpVerbs.Get, "/users/@self")] 129 | public async Task GetUser() 130 | { 131 | await HttpContext.SendPacketAsync(HttpContext.EnsureLogined()); 132 | } 133 | 134 | [Route(HttpVerbs.Get, "/users/{userName}")] 135 | public async Task GetUserInfo(string userName) 136 | { 137 | HttpContext.EnsureLevel(PermissionLevel.Administrator); 138 | 139 | if (!UserManager.Users.TryGetValue(userName, out User? user)) 140 | throw HttpException.NotFound("用户不存在"); 141 | else 142 | await HttpContext.SendPacketAsync(user as UserWithoutPwd); 143 | } 144 | 145 | [Route(HttpVerbs.Delete, "/users/{userName}")] 146 | public async Task RemoveUser(string userName) 147 | { 148 | HttpContext.EnsureLevel(PermissionLevel.Administrator); 149 | 150 | if ( 151 | userName == "@self" 152 | || userName == HttpContext.Session[SessionKeyConstants.UserName]?.ToString() 153 | ) 154 | throw HttpException.Forbidden("不能删除自己"); 155 | 156 | if (UserManager.Remove(userName)) 157 | { 158 | UserManager.Save(); 159 | await HttpContext.SendPacketAsync(); 160 | Logger.LogInformation("[{}] 删除用户{}成功", HttpContext.Id, userName); 161 | } 162 | else 163 | throw HttpException.NotFound("用户不存在"); 164 | } 165 | 166 | [Route(HttpVerbs.Post, "/users/{userName}")] 167 | public async Task CreateUser(string userName) 168 | { 169 | HttpContext.EnsureLevel(PermissionLevel.Administrator); 170 | 171 | if ( 172 | userName == "@self" 173 | || userName == HttpContext.Session[SessionKeyConstants.UserName]?.ToString() 174 | ) 175 | throw HttpException.Forbidden("不能创建自己"); 176 | 177 | if (UserManager.Users.ContainsKey(userName)) 178 | throw new HttpException(HttpStatusCode.Conflict, "用户已存在"); 179 | 180 | var user = 181 | await HttpContext.ConvertRequestTo() ?? throw HttpException.BadRequest("用户对象为空"); 182 | 183 | if ( 184 | !UserManager.ValidateUserName(userName, out string? message) 185 | || !UserManager.ValidatePassword(user.Password, false, out message) 186 | ) 187 | throw HttpException.BadRequest(message); 188 | 189 | UserManager.Add(userName, user); 190 | UserManager.Save(); 191 | 192 | await HttpContext.SendPacketAsync(); 193 | Logger.LogInformation("[{}] 创建用户{}成功", HttpContext.Id, userName); 194 | } 195 | 196 | [Route(HttpVerbs.Put, "/users/{userName}")] 197 | public async Task EditUser(string userName) 198 | { 199 | HttpContext.EnsureLevel(PermissionLevel.Administrator); 200 | 201 | var newUser = 202 | await HttpContext.ConvertRequestTo() ?? throw HttpException.BadRequest("用户对象不正确"); 203 | 204 | User? user; 205 | string? message; 206 | 207 | if ( 208 | userName == "@self" 209 | || userName == HttpContext.Session[SessionKeyConstants.UserName]?.ToString() 210 | ) 211 | if ( 212 | HttpContext.Session.TryGetValue(SessionKeyConstants.User, out user) 213 | && user is not null 214 | ) 215 | { 216 | if (!UserManager.ValidatePassword(newUser.Password, false, out message)) 217 | throw HttpException.BadRequest(message); 218 | 219 | user.Password = newUser.Password; 220 | await HttpContext.SendPacketAsync(); 221 | UserManager.Save(); 222 | Logger.LogInformation("[{}] 更新用户{}成功", HttpContext.Id, userName); 223 | return; 224 | } 225 | else 226 | throw new InvalidOperationException(); 227 | 228 | if (!UserManager.Users.TryGetValue(userName, out user)) 229 | throw HttpException.NotFound("用户不存在"); 230 | 231 | user.Level = newUser.Level; 232 | user.Instances = newUser.Instances ?? user.Instances; 233 | 234 | if (!UserManager.ValidatePassword(newUser.Password, true, out message)) 235 | throw HttpException.BadRequest(message); 236 | 237 | user.Password = newUser.Password ?? user.Password; 238 | user.Description = newUser.Description ?? user.Description; 239 | UserManager.Save(); 240 | 241 | await HttpContext.SendPacketAsync(); 242 | Logger.LogInformation("[{}] 更新用户{}成功", HttpContext.Id, userName); 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /iPanel/Core/Server/HttpServer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Linq; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | using EmbedIO; 8 | using EmbedIO.Sessions; 9 | using EmbedIO.WebApi; 10 | 11 | using iPanel.Core.Models.Settings; 12 | using iPanel.Core.Server.Api; 13 | using iPanel.Core.Server.WebSocket; 14 | using iPanel.Utils; 15 | 16 | using Microsoft.Extensions.DependencyInjection; 17 | using Microsoft.Extensions.Hosting; 18 | using Microsoft.Extensions.Logging; 19 | 20 | namespace iPanel.Core.Server; 21 | 22 | public class HttpServer : IDisposable 23 | { 24 | private readonly IHost _host; 25 | private IServiceProvider Services => _host.Services; 26 | private ILogger Logger => Services.GetRequiredService>(); 27 | private InstanceWsModule InstanceWsModule => Services.GetRequiredService(); 28 | private BroadcastWsModule BroadcastWsModule => Services.GetRequiredService(); 29 | private IPBannerModule IPBannerModule => Services.GetRequiredService(); 30 | private DebugWsModule DebugWsModule => Services.GetRequiredService(); 31 | private Setting Setting => Services.GetRequiredService(); 32 | private readonly WebServer _server; 33 | 34 | public HttpServer(IHost host) 35 | { 36 | _host = host; 37 | _server = new(CreateOptions()); 38 | 39 | if (Setting.WebServer.AllowCrossOrigin) 40 | _server.WithCors(); 41 | 42 | if (Setting.Debug) 43 | _server.WithModule(nameof(DebugWsModule), DebugWsModule); 44 | 45 | _server.OnUnhandledException += HandleException; 46 | _server.WithLocalSessionManager(ConfigureLocalSessionManager); 47 | _server.WithModule(nameof(IPBannerModule), IPBannerModule); 48 | _server.WithModule(nameof(InstanceWsModule), InstanceWsModule); 49 | _server.WithModule(nameof(BroadcastWsModule), BroadcastWsModule); 50 | _server.WithWebApi( 51 | "/api", 52 | (module) => 53 | module 54 | .WithController(() => new ApiMap(_host)) 55 | .HandleHttpException(ApiHelper.HandleHttpException) 56 | .HandleUnhandledException(ApiHelper.HandleException) 57 | ); 58 | 59 | if (Directory.Exists(Setting.WebServer.Directory)) 60 | { 61 | _server.WithStaticFolder( 62 | "/", 63 | Setting.WebServer.Directory, 64 | Setting.WebServer.DisableFilesHotUpdate 65 | ); 66 | _server.HandleHttpException(HandleHttpException); 67 | } 68 | else 69 | Logger.LogWarning("静态网页目录不存在"); 70 | } 71 | 72 | private static void ConfigureLocalSessionManager(LocalSessionManager localSessionManager) 73 | { 74 | localSessionManager.CookieHttpOnly = false; 75 | localSessionManager.SessionDuration = TimeSpan.FromHours(1); 76 | } 77 | 78 | private async Task HandleException(IHttpContext httpContext, Exception e) 79 | { 80 | Logger.LogCritical(e, "[{}]", httpContext.Id); 81 | await Task.CompletedTask; 82 | } 83 | 84 | private WebServerOptions CreateOptions() 85 | { 86 | WebServerOptions options = new(); 87 | 88 | try 89 | { 90 | Setting.WebServer.UrlPrefixes.ToList().ForEach((url) => options.AddUrlPrefix(url)); 91 | } 92 | catch (Exception e) 93 | { 94 | Logger.LogError(e, ""); 95 | } 96 | 97 | if (Setting.WebServer.Certificate.Enable) 98 | { 99 | options.AutoLoadCertificate = Setting.WebServer.Certificate.AutoLoadCertificate; 100 | options.AutoRegisterCertificate = Setting.WebServer.Certificate.AutoRegisterCertificate; 101 | 102 | if (string.IsNullOrEmpty(Setting.WebServer.Certificate.Path)) 103 | return options; 104 | 105 | if (File.Exists(Setting.WebServer.Certificate.Path)) 106 | options.Certificate = string.IsNullOrEmpty(Setting.WebServer.Certificate.Password) 107 | ? new(Setting.WebServer.Certificate.Path!) 108 | : new( 109 | Setting.WebServer.Certificate.Path!, 110 | Setting.WebServer.Certificate.Password 111 | ); 112 | else 113 | Logger.LogWarning("证书文件“{}”不存在", Setting.WebServer.Certificate.Path); 114 | } 115 | 116 | return options; 117 | } 118 | 119 | private async Task HandleHttpException(IHttpContext context, IHttpException exception) 120 | { 121 | if (exception.StatusCode == 404) 122 | { 123 | if (Setting.WebServer.DisableFilesHotUpdate) 124 | _404HtmlContent ??= File.Exists(_404HtmlPath) 125 | ? File.ReadAllText(_404HtmlPath) 126 | : null; 127 | else 128 | _404HtmlContent = File.Exists(_404HtmlPath) 129 | ? File.ReadAllText(_404HtmlPath) 130 | : _404HtmlContent; 131 | 132 | if (!string.IsNullOrEmpty(_404HtmlContent)) 133 | { 134 | context.Response.StatusCode = 200; 135 | await context.SendStringAsync(_404HtmlContent!, "text/html", EncodingsMap.UTF8); 136 | Logger.LogInformation( 137 | "[{}] {} {}: 404 -> 200", 138 | context.Id, 139 | context.Request.HttpMethod, 140 | context.RequestedPath 141 | ); 142 | return; 143 | } 144 | } 145 | context.Response.StatusCode = exception.StatusCode; 146 | await context.SendStandardHtmlAsync(exception.StatusCode); 147 | } 148 | 149 | public void Start(CancellationToken cancellationToken) 150 | { 151 | _server.Start(cancellationToken); 152 | } 153 | 154 | public void Dispose() 155 | { 156 | _server.Dispose(); 157 | GC.SuppressFinalize(this); 158 | } 159 | 160 | private string? _404HtmlPath => 161 | Path.Combine(Setting.WebServer.Directory, Setting.WebServer.Page404); 162 | 163 | private string? _404HtmlContent; 164 | } 165 | -------------------------------------------------------------------------------- /iPanel/Core/Server/IPBannerModule.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | using EmbedIO; 4 | using EmbedIO.Security; 5 | 6 | using iPanel.Core.Models.Settings; 7 | using iPanel.Core.Server.Api; 8 | 9 | using Microsoft.Extensions.DependencyInjection; 10 | using Microsoft.Extensions.Hosting; 11 | 12 | namespace iPanel.Core.Server; 13 | 14 | public class IPBannerModule : IPBanningModule 15 | { 16 | private readonly IHost _host; 17 | private Setting Setting => _host.Services.GetRequiredService(); 18 | 19 | public IPBannerModule(IHost host) 20 | : base("/") 21 | { 22 | _host = host; 23 | this.WithMaxRequestsPerSecond(Setting.WebServer.MaxRequestsPerSecond); 24 | this.WithWhitelist(Setting.WebServer.WhiteList); 25 | OnHttpException = Handle403; 26 | } 27 | 28 | public static async Task Handle403(IHttpContext context, IHttpException exception) 29 | { 30 | if (exception.StatusCode == 403 && context.RequestedPath.StartsWith("/api")) 31 | { 32 | await ApiHelper.HandleHttpException(context, exception); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /iPanel/Core/Server/SessionKeyConstants.cs: -------------------------------------------------------------------------------- 1 | namespace iPanel.Core.Server; 2 | 3 | public static class SessionKeyConstants 4 | { 5 | public const string User = "USER"; 6 | 7 | public const string UserName = "USERNAME"; 8 | 9 | public const string InstanceId = "INSTANCEID"; 10 | } 11 | -------------------------------------------------------------------------------- /iPanel/Core/Server/WebSocket/BroadcastWsModule.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | 6 | using EmbedIO.WebSockets; 7 | 8 | using iPanel.Core.Models.Client; 9 | using iPanel.Core.Models.Packets; 10 | using iPanel.Core.Models.Users; 11 | using iPanel.Utils; 12 | using iPanel.Utils.Extensions; 13 | 14 | using Microsoft.Extensions.DependencyInjection; 15 | using Microsoft.Extensions.Hosting; 16 | using Microsoft.Extensions.Logging; 17 | 18 | namespace iPanel.Core.Server.WebSocket; 19 | 20 | public class BroadcastWsModule : WebSocketModule 21 | { 22 | public readonly Dictionary Listeners = new(); 23 | private readonly string _salt = Guid.NewGuid().ToString("N"); 24 | 25 | private readonly IHost _host; 26 | private IServiceProvider Services => _host.Services; 27 | private ILogger Logger => 28 | Services.GetRequiredService>(); 29 | 30 | public BroadcastWsModule(IHost host) 31 | : base("/ws/broadcast", true) 32 | { 33 | _host = host; 34 | Encoding = EncodingsMap.UTF8; 35 | } 36 | 37 | protected override void OnStart(CancellationToken cancellationToken) 38 | { 39 | Logger.LogInformation("广播WS服务器已开启"); 40 | } 41 | 42 | protected override async Task OnClientConnectedAsync(IWebSocketContext context) 43 | { 44 | var clientUrl = context.RemoteEndPoint.ToString(); 45 | if (context.Session[SessionKeyConstants.User] is not User user || user is null) 46 | { 47 | Logger.LogWarning("[{}] 尝试连接广播WS服务器(401)", clientUrl); 48 | await context.CloseAsync(CloseStatusCode.Abnormal, "Unauthorized"); 49 | 50 | return; 51 | } 52 | 53 | var connectionId = Encryption.GetMD5($"{_salt}.{context.RemoteEndPoint}"); 54 | context.SendAsync(new WsSentPacket("return", "connection_id", connectionId)); 55 | Listeners[connectionId] = new(user, context, connectionId); 56 | 57 | Logger.LogInformation("[{}] 连接到广播WS服务器", clientUrl); 58 | } 59 | 60 | protected override Task OnClientDisconnectedAsync(IWebSocketContext context) 61 | { 62 | Listeners.Remove(Encryption.GetMD5($"{_salt}.{context.RemoteEndPoint}")); 63 | Logger.LogInformation("[{}] 从广播WS服务器断开连接", context.RemoteEndPoint); 64 | return Task.CompletedTask; 65 | } 66 | 67 | protected override Task OnMessageReceivedAsync( 68 | IWebSocketContext context, 69 | byte[] buffer, 70 | IWebSocketReceiveResult result 71 | ) => Task.CompletedTask; 72 | } 73 | -------------------------------------------------------------------------------- /iPanel/Core/Server/WebSocket/DebugWsModule.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text.Json; 4 | using System.Text.Json.Nodes; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | using EmbedIO.WebSockets; 9 | 10 | using iPanel.Utils; 11 | using iPanel.Utils.Extensions; 12 | using iPanel.Utils.Json; 13 | 14 | using Microsoft.Extensions.DependencyInjection; 15 | using Microsoft.Extensions.Hosting; 16 | using Microsoft.Extensions.Logging; 17 | 18 | namespace iPanel.Core.Server.WebSocket; 19 | 20 | public class DebugWsModule : WebSocketModule 21 | { 22 | private readonly string _password = Guid.NewGuid().ToString("N"); 23 | 24 | private readonly List _ips = new(); 25 | private readonly IHost _host; 26 | private IServiceProvider Services => _host.Services; 27 | private ILogger Logger => 28 | Services.GetRequiredService>(); 29 | 30 | public DebugWsModule(IHost host) 31 | : base("/ws/debug", true) 32 | { 33 | _host = host; 34 | SimpleLogger.OnMessage = (level, lines) => 35 | { 36 | try 37 | { 38 | BroadcastAsync( 39 | JsonSerializer.Serialize( 40 | new JsonObject 41 | { 42 | { nameof(lines), JsonSerializer.SerializeToNode(lines) }, 43 | { nameof(level), level } 44 | }, 45 | JsonSerializerOptionsFactory.CamelCase 46 | ), 47 | (ctx) => _ips.Contains(ctx.RemoteEndPoint.ToString()) 48 | ); 49 | } 50 | catch { } 51 | }; 52 | } 53 | 54 | protected override Task OnClientDisconnectedAsync(IWebSocketContext context) 55 | { 56 | var address = context.RemoteEndPoint.ToString(); 57 | _ips.Remove(address); 58 | Logger.LogInformation("[{}] 从调试服务器断开", address); 59 | return Task.CompletedTask; 60 | } 61 | 62 | protected override Task OnClientConnectedAsync(IWebSocketContext context) 63 | { 64 | Logger.LogInformation("[{}] 连接到调试服务器", context.RemoteEndPoint); 65 | return Task.CompletedTask; 66 | } 67 | 68 | protected override async Task OnMessageReceivedAsync( 69 | IWebSocketContext context, 70 | byte[] buffer, 71 | IWebSocketReceiveResult result 72 | ) 73 | { 74 | var data = Encoding.GetString(buffer); 75 | var address = context.RemoteEndPoint.ToString(); 76 | if (data == _password) 77 | { 78 | Logger.LogInformation("[{}] 通过调试服务器验证", address); 79 | Logger.LogWarning("[{}] 如果这不是信任的连接,请立即停止iPanel并在设置中禁用调试", address); 80 | _ips.Add(address); 81 | } 82 | else 83 | await context.CloseAsync(); 84 | } 85 | 86 | protected override void OnStart(CancellationToken cancellationToken) 87 | { 88 | Logger.LogInformation("调试服务器已开启。连接密码:[{}]", _password); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /iPanel/Core/Server/WebSocket/Handlers/BroadcastHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.Json; 3 | using System.Text.Json.Nodes; 4 | using System.Threading.Tasks; 5 | 6 | using iPanel.Core.Models.Client; 7 | using iPanel.Core.Models.Packets; 8 | using iPanel.Core.Models.Packets.Event; 9 | 10 | using Microsoft.Extensions.DependencyInjection; 11 | using Microsoft.Extensions.Hosting; 12 | using Microsoft.Extensions.Logging; 13 | 14 | namespace iPanel.Core.Server.WebSocket.Handlers; 15 | 16 | [Handler("broadcast.server_input")] 17 | [Handler("broadcast.server_output")] 18 | [Handler("broadcast.server_start")] 19 | [Handler("broadcast.server_stop")] 20 | public class BroadcastHandler : HandlerBase 21 | { 22 | public BroadcastHandler(IHost host) 23 | : base(host) { } 24 | 25 | private BroadcastWsModule BroadcastWsModule => Services.GetRequiredService(); 26 | 27 | private ILogger Logger => 28 | Services.GetRequiredService>(); 29 | 30 | private void Send(Instance instance, string subType, object? data) 31 | { 32 | var instanceID = 33 | instance.InstanceId 34 | ?? throw new NullReferenceException($"{nameof(instance.InstanceId)}为空"); 35 | 36 | lock (BroadcastWsModule.Listeners) 37 | { 38 | foreach (ConsoleListener console in BroadcastWsModule.Listeners.Values) 39 | { 40 | if ( 41 | console.InstanceIdSubscribed == instanceID 42 | || console.InstanceIdSubscribed == "*" 43 | ) 44 | { 45 | console.SendAsync( 46 | new WsSentPacket( 47 | "broadcast", 48 | subType, 49 | data, 50 | Sender.From(instance) 51 | ).ToString() 52 | ); 53 | } 54 | } 55 | } 56 | } 57 | 58 | public override async Task Handle(Instance instance, WsReceivedPacket packet) 59 | { 60 | Logger.LogInformation( 61 | "[{}] 收到广播:{},数据:{}", 62 | instance.Address, 63 | packet.SubType, 64 | packet.Data ?? "空" 65 | ); 66 | switch (packet.SubType) 67 | { 68 | case "server_input": 69 | case "server_output": 70 | if (packet.Data is not JsonArray) 71 | { 72 | await instance.SendAsync(new InvalidDataPacket("“data”字段类型错误")); 73 | break; 74 | } 75 | Send(instance, packet.SubType, packet.Data); 76 | break; 77 | 78 | case "server_start": 79 | Send(instance, packet.SubType, null); 80 | break; 81 | 82 | case "server_stop": 83 | if ( 84 | packet.Data is not JsonValue jsonValue 85 | || jsonValue.AsValue().GetValueKind() != JsonValueKind.Number 86 | ) 87 | { 88 | instance.SendAsync(new InvalidParamPacket("“data”字段类型错误")); 89 | break; 90 | } 91 | Send(instance, packet.SubType, packet.Data); 92 | break; 93 | 94 | default: 95 | instance.SendAsync(new InvalidParamPacket($"所请求的“{packet.SubType}”类型不存在或无法调用")); 96 | break; 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /iPanel/Core/Server/WebSocket/Handlers/HandlerAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace iPanel.Core.Server.WebSocket.Handlers; 4 | 5 | [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)] 6 | public sealed class HandlerAttribute : Attribute 7 | { 8 | public HandlerAttribute(string path) 9 | { 10 | Path = path; 11 | } 12 | 13 | public string Path { get; } 14 | } 15 | -------------------------------------------------------------------------------- /iPanel/Core/Server/WebSocket/Handlers/HandlerBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | using iPanel.Core.Models.Client; 5 | using iPanel.Core.Models.Packets; 6 | 7 | using Microsoft.Extensions.Hosting; 8 | 9 | namespace iPanel.Core.Server.WebSocket.Handlers; 10 | 11 | public abstract class HandlerBase 12 | { 13 | protected HandlerBase(IHost host) 14 | { 15 | _host = host; 16 | } 17 | 18 | protected readonly IHost _host; 19 | protected IServiceProvider Services => _host.Services; 20 | 21 | public abstract Task Handle(Instance instance, WsReceivedPacket packet); 22 | } 23 | -------------------------------------------------------------------------------- /iPanel/Core/Server/WebSocket/Handlers/RequestsFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text.Json.Nodes; 4 | using System.Threading.Tasks; 5 | 6 | using iPanel.Core.Models; 7 | using iPanel.Core.Models.Client; 8 | using iPanel.Core.Models.Packets; 9 | using iPanel.Utils.Json; 10 | 11 | namespace iPanel.Core.Server.WebSocket.Handlers; 12 | 13 | public static class RequestsFactory 14 | { 15 | public static readonly Dictionary Requests = new(); 16 | 17 | public static async Task Create(Instance instance, string subType, object? body = null) 18 | where T : notnull 19 | { 20 | string id = Guid.NewGuid().ToString("N"); 21 | Request request = new(instance.InstanceId); 22 | Requests.Add(id, request); 23 | 24 | await instance.SendAsync(new WsSentPacket("request", subType, body) { RequestId = id }); 25 | 26 | for (int i = 0; i < 200; i++) 27 | { 28 | if (request.HasReceived) 29 | { 30 | return request.Data is null ? default : request.Data.ToObject(); 31 | } 32 | await Task.Delay(50); 33 | } 34 | throw new TimeoutException(); 35 | } 36 | 37 | public static void MarkAsReceived(string id, string instanceId, JsonNode? body = null) 38 | { 39 | if (!Requests.TryGetValue(id, out Request? request) || request.InstanceId != instanceId) 40 | { 41 | throw new ArgumentException("无法找到指定请求ID", nameof(id)); 42 | } 43 | 44 | request.HasReceived = true; 45 | request.Data = body; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /iPanel/Core/Server/WebSocket/Handlers/ReturnHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | using iPanel.Core.Models.Client; 5 | using iPanel.Core.Models.Client.Infos; 6 | using iPanel.Core.Models.Packets; 7 | using iPanel.Core.Models.Packets.Event; 8 | using iPanel.Utils.Json; 9 | 10 | using Microsoft.Extensions.DependencyInjection; 11 | using Microsoft.Extensions.Hosting; 12 | using Microsoft.Extensions.Logging; 13 | 14 | namespace iPanel.Core.Server.WebSocket.Handlers; 15 | 16 | [Handler("return.*")] 17 | public class ReturnHandler : HandlerBase 18 | { 19 | public ReturnHandler(IHost host) 20 | : base(host) { } 21 | 22 | private ILogger Logger => Services.GetRequiredService>(); 23 | 24 | public override async Task Handle(Instance instance, WsReceivedPacket packet) 25 | { 26 | switch (packet.SubType) 27 | { 28 | case "heartbeat": 29 | InstanceInfo? info = packet.Data?.ToObject(); 30 | if (info is null) 31 | { 32 | await instance.SendAsync(new InvalidDataPacket("“data”字段为null")); 33 | break; 34 | } 35 | instance.Info = info; 36 | 37 | break; 38 | 39 | case "dir_info": 40 | HandleAsRequest(instance, packet); 41 | break; 42 | 43 | default: 44 | await instance.SendAsync( 45 | new InvalidParamPacket($"所请求的“{packet.SubType}”类型不存在或无法调用") 46 | ); 47 | break; 48 | } 49 | } 50 | 51 | private void HandleAsRequest(Instance instance, WsReceivedPacket packet) 52 | { 53 | if (string.IsNullOrEmpty(packet.RequestId)) 54 | return; 55 | 56 | try 57 | { 58 | RequestsFactory.MarkAsReceived(packet.RequestId, instance.InstanceId, packet.Data); 59 | } 60 | catch (Exception e) 61 | { 62 | Logger.LogError(e, ""); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /iPanel/Core/Server/WebSocket/InstanceWsModule.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net.WebSockets; 5 | using System.Reflection; 6 | using System.Text; 7 | using System.Text.Json; 8 | using System.Text.RegularExpressions; 9 | using System.Threading; 10 | using System.Threading.Tasks; 11 | 12 | using EmbedIO.WebSockets; 13 | 14 | using iPanel.Core.Models.Client; 15 | using iPanel.Core.Models.Exceptions; 16 | using iPanel.Core.Models.Packets; 17 | using iPanel.Core.Models.Packets.Data; 18 | using iPanel.Core.Models.Packets.Event; 19 | using iPanel.Core.Models.Settings; 20 | using iPanel.Core.Server.WebSocket.Handlers; 21 | using iPanel.Utils; 22 | using iPanel.Utils.Extensions; 23 | using iPanel.Utils.Json; 24 | 25 | using Microsoft.Extensions.DependencyInjection; 26 | using Microsoft.Extensions.Hosting; 27 | using Microsoft.Extensions.Logging; 28 | 29 | using Timer = System.Timers.Timer; 30 | 31 | namespace iPanel.Core.Server.WebSocket; 32 | 33 | public class InstanceWsModule : WebSocketModule 34 | { 35 | private readonly Timer _heartbeatTimer = new(5000); 36 | private readonly Dictionary _handlers = new(); 37 | public readonly Dictionary Instances = new(); 38 | private readonly IHost _host; 39 | private IServiceProvider Services => _host.Services; 40 | private Setting Setting => Services.GetRequiredService(); 41 | private ILogger Logger => Services.GetRequiredService>(); 42 | 43 | public InstanceWsModule(IHost host) 44 | : base("/ws/instance", true) 45 | { 46 | _host = host; 47 | Encoding = EncodingsMap.UTF8; 48 | 49 | _heartbeatTimer.Elapsed += async (_, _) => 50 | await BroadcastAsync( 51 | new WsSentPacket("request", "heartbeat"), 52 | (context) => 53 | !string.IsNullOrEmpty(context.Session[SessionKeyConstants.InstanceId] as string) 54 | ); 55 | 56 | foreach (var type in Assembly.GetExecutingAssembly().GetTypes()) 57 | { 58 | var attributes = type.GetCustomAttributes(); 59 | if (!attributes.Any()) 60 | continue; 61 | 62 | var handler = (HandlerBase?)Activator.CreateInstance(type, _host); 63 | 64 | if (handler is null) 65 | continue; 66 | 67 | foreach (var attribute in attributes) 68 | _handlers[attribute.Path] = handler; 69 | } 70 | } 71 | 72 | protected override void OnStart(CancellationToken cancellationToken) 73 | { 74 | _heartbeatTimer.Start(); 75 | Logger.LogInformation("实例WS服务器已开启"); 76 | } 77 | 78 | protected override async Task OnMessageReceivedAsync( 79 | IWebSocketContext context, 80 | byte[] buffer, 81 | IWebSocketReceiveResult result 82 | ) 83 | { 84 | WsReceivedPacket? packet; 85 | var instanceId = context.Session[SessionKeyConstants.InstanceId] as string ?? string.Empty; 86 | var clientUrl = context.RemoteEndPoint.ToString(); 87 | var message = Encoding.GetString(buffer); 88 | var verified = Instances.TryGetValue(instanceId, out Instance? instance); 89 | 90 | try 91 | { 92 | packet = 93 | JsonSerializer.Deserialize( 94 | message, 95 | JsonSerializerOptionsFactory.CamelCase 96 | ) ?? throw new PacketException("空数据包"); 97 | 98 | Logger.LogDebug("[{}] 收到数据\n{}", clientUrl, packet); 99 | } 100 | catch (Exception e) 101 | { 102 | Logger.LogDebug("[{}] 收到数据\n{}", clientUrl, message); 103 | Logger.LogWarning(e, "[{}] 处理数据包异常", clientUrl); 104 | await context.SendAsync( 105 | new WsSentPacket( 106 | "event", 107 | verified ? "invalid_packet" : "disconnection", 108 | new Result($"发送的数据包存在问题:{e.Message}") 109 | ) 110 | ); 111 | 112 | if (!verified) 113 | await context.CloseAsync(); 114 | 115 | return; 116 | } 117 | 118 | var path = $"{packet.Type}.{packet.SubType}"; 119 | 120 | if (!verified || instance is null) 121 | { 122 | if (path != "request.verify") 123 | { 124 | await context.CloseAsync(); 125 | Logger.LogWarning("[{}] 发送了未经允许的数据包类型:{}", clientUrl, path); 126 | return; 127 | } 128 | 129 | await Verify(context, packet); 130 | return; 131 | } 132 | 133 | if (_handlers.TryGetValue(path, out var handler)) 134 | await handler.Handle(instance, packet); 135 | else if (_handlers.TryGetValue($"{packet.Type}.*", out handler)) 136 | await handler.Handle(instance, packet); 137 | else 138 | await context.SendAsync( 139 | new WsSentPacket( 140 | "event", 141 | "invalid_param", 142 | new Result($"所请求的“{packet.Type}”类型不存在或无法调用") 143 | ) 144 | ); 145 | } 146 | 147 | private async Task Verify(IWebSocketContext context, WsReceivedPacket packet) 148 | { 149 | try 150 | { 151 | var verifyBody = 152 | packet.Data?.ToObject() ?? throw new PacketException("数据为空"); 153 | 154 | if (string.IsNullOrEmpty(verifyBody.Time)) 155 | throw new PacketException($"{nameof(verifyBody.Time)}为空"); 156 | 157 | if (!DateTime.TryParse(verifyBody.Time, out DateTime dateTime)) 158 | throw new PacketException($"{nameof(verifyBody.Time)}无效"); 159 | 160 | var span = dateTime - DateTime.Now; 161 | if (span.TotalSeconds < -10 || span.TotalMinutes > 10) 162 | throw new PacketException($"{nameof(verifyBody.Time)}过期"); 163 | 164 | if ( 165 | string.IsNullOrEmpty(verifyBody.InstanceId) 166 | || verifyBody.InstanceId.Length != 32 167 | || !Regex.IsMatch(verifyBody.InstanceId, @"^\w{32}$") 168 | || Instances.ContainsKey(verifyBody.InstanceId) 169 | ) 170 | throw new PacketException($"{nameof(verifyBody.InstanceId)}无效"); 171 | 172 | var expectedValue = Encryption.GetMD5($"{verifyBody.Time}.{Setting.InstancePassword}"); 173 | if (verifyBody.MD5 == expectedValue) 174 | { 175 | Instances.Add( 176 | verifyBody.InstanceId, 177 | new(verifyBody.InstanceId) 178 | { 179 | CustomName = verifyBody.CustomName, 180 | Context = context, 181 | Metadata = verifyBody.Metadata ?? new() 182 | } 183 | ); 184 | 185 | await context.SendAsync(new VerifyResultPacket(true)); 186 | context.Session[SessionKeyConstants.InstanceId] = verifyBody.InstanceId; 187 | Logger.LogInformation("[{}] 验证成功", context.RemoteEndPoint); 188 | 189 | return; 190 | } 191 | 192 | Logger.LogWarning( 193 | "[{}] 预期MD5:\"{}\",实际接收:\"{}\"", 194 | context.RemoteEndPoint, 195 | expectedValue, 196 | verifyBody.MD5 197 | ); 198 | throw new PacketException("验证失败"); 199 | } 200 | catch (Exception e) 201 | { 202 | Logger.LogWarning(e, "[{}] 验证失败", context.RemoteEndPoint); 203 | await context.SendAsync(new VerifyResultPacket(false, e.Message)); 204 | await context.CloseAsync(); 205 | } 206 | } 207 | 208 | protected override Task OnClientConnectedAsync(IWebSocketContext context) 209 | { 210 | context.Session[SessionKeyConstants.InstanceId] = string.Empty; 211 | Logger.LogInformation("[{}] 连接到实例WS服务器", context.RemoteEndPoint); 212 | 213 | Task.Run(async () => 214 | { 215 | await Task.Delay(5000); 216 | 217 | var instanceId = 218 | context.Session[SessionKeyConstants.InstanceId] as string ?? string.Empty; 219 | if ( 220 | (string.IsNullOrEmpty(instanceId) || !Instances.ContainsKey(instanceId)) 221 | && context.WebSocket.State == WebSocketState.Open 222 | ) 223 | { 224 | await context.SendAsync(new VerifyResultPacket(false, "验证超时")); 225 | await context.CloseAsync(); 226 | Logger.LogWarning("[{}] 验证超时", context.RemoteEndPoint); 227 | } 228 | }); 229 | 230 | return Task.CompletedTask; 231 | } 232 | 233 | protected override Task OnClientDisconnectedAsync(IWebSocketContext context) 234 | { 235 | Logger.LogInformation("[{}] 从实例WS服务器断开了连接", context.RemoteEndPoint); 236 | 237 | var instanceId = context.Session[SessionKeyConstants.InstanceId]?.ToString(); 238 | context.Session[SessionKeyConstants.InstanceId] = string.Empty; 239 | 240 | if (!string.IsNullOrEmpty(instanceId)) 241 | Instances.Remove(instanceId); 242 | 243 | return Task.CompletedTask; 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /iPanel/Core/Service/UserManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics.CodeAnalysis; 4 | using System.IO; 5 | using System.Text.Json; 6 | using System.Timers; 7 | 8 | using iPanel.Core.Models.Users; 9 | using iPanel.Utils.Json; 10 | 11 | using Microsoft.Extensions.DependencyInjection; 12 | using Microsoft.Extensions.Hosting; 13 | using Microsoft.Extensions.Logging; 14 | 15 | namespace iPanel.Core.Service; 16 | 17 | public class UserManager 18 | { 19 | public static readonly IReadOnlyDictionary LevelNames = new Dictionary< 20 | PermissionLevel, 21 | string 22 | >() 23 | { 24 | { PermissionLevel.Guest, "游客" }, 25 | { PermissionLevel.ReadOnly, "只读" }, 26 | { PermissionLevel.Assistant, "助手" }, 27 | { PermissionLevel.Administrator, "管理员" } 28 | }; 29 | 30 | private Dictionary _users = new(); 31 | public IReadOnlyDictionary Users => _users; 32 | 33 | private readonly Timer _timer = new(60_000) { AutoReset = true }; 34 | 35 | public UserManager(IHost host) 36 | { 37 | _host = host; 38 | _timer.Elapsed += (_, _) => Save(); 39 | _timer.Start(); 40 | } 41 | 42 | private const string _path = "data/users.json"; 43 | 44 | private readonly object _lock = new(); 45 | private readonly IHost _host; 46 | 47 | private IServiceProvider Services => _host.Services; 48 | private ILogger Logger => Services.GetRequiredService>(); 49 | 50 | public void Read() 51 | { 52 | Directory.CreateDirectory("data"); 53 | lock (_lock) 54 | { 55 | if (!File.Exists(_path)) 56 | { 57 | Logger.LogWarning("用户列表为空,请使用\"user create\"创建一个用户"); 58 | Save(); 59 | return; 60 | } 61 | 62 | try 63 | { 64 | _users = 65 | JsonSerializer.Deserialize>( 66 | File.ReadAllText(_path), 67 | JsonSerializerOptionsFactory.CamelCase 68 | ) ?? throw new FileLoadException("文件数据异常"); 69 | 70 | if (_users.Count == 0) 71 | Logger.LogWarning("用户列表为空,请使用\"user create\"创建一个用户"); 72 | } 73 | catch (Exception e) 74 | { 75 | Logger.LogError(e, "加载用户文件{}时出错", _path); 76 | } 77 | } 78 | } 79 | 80 | public void Save() 81 | { 82 | Directory.CreateDirectory("data"); 83 | lock (_lock) 84 | { 85 | File.WriteAllText( 86 | _path, 87 | JsonSerializer.Serialize( 88 | Users, 89 | options: new(JsonSerializerOptionsFactory.CamelCase) { WriteIndented = true } 90 | ) 91 | ); 92 | } 93 | } 94 | 95 | public bool Add(string name, User user) 96 | { 97 | if (Users.ContainsKey(name)) 98 | return false; 99 | 100 | _users[name] = user; 101 | Save(); 102 | return true; 103 | } 104 | 105 | public bool Remove(string name) 106 | { 107 | var result = _users.Remove(name); 108 | Save(); 109 | return result; 110 | } 111 | 112 | public void Clear() 113 | { 114 | _users.Clear(); 115 | Save(); 116 | } 117 | 118 | public static bool ValidatePassword( 119 | [NotNullWhen(true)] string? password, 120 | bool ignoreNull, 121 | [NotNullWhen(false)] out string? message 122 | ) 123 | { 124 | message = null; 125 | if (ignoreNull && password is null) 126 | return true; 127 | 128 | if (password is null || password.Length < 6) 129 | { 130 | message = "密码长度过短"; 131 | return false; 132 | } 133 | 134 | if ( 135 | password.Contains('\\') 136 | || password.Contains('"') 137 | || password.Contains('\'') 138 | || password.Contains(' ') 139 | || ContainsControlChars(password) 140 | ) 141 | { 142 | message = "密码不得含有特殊字符"; 143 | return false; 144 | } 145 | return true; 146 | } 147 | 148 | public static bool ValidateUserName( 149 | [NotNullWhen(true)] string? userName, 150 | [NotNullWhen(false)] out string? message 151 | ) 152 | { 153 | message = null; 154 | 155 | if (userName is null || userName.Length < 3) 156 | { 157 | message = "用户名长度过短"; 158 | return false; 159 | } 160 | 161 | if ( 162 | userName.Contains('\\') 163 | || userName.Contains('"') 164 | || userName.Contains('\'') 165 | || userName.Contains('@') 166 | || userName.Contains(' ') 167 | || ContainsControlChars(userName) 168 | ) 169 | { 170 | message = "用户名不得含有特殊字符"; 171 | return false; 172 | } 173 | return true; 174 | } 175 | 176 | private static bool ContainsControlChars(string text) 177 | { 178 | foreach (var c in text) 179 | if (c <= 31 || c == 127) 180 | return true; 181 | return false; 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /iPanel/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.CommandLine; 3 | using System.CommandLine.Parsing; 4 | using System.IO; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | using iPanel.Utils; 9 | 10 | using Swan.Logging; 11 | 12 | namespace iPanel; 13 | 14 | public static class Program 15 | { 16 | public static int Main(string[] args) 17 | { 18 | Environment.CurrentDirectory = AppDomain.CurrentDomain.BaseDirectory; 19 | 20 | Console.OutputEncoding = EncodingsMap.UTF8; 21 | Console.InputEncoding = EncodingsMap.UTF8; 22 | Console.CancelKeyPress += (_, _) => Console.WriteLine("^C"); 23 | 24 | if (Environment.OSVersion.Platform == PlatformID.Win32NT) 25 | Console.Title = $"iPanel {Constant.Version}"; 26 | 27 | Logger.UnregisterLogger(); 28 | Logger.RegisterLogger(); 29 | 30 | TaskScheduler.UnobservedTaskException += (_, e) => 31 | Logger.Error(e.Exception, nameof(TaskScheduler), string.Empty); 32 | AppDomain.CurrentDomain.UnhandledException += (_, e) => 33 | Logger.Error((e.ExceptionObject as Exception)!, nameof(AppDomain), string.Empty); 34 | AppDomain.CurrentDomain.UnhandledException += (_, e) => 35 | SaveException(e.ExceptionObject as Exception); 36 | 37 | return CommandLineHelper.Create().Invoke(args); 38 | } 39 | 40 | private static void SaveException(Exception? e) 41 | { 42 | Directory.CreateDirectory("logs"); 43 | 44 | var stringBuilder = new StringBuilder(); 45 | stringBuilder.AppendLine($"时间: {DateTime.Now}"); 46 | stringBuilder.AppendLine($"版本: {Constant.Version}"); 47 | 48 | var commandlineArgs = Environment.GetCommandLineArgs(); 49 | if (commandlineArgs.Length > 0 && File.Exists(commandlineArgs[0])) 50 | { 51 | stringBuilder.AppendLine($"文件名: {Path.GetFileName(commandlineArgs[0])}"); 52 | stringBuilder.AppendLine( 53 | $"MD5: {Encryption.GetMD5(File.ReadAllBytes(commandlineArgs[0]))}" 54 | ); 55 | } 56 | stringBuilder.AppendLine(e?.ToString()); 57 | 58 | var fileName = $"logs/crash-{DateTime.Now:yyyy-MM-dd-hh:mm:ss}.txt"; 59 | File.WriteAllText(fileName, stringBuilder.ToString()); 60 | Console.WriteLine( 61 | $"完整日志已保存在\"{fileName}\",你可以在上反馈此问题" 62 | ); 63 | 64 | if (!Console.IsInputRedirected) 65 | Console.ReadKey(true); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /iPanel/Sources/logo.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iPanelDev/iPanel/dce56dacb48c7b8991adcf045807dfc949c9ae17/iPanel/Sources/logo.ico -------------------------------------------------------------------------------- /iPanel/Sources/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iPanelDev/iPanel/dce56dacb48c7b8991adcf045807dfc949c9ae17/iPanel/Sources/logo.png -------------------------------------------------------------------------------- /iPanel/Utils/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | [assembly: InternalsVisibleTo("iPanel.Tests")] 4 | -------------------------------------------------------------------------------- /iPanel/Utils/CommandLineHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.CommandLine; 3 | using System.CommandLine.Builder; 4 | using System.CommandLine.Invocation; 5 | using System.CommandLine.Parsing; 6 | using System.IO; 7 | using System.Text.Json; 8 | 9 | using iPanel.Core.Models.Settings; 10 | using iPanel.Utils.Json; 11 | 12 | using Microsoft.Extensions.Hosting; 13 | 14 | namespace iPanel.Utils; 15 | 16 | public static class CommandLineHelper 17 | { 18 | public static Parser Create() 19 | { 20 | var rootCommnad = new RootCommand(); 21 | var settingsCmd = new Command("setting", "创建设置文件"); 22 | 23 | rootCommnad.AddCommand(settingsCmd); 24 | settingsCmd.SetHandler(WriteSetting); 25 | rootCommnad.SetHandler(() => new AppBuilder(ReadSetting()).Build().Run()); 26 | 27 | return new CommandLineBuilder(rootCommnad) 28 | .UseExceptionHandler(WriteError) 29 | .UseTypoCorrections() 30 | .UseVersionOption() 31 | .UseHelp() 32 | .UseTypoCorrections() 33 | .UseParseErrorReporting() 34 | .RegisterWithDotnetSuggest() 35 | .Build(); 36 | } 37 | 38 | private static void WriteError(Exception e, InvocationContext? context) 39 | { 40 | Console.ForegroundColor = ConsoleColor.Red; 41 | Console.WriteLine(e.ToString()); 42 | 43 | if (!Console.IsInputRedirected) 44 | Console.ReadLine(); 45 | 46 | Console.ResetColor(); 47 | 48 | if (e.HResult != 0 && context is not null) 49 | context.ExitCode = e.HResult; 50 | } 51 | 52 | private static void WriteSetting() => 53 | File.WriteAllText( 54 | "setting.json", 55 | JsonSerializer.Serialize( 56 | new Setting(), 57 | options: new(JsonSerializerOptionsFactory.CamelCase) { WriteIndented = true } 58 | ) 59 | ); 60 | 61 | public static Setting ReadSetting() 62 | { 63 | if (!File.Exists("setting.json")) 64 | { 65 | WriteSetting(); 66 | throw new FileNotFoundException("\"setting.json\"不存在,现已重新创建,请修改后重启"); 67 | } 68 | return JsonSerializer.Deserialize( 69 | File.ReadAllText("setting.json"), 70 | JsonSerializerOptionsFactory.CamelCase 71 | ) ?? throw new InvalidOperationException("文件为空"); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /iPanel/Utils/EncodingsMap.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | 3 | namespace iPanel.Utils; 4 | 5 | public static class EncodingsMap 6 | { 7 | public static readonly Encoding UTF8 = new UTF8Encoding(false); 8 | } 9 | -------------------------------------------------------------------------------- /iPanel/Utils/Encryption.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Security.Cryptography; 3 | using System.Text; 4 | 5 | namespace iPanel.Utils; 6 | 7 | public static class Encryption 8 | { 9 | public static string GetMD5(string text) => 10 | GetHexString(MD5.Create().ComputeHash(EncodingsMap.UTF8.GetBytes(text))); 11 | 12 | public static string GetMD5(byte[] bytes) => GetHexString(MD5.Create().ComputeHash(bytes)); 13 | 14 | public static string GetMD5(Stream stream) => GetHexString(MD5.Create().ComputeHash(stream)); 15 | 16 | public static string GetHexString(byte[] targetData) 17 | { 18 | StringBuilder stringBuilder = new(); 19 | for (int i = 0; i < targetData.Length; i++) 20 | { 21 | stringBuilder.Append(targetData[i].ToString("x2")); 22 | } 23 | return stringBuilder.ToString(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /iPanel/Utils/Extensions/WebSocketExtension.cs: -------------------------------------------------------------------------------- 1 | using System.Net.WebSockets; 2 | using System.Threading.Tasks; 3 | 4 | using EmbedIO.WebSockets; 5 | 6 | using Swan.Logging; 7 | 8 | namespace iPanel.Utils.Extensions; 9 | 10 | public static class WebSocketExtension 11 | { 12 | public static async Task SendAsync(this IWebSocketContext context, string payload) 13 | { 14 | if (context.WebSocket.State != WebSocketState.Open) 15 | return; 16 | 17 | await context.WebSocket.SendAsync(EncodingsMap.UTF8.GetBytes(payload), true); 18 | 19 | Logger.Debug($"[{context.RemoteEndPoint}] 发送数据\n{payload}"); 20 | } 21 | 22 | public static async Task CloseAsync(this IWebSocketContext context) => 23 | await context.WebSocket.CloseAsync(); 24 | 25 | public static async Task CloseAsync( 26 | this IWebSocketContext context, 27 | CloseStatusCode code, 28 | string reason 29 | ) => await context.WebSocket.CloseAsync(code, reason); 30 | } 31 | -------------------------------------------------------------------------------- /iPanel/Utils/Json/JsonNodeExtension.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.Nodes; 3 | 4 | namespace iPanel.Utils.Json; 5 | 6 | public static class JsonNodeExtension 7 | { 8 | public static T? ToObject(this JsonNode jsonNode, JsonSerializerOptions? options = null) 9 | where T : notnull 10 | { 11 | return JsonSerializer.Deserialize( 12 | jsonNode, 13 | options ?? JsonSerializerOptionsFactory.CamelCase 14 | ); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /iPanel/Utils/Json/JsonSerializerOptionsFactory.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Encodings.Web; 2 | using System.Text.Json; 3 | 4 | namespace iPanel.Utils.Json; 5 | 6 | public static class JsonSerializerOptionsFactory 7 | { 8 | public static readonly JsonSerializerOptions CamelCase = 9 | new() 10 | { 11 | PropertyNamingPolicy = JsonNamingPolicy.CamelCase, 12 | Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, 13 | AllowTrailingCommas = true, 14 | ReadCommentHandling = JsonCommentHandling.Skip 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /iPanel/Utils/ResourceFileManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.IO.Compression; 4 | using System.Reflection; 5 | 6 | using iPanel.Core.Models.Settings; 7 | 8 | using Microsoft.Extensions.DependencyInjection; 9 | using Microsoft.Extensions.Hosting; 10 | using Microsoft.Extensions.Logging; 11 | 12 | using Spectre.Console; 13 | 14 | namespace iPanel.Utils; 15 | 16 | public class ResourceFileManager 17 | { 18 | private IServiceProvider Services => _host.Services; 19 | private ILogger Logger => 20 | Services.GetRequiredService>(); 21 | private Setting Setting => Services.GetRequiredService(); 22 | private const string _resourceKey = "iPanel.Sources.webconsole.zip"; 23 | private readonly IHost _host; 24 | 25 | public ResourceFileManager(IHost host) 26 | { 27 | _host = host; 28 | } 29 | 30 | public void Release() 31 | { 32 | if (Directory.Exists(Setting.WebServer.Directory)) 33 | return; 34 | 35 | var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(_resourceKey); 36 | if (stream is null) 37 | { 38 | Logger.LogWarning( 39 | "嵌入文件“{}”丢失,请自行检查编译环境。此文件为此软件对应版本的前端网页的压缩包,请自行到 https://github.com/iPanelDev/WebConsole/releases/latest 下载最新版本,解压后放在“{}”文件夹下并重启", 40 | _resourceKey, 41 | Setting.WebServer.Directory 42 | ); 43 | return; 44 | } 45 | 46 | Directory.CreateDirectory(Setting.WebServer.Directory); 47 | 48 | var fileStream = new FileStream(_resourceKey, FileMode.Create); 49 | var bytes = new byte[stream.Length]; 50 | stream.Read(bytes, 0, bytes.Length); 51 | fileStream.Write(bytes, 0, bytes.Length); 52 | fileStream.Close(); 53 | 54 | Logger.LogInformation("嵌入文件“{}”已释放", _resourceKey); 55 | ZipFile.ExtractToDirectory(_resourceKey, Setting.WebServer.Directory); 56 | 57 | var table = new Table() 58 | .RoundedBorder() 59 | .AddColumns(new("文件名"), new TableColumn("大小") { Alignment = Justify.Right }) 60 | .Expand(); 61 | 62 | var dir = Path.GetFullPath(Setting.WebServer.Directory); 63 | foreach ( 64 | string fileName in Directory.GetFiles( 65 | Setting.WebServer.Directory, 66 | "*", 67 | SearchOption.AllDirectories 68 | ) 69 | ) 70 | { 71 | table.AddRow( 72 | Markup.Escape(Path.GetDirectoryName(fileName)!.Replace(dir, string.Empty)) 73 | + Path.DirectorySeparatorChar 74 | + Markup.Escape(Path.GetFileName(fileName)), 75 | ((double)new FileInfo(fileName).Length / 1024).ToString("N1") + "KB" 76 | ); 77 | } 78 | 79 | Logger.LogInformation("嵌入文件解压完成"); 80 | AnsiConsole.Write(table); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /iPanel/Utils/SimpleLogger.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | 4 | using Spectre.Console; 5 | 6 | using Swan.Logging; 7 | 8 | using IMicrosoftLogger = Microsoft.Extensions.Logging.ILogger; 9 | using ISwanLogger = Swan.Logging.ILogger; 10 | using MicrosoftLogLevel = Microsoft.Extensions.Logging.LogLevel; 11 | 12 | namespace iPanel.Utils; 13 | 14 | public class SimpleLogger : IMicrosoftLogger, ISwanLogger 15 | { 16 | private static readonly object _lock = new(); 17 | 18 | public static LogLevel StaticLogLevel { get; set; } = LogLevel.Debug; 19 | public LogLevel LogLevel => StaticLogLevel; 20 | public static Action? OnMessage { get; set; } 21 | 22 | private static void Info(string[] lines) 23 | { 24 | lock (_lock) 25 | foreach (var line in lines) 26 | AnsiConsole.MarkupLineInterpolated( 27 | $"{DateTime.Now:T} [cadetblue_1]Info[/] {line}" 28 | ); 29 | } 30 | 31 | private static void Warn(string[] lines) 32 | { 33 | lock (_lock) 34 | foreach (var line in lines) 35 | AnsiConsole.MarkupLineInterpolated( 36 | $"{DateTime.Now:T} [yellow bold]Warn {line}[/]" 37 | ); 38 | } 39 | 40 | private static void Error(string[] lines) 41 | { 42 | lock (_lock) 43 | foreach (var line in lines) 44 | AnsiConsole.MarkupLineInterpolated($"{DateTime.Now:T} [red bold]Error {line}[/]"); 45 | } 46 | 47 | private static void Fatal(string[] lines) 48 | { 49 | lock (_lock) 50 | foreach (var line in lines) 51 | AnsiConsole.MarkupLineInterpolated( 52 | $"{DateTime.Now:T} [maroon blod]Fatal {line}[/]" 53 | ); 54 | } 55 | 56 | private static void Debug(string[] lines) 57 | { 58 | lock (_lock) 59 | foreach (var line in lines) 60 | AnsiConsole.MarkupLineInterpolated( 61 | $"{DateTime.Now:T} [mediumpurple4]Debug[/] {line}" 62 | ); 63 | } 64 | 65 | public void Log(LogMessageReceivedEventArgs logEvent) 66 | { 67 | var stringBuilder = new StringBuilder(); 68 | if (!string.IsNullOrEmpty(logEvent.Message)) 69 | stringBuilder.AppendLine(logEvent.Message); 70 | 71 | if (logEvent.Exception is not null) 72 | stringBuilder.Append(logEvent.Exception); 73 | 74 | var lines = stringBuilder.ToString().TrimEnd('\r', '\n').Replace("\r", null).Split('\n'); 75 | 76 | switch (logEvent.MessageType) 77 | { 78 | case LogLevel.Debug: 79 | Debug(lines); 80 | break; 81 | 82 | case LogLevel.Info: 83 | Info(lines); 84 | break; 85 | 86 | case LogLevel.Warning: 87 | Warn(lines); 88 | break; 89 | 90 | case LogLevel.Error: 91 | Error(lines); 92 | break; 93 | 94 | case LogLevel.Fatal: 95 | Fatal(lines); 96 | break; 97 | } 98 | OnMessage?.Invoke((uint)logEvent.MessageType, lines); 99 | } 100 | 101 | public void Dispose() 102 | { 103 | GC.SuppressFinalize(this); 104 | } 105 | 106 | public void Log( 107 | MicrosoftLogLevel logLevel, 108 | Microsoft.Extensions.Logging.EventId eventId, 109 | TState state, 110 | Exception? exception, 111 | Func formatter 112 | ) 113 | { 114 | var stringBuilder = new StringBuilder(); 115 | var stringState = state?.ToString() ?? string.Empty; 116 | 117 | if (!string.IsNullOrEmpty(stringState)) 118 | stringBuilder.AppendLine(stringState); 119 | 120 | if (exception is not null) 121 | stringBuilder.Append(exception); 122 | 123 | var lines = stringBuilder.ToString().TrimEnd('\r', '\n').Replace("\r", null).Split('\n'); 124 | 125 | switch (logLevel) 126 | { 127 | case MicrosoftLogLevel.Debug: 128 | Debug(lines); 129 | break; 130 | 131 | case MicrosoftLogLevel.Information: 132 | Info(lines); 133 | break; 134 | 135 | case MicrosoftLogLevel.Warning: 136 | Warn(lines); 137 | break; 138 | 139 | case MicrosoftLogLevel.Error: 140 | Error(lines); 141 | break; 142 | 143 | case MicrosoftLogLevel.Critical: 144 | Fatal(lines); 145 | break; 146 | } 147 | 148 | var level = logLevel switch 149 | { 150 | MicrosoftLogLevel.Trace => LogLevel.Trace, 151 | MicrosoftLogLevel.Debug => LogLevel.Debug, 152 | MicrosoftLogLevel.Information => LogLevel.Info, 153 | MicrosoftLogLevel.Warning => LogLevel.Warning, 154 | MicrosoftLogLevel.Error => LogLevel.Error, 155 | MicrosoftLogLevel.Critical => LogLevel.Fatal, 156 | MicrosoftLogLevel.None => LogLevel.None, 157 | _ => throw new ArgumentOutOfRangeException(nameof(logLevel)) 158 | }; 159 | OnMessage?.Invoke((uint)level, lines); 160 | } 161 | 162 | public bool IsEnabled(MicrosoftLogLevel logLevel) 163 | { 164 | var level = logLevel switch 165 | { 166 | MicrosoftLogLevel.Trace => LogLevel.Trace, 167 | MicrosoftLogLevel.Debug => LogLevel.Debug, 168 | MicrosoftLogLevel.Information => LogLevel.Info, 169 | MicrosoftLogLevel.Warning => LogLevel.Warning, 170 | MicrosoftLogLevel.Error => LogLevel.Error, 171 | MicrosoftLogLevel.Critical => LogLevel.Fatal, 172 | MicrosoftLogLevel.None => LogLevel.None, 173 | _ => throw new ArgumentOutOfRangeException(nameof(logLevel)) 174 | }; 175 | 176 | return LogLevel >= level; 177 | } 178 | 179 | public IDisposable? BeginScope(TState state) 180 | where TState : notnull 181 | { 182 | return null; 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /iPanel/Utils/SimpleLoggerProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | using Microsoft.Extensions.Logging; 5 | 6 | namespace iPanel.Utils; 7 | 8 | public class SimpleLoggerProvider : ILoggerProvider 9 | { 10 | private readonly Dictionary _loggers; 11 | 12 | public SimpleLoggerProvider() 13 | { 14 | _loggers = new(); 15 | } 16 | 17 | public ILogger CreateLogger(string categoryName) 18 | { 19 | lock (_loggers) 20 | { 21 | if (_loggers.TryGetValue(categoryName, out ILogger? logger)) 22 | return logger; 23 | 24 | logger = new SimpleLogger(); 25 | _loggers[categoryName] = logger; 26 | return logger; 27 | } 28 | } 29 | 30 | public void Dispose() 31 | { 32 | _loggers.Clear(); 33 | GC.SuppressFinalize(this); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /iPanel/iPanel.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net6.0 6 | Sources/logo.ico 7 | enable 8 | 9 | iPanelDev 10 | 2.3.0.1 11 | https://ipaneldev.github.io/ 12 | iPanel网页面板后端 13 | iPanel网页面板后端 14 | Copyright (C) 2023 iPanelDev 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | --------------------------------------------------------------------------------