├── .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 | logo 3 |
4 | 5 | # DotnetCat 6 | 7 |
8 | csharp-13-badge 9 | license-badge 10 | contributors-badge 11 | pull-requests-badge 12 |
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 list = []; 162 | bool actual = list.IsNullOrEmpty(); 163 | 164 | Assert.IsTrue(actual, "Expected empty list to be null or empty"); 165 | } 166 | 167 | /// 168 | /// Assert that a populated generic input list returns false. 169 | /// 170 | [DataTestMethod] 171 | [DataRow(1, 2, 3)] 172 | [DataRow("test", "data")] 173 | public void IsNullOrEmpty_PopulatedList_ReturnsFalse(params object[] array) 174 | { 175 | List list = [.. array]; 176 | bool actual = list.IsNullOrEmpty(); 177 | 178 | Assert.IsFalse(actual, "Expected populated list to not be null or empty"); 179 | } 180 | 181 | /// 182 | /// Assert that an input string whose value is equal to 183 | /// another string when casing is ignored returns true. 184 | /// 185 | [DataTestMethod] 186 | [DataRow("", "")] 187 | [DataRow(" ", " ")] 188 | [DataRow("test", "TEST")] 189 | [DataRow("TEST", "test")] 190 | [DataRow("tEsT", "TeSt")] 191 | public void NoCaseEquals_EqualStrings_ReturnsTrue(string? str, string? value) 192 | { 193 | bool actual = str.NoCaseEquals(value); 194 | Assert.IsTrue(actual); 195 | } 196 | 197 | /// 198 | /// Assert that an input string whose value is not equal to 199 | /// another string when casing is ignored returns false. 200 | /// 201 | [DataTestMethod] 202 | [DataRow(null, "test")] 203 | [DataRow("test", null)] 204 | [DataRow("tEsT", "DaTa")] 205 | public void NoCaseEquals_NotEqualStrings_ReturnsFalse(string? str, string? value) 206 | { 207 | bool actual = str.NoCaseEquals(value); 208 | Assert.IsFalse(actual); 209 | } 210 | 211 | /// 212 | /// Assert that a null input string compared to another null string returns true. 213 | /// 214 | [TestMethod] 215 | public void NoCaseEquals_NullStrings_ReturnsTrue() 216 | { 217 | string? str = null; 218 | bool actual = str.NoCaseEquals(null); 219 | 220 | Assert.IsTrue(actual); 221 | } 222 | 223 | /// 224 | /// Assert that an input string starting with a specific character returns true. 225 | /// 226 | [DataTestMethod] 227 | [DataRow("test data", 't')] 228 | [DataRow(" test data ", ' ')] 229 | [DataRow("1 test data", '1')] 230 | [DataRow("\0test data", '\0')] 231 | public void StartsWithValue_CharDoes_ReturnsTrue(string? str, char value) 232 | { 233 | bool actual = str.StartsWithValue(value); 234 | Assert.IsTrue(actual, $"Expected '{str}' to start with '{value}'"); 235 | } 236 | 237 | /// 238 | /// Assert that an input string not starting with a specific character returns false. 239 | /// 240 | [DataTestMethod] 241 | [DataRow(null, 't')] 242 | [DataRow(" test data ", 't')] 243 | [DataRow("test data ", ' ')] 244 | [DataRow("test data\0", '\0')] 245 | public void StartsWithValue_CharDoesNot_ReturnsFalse(string? str, char value) 246 | { 247 | bool actual = str.StartsWithValue(value); 248 | Assert.IsFalse(actual, $"Expected '{str}' to not start with '{value}'"); 249 | } 250 | 251 | /// 252 | /// Assert that an input string starting with a specific substring returns true. 253 | /// 254 | [DataTestMethod] 255 | [DataRow("test data", "test ")] 256 | [DataRow(" test data ", " test d")] 257 | [DataRow("test data", "test data")] 258 | public void StartsWithValue_StringDoes_ReturnsTrue(string? str, string? value) 259 | { 260 | bool actual = str.StartsWithValue(value); 261 | Assert.IsTrue(actual, $"Expected '{str}' to start with '{value}'"); 262 | } 263 | 264 | /// 265 | /// Assert that an input string not starting with a specific substring returns false. 266 | /// 267 | [DataTestMethod] 268 | [DataRow(null, "test data")] 269 | [DataRow("test data", null)] 270 | [DataRow("test data", " test ")] 271 | public void StartsWithValue_StringDoesNot_ReturnsFalse(string? str, string? value) 272 | { 273 | bool actual = str.StartsWithValue(value); 274 | Assert.IsFalse(actual, $"Expected '{str}' to not start with '{value}'"); 275 | } 276 | 277 | /// 278 | /// Assert that an 279 | /// is thrown when the input array is null. 280 | /// 281 | [DataTestMethod] 282 | [DataRow("")] 283 | [DataRow("|")] 284 | public void Join_NullArray_ThrowsArgumentNullException(string delim) 285 | { 286 | string[]? array = null; 287 | Func func = () => array.Join(delim); 288 | 289 | Assert.ThrowsException(func); 290 | } 291 | 292 | /// 293 | /// Assert that the joined output string is equal to the expected result. 294 | /// 295 | [DataTestMethod] 296 | [DataRow(new object[] { 1, 2, 3 }, "|")] 297 | [DataRow(new string[] { "test", "data" }, null)] 298 | public void Join_NonNullArray_EqualsExpected(object[] array, string? delim) 299 | { 300 | string expected = string.Join(delim, array); 301 | string actual = array.Join(delim); 302 | 303 | Assert.AreEqual(expected, actual, $"Expected result string: '{expected}'"); 304 | } 305 | 306 | /// 307 | /// Assert that an 308 | /// is thrown when the input array is null. 309 | /// 310 | [TestMethod] 311 | public void JoinLines_NullArray_ThrowsArgumentNullException() 312 | { 313 | string[]? array = null; 314 | Assert.ThrowsException(array.JoinLines); 315 | } 316 | 317 | /// 318 | /// Assert that a populated input array returns the expected tuple enumerable. 319 | /// 320 | [DataTestMethod] 321 | [DataRow(0, 1, 2, 3)] 322 | [DataRow("test", "data")] 323 | [DataRow('t', 'e', 's', 't')] 324 | public void Enumerate_PopulatedArray_ReturnsExpected(params object[] array) 325 | { 326 | IEnumerable? values = array; 327 | 328 | (int, object)[] expected = [.. values.Select((v, i) => (i, v))]; 329 | (int, object)[] actual = [.. values.Enumerate()]; 330 | 331 | CollectionAssert.AreEquivalent(actual, expected, "Unexpected results"); 332 | } 333 | 334 | /// 335 | /// Assert that an empty input array returns an empty tuple enumerable. 336 | /// 337 | /// 338 | /// Array of type is used in place of 339 | /// for compatibility with 340 | /// assertions. 341 | /// 342 | [TestMethod] 343 | public void Enumerate_EmptyArray_ReturnsEmpty() 344 | { 345 | IEnumerable? values = []; 346 | (int, string)[] expected = []; 347 | 348 | IEnumerable<(int, string)> actualEnumerable = values.Enumerate(); 349 | (int, string)[] actual = [.. actualEnumerable]; 350 | 351 | CollectionAssert.AreEquivalent(actual, expected, "Unexpected results"); 352 | } 353 | 354 | /// 355 | /// Assert that a null input enumerable returns an empty tuple enumerable. 356 | /// 357 | /// 358 | /// Array of type is used in place of 359 | /// for compatibility with 360 | /// assertions. 361 | /// 362 | [TestMethod] 363 | public void Enumerate_NullEnumerable_ReturnsEmpty() 364 | { 365 | IEnumerable? values = null; 366 | 367 | (int, string)[] expected = []; 368 | (int, string)[] actual = [.. values.Enumerate()]; 369 | 370 | CollectionAssert.AreEquivalent(actual, expected, "Unexpected results"); 371 | } 372 | #endregion // MethodTests 373 | } 374 | -------------------------------------------------------------------------------- /src/DotnetCatTests/Utils/ParserTests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.VisualStudio.TestTools.UnitTesting; 2 | using DotnetCat.Utils; 3 | 4 | namespace DotnetCatTests.Utils; 5 | 6 | /// 7 | /// Unit tests for class . 8 | /// 9 | [TestClass] 10 | public class ParserTests 11 | { 12 | #region MethodTests 13 | /// 14 | /// Assert that an input command-line argument array containing a 15 | /// help flag or flag alias (-?, -h, --help) 16 | /// sets the property to true. 17 | /// 18 | [DataTestMethod] 19 | [DataRow("-?")] 20 | [DataRow("-h")] 21 | [DataRow("--help")] 22 | [DataRow("-d?", "--listen")] 23 | [DataRow("-hz", "--debug")] 24 | [DataRow("-v", "--help")] 25 | [DataRow("-vp", "22", "-?", "localhost")] 26 | [DataRow("--exec", "pwsh.exe", "-h", "localhost")] 27 | [DataRow("--listen", "--help", "localhost")] 28 | public void Parse_HelpFlag_HelpPropertyTrue(params string[] args) 29 | { 30 | Parser parser = new(); 31 | 32 | CmdLineArgs cmdArgs = parser.Parse(args); 33 | bool actual = cmdArgs.Help; 34 | 35 | Assert.IsTrue(actual, "Failed to parse help flag or flag alias"); 36 | } 37 | 38 | /// 39 | /// Assert that an input command-line argument array not containing any 40 | /// values sets the property to true. 41 | /// 42 | [TestMethod] 43 | public void Parse_NoArguments_HelpPropertyTrue() 44 | { 45 | string[] args = []; 46 | Parser parser = new(); 47 | 48 | CmdLineArgs cmdArgs = parser.Parse(args); 49 | bool actual = cmdArgs.Help; 50 | 51 | Assert.IsTrue(actual, "Help should be true when no arguments are provided"); 52 | } 53 | 54 | /// 55 | /// Assert that an input command-line argument array not containing 56 | /// a help flag or flag alias (-?, -h, --help) 57 | /// sets the property to false. 58 | /// 59 | [DataTestMethod] 60 | [DataRow("-vp", "22", "-e", "pwsh.exe", "192.168.1.100")] 61 | [DataRow("--verbose", "--send", "~/test.txt", "localhost")] 62 | [DataRow("--listen", "-vo", "~/recv_data.txt", "-p", "31337")] 63 | public void Parse_NoHelpFlag_HelpPropertyFalse(params string[] args) 64 | { 65 | Parser parser = new(); 66 | 67 | CmdLineArgs cmdArgs = parser.Parse(args); 68 | bool actual = cmdArgs.Help; 69 | 70 | Assert.IsFalse(actual, "Unexpectedly parsed help flag or flag alias"); 71 | } 72 | #endregion // MethodTests 73 | } 74 | -------------------------------------------------------------------------------- /src/DotnetCatTests/bin/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !**/.gitignore 3 | -------------------------------------------------------------------------------- /src/DotnetCatTests/obj/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !**/.gitignore 3 | -------------------------------------------------------------------------------- /tools/dncat-install.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | DotnetCat installer script for x64 and x86 Windows systems. 4 | .DESCRIPTION 5 | DotnetCat remote command shell application 6 | installer script for x64 and x86 Windows systems. 7 | .LINK 8 | Application repository: https://github.com/vandavey/DotnetCat 9 | #> 10 | using namespace System.Runtime.InteropServices 11 | using namespace System.Security.Principal 12 | 13 | [CmdletBinding()] 14 | param () 15 | 16 | $DefaultErrorPreference = $ErrorActionPreference 17 | $DefaultProgressPreference = $ProgressPreference 18 | 19 | # Reset the global preference variables. 20 | function Reset-Preferences { 21 | $ErrorActionPreference = $DefaultErrorPreference 22 | $ProgressPreference = $DefaultProgressPreference 23 | } 24 | 25 | # Write an error message to stderr and exit. 26 | function Show-Error { 27 | $Symbol = "[x]" 28 | Reset-Preferences 29 | 30 | if ($PSVersionTable.PSVersion.Major -ge 6) { 31 | $Symbol = "`e[91m${Symbol}`e[0m" 32 | } 33 | [Console]::Error.WriteLine("${Symbol} ${args}`n") 34 | exit 1 35 | } 36 | 37 | # Write a status message to stdout. 38 | function Show-Status { 39 | $Symbol = "[*]" 40 | 41 | if ($PSVersionTable.PSVersion.Major -ge 6) { 42 | $Symbol = "`e[96m${Symbol}`e[0m" 43 | } 44 | Write-Output "${Symbol} ${args}" 45 | } 46 | 47 | $ErrorActionPreference = "Stop" 48 | $ProgressPreference = "SilentlyContinue" 49 | 50 | # Require Windows operating system 51 | if (-not [RuntimeInformation]::IsOSPlatform([OSPlatform]::Windows)) { 52 | Show-Error "Windows operating system required" 53 | } 54 | 55 | $RepoRoot = "https://raw.githubusercontent.com/vandavey/DotnetCat/master" 56 | $UserPrincipal = [WindowsPrincipal]::new([WindowsIdentity]::GetCurrent()) 57 | 58 | # Require elevated shell privileges 59 | if (-not $UserPrincipal.IsInRole([WindowsBuiltInRole]::Administrator)) { 60 | Show-Error "Administrator shell privileges required" 61 | } 62 | 63 | # Validate CPU architecture and set variables 64 | if ($env:PROCESSOR_ARCHITECTURE -eq "AMD64") { 65 | $AppDir = "${env:ProgramFiles}\DotnetCat" 66 | $ZipUrl = "${RepoRoot}/src/DotnetCat/bin/Zips/DotnetCat_win-x64.zip" 67 | } 68 | elseif ($env:PROCESSOR_ARCHITECTURE -eq "x86") { 69 | $AppDir = "${env:ProgramFiles(x86)}\DotnetCat" 70 | $ZipUrl = "${RepoRoot}/src/DotnetCat/bin/Zips/DotnetCat_win-x86.zip" 71 | } 72 | else { 73 | Show-Error "Unsupported processor architecture: '${env:PROCESSOR_ARCHITECTURE}'" 74 | } 75 | 76 | # Remove existing installation 77 | if (Test-Path $AppDir) { 78 | Show-Status "Removing existing installation from '${AppDir}'..." 79 | Remove-Item $AppDir -Recurse -Force 80 | } 81 | 82 | Show-Status "Creating install directory '${AppDir}'..." 83 | New-Item $AppDir -ItemType Directory -Force > $null 84 | 85 | $ZipPath = "${AppDir}\dncat.zip" 86 | Show-Status "Downloading temporary zip file to '${ZipPath}'..." 87 | 88 | # Download application zip file 89 | try { 90 | Invoke-WebRequest $ZipUrl -DisableKeepAlive -OutFile $ZipPath 91 | } 92 | catch { 93 | $ErrorMsg = $Error[0].ErrorDetails.Message 94 | Show-Error "Failed to download '$(Split-Path $ZipUrl -Leaf)' (${ErrorMsg})" 95 | } 96 | 97 | Show-Status "Installing application files to '${AppDir}'..." 98 | Expand-Archive $ZipPath $AppDir -Force > $null 99 | 100 | Show-Status "Deleting temporary zip file '${ZipPath}'..." 101 | Remove-Item $ZipPath -Force 102 | 103 | $EnvTarget = [EnvironmentVariableTarget]::Machine 104 | $EnvPath = [Environment]::GetEnvironmentVariable("Path", $EnvTarget) 105 | 106 | # Add application directory to environment path 107 | if (-not $EnvPath.Contains($AppDir)) { 108 | if ($EnvPath -and -not $EnvPath.EndsWith(";")) { 109 | $EnvPath += ";" 110 | } 111 | 112 | $EnvPath += "${AppDir};" 113 | [Environment]::SetEnvironmentVariable("Path", $EnvPath, $EnvTarget) 114 | 115 | if ($?) { 116 | Show-Status "Added '${AppDir}' to environment path" 117 | } 118 | else { 119 | Show-Error "Failed to add '${AppDir}' to environment path" 120 | } 121 | } 122 | 123 | Reset-Preferences 124 | Show-Status "DotnetCat was successfully installed, please restart your shell`n" 125 | -------------------------------------------------------------------------------- /tools/dncat-install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # DESCRIPTION 4 | # DotnetCat installer script for ARM64 and x64 Linux systems. 5 | # REPOSITORY 6 | # https://github.com/vandavey/DotnetCat 7 | 8 | APP_DIR="/opt/dncat" 9 | BIN_DIR="${APP_DIR}/bin" 10 | SHARE_DIR="${APP_DIR}/share" 11 | 12 | # Write an error message to stderr and exit. 13 | error() { 14 | echo -e "\033[91m[x]\033[0m ${*}" >&2 15 | exit 1 16 | } 17 | 18 | # Write a status message to stdout. 19 | status() { 20 | echo -e "\033[96m[*]\033[0m ${*}" 21 | } 22 | 23 | # Add the bin directory environment path export to a file. 24 | add_bin_export() { 25 | local line_num 26 | local line="export PATH=\"\${PATH}:${BIN_DIR}\"" 27 | 28 | if [[ -f $1 ]] && ! grep -q "${line}" "${1}"; then 29 | echo "${line}" >> "${1}" 30 | line_num=$(wc -l "${1}" | cut -d " " -f 1) 31 | 32 | status "Added environment path export at '${1}':${line_num}" 33 | fi 34 | } 35 | 36 | # Move application files to a new directory. 37 | move_app_files() { 38 | local files=("${@:1:$#-1}") 39 | 40 | if ! sudo mv -v "${files[@]}" "${!#}"; then 41 | error "Failed to move application files to '${!#}'" 42 | fi 43 | } 44 | 45 | # Validate that an installer dependency is satisfied. 46 | validate_dep() { 47 | if ! command -v "${1}" &> /dev/null; then 48 | error "Unsatisfied installer dependency: '${1}'" 49 | fi 50 | } 51 | 52 | ARCH=$(uname -m) 53 | REPO_ROOT="https://raw.githubusercontent.com/vandavey/DotnetCat/master" 54 | 55 | # Validate CPU architecture and set variables 56 | if [[ $ARCH == "aarch64" ]]; then 57 | ZIP_URL="${REPO_ROOT}/src/DotnetCat/bin/Zips/DotnetCat_linux-arm64.zip" 58 | elif [[ $ARCH == "x86_64" ]]; then 59 | ZIP_URL="${REPO_ROOT}/src/DotnetCat/bin/Zips/DotnetCat_linux-x64.zip" 60 | else 61 | error "Unsupported processor architecture: '${ARCH}'" 62 | fi 63 | 64 | validate_dep 7z 65 | validate_dep curl 66 | 67 | # Require elevated shell privileges 68 | if ! sudo -n true 2> /dev/null; then 69 | status "Elevated shell privileges required..." 70 | 71 | if ! sudo -v; then 72 | error "Failed to elevate shell privileges" 73 | fi 74 | fi 75 | 76 | # Remove existing installation 77 | if [[ -d $APP_DIR ]]; then 78 | status "Removing existing installation..." 79 | 80 | if ! sudo rm -rv $APP_DIR; then 81 | error "Failed to remove existing installation from '${APP_DIR}'" 82 | fi 83 | fi 84 | 85 | status "Creating install directories..." 86 | 87 | # Create install directories 88 | if ! sudo mkdir -pv $BIN_DIR $SHARE_DIR; then 89 | error "Failed to create one or more directories in '${APP_DIR}'" 90 | fi 91 | 92 | ZIP_PATH="${APP_DIR}/dncat.zip" 93 | HTTP_STATUS=$(curl -sILSw "%{http_code}" $ZIP_URL -o /dev/null) 94 | 95 | # Failed to access download URL 96 | if [[ $HTTP_STATUS -ne 200 ]]; then 97 | error "Unable to download zip file: HTTP ${HTTP_STATUS}" 98 | fi 99 | 100 | status "Downloading temporary zip file to '${ZIP_PATH}'..." 101 | 102 | # Download application zip file 103 | if ! sudo curl -sLS $ZIP_URL -o $ZIP_PATH; then 104 | error "Failed to download zip file from '${ZIP_URL}'" 105 | fi 106 | 107 | status "Unpacking zip file to '${APP_DIR}'..." 108 | 109 | # Unpack application zip file 110 | if ! sudo 7z x "${ZIP_PATH}" -bb0 -bd -o"${APP_DIR}" > /dev/null; then 111 | error "Failed to unpack zip file '${ZIP_PATH}'" 112 | fi 113 | 114 | status "Deleting temporary zip file..." 115 | 116 | # Remove application zip file 117 | if ! sudo rm -v $ZIP_PATH; then 118 | error "Failed to remove zip file '${ZIP_PATH}'" 119 | fi 120 | 121 | status "Installing application files to '${APP_DIR}'..." 122 | 123 | move_app_files $APP_DIR/*.md $SHARE_DIR 124 | move_app_files $APP_DIR/{dncat,*.sh} $BIN_DIR 125 | 126 | status "Enabling execution of files in '${BIN_DIR}'..." 127 | 128 | # Enable execute permissions 129 | if ! sudo chmod +x $BIN_DIR/{dncat,*.sh}; then 130 | error "Failed to enable execution of files in '${BIN_DIR}'" 131 | fi 132 | 133 | # Create bash configuration file 134 | if [[ ! -f ~/.bashrc && ! -f ~/.zshrc ]]; then 135 | status "Creating bash configuration file '${HOME}/.bashrc'..." 136 | : >> ~/.bashrc 137 | fi 138 | 139 | add_bin_export ~/.bashrc 140 | add_bin_export ~/.zshrc 141 | 142 | status "DotnetCat was successfully installed, please restart your shell" 143 | -------------------------------------------------------------------------------- /tools/dncat-uninstall.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | DotnetCat uninstaller script for x64 and x86 Windows systems. 4 | .DESCRIPTION 5 | DotnetCat remote command shell application 6 | uninstaller script for x64 and x86 Windows systems. 7 | .LINK 8 | Application repository: https://github.com/vandavey/DotnetCat 9 | #> 10 | using namespace System.Runtime.InteropServices 11 | using namespace System.Security.Principal 12 | 13 | [CmdletBinding()] 14 | param () 15 | 16 | $DefaultErrorPreference = $ErrorActionPreference 17 | $DefaultProgressPreference = $ProgressPreference 18 | 19 | # Reset the global preference variables. 20 | function Reset-Preferences { 21 | $ErrorActionPreference = $DefaultErrorPreference 22 | $ProgressPreference = $DefaultProgressPreference 23 | } 24 | 25 | # Write an error message to stderr and exit. 26 | function Show-Error { 27 | $Symbol = "[x]" 28 | Reset-Preferences 29 | 30 | if ($PSVersionTable.PSVersion.Major -ge 6) { 31 | $Symbol = "`e[91m${Symbol}`e[0m" 32 | } 33 | [Console]::Error.WriteLine("${Symbol} ${args}`n") 34 | exit 1 35 | } 36 | 37 | # Write a status message to stdout. 38 | function Show-Status { 39 | $Symbol = "[*]" 40 | 41 | if ($PSVersionTable.PSVersion.Major -ge 6) { 42 | $Symbol = "`e[96m${Symbol}`e[0m" 43 | } 44 | Write-Output "${Symbol} ${args}" 45 | } 46 | 47 | $ErrorActionPreference = "Stop" 48 | $ProgressPreference = "SilentlyContinue" 49 | 50 | # Require Windows operating system 51 | if (-not [RuntimeInformation]::IsOSPlatform([OSPlatform]::Windows)) { 52 | Show-Error "Windows operating system required" 53 | } 54 | 55 | $UserPrincipal = [WindowsPrincipal]::new([WindowsIdentity]::GetCurrent()) 56 | 57 | # Require elevated shell privileges 58 | if (-not $UserPrincipal.IsInRole([WindowsBuiltInRole]::Administrator)) { 59 | Show-Error "Administrator shell privileges required" 60 | } 61 | 62 | # Validate CPU architecture and set variables 63 | if ($env:PROCESSOR_ARCHITECTURE -eq "AMD64") { 64 | $AppDir = "${env:ProgramFiles}\DotnetCat" 65 | } 66 | elseif ($env:PROCESSOR_ARCHITECTURE -eq "x86") { 67 | $AppDir = "${env:ProgramFiles(x86)}\DotnetCat" 68 | } 69 | else { 70 | Show-Error "Unsupported processor architecture: '${env:PROCESSOR_ARCHITECTURE}'" 71 | } 72 | 73 | # Remove all application files 74 | if (Test-Path $AppDir) { 75 | Show-Status "Removing application files from '${AppDir}'..." 76 | Remove-Item $AppDir -Recurse -Force 77 | } 78 | else { 79 | Show-Status "No application files to remove from '${AppDir}'" 80 | } 81 | 82 | $EnvTarget = [EnvironmentVariableTarget]::Machine 83 | $EnvPath = [Environment]::GetEnvironmentVariable("Path", $EnvTarget) 84 | 85 | # Remove application directory from environment path 86 | if ($EnvPath.Contains($AppDir)) { 87 | if ($EnvPath -eq $AppDir -or $EnvPath -eq "${AppDir};") { 88 | $EnvPath = [string]::Empty 89 | } 90 | elseif ($EnvPath.StartsWith($AppDir)) { 91 | $EnvPath = $EnvPath.Replace("${AppDir};", $null) 92 | } 93 | else { 94 | $EnvPath = $EnvPath.Replace(";${AppDir}", $null) 95 | } 96 | 97 | [Environment]::SetEnvironmentVariable("Path", $EnvPath, $EnvTarget) 98 | 99 | if ($?) { 100 | Show-Status "Removed '${AppDir}' from environment path" 101 | } 102 | else { 103 | Show-Error "Failed to remove '${AppDir}' from environment path" 104 | } 105 | } 106 | 107 | Reset-Preferences 108 | Show-Status "DotnetCat was successfully uninstalled`n" 109 | -------------------------------------------------------------------------------- /tools/dncat-uninstall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # DESCRIPTION 4 | # DotnetCat uninstaller script for ARM64 and x64 Linux systems. 5 | # REPOSITORY 6 | # https://github.com/vandavey/DotnetCat 7 | 8 | APP_DIR="/opt/dncat" 9 | BIN_DIR="${APP_DIR}/bin" 10 | 11 | # Write an error message to stderr and exit. 12 | error() { 13 | echo -e "\033[91m[x]\033[0m ${*}" >&2 14 | exit 1 15 | } 16 | 17 | # Write a status message to stdout. 18 | status() { 19 | echo -e "\033[96m[*]\033[0m ${*}" 20 | } 21 | 22 | # Remove the bin directory environment path export from a file. 23 | remove_bin_export() { 24 | local line_num 25 | local line="export PATH=\"\${PATH}:${BIN_DIR}\"" 26 | 27 | if [[ -f $1 ]] && grep -q "${line}" "${1}"; then 28 | line_num=$(cat -n "${1}" | grep "${line}" | awk '{print $1}') 29 | 30 | if [[ -n $line_num ]]; then 31 | if ! sed -i "${line_num}d" "${1}"; then 32 | error "Failed to remove path export at '${1}':${line_num}" 33 | fi 34 | status "Removed environment path export at '${1}':${line_num}" 35 | fi 36 | fi 37 | } 38 | 39 | ARCH=$(uname -m) 40 | 41 | # Validate CPU architecture 42 | if [[ ! $ARCH =~ ^(aarch64|x86_64)$ ]]; then 43 | error "Unsupported processor architecture: '${ARCH}'" 44 | fi 45 | 46 | # Require elevated shell privileges 47 | if ! sudo -n true 2> /dev/null; then 48 | status "Elevated shell privileges required..." 49 | 50 | if ! sudo -v; then 51 | error "Failed to elevate shell privileges" 52 | fi 53 | fi 54 | 55 | # Remove all application files 56 | if [[ -d $APP_DIR ]]; then 57 | status "Removing application files from '${APP_DIR}'..." 58 | 59 | if ! sudo rm -rv $APP_DIR; then 60 | error "Failed to remove application files from '${APP_DIR}'" 61 | fi 62 | else 63 | status "No application files to remove from '${APP_DIR}'" 64 | fi 65 | 66 | remove_bin_export ~/.bashrc 67 | remove_bin_export ~/.zshrc 68 | 69 | status "DotnetCat was successfully uninstalled" 70 | --------------------------------------------------------------------------------