├── .gitattributes
├── .gitignore
├── LICENSE.md
├── README.md
├── src
├── .editorconfig
├── DotnetCat.sln
├── DotnetCat
│ ├── DotnetCat.csproj
│ ├── Errors
│ │ ├── Error.cs
│ │ ├── ErrorMessage.cs
│ │ ├── Except.cs
│ │ └── ThrowIf.cs
│ ├── IO
│ │ ├── FileSys.cs
│ │ ├── IConnectable.cs
│ │ ├── LogLevel.cs
│ │ ├── Output.cs
│ │ ├── Pipelines
│ │ │ ├── FilePipe.cs
│ │ │ ├── PipeType.cs
│ │ │ ├── ProcessPipe.cs
│ │ │ ├── SocketPipe.cs
│ │ │ ├── StatusPipe.cs
│ │ │ ├── StreamPipe.cs
│ │ │ ├── TextPipe.cs
│ │ │ └── TransferOpt.cs
│ │ └── Sequence.cs
│ ├── Network
│ │ ├── HostEndPoint.cs
│ │ ├── Net.cs
│ │ └── Nodes
│ │ │ ├── ClientNode.cs
│ │ │ ├── Node.cs
│ │ │ └── ServerNode.cs
│ ├── Program.cs
│ ├── Properties
│ │ └── PublishProfiles
│ │ │ ├── Linux-arm64.pubxml
│ │ │ ├── Linux-x64.pubxml
│ │ │ ├── Win-x64.pubxml
│ │ │ └── Win-x86.pubxml
│ ├── Resources
│ │ └── Icon.ico
│ ├── Shell
│ │ ├── Command.cs
│ │ ├── SysInfo.cs
│ │ └── WinApi
│ │ │ ├── ConsoleApi.cs
│ │ │ ├── ExternException.cs
│ │ │ ├── InputMode.cs
│ │ │ ├── OutputMode.cs
│ │ │ ├── WinInterop.cs
│ │ │ └── WinSafeHandle.cs
│ ├── Utils
│ │ ├── CmdLineArgs.cs
│ │ ├── Extensions.cs
│ │ └── Parser.cs
│ ├── bin
│ │ ├── ARM64
│ │ │ └── .gitignore
│ │ ├── Debug
│ │ │ └── .gitignore
│ │ ├── Linux
│ │ │ └── .gitignore
│ │ ├── Publish
│ │ │ └── .gitignore
│ │ ├── Release
│ │ │ └── .gitignore
│ │ └── Zips
│ │ │ ├── DotnetCat_linux-arm64.zip
│ │ │ ├── DotnetCat_linux-x64.zip
│ │ │ ├── DotnetCat_win-x64.zip
│ │ │ └── DotnetCat_win-x86.zip
│ └── obj
│ │ └── .gitignore
└── DotnetCatTests
│ ├── DotnetCatTests.csproj
│ ├── Errors
│ └── ErrorMessageTests.cs
│ ├── IO
│ └── FileSysTests.cs
│ ├── Network
│ └── NetTests.cs
│ ├── Properties
│ └── .gitignore
│ ├── Shell
│ └── CommandTests.cs
│ ├── Utils
│ ├── ExtensionsTests.cs
│ └── ParserTests.cs
│ ├── bin
│ └── .gitignore
│ └── obj
│ └── .gitignore
└── tools
├── dncat-install.ps1
├── dncat-install.sh
├── dncat-uninstall.ps1
└── dncat-uninstall.sh
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text encoding=utf-8 eol=lf
2 |
3 | *.aps binary
4 | *.assets binary
5 | *.cache binary
6 | *.class binary
7 | *.com binary
8 | *.dgspec binary
9 | *.dll binary
10 | *.docx binary
11 | *.exe binary
12 | *.ico binary
13 | *.jpeg binary
14 | *.jpg binary
15 | *.obj binary
16 | *.pdb binary
17 | *.pdf binary
18 | *.png binary
19 | *.suo binary
20 | *.zip binary
21 |
22 | *.cs linguist-detectable
23 | *.ps1 linguist-detectable
24 | *.sh linguist-detectable
25 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | **/*.aps
2 | **/*.assets
3 | **/*.bak
4 | **/*.bak.*
5 | **/*.cache
6 | **/*.class
7 | **/*.com
8 | **/*.dgspec
9 | **/*.dll
10 | **/*.dll.config
11 | **/*.idb
12 | **/*.ifc
13 | **/*.ifcast
14 | **/*.iobj
15 | **/*.ipdb
16 | **/*.log
17 | **/*.obj
18 | **/*.old
19 | **/*.pdb
20 | **/*.recipe
21 | **/*.sarif
22 | **/*.suo
23 | **/*.targets
24 | **/*.tlog
25 | **/*.user
26 |
27 | **/launchSettings.json
28 |
29 | **/*.bak/
30 | **/*.bak.*/
31 | **/*.old*/
32 | **/.atom/
33 | **/.dist/
34 | **/.idea/
35 | **/.vs/
36 | **/.vscode/
37 | **/TestResults/
38 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | Copyright © 2025, Dave Van Dewerker
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |

