├── .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 | [](https://github.com/iPanelDev/iPanel/actions/workflows/iPanel-build.yml)
8 | 
9 | [](https://github.com/iPanelDev/iPanel)
10 | [](https://wakatime.com/badge/user/724e95cb-6b0f-48fb-9f96-915cce8cc845/project/77afa545-87b2-4608-8df2-2e2355550d67)
11 | 
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