3 |
4 |
5 | # DotnetCat
6 |
7 |
13 |
14 | Remote command shell application written in C# targeting the
15 | [.NET 9.0 runtime](https://dotnet.microsoft.com/download/dotnet/9.0).
16 |
17 | ***
18 |
19 | ## Overview
20 |
21 | DotnetCat is a multithreaded console application that can be used to spawn bind and reverse
22 | command shells, upload and download files, perform connection testing, and transmit user-defined
23 | payloads. This application uses [Transmission Control Protocol (TCP)](https://www.ietf.org/rfc/rfc9293.html)
24 | network sockets to perform network communications.
25 |
26 | At its core, DotnetCat is built of unidirectional TCP [socket pipelines](src/DotnetCat/IO/Pipelines),
27 | each responsible for asynchronously reading from or writing to a connected socket. This allows a
28 | single socket stream to be used by multiple pipelines simultaneously without deadlock issues
29 | occurring.
30 |
31 | ### Features
32 |
33 | * Bind command shells
34 | * Reverse command shells
35 | * Remote file uploads and downloads
36 | * Connection probing
37 | * User-defined data transmission
38 |
39 | ***
40 |
41 | ## Basic Usage
42 |
43 | ### Linux Systems
44 |
45 | ```bash
46 | dncat [OPTIONS] TARGET
47 | ```
48 |
49 | ### Windows Systems
50 |
51 | ```powershell
52 | dncat.exe [OPTIONS] TARGET
53 | ```
54 |
55 | ***
56 |
57 | ## Command-Line Options
58 |
59 | All available DotnetCat command-line arguments are listed below:
60 |
61 | | Argument | Type | Description | Default |
62 | |:------------------:|:----------:|:----------------------------------:|:----------------:|
63 | | `TARGET` | *Required* | Host to use for the connection | *N/A or 0.0.0.0* |
64 | | `-p/--port PORT` | *Optional* | Port to use for the connection | *44444* |
65 | | `-e/--exec EXEC` | *Optional* | Pipe executable I/O data (shell) | *N/A* |
66 | | `-o/--output PATH` | *Optional* | Download a file from a remote host | *N/A* |
67 | | `-s/--send PATH` | *Optional* | Send a local file to a remote host | *N/A* |
68 | | `-t, --text` | *Optional* | Send a string to a remote host | *False* |
69 | | `-l, --listen` | *Optional* | Listen for an inbound connection | *False* |
70 | | `-z, --zero-io` | *Optional* | Determine if an endpoint is open | *False* |
71 | | `-v, --verbose` | *Optional* | Enable verbose console output | *False* |
72 | | `-h/-?, --help` | *Optional* | Display the app help menu and exit | *False* |
73 |
74 | > See the [Usage Examples](#usage-examples) section for more information.
75 |
76 | ***
77 |
78 | ## Installation
79 |
80 | DotnetCat can be automatically configured and installed or
81 | updated using the installers in the [tools](tools) directory.
82 |
83 | It can be installed manually by building from source or using the precompiled
84 | standalone executables in the [Zips](src/DotnetCat/bin/Zips) directory.
85 |
86 | ### Linux Systems
87 |
88 | Download and execute the [dncat-install.sh](tools/dncat-install.sh) installer script using Bash:
89 |
90 | ```bash
91 | curl -sLS "https://raw.githubusercontent.com/vandavey/DotnetCat/master/tools/dncat-install.sh" | bash
92 | ```
93 |
94 |
95 | dncat-install.sh only supports ARM64 and
96 | x64 architectures and is dependent on 7-Zip
97 | and curl
.
98 |
99 |
100 | ### Windows Systems
101 |
102 | Download and execute the [dncat-install.ps1](tools/dncat-install.ps1) installer script using PowerShell:
103 |
104 | ```powershell
105 | irm -d "https://raw.githubusercontent.com/vandavey/DotnetCat/master/tools/dncat-install.ps1" | powershell -
106 | ```
107 |
108 | > [dncat-install.ps1](tools/dncat-install.ps1) only supports *x64*
109 | and *x86* architectures and must be executed as an administrator.
110 |
111 | ### Manual Setup
112 |
113 | DotnetCat can be manually installed using the following precompiled standalone executables:
114 |
115 | * [Linux-x64](https://raw.githubusercontent.com/vandavey/DotnetCat/master/src/DotnetCat/bin/Zips/DotnetCat_linux-x64.zip)
116 | * [Linux-ARM64](https://raw.githubusercontent.com/vandavey/DotnetCat/master/src/DotnetCat/bin/Zips/DotnetCat_linux-arm64.zip)
117 | * [Windows-x64](https://raw.githubusercontent.com/vandavey/DotnetCat/master/src/DotnetCat/bin/Zips/DotnetCat_win-x64.zip)
118 | * [Windows-x86](https://raw.githubusercontent.com/vandavey/DotnetCat/master/src/DotnetCat/bin/Zips/DotnetCat_win-x86.zip)
119 |
120 | It can be built from source by publishing [DotnetCat.csproj](src/DotnetCat/DotnetCat.csproj) using
121 | the publish profiles in the [PublishProfiles](src/DotnetCat/Properties/PublishProfiles) directory.
122 |
123 | ***
124 |
125 | ## Uninstallation
126 |
127 | DotnetCat can be uninstalled automatically using the uninstallers in the [tools](tools) directory.
128 |
129 | It can be uninstalled manually by deleting the install
130 | directory and removing it from the local environment path.
131 |
132 | ### Linux Systems
133 |
134 | Execute the [dncat-uninstall.sh](tools/dncat-uninstall.sh) uninstaller script using Bash:
135 |
136 | ```bash
137 | source /opt/dncat/bin/dncat-uninstall.sh
138 | ```
139 |
140 | > [dncat-uninstall.sh](tools/dncat-uninstall.sh) only supports *ARM64* and *x64* architectures.
141 |
142 | ### Windows Systems
143 |
144 | Execute the [dncat-uninstall.ps1](tools/dncat-uninstall.ps1) uninstaller script using PowerShell:
145 |
146 | ```powershell
147 | gc "${env:ProgramFiles}\DotnetCat\dncat-uninstall.ps1" | powershell -
148 | ```
149 |
150 | > [dncat-uninstall.ps1](tools/dncat-uninstall.ps1) only supports *x64*
151 | and *x86* architectures and must be executed as an administrator.
152 |
153 | ***
154 |
155 | ## Usage Examples
156 |
157 | ### Basic Operations
158 |
159 | Print the application help menu, then exit:
160 |
161 | ```powershell
162 | dncat --help
163 | ```
164 |
165 | Connect to remote endpoint `192.168.1.1:1524`:
166 |
167 | ```powershell
168 | dncat "192.168.1.1" --port 1524
169 | ```
170 |
171 | Listen for an inbound connection on any local Wi-Fi interface:
172 |
173 | ```powershell
174 | dncat --listen
175 | ```
176 |
177 | > `TARGET` defaults to `0.0.0.0` when the `-l` or `--listen` flag is specified.
178 |
179 | Determine whether `localhost` is accepting connections on port `22`:
180 |
181 | ```powershell
182 | dncat -z localhost -p 22
183 | ```
184 |
185 | ### Command Shells
186 |
187 | Connect to remote endpoint `127.0.0.1:4444` to establish a bind shell:
188 |
189 | ```powershell
190 | dncat "127.0.0.1" -p 4444
191 | ```
192 |
193 | Listen for an inbound connection to establish a reverse `bash` shell:
194 |
195 | ```powershell
196 | dncat -lv --exec bash
197 | ```
198 |
199 | ### Data Transfer
200 |
201 | Transmit string payload *Hello world!* to remote endpoint `fake.addr.com:80`:
202 |
203 | ```powershell
204 | dncat -vt "Hello world!" fake.addr.com -p 80
205 | ```
206 |
207 | ### File Transfer
208 |
209 | Listen for inbound file data and write the contents to path `C:\TestFile.txt`:
210 |
211 | ```powershell
212 | dncat -lvo C:\TestFile.txt
213 | ```
214 |
215 | Transmit the contents of file `/home/user/profit.json` to remote target `Joe-Mama`:
216 |
217 | ```powershell
218 | dncat --send /home/user/profit.json Joe-Mama
219 | ```
220 |
221 | ***
222 |
223 | ## Remarks
224 |
225 | * This application only supports Linux and Windows operating systems.
226 | * Please use discretion as this application is still in development.
227 |
228 | ***
229 |
230 | ## Copyright & Licensing
231 |
232 | DotnetCat is licensed under the [MIT license](LICENSE.md) and officially
233 | hosted in [this](https://github.com/vandavey/DotnetCat) repository.
234 |
--------------------------------------------------------------------------------
/src/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | # All text files
4 | [*]
5 |
6 | #### Core EditorConfig Options ####
7 |
8 | # Indentation and spacing
9 | indent_size = 4
10 | indent_style = space
11 | tab_width = 4
12 | trim_trailing_whitespace = true
13 |
14 | # New line and encoding preferences
15 | charset = utf-8
16 | end_of_line = lf
17 | insert_final_newline = true
18 |
19 | #### .NET Coding Conventions ####
20 |
21 | # Organize usings
22 | dotnet_separate_import_directive_groups = false
23 | dotnet_sort_system_directives_first = true
24 | file_header_template = unset
25 |
26 | # this. and Me. preferences
27 | dotnet_style_qualification_for_event = false:suggestion
28 | dotnet_style_qualification_for_field = false:suggestion
29 | dotnet_style_qualification_for_method = false:suggestion
30 | dotnet_style_qualification_for_property = false:suggestion
31 |
32 | # Language keywords vs BCL types preferences
33 | dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion
34 | dotnet_style_predefined_type_for_member_access = true:suggestion
35 |
36 | # Parentheses preferences
37 | dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:suggestion
38 | dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:suggestion
39 | dotnet_style_parentheses_in_other_operators = never_if_unnecessary:suggestion
40 | dotnet_style_parentheses_in_relational_binary_operators = never_if_unnecessary:suggestion
41 |
42 | # Modifier preferences
43 | dotnet_style_require_accessibility_modifiers = always:suggestion
44 |
45 | # Expression-level preferences
46 | dotnet_style_coalesce_expression = true:suggestion
47 | dotnet_style_collection_initializer = true:suggestion
48 | dotnet_style_explicit_tuple_names = true:suggestion
49 | dotnet_style_namespace_match_folder = true:suggestion
50 | dotnet_style_null_propagation = true:suggestion
51 | dotnet_style_object_initializer = true:suggestion
52 | dotnet_style_operator_placement_when_wrapping = beginning_of_line
53 | dotnet_style_prefer_auto_properties = true:suggestion
54 | dotnet_style_prefer_collection_expression = when_types_loosely_match:suggestion
55 | dotnet_style_prefer_compound_assignment = true:suggestion
56 | dotnet_style_prefer_conditional_expression_over_assignment = true:silent
57 | dotnet_style_prefer_conditional_expression_over_return = true:silent
58 | dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed
59 | dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
60 | dotnet_style_prefer_inferred_tuple_names = true:suggestion
61 | dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion
62 | dotnet_style_prefer_simplified_boolean_expressions = true:suggestion
63 | dotnet_style_prefer_simplified_interpolation = true:suggestion
64 |
65 | # Field preferences
66 | dotnet_style_readonly_field = true:suggestion
67 |
68 | # Parameter preferences
69 | dotnet_code_quality_unused_parameters = all:suggestion
70 |
71 | # Suppression preferences
72 | dotnet_remove_unnecessary_suppression_exclusions = none
73 |
74 | # New line preferences
75 | dotnet_style_allow_multiple_blank_lines_experimental = false:suggestion
76 | dotnet_style_allow_statement_immediately_after_block_experimental = true:silent
77 |
78 | #### C# Coding Conventions ####
79 |
80 | # var preferences
81 | csharp_style_var_elsewhere = false:suggestion
82 | csharp_style_var_for_built_in_types = false:suggestion
83 | csharp_style_var_when_type_is_apparent = false:suggestion
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:silent
90 | csharp_style_expression_bodied_local_functions = true: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_extended_property_pattern = true:suggestion
99 | csharp_style_prefer_not_pattern = true:suggestion
100 | csharp_style_prefer_pattern_matching = true:suggestion
101 | csharp_style_prefer_switch_expression = true:suggestion
102 |
103 | # Null-checking preferences
104 | csharp_style_conditional_delegate_call = true:suggestion
105 |
106 | # Modifier preferences
107 | csharp_prefer_static_anonymous_function = true:suggestion
108 | csharp_prefer_static_local_function = true:suggestion
109 | csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async
110 | csharp_style_prefer_readonly_struct = true:suggestion
111 | csharp_style_prefer_readonly_struct_member = true:suggestion
112 |
113 | # Code-block preferences
114 | csharp_prefer_braces = true:suggestion
115 | csharp_prefer_simple_using_statement = true:suggestion
116 | csharp_style_namespace_declarations = file_scoped:suggestion
117 | csharp_style_prefer_method_group_conversion = true:suggestion
118 | csharp_style_prefer_primary_constructors = false:suggestion
119 | csharp_style_prefer_top_level_statements = false:suggestion
120 |
121 | # Expression-level preferences
122 | csharp_prefer_simple_default_expression = true:suggestion
123 | csharp_style_deconstructed_variable_declaration = true:suggestion
124 | csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion
125 | csharp_style_inlined_variable_declaration = true:suggestion
126 | csharp_style_prefer_index_operator = true:suggestion
127 | csharp_style_prefer_local_over_anonymous_function = true:silent
128 | csharp_style_prefer_null_check_over_type_check = true:suggestion
129 | csharp_style_prefer_range_operator = true:suggestion
130 | csharp_style_prefer_tuple_swap = true:suggestion
131 | csharp_style_prefer_utf8_string_literals = true:suggestion
132 | csharp_style_throw_expression = true:silent
133 | csharp_style_unused_value_assignment_preference = discard_variable:suggestion
134 | csharp_style_unused_value_expression_statement_preference = discard_variable:silent
135 |
136 | # 'using' directive preferences
137 | csharp_using_directive_placement = outside_namespace:silent
138 |
139 | # New line preferences
140 | csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = false:suggestion
141 | csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true:silent
142 | csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = false:silent
143 | csharp_style_allow_blank_lines_between_consecutive_braces_experimental = false:suggestion
144 | csharp_style_allow_embedded_statements_on_same_line_experimental = true:silent
145 |
146 | #### C# Formatting Rules ####
147 |
148 | # New line preferences
149 | csharp_new_line_before_catch = true
150 | csharp_new_line_before_else = true
151 | csharp_new_line_before_finally = true
152 | csharp_new_line_before_members_in_anonymous_types = true
153 | csharp_new_line_before_members_in_object_initializers = true
154 | csharp_new_line_before_open_brace = all
155 | csharp_new_line_between_query_expression_clauses = true
156 |
157 | # Indentation preferences
158 | csharp_indent_block_contents = true
159 | csharp_indent_braces = false
160 | csharp_indent_case_contents = true
161 | csharp_indent_case_contents_when_block = false
162 | csharp_indent_labels = one_less_than_current
163 | csharp_indent_switch_labels = true
164 |
165 | # Space preferences
166 | csharp_space_after_cast = false
167 | csharp_space_after_colon_in_inheritance_clause = true
168 | csharp_space_after_comma = true
169 | csharp_space_after_dot = false
170 | csharp_space_after_keywords_in_control_flow_statements = true
171 | csharp_space_after_semicolon_in_for_statement = true
172 | csharp_space_around_binary_operators = before_and_after
173 | csharp_space_around_declaration_statements = false
174 | csharp_space_before_colon_in_inheritance_clause = true
175 | csharp_space_before_comma = false
176 | csharp_space_before_dot = false
177 | csharp_space_before_open_square_brackets = false
178 | csharp_space_before_semicolon_in_for_statement = false
179 | csharp_space_between_empty_square_brackets = false
180 | csharp_space_between_method_call_empty_parameter_list_parentheses = false
181 | csharp_space_between_method_call_name_and_opening_parenthesis = false
182 | csharp_space_between_method_call_parameter_list_parentheses = false
183 | csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
184 | csharp_space_between_method_declaration_name_and_open_parenthesis = false
185 | csharp_space_between_method_declaration_parameter_list_parentheses = false
186 | csharp_space_between_parentheses = false
187 | csharp_space_between_square_brackets = false
188 |
189 | # Wrapping preferences
190 | csharp_preserve_single_line_blocks = true
191 | csharp_preserve_single_line_statements = false
192 |
193 | #### Naming Styles ####
194 |
195 | # Naming rules
196 |
197 | dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion
198 | dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface
199 | dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i
200 |
201 | dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion
202 | dotnet_naming_rule.types_should_be_pascal_case.symbols = types
203 | dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case
204 |
205 | dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion
206 | dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members
207 | dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case
208 |
209 | # Symbol specifications
210 |
211 | dotnet_naming_symbols.interface.applicable_kinds = interface
212 | dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
213 | dotnet_naming_symbols.interface.required_modifiers =
214 |
215 | dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum
216 | dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
217 | dotnet_naming_symbols.types.required_modifiers =
218 |
219 | dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
220 | dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
221 | dotnet_naming_symbols.non_field_members.required_modifiers =
222 |
223 | # Naming styles
224 |
225 | dotnet_naming_style.pascal_case.required_prefix =
226 | dotnet_naming_style.pascal_case.required_suffix =
227 | dotnet_naming_style.pascal_case.word_separator =
228 | dotnet_naming_style.pascal_case.capitalization = pascal_case
229 |
230 | dotnet_naming_style.begins_with_i.required_prefix = I
231 | dotnet_naming_style.begins_with_i.required_suffix =
232 | dotnet_naming_style.begins_with_i.word_separator =
233 | dotnet_naming_style.begins_with_i.capitalization = pascal_case
234 |
235 | #### VSSPELL Options ####
236 |
237 | # VSSPELL: Spell Checker Settings
238 | vsspell_code_analyzers_enabled = true
239 | vsspell_detect_doubled_words = true
240 | vsspell_ignore_format_specifiers = true
241 | vsspell_spell_check_as_you_type = true
242 | vsspell_treat_underscore_as_separator = true
243 |
244 | # VSSPELL: Spell Checker Ignored Words
245 | vsspell_section_id = 351afbc38c654ab7b190db1dc718de3c
246 | vsspell_ignored_keywords_351afbc38c654ab7b190db1dc718de3c = cts|delim|dest|dncat|eol|env|esc|exe|pwsh|recv|repo|src
247 |
--------------------------------------------------------------------------------
/src/DotnetCat.sln:
--------------------------------------------------------------------------------
1 | Microsoft Visual Studio Solution File, Format Version 12.00
2 | # Visual Studio Version 17
3 | VisualStudioVersion = 17.12.3
4 | MinimumVisualStudioVersion = 17.4.0
5 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotnetCat", "DotNetCat\DotnetCat.csproj", "{88C24379-AE50-44CE-9B66-4DFB95FE51D5}"
6 | EndProject
7 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{19DAD4D2-BB3A-45ED-A024-938DAEA54EB8}"
8 | ProjectSection(SolutionItems) = preProject
9 | .editorconfig = .editorconfig
10 | ..\.gitattributes = ..\.gitattributes
11 | ..\.gitignore = ..\.gitignore
12 | ..\LICENSE.md = ..\LICENSE.md
13 | ..\README.md = ..\README.md
14 | ..\tools\dncat-install.ps1 = ..\tools\dncat-install.ps1
15 | ..\tools\dncat-install.sh = ..\tools\dncat-install.sh
16 | ..\tools\dncat-uninstall.ps1 = ..\tools\dncat-uninstall.ps1
17 | ..\tools\dncat-uninstall.sh = ..\tools\dncat-uninstall.sh
18 | EndProjectSection
19 | EndProject
20 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotnetCatTests", "DotnetCatTests\DotnetCatTests.csproj", "{35960C17-17B6-4245-8F20-AF7C70C030E1}"
21 | EndProject
22 | Global
23 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
24 | ARM64|Any CPU = ARM64|Any CPU
25 | Debug|Any CPU = Debug|Any CPU
26 | Linux|Any CPU = Linux|Any CPU
27 | Release|Any CPU = Release|Any CPU
28 | EndGlobalSection
29 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
30 | {88C24379-AE50-44CE-9B66-4DFB95FE51D5}.ARM64|Any CPU.ActiveCfg = ARM64|Any CPU
31 | {88C24379-AE50-44CE-9B66-4DFB95FE51D5}.ARM64|Any CPU.Build.0 = ARM64|Any CPU
32 | {88C24379-AE50-44CE-9B66-4DFB95FE51D5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
33 | {88C24379-AE50-44CE-9B66-4DFB95FE51D5}.Debug|Any CPU.Build.0 = Debug|Any CPU
34 | {88C24379-AE50-44CE-9B66-4DFB95FE51D5}.Linux|Any CPU.ActiveCfg = Linux|Any CPU
35 | {88C24379-AE50-44CE-9B66-4DFB95FE51D5}.Linux|Any CPU.Build.0 = Linux|Any CPU
36 | {88C24379-AE50-44CE-9B66-4DFB95FE51D5}.Release|Any CPU.ActiveCfg = Release|Any CPU
37 | {88C24379-AE50-44CE-9B66-4DFB95FE51D5}.Release|Any CPU.Build.0 = Release|Any CPU
38 | {35960C17-17B6-4245-8F20-AF7C70C030E1}.ARM64|Any CPU.ActiveCfg = ARM64|Any CPU
39 | {35960C17-17B6-4245-8F20-AF7C70C030E1}.ARM64|Any CPU.Build.0 = ARM64|Any CPU
40 | {35960C17-17B6-4245-8F20-AF7C70C030E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
41 | {35960C17-17B6-4245-8F20-AF7C70C030E1}.Debug|Any CPU.Build.0 = Debug|Any CPU
42 | {35960C17-17B6-4245-8F20-AF7C70C030E1}.Linux|Any CPU.ActiveCfg = Linux|Any CPU
43 | {35960C17-17B6-4245-8F20-AF7C70C030E1}.Linux|Any CPU.Build.0 = Linux|Any CPU
44 | {35960C17-17B6-4245-8F20-AF7C70C030E1}.Release|Any CPU.ActiveCfg = Release|Any CPU
45 | {35960C17-17B6-4245-8F20-AF7C70C030E1}.Release|Any CPU.Build.0 = Release|Any CPU
46 | EndGlobalSection
47 | GlobalSection(SolutionProperties) = preSolution
48 | HideSolutionNode = FALSE
49 | EndGlobalSection
50 | GlobalSection(ExtensibilityGlobals) = postSolution
51 | SolutionGuid = {090BA55F-C5A4-49A1-8655-ED89ACC022F5}
52 | EndGlobalSection
53 | EndGlobal
54 |
--------------------------------------------------------------------------------
/src/DotnetCat/DotnetCat.csproj:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 | true
7 | Resources/Icon.ico
8 | dncat
9 | vandavey
10 | Debug; Release; Linux; ARM64
11 | Copyright (c) 2025 vandavey
12 | false
13 | None
14 | DotnetCat
15 | None
16 | false
17 | Disable
18 | true
19 | obj/$(Configuration)
20 | false
21 | Enable
22 | true
23 | bin/$(Configuration)
24 | Exe
25 | $(SolutionDir)../LICENSE.md
26 | AnyCPU
27 | AnyCPU
28 | true
29 | true
30 | true
31 | true
32 | true
33 | https://github.com/vandavey/DotnetCat
34 | DotnetCat
35 | true
36 | DotnetCat.Program
37 | net9.0
38 | 7
39 |
40 |
41 |
44 |
45 | true
46 | Portable
47 | false
48 | false
49 |
50 |
51 |
54 |
55 | true
56 | true
57 | true
58 | true
59 | true
60 |
61 |
62 |
65 |
66 | LINUX
67 | WINDOWS
68 |
69 |
70 |
73 |
74 |
75 |
76 |
77 |
80 |
81 |
82 |
83 |
84 |
85 |
88 |
89 |
90 |
91 | Always
92 | %(Filename)%(Extension)
93 |
94 |
95 |
96 |
97 | Always
98 | %(Filename)%(Extension)
99 |
100 |
101 |
102 |
103 | Always
104 | %(Filename)%(Extension)
105 |
106 |
107 |
108 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
121 |
122 |
123 |
126 |
127 |
128 |
--------------------------------------------------------------------------------
/src/DotnetCat/Errors/Error.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Diagnostics.CodeAnalysis;
3 | using DotnetCat.IO;
4 | using DotnetCat.Network;
5 | using DotnetCat.Utils;
6 |
7 | namespace DotnetCat.Errors;
8 |
9 | ///
10 | /// Error and exception utility class.
11 | ///
12 | internal static class Error
13 | {
14 | ///
15 | /// Initialize the static class members.
16 | ///
17 | static Error() => Verbose = false;
18 |
19 | ///
20 | /// Enable verbose exceptions.
21 | ///
22 | public static bool Verbose { get; set; }
23 |
24 | ///
25 | /// Handle user-defined exceptions related to DotnetCat and exit.
26 | ///
27 | [DoesNotReturn]
28 | public static void Handle(Except exType,
29 | [NotNull] string? arg,
30 | Exception? ex = default)
31 | {
32 | Handle(exType, arg, false, ex);
33 | }
34 |
35 | ///
36 | /// Handle user-defined exceptions related to DotnetCat and exit.
37 | ///
38 | [DoesNotReturn]
39 | public static void Handle(Except exType,
40 | [NotNull] string? arg,
41 | bool showUsage,
42 | Exception? ex = default)
43 | {
44 | ThrowIf.NullOrEmpty(arg);
45 |
46 | // Print application usage
47 | if (showUsage)
48 | {
49 | Console.WriteLine(Parser.Usage);
50 | }
51 |
52 | ErrorMessage errorMsg = MakeErrorMessage(exType, arg);
53 | Output.Error(errorMsg.Message);
54 |
55 | // Print verbose error details
56 | if (Verbose && ex is not null)
57 | {
58 | if (ex is AggregateException aggregateEx)
59 | {
60 | ex = Net.SocketException(aggregateEx) ?? ex;
61 | }
62 |
63 | string errorName = Sequence.Colorize(ex.GetType().FullName, ConsoleColor.Red);
64 | string header = $"----[ {errorName} ]----";
65 |
66 | Console.WriteLine($"""
67 | {header}
68 | {ex?.ToString()}
69 | {new string('-', Sequence.Length(header))}
70 | """
71 | );
72 | }
73 |
74 | Console.WriteLine();
75 | Environment.Exit(1);
76 | }
77 |
78 | ///
79 | /// Get a new error message that corresponds to the given
80 | /// exception enumeration type.
81 | ///
82 | private static ErrorMessage MakeErrorMessage(Except exType, string? arg = default)
83 | {
84 | ErrorMessage message = new(exType switch
85 | {
86 | Except.AddressInUse => "The endpoint is already in use: %",
87 | Except.ArgsCombo => "Invalid argument combination: %",
88 | Except.ConnectionAborted => "Local software aborted connection to %",
89 | Except.ConnectionRefused => "Connection was actively refused by %",
90 | Except.ConnectionReset => "Connection was reset by %",
91 | Except.DirectoryPath => "Unable to locate parent directory: '%'",
92 | Except.EmptyPath => "A value is required for option(s): %",
93 | Except.ExePath => "Unable to locate executable file: '%'",
94 | Except.ExeProcess => "Unable to launch executable process: %",
95 | Except.FilePath => "Unable to locate file: '%'",
96 | Except.HostNotFound => "Unable to resolve hostname: '%'",
97 | Except.HostUnreachable => "Unable to reach host %",
98 | Except.InvalidArgs => "Unable to validate argument(s): %",
99 | Except.InvalidPort => "'%' is not a valid port number",
100 | Except.NamedArgs => "Missing value for named argument(s): %",
101 | Except.NetworkDown => "The network is down: %",
102 | Except.NetworkReset => "Connection to % was lost in network reset",
103 | Except.NetworkUnreachable => "The network is unreachable: %",
104 | Except.Payload => "Invalid payload data: %",
105 | Except.RequiredArgs => "Missing required argument(s): %",
106 | Except.SocketError => "Unspecified socket error occurred: %",
107 | Except.StringEol => "Missing EOL in argument(s): %",
108 | Except.TimedOut => "Socket timeout occurred: %",
109 | Except.UnknownArgs => "One or more unknown arguments: %",
110 | Except.Unhandled or _ => "Unhandled exception occurred: %"
111 | });
112 |
113 | if (!arg.IsNullOrEmpty())
114 | {
115 | message.Build(arg);
116 | }
117 | return message;
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/src/DotnetCat/Errors/ErrorMessage.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace DotnetCat.Errors;
4 |
5 | ///
6 | /// Custom error message specifically related to DotnetCat.
7 | ///
8 | internal class ErrorMessage
9 | {
10 | private string? _message; // Error message
11 |
12 | ///
13 | /// Initialize the object.
14 | ///
15 | public ErrorMessage(string msg) => Message = msg;
16 |
17 | ///
18 | /// Error message string.
19 | ///
20 | public string Message
21 | {
22 | get => _message ??= string.Empty;
23 | private set
24 | {
25 | ThrowIf.NullOrEmpty(value);
26 |
27 | if (MsgBuilt(value))
28 | {
29 | throw new ArgumentException("Message already built.", nameof(value));
30 | }
31 | _message = value;
32 | }
33 | }
34 |
35 | ///
36 | /// Interpolate the given argument in the underlying message string.
37 | ///
38 | public string Build(string? arg)
39 | {
40 | if (MsgBuilt())
41 | {
42 | throw new InvalidOperationException("Underlying message already built.");
43 | }
44 | return _message = Message.Replace("%", arg).Replace("{}", arg);
45 | }
46 |
47 | ///
48 | /// Determine whether the given message string contains
49 | /// any format specifier placeholders (%, {}).
50 | ///
51 | private static bool MsgBuilt(string msg) => !msg.Contains('%') && !msg.Contains("{}");
52 |
53 | ///
54 | /// Determine whether the underlying message string contains
55 | /// any format specifier placeholders (%, {}).
56 | ///
57 | private bool MsgBuilt() => MsgBuilt(Message);
58 | }
59 |
--------------------------------------------------------------------------------
/src/DotnetCat/Errors/Except.cs:
--------------------------------------------------------------------------------
1 | namespace DotnetCat.Errors;
2 |
3 | ///
4 | /// DotnetCat error and exception enumeration type.
5 | ///
6 | internal enum Except : byte
7 | {
8 | ///
9 | /// Unhandled exception occurred.
10 | ///
11 | Unhandled,
12 |
13 | ///
14 | /// Local IP endpoint already in use.
15 | ///
16 | AddressInUse,
17 |
18 | ///
19 | /// Invalid argument combination specified.
20 | ///
21 | ArgsCombo,
22 |
23 | ///
24 | /// Socket connection was aborted by software.
25 | ///
26 | ConnectionAborted,
27 |
28 | ///
29 | /// Socket connection was refused.
30 | ///
31 | ConnectionRefused,
32 |
33 | ///
34 | /// Socket connection was reset.
35 | ///
36 | ConnectionReset,
37 |
38 | ///
39 | /// Invalid directory path specified.
40 | ///
41 | DirectoryPath,
42 |
43 | ///
44 | /// File path missing in named argument.
45 | ///
46 | EmptyPath,
47 |
48 | ///
49 | /// Invalid executable file path specified.
50 | ///
51 | ExePath,
52 |
53 | ///
54 | /// One or more executable process errors occurred.
55 | ///
56 | ExeProcess,
57 |
58 | ///
59 | /// Invalid file path specified.
60 | ///
61 | FilePath,
62 |
63 | ///
64 | /// Invalid IP address or DNS hostname resolution failure.
65 | ///
66 | HostNotFound,
67 |
68 | ///
69 | /// Unable to find route to specified host.
70 | ///
71 | HostUnreachable,
72 |
73 | ///
74 | /// One or more invalid arguments specified.
75 | ///
76 | InvalidArgs,
77 |
78 | ///
79 | /// One or more invalid network port numbers specified.
80 | ///
81 | InvalidPort,
82 |
83 | ///
84 | /// One or more invalid named arguments specified.
85 | ///
86 | NamedArgs,
87 |
88 | ///
89 | /// Local network is down.
90 | ///
91 | NetworkDown,
92 |
93 | ///
94 | /// Connection dropped in network reset.
95 | ///
96 | NetworkReset,
97 |
98 | ///
99 | /// Attempted to communicate with unreachable network.
100 | ///
101 | NetworkUnreachable,
102 |
103 | ///
104 | /// Invalid string payload argument specified.
105 | ///
106 | Payload,
107 |
108 | ///
109 | /// One or more invalid required arguments specified.
110 | ///
111 | RequiredArgs,
112 |
113 | ///
114 | /// Unspecified socket error occurred.
115 | ///
116 | SocketError,
117 |
118 | ///
119 | /// Missing one or more string EOL characters.
120 | ///
121 | StringEol,
122 |
123 | ///
124 | /// Socket connection timeout occurred.
125 | ///
126 | TimedOut,
127 |
128 | ///
129 | /// One or more unknown or unexpected arguments specified.
130 | ///
131 | UnknownArgs
132 | }
133 |
--------------------------------------------------------------------------------
/src/DotnetCat/Errors/ThrowIf.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Diagnostics.CodeAnalysis;
4 | using System.Linq;
5 | using System.Numerics;
6 | using System.Runtime.CompilerServices;
7 | using System.Net;
8 | using System.Net.Sockets;
9 | using DotnetCat.Network;
10 |
11 | #if WINDOWS
12 | using System.Runtime.InteropServices;
13 | #endif // WINDOWS
14 |
15 | namespace DotnetCat.Errors;
16 |
17 | ///
18 | /// Utility class for exception handling and validation.
19 | ///
20 | internal class ThrowIf
21 | {
22 | #if WINDOWS
23 | ///
24 | /// Throw an exception if the given safe handle is invalid.
25 | ///
26 | public static void InvalidHandle([NotNull] SafeHandle arg,
27 | [CallerArgumentExpression(nameof(arg))]
28 | string? name = default)
29 | {
30 | if (arg.IsInvalid)
31 | {
32 | throw new ArgumentException($"Invalid handle: {arg}.", name);
33 | }
34 | }
35 | #endif // WINDOWS
36 |
37 | ///
38 | /// Throw an exception if the given port number is invalid.
39 | ///
40 | public static void InvalidPort([NotNull] int arg,
41 | [CallerArgumentExpression(nameof(arg))]
42 | string? name = default)
43 | {
44 | if (!Net.ValidPort(arg))
45 | {
46 | throw new ArgumentOutOfRangeException($"Invalid port number: {arg}.", name);
47 | }
48 | }
49 |
50 | ///
51 | /// Throw an exception if the given number is less than zero.
52 | ///
53 | public static void Negative([NotNull] T arg,
54 | [CallerArgumentExpression(nameof(arg))]
55 | string? name = default)
56 | where T : INumber
57 | {
58 | ArgumentOutOfRangeException.ThrowIfNegative(arg, name);
59 | }
60 |
61 | ///
62 | /// Throw an exception if the given IP address is not an IPv4 address.
63 | ///
64 | public static void NotIPv4Address([NotNull] IPAddress? arg,
65 | [CallerArgumentExpression(nameof(arg))]
66 | string? name = default)
67 | {
68 | if (arg?.AddressFamily is not AddressFamily.InterNetwork)
69 | {
70 | throw new ArgumentException("IP address family is not IPv4.", name);
71 | }
72 | }
73 |
74 | ///
75 | /// Throw an exception if the given argument is null.
76 | ///
77 | public static void Null([NotNull] T? arg,
78 | [CallerArgumentExpression(nameof(arg))]
79 | string? name = default)
80 | {
81 | ArgumentNullException.ThrowIfNull(arg, name);
82 | }
83 |
84 | ///
85 | /// Throw an exception if the given argument is null or empty.
86 | ///
87 | public static void NullOrEmpty([NotNull] IEnumerable? arg,
88 | [CallerArgumentExpression(nameof(arg))]
89 | string? name = default)
90 | {
91 | Null(arg, name);
92 |
93 | if (!arg.Any())
94 | {
95 | throw new ArgumentException("Collection cannot be empty.", name);
96 | }
97 | }
98 |
99 | ///
100 | /// Throw an exception if the given argument is null or empty.
101 | ///
102 | public static void NullOrEmpty([NotNull] string? arg,
103 | [CallerArgumentExpression(nameof(arg))]
104 | string? name = default)
105 | {
106 | ArgumentException.ThrowIfNullOrWhiteSpace(arg, name);
107 | }
108 |
109 | ///
110 | /// Throw an exception if the given number is zero.
111 | ///
112 | public static void Zero([NotNull] T arg,
113 | [CallerArgumentExpression(nameof(arg))]
114 | string? name = default)
115 | where T : INumberBase
116 | {
117 | ArgumentOutOfRangeException.ThrowIfZero(arg, name);
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/src/DotnetCat/IO/FileSys.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Diagnostics.CodeAnalysis;
4 | using System.IO;
5 | using System.Linq;
6 | using DotnetCat.Errors;
7 | using DotnetCat.Shell;
8 | using DotnetCat.Utils;
9 | using SpecialFolder = System.Environment.SpecialFolder;
10 |
11 | namespace DotnetCat.IO;
12 |
13 | ///
14 | /// File system utility class.
15 | ///
16 | internal static class FileSys
17 | {
18 | private static readonly string[] _envPaths; // Environment path directories
19 | private static readonly string[] _pathExtensions; // Environment path file extensions
20 |
21 | ///
22 | /// Initialize the static class members.
23 | ///
24 | static FileSys()
25 | {
26 | _envPaths = EnvironmentPaths();
27 | _pathExtensions = PathExtensions();
28 | }
29 |
30 | ///
31 | /// User home directory absolute file path.
32 | ///
33 | public static string UserProfile
34 | {
35 | get => Environment.GetFolderPath(SpecialFolder.UserProfile);
36 | }
37 |
38 | ///
39 | /// Determine whether a file system entry exists at the given file path.
40 | ///
41 | public static bool Exists([NotNullWhen(true)] string? path)
42 | {
43 | return FileExists(path) || DirectoryExists(path);
44 | }
45 |
46 | ///
47 | /// Determine whether a file entry exists at the given file path.
48 | ///
49 | public static bool FileExists([NotNullWhen(true)] string? path)
50 | {
51 | return File.Exists(ResolvePath(path));
52 | }
53 |
54 | ///
55 | /// Determine whether a directory entry exists at the given file path.
56 | ///
57 | public static bool DirectoryExists([NotNullWhen(true)] string? path)
58 | {
59 | return Directory.Exists(ResolvePath(path));
60 | }
61 |
62 | ///
63 | /// Get the absolute parent directory path from the given file path.
64 | ///
65 | [return: NotNullIfNotNull(nameof(path))]
66 | public static string? ParentPath(string? path)
67 | {
68 | string? parent = null;
69 |
70 | if (!path.IsNullOrEmpty())
71 | {
72 | parent = Directory.GetParent(path)?.FullName;
73 | }
74 | return ResolvePath(parent);
75 | }
76 |
77 | ///
78 | /// Resolve the absolute file path of the given relative file path.
79 | ///
80 | [return: NotNullIfNotNull(nameof(path))]
81 | public static string? ResolvePath(string? path)
82 | {
83 | string? fullPath = null;
84 |
85 | if (!path.IsNullOrEmpty())
86 | {
87 | if (SysInfo.IsWindows() && path.EndsWithValue(Path.VolumeSeparatorChar))
88 | {
89 | path += Path.DirectorySeparatorChar;
90 | }
91 | fullPath = Path.GetFullPath(path.Replace("~", UserProfile));
92 | }
93 | return fullPath;
94 | }
95 |
96 | ///
97 | /// Determine whether the given executable can
98 | /// be located using the local environment path.
99 | ///
100 | public static bool ExistsOnPath([NotNull] string? exe,
101 | [NotNullWhen(true)] out string? path)
102 | {
103 | ThrowIf.NullOrEmpty(exe);
104 | path = null;
105 |
106 | if (FileExists(exe))
107 | {
108 | path = ResolvePath(exe);
109 | }
110 | else if (!Path.IsPathRooted(exe))
111 | {
112 | path = FindExecutable(exe);
113 | }
114 | return path is not null;
115 | }
116 |
117 | ///
118 | /// Get the directory paths defined by the local environment path variable.
119 | ///
120 | private static string[] EnvironmentPaths()
121 | {
122 | if (!Command.TryEnvVariable("PATH", out string? envPath))
123 | {
124 | throw new InvalidOperationException("Failed to get local environment paths.");
125 | }
126 | return envPath.Split(Path.PathSeparator);
127 | }
128 |
129 | ///
130 | /// Get the locally supported environment path executable file extensions.
131 | ///
132 | private static string[] PathExtensions()
133 | {
134 | string[] extensions;
135 |
136 | if (SysInfo.IsWindows())
137 | {
138 | if (Command.TryEnvVariable("PATHEXT", out string? pathExt))
139 | {
140 | extensions = pathExt.Split(Path.PathSeparator);
141 | }
142 | else // Use common Windows extensions
143 | {
144 | extensions = [".exe", ".bat", ".cmd", ".ps1", ".py"];
145 | }
146 | }
147 | else // Use Linux extensions
148 | {
149 | extensions = [".sh", ".py", ".bin", ".run", ".elf"];
150 | }
151 |
152 | return [.. extensions.Select(e => e.ToLower())];
153 | }
154 |
155 | ///
156 | /// Search the local environment path for the given executable.
157 | ///
158 | private static string? FindExecutable([NotNull] string? exe)
159 | {
160 | ThrowIf.NullOrEmpty(exe);
161 | string exeName = Path.GetFileName(exe);
162 |
163 | IEnumerable executables =
164 | from dir in _envPaths
165 | where DirectoryExists(dir)
166 | from file in Directory.GetFiles(dir)
167 | where file.EndsWith(Path.DirectorySeparatorChar + exeName)
168 | || (!Path.HasExtension(exeName)
169 | && Path.GetFileNameWithoutExtension(file) == exeName
170 | && _pathExtensions.Contains(Path.GetExtension(file)))
171 | select file;
172 |
173 | return executables.FirstOrDefault();
174 | }
175 | }
176 |
--------------------------------------------------------------------------------
/src/DotnetCat/IO/IConnectable.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Diagnostics.CodeAnalysis;
3 | using DotnetCat.Errors;
4 | using DotnetCat.Network;
5 |
6 | namespace DotnetCat.IO;
7 |
8 | ///
9 | /// Interface enforcing mechanisms to connect and release unmanaged resources.
10 | ///
11 | internal interface IConnectable : IDisposable
12 | {
13 | ///
14 | /// Connect the unmanaged resource(s).
15 | ///
16 | void Connect();
17 |
18 | ///
19 | /// Dispose of all unmanaged resources and handle the given error.
20 | ///
21 | [DoesNotReturn]
22 | void PipeError(Except type, [NotNull] string? arg, Exception? ex = default);
23 |
24 | ///
25 | /// Dispose of all unmanaged resources and handle the given error.
26 | ///
27 | [DoesNotReturn]
28 | void PipeError(Except type, HostEndPoint target, Exception? ex = default);
29 | }
30 |
--------------------------------------------------------------------------------
/src/DotnetCat/IO/LogLevel.cs:
--------------------------------------------------------------------------------
1 | namespace DotnetCat.IO;
2 |
3 | ///
4 | /// Console logging level enumeration type.
5 | ///
6 | internal enum LogLevel : byte
7 | {
8 | ///
9 | /// General information log messages (stdout).
10 | ///
11 | Info,
12 |
13 | ///
14 | /// Status and completion log messages (stdout).
15 | ///
16 | Status,
17 |
18 | ///
19 | /// Warning log messages (stderr).
20 | ///
21 | Warn,
22 |
23 | ///
24 | /// Error and exception log messages (stderr).
25 | ///
26 | Error
27 | }
28 |
--------------------------------------------------------------------------------
/src/DotnetCat/IO/Output.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Diagnostics.CodeAnalysis;
3 | using System.IO;
4 | using DotnetCat.Errors;
5 |
6 | namespace DotnetCat.IO;
7 |
8 | ///
9 | /// Console output utility class.
10 | ///
11 | internal static class Output
12 | {
13 | ///
14 | /// Write the given error message to the standard error stream.
15 | ///
16 | public static void Error([NotNull] string? msg) => Log(msg, LogLevel.Error);
17 |
18 | ///
19 | /// Write the given status or completion message to the standard output stream.
20 | ///
21 | public static void Status([NotNull] string? msg) => Log(msg, LogLevel.Status);
22 |
23 | ///
24 | /// Write the given message to the standard console
25 | /// stream corresponding to the specified logging level.
26 | ///
27 | public static void Log([NotNull] string? msg, LogLevel level = default)
28 | {
29 | ThrowIf.NullOrEmpty(msg);
30 | string msgPrefix = Sequence.Colorize(LogPrefix(level), PrefixColor(level));
31 |
32 | ConsoleStream(level).WriteLine($"{msgPrefix} {msg}");
33 | }
34 |
35 | ///
36 | /// Get the log message prefix symbol corresponding to the given logging level.
37 | ///
38 | private static string LogPrefix(LogLevel level) => level switch
39 | {
40 | LogLevel.Info => "[*]",
41 | LogLevel.Status => "[+]",
42 | LogLevel.Warn => "[!]",
43 | LogLevel.Error => "[x]",
44 | _ => throw new ArgumentException("Invalid logging level.", nameof(level))
45 | };
46 |
47 | ///
48 | /// Get the log message prefix symbol console color
49 | /// corresponding to the given logging level.
50 | ///
51 | private static ConsoleColor PrefixColor(LogLevel level) => level switch
52 | {
53 | LogLevel.Info => ConsoleColor.Cyan,
54 | LogLevel.Status => ConsoleColor.Green,
55 | LogLevel.Warn => ConsoleColor.Yellow,
56 | LogLevel.Error => ConsoleColor.Red,
57 | _ => throw new ArgumentException("Invalid logging level.", nameof(level))
58 | };
59 |
60 | ///
61 | /// Get the standard console stream corresponding to the given logging level.
62 | ///
63 | private static TextWriter ConsoleStream(LogLevel level)
64 | {
65 | return level is LogLevel.Error or LogLevel.Warn ? Console.Error : Console.Out;
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/DotnetCat/IO/Pipelines/FilePipe.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics.CodeAnalysis;
2 | using System.IO;
3 | using System.Text;
4 | using System.Threading;
5 | using System.Threading.Tasks;
6 | using DotnetCat.Errors;
7 | using DotnetCat.Utils;
8 |
9 | namespace DotnetCat.IO.Pipelines;
10 |
11 | ///
12 | /// Unidirectional socket pipeline used to transfer file data.
13 | ///
14 | internal class FilePipe : SocketPipe
15 | {
16 | private readonly TransferOpt _transfer; // File transfer option
17 |
18 | ///
19 | /// Initialize the object.
20 | ///
21 | public FilePipe(CmdLineArgs args, [NotNull] StreamReader? src) : base(args)
22 | {
23 | ThrowIf.NullOrEmpty(args.FilePath);
24 | ThrowIf.Null(src);
25 |
26 | _transfer = TransferOpt.Collect;
27 |
28 | Source = src;
29 | Dest = new StreamWriter(CreateFile(FilePath)) { AutoFlush = true };
30 | }
31 |
32 | ///
33 | /// Initialize the object.
34 | ///
35 | public FilePipe(CmdLineArgs args, [NotNull] StreamWriter? dest) : base(args)
36 | {
37 | ThrowIf.NullOrEmpty(args.FilePath);
38 | ThrowIf.Null(dest);
39 |
40 | _transfer = TransferOpt.Transmit;
41 |
42 | Dest = dest;
43 | Source = new StreamReader(OpenFile(FilePath));
44 | }
45 |
46 | ///
47 | /// Finalize the object.
48 | ///
49 | ~FilePipe() => Dispose(false);
50 |
51 | ///
52 | /// Source or destination path.
53 | ///
54 | public string FilePath
55 | {
56 | get => Args.FilePath ??= string.Empty;
57 | set => Args.FilePath = value ?? string.Empty;
58 | }
59 |
60 | ///
61 | /// Create or overwrite a file at the given file path for writing.
62 | ///
63 | protected FileStream CreateFile(string path)
64 | {
65 | if (path.IsNullOrEmpty())
66 | {
67 | PipeError(Except.EmptyPath, "-o/--output");
68 | }
69 | string? parentPath = FileSys.ParentPath(path);
70 |
71 | if (!FileSys.DirectoryExists(parentPath))
72 | {
73 | PipeError(Except.DirectoryPath, parentPath);
74 | }
75 |
76 | return new FileStream(path,
77 | FileMode.Create,
78 | FileAccess.Write,
79 | FileShare.Write,
80 | READ_BUFFER_SIZE,
81 | useAsync: true);
82 | }
83 |
84 | ///
85 | /// Open an existing file at the given file path for reading.
86 | ///
87 | protected FileStream OpenFile(string? path)
88 | {
89 | if (path.IsNullOrEmpty())
90 | {
91 | PipeError(Except.EmptyPath, "-s/--send");
92 | }
93 | FileInfo info = new(path ?? string.Empty);
94 |
95 | if (!info.Exists)
96 | {
97 | PipeError(Except.FilePath, info.FullName);
98 | }
99 |
100 | return new FileStream(info.FullName,
101 | FileMode.Open,
102 | FileAccess.Read,
103 | FileShare.Read,
104 | WRITE_BUFFER_SIZE,
105 | useAsync: true);
106 | }
107 |
108 | ///
109 | /// Asynchronously perform the file transfer between the underlying streams.
110 | ///
111 | protected override async Task ConnectAsync(CancellationToken token)
112 | {
113 | Connected = true;
114 | StringBuilder data = new();
115 |
116 | if (_transfer is TransferOpt.Collect)
117 | {
118 | Output.Log($"Downloading socket data to '{FilePath}'...");
119 | }
120 | else if (_transfer is TransferOpt.Transmit)
121 | {
122 | Output.Log($"Transmitting '{FilePath}' data...");
123 | }
124 |
125 | data.Append(await ReadToEndAsync());
126 | await WriteAsync(data, token);
127 |
128 | if (_transfer is TransferOpt.Collect)
129 | {
130 | Output.Status($"File successfully downloaded to '{FilePath}'");
131 | }
132 | else if (_transfer is TransferOpt.Transmit)
133 | {
134 | Output.Status("File successfully transmitted");
135 | }
136 |
137 | Disconnect();
138 | Dispose();
139 | }
140 | }
141 |
--------------------------------------------------------------------------------
/src/DotnetCat/IO/Pipelines/PipeType.cs:
--------------------------------------------------------------------------------
1 | namespace DotnetCat.IO.Pipelines;
2 |
3 | ///
4 | /// Socket pipeline enumeration type.
5 | ///
6 | internal enum PipeType : byte
7 | {
8 | ///
9 | /// Standard console stream socket pipeline.
10 | ///
11 | Stream,
12 |
13 | ///
14 | /// File transfer socket pipeline.
15 | ///
16 | File,
17 |
18 | ///
19 | /// Executable process socket pipeline.
20 | ///
21 | Process,
22 |
23 | ///
24 | /// Connection status socket pipeline.
25 | ///
26 | Status,
27 |
28 | ///
29 | /// Text transmission socket pipeline.
30 | ///
31 | Text
32 | }
33 |
--------------------------------------------------------------------------------
/src/DotnetCat/IO/Pipelines/ProcessPipe.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Diagnostics.CodeAnalysis;
3 | using System.IO;
4 | using System.Text;
5 | using System.Threading;
6 | using System.Threading.Tasks;
7 | using DotnetCat.Errors;
8 | using DotnetCat.Utils;
9 |
10 | namespace DotnetCat.IO.Pipelines;
11 |
12 | ///
13 | /// Unidirectional socket pipeline used to transfer executable process data.
14 | ///
15 | internal class ProcessPipe : SocketPipe
16 | {
17 | ///
18 | /// Initialize the object.
19 | ///
20 | public ProcessPipe(CmdLineArgs args,
21 | [NotNull] StreamReader? src,
22 | [NotNull] StreamWriter? dest)
23 | : base(args)
24 | {
25 | ThrowIf.Null(src);
26 | ThrowIf.Null(dest);
27 |
28 | Source = src;
29 | Dest = dest;
30 | }
31 |
32 | ///
33 | /// Finalize the object.
34 | ///
35 | ~ProcessPipe() => Dispose(false);
36 |
37 | ///
38 | /// Asynchronously transfer the executable process data
39 | /// between the underlying streams.
40 | ///
41 | protected override async Task ConnectAsync(CancellationToken token)
42 | {
43 | StringBuilder data = new();
44 |
45 | int charsRead;
46 | Connected = true;
47 |
48 | while (Client is not null && Client.Connected)
49 | {
50 | if (token.IsCancellationRequested)
51 | {
52 | Disconnect();
53 | break;
54 | }
55 |
56 | charsRead = await ReadAsync(token);
57 | data.Append(Buffer.ToArray(), 0, charsRead);
58 |
59 | if (!Client.Connected || charsRead <= 0)
60 | {
61 | Disconnect();
62 | break;
63 | }
64 |
65 | await WriteAsync(data.ReplaceLineEndings(), token);
66 | data.Clear();
67 | }
68 |
69 | if (!Args.UsingExe)
70 | {
71 | Console.WriteLine();
72 | }
73 | Dispose();
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/DotnetCat/IO/Pipelines/SocketPipe.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Diagnostics.CodeAnalysis;
3 | using System.IO;
4 | using System.Net.Sockets;
5 | using System.Text;
6 | using System.Threading;
7 | using System.Threading.Tasks;
8 | using DotnetCat.Errors;
9 | using DotnetCat.Network;
10 | using DotnetCat.Shell;
11 | using DotnetCat.Utils;
12 |
13 | namespace DotnetCat.IO.Pipelines;
14 |
15 | ///
16 | /// Abstract unidirectional socket pipeline. This is the base class
17 | /// for all socket pipelines in the namespace.
18 | ///
19 | internal abstract class SocketPipe : IConnectable
20 | {
21 | protected const int READ_BUFFER_SIZE = 1024;
22 | protected const int WRITE_BUFFER_SIZE = READ_BUFFER_SIZE * 4;
23 |
24 | private bool _disposed; // Object disposed
25 |
26 | ///
27 | /// Initialize the object.
28 | ///
29 | protected SocketPipe()
30 | {
31 | _disposed = false;
32 | Connected = false;
33 |
34 | Args = new CmdLineArgs();
35 | NewLine = new StringBuilder(SysInfo.Eol);
36 | }
37 |
38 | ///
39 | /// Initialize the object.
40 | ///
41 | protected SocketPipe(CmdLineArgs args) : this() => Args = args;
42 |
43 | ///
44 | /// Finalize the object.
45 | ///
46 | ~SocketPipe() => Dispose(false);
47 |
48 | ///
49 | /// Underlying streams are connected.
50 | ///
51 | public bool Connected { get; protected set; }
52 |
53 | ///
54 | /// TCP socket client.
55 | ///
56 | protected static TcpClient? Client => Program.SockNode?.Client;
57 |
58 | ///
59 | /// TCP client is connected.
60 | ///
61 | protected static bool ClientConnected => Client?.Connected ?? false;
62 |
63 | ///
64 | /// Platform based EOL control sequence.
65 | ///
66 | protected StringBuilder NewLine { get; }
67 |
68 | ///
69 | /// Character memory buffer.
70 | ///
71 | protected Memory Buffer { get; set; }
72 |
73 | ///
74 | /// Pipeline cancellation token source.
75 | ///
76 | protected CancellationTokenSource? TokenSource { get; set; }
77 |
78 | ///
79 | /// Command-line arguments.
80 | ///
81 | protected CmdLineArgs Args { get; set; }
82 |
83 | ///
84 | /// Pipeline data source.
85 | ///
86 | protected StreamReader? Source { get; set; }
87 |
88 | ///
89 | /// Pipeline data destination.
90 | ///
91 | protected StreamWriter? Dest { get; set; }
92 |
93 | ///
94 | /// Pipeline data transfer task.
95 | ///
96 | protected Task? Worker { get; set; }
97 |
98 | ///
99 | /// Activate communication between the underlying streams.
100 | ///
101 | public void Connect()
102 | {
103 | ThrowIf.Null(Source);
104 | ThrowIf.Null(Dest);
105 |
106 | Buffer = new Memory(new char[READ_BUFFER_SIZE]);
107 | TokenSource = new CancellationTokenSource();
108 |
109 | Worker = ConnectAsync(TokenSource.Token);
110 | }
111 |
112 | ///
113 | /// Free the underlying resources.
114 | ///
115 | public void Dispose()
116 | {
117 | Dispose(true);
118 | GC.SuppressFinalize(this);
119 | }
120 |
121 | ///
122 | /// Dispose of all unmanaged resources and handle the given error.
123 | ///
124 | [DoesNotReturn]
125 | public void PipeError(Except type, [NotNull] string? arg, Exception? ex = default)
126 | {
127 | Dispose();
128 | Error.Handle(type, arg, ex);
129 | }
130 |
131 | ///
132 | /// Dispose of all unmanaged resources and handle the given error.
133 | ///
134 | [DoesNotReturn]
135 | public void PipeError(Except type, HostEndPoint target, Exception? ex = default)
136 | {
137 | PipeError(type, target.ToString(), ex);
138 | }
139 |
140 | ///
141 | /// Free the underlying resources.
142 | ///
143 | protected virtual void Dispose(bool disposing)
144 | {
145 | if (!_disposed)
146 | {
147 | if (disposing)
148 | {
149 | Source?.Dispose();
150 | Dest?.Dispose();
151 | TokenSource?.Dispose();
152 | Client?.Dispose();
153 | }
154 | _disposed = true;
155 | }
156 | }
157 |
158 | ///
159 | /// Cancel communication between the underlying streams.
160 | ///
161 | protected void Disconnect()
162 | {
163 | Connected = false;
164 | TokenSource?.Cancel();
165 | }
166 |
167 | ///
168 | /// Asynchronously transfer data between the underlying streams.
169 | ///
170 | protected abstract Task ConnectAsync(CancellationToken token);
171 |
172 | ///
173 | /// Asynchronously read data from the underlying source stream
174 | /// and write it to the underlying memory buffer.
175 | ///
176 | protected virtual async ValueTask ReadAsync(CancellationToken token)
177 | {
178 | int bytesRead = -1;
179 |
180 | if (Source is not null && ClientConnected)
181 | {
182 | bytesRead = await Source.ReadAsync(Buffer, token);
183 | }
184 | return bytesRead;
185 | }
186 |
187 | ///
188 | /// Asynchronously read all the data that is currently available
189 | /// in the underlying source stream.
190 | ///
191 | protected virtual async ValueTask ReadToEndAsync()
192 | {
193 | string buffer = string.Empty;
194 |
195 | if (Source is not null && ClientConnected)
196 | {
197 | buffer = await Source.ReadToEndAsync();
198 | }
199 | return buffer;
200 | }
201 |
202 | ///
203 | /// Asynchronously write all the given data to the underlying destination stream.
204 | ///
205 | protected virtual async Task WriteAsync(StringBuilder data, CancellationToken token)
206 | {
207 | if (Dest is not null && ClientConnected)
208 | {
209 | await Dest.WriteAsync(data, token);
210 | }
211 | }
212 | }
213 |
--------------------------------------------------------------------------------
/src/DotnetCat/IO/Pipelines/StatusPipe.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics.CodeAnalysis;
2 | using System.IO;
3 | using System.Text;
4 | using System.Threading;
5 | using System.Threading.Tasks;
6 | using DotnetCat.Utils;
7 |
8 | namespace DotnetCat.IO.Pipelines;
9 |
10 | ///
11 | /// Unidirectional socket pipeline used to perform network connection testing.
12 | ///
13 | internal class StatusPipe : TextPipe
14 | {
15 | ///
16 | /// Initialize the object.
17 | ///
18 | public StatusPipe(CmdLineArgs args, [NotNull] StreamWriter? dest) : base(args, dest)
19 | {
20 | }
21 |
22 | ///
23 | /// Finalize the object.
24 | ///
25 | ~StatusPipe() => Dispose(false);
26 |
27 | ///
28 | /// Asynchronously transfer an empty string between the underlying streams.
29 | ///
30 | protected override async Task ConnectAsync(CancellationToken token)
31 | {
32 | Connected = true;
33 |
34 | StringBuilder data = new(await ReadToEndAsync());
35 | await WriteAsync(data, token);
36 |
37 | Output.Status($"Connection accepted by {Program.SockNode?.Endpoint}");
38 |
39 | Disconnect();
40 | Dispose();
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/DotnetCat/IO/Pipelines/StreamPipe.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics.CodeAnalysis;
2 | using System.IO;
3 | using System.Text;
4 | using System.Threading;
5 | using System.Threading.Tasks;
6 | using DotnetCat.Errors;
7 | using DotnetCat.Shell;
8 | using DotnetCat.Utils;
9 |
10 | namespace DotnetCat.IO.Pipelines;
11 |
12 | ///
13 | /// Unidirectional socket pipeline used to transfer standard console stream data.
14 | ///
15 | internal class StreamPipe : SocketPipe
16 | {
17 | ///
18 | /// Initialize the object.
19 | ///
20 | public StreamPipe([NotNull] StreamReader? src, [NotNull] StreamWriter? dest) : base()
21 | {
22 | ThrowIf.Null(src);
23 | ThrowIf.Null(dest);
24 |
25 | Source = src;
26 | Dest = dest;
27 | }
28 |
29 | ///
30 | /// Finalize the object.
31 | ///
32 | ~StreamPipe() => Dispose(false);
33 |
34 | ///
35 | /// Asynchronously transfer console stream data between the underlying streams.
36 | ///
37 | protected override async Task ConnectAsync(CancellationToken token)
38 | {
39 | StringBuilder data = new();
40 |
41 | int charsRead;
42 | Connected = true;
43 |
44 | if (Client is not null)
45 | {
46 | while (Client.Connected)
47 | {
48 | if (token.IsCancellationRequested)
49 | {
50 | Disconnect();
51 | break;
52 | }
53 |
54 | charsRead = await ReadAsync(token);
55 | data.Append(Buffer.ToArray(), 0, charsRead);
56 |
57 | if (!Client.Connected || charsRead <= 0)
58 | {
59 | Disconnect();
60 | break;
61 | }
62 | data = data.ReplaceLineEndings();
63 |
64 | // Clear the console screen buffer
65 | if (Command.IsClearCmd(data.ToString()))
66 | {
67 | Sequence.ClearScreen();
68 | await WriteAsync(NewLine, token);
69 | }
70 | else // Send the command
71 | {
72 | await WriteAsync(data, token);
73 | }
74 | data.Clear();
75 | }
76 | Dispose();
77 | }
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/src/DotnetCat/IO/Pipelines/TextPipe.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics.CodeAnalysis;
2 | using System.IO;
3 | using System.Text;
4 | using System.Threading;
5 | using System.Threading.Tasks;
6 | using DotnetCat.Errors;
7 | using DotnetCat.Utils;
8 |
9 | namespace DotnetCat.IO.Pipelines;
10 |
11 | ///
12 | /// Unidirectional socket pipeline used to transmit arbitrary string data.
13 | ///
14 | internal class TextPipe : SocketPipe
15 | {
16 | private bool _disposed; // Object disposed
17 |
18 | private MemoryStream _memoryStream; // Memory stream buffer
19 |
20 | ///
21 | /// Initialize the object.
22 | ///
23 | public TextPipe(CmdLineArgs args, [NotNull] StreamWriter? dest) : base(args)
24 | {
25 | ThrowIf.Null(dest);
26 |
27 | _disposed = false;
28 | _memoryStream = new MemoryStream();
29 |
30 | Dest = dest;
31 | Source = new StreamReader(_memoryStream);
32 | }
33 |
34 | ///
35 | /// Finalize the object.
36 | ///
37 | ~TextPipe() => Dispose(false);
38 |
39 | ///
40 | /// String network payload.
41 | ///
42 | protected string Payload
43 | {
44 | get => Args.Payload ?? string.Empty;
45 | set
46 | {
47 | ThrowIf.NullOrEmpty(value);
48 | _memoryStream = new MemoryStream(Encoding.UTF8.GetBytes(value));
49 | Args.Payload = value;
50 | }
51 | }
52 |
53 | ///
54 | /// Free the underlying resources.
55 | ///
56 | protected override void Dispose(bool disposing)
57 | {
58 | if (!_disposed)
59 | {
60 | if (disposing)
61 | {
62 | _memoryStream?.Dispose();
63 | }
64 | _disposed = true;
65 | }
66 | base.Dispose(disposing);
67 | }
68 |
69 | ///
70 | /// Asynchronously transfer the user-defined
71 | /// string payload between the underlying streams.
72 | ///
73 | protected override async Task ConnectAsync(CancellationToken token)
74 | {
75 | Connected = true;
76 | StringBuilder data = new(await ReadToEndAsync());
77 |
78 | await WriteAsync(data, token);
79 | Output.Status("Payload successfully transmitted");
80 |
81 | Disconnect();
82 | Dispose();
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/src/DotnetCat/IO/Pipelines/TransferOpt.cs:
--------------------------------------------------------------------------------
1 | namespace DotnetCat.IO.Pipelines;
2 |
3 | ///
4 | /// Socket pipeline file transfer option enumeration type.
5 | ///
6 | internal enum TransferOpt : byte
7 | {
8 | ///
9 | /// No stream redirection.
10 | ///
11 | None,
12 |
13 | ///
14 | /// Redirect source stream data to file stream.
15 | ///
16 | Collect,
17 |
18 | ///
19 | /// Redirect file stream data to destination stream.
20 | ///
21 | Transmit
22 | }
23 |
--------------------------------------------------------------------------------
/src/DotnetCat/IO/Sequence.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Diagnostics.CodeAnalysis;
3 | using System.Text.RegularExpressions;
4 | using DotnetCat.Errors;
5 | using DotnetCat.Shell.WinApi;
6 | using DotnetCat.Utils;
7 |
8 | namespace DotnetCat.IO;
9 |
10 | ///
11 | /// Virtual terminal control sequence utility class.
12 | ///
13 | internal static partial class Sequence
14 | {
15 | private const string ESC = "\e";
16 | private const string CLEAR = $"{CSI}H{CSI}2J{CSI}3J";
17 | private const string CSI = $"{ESC}[";
18 | private const string RESET = $"{CSI}0m";
19 |
20 | ///
21 | /// Initialize the static class members.
22 | ///
23 | static Sequence() => ConsoleApi.EnableVirtualTerm();
24 |
25 | ///
26 | /// Clear the current console screen buffer.
27 | ///
28 | public static void ClearScreen() => Console.Write(CLEAR);
29 |
30 | ///
31 | /// Get the length of the given message excluding ANSI SGR control sequences.
32 | ///
33 | public static int Length([NotNull] string? msg) => Decolorize(msg).Length;
34 |
35 | ///
36 | /// Get the ANSI foreground color SGR control sequence
37 | /// that corresponds to the given console color.
38 | ///
39 | public static string Colorize(ConsoleColor color) => ColorSequence(color);
40 |
41 | ///
42 | /// Style the given message using ANSI SGR control sequences so the
43 | /// foreground color is changed and terminated by a reset sequence.
44 | ///
45 | public static string Colorize([NotNull] string? msg, ConsoleColor color)
46 | {
47 | ThrowIf.NullOrEmpty(msg);
48 | return ColorSequence(color) + msg + RESET;
49 | }
50 |
51 | ///
52 | /// Remove styling from given message by erasing all ANSI SGR control sequences.
53 | ///
54 | public static string Decolorize([NotNull] string? msg) => SgrRegex().Erase(msg);
55 |
56 | ///
57 | /// Get the ANSI foreground color SGR control sequence that
58 | /// corresponds to the given console color.
59 | ///
60 | private static string ColorSequence(ConsoleColor color) => color switch
61 | {
62 | ConsoleColor.Black => $"{CSI}0;30m",
63 | ConsoleColor.DarkBlue => $"{CSI}0;34m",
64 | ConsoleColor.DarkGreen => $"{CSI}0;32m",
65 | ConsoleColor.DarkCyan => $"{CSI}0;36m",
66 | ConsoleColor.DarkRed => $"{CSI}0;31m",
67 | ConsoleColor.DarkMagenta => $"{CSI}0;35m",
68 | ConsoleColor.DarkYellow => $"{CSI}0;33m",
69 | ConsoleColor.Gray => $"{CSI}0;37m",
70 | ConsoleColor.DarkGray => $"{CSI}1;30m",
71 | ConsoleColor.Blue => $"{CSI}1;34m",
72 | ConsoleColor.Green => $"{CSI}38;2;166;226;46m",
73 | ConsoleColor.Cyan => $"{CSI}38;2;0;255;255m",
74 | ConsoleColor.Red => $"{CSI}38;2;246;0;0m",
75 | ConsoleColor.Magenta => $"{CSI}1;35m",
76 | ConsoleColor.Yellow => $"{CSI}38;2;250;230;39m",
77 | ConsoleColor.White => $"{CSI}1;37m",
78 | _ => RESET
79 | };
80 |
81 | ///
82 | /// ANSI SGR control sequence regular expression.
83 | ///
84 | [GeneratedRegex(@"\e\[[0-9]+(;[0-9]+)*m")]
85 | private static partial Regex SgrRegex();
86 | }
87 |
--------------------------------------------------------------------------------
/src/DotnetCat/Network/HostEndPoint.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics.CodeAnalysis;
2 | using System.Net;
3 | using DotnetCat.Errors;
4 |
5 | namespace DotnetCat.Network;
6 |
7 | ///
8 | /// IPv4 hostname socket endpoint.
9 | ///
10 | internal class HostEndPoint
11 | {
12 | private int _port; // Network port number
13 |
14 | private string? _hostName; // Network hostname
15 |
16 | private IPAddress _address; // IPv4 address
17 |
18 | ///
19 | /// Initialize the object.
20 | ///
21 | public HostEndPoint()
22 | {
23 | _port = 44444;
24 | _address = IPAddress.Any;
25 | }
26 |
27 | ///
28 | /// Initialize the object.
29 | ///
30 | public HostEndPoint([NotNull] string? hostName, IPAddress? address, int port) : this()
31 | {
32 | ThrowIf.NullOrEmpty(hostName);
33 | ThrowIf.Null(address);
34 |
35 | Port = port;
36 | HostName = hostName;
37 | Address = address;
38 | }
39 |
40 | ///
41 | /// Network port number.
42 | ///
43 | public int Port
44 | {
45 | get => _port;
46 | set
47 | {
48 | ThrowIf.InvalidPort(value);
49 | _port = value;
50 | }
51 | }
52 |
53 | ///
54 | /// Network hostname.
55 | ///
56 | public string HostName
57 | {
58 | get => _hostName ?? Address.ToString();
59 | set => _hostName = value;
60 | }
61 |
62 | ///
63 | /// IPv4 address.
64 | ///
65 | public IPAddress Address
66 | {
67 | get => _address;
68 | set
69 | {
70 | ThrowIf.NotIPv4Address(value);
71 | _address = value;
72 | }
73 | }
74 |
75 | ///
76 | /// Get the string representation of the underlying endpoint information.
77 | ///
78 | public override string? ToString() => $"{HostName}:{Port}";
79 |
80 | ///
81 | /// Parse the given IPv4 endpoint.
82 | ///
83 | public void ParseEndpoint([NotNull] IPEndPoint? ipEndpoint)
84 | {
85 | ThrowIf.Null(ipEndpoint);
86 | Address = ipEndpoint.Address;
87 | Port = ipEndpoint.Port;
88 | }
89 |
90 | ///
91 | /// Initialize an IPv4 endpoint from the underlying IP address and port number.
92 | ///
93 | public IPEndPoint IPv4Endpoint() => new(Address, Port);
94 | }
95 |
--------------------------------------------------------------------------------
/src/DotnetCat/Network/Net.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Net;
5 | using System.Net.Sockets;
6 | using DotnetCat.Errors;
7 | using DotnetCat.Shell;
8 | using DotnetCat.Utils;
9 |
10 | namespace DotnetCat.Network;
11 |
12 | ///
13 | /// Network and socket utility class.
14 | ///
15 | internal static class Net
16 | {
17 | ///
18 | /// Determine whether the given port is a valid network port number.
19 | ///
20 | public static bool ValidPort(int port) => port is > 0 and <= 65535;
21 |
22 | ///
23 | /// Resolve the IPv4 address associated with the given hostname.
24 | ///
25 | public static IPAddress ResolveName(string hostName, out Exception? ex)
26 | {
27 | ex = null;
28 | IPHostEntry? dnsAns = null;
29 |
30 | try // Resolve IPv4 from hostname
31 | {
32 | dnsAns = Dns.GetHostEntry(hostName);
33 | }
34 | catch (SocketException sockEx)
35 | {
36 | ex = sockEx;
37 | }
38 |
39 | IPAddress address = IPAddress.None;
40 |
41 | // Extract first resulting IPv4 address
42 | if (dnsAns is not null && !hostName.NoCaseEquals(SysInfo.Hostname))
43 | {
44 | IPAddress? addr = IPv4Addresses(dnsAns.AddressList).FirstOrDefault();
45 |
46 | if (addr is not null)
47 | {
48 | address = addr;
49 | }
50 | else // Name resolution failure
51 | {
52 | ex = MakeException(SocketError.HostNotFound);
53 | }
54 | }
55 |
56 | return ex is null && address.Equals(IPAddress.None) ? ActiveAddress() : address;
57 | }
58 |
59 | ///
60 | /// Initialize a socket exception from the given socket error.
61 | ///
62 | public static SocketException MakeException(SocketError error)
63 | {
64 | return new SocketException((int)error);
65 | }
66 |
67 | ///
68 | /// Get the exception enum member associated to the given aggregate exception.
69 | ///
70 | public static Except GetExcept(AggregateException ex)
71 | {
72 | return GetExcept(SocketException(ex));
73 | }
74 |
75 | ///
76 | /// Get the exception enum member associated to the given socket exception.
77 | ///
78 | public static Except GetExcept(SocketException? ex) => ex?.SocketErrorCode switch
79 | {
80 | SocketError.AddressAlreadyInUse => Except.AddressInUse,
81 | SocketError.NetworkDown => Except.NetworkDown,
82 | SocketError.NetworkUnreachable => Except.NetworkUnreachable,
83 | SocketError.NetworkReset => Except.NetworkReset,
84 | SocketError.ConnectionAborted => Except.ConnectionAborted,
85 | SocketError.ConnectionReset => Except.ConnectionReset,
86 | SocketError.TimedOut => Except.TimedOut,
87 | SocketError.ConnectionRefused => Except.ConnectionRefused,
88 | SocketError.HostUnreachable => Except.HostUnreachable,
89 | SocketError.HostNotFound => Except.HostNotFound,
90 | SocketError.SocketError or _ => Except.SocketError
91 | };
92 |
93 | ///
94 | /// Get the first socket exception nested within the given aggregate exception.
95 | ///
96 | public static SocketException? SocketException(AggregateException ex)
97 | {
98 | return ex.InnerExceptions.FirstOrDefaultOfType();
99 | }
100 |
101 | ///
102 | /// Get all the IPv4 addresses from the given address collection.
103 | ///
104 | private static IEnumerable IPv4Addresses(IEnumerable addresses)
105 | {
106 | return addresses.Where(a => a.AddressFamily is AddressFamily.InterNetwork);
107 | }
108 |
109 | ///
110 | /// Get the currently active local IPv4 address.
111 | ///
112 | private static IPAddress ActiveAddress()
113 | {
114 | using Socket socket = new(AddressFamily.InterNetwork,
115 | SocketType.Dgram,
116 | ProtocolType.Udp);
117 |
118 | socket.Connect("8.8.8.8", 53);
119 |
120 | return (socket.LocalEndPoint as IPEndPoint)?.Address ?? IPAddress.Any;
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/src/DotnetCat/Network/Nodes/ClientNode.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Net.Sockets;
4 | using DotnetCat.Errors;
5 | using DotnetCat.IO;
6 | using DotnetCat.IO.Pipelines;
7 | using DotnetCat.Utils;
8 |
9 | namespace DotnetCat.Network.Nodes;
10 |
11 | ///
12 | /// TCP network socket client node.
13 | ///
14 | internal class ClientNode : Node
15 | {
16 | ///
17 | /// Initialize the object.
18 | ///
19 | public ClientNode(CmdLineArgs args) : base(args)
20 | {
21 | }
22 |
23 | ///
24 | /// Finalize the object.
25 | ///
26 | ~ClientNode() => Dispose(false);
27 |
28 | ///
29 | /// Establish a socket connection to the underlying IPv4 endpoint.
30 | ///
31 | public override void Connect()
32 | {
33 | ValidateArgsCombinations();
34 |
35 | try // Connect to the remote endpoint
36 | {
37 | if (!Client.ConnectAsync(Endpoint.IPv4Endpoint()).Wait(3500))
38 | {
39 | throw Net.MakeException(SocketError.TimedOut);
40 | }
41 | NetStream = Client.GetStream();
42 |
43 | // Start the executable process
44 | if (Args.UsingExe && !StartProcess(ExePath))
45 | {
46 | PipeError(Except.ExeProcess, ExePath);
47 | }
48 |
49 | if (Args.PipeVariant is not PipeType.Status)
50 | {
51 | Output.Log($"Connected to {Endpoint}");
52 | }
53 |
54 | base.Connect();
55 | WaitForExit();
56 |
57 | Output.Log($"Connection to {Endpoint} closed");
58 | }
59 | catch (AggregateException ex)
60 | {
61 | PipeError(Net.GetExcept(ex), Endpoint, ex);
62 | }
63 | catch (SocketException ex)
64 | {
65 | PipeError(Net.GetExcept(ex), Endpoint, ex);
66 | }
67 | catch (IOException ex)
68 | {
69 | PipeError(Except.ConnectionReset, Endpoint, ex);
70 | }
71 |
72 | Dispose();
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/src/DotnetCat/Network/Nodes/Node.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Diagnostics;
4 | using System.Diagnostics.CodeAnalysis;
5 | using System.IO;
6 | using System.Linq;
7 | using System.Net.Sockets;
8 | using System.Threading.Tasks;
9 | using DotnetCat.Errors;
10 | using DotnetCat.IO;
11 | using DotnetCat.IO.Pipelines;
12 | using DotnetCat.Shell;
13 | using DotnetCat.Utils;
14 |
15 | namespace DotnetCat.Network.Nodes;
16 |
17 | ///
18 | /// Abstract TCP network socket node. This is the base class
19 | /// for all socket nodes in the namespace.
20 | ///
21 | internal abstract class Node : IConnectable
22 | {
23 | private readonly List _pipes; // TCP socket pipelines
24 |
25 | private bool _disposed; // Object disposed
26 | private bool _validArgsCombos; // Valid command-line arguments
27 |
28 | private Process? _process; // Executable process
29 |
30 | private StreamReader? _netReader; // TCP stream reader
31 |
32 | private StreamWriter? _netWriter; // TCP stream writer
33 |
34 | ///
35 | /// Initialize the object.
36 | ///
37 | protected Node(CmdLineArgs args)
38 | {
39 | _pipes = [];
40 | _disposed = _validArgsCombos = false;
41 |
42 | Args = args;
43 |
44 | Client = new TcpClient();
45 | Endpoint = new HostEndPoint(Args.HostName, Args.Address, Args.Port);
46 | }
47 |
48 | ///
49 | /// Finalize the object.
50 | ///
51 | ~Node() => Dispose(false);
52 |
53 | ///
54 | /// Executable file path.
55 | ///
56 | public string? ExePath
57 | {
58 | get => Args.ExePath;
59 | private set => Args.ExePath = value;
60 | }
61 |
62 | ///
63 | /// Host endpoint to use for connection.
64 | ///
65 | public HostEndPoint Endpoint { get; }
66 |
67 | ///
68 | /// TCP socket client.
69 | ///
70 | public TcpClient Client { get; }
71 |
72 | ///
73 | /// File transfer option is set.
74 | ///
75 | protected bool Transfer => Args.TransOpt is not TransferOpt.None;
76 |
77 | ///
78 | /// Using an executable pipeline.
79 | ///
80 | protected bool UsingExe => Args.UsingExe;
81 |
82 | ///
83 | /// Command-line arguments.
84 | ///
85 | protected CmdLineArgs Args { get; private set; }
86 |
87 | ///
88 | /// TCP network stream.
89 | ///
90 | protected NetworkStream? NetStream { get; set; }
91 |
92 | ///
93 | /// Initialize a client or server node from the given command-line arguments.
94 | ///
95 | public static Node New(CmdLineArgs args)
96 | {
97 | return args.Listen ? new ServerNode(args) : new ClientNode(args);
98 | }
99 |
100 | ///
101 | /// Activate asynchronous communication between the source and
102 | /// destination streams in each of the underlying pipelines.
103 | ///
104 | public virtual void Connect()
105 | {
106 | ThrowIf.Null(NetStream);
107 | ValidateArgsCombinations();
108 |
109 | AddPipes(Args.PipeVariant);
110 | _pipes?.ForEach(p => p?.Connect());
111 | }
112 |
113 | ///
114 | /// Free the underlying resources.
115 | ///
116 | public void Dispose()
117 | {
118 | Dispose(true);
119 | GC.SuppressFinalize(this);
120 | }
121 |
122 | ///
123 | /// Dispose of all unmanaged resources and handle the given error.
124 | ///
125 | [DoesNotReturn]
126 | public void PipeError(Except type, [NotNull] string? arg, Exception? ex = default)
127 | {
128 | Dispose();
129 | Error.Handle(type, arg, ex);
130 | }
131 |
132 | ///
133 | /// Dispose of all unmanaged resources and handle the given error.
134 | ///
135 | [DoesNotReturn]
136 | public void PipeError(Except type, HostEndPoint target, Exception? ex = default)
137 | {
138 | PipeError(type, target.ToString(), ex);
139 | }
140 |
141 | ///
142 | /// Initialize and execute the given executable on the local system.
143 | ///
144 | public bool StartProcess([NotNull] string? exe)
145 | {
146 | if (!FileSys.ExistsOnPath(exe, out string? path))
147 | {
148 | Dispose();
149 | Error.Handle(Except.ExePath, exe, true);
150 | }
151 | ExePath = path;
152 |
153 | _process = new Process
154 | {
155 | StartInfo = Command.ExeStartInfo(ExePath)
156 | };
157 | return _process.Start();
158 | }
159 |
160 | ///
161 | /// Free the underlying resources.
162 | ///
163 | protected virtual void Dispose(bool disposing)
164 | {
165 | if (!_disposed)
166 | {
167 | if (disposing)
168 | {
169 | _pipes?.ForEach(p => p?.Dispose());
170 |
171 | _process?.Dispose();
172 | _netReader?.Dispose();
173 | _netWriter?.Dispose();
174 |
175 | Client?.Close();
176 | NetStream?.Dispose();
177 | }
178 | _disposed = true;
179 | }
180 | }
181 |
182 | ///
183 | /// Validate the underlying command-line argument combinations.
184 | ///
185 | protected void ValidateArgsCombinations()
186 | {
187 | if (!_validArgsCombos)
188 | {
189 | // Combination: --exec, --output/--send
190 | if (UsingExe && Transfer)
191 | {
192 | Console.WriteLine(Parser.Usage);
193 | PipeError(Except.ArgsCombo, "--exec, --output/--send");
194 | }
195 |
196 | bool isTextPipe = !Args.Payload.IsNullOrEmpty();
197 |
198 | // Combination: --exec, --text
199 | if (UsingExe && isTextPipe)
200 | {
201 | Console.WriteLine(Parser.Usage);
202 | PipeError(Except.ArgsCombo, "--exec, --text");
203 | }
204 |
205 | // Combination: --text, --output/--send
206 | if (isTextPipe && Transfer)
207 | {
208 | Console.WriteLine(Parser.Usage);
209 | PipeError(Except.ArgsCombo, "--text, --output/--send");
210 | }
211 |
212 | if (Args.PipeVariant is PipeType.Status)
213 | {
214 | // Combination: --listen, --zero-io
215 | if (Program.SockNode is ServerNode)
216 | {
217 | Console.WriteLine(Parser.Usage);
218 | PipeError(Except.ArgsCombo, "--listen, --zero-io");
219 | }
220 |
221 | // Combination: --zero-io, --text
222 | if (isTextPipe)
223 | {
224 | Console.WriteLine(Parser.Usage);
225 | PipeError(Except.ArgsCombo, "--zero-io, --text");
226 | }
227 |
228 | // Combination: --zero-io, --output/--send
229 | if (Transfer)
230 | {
231 | Console.WriteLine(Parser.Usage);
232 | PipeError(Except.ArgsCombo, "--zero-io, --output/--send");
233 | }
234 |
235 | // Combination: --exec, --zero-io
236 | if (UsingExe)
237 | {
238 | Console.WriteLine(Parser.Usage);
239 | PipeError(Except.ArgsCombo, "--exec, --zero-io");
240 | }
241 | }
242 |
243 | _validArgsCombos = true;
244 | }
245 | }
246 |
247 | ///
248 | /// Initialize the underlying pipelines based on the given pipeline type.
249 | ///
250 | protected void AddPipes(PipeType pipeType)
251 | {
252 | if (NetStream is null || !NetStream.CanRead || !NetStream.CanWrite)
253 | {
254 | throw new InvalidOperationException("Invalid network stream state.");
255 | }
256 |
257 | _netWriter = new StreamWriter(NetStream)
258 | {
259 | AutoFlush = true
260 | };
261 | _netReader = new StreamReader(NetStream);
262 |
263 | _pipes.AddRange(MakePipes(pipeType));
264 | }
265 |
266 | ///
267 | /// Wait for the underlying pipeline(s) to be disconnected or
268 | /// the system command shell process to exit.
269 | ///
270 | protected void WaitForExit(int msPollDelay = 100)
271 | {
272 | do
273 | {
274 | Task.Delay(msPollDelay).Wait();
275 |
276 | if (ProcessExited() || !PipelinesConnected())
277 | {
278 | break;
279 | }
280 | }
281 | while (Client.Connected);
282 | }
283 |
284 | ///
285 | /// Initialize a list of pipelines from the given pipeline type.
286 | ///
287 | private List MakePipes(PipeType type)
288 | {
289 | List pipelines = [];
290 |
291 | switch (type)
292 | {
293 | case PipeType.Stream:
294 | pipelines.AddRange(MakeStreamPipes);
295 | break;
296 | case PipeType.File:
297 | pipelines.Add(MakeFilePipe);
298 | break;
299 | case PipeType.Process:
300 | pipelines.AddRange(MakeProcessPipes);
301 | break;
302 | case PipeType.Status:
303 | pipelines.Add(new StatusPipe(Args, _netWriter));
304 | break;
305 | case PipeType.Text:
306 | pipelines.Add(new TextPipe(Args, _netWriter));
307 | break;
308 | default:
309 | break;
310 | }
311 | return pipelines;
312 | }
313 |
314 | ///
315 | /// Initialize an array of console stream pipelines.
316 | ///
317 | private StreamPipe[] MakeStreamPipes() =>
318 | [
319 | new StreamPipe(_netReader, new StreamWriter(Console.OpenStandardOutput())
320 | {
321 | AutoFlush = true
322 | }),
323 | new StreamPipe(new StreamReader(Console.OpenStandardInput()), _netWriter)
324 | ];
325 |
326 | ///
327 | /// Initialize an array of executable process pipelines.
328 | ///
329 | private ProcessPipe[] MakeProcessPipes() =>
330 | [
331 | new ProcessPipe(Args, _netReader, _process?.StandardInput),
332 | new ProcessPipe(Args, _process?.StandardOutput, _netWriter),
333 | new ProcessPipe(Args, _process?.StandardError, _netWriter)
334 | ];
335 |
336 | ///
337 | /// Initialize a file pipeline.
338 | ///
339 | private FilePipe MakeFilePipe()
340 | {
341 | if (Args.TransOpt is TransferOpt.None)
342 | {
343 | throw new InvalidOperationException("No file transfer option set.");
344 | }
345 | FilePipe filePipe;
346 |
347 | // Pipe data from socket to file
348 | if (Args.TransOpt is TransferOpt.Collect)
349 | {
350 | filePipe = new FilePipe(Args, _netReader);
351 | }
352 | else // Pipe data from file to socket
353 | {
354 | filePipe = new FilePipe(Args, _netWriter);
355 | }
356 | return filePipe;
357 | }
358 |
359 | ///
360 | /// Determine whether the underlying command shell process exited.
361 | ///
362 | private bool ProcessExited() => UsingExe && (_process?.HasExited ?? false);
363 |
364 | ///
365 | /// Determine whether any of the non-null underlying pipelines are connected.
366 | ///
367 | private bool PipelinesConnected() => _pipes.Any(p => p?.Connected ?? false);
368 | }
369 |
--------------------------------------------------------------------------------
/src/DotnetCat/Network/Nodes/ServerNode.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Net;
4 | using System.Net.Sockets;
5 | using DotnetCat.Errors;
6 | using DotnetCat.IO;
7 | using DotnetCat.Utils;
8 |
9 | namespace DotnetCat.Network.Nodes;
10 |
11 | ///
12 | /// TCP network socket server node.
13 | ///
14 | internal class ServerNode : Node
15 | {
16 | private bool _disposed; // Object disposed
17 |
18 | private Socket? _listener; // Listener socket
19 |
20 | ///
21 | /// Initialize the object.
22 | ///
23 | public ServerNode(CmdLineArgs args) : base(args) => _disposed = false;
24 |
25 | ///
26 | /// Finalize the object.
27 | ///
28 | ~ServerNode() => Dispose(false);
29 |
30 | ///
31 | /// Listen for an inbound TCP connection on the underlying listener socket.
32 | ///
33 | public override void Connect()
34 | {
35 | ValidateArgsCombinations();
36 |
37 | HostEndPoint remoteEndpoint = new();
38 | BindListener(Endpoint.IPv4Endpoint());
39 |
40 | try // Listen for an inbound connection
41 | {
42 | _listener?.Listen(1);
43 | Output.Log($"Listening for incoming connections on {Endpoint}...");
44 |
45 | if (_listener is not null)
46 | {
47 | Client.Client = _listener.Accept();
48 | }
49 | NetStream = Client.GetStream();
50 |
51 | if (Args.UsingExe && !StartProcess(ExePath))
52 | {
53 | PipeError(Except.ExeProcess, ExePath);
54 | }
55 |
56 | remoteEndpoint.ParseEndpoint(Client.Client.RemoteEndPoint as IPEndPoint);
57 | Output.Log($"Connected to {remoteEndpoint}");
58 |
59 | base.Connect();
60 | WaitForExit();
61 |
62 | Console.WriteLine();
63 | Output.Log($"Connection to {remoteEndpoint} closed");
64 | }
65 | catch (AggregateException ex)
66 | {
67 | PipeError(Net.GetExcept(ex), remoteEndpoint, ex);
68 | }
69 | catch (SocketException ex)
70 | {
71 | PipeError(Net.GetExcept(ex), remoteEndpoint, ex);
72 | }
73 | catch (IOException ex)
74 | {
75 | PipeError(Except.ConnectionReset, remoteEndpoint, ex);
76 | }
77 |
78 | Dispose();
79 | }
80 |
81 | ///
82 | /// Free the underlying resources.
83 | ///
84 | protected override void Dispose(bool disposing)
85 | {
86 | if (!_disposed)
87 | {
88 | if (disposing)
89 | {
90 | _listener?.Close();
91 | }
92 | _disposed = true;
93 | }
94 | base.Dispose(disposing);
95 | }
96 |
97 | ///
98 | /// Bind the underlying listener socket to the given IPv4 endpoint.
99 | ///
100 | private void BindListener(IPEndPoint ep)
101 | {
102 | ThrowIf.Null(ep);
103 |
104 | _listener = new Socket(AddressFamily.InterNetwork,
105 | SocketType.Stream,
106 | ProtocolType.Tcp);
107 |
108 | try // Bind the listener socket
109 | {
110 | _listener.Bind(ep);
111 | }
112 | catch (SocketException ex)
113 | {
114 | PipeError(Net.GetExcept(ex), ep.ToString(), ex);
115 | }
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/src/DotnetCat/Program.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using DotnetCat.Network.Nodes;
3 | using DotnetCat.Utils;
4 |
5 | namespace DotnetCat;
6 |
7 | ///
8 | /// Application startup object.
9 | ///
10 | internal class Program
11 | {
12 | ///
13 | /// Initialize the static class members.
14 | ///
15 | static Program() => Console.Title = $"DotnetCat ({Parser.Repo})";
16 |
17 | ///
18 | /// Network socket node.
19 | ///
20 | public static Node? SockNode { get; private set; }
21 |
22 | ///
23 | /// Application entry point.
24 | ///
25 | public static void Main(string[] args)
26 | {
27 | Parser parser = new(args);
28 |
29 | // Display help information and exit
30 | if (parser.CmdArgs.Help)
31 | {
32 | Parser.PrintHelp();
33 | }
34 |
35 | SockNode = Node.New(parser.CmdArgs);
36 | SockNode.Connect();
37 |
38 | Console.WriteLine();
39 | Environment.Exit(0);
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/DotnetCat/Properties/PublishProfiles/Linux-arm64.pubxml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 | ARM64
8 | false
9 | None
10 | true
11 | true
12 | true
13 | AnyCPU
14 | bin/Publish/linux-arm64
15 | FileSystem
16 | true
17 | true
18 | true
19 | true
20 | true
21 | linux-arm64
22 | true
23 | net9.0
24 | <_TargetId>Folder
25 |
26 |
27 |
--------------------------------------------------------------------------------
/src/DotnetCat/Properties/PublishProfiles/Linux-x64.pubxml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 | Linux
8 | false
9 | None
10 | true
11 | true
12 | true
13 | x64
14 | bin/Publish/linux-x64
15 | FileSystem
16 | true
17 | true
18 | true
19 | true
20 | true
21 | linux-x64
22 | true
23 | net9.0
24 | <_TargetId>Folder
25 |
26 |
27 |
--------------------------------------------------------------------------------
/src/DotnetCat/Properties/PublishProfiles/Win-x64.pubxml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 | Release
8 | false
9 | None
10 | true
11 | true
12 | true
13 | x64
14 | bin/Publish/win-x64
15 | FileSystem
16 | true
17 | true
18 | true
19 | true
20 | true
21 | win-x64
22 | true
23 | net9.0
24 | <_TargetId>Folder
25 |
26 |
27 |
--------------------------------------------------------------------------------
/src/DotnetCat/Properties/PublishProfiles/Win-x86.pubxml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 | Release
8 | false
9 | None
10 | true
11 | true
12 | true
13 | AnyCPU
14 | bin/Publish/win-x86
15 | FileSystem
16 | true
17 | true
18 | true
19 | true
20 | true
21 | win-x86
22 | true
23 | net9.0
24 | <_TargetId>Folder
25 |
26 |
27 |
--------------------------------------------------------------------------------
/src/DotnetCat/Resources/Icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vandavey/DotnetCat/38470154f0dc355cf5b6bc961d346f6dd1838ad7/src/DotnetCat/Resources/Icon.ico
--------------------------------------------------------------------------------
/src/DotnetCat/Shell/Command.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Diagnostics;
3 | using System.Diagnostics.CodeAnalysis;
4 | using System.Linq;
5 | using DotnetCat.Errors;
6 | using DotnetCat.IO;
7 | using DotnetCat.Utils;
8 |
9 | namespace DotnetCat.Shell;
10 |
11 | ///
12 | /// Command shell and executable process utility class.
13 | ///
14 | internal static class Command
15 | {
16 | private static readonly string[] _clsCommands; // Clear screen commands
17 |
18 | ///
19 | /// Initialize the static class members.
20 | ///
21 | static Command() => _clsCommands = ["clear", "clear-host", "cls"];
22 |
23 | ///
24 | /// Get the value of the given environment variable.
25 | ///
26 | public static string? EnvVariable(string varName)
27 | {
28 | return Environment.GetEnvironmentVariable(varName);
29 | }
30 |
31 | ///
32 | /// Try to get the value of the given environment variable.
33 | ///
34 | public static bool TryEnvVariable(string varName,
35 | [NotNullWhen(true)] out string? value)
36 | {
37 | return (value = EnvVariable(varName)) is not null;
38 | }
39 |
40 | ///
41 | /// Get process startup information to initialize the given command shell.
42 | ///
43 | public static ProcessStartInfo ExeStartInfo([NotNull] string? shell)
44 | {
45 | ThrowIf.NullOrEmpty(shell);
46 |
47 | ProcessStartInfo startInfo = new(shell)
48 | {
49 | CreateNoWindow = true,
50 | RedirectStandardError = true,
51 | RedirectStandardInput = true,
52 | RedirectStandardOutput = true,
53 | UseShellExecute = false,
54 | WorkingDirectory = FileSys.UserProfile
55 | };
56 |
57 | // Profile loading only supported on Windows
58 | if (OperatingSystem.IsWindows())
59 | {
60 | startInfo.LoadUserProfile = true;
61 | }
62 | return startInfo;
63 | }
64 |
65 | ///
66 | /// Determine whether the given data contains a clear command.
67 | ///
68 | public static bool IsClearCmd(string command)
69 | {
70 | bool clearCommand = false;
71 |
72 | if (!command.IsNullOrEmpty())
73 | {
74 | clearCommand = _clsCommands.Contains(ParseCommand(command));
75 | }
76 | return clearCommand;
77 | }
78 |
79 | ///
80 | /// Parse a shell command from the raw command data.
81 | ///
82 | private static string ParseCommand(string data)
83 | {
84 | data = data.ReplaceLineEndings(string.Empty).Trim();
85 | return data.ToLower().Split(SysInfo.Eol)[0];
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/src/DotnetCat/Shell/SysInfo.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Numerics;
4 | using System.Text;
5 |
6 | namespace DotnetCat.Shell;
7 |
8 | ///
9 | /// System information provider utility class.
10 | ///
11 | internal static class SysInfo
12 | {
13 | ///
14 | /// Environment-specific newline string.
15 | ///
16 | public static string Eol => Environment.NewLine;
17 |
18 | ///
19 | /// Local machine hostname.
20 | ///
21 | public static string Hostname => Environment.MachineName;
22 |
23 | ///
24 | /// Determine whether the local operating system is Linux.
25 | ///
26 | public static bool IsLinux() => OperatingSystem.IsLinux();
27 |
28 | ///
29 | /// Determine whether the local operating system is Windows.
30 | ///
31 | public static bool IsWindows() => OperatingSystem.IsWindows();
32 |
33 | ///
34 | /// Get a string containing information about the local drives.
35 | ///
36 | public static string AllDriveInfo()
37 | {
38 | string title = "Drive Information";
39 | string underline = new('-', title.Length);
40 |
41 | StringBuilder infoBuilder = new($"{title}\n{underline}\n");
42 | DriveInfo[] allDriveInfo = DriveInfo.GetDrives();
43 |
44 | for (int i = 0; i < allDriveInfo.Length; i++)
45 | {
46 | if (i == allDriveInfo.Length - 1)
47 | {
48 | infoBuilder.Append(SizeInfo(allDriveInfo[i]));
49 | }
50 | else
51 | {
52 | infoBuilder.AppendLine($"{SizeInfo(allDriveInfo[i])}\n");
53 | }
54 | }
55 | return infoBuilder.ToString();
56 | }
57 |
58 | ///
59 | /// Get a string containing size information from the given drive information.
60 | ///
61 | private static string SizeInfo(DriveInfo info)
62 | {
63 | string infoString = $"""
64 | Drive Name : {info.Name}
65 | Drive Type : {info.DriveType}
66 | Total Size : {ToGigabytes(info.TotalSize):n3} GB
67 | Used Space : {ToGigabytes(info.TotalSize - info.TotalFreeSpace):n3} GB
68 | Free Space : {ToGigabytes(info.TotalFreeSpace):n3} GB
69 | """;
70 | return infoString;
71 | }
72 |
73 | ///
74 | /// Convert the given size in bytes to gigabytes.
75 | ///
76 | private static double ToGigabytes(T bytes) where T : INumber
77 | {
78 | return Convert.ToDouble(bytes) / 1024 / 1024 / 1024;
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src/DotnetCat/Shell/WinApi/ConsoleApi.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | #if WINDOWS
4 | using DotnetCat.Errors;
5 | using DotnetCat.Utils;
6 | #endif // WINDOWS
7 |
8 | namespace DotnetCat.Shell.WinApi;
9 |
10 | ///
11 | /// Windows console API interoperability utility class.
12 | ///
13 | internal static class ConsoleApi
14 | {
15 | #if WINDOWS
16 | private const int STD_ERROR_HANDLE = -12;
17 | private const int STD_INPUT_HANDLE = -10;
18 | private const int STD_OUTPUT_HANDLE = -11;
19 | #endif // WINDOWS
20 |
21 | private static bool _virtualTermEnabled; // Virtual terminal processing enabled
22 |
23 | ///
24 | /// Initialize the static class members.
25 | ///
26 | static ConsoleApi()
27 | {
28 | _virtualTermEnabled = !OperatingSystem.IsWindows();
29 | EnableVirtualTerm();
30 | }
31 |
32 | ///
33 | /// Enable console virtual terminal sequence processing.
34 | ///
35 | public static void EnableVirtualTerm()
36 | {
37 | #if WINDOWS
38 | if (!_virtualTermEnabled)
39 | {
40 | EnableVirtualTerm(InputMode.ENABLE_VIRTUAL_TERMINAL_INPUT,
41 | OutputMode.ENABLE_VIRTUAL_TERMINAL_PROCESSING);
42 | }
43 | #endif // WINDOWS
44 | }
45 |
46 | #if WINDOWS
47 | ///
48 | /// Enable console virtual terminal sequence processing
49 | /// using the given console input and output modes.
50 | ///
51 | private static void EnableVirtualTerm(InputMode inputMode, OutputMode outputMode)
52 | {
53 | if (!_virtualTermEnabled)
54 | {
55 | using WinSafeHandle stdInHandle = GetStdHandle(STD_INPUT_HANDLE);
56 | using WinSafeHandle stdOutHandle = GetStdHandle(STD_OUTPUT_HANDLE);
57 | using WinSafeHandle stdErrHandle = GetStdHandle(STD_ERROR_HANDLE);
58 |
59 | SetConsoleMode(stdInHandle, GetConsoleMode(stdInHandle, (uint)inputMode));
60 | SetConsoleMode(stdOutHandle, GetConsoleMode(stdOutHandle, (uint)outputMode));
61 | SetConsoleMode(stdErrHandle, GetConsoleMode(stdErrHandle, (uint)outputMode));
62 |
63 | _virtualTermEnabled = true;
64 | }
65 | }
66 |
67 | ///
68 | /// Get a safe handle to the standard console buffer
69 | /// corresponding to the given console buffer ID.
70 | ///
71 | private static WinSafeHandle GetStdHandle(int bufferId)
72 | {
73 | if (!bufferId.EqualsAny(STD_INPUT_HANDLE, STD_OUTPUT_HANDLE, STD_ERROR_HANDLE))
74 | {
75 | throw new ArgumentException("Invalid console buffer ID.", nameof(bufferId));
76 | }
77 | WinSafeHandle safeHandle = new(WinInterop.GetStdHandle(bufferId));
78 |
79 | if (safeHandle.IsInvalid)
80 | {
81 | throw new ExternException(nameof(WinInterop.GetStdHandle));
82 | }
83 | return safeHandle;
84 | }
85 |
86 | ///
87 | /// Get a new mode to set for the console buffer
88 | /// corresponding to the given console buffer safe handle.
89 | ///
90 | private static uint GetConsoleMode(WinSafeHandle safeHandle, uint mode)
91 | {
92 | ThrowIf.InvalidHandle(safeHandle);
93 | ThrowIf.Zero(mode);
94 |
95 | if (!WinInterop.GetConsoleMode(safeHandle.Handle, out uint outputMode))
96 | {
97 | throw new ExternException(nameof(WinInterop.GetConsoleMode));
98 | }
99 | return outputMode | mode;
100 | }
101 |
102 | ///
103 | /// Set the mode of the console buffer corresponding
104 | /// to the given console buffer safe handle.
105 | ///
106 | private static void SetConsoleMode(WinSafeHandle safeHandle, uint mode)
107 | {
108 | ThrowIf.InvalidHandle(safeHandle);
109 | ThrowIf.Zero(mode);
110 |
111 | if (!WinInterop.SetConsoleMode(safeHandle.Handle, mode))
112 | {
113 | throw new ExternException(nameof(WinInterop.SetConsoleMode));
114 | }
115 | }
116 | #endif // WINDOWS
117 | }
118 |
--------------------------------------------------------------------------------
/src/DotnetCat/Shell/WinApi/ExternException.cs:
--------------------------------------------------------------------------------
1 | #if WINDOWS
2 | using System;
3 | using System.Diagnostics.CodeAnalysis;
4 | using DotnetCat.Errors;
5 |
6 | namespace DotnetCat.Shell.WinApi;
7 |
8 | ///
9 | /// Windows API unmanaged function exception.
10 | ///
11 | internal class ExternException : Exception
12 | {
13 | ///
14 | /// Initialize the object.
15 | ///
16 | public ExternException([NotNull] string? externName) : base()
17 | {
18 | ThrowIf.NullOrEmpty(externName);
19 | Name = externName;
20 |
21 | ErrorCode = WinInterop.GetLastError();
22 | Message = $"Error occurred in extern '{Name}': {ErrorCode}.";
23 | }
24 |
25 | ///
26 | /// Windows API error code.
27 | ///
28 | public int ErrorCode { get; }
29 |
30 | ///
31 | /// Error message.
32 | ///
33 | public override string Message { get; }
34 |
35 | ///
36 | /// Windows API function name.
37 | ///
38 | public string Name { get; }
39 | }
40 |
41 | #endif // WINDOWS
42 |
--------------------------------------------------------------------------------
/src/DotnetCat/Shell/WinApi/InputMode.cs:
--------------------------------------------------------------------------------
1 | #if WINDOWS
2 | using System;
3 |
4 | namespace DotnetCat.Shell.WinApi;
5 |
6 | ///
7 | /// Windows API console input mode enumeration type flags.
8 | ///
9 | [Flags]
10 | internal enum InputMode : uint
11 | {
12 | UNKNOWN_INPUT_MODE = 0x0000,
13 | ENABLE_PROCESSED_INPUT = 0x0001,
14 | ENABLE_LINE_INPUT = 0x0002,
15 | ENABLE_ECHO_INPUT = 0x0004,
16 | ENABLE_WINDOW_INPUT = 0x0008,
17 | ENABLE_MOUSE_INPUT = 0x0010,
18 | ENABLE_INSERT_MODE = 0x0020,
19 | ENABLE_QUICK_EDIT_MODE = 0x0040,
20 | ENABLE_EXTENDED_FLAGS = 0x0080,
21 | ENABLE_AUTO_POSITION = 0x0100,
22 | ENABLE_VIRTUAL_TERMINAL_INPUT = 0x0200
23 | }
24 |
25 | #endif // WINDOWS
26 |
--------------------------------------------------------------------------------
/src/DotnetCat/Shell/WinApi/OutputMode.cs:
--------------------------------------------------------------------------------
1 | #if WINDOWS
2 | using System;
3 |
4 | namespace DotnetCat.Shell.WinApi;
5 |
6 | ///
7 | /// Windows API console output mode enumeration type flags.
8 | ///
9 | [Flags]
10 | internal enum OutputMode : uint
11 | {
12 | UNKNOWN_OUTPUT_MODE = 0x0000,
13 | ENABLE_PROCESSED_OUTPUT = 0x0001,
14 | ENABLE_WRAP_AT_EOL_OUTPUT = 0x0002,
15 | ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004,
16 | DISABLE_NEWLINE_AUTO_RETURN = 0x0008,
17 | ENABLE_LVB_GRID_WORLDWIDE = 0x0010
18 | }
19 |
20 | #endif // WINDOWS
21 |
--------------------------------------------------------------------------------
/src/DotnetCat/Shell/WinApi/WinInterop.cs:
--------------------------------------------------------------------------------
1 | #if WINDOWS
2 | using System.Runtime.InteropServices;
3 |
4 | namespace DotnetCat.Shell.WinApi;
5 |
6 | ///
7 | /// Windows API interoperability utility class.
8 | ///
9 | internal static partial class WinInterop
10 | {
11 | private const string KERNEL32 = "kernel32.dll";
12 |
13 | ///
14 | /// Close the given open object handle.
15 | ///
16 | [LibraryImport(KERNEL32, SetLastError = true)]
17 | [return: MarshalAs(UnmanagedType.Bool)]
18 | public static partial bool CloseHandle(nint hObject);
19 |
20 | ///
21 | /// Get the input or output mode of the console buffer
22 | /// corresponding to the given console buffer handle.
23 | ///
24 | [LibraryImport(KERNEL32, SetLastError = true)]
25 | [return: MarshalAs(UnmanagedType.Bool)]
26 | public static partial bool GetConsoleMode(nint hConsoleHandle, out uint lpMode);
27 |
28 | ///
29 | /// Set the input or output mode of the console buffer
30 | /// corresponding to the given console buffer handle.
31 | ///
32 | [LibraryImport(KERNEL32, SetLastError = true)]
33 | [return: MarshalAs(UnmanagedType.Bool)]
34 | public static partial bool SetConsoleMode(nint hConsoleHandle, uint dwMode);
35 |
36 | ///
37 | /// Get a handle to the standard console buffer
38 | /// corresponding to the given console buffer ID.
39 | ///
40 | [LibraryImport(KERNEL32, SetLastError = true)]
41 | [return: MarshalAs(UnmanagedType.SysInt)]
42 | public static partial nint GetStdHandle(int nStdHandle);
43 |
44 | ///
45 | /// Get the error code returned by the last extern call.
46 | ///
47 | public static int GetLastError() => Marshal.GetLastWin32Error();
48 | }
49 |
50 | #endif // WINDOWS
51 |
--------------------------------------------------------------------------------
/src/DotnetCat/Shell/WinApi/WinSafeHandle.cs:
--------------------------------------------------------------------------------
1 | #if WINDOWS
2 | using Microsoft.Win32.SafeHandles;
3 |
4 | namespace DotnetCat.Shell.WinApi;
5 |
6 | ///
7 | /// Windows API safe handle.
8 | ///
9 | internal class WinSafeHandle : SafeHandleZeroOrMinusOneIsInvalid
10 | {
11 | private readonly bool _ownsHandle; // Underlying handle owned
12 |
13 | private bool _disposed; // Object disposed
14 |
15 | ///
16 | /// Initialize the object.
17 | ///
18 | public WinSafeHandle(nint handle, bool ownsHandle = false) : base(ownsHandle)
19 | {
20 | _ownsHandle = ownsHandle;
21 | _disposed = false;
22 |
23 | Handle = handle;
24 | }
25 |
26 | ///
27 | /// Windows API handle.
28 | ///
29 | public nint Handle
30 | {
31 | get => handle;
32 | private set => handle = value;
33 | }
34 |
35 | ///
36 | /// Free the underlying resources.
37 | ///
38 | protected override void Dispose(bool disposing)
39 | {
40 | if (!_disposed)
41 | {
42 | if (_ownsHandle)
43 | {
44 | WinInterop.CloseHandle(Handle);
45 | Handle = nint.Zero;
46 | }
47 | _disposed = true;
48 | }
49 | base.Dispose(disposing);
50 | }
51 |
52 | ///
53 | /// Free the underlying open object handle.
54 | ///
55 | protected override bool ReleaseHandle()
56 | {
57 | return !_ownsHandle || WinInterop.CloseHandle(Handle);
58 | }
59 | }
60 |
61 | #endif // WINDOWS
62 |
--------------------------------------------------------------------------------
/src/DotnetCat/Utils/CmdLineArgs.cs:
--------------------------------------------------------------------------------
1 | using System.Net;
2 | using DotnetCat.IO.Pipelines;
3 |
4 | namespace DotnetCat.Utils;
5 |
6 | ///
7 | /// DotnetCat command-line arguments.
8 | ///
9 | internal class CmdLineArgs
10 | {
11 | private string? _hostName; // Network hostname
12 |
13 | ///
14 | /// Initialize the object.
15 | ///
16 | public CmdLineArgs()
17 | {
18 | Help = Listen = Verbose = false;
19 |
20 | PipeVariant = PipeType.Stream;
21 | TransOpt = TransferOpt.None;
22 | Port = 44444;
23 |
24 | Address = IPAddress.Any;
25 | }
26 |
27 | ///
28 | /// Display extended usage information and exit.
29 | ///
30 | public bool Help { get; set; }
31 |
32 | ///
33 | /// Run server and listen for inbound connection.
34 | ///
35 | public bool Listen { get; set; }
36 |
37 | ///
38 | /// Using executable pipeline.
39 | ///
40 | public bool UsingExe => !ExePath.IsNullOrEmpty();
41 |
42 | ///
43 | /// Enable verbose console output.
44 | ///
45 | public bool Verbose { get; set; }
46 |
47 | ///
48 | /// Pipeline variant.
49 | ///
50 | public PipeType PipeVariant { get; set; }
51 |
52 | ///
53 | /// File transfer option.
54 | ///
55 | public TransferOpt TransOpt { get; set; }
56 |
57 | ///
58 | /// Connection port number.
59 | ///
60 | public int Port { get; set; }
61 |
62 | ///
63 | /// Executable file path.
64 | ///
65 | public string? ExePath { get; set; }
66 |
67 | ///
68 | /// Transfer file path.
69 | ///
70 | public string? FilePath { get; set; }
71 |
72 | ///
73 | /// Hostname of the connection IPv4 address.
74 | ///
75 | public string HostName
76 | {
77 | get => _hostName ?? Address.ToString();
78 | set => _hostName = value;
79 | }
80 |
81 | ///
82 | /// User-defined string payload.
83 | ///
84 | public string? Payload { get; set; }
85 |
86 | ///
87 | /// IPv4 address to use for connection.
88 | ///
89 | public IPAddress Address { get; set; }
90 | }
91 |
--------------------------------------------------------------------------------
/src/DotnetCat/Utils/Extensions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections;
3 | using System.Collections.Generic;
4 | using System.Diagnostics.CodeAnalysis;
5 | using System.Linq;
6 | using System.Runtime.CompilerServices;
7 | using System.Text;
8 | using System.Text.RegularExpressions;
9 | using DotnetCat.Errors;
10 | using DotnetCat.Shell;
11 |
12 | namespace DotnetCat.Utils;
13 |
14 | ///
15 | /// Utility class for user-defined extension methods.
16 | ///
17 | internal static class Extensions
18 | {
19 | ///
20 | /// Add the result of the given functor to a collection.
21 | ///
22 | public static void Add([NotNull] this ICollection? values, Func func)
23 | {
24 | ThrowIf.Null(values);
25 | values.Add(func());
26 | }
27 |
28 | ///
29 | /// Add the results of the given functor to a collection.
30 | ///
31 | public static void AddRange([NotNull] this List? values,
32 | Func> func)
33 | {
34 | ThrowIf.Null(values);
35 | values.AddRange(func());
36 | }
37 |
38 | ///
39 | /// Perform an action on each element of a collection.
40 | ///
41 | public static void ForEach(this IEnumerable? values, Action action)
42 | {
43 | values?.ToList().ForEach(action);
44 | }
45 |
46 | ///
47 | /// Determine whether a string ends with a single or double quotation mark character.
48 | ///
49 | public static bool EndsWithQuote([NotNullWhen(true)] this string? str)
50 | {
51 | return str.EndsWithValue('"') || str.EndsWithValue('\'');
52 | }
53 |
54 | ///
55 | /// Determine whether a string ends with the given character.
56 | ///
57 | public static bool EndsWithValue([NotNullWhen(true)] this string? str, char value)
58 | {
59 | return str?.EndsWith(value) ?? false;
60 | }
61 |
62 | ///
63 | /// Determine whether a string ends with the given substring.
64 | ///
65 | public static bool EndsWithValue([NotNullWhen(true)] this string? str, string? value)
66 | {
67 | bool endsWith = false;
68 |
69 | if (str is not null && value is not null)
70 | {
71 | endsWith = str.EndsWith(value);
72 | }
73 | return endsWith;
74 | }
75 |
76 | ///
77 | /// Determine whether an object is equal to one or more values in a collection.
78 | ///
79 | public static bool EqualsAny([NotNullWhen(true)] this T? obj,
80 | params IEnumerable values)
81 | {
82 | return values.Any(v => RuntimeHelpers.Equals(v, obj));
83 | }
84 |
85 | ///
86 | /// Determine whether a string is null or empty.
87 | ///
88 | public static bool IsNullOrEmpty([NotNullWhen(false)] this string? str)
89 | {
90 | return str is null || str.Trim().Length == 0;
91 | }
92 |
93 | ///
94 | /// Determine whether a collection is null or empty.
95 | ///
96 | public static bool IsNullOrEmpty([NotNullWhen(false)] this IEnumerable? values)
97 | {
98 | return values is null || !values.Any();
99 | }
100 |
101 | ///
102 | /// Determine whether a string is equal to another
103 | /// string when all string casing is ignored.
104 | ///
105 | public static bool NoCaseEquals(this string? str, string? value)
106 | {
107 | return str?.ToLower() == value?.ToLower();
108 | }
109 |
110 | ///
111 | /// Determine whether a string starts with a
112 | /// single or double quotation mark character.
113 | ///
114 | public static bool StartsWithQuote([NotNullWhen(true)] this string? str)
115 | {
116 | return str.StartsWithValue('"') || str.StartsWithValue('\'');
117 | }
118 |
119 | ///
120 | /// Determine whether a string starts with the given character.
121 | ///
122 | public static bool StartsWithValue([NotNullWhen(true)] this string? str, char value)
123 | {
124 | return str?.StartsWith(value) ?? false;
125 | }
126 |
127 | ///
128 | /// Determine whether a string starts with the given substring.
129 | ///
130 | public static bool StartsWithValue([NotNullWhen(true)] this string? str,
131 | string? value)
132 | {
133 | bool startsWith = false;
134 |
135 | if (str is not null && value is not null)
136 | {
137 | startsWith = str.StartsWith(value);
138 | }
139 | return startsWith;
140 | }
141 |
142 | ///
143 | /// Erase all substrings matching a regular expression from the given data.
144 | ///
145 | public static string Erase(this Regex regex, [NotNull] string? data)
146 | {
147 | ThrowIf.NullOrEmpty(data);
148 | return regex.Replace(data, string.Empty);
149 | }
150 |
151 | ///
152 | /// Join each element of a collection separated by the given delimiter.
153 | ///
154 | public static string Join([NotNull] this IEnumerable? values,
155 | string? delim = default)
156 | {
157 | ThrowIf.NullOrEmpty(values);
158 | return string.Join(delim, values);
159 | }
160 |
161 | ///
162 | /// Join each element of a collection separated by the default system EOL.
163 | ///
164 | public static string JoinLines([NotNull] this IEnumerable? values)
165 | {
166 | return Join(values, SysInfo.Eol);
167 | }
168 |
169 | ///
170 | /// Normalize all the newline substrings in a string builder.
171 | ///
172 | public static StringBuilder ReplaceLineEndings(this StringBuilder sb)
173 | {
174 | return new StringBuilder(sb.ToString().ReplaceLineEndings());
175 | }
176 |
177 | ///
178 | /// Get the first value of the given type in a collection, or the
179 | /// default value of the type if no matching values are found.
180 | ///
181 | public static T? FirstOrDefaultOfType(this IEnumerable? values)
182 | {
183 | return values is null ? default : values.OfType().FirstOrDefault();
184 | }
185 |
186 | ///
187 | /// Enumerate the values of a collection as a collection of
188 | /// tuples containing the index and value of each element.
189 | ///
190 | public static IEnumerable<(int, T)> Enumerate(this IEnumerable? values)
191 | {
192 | return values?.Select((v, i) => (i, v)) ?? [];
193 | }
194 |
195 | ///
196 | /// Enumerate the values of a collection as a collection of tuples
197 | /// containing the index and value of each element, then filter the results.
198 | ///
199 | public static IEnumerable<(int, T)> Enumerate(this IEnumerable? values,
200 | Func<(int, T), bool> filter)
201 | {
202 | return Enumerate(values).Where(filter);
203 | }
204 | }
205 |
--------------------------------------------------------------------------------
/src/DotnetCat/Utils/Parser.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Diagnostics.CodeAnalysis;
4 | using System.Linq;
5 | using System.Net;
6 | using System.Text.RegularExpressions;
7 | using DotnetCat.Errors;
8 | using DotnetCat.IO;
9 | using DotnetCat.IO.Pipelines;
10 | using DotnetCat.Network;
11 | using DotnetCat.Shell;
12 | using IndexedAlias = (int Index, string Alias);
13 | using IndexedArg = (int Index, string Arg);
14 | using IndexedFlag = (int Index, string Flag);
15 |
16 | namespace DotnetCat.Utils;
17 |
18 | ///
19 | /// Command-line argument parser and validator.
20 | ///
21 | internal partial class Parser
22 | {
23 | private static readonly string _title; // Application title
24 |
25 | private readonly List _processedIndexes; // Processed argument indexes
26 |
27 | private List _argsList; // Command-line argument list
28 |
29 | ///
30 | /// Initialize the static class members.
31 | ///
32 | static Parser() => _title = SysInfo.IsLinux() ? "dncat" : "dncat.exe";
33 |
34 | ///
35 | /// Initialize the object.
36 | ///
37 | public Parser()
38 | {
39 | _processedIndexes = [];
40 | _argsList = [];
41 |
42 | CmdArgs = new CmdLineArgs();
43 | }
44 |
45 | ///
46 | /// Initialize the object.
47 | ///
48 | public Parser(IEnumerable args) : this() => Parse(args);
49 |
50 | ///
51 | /// Application repository URL.
52 | ///
53 | public static string Repo => "https://github.com/vandavey/DotnetCat";
54 |
55 | ///
56 | /// Application usage string.
57 | ///
58 | public static string Usage => $"Usage: {_title} [OPTIONS] TARGET";
59 |
60 | ///
61 | /// Parsed command-line arguments.
62 | ///
63 | public CmdLineArgs CmdArgs { get; }
64 |
65 | ///
66 | /// Write the extended application usage information to the
67 | /// standard console output stream and exit the application.
68 | ///
69 | [DoesNotReturn]
70 | public static void PrintHelp()
71 | {
72 | Console.WriteLine(GetHelpMessage());
73 | Environment.Exit(0);
74 | }
75 |
76 | ///
77 | /// Parse and validate all the arguments in
78 | /// the given command-line argument collection.
79 | ///
80 | public CmdLineArgs Parse(IEnumerable args)
81 | {
82 | _argsList = DefragArguments([.. args]);
83 | CmdArgs.Help = HelpFlagParsed();
84 |
85 | if (!CmdArgs.Help)
86 | {
87 | HandleMalformedArgs();
88 | ParseCharArgs();
89 | ParseFlagArgs();
90 | ParsePositionalArgs();
91 | }
92 | return CmdArgs;
93 | }
94 |
95 | ///
96 | /// Defragment the given fragmented command-line arguments
97 | /// so quoted strings are interpreted as single arguments.
98 | ///
99 | private static List DefragArguments(List args)
100 | {
101 | List defraggedArgs = [];
102 |
103 | // Defragment the given arguments
104 | for (int i = 0; i < args.Count; i++)
105 | {
106 | bool begQuoted = args[i].StartsWithQuote();
107 |
108 | if (!begQuoted || (begQuoted && args[i].EndsWithQuote()))
109 | {
110 | defraggedArgs.Add(args[i]);
111 | continue;
112 | }
113 |
114 | if (i == args.Count - 1)
115 | {
116 | defraggedArgs.Add(args[i]);
117 | break;
118 | }
119 |
120 | // Locate terminating argument and parse the range
121 | for (int j = i + 1; j < args.Count; j++)
122 | {
123 | if (args[j].EndsWithQuote())
124 | {
125 | string argStr = args[i..(j + 1)].Join(" ");
126 |
127 | // Remove leading and trailing quotes
128 | if (argStr.Length >= 2)
129 | {
130 | argStr = argStr[1..^1];
131 | }
132 | defraggedArgs.Add(argStr);
133 |
134 | i = j;
135 | break;
136 | }
137 | }
138 | }
139 |
140 | return defraggedArgs;
141 | }
142 |
143 | ///
144 | /// Determine whether the given argument is a command-line flag alias
145 | /// argument. Flag alias arguments begin with one dash (e.g., -f).
146 | ///
147 | private static bool IsAlias(string arg) => AliasRegex().IsMatch(arg);
148 |
149 | ///
150 | /// Determine whether the argument in the given tuple is a command-line flag
151 | /// alias argument. Flag alias arguments begin with one dash (e.g., -f).
152 | ///
153 | private static bool IsAlias(IndexedArg idxArg) => IsAlias(idxArg.Arg);
154 |
155 | ///
156 | /// Determine whether the given argument is a command-line flag
157 | /// argument. Flag arguments begin with two dashes (e.g., --foo).
158 | ///
159 | private static bool IsFlag(string arg) => FlagRegex().IsMatch(arg);
160 |
161 | ///
162 | /// Determine whether the argument in the given tuple is a command-line
163 | /// flag argument. Flag arguments begin with two dashes (e.g., --foo).
164 | ///
165 | private static bool IsFlag(IndexedArg idxArg) => IsFlag(idxArg.Arg);
166 |
167 | ///
168 | /// Get the extended application usage information message.
169 | ///
170 | private static string GetHelpMessage()
171 | {
172 | string exampleShell = SysInfo.IsLinux() ? "bash" : "powershell.exe";
173 |
174 | string helpMessage = $"""
175 | DotnetCat ({Repo})
176 | {Usage}{SysInfo.Eol}
177 | Remote command shell application{SysInfo.Eol}
178 | Positional Arguments:
179 | TARGET Remote or local IPv4 address{SysInfo.Eol}
180 | Optional Arguments:
181 | -h/-?, --help Show this help message and exit
182 | -v, --verbose Enable verbose console output
183 | -l, --listen Listen for incoming connections
184 | -z, --zero-io Report connection status only
185 | -p PORT, --port PORT Specify port to use for endpoint.
186 | (Default: 44444)
187 | -e EXEC, --exec EXEC Executable process file path
188 | -o PATH, --output PATH Receive file from remote host
189 | -s PATH, --send PATH Send local file or folder
190 | -t DATA, --text DATA Send string to remote host{SysInfo.Eol}
191 | Usage Examples:
192 | {_title} --listen --exec {exampleShell}
193 | {_title} -v localhost -p 44444
194 | {_title} -vs test.txt -p 2009 192.168.1.9{SysInfo.Eol}
195 | """;
196 | return helpMessage;
197 | }
198 |
199 | ///
200 | /// Command-line flag alias argument regular expression.
201 | ///
202 | [GeneratedRegex(@"^-[?\w]+$")]
203 | private static partial Regex AliasRegex();
204 |
205 | ///
206 | /// Command-line flag argument regular expression.
207 | ///
208 | [GeneratedRegex(@"^--\w+(-*\w*)*$")]
209 | private static partial Regex FlagRegex();
210 |
211 | ///
212 | /// Command-line help flag or flag alias argument regular expression.
213 | ///
214 | [GeneratedRegex(@"^-(-help|\w*[?Hh]+\w*)$")]
215 | private static partial Regex HelpFlagRegex();
216 |
217 | ///
218 | /// Determine whether any of the help flag (-h, -?, --help)
219 | /// named arguments exist in the underlying command-line argument list.
220 | ///
221 | private bool HelpFlagParsed()
222 | {
223 | return _argsList.IsNullOrEmpty() || _argsList.Any(HelpFlagRegex().IsMatch);
224 | }
225 |
226 | ///
227 | /// Handle malformed argument errors if any malformed arguments were parsed.
228 | ///
229 | private void HandleMalformedArgs()
230 | {
231 | if (_argsList.Contains("-"))
232 | {
233 | Error.Handle(Except.InvalidArgs, "-", true);
234 | }
235 | else if (_argsList.Contains("--"))
236 | {
237 | Error.Handle(Except.InvalidArgs, "--", true);
238 | }
239 | }
240 |
241 | ///
242 | /// Parse the named flag alias arguments in the underlying command-line
243 | /// argument list. Flag alias arguments begin with one dash (e.g., -f).
244 | ///
245 | private void ParseCharArgs()
246 | {
247 | foreach (IndexedAlias idxAlias in _argsList.Enumerate(IsAlias))
248 | {
249 | foreach (char ch in idxAlias.Alias)
250 | {
251 | switch (ch)
252 | {
253 | case '-':
254 | continue;
255 | case 'l':
256 | CmdArgs.Listen = true;
257 | break;
258 | case 'v':
259 | CmdArgs.Verbose = Error.Verbose = true;
260 | break;
261 | case 'z':
262 | CmdArgs.PipeVariant = PipeType.Status;
263 | break;
264 | case 'p':
265 | ParsePort(idxAlias);
266 | break;
267 | case 'e':
268 | ParseExecutable(idxAlias);
269 | break;
270 | case 't':
271 | ParseTextPayload(idxAlias);
272 | break;
273 | case 'o':
274 | ParseTransferPath(idxAlias, TransferOpt.Collect);
275 | break;
276 | case 's':
277 | ParseTransferPath(idxAlias, TransferOpt.Transmit);
278 | break;
279 | default:
280 | Error.Handle(Except.UnknownArgs, $"-{ch}", true);
281 | break;
282 | }
283 | }
284 | AddProcessedArg(idxAlias);
285 | }
286 |
287 | RemoveProcessedArgs();
288 | }
289 |
290 | ///
291 | /// Parse the named flag arguments in the underlying command-line argument
292 | /// list. Flag arguments begin with two dashes (e.g., --foo).
293 | ///
294 | private void ParseFlagArgs()
295 | {
296 | foreach (IndexedFlag idxFlag in _argsList.Enumerate(IsFlag))
297 | {
298 | switch (idxFlag.Flag)
299 | {
300 | case "--listen":
301 | CmdArgs.Listen = true;
302 | break;
303 | case "--verbose":
304 | CmdArgs.Verbose = Error.Verbose = true;
305 | break;
306 | case "--zero-io":
307 | CmdArgs.PipeVariant = PipeType.Status;
308 | break;
309 | case "--port":
310 | ParsePort(idxFlag);
311 | break;
312 | case "--exec":
313 | ParseExecutable(idxFlag);
314 | break;
315 | case "--text":
316 | ParseTextPayload(idxFlag);
317 | break;
318 | case "--output":
319 | ParseTransferPath(idxFlag, TransferOpt.Collect);
320 | break;
321 | case "--send":
322 | ParseTransferPath(idxFlag, TransferOpt.Transmit);
323 | break;
324 | default:
325 | Error.Handle(Except.UnknownArgs, idxFlag.Flag, true);
326 | break;
327 | }
328 | AddProcessedArg(idxFlag);
329 | }
330 |
331 | RemoveProcessedArgs();
332 | }
333 |
334 | ///
335 | /// Remove processed command-line arguments from the underlying argument list.
336 | ///
337 | private void RemoveProcessedArgs()
338 | {
339 | int delta = 0;
340 |
341 | _processedIndexes.Order().ForEach(i => _argsList.RemoveAt(i - delta++));
342 | _processedIndexes.Clear();
343 | }
344 |
345 | ///
346 | /// Parse the positional arguments in the underlying command-line argument
347 | /// list. Positional arguments do not begin with - or --.
348 | ///
349 | private void ParsePositionalArgs()
350 | {
351 | switch (_argsList.Count)
352 | {
353 | case 0: // Missing TARGET
354 | {
355 | if (!CmdArgs.Listen)
356 | {
357 | Error.Handle(Except.RequiredArgs, "TARGET", true);
358 | }
359 | break;
360 | }
361 | case 1: // Validate TARGET
362 | {
363 | if (_argsList[0].StartsWith('-'))
364 | {
365 | Error.Handle(Except.UnknownArgs, _argsList[0], true);
366 | }
367 | Exception? ex = null;
368 |
369 | // Parse the connection IPv4 address
370 | if (IPAddress.TryParse(_argsList[0], out IPAddress? addr))
371 | {
372 | CmdArgs.Address = addr;
373 | }
374 | else // Resolve the hostname
375 | {
376 | CmdArgs.Address = Net.ResolveName(_argsList[0], out ex);
377 | }
378 | CmdArgs.HostName = _argsList[0];
379 |
380 | if (CmdArgs.Address.Equals(IPAddress.None))
381 | {
382 | Error.Handle(Except.HostNotFound, _argsList[0], true, ex);
383 | }
384 | break;
385 | }
386 | default: // Unexpected arguments
387 | {
388 | string argsStr = _argsList.Join(", ");
389 |
390 | if (_argsList[0].StartsWithValue('-'))
391 | {
392 | Error.Handle(Except.UnknownArgs, argsStr, true);
393 | }
394 | Error.Handle(Except.InvalidArgs, argsStr, true);
395 | break;
396 | }
397 | }
398 | }
399 |
400 | ///
401 | /// Determine whether the given index is a valid index of an
402 | /// argument in the underlying command-line argument list.
403 | ///
404 | private bool ValidIndex(int index) => index >= 0 && index < _argsList.Count;
405 |
406 | ///
407 | /// Get the value of the argument located at the given
408 | /// index in the underlying command-line argument list.
409 | ///
410 | private string ArgsValueAt(int index)
411 | {
412 | if (!ValidIndex(index))
413 | {
414 | Error.Handle(Except.NamedArgs, _argsList[index - 1], true);
415 | }
416 | return _argsList[index];
417 | }
418 |
419 | ///
420 | /// Parse and validate the network port number argument in the underlying
421 | /// command-line argument list using the given flag or flag alias index.
422 | ///
423 | private void ParsePort(IndexedFlag idxFlag)
424 | {
425 | if (!ValidIndex(idxFlag.Index + 1))
426 | {
427 | Error.Handle(Except.NamedArgs, idxFlag.Flag, true);
428 | }
429 | string portStr = ArgsValueAt(idxFlag.Index + 1);
430 |
431 | if (!int.TryParse(portStr, out int port) || !Net.ValidPort(port))
432 | {
433 | Console.WriteLine(Usage);
434 | Error.Handle(Except.InvalidPort, portStr);
435 | }
436 |
437 | CmdArgs.Port = port;
438 | AddProcessedValueArg(idxFlag);
439 | }
440 |
441 | ///
442 | /// Parse and validate the executable path argument in the underlying
443 | /// command-line argument list using the given flag or flag alias index.
444 | ///
445 | private void ParseExecutable(IndexedFlag idxFlag)
446 | {
447 | if (!ValidIndex(idxFlag.Index + 1))
448 | {
449 | Error.Handle(Except.NamedArgs, idxFlag.Flag, true);
450 | }
451 | string exe = ArgsValueAt(idxFlag.Index + 1);
452 |
453 | if (!FileSys.ExistsOnPath(exe, out string? path))
454 | {
455 | Error.Handle(Except.ExePath, exe, true);
456 | }
457 |
458 | CmdArgs.ExePath = path;
459 | CmdArgs.PipeVariant = PipeType.Process;
460 |
461 | AddProcessedValueArg(idxFlag);
462 | }
463 |
464 | ///
465 | /// Parse and validate the transfer file path argument in the underlying
466 | /// command-line argument list using the given flag or flag alias index.
467 | ///
468 | private void ParseTransferPath(IndexedFlag idxFlag, TransferOpt transfer)
469 | {
470 | if (transfer is TransferOpt.None)
471 | {
472 | throw new ArgumentException("No file transfer option set.", nameof(transfer));
473 | }
474 |
475 | // No corresponding argument value
476 | if (!ValidIndex(idxFlag.Index + 1))
477 | {
478 | Error.Handle(Except.NamedArgs, idxFlag.Flag, true);
479 | }
480 | string? path = FileSys.ResolvePath(ArgsValueAt(idxFlag.Index + 1));
481 |
482 | // File path resolution failure
483 | if (path.IsNullOrEmpty())
484 | {
485 | Error.Handle(Except.FilePath, path, true);
486 | }
487 | string? parentPath = FileSys.ParentPath(path);
488 |
489 | // Parent path must exist for both collection and transmission
490 | if (parentPath.IsNullOrEmpty() || !FileSys.DirectoryExists(parentPath))
491 | {
492 | Error.Handle(Except.DirectoryPath, parentPath, true);
493 | }
494 |
495 | // File must exist to be transmitted
496 | if (!FileSys.FileExists(path) && transfer is TransferOpt.Transmit)
497 | {
498 | Error.Handle(Except.FilePath, path, true);
499 | }
500 |
501 | CmdArgs.FilePath = path;
502 | CmdArgs.PipeVariant = PipeType.File;
503 | CmdArgs.TransOpt = transfer;
504 |
505 | AddProcessedValueArg(idxFlag);
506 | }
507 |
508 | ///
509 | /// Parse and validate the arbitrary payload argument in the underlying
510 | /// command-line argument list using the given flag or flag alias index.
511 | ///
512 | private void ParseTextPayload(IndexedFlag idxFlag)
513 | {
514 | if (!ValidIndex(idxFlag.Index + 1))
515 | {
516 | Error.Handle(Except.NamedArgs, idxFlag.Flag, true);
517 | }
518 | string data = ArgsValueAt(idxFlag.Index + 1);
519 |
520 | if (data.IsNullOrEmpty())
521 | {
522 | Error.Handle(Except.Payload, idxFlag.Flag, true);
523 | }
524 |
525 | CmdArgs.Payload = data;
526 | CmdArgs.PipeVariant = PipeType.Text;
527 |
528 | AddProcessedValueArg(idxFlag);
529 | }
530 |
531 | ///
532 | /// Mark the command-line argument at the given index as processed.
533 | ///
534 | private void AddProcessedArg(IndexedArg idxArg, int indexOffset = 0)
535 | {
536 | ThrowIf.Negative(idxArg.Index);
537 | ThrowIf.Negative(indexOffset);
538 |
539 | _processedIndexes.Add(idxArg.Index + indexOffset);
540 | }
541 |
542 | ///
543 | /// Mark the command-line flag or flag alias value argument
544 | /// immediately after the given index as processed.
545 | ///
546 | ///
547 | /// The command-line flag or flag alias will not be marked
548 | /// as processed, only its corresponding value argument.
549 | ///
550 | private void AddProcessedValueArg(IndexedFlag idxFlag) => AddProcessedArg(idxFlag, 1);
551 | }
552 |
--------------------------------------------------------------------------------
/src/DotnetCat/bin/ARM64/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !**/.gitignore
3 |
--------------------------------------------------------------------------------
/src/DotnetCat/bin/Debug/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !**/.gitignore
3 |
--------------------------------------------------------------------------------
/src/DotnetCat/bin/Linux/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !**/.gitignore
3 |
--------------------------------------------------------------------------------
/src/DotnetCat/bin/Publish/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !**/.gitignore
3 |
--------------------------------------------------------------------------------
/src/DotnetCat/bin/Release/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !**/.gitignore
3 |
--------------------------------------------------------------------------------
/src/DotnetCat/bin/Zips/DotnetCat_linux-arm64.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vandavey/DotnetCat/38470154f0dc355cf5b6bc961d346f6dd1838ad7/src/DotnetCat/bin/Zips/DotnetCat_linux-arm64.zip
--------------------------------------------------------------------------------
/src/DotnetCat/bin/Zips/DotnetCat_linux-x64.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vandavey/DotnetCat/38470154f0dc355cf5b6bc961d346f6dd1838ad7/src/DotnetCat/bin/Zips/DotnetCat_linux-x64.zip
--------------------------------------------------------------------------------
/src/DotnetCat/bin/Zips/DotnetCat_win-x64.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vandavey/DotnetCat/38470154f0dc355cf5b6bc961d346f6dd1838ad7/src/DotnetCat/bin/Zips/DotnetCat_win-x64.zip
--------------------------------------------------------------------------------
/src/DotnetCat/bin/Zips/DotnetCat_win-x86.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vandavey/DotnetCat/38470154f0dc355cf5b6bc961d346f6dd1838ad7/src/DotnetCat/bin/Zips/DotnetCat_win-x86.zip
--------------------------------------------------------------------------------
/src/DotnetCat/obj/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !**/.gitignore
3 |
--------------------------------------------------------------------------------
/src/DotnetCatTests/DotnetCatTests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 | false
7 | DotnetCatTests
8 | vandavey
9 | Debug; Release; Linux; ARM64
10 | Copyright (c) 2024 vandavey
11 | false
12 | None
13 | DotnetCat unit tests
14 | None
15 | false
16 | Disable
17 | false
18 | obj/$(Configuration)
19 | false
20 | Enable
21 | false
22 | bin/$(Configuration)
23 | Library
24 | ../DotnetCat/Resources/LICENSE.md
25 | AnyCPU
26 | AnyCPU
27 | https://github.com/vandavey/DotnetCat
28 | DotnetCatTests
29 | net9.0
30 | 7
31 |
32 |
33 |
36 |
37 | true
38 | Portable
39 |
40 |
41 |
44 |
45 | true
46 | true
47 | true
48 | true
49 |
50 |
51 |
54 |
55 | LINUX
56 | WINDOWS
57 |
58 |
59 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
71 |
72 |
73 |
74 |
75 |
78 |
79 |
80 | <_Parameter1>Scope = Microsoft.VisualStudio.TestTools.UnitTesting.ExecutionScope.MethodLevel
81 | <_Parameter1_IsLiteral>true
82 |
83 |
84 |
85 |
--------------------------------------------------------------------------------
/src/DotnetCatTests/Errors/ErrorMessageTests.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Microsoft.VisualStudio.TestTools.UnitTesting;
3 | using DotnetCat.Errors;
4 |
5 | namespace DotnetCatTests.Errors;
6 |
7 | ///
8 | /// Unit tests for class .
9 | ///
10 | [TestClass]
11 | public class ErrorMessageTests
12 | {
13 | #region ConstructorTests
14 | ///
15 | /// Assert that is updated
16 | /// when the object is constructed with a valid input message.
17 | ///
18 | [DataTestMethod]
19 | [DataRow("test: %")]
20 | [DataRow("test: {}")]
21 | [DataRow("test: %, {}")]
22 | public void ErrorMessage_ValidMessage_SetsMessage(string expected)
23 | {
24 | ErrorMessage errorMsg = new(expected);
25 | string actual = errorMsg.Message;
26 |
27 | Assert.AreEqual(expected, actual, $"Expected property value: '{expected}'");
28 | }
29 |
30 | ///
31 | /// Assert that an is thrown
32 | /// when the object is constructed with a built input message.
33 | ///
34 | [DataTestMethod]
35 | [DataRow("test")]
36 | [DataRow("error")]
37 | [DataRow("test message")]
38 | public void ErrorMessage_BuiltMessage_ThrowsArgumentException(string msg)
39 | {
40 | Func func = () => _ = new ErrorMessage(msg);
41 | Assert.ThrowsException(func);
42 | }
43 |
44 | ///
45 | /// Assert that an is thrown when
46 | /// the object is constructed with an empty or blank input message.
47 | ///
48 | [DataTestMethod]
49 | [DataRow("")]
50 | [DataRow(" ")]
51 | public void ErrorMessage_EmptyMessage_ThrowsArgumentException(string msg)
52 | {
53 | Func func = () => _ = new ErrorMessage(msg);
54 | Assert.ThrowsException(func);
55 | }
56 |
57 | ///
58 | /// Assert that an is thrown
59 | /// when the object is constructed with a null input message.
60 | ///
61 | [TestMethod]
62 | public void ErrorMessage_NullMessage_ThrowsArgumentNullException()
63 | {
64 | string? msg = null;
65 |
66 | #nullable disable
67 | Func func = () => _ = new ErrorMessage(msg);
68 | #nullable enable
69 |
70 | Assert.ThrowsException(func);
71 | }
72 | #endregion // ConstructorTests
73 |
74 | #region MethodTests
75 | ///
76 | /// Assert that is correctly
77 | /// updated when the input argument is valid.
78 | ///
79 | [DataTestMethod]
80 | [DataRow("test: '%'", "", "test: ''")]
81 | [DataRow("test: '%'", " ", "test: ' '")]
82 | [DataRow("test: '{}'", null, "test: ''")]
83 | [DataRow("test: '%', '{}'", "data", "test: 'data', 'data'")]
84 | public void Build_ValidArg_BuildsMessage(string msg, string? arg, string expected)
85 | {
86 | ErrorMessage errorMsg = new(msg);
87 |
88 | _ = errorMsg.Build(arg);
89 | string actual = errorMsg.Message;
90 |
91 | Assert.AreEqual(expected, actual);
92 | }
93 |
94 | ///
95 | /// Assert that the correctly built error message value is
96 | /// returned when the input argument is valid.
97 | ///
98 | [DataTestMethod]
99 | [DataRow("test: '%'", "", "test: ''")]
100 | [DataRow("test: '%'", " ", "test: ' '")]
101 | [DataRow("test: '{}'", null, "test: ''")]
102 | [DataRow("test: '%', '{}'", "data", "test: 'data', 'data'")]
103 | public void Build_ValidArg_ReturnsExpected(string msg, string? arg, string expected)
104 | {
105 | ErrorMessage errorMsg = new(msg);
106 | string actual = errorMsg.Build(arg);
107 |
108 | Assert.AreEqual(expected, actual);
109 | }
110 |
111 | ///
112 | /// Assert that an is thrown
113 | /// when is already built.
114 | ///
115 | [DataTestMethod]
116 | [DataRow("test: '%'", "")]
117 | [DataRow("test: '%'", " ")]
118 | [DataRow("test: '{}'", null)]
119 | [DataRow("test: '%', '{}'", "data")]
120 | public void Build_MsgBuilt_ThrowsInvalidOperationException(string msg, string? arg)
121 | {
122 | ErrorMessage errorMsg = new(msg);
123 | _ = errorMsg.Build(arg);
124 |
125 | Func func = () => _ = errorMsg.Build(arg);
126 |
127 | Assert.ThrowsException(func);
128 | }
129 | #endregion // MethodTests
130 | }
131 |
--------------------------------------------------------------------------------
/src/DotnetCatTests/IO/FileSysTests.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using Microsoft.VisualStudio.TestTools.UnitTesting;
4 | using DotnetCat.IO;
5 | using SpecialFolder = System.Environment.SpecialFolder;
6 |
7 | namespace DotnetCatTests.IO;
8 |
9 | ///
10 | /// Unit tests for utility class .
11 | ///
12 | [TestClass]
13 | public class FileSysTests
14 | {
15 | #region PropertyTests
16 | ///
17 | /// Assert that the current user's home directory path is returned.
18 | ///
19 | [TestMethod]
20 | public void UserProfile_Getter_ReturnsHomePath()
21 | {
22 | SpecialFolder folder = SpecialFolder.UserProfile;
23 |
24 | string expected = Environment.GetFolderPath(folder);
25 | string actual = FileSys.UserProfile;
26 |
27 | Assert.AreEqual(expected, actual, $"Expected home path: '{expected}'");
28 | }
29 |
30 | ///
31 | /// Assert that the resulting user home directory path exists.
32 | ///
33 | [TestMethod]
34 | public void UserProfile_Getter_HomePathExists()
35 | {
36 | string homePath = FileSys.UserProfile;
37 | bool actual = Directory.Exists(homePath);
38 |
39 | Assert.IsTrue(actual, $"User home path '{homePath}' does not exist");
40 | }
41 | #endregion // PropertyTests
42 |
43 | #region MethodTests
44 | ///
45 | /// Assert that a valid (existing) input file or directory
46 | /// path on Linux operating systems returns true.
47 | ///
48 | [DataTestMethod]
49 | [DataRow("~")]
50 | [DataRow(@".\..")]
51 | [DataRow("/etc")]
52 | [DataRow(@"\dev\error")]
53 | [DataRow("/dev/stdout")]
54 | public void Exists_ValidLinuxPath_ReturnsTrue(string? path)
55 | {
56 | if (OperatingSystem.IsLinux())
57 | {
58 | bool actual = FileSys.Exists(path);
59 | Assert.IsTrue(actual, $"Expected path '{path}' to exist");
60 | }
61 | }
62 |
63 | ///
64 | /// Assert that a valid (existing) input file or directory
65 | /// path on Windows operating systems returns true.
66 | ///
67 | [DataTestMethod]
68 | [DataRow("~")]
69 | [DataRow(@".\..")]
70 | [DataRow(@"C:\Users")]
71 | [DataRow("/Windows/System32/dism.exe")]
72 | [DataRow(@"C:\Windows/System32\sfc.exe")]
73 | public void Exists_ValidWindowsPath_ReturnsTrue(string? path)
74 | {
75 | if (OperatingSystem.IsWindows())
76 | {
77 | bool actual = FileSys.Exists(path);
78 | Assert.IsTrue(actual, $"Expected path '{path}' to exist");
79 | }
80 | }
81 |
82 | ///
83 | /// Assert that an invalid (nonexistent) input file or directory path returns false.
84 | ///
85 | [DataTestMethod]
86 | [DataRow("")]
87 | [DataRow(" ")]
88 | #if WINDOWS
89 | [DataRow("/Windows/Desktop")]
90 | [DataRow(@"C:\Windows\Files\explorer.exe")]
91 | #elif LINUX
92 | [DataRow("/usr/shared")]
93 | [DataRow(@"\bin\files\run")]
94 | #endif // WINDOWS
95 | public void Exists_InvalidPath_ReturnsFalse(string? path)
96 | {
97 | bool actual = FileSys.Exists(path);
98 | Assert.IsFalse(actual, $"Expected path '{path}' to not exist");
99 | }
100 |
101 | ///
102 | /// Assert that a null input file or directory path returns false.
103 | ///
104 | [TestMethod]
105 | public void Exists_NullPath_ReturnsFalse()
106 | {
107 | string? path = null;
108 | bool actual = FileSys.Exists(path);
109 |
110 | Assert.IsFalse(actual, "Expected null path to not exist");
111 | }
112 |
113 | ///
114 | /// Assert that a valid (existing) input file
115 | /// path on Linux operating systems returns true.
116 | ///
117 | [DataTestMethod]
118 | [DataRow("/etc/hosts")]
119 | [DataRow(@"/bin\sh")]
120 | [DataRow(@"\dev\stdout")]
121 | public void FileExists_ValidLinuxPath_ReturnsTrue(string? path)
122 | {
123 | if (OperatingSystem.IsLinux())
124 | {
125 | bool actual = FileSys.FileExists(path);
126 | Assert.IsTrue(actual, $"Expected file path '{path}' to exist");
127 | }
128 | }
129 |
130 | ///
131 | /// Assert that a valid (existing) input file path
132 | /// on Windows operating systems returns true.
133 | ///
134 | [DataTestMethod]
135 | [DataRow("/Windows/explorer.exe")]
136 | [DataRow(@"C:\Windows/System32\sfc.exe")]
137 | [DataRow(@"\Windows\System32\dism.exe")]
138 | public void FileExists_ValidWindowsPath_ReturnsTrue(string? path)
139 | {
140 | if (OperatingSystem.IsWindows())
141 | {
142 | bool actual = FileSys.FileExists(path);
143 | Assert.IsTrue(actual, $"Expected file path '{path}' to exist");
144 | }
145 | }
146 |
147 | ///
148 | /// Assert that an invalid (nonexistent) input file path returns false.
149 | ///
150 | [DataTestMethod]
151 | [DataRow("")]
152 | [DataRow(" ")]
153 | [DataRow("~")]
154 | #if WINDOWS
155 | [DataRow("~/Documents")]
156 | [DataRow(@"C:\Windows\System32\explorer.exe")]
157 | #elif LINUX
158 | [DataRow("/dev/output")]
159 | [DataRow(@"\bin\sbin\sh")]
160 | #endif // WINDOWS
161 | public void FileExists_InvalidPath_ReturnsFalse(string? path)
162 | {
163 | bool actual = FileSys.FileExists(path);
164 | Assert.IsFalse(actual, $"Expected file path '{path}' to not exist");
165 | }
166 |
167 | ///
168 | /// Assert that a null input file path returns false.
169 | ///
170 | [TestMethod]
171 | public void FileExists_NullPath_ReturnsFalse()
172 | {
173 | string? path = null;
174 | bool actual = FileSys.FileExists(path);
175 |
176 | Assert.IsFalse(actual, "Expected null file path to not exist");
177 | }
178 |
179 | ///
180 | /// Assert that a valid (existing) input directory path returns true.
181 | ///
182 | [DataTestMethod]
183 | [DataRow("~")]
184 | [DataRow("/")]
185 | [DataRow(".")]
186 | [DataRow("..")]
187 | [DataRow(@"..\..")]
188 | public void DirectoryExists_ValidPath_ReturnsTrue(string? path)
189 | {
190 | bool actual = FileSys.DirectoryExists(path);
191 | Assert.IsTrue(actual, $"Expected directory path '{path}' to exist");
192 | }
193 |
194 | ///
195 | /// Assert that an invalid (nonexistent) input directory path returns false.
196 | ///
197 | [DataTestMethod]
198 | [DataRow("")]
199 | [DataRow(" ")]
200 | #if WINDOWS
201 | [DataRow("~/Uploads")]
202 | [DataRow(@"\Windows\System32\explorer.exe")]
203 | #elif LINUX
204 | [DataRow("/bin/sh")]
205 | [DataRow(@"\dev\output")]
206 | #endif // WINDOWS
207 | public void DirectoryExists_InvalidPath_ReturnsFalse(string? path)
208 | {
209 | bool actual = FileSys.DirectoryExists(path);
210 | Assert.IsFalse(actual, $"Expected directory path '{path}' to not exist");
211 | }
212 |
213 | ///
214 | /// Assert that a null input directory path returns false.
215 | ///
216 | [TestMethod]
217 | public void DirectoryExists_NullPath_ReturnsFalse()
218 | {
219 | string? path = null;
220 | bool actual = FileSys.DirectoryExists(path);
221 |
222 | Assert.IsFalse(actual, "Expected null directory path to not exist");
223 | }
224 | #endregion // MethodTests
225 | }
226 |
--------------------------------------------------------------------------------
/src/DotnetCatTests/Network/NetTests.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Net.Sockets;
3 | using Microsoft.VisualStudio.TestTools.UnitTesting;
4 | using DotnetCat.Errors;
5 | using DotnetCat.Network;
6 |
7 | namespace DotnetCatTests.Network;
8 |
9 | ///
10 | /// Unit tests for utility class .
11 | ///
12 | [TestClass]
13 | public class NetTests
14 | {
15 | #region MethodTests
16 | ///
17 | /// Assert that an input returns
18 | /// the expected enumeration type member.
19 | ///
20 | [DataTestMethod]
21 | [DataRow(SocketError.SocketError, (byte)Except.SocketError)]
22 | [DataRow(SocketError.ConnectionRefused, (byte)Except.ConnectionRefused)]
23 | [DataRow(SocketError.SystemNotReady, (byte)Except.SocketError)]
24 | public void GetExcept_AggregateException_ReturnsExpected(SocketError error,
25 | byte expectedByte)
26 | {
27 | SocketException innerEx = new((int)error);
28 | AggregateException aggregateEx = new(innerEx);
29 |
30 | Except expected = (Except)expectedByte;
31 | Except actual = Net.GetExcept(aggregateEx);
32 |
33 | Assert.AreEqual(expected, actual, $"Enum result should be '{expected}'");
34 | }
35 |
36 | ///
37 | /// Assert that an input returns
38 | /// the expected enumeration type member.
39 | ///
40 | [DataTestMethod]
41 | [DataRow(SocketError.SocketError, (byte)Except.SocketError)]
42 | [DataRow(SocketError.ConnectionRefused, (byte)Except.ConnectionRefused)]
43 | [DataRow(SocketError.SystemNotReady, (byte)Except.SocketError)]
44 | public void GetExcept_SocketException_ReturnsExpected(SocketError error,
45 | byte expectedByte)
46 | {
47 | SocketException socketEx = new((int)error);
48 |
49 | Except expected = (Except)expectedByte;
50 | Except actual = Net.GetExcept(socketEx);
51 |
52 | Assert.AreEqual(expected, actual, $"Enum result should be '{expected}'");
53 | }
54 |
55 | ///
56 | /// Assert that an input with an inner
57 | /// returns the inner .
58 | ///
59 | [DataTestMethod]
60 | [DataRow(SocketError.SocketError)]
61 | [DataRow(SocketError.ConnectionRefused)]
62 | [DataRow(SocketError.SystemNotReady)]
63 | public void SocketException_InnerException_ReturnsException(SocketError error)
64 | {
65 | SocketException expected = new((int)error);
66 | AggregateException aggregateEx = new(expected);
67 |
68 | SocketException? actual = Net.SocketException(aggregateEx);
69 |
70 | Assert.AreEqual(expected, actual, "Failure extracting socket exception");
71 | }
72 |
73 | ///
74 | /// Assert that an input without
75 | /// an inner returns null.
76 | ///
77 | [TestMethod]
78 | public void SocketException_NoInnerException_ReturnsNull()
79 | {
80 | AggregateException aggregateEx = new();
81 | SocketException? actual = Net.SocketException(aggregateEx);
82 |
83 | Assert.IsNull(actual, "Resulting socket exception should be null");
84 | }
85 |
86 | ///
87 | /// Assert that a valid input network port number returns true.
88 | ///
89 | [DataTestMethod]
90 | [DataRow(80)]
91 | [DataRow(443)]
92 | [DataRow(8443)]
93 | public void ValidPort_ValidPort_ReturnsTrue(int port)
94 | {
95 | bool actual = Net.ValidPort(port);
96 | Assert.IsTrue(actual, $"Port '{port}' should be considered valid");
97 | }
98 |
99 | ///
100 | /// Assert that an invalid input network port number returns false.
101 | ///
102 | [DataTestMethod]
103 | [DataRow(-80)]
104 | [DataRow(0)]
105 | [DataRow(65536)]
106 | public void ValidPort_InvalidPort_ReturnsFalse(int port)
107 | {
108 | bool actual = Net.ValidPort(port);
109 | Assert.IsFalse(actual, $"Port '{port}' should be considered invalid");
110 | }
111 |
112 | ///
113 | /// Assert that an input socket error returns a
114 | /// that was constructed with the correct socket error.
115 | ///
116 | [DataTestMethod]
117 | [DataRow(SocketError.HostDown)]
118 | [DataRow(SocketError.NetworkUnreachable)]
119 | [DataRow(SocketError.TimedOut)]
120 | public void MakeException_Error_ReturnsWithCorrectError(SocketError expected)
121 | {
122 | SocketException socketEx = Net.MakeException(expected);
123 | SocketError actual = socketEx.SocketErrorCode;
124 |
125 | Assert.AreEqual(expected, actual, $"Expected error code: '{expected}'");
126 | }
127 | #endregion // MethodTests
128 | }
129 |
--------------------------------------------------------------------------------
/src/DotnetCatTests/Properties/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !**/.gitignore
3 |
--------------------------------------------------------------------------------
/src/DotnetCatTests/Shell/CommandTests.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Diagnostics;
3 | using Microsoft.VisualStudio.TestTools.UnitTesting;
4 | using DotnetCat.Shell;
5 |
6 | namespace DotnetCatTests.Shell;
7 |
8 | ///
9 | /// Unit tests for utility class .
10 | ///
11 | [TestClass]
12 | public class CommandTests
13 | {
14 | #region MethodTests
15 | ///
16 | /// Assert that a valid input environment variable name returns the
17 | /// value of the corresponding variable in the local system.
18 | ///
19 | [DataTestMethod]
20 | [DataRow("PATH")]
21 | #if WINDOWS
22 | [DataRow("USERNAME")]
23 | [DataRow("USERPROFILE")]
24 | #elif LINUX
25 | [DataRow("HOME")]
26 | [DataRow("USER")]
27 | #endif // WINDOWS
28 | public void EnvVariable_ValidEnvVariableName_ReturnsExpected(string name)
29 | {
30 | string? expected = Environment.GetEnvironmentVariable(name);
31 | string? actual = Command.EnvVariable(name);
32 |
33 | Assert.AreEqual(expected, actual, $"Incorrect value for variable '{name}'");
34 | }
35 |
36 | ///
37 | /// Assert that an invalid input environment variable name returns null.
38 | ///
39 | [DataTestMethod]
40 | [DataRow("DotnetCatTest_Test")]
41 | [DataRow("DotnetCatTest_Data")]
42 | public void EnvVariable_InvalidEnvVariableName_ReturnsNull(string name)
43 | {
44 | string? actual = Command.EnvVariable(name);
45 | Assert.IsNull(actual, $"Value for variable '{name}' should be null");
46 | }
47 |
48 | ///
49 | /// Assert that a null input environment variable name causes
50 | /// an to be thrown.
51 | ///
52 | [TestMethod]
53 | public void EnvVariable_NullEnvVariableName_ThrowsArgumentNullException()
54 | {
55 | string? name = null;
56 |
57 | #nullable disable
58 | Func func = () => Command.EnvVariable(name);
59 | #nullable enable
60 |
61 | Assert.ThrowsException(func);
62 | }
63 |
64 | ///
65 | /// Assert that a non-null input shell name returns
66 | /// a new object.
67 | ///
68 | [DataTestMethod]
69 | [DataRow("test")]
70 | [DataRow("data.exe")]
71 | public void ExeStartInfo_NonNullShell_ReturnsNewStartInfo(string shell)
72 | {
73 | ProcessStartInfo? actual = Command.ExeStartInfo(shell);
74 | Assert.IsNotNull(actual, "Resulting startup information should not be null");
75 | }
76 |
77 | ///
78 | /// Assert that a null input shell name causes an
79 | /// to be thrown.
80 | ///
81 | [TestMethod]
82 | public void ExeStartInfo_NullShell_ThrowsArgumentNullException()
83 | {
84 | string? shell = null;
85 | Func func = () => Command.ExeStartInfo(shell);
86 |
87 | Assert.ThrowsException(func);
88 | }
89 |
90 | ///
91 | /// Assert that a valid input clear-screen command name returns true.
92 | ///
93 | [DataTestMethod]
94 | [DataRow("clear")]
95 | [DataRow("cls\n")]
96 | [DataRow("Clear-Host")]
97 | public void IsClearCmd_ValidCommand_ReturnsTrue(string command)
98 | {
99 | bool actual = Command.IsClearCmd(command);
100 | Assert.IsTrue(actual, $"'{command}' should be a clear-screen command");
101 | }
102 |
103 | ///
104 | /// Assert that an invalid input clear-screen command name returns false.
105 | ///
106 | [DataTestMethod]
107 | [DataRow("sudo")]
108 | [DataRow("cat\n")]
109 | [DataRow("Get-Location")]
110 | public void IsClearCmd_InvalidCommand_ReturnsFalse(string command)
111 | {
112 | bool actual = Command.IsClearCmd(command);
113 | Assert.IsFalse(actual, $"'{command}' should not be a clear-screen command");
114 | }
115 | #endregion // MethodTests
116 | }
117 |
--------------------------------------------------------------------------------
/src/DotnetCatTests/Utils/ExtensionsTests.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using Microsoft.VisualStudio.TestTools.UnitTesting;
5 | using DotnetCat.Utils;
6 |
7 | namespace DotnetCatTests.Utils;
8 |
9 | ///
10 | /// Unit tests for utility class .
11 | ///
12 | [TestClass]
13 | public class ExtensionsTests
14 | {
15 | #region MethodTests
16 | ///
17 | /// Assert that an input string ending with a specific character returns true.
18 | ///
19 | [DataTestMethod]
20 | [DataRow("test data", 'a')]
21 | [DataRow(" test data ", ' ')]
22 | [DataRow("test data 1", '1')]
23 | [DataRow("test data\0", '\0')]
24 | public void EndsWithValue_CharDoes_ReturnsTrue(string? str, char value)
25 | {
26 | bool actual = str.EndsWithValue(value);
27 | Assert.IsTrue(actual, $"Expected '{str}' to end with '{value}'");
28 | }
29 |
30 | ///
31 | /// Assert that an input string not ending with a specific character returns false.
32 | ///
33 | [DataTestMethod]
34 | [DataRow(null, 't')]
35 | [DataRow(" test data", ' ')]
36 | [DataRow("test data", 't')]
37 | [DataRow("\0test data", '\0')]
38 | public void EndsWithValue_CharDoesNot_ReturnsFalse(string? str, char value)
39 | {
40 | bool actual = str.EndsWithValue(value);
41 | Assert.IsFalse(actual, $"Expected '{str}' to not end with '{value}'");
42 | }
43 |
44 | ///
45 | /// Assert that an input string ending with a specific substring returns true.
46 | ///
47 | [DataTestMethod]
48 | [DataRow("test data", " data")]
49 | [DataRow("test data ", "data ")]
50 | [DataRow("test data", "test data")]
51 | public void EndsWithValue_StringDoes_ReturnsTrue(string? str, string? value)
52 | {
53 | bool actual = str.EndsWithValue(value);
54 | Assert.IsTrue(actual, $"Expected '{str}' to end with '{value}'");
55 | }
56 |
57 | ///
58 | /// Assert that an input string not ending with a specific substring returns false.
59 | ///
60 | [DataTestMethod]
61 | [DataRow(null, "test data")]
62 | [DataRow("test data", null)]
63 | [DataRow("test data", " data ")]
64 | public void EndsWithValue_StringDoesNot_ReturnsFalse(string? str, string? value)
65 | {
66 | bool actual = str.EndsWithValue(value);
67 | Assert.IsFalse(actual, $"Expected '{str}' to not end with '{value}'");
68 | }
69 |
70 | ///
71 | /// Assert that a null input string returns true.
72 | ///
73 | [TestMethod]
74 | public void IsNullOrEmpty_NullString_ReturnsTrue()
75 | {
76 | string? str = null;
77 | bool actual = str.IsNullOrEmpty();
78 |
79 | Assert.IsTrue(actual, "Expected null string to be null or empty");
80 | }
81 |
82 | ///
83 | /// Assert that an empty or blank input string returns true.
84 | ///
85 | [DataTestMethod]
86 | [DataRow("")]
87 | [DataRow(" ")]
88 | public void IsNullOrEmpty_EmptyOrBlankString_ReturnsTrue(string? str)
89 | {
90 | bool actual = str.IsNullOrEmpty();
91 | Assert.IsTrue(actual, "Expected empty/blank string to be null or empty");
92 | }
93 |
94 | ///
95 | /// Assert that a populated input string returns false.
96 | ///
97 | [DataTestMethod]
98 | [DataRow("testing")]
99 | [DataRow(" testing")]
100 | [DataRow("testing ")]
101 | public void IsNullOrEmpty_PopulatedString_ReturnsFalse(string? str)
102 | {
103 | bool actual = str.IsNullOrEmpty();
104 | Assert.IsFalse(actual, "Expected populated string to not be null or empty");
105 | }
106 |
107 | ///
108 | /// Assert that a null input array returns true.
109 | ///
110 | [TestMethod]
111 | public void IsNullOrEmpty_NullArray_ReturnsTrue()
112 | {
113 | string[]? array = null;
114 | bool actual = array.IsNullOrEmpty();
115 |
116 | Assert.IsTrue(actual, "Expected null array to be null or empty");
117 | }
118 |
119 | ///
120 | /// Assert that an empty input array returns true.
121 | ///
122 | [TestMethod]
123 | public void IsNullOrEmpty_EmptyArray_ReturnsTrue()
124 | {
125 | string[] array = [];
126 | bool actual = array.IsNullOrEmpty();
127 |
128 | Assert.IsTrue(actual, "Expected empty array to be null or empty");
129 | }
130 |
131 | ///
132 | /// Assert that a populated input array returns false.
133 | ///
134 | [DataTestMethod]
135 | [DataRow(1, 2, 3)]
136 | [DataRow("test", "data")]
137 | public void IsNullOrEmpty_PopulatedArray_ReturnsFalse(params object[] array)
138 | {
139 | bool actual = array.IsNullOrEmpty();
140 | Assert.IsFalse(actual, "Expected populated array to not be null or empty");
141 | }
142 |
143 | ///
144 | /// Assert that a null generic input list returns true.
145 | ///
146 | [TestMethod]
147 | public void IsNullOrEmpty_NullList_ReturnsTrue()
148 | {
149 | List? list = null;
150 | bool actual = list.IsNullOrEmpty();
151 |
152 | Assert.IsTrue(actual, "Expected null list to be null or empty");
153 | }
154 |
155 | ///
156 | /// Assert that an empty generic input list returns true.
157 | ///
158 | [TestMethod]
159 | public void IsNullOrEmpty_EmptyList_ReturnsTrue()
160 | {
161 | List