Compare commits

...

102 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
9ba3cf0727 Add keyboard shortcuts documentation for session navigation
Co-authored-by: Kvarkas <3611964+Kvarkas@users.noreply.github.com>
2026-02-13 10:37:15 +00:00
copilot-swe-agent[bot]
71911bba7b Add Sessions menu with keyboard shortcuts for tab navigation
Co-authored-by: Kvarkas <3611964+Kvarkas@users.noreply.github.com>
2026-02-13 10:36:32 +00:00
copilot-swe-agent[bot]
e9f94cbe31 Initial plan 2026-02-13 10:32:08 +00:00
Dimitrij
db1496d4a2 Merge pull request #3140 from mRemoteNG/renovate/actions-setup-dotnet-5.x
Update actions/setup-dotnet action to v5
2026-02-13 10:29:59 +00:00
renovate[bot]
a8b12c9ba1 Update actions/setup-dotnet action to v5 2026-02-12 23:12:53 +00:00
Dimitrij
a2b408e537 Update Build_mR-NB.yml
add .net 10 install
2026-02-12 23:12:17 +00:00
Dimitrij
a871624ab7 Update MSBuild version range in workflow
set ver for lower
2026-02-12 23:03:10 +00:00
Dimitrij
e40a800bc4 Update Build_mR-NB.yml
fix build tools ver
2026-02-12 22:58:46 +00:00
Dimitrij
48cb1ce770 Merge pull request #3139 from mRemoteNG/renovate/aws-sdk-net-monorepo
Update dependency AWSSDK.EC2 to 4.0.74
2026-02-12 22:56:04 +00:00
Kvarkas
6733d758aa upd action 2026-02-12 22:55:25 +00:00
Dimitrij
f78b9bf51c Refactor self-contained build process in workflow
fix msbuild
2026-02-12 21:53:24 +00:00
renovate[bot]
213ea6a4d3 Update dependency AWSSDK.EC2 to 4.0.74 2026-02-12 21:17:33 +00:00
Kvarkas
ac3d7e6366 added new option - build Self-Contained, this allow to run mR without installing .NET 2026-02-12 21:16:31 +00:00
Dimitrij
9fae2e066e Merge pull request #3138 from mRemoteNG/renovate/aws-sdk-net-monorepo
Update aws-sdk-net monorepo
2026-02-12 10:30:06 +00:00
renovate[bot]
cf66e84d31 Update aws-sdk-net monorepo 2026-02-11 22:12:21 +00:00
Dimitrij
f7326aff62 Merge pull request #3137 from mRemoteNG/renovate/aws-sdk-net-monorepo
Update dependency AWSSDK.Core to 4.0.3.13
2026-02-11 09:22:09 +00:00
Kvarkas
d4bca6b03d small fixes 2026-02-10 22:03:02 +00:00
renovate[bot]
1d86015f9d Update dependency AWSSDK.Core to 4.0.3.13 2026-02-10 21:19:54 +00:00
Dimitrij
5efcc653eb Merge pull request #3038 from mRemoteNG/copilot/fix-command-injection-vulnerability
Fix command injection vulnerabilities in Process.Start calls
2026-02-10 21:18:59 +00:00
Kvarkas
333588e101 Merge branch 'v1.78.2-dev' of https://github.com/mRemoteNG/mRemoteNG into v1.78.2-dev 2026-02-10 21:18:29 +00:00
Kvarkas
4046681fc5 remove dependency what are now included with .Net 10
small fixes
2026-02-10 21:18:17 +00:00
Dimitrij
91c7df22b2 Update mRemoteNGTests/Connection/Protocol/ProtocolAnydeskTests.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-10 21:10:35 +00:00
Dimitrij
173b208eb1 Update mRemoteNG/UI/Forms/FrmAbout.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-10 21:10:12 +00:00
Dimitrij
50de37c3a4 Update mRemoteNG/UI/Forms/OptionsPages/NotificationsPage.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-10 21:09:56 +00:00
Dimitrij
380e91de07 Update mRemoteNG/Connection/Protocol/AnyDesk/ProtocolAnyDesk.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-10 21:09:06 +00:00
Dimitrij
ba97933f33 Update mRemoteNG/UI/Forms/OptionsPages/NotificationsPage.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-10 21:08:46 +00:00
Dimitrij
539b761199 Merge pull request #3136 from mRemoteNG/renovate/major-github-artifact-actions
Update GitHub Artifact Actions (major)
2026-02-10 19:58:22 +00:00
renovate[bot]
aaff6e4548 Update GitHub Artifact Actions 2026-02-10 18:31:58 +00:00
Kvarkas
a1e3b34580 do release 2026-02-10 18:30:52 +00:00
Kvarkas
276585e379 upgrade so release include self contained version 2026-02-10 18:27:03 +00:00
Dimitrij
9242dc0faf Merge pull request #3135 from mRemoteNG/renovate/dotnet-monorepo
Update dotnet monorepo to 10.0.3
2026-02-10 18:13:22 +00:00
renovate[bot]
14d08d8d62 Update dotnet monorepo to 10.0.3 2026-02-10 17:56:13 +00:00
Dimitrij
421d8eb581 Merge pull request #3132 from mRemoteNG/renovate/aws-sdk-net-monorepo
Update dependency AWSSDK.EC2 to 4.0.72
2026-02-10 11:37:36 +00:00
renovate[bot]
aed0006f1d Update dependency AWSSDK.EC2 to 4.0.72 2026-02-10 01:15:15 +00:00
Dimitrij
86d986a633 Merge pull request #3101 from mRemoteNG/copilot/fix-new-connection-error
Show error popup when connecting without hostname/IP
2026-02-01 20:18:51 +00:00
Dimitrij
5163aeb4d2 Change error message to warning for blank hostname 2026-02-01 20:18:11 +00:00
copilot-swe-agent[bot]
a103939c64 Address code review: fix typos, improve test robustness, use Language resources
Co-authored-by: Kvarkas <3611964+Kvarkas@users.noreply.github.com>
2026-02-01 19:59:39 +00:00
copilot-swe-agent[bot]
bcb8e05698 Update documentation for new popup error default setting
Co-authored-by: Kvarkas <3611964+Kvarkas@users.noreply.github.com>
2026-02-01 19:58:03 +00:00
copilot-swe-agent[bot]
2e74313f07 Change hostname validation to ErrorMsg and enable popup errors by default
Co-authored-by: Kvarkas <3611964+Kvarkas@users.noreply.github.com>
2026-02-01 19:56:53 +00:00
copilot-swe-agent[bot]
c9791454ec Initial plan 2026-02-01 19:52:25 +00:00
Dimitrij
b1c1696acb Merge pull request #3099 from jafin/fix/frmInputBox-autosize
Fix FrmInputBox to use dynamic sizing instead of fixed dimensions
2026-02-01 19:24:31 +00:00
Dimitrij
ab668ac677 Merge pull request #3097 from mRemoteNG/renovate/chromiumembeddedframework.runtime.win-x64-144.x
Update dependency chromiumembeddedframework.runtime.win-x64 to v144
2026-02-01 19:23:58 +00:00
Dimitrij
1777c4840a Merge pull request #3096 from mRemoteNG/renovate/chromiumembeddedframework.runtime.win-arm64-144.x
Update dependency chromiumembeddedframework.runtime.win-arm64 to v144
2026-02-01 19:23:44 +00:00
Dimitrij
90fcd672d8 Merge pull request #3095 from mRemoteNG/renovate/protobuf-monorepo
Update dependency Google.Protobuf to 3.33.5
2026-02-01 19:23:27 +00:00
Dimitrij
4226396cbf Merge pull request #3094 from mRemoteNG/renovate/microsoft.web.webview2-1.x
Update dependency Microsoft.Web.WebView2 to 1.0.3719.77
2026-02-01 19:23:11 +00:00
Dimitrij
3591ca0f4c Merge pull request #3093 from mRemoteNG/renovate/aws-sdk-net-monorepo
Update aws-sdk-net monorepo
2026-02-01 19:22:55 +00:00
Jason Finch
09cbcccf30 Fix FrmInputBox to use dynamic sizing instead of fixed dimensions 2026-02-01 13:32:00 +10:00
renovate[bot]
4e36b5666e Update dependency chromiumembeddedframework.runtime.win-x64 to v144 2026-01-31 05:48:20 +00:00
renovate[bot]
308253a325 Update dependency chromiumembeddedframework.runtime.win-arm64 to v144 2026-01-31 05:48:13 +00:00
renovate[bot]
30bb4016b4 Update dependency Google.Protobuf to 3.33.5 2026-01-30 00:59:36 +00:00
renovate[bot]
e616ae16e1 Update aws-sdk-net monorepo 2026-01-30 00:59:30 +00:00
renovate[bot]
469528a07a Update dependency Microsoft.Web.WebView2 to 1.0.3719.77 2026-01-27 12:45:38 +00:00
Dimitrij
c7f831e9f9 Merge pull request #3089 from mRemoteNG/renovate/aws-sdk-net-monorepo
Update dependency AWSSDK.EC2 to 4.0.67
2026-01-26 12:51:28 +00:00
Dimitrij
dd9922be45 Merge pull request #3087 from mRemoteNG/renovate/mysql.data-9.x
Update dependency MySql.Data to 9.6.0
2026-01-26 12:51:11 +00:00
Dimitrij
d0a468c22b Merge pull request #3090 from mRemoteNG/renovate/cucumber.messages-32.x
Update dependency Cucumber.Messages to v32
2026-01-26 12:50:56 +00:00
Dimitrij
ff5dbc88fe Merge pull request #3091 from mRemoteNG/renovate/gherkin-38.x
Update dependency Gherkin to v38
2026-01-26 12:50:42 +00:00
renovate[bot]
3a4ae9b098 Update dependency Cucumber.Messages to v32 2026-01-25 13:44:37 +00:00
renovate[bot]
2de24e534c Update dependency AWSSDK.EC2 to 4.0.67 2026-01-22 21:05:19 +00:00
renovate[bot]
59412a65e1 Update dependency Gherkin to v38 2026-01-22 02:30:57 +00:00
renovate[bot]
adedb6962f Update dependency MySql.Data to 9.6.0 2026-01-21 10:47:43 +00:00
Dimitrij
d3fa608ae9 Merge pull request #3086 from mRemoteNG/renovate/aws-sdk-net-monorepo
Update aws-sdk-net monorepo
2026-01-21 10:47:03 +00:00
renovate[bot]
3159903875 Update aws-sdk-net monorepo 2026-01-21 01:59:43 +00:00
Dimitrij
31c28c4917 Merge pull request #3084 from mRemoteNG/renovate/microsoft.data.sqlclient-6.x
Update dependency Microsoft.Data.SqlClient to 6.1.4
2026-01-16 09:59:33 +00:00
Dimitrij
2c13f7c3a7 Merge pull request #3085 from mRemoteNG/renovate/aws-sdk-net-monorepo
Update aws-sdk-net monorepo
2026-01-16 09:59:18 +00:00
renovate[bot]
725ee92147 Update aws-sdk-net monorepo 2026-01-16 01:27:24 +00:00
renovate[bot]
14406f79a2 Update dependency Microsoft.Data.SqlClient to 6.1.4 2026-01-15 18:42:41 +00:00
Kvarkas
3e202c3a19 lib update 2026-01-14 10:26:47 +00:00
Dimitrij
412c727e4c Merge pull request #3081 from mRemoteNG/renovate/dotnet-monorepo
Update dotnet monorepo to 10.0.2
2026-01-13 19:19:31 +00:00
renovate[bot]
31e7b9e443 Update dotnet monorepo to 10.0.2 2026-01-13 18:40:14 +00:00
Dimitrij
a18e292765 Merge pull request #3078 from mRemoteNG/renovate/protobuf-monorepo
Update dependency Google.Protobuf to 3.33.4
2026-01-13 11:23:46 +00:00
Dimitrij
258ea87f90 Merge pull request #3079 from mRemoteNG/renovate/cucumber.messages-31.x
Update dependency Cucumber.Messages to 31.2.0
2026-01-13 10:38:07 +00:00
renovate[bot]
fd9eabe1e6 Update dependency Google.Protobuf to 3.33.4 2026-01-12 21:55:32 +00:00
renovate[bot]
a9dd06df45 Update dependency Cucumber.Messages to 31.2.0 2026-01-12 00:24:29 +00:00
Dimitrij
dfc24b0cb2 Merge pull request #3076 from mRemoteNG/renovate/nunit3testadapter-6.x
Update dependency NUnit3TestAdapter to 6.1.0
2026-01-08 09:46:33 +00:00
Dimitrij
63f5325f29 Merge pull request #3075 from mRemoteNG/renovate/aws-sdk-net-monorepo
Update aws-sdk-net monorepo
2026-01-08 09:46:04 +00:00
renovate[bot]
6f7949214b Update dependency NUnit3TestAdapter to 6.1.0 2026-01-07 21:23:22 +00:00
renovate[bot]
6cfec060a0 Update aws-sdk-net monorepo 2026-01-07 21:23:16 +00:00
Dimitrij
db733424ca Merge pull request #3074 from mRemoteNG/renovate/aws-sdk-net-monorepo
Update dependency AWSSDK.EC2 to 4.0.65.1
2026-01-06 10:49:59 +00:00
renovate[bot]
49eab4d377 Update dependency AWSSDK.EC2 to 4.0.65.1 2026-01-06 01:42:04 +00:00
Dimitrij
53e5396031 Merge pull request #3073 from mRemoteNG/renovate/aws-sdk-net-monorepo
Update dependency AWSSDK.Core to 4.0.3.7
2026-01-05 21:17:14 +00:00
renovate[bot]
99d01130bf Update dependency AWSSDK.Core to 4.0.3.7 2026-01-05 21:04:09 +00:00
Dimitrij
62ce6cd6e7 Merge pull request #3071 from mRemoteNG/renovate/nunit.consolerunner-3.x
Update dependency NUnit.ConsoleRunner to 3.22.0
2026-01-04 17:53:30 +00:00
renovate[bot]
35c66b0e4a Update dependency NUnit.ConsoleRunner to 3.22.0 2026-01-04 17:53:20 +00:00
Dimitrij
dafc05dc42 Merge pull request #3072 from mRemoteNG/renovate/zstdsharp.port-0.x
Update dependency ZstdSharp.Port to 0.8.7
2026-01-04 17:53:13 +00:00
Dimitrij
458a05ea5f Merge pull request #3070 from mRemoteNG/renovate/nunit.console-3.x
Update dependency NUnit.Console to 3.22.0
2026-01-04 17:52:43 +00:00
renovate[bot]
38acc1e960 Update dependency ZstdSharp.Port to 0.8.7 2026-01-03 17:58:37 +00:00
renovate[bot]
f83209a2b9 Update dependency NUnit.Console to 3.22.0 2026-01-03 01:59:37 +00:00
Dimitrij
9d1546c8b7 Merge pull request #3068 from mRemoteNG/copilot/fix-sql-injection-issue
Fix WQL injection vulnerability in InstalledWindowsUpdateChecker
2025-12-30 12:30:53 +00:00
copilot-swe-agent[bot]
dca2517cf0 Normalize digit-only KB IDs to include KB prefix
Co-authored-by: Kvarkas <3611964+Kvarkas@users.noreply.github.com>
2025-12-30 12:19:35 +00:00
copilot-swe-agent[bot]
ff54ca9015 Fix inaccurate comment in SanitizeKbId method
Co-authored-by: Kvarkas <3611964+Kvarkas@users.noreply.github.com>
2025-12-30 12:17:52 +00:00
copilot-swe-agent[bot]
76cb0a1e0b Address code review feedback - optimize regex and use Array.Empty
Co-authored-by: Kvarkas <3611964+Kvarkas@users.noreply.github.com>
2025-12-30 12:16:52 +00:00
copilot-swe-agent[bot]
c683854678 Add WQL injection prevention via KB ID sanitization
Co-authored-by: Kvarkas <3611964+Kvarkas@users.noreply.github.com>
2025-12-30 12:15:31 +00:00
copilot-swe-agent[bot]
e9d0a8aa69 Initial plan 2025-12-30 12:11:00 +00:00
Dimitrij
30cb1de711 Merge pull request #3064 from mRemoteNG/copilot/fix-ldap-query-injection
Fix LDAP injection vulnerability in Active Directory integration
2025-12-30 12:06:35 +00:00
Dimitrij
699b93e175 Merge pull request #3066 from mRemoteNG/copilot/setup-copilot-instructions
Add GitHub Copilot instructions for repository
2025-12-30 12:05:30 +00:00
copilot-swe-agent[bot]
c7df6f3715 Add comprehensive GitHub Copilot instructions
Co-authored-by: Kvarkas <3611964+Kvarkas@users.noreply.github.com>
2025-12-29 13:17:42 +00:00
copilot-swe-agent[bot]
f1d1a19779 Initial plan 2025-12-29 13:13:24 +00:00
copilot-swe-agent[bot]
469ca48592 Fix compilation issues and improve code review feedback
Co-authored-by: Kvarkas <3611964+Kvarkas@users.noreply.github.com>
2025-12-08 13:12:15 +00:00
copilot-swe-agent[bot]
208ce663b2 Address code review feedback - improve security validations
Co-authored-by: Kvarkas <3611964+Kvarkas@users.noreply.github.com>
2025-12-08 13:08:50 +00:00
copilot-swe-agent[bot]
843243c75e Add comprehensive tests for AnyDesk ID validation
Co-authored-by: Kvarkas <3611964+Kvarkas@users.noreply.github.com>
2025-12-08 13:06:16 +00:00
copilot-swe-agent[bot]
b7ed5a300d Fix command injection vulnerabilities in Process.Start calls
Co-authored-by: Kvarkas <3611964+Kvarkas@users.noreply.github.com>
2025-12-08 13:03:54 +00:00
copilot-swe-agent[bot]
415a649a76 Initial plan 2025-12-08 12:55:04 +00:00
40 changed files with 1711 additions and 251 deletions

242
.github/copilot-instructions.md vendored Normal file
View File

@@ -0,0 +1,242 @@
# GitHub Copilot Instructions for mRemoteNG
## Project Overview
mRemoteNG is an open-source, multi-protocol, tabbed remote connections manager for Windows. It's a fork of mRemote that allows users to view and manage all their remote connections (RDP, VNC, SSH, Telnet, HTTP/HTTPS, rlogin, Raw Socket, PowerShell remoting, AnyDesk) in a simple yet powerful interface.
## Technology Stack
- **Language**: C# with latest language version
- **Framework**: .NET 10.0 for Windows (net10.0-windows10.0.26100.0)
- **UI Framework**: Windows Forms with WPF support
- **Target Platforms**: x64 and ARM64
- **Testing Framework**: NUnit with NSubstitute for mocking
- **Build System**: MSBuild with .NET SDK-style projects
## Building the Project
### Prerequisites
- Visual Studio 2022 (version 17.14.12 or later)
- .NET 10.0 Desktop Runtime
- Windows 10/11 or Windows Server 2016+
### Build Commands
```powershell
# Restore NuGet packages
dotnet restore
# Build the solution
msbuild mRemoteNG.sln -p:Configuration=Release -p:Platform=x64
# Or for ARM64
msbuild mRemoteNG.sln -p:Configuration=Release -p:Platform=arm64
```
### Running Tests
The project uses NUnit for testing. Test projects are in `mRemoteNGTests/` directory.
```powershell
dotnet test mRemoteNGTests/mRemoteNGTests.csproj
```
## Code Organization
### Main Projects
- **mRemoteNG**: Main application project
- **mRemoteNGTests**: Unit and integration tests
- **mRemoteNGSpecs**: Specification tests
- **ObjectListView.NetCore**: Custom list view control
- **ExternalConnectors**: External protocol connector implementations
- **mRemoteNGDocumentation**: reStructuredText documentation
### Key Directories in mRemoteNG Project
- `App/`: Application startup and initialization
- `Connection/`: Connection models, protocols, and management
- `Config/`: Configuration and serialization (XML, CSV)
- `Container/`: Container/folder node implementations
- `Credential/`: Credential storage and repositories
- `Language/`: Localization resource files (.resx)
- `Security/`: Authentication, encryption, and password management
- `Tree/`: Connection tree UI and management
- `UI/`: User interface components and forms
- `Tools/`: Utility classes and helpers
## Code Style and Conventions
### EditorConfig Settings
The project uses EditorConfig (`.editorconfig` in `mRemoteNG/` directory) with these key rules:
- **Indentation**: 4 spaces (no tabs)
- **Line Endings**: CRLF (Windows-style)
- **Charset**: UTF-8 with BOM for C# files
- **New Lines**: Opening braces on new lines (Allman style)
- **Using Directives**: Sort System directives first
- **this. Qualifier**: Do not use `this.` prefix unless necessary
### Naming Conventions
- **Classes/Interfaces**: PascalCase (e.g., `ConnectionInfo`, `IInheritable`)
- **Methods**: PascalCase (e.g., `GetConnection`, `SaveToXml`)
- **Properties**: PascalCase (e.g., `ConnectionName`, `Port`)
- **Fields**: Use camelCase for private fields, consider `_` prefix for backing fields
- **Constants**: PascalCase
### Code Patterns
#### Inheritance System
mRemoteNG has a sophisticated property inheritance system for connection settings:
1. Properties can be inherited from parent containers/folders
2. Each inheritable property has a corresponding `Inherit<PropertyName>` boolean in `ConnectionInfoInheritance` class
3. Use `IInheritable` interface for objects that support inheritance
4. Example: `ConnectionFrameColor` property has `InheritConnectionFrameColor` boolean
#### Serialization
The project supports multiple serialization formats:
**XML Serialization** (`Config/Serializers/ConnectionSerializers/Xml/`):
- Node-based serializers (e.g., `XmlConnectionNodeSerializer28.cs`)
- Deserializers handle backward compatibility
- Attributes for properties and inheritance flags
- Always maintain backward compatibility - old files must still load
**CSV Serialization** (`Config/Serializers/ConnectionSerializers/Csv/`):
- Export connections to CSV format
- Include both value and inheritance columns
- Maintain column order consistency
#### Localization
All user-facing strings must be localized:
1. Add entries to `Language/Language.resx` (English base)
2. Use `Language.ResourceName` to access strings in code
3. Resource naming:
- Properties: `PropertyName` (e.g., `ConnectionFrameColor`)
- Descriptions: `PropertyDescription<PropertyName>`
- Enum values: `<EnumName><Value>` (e.g., `FrameColorRed`)
4. Provide clear, concise descriptions for PropertyGrid tooltips
### Common Patterns
#### Adding a New Connection Property
1. **Add enum** (if needed) in `Connection/` directory
2. **Add property** to `AbstractConnectionRecord.cs` or `ConnectionInfo.cs`
- Add `[Category("CategoryName")]` attribute for PropertyGrid grouping
- Add `[Description("Language.PropertyDescription<Name>")]` for tooltip
- Use appropriate TypeConverter if needed
3. **Add inheritance support** in `ConnectionInfoInheritance.cs`
4. **Update serializers**:
- XML: Update latest `XmlConnectionNodeSerializer*.cs` and `XmlConnectionsDeserializer.cs`
- CSV: Update `CsvConnectionsSerializerMremotengFormat.cs`
5. **Add localization** in `Language/Language.resx`
6. **Write tests** in `mRemoteNGTests/Connection/`
7. **Update documentation** in `mRemoteNGDocumentation/` if user-facing
#### UI Controls
- Prefer existing mRemoteNG patterns for UI controls
- Connection panels use `InterfaceControl` base class
- Custom painting: Override `OnPaint` or handle `Paint` event
- Follow Windows Forms best practices
## Testing Guidelines
### Test Structure
- Use NUnit's `[Test]` attribute for test methods
- Use `[SetUp]` and `[TearDown]` for test initialization/cleanup
- Use `[TestCase]` for parameterized tests
- Use NSubstitute for mocking: `Substitute.For<IInterface>()`
### Test Naming
- Use descriptive names: `MethodName_Scenario_ExpectedBehavior`
- Example: `ConnectionInfo_SetPassword_EncryptsValue`
### Test Organization
Mirror the main project structure in `mRemoteNGTests/`:
- `Connection/` for connection tests
- `Security/` for security tests
- `Config/` for configuration/serialization tests
## Important Notes and Pitfalls
### Backward Compatibility
- **Critical**: Never break loading of old connection files
- Always provide default values for new properties
- Test that files without new attributes still load correctly
### Security
- Sensitive data (passwords, credentials) must be encrypted
- Use existing security providers in `Security/` namespace
- Never log or expose credentials in plain text
### Performance
- Connection tree can contain thousands of nodes
- Optimize for large connection files
- Avoid unnecessary UI refreshes
- Use async/await for I/O operations
### Platform-Specific Code
- The project targets both x64 and ARM64
- Avoid platform-specific code unless absolutely necessary
- Test on both platforms when possible
### External Dependencies
- PuTTY for SSH/Telnet (bundled)
- Terminal Service Client for RDP
- Various protocol-specific libraries (see CREDITS.md)
## Documentation
### User Documentation
- Located in `mRemoteNGDocumentation/`
- Written in reStructuredText (.rst)
- Follows ReadTheDocs format
- Include screenshots and examples for new features
### Code Documentation
- Use XML documentation comments for public APIs
- Document complex algorithms and business logic
- Keep implementation notes in `IMPLEMENTATION_NOTES.md` for significant features
## Common Tasks
### Adding a Protocol
1. Create protocol implementation in `Connection/Protocol/`
2. Add protocol enum value to protocol types
3. Implement connection logic
4. Add UI controls if needed
5. Update documentation
### Adding a Theme
1. Add theme files to `Themes/` directory
2. Update theme manager
3. Add documentation in `mRemoteNGDocumentation/themes/`
### Updating Dependencies
- Dependencies are centrally managed in `Directory.Packages.props`
- Use `dotnet restore` after updating
- Test thoroughly after dependency updates
## Version and Release
- Current development version: 1.78.2-dev
- Version is set in `mRemoteNG.csproj`
- Builds are automated via GitHub Actions (`.github/workflows/`)
- Changelog maintained in `CHANGELOG.md`
## Getting Help
- Project website: https://mremoteng.org
- Documentation: https://mremoteng.readthedocs.io
- GitHub Issues: Report bugs and feature requests
- Community: Reddit r/mRemoteNG, Matrix chat
## Summary
When contributing to mRemoteNG:
1. Follow the existing code style (EditorConfig)
2. Maintain backward compatibility with old connection files
3. Localize all user-facing strings
4. Write tests for new functionality
5. Update documentation for user-facing changes
6. Test on both x64 and ARM64 if possible
7. Keep security and performance in mind

View File

@@ -1,4 +1,4 @@
name: Build_and_Release_mR-NB
name: Build_and_Release_mR-NB_MultiDeploy
on:
push:
branches:
@@ -19,12 +19,28 @@ jobs:
strategy:
matrix:
include:
# Framework-Dependent builds (requires .NET runtime on user machine)
- runner: windows-latest
platform: x64
arch: x64
deployment: framework-dependent
deploy_suffix: FD
- runner: windows-11-arm
platform: ARM64
arch: arm64
deployment: framework-dependent
deploy_suffix: FD
# Self-Contained builds (includes .NET runtime)
- runner: windows-latest
platform: x64
arch: x64
deployment: self-contained
deploy_suffix: SC
- runner: windows-11-arm
platform: ARM64
arch: arm64
deployment: self-contained
deploy_suffix: SC
runs-on: ${{ matrix.runner }}
# Only run if:
# - manual dispatch, OR
@@ -36,13 +52,18 @@ jobs:
steps:
- name: (01) Checkout Repository
uses: actions/checkout@v6
- name: (02) Setup MSBuild
- name: (02) Install .NET 10 SDK
uses: actions/setup-dotnet@v5
with:
dotnet-version: '10.0.x'
- name: (03) Setup MSBuild
uses: microsoft/setup-msbuild@v2
with:
vs-version: '17.14.12'
vs-version: '[17.0,18.0)'
- name: (03) Install and run dotnet-t4 to transform T4 templates
- name: (04) Install and run dotnet-t4 to transform T4 templates
shell: pwsh
run: |
dotnet tool install --global dotnet-t4
@@ -56,24 +77,32 @@ jobs:
env:
PLATFORM: '${{ matrix.platform }}'
- name: (04) Cache NuGet Packages
- name: (05) Cache NuGet Packages
uses: actions/cache@v5
with:
path: ~/.nuget/packages
key: ${{ runner.os }}-${{ matrix.arch }}-nuget-${{ hashFiles('**/*.csproj') }}
key: ${{ runner.os }}-${{ matrix.arch }}-${{ matrix.deployment }}-nuget-${{ hashFiles('**/*.csproj') }}
restore-keys: |
${{ runner.os }}-${{ matrix.arch }}-${{ matrix.deployment }}-nuget-
${{ runner.os }}-${{ matrix.arch }}-nuget-
- name: (05) Restore NuGet Packages
- name: (06) Restore NuGet Packages
shell: pwsh
run: dotnet restore
- name: (06) Build Release
- name: (07) Build Framework-Dependent Release
if: matrix.deployment == 'framework-dependent'
shell: pwsh
run: |
msbuild "$Env:GITHUB_WORKSPACE\mRemoteNG.sln" -p:Configuration="Release" -p:Platform=${{ matrix.platform }} /verbosity:minimal
- name: (07) Release Information
- name: (08) Build Self-Contained Release
if: matrix.deployment == 'self-contained'
shell: pwsh
run: |
msbuild "$Env:GITHUB_WORKSPACE\mRemoteNG.sln" /t:Publish /p:Configuration="Release Self-Contained" -p:Platform=${{ matrix.platform }} /verbosity:minimal /p:SelfContained=true /p:PublishDir="bin\${{ matrix.platform }}\Release"
- name: (09) Release Information
id: version
shell: pwsh
run: |
@@ -86,7 +115,7 @@ jobs:
throw "Could not extract version and build number"
}
$date = Get-Date -Format "yyyyMMdd"
$zipName = "mRemoteNG-$date-v$version-NB-$build-${{ matrix.arch }}.zip"
$zipName = "mRemoteNG-$date-v$version-NB-$build-${{ matrix.arch }}-${{ matrix.deploy_suffix }}.zip"
$tag = "$date-v$version-NB-($build)"
$message = git log -1 --pretty=%B
echo "message=$message" >> $env:GITHUB_OUTPUT
@@ -94,9 +123,9 @@ jobs:
echo "version=$version" >> $env:GITHUB_OUTPUT
echo "build=$build" >> $env:GITHUB_OUTPUT
echo "tag=$tag" >> $env:GITHUB_OUTPUT
$version = "${{ steps.version.outputs.version }}"
echo "deployment=${{ matrix.deployment }}" >> $env:GITHUB_OUTPUT
- name: (08) Extract Changelog Section
- name: (10) Extract Changelog Section
id: changelog
shell: pwsh
run: |
@@ -127,30 +156,128 @@ jobs:
echo "log<<EOF" >> $env:GITHUB_OUTPUT
echo $joined >> $env:GITHUB_OUTPUT
echo "EOF" >> $env:GITHUB_OUTPUT
echo "log=$escaped"
- name: (09) Create Zip File
- name: (11) Create Zip File
shell: pwsh
run: |
$sourceDir = "$Env:GITHUB_WORKSPACE\mRemoteNG\bin\${{ matrix.platform }}\Release"
Compress-Archive -Path "$sourceDir\*" -DestinationPath ${{ steps.version.outputs.zipname }}
echo "File: ${{ steps.version.outputs.zipname }}"
# Show file size
$fileSize = (Get-Item ${{ steps.version.outputs.zipname }}).Length / 1MB
echo "Size: $([math]::Round($fileSize, 2)) MB"
- name: (10) Create release
id: create_release
- name: (10) Upload artifacts for combination
uses: actions/upload-artifact@v6
with:
name: release-${{ matrix.arch }}-${{ matrix.deploy_suffix }}
path: ${{ steps.version.outputs.zipname }}
retention-days: 1
Create-Combined-Release:
needs: NB-Build-and-Release
runs-on: windows-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v6
- name: Download all artifacts
uses: actions/download-artifact@v7
with:
path: artifacts
- name: Extract version info
id: version
shell: pwsh
run: |
$assemblyInfoPath = "${{ github.workspace }}\mRemoteNG\Properties\AssemblyInfo.cs"
$line = Get-Content $assemblyInfoPath | Where-Object { $_ -match 'AssemblyVersion\("(.+?)"\)' }
if ($line -match 'AssemblyVersion\("(?<ver>\d+\.\d+\.\d+)\.(?<build>\d+)"\)') {
$version = $matches['ver']
$build = $matches['build']
} else {
throw "Could not extract version and build number"
}
$date = Get-Date -Format "yyyyMMdd"
$tag = "$date-v$version-NB-($build)"
$message = git log -1 --pretty=%B
echo "version=$version" >> $env:GITHUB_OUTPUT
echo "build=$build" >> $env:GITHUB_OUTPUT
echo "tag=$tag" >> $env:GITHUB_OUTPUT
echo "message=$message" >> $env:GITHUB_OUTPUT
- name: Extract Changelog
id: changelog
shell: pwsh
run: |
$changelogPath = "$env:GITHUB_WORKSPACE\CHANGELOG.md"
$lines = Get-Content $changelogPath
$startIndex = -1
for ($i = 0; $i -lt $lines.Count; $i++) {
if ($lines[$i] -match '^## \[') {
$startIndex = $i
break
}
}
if ($startIndex -eq -1) {
throw "No version header found in CHANGELOG.md"
}
$section = @()
for ($i = $startIndex + 1; $i -lt $lines.Count; $i++) {
if ($lines[$i] -match '^## ') {
break
}
$section += $lines[$i]
}
$joined = $section -join "`n"
echo "log<<EOF" >> $env:GITHUB_OUTPUT
echo $joined >> $env:GITHUB_OUTPUT
echo "EOF" >> $env:GITHUB_OUTPUT
- name: Collect all release files
shell: pwsh
run: |
Get-ChildItem -Path artifacts -Recurse -Filter "*.zip" | ForEach-Object {
Copy-Item $_.FullName -Destination .
echo "Found: $($_.Name)"
}
- name: Create combined release
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ steps.version.outputs.tag }}
tag_name: ${{ steps.version.outputs.tag }}
name: "mRemoteNG ${{ steps.version.outputs.version }} NB ${{ steps.version.outputs.build }}"
files: ${{ steps.version.outputs.zipname }}
files: '*.zip'
body: |
Changes in this Release:
## mRemoteNG ${{ steps.version.outputs.version }} NB Build ${{ steps.version.outputs.build }}
### 📦 Available Downloads
**Framework-Dependent (FD)** - Requires .NET 10 Runtime installed:
- Smaller download size (~15-25 MB)
- Requires .NET 10 Desktop Runtime on user machine
- Files: `*-FD.zip`
**Self-Contained (SC)** - Portable, no installation required:
- Larger download size (~80-150 MB)
- Includes .NET 10 runtime - no installation needed
- Files: `*-SC.zip`
Both versions are available for **x64** and **ARM64** architectures.
---
### 📝 Changes in this Release
${{ steps.changelog.outputs.log }}
Last Commit Message:
### 💬 Last Commit Message
${{ steps.version.outputs.message }}
draft: false

167
DEPLOYMENT_OPTIONS.md Normal file
View File

@@ -0,0 +1,167 @@
# mRemoteNG Deployment Options
This document explains the two deployment options for mRemoteNG and how to build each version.
## Deployment Types
### 1. Framework-Dependent (FD)
**File suffix: `-FD.zip`**
- **Size**: ~15-25 MB
- **Requirements**: User must have .NET 10 Desktop Runtime installed
- **Startup**: Application checks for .NET runtime and prompts user to download if missing
- **Use case**: Standard release for users comfortable installing prerequisites
### 2. Self-Contained (SC)
**File suffix: `-SC.zip`**
- **Size**: ~80-150 MB
- **Requirements**: None - includes .NET 10 runtime
- **Startup**: No runtime checks performed (runtime is bundled)
- **Use case**: Portable version for users who want zero installation/configuration
## Building Locally
### Framework-Dependent Build
```powershell
# x64
msbuild mRemoteNG.sln -p:Configuration=Release -p:Platform=x64
# ARM64
msbuild mRemoteNG.sln -p:Configuration=Release -p:Platform=ARM64
```
### Self-Contained Build
```powershell
# x64
dotnet publish mRemoteNG\mRemoteNG.csproj `
--configuration Release `
--runtime win-x64 `
--self-contained true `
-p:Platform=x64 `
-p:PublishSingleFile=false `
-p:PublishReadyToRun=true `
-p:DefineConstants="SELF_CONTAINED"
# ARM64
dotnet publish mRemoteNG\mRemoteNG.csproj `
--configuration Release `
--runtime win-arm64 `
--self-contained true `
-p:Platform=ARM64 `
-p:PublishSingleFile=false `
-p:PublishReadyToRun=true `
-p:DefineConstants="SELF_CONTAINED"
```
## GitHub Actions Workflow
The new workflow file `Build_and_Release_mR-NB-MultiDeploy.yml` automatically builds all four variants:
1. **x64 Framework-Dependent** - `mRemoteNG-YYYYMMDD-vX.X.X-NB-XXXX-x64-FD.zip`
2. **x64 Self-Contained** - `mRemoteNG-YYYYMMDD-vX.X.X-NB-XXXX-x64-SC.zip`
3. **ARM64 Framework-Dependent** - `mRemoteNG-YYYYMMDD-vX.X.X-NB-XXXX-arm64-FD.zip`
4. **ARM64 Self-Contained** - `mRemoteNG-YYYYMMDD-vX.X.X-NB-XXXX-arm64-SC.zip`
### Workflow Triggers
The workflow runs when:
- You push to `v1.78.2-dev` branch with commit message containing "NB release"
- You manually trigger via workflow_dispatch
### Release Output
All four zip files are uploaded to a single GitHub Release with clear descriptions:
- Framework-Dependent versions are marked as requiring .NET 10 Runtime
- Self-Contained versions are marked as portable/no installation needed
## Code Changes
### ProgramRoot.cs
The `MainAsync` method now uses conditional compilation:
```csharp
#if !SELF_CONTAINED
// Runtime check code only included in Framework-Dependent builds
// Checks for .NET Runtime and Visual C++ Redistributable
#endif
```
When building with `-p:DefineConstants="SELF_CONTAINED"`, the runtime checks are completely excluded from the compiled binary.
## Recommendations
### For Users
**Choose Framework-Dependent (FD) if:**
- You don't mind installing .NET 10 Desktop Runtime once
- You want smaller download size
- You're using multiple .NET applications (runtime is shared)
**Choose Self-Contained (SC) if:**
- You want zero installation/setup
- You need portable deployment (USB drive, restricted environments)
- You don't want to deal with prerequisites
### For Distribution
Consider offering both options:
- Make Framework-Dependent the **default/recommended** option (smaller, faster updates)
- Offer Self-Contained as **portable alternative** for special use cases
## Technical Details
### Compilation Symbols
- Framework-Dependent builds: No special symbols
- Self-Contained builds: `SELF_CONTAINED` symbol defined
### Runtime Identifiers
- x64: `win-x64`
- ARM64: `win-arm64`
### Publish Options
Self-contained builds use these optimizations:
- `PublishReadyToRun=true` - AOT compilation for faster startup
- `IncludeNativeLibrariesForSelfExtract=true` - Bundle native dependencies
- `PublishSingleFile=false` - Keep files separate for better compatibility with mRemoteNG's plugin system
## File Size Comparison
Typical build sizes:
| Version | Framework-Dependent | Self-Contained |
|---------|---------------------|----------------|
| x64 | ~18 MB | ~95 MB |
| ARM64 | ~18 MB | ~95 MB |
*Note: Self-contained includes entire .NET 10 runtime (~80 MB overhead)*
## Testing
### Framework-Dependent Build
1. Uninstall .NET 10 Runtime (if installed)
2. Run mRemoteNG.exe
3. Should prompt to download .NET 10 Runtime
4. Install runtime and verify app launches
### Self-Contained Build
1. Uninstall .NET 10 Runtime (if installed)
2. Run mRemoteNG.exe
3. Should launch immediately without runtime check
4. Verify full functionality
## Migration from Old Workflow
The original workflow `Build_and_Release_mR-NB.yml` is preserved. To migrate:
1. Rename or remove old workflow: `Build_and_Release_mR-NB.yml`
2. Rename new workflow: `Build_and_Release_mR-NB-MultiDeploy.yml``Build_and_Release_mR-NB.yml`
3. Commit and push with "NB release" in message

View File

@@ -2,47 +2,47 @@
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>
<NoWarn>$(NoWarn);NU1507</NoWarn>
<NoWarn>$(NoWarn);NU1507;NU1701</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="AWSSDK.Core" Version="4.0.3.6" />
<PackageVersion Include="AWSSDK.EC2" Version="4.0.65" />
<PackageVersion Include="AWSSDK.Core" Version="4.0.3.14" />
<PackageVersion Include="AWSSDK.EC2" Version="4.0.74" />
<PackageVersion Include="BouncyCastle.Cryptography" Version="2.6.2" />
<PackageVersion Include="Castle.Core" Version="5.2.1" />
<PackageVersion Include="ConsoleControl" Version="1.3.0" />
<PackageVersion Include="ConsoleControlAPI" Version="1.3.0" />
<PackageVersion Include="coverlet.collector" Version="6.0.4" />
<PackageVersion Include="Cucumber.Messages" Version="31.1.0" />
<PackageVersion Include="Cucumber.Messages" Version="32.0.1" />
<PackageVersion Include="DockPanelSuite" Version="3.1.1" />
<PackageVersion Include="DockPanelSuite.ThemeVS2015" Version="3.1.1" />
<PackageVersion Include="envdte" Version="17.14.40260" />
<PackageVersion Include="Gherkin" Version="37.0.1" />
<PackageVersion Include="Google.Protobuf" Version="3.33.2" />
<PackageVersion Include="Gherkin" Version="38.0.0" />
<PackageVersion Include="Google.Protobuf" Version="3.33.5" />
<PackageVersion Include="LiteDB" Version="5.0.21" />
<PackageVersion Include="log4net" Version="3.2.0" />
<PackageVersion Include="Microsoft.Data.SqlClient" Version="6.1.3" />
<PackageVersion Include="Microsoft.Data.SqlClient" Version="6.1.4" />
<PackageVersion Include="Microsoft.Data.SqlClient.SNI" Version="6.0.2" />
<PackageVersion Include="Microsoft.Data.SqlClient.SNI.runtime" Version="6.0.2" />
<PackageVersion Include="Microsoft.Extensions.DependencyModel" Version="10.0.1" />
<PackageVersion Include="Microsoft.Extensions.Configuration.UserSecrets" Version="10.0.1" />
<PackageVersion Include="Microsoft.Extensions.DependencyModel" Version="10.0.3" />
<PackageVersion Include="Microsoft.Extensions.Configuration.UserSecrets" Version="10.0.3" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
<PackageVersion Include="Microsoft.NETCore.Platforms" Version="7.0.4" />
<PackageVersion Include="Microsoft.NETCore.Targets" Version="5.0.0" />
<PackageVersion Include="Microsoft.VisualStudio.TextTemplating.VSHost" Version="17.14.40265" />
<PackageVersion Include="Microsoft.Web.WebView2" Version="1.0.3650.58" />
<PackageVersion Include="Microsoft.Web.WebView2" Version="1.0.3719.77" />
<PackageVersion Include="Microsoft.Xaml.Behaviors.Wpf" Version="1.1.135" />
<PackageVersion Include="Microsoft-WindowsAPICodePack-Shell" Version="1.1.5" />
<PackageVersion Include="MySql.Data" Version="9.5.0" />
<PackageVersion Include="MySql.Data" Version="9.6.0" />
<PackageVersion Include="NETStandard.Library" Version="2.0.3" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.4" />
<PackageVersion Include="Newtonsoft.Json.Schema" Version="4.0.1" />
<PackageVersion Include="NSubstitute" Version="5.3.0" />
<PackageVersion Include="NUnit" Version="4.4.0" />
<PackageVersion Include="NUnit.Console" Version="3.21.1" />
<PackageVersion Include="NUnit.ConsoleRunner" Version="3.21.1" />
<PackageVersion Include="NUnit.Console" Version="3.22.0" />
<PackageVersion Include="NUnit.ConsoleRunner" Version="3.22.0" />
<PackageVersion Include="NUnit.Extension.TeamCityEventListener" Version="1.0.10" />
<PackageVersion Include="NUnit.Runners" Version="3.12.0" />
<PackageVersion Include="NUnit3TestAdapter" Version="6.0.1" />
<PackageVersion Include="NUnit3TestAdapter" Version="6.1.0" />
<PackageVersion Include="OpenCover" Version="4.7.1221" />
<PackageVersion Include="Renci.SshNet.Async" Version="1.4.0" />
<PackageVersion Include="ReportGenerator" Version="5.5.1" />
@@ -58,18 +58,18 @@
<PackageVersion Include="runtime.ubuntu.16.10-x64.runtime.native.System.Security.Cryptography.OpenSsl" Version="4.3.3" />
<PackageVersion Include="SSH.NET" Version="2025.1.0" />
<PackageVersion Include="System.Buffers" Version="4.6.1" />
<PackageVersion Include="System.Configuration.ConfigurationManager" Version="10.0.1" />
<PackageVersion Include="System.Collections.Immutable" Version="10.0.1" />
<PackageVersion Include="System.Configuration.ConfigurationManager" Version="10.0.3" />
<PackageVersion Include="System.Collections.Immutable" Version="10.0.3" />
<PackageVersion Include="System.Console" Version="4.3.1" />
<PackageVersion Include="System.Data.Common" Version="4.3.0" />
<PackageVersion Include="System.Diagnostics.DiagnosticSource" Version="10.0.1" />
<PackageVersion Include="System.Drawing.Common" Version="10.0.1" />
<PackageVersion Include="System.Diagnostics.EventLog" Version="10.0.1" />
<PackageVersion Include="System.DirectoryServices" Version="10.0.1" />
<PackageVersion Include="System.Diagnostics.DiagnosticSource" Version="10.0.3" />
<PackageVersion Include="System.Drawing.Common" Version="10.0.3" />
<PackageVersion Include="System.Diagnostics.EventLog" Version="10.0.3" />
<PackageVersion Include="System.DirectoryServices" Version="10.0.3" />
<PackageVersion Include="System.Dynamic.Runtime" Version="4.3.0" />
<PackageVersion Include="System.IO.Pipelines" Version="10.0.1" />
<PackageVersion Include="System.Formats.Asn1" Version="10.0.1" />
<PackageVersion Include="System.Management" Version="10.0.1" />
<PackageVersion Include="System.IO.Pipelines" Version="10.0.3" />
<PackageVersion Include="System.Formats.Asn1" Version="10.0.3" />
<PackageVersion Include="System.Management" Version="10.0.3" />
<PackageVersion Include="System.Memory" Version="4.6.3" />
<PackageVersion Include="System.Net.Http" Version="4.3.4" />
<PackageVersion Include="System.Net.Primitives" Version="4.3.1" />
@@ -77,7 +77,7 @@
<PackageVersion Include="System.Reflection.Emit" Version="4.7.0" />
<PackageVersion Include="System.Reflection.Emit.ILGeneration" Version="4.7.0" />
<PackageVersion Include="System.Reflection.Emit.Lightweight" Version="4.7.0" />
<PackageVersion Include="System.Reflection.Metadata" Version="10.0.1" />
<PackageVersion Include="System.Reflection.Metadata" Version="10.0.3" />
<PackageVersion Include="System.Reflection.TypeExtensions" Version="4.7.0" />
<PackageVersion Include="System.Resources.ResourceManager" Version="4.3.0" />
<PackageVersion Include="System.Runtime" Version="4.3.1" />
@@ -87,19 +87,19 @@
<PackageVersion Include="System.Security.Cryptography.Algorithms" Version="4.3.1" />
<PackageVersion Include="System.Security.Cryptography.Cng" Version="5.0.0" />
<PackageVersion Include="System.Security.Cryptography.OpenSsl" Version="5.0.0" />
<PackageVersion Include="System.Security.Cryptography.ProtectedData" Version="10.0.1" />
<PackageVersion Include="System.Security.Cryptography.ProtectedData" Version="10.0.3" />
<PackageVersion Include="System.Security.Cryptography.X509Certificates" Version="4.3.2" />
<PackageVersion Include="System.Security.Permissions" Version="10.0.1" />
<PackageVersion Include="System.Security.Permissions" Version="10.0.3" />
<PackageVersion Include="System.Security.Principal.Windows" Version="5.0.0" />
<PackageVersion Include="System.Text.Encoding.CodePages" Version="10.0.1" />
<PackageVersion Include="System.Text.Json" Version="10.0.1" />
<PackageVersion Include="System.Text.Encoding.CodePages" Version="10.0.3" />
<PackageVersion Include="System.Text.Json" Version="10.0.3" />
<PackageVersion Include="System.Text.RegularExpressions" Version="4.3.1" />
<PackageVersion Include="System.Threading.Tasks.Extensions" Version="4.6.3" />
<PackageVersion Include="System.ValueTuple" Version="4.6.1" />
<PackageVersion Include="System.Windows.Extensions" Version="10.0.1" />
<PackageVersion Include="System.Windows.Extensions" Version="10.0.3" />
<PackageVersion Include="System.Xml.ReaderWriter" Version="4.3.1" />
<PackageVersion Include="VaultSharp" Version="1.17.5.1" />
<PackageVersion Include="VncSharpCore" Version="1.2.1" />
<PackageVersion Include="ZstdSharp.Port" Version="0.8.6" />
<PackageVersion Include="ZstdSharp.Port" Version="0.8.7" />
</ItemGroup>
</Project>

View File

@@ -1,18 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0-windows10.0.26100.0</TargetFramework>
<TargetFramework>net10.0-windows10.0.26100.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<OutputType>Library</OutputType>
<UseWindowsForms>True</UseWindowsForms>
<Platforms>x64;arm64</Platforms>
<Configurations>Debug;Release;Debug Portable;Release Portable;Deploy to github</Configurations>
<SupportedOSPlatformVersion>10.0.26100.0</SupportedOSPlatformVersion>
<Configurations>Debug;Release;Debug Portable;Release Self-Contained;Deploy to github</Configurations>
<SupportedOSPlatformVersion>10.0.22621.0</SupportedOSPlatformVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release Portable|x64'">
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release Self-Contained|x64'">
<Optimize>True</Optimize>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release Portable|arm64'">
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release Self-Contained|arm64'">
<Optimize>True</Optimize>
</PropertyGroup>
<ItemGroup>

View File

@@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0-windows10.0.26100.0</TargetFramework>
<TargetFramework>net10.0-windows10.0.26100.0</TargetFramework>
<Deterministic>false</Deterministic>
<RootNamespace>BrightIdeasSoftware</RootNamespace>
<AssemblyName>ObjectListView</AssemblyName>
@@ -35,8 +35,4 @@
<Folder Include="Resources\" />
<Folder Include="Rendering\" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="System.Drawing.Common" />
<PackageReference Include="System.Runtime.CompilerServices.Unsafe" />
</ItemGroup>
</Project>

View File

@@ -1906,7 +1906,7 @@ namespace BrightIdeasSoftware
[Category("ObjectListView"),
Description("The image list from which group header will take their images"),
DefaultValue(null)]
public ImageList GroupImageList
public new ImageList GroupImageList
{
get { return this.groupImageList; }
set

View File

@@ -645,7 +645,6 @@ namespace BrightIdeasSoftware
/// Mess with the basic message pump of the tooltip
/// </summary>
/// <param name="msg"></param>
[SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.UnmanagedCode)]
override protected void WndProc(ref Message msg) {
//System.Diagnostics.Trace.WriteLine(String.Format("xx {0:x}", msg.Msg));
switch (msg.Msg) {
@@ -697,5 +696,4 @@ namespace BrightIdeasSoftware
#endregion
}
}

View File

@@ -101,7 +101,7 @@ namespace BrightIdeasSoftware
/// <para>
/// Although it isn't documented, .NET virtual lists cannot have checkboxes. This class codes around this limitation,
/// but you must use the functions provided by ObjectListView: CheckedObjects, CheckObject(), UncheckObject() and their friends.
/// If you use the normal check box properties (CheckedItems or CheckedIndicies), they will throw an exception, since the
/// If you use the normal check box properties (CheckedItems or CheckedIndicie), they will throw an exception, since the
/// list is in virtual mode, and .NET "knows" it can't handle checkboxes in virtual mode.
/// </para>
/// <para>Due to the limits of the underlying Windows control, virtual lists do not trigger ItemCheck/ItemChecked events.
@@ -155,7 +155,7 @@ namespace BrightIdeasSoftware
/// <para>
/// This property returns a simple collection. Changes made to the returned
/// collection do NOT affect the list. This is different to the behaviour of
/// CheckedIndicies collection.
/// CheckedIndicie collection.
/// </para>
/// <para>
/// When getting CheckedObjects, the performance of this method is O(n) where n is the number of checked objects.
@@ -405,8 +405,6 @@ namespace BrightIdeasSoftware
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_virtualListSize")]
private static extern ref int GetVirtualListSizeField(ListView listView);
static private FieldInfo virtualListSizeFieldInfo;
#endregion
#region OLV accessing

View File

@@ -1,4 +1,4 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31912.275
@@ -13,8 +13,8 @@ Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|arm64 = Debug|arm64
Debug|x64 = Debug|x64
Release Installer and Portable|arm64 = Release Installer and Portable|arm64
Release Installer and Portable|x64 = Release Installer and Portable|x64
Release Self-Contained|arm64 = Release Self-Contained|arm64
Release Self-Contained|x64 = Release Self-Contained|x64
Release|arm64 = Release|arm64
Release|x64 = Release|x64
EndGlobalSection
@@ -23,10 +23,10 @@ Global
{4934A491-40BC-4E5B-9166-EA1169A220F6}.Debug|arm64.Build.0 = Debug|arm64
{4934A491-40BC-4E5B-9166-EA1169A220F6}.Debug|x64.ActiveCfg = Debug|x64
{4934A491-40BC-4E5B-9166-EA1169A220F6}.Debug|x64.Build.0 = Debug|x64
{4934A491-40BC-4E5B-9166-EA1169A220F6}.Release Installer and Portable|arm64.ActiveCfg = Release Portable|arm64
{4934A491-40BC-4E5B-9166-EA1169A220F6}.Release Installer and Portable|arm64.Build.0 = Release Portable|arm64
{4934A491-40BC-4E5B-9166-EA1169A220F6}.Release Installer and Portable|x64.ActiveCfg = Release Portable|x64
{4934A491-40BC-4E5B-9166-EA1169A220F6}.Release Installer and Portable|x64.Build.0 = Release Portable|x64
{4934A491-40BC-4E5B-9166-EA1169A220F6}.Release Self-Contained|arm64.ActiveCfg = Release Self-Contained|arm64
{4934A491-40BC-4E5B-9166-EA1169A220F6}.Release Self-Contained|arm64.Build.0 = Release Self-Contained|arm64
{4934A491-40BC-4E5B-9166-EA1169A220F6}.Release Self-Contained|x64.ActiveCfg = Release Self-Contained|x64
{4934A491-40BC-4E5B-9166-EA1169A220F6}.Release Self-Contained|x64.Build.0 = Release Self-Contained|x64
{4934A491-40BC-4E5B-9166-EA1169A220F6}.Release|arm64.ActiveCfg = Release|arm64
{4934A491-40BC-4E5B-9166-EA1169A220F6}.Release|arm64.Build.0 = Release|arm64
{4934A491-40BC-4E5B-9166-EA1169A220F6}.Release|x64.ActiveCfg = Release|x64
@@ -35,10 +35,10 @@ Global
{A56A2029-79B8-492A-ABE5-D2BFE05801BD}.Debug|arm64.Build.0 = Debug|arm64
{A56A2029-79B8-492A-ABE5-D2BFE05801BD}.Debug|x64.ActiveCfg = Debug|x64
{A56A2029-79B8-492A-ABE5-D2BFE05801BD}.Debug|x64.Build.0 = Debug|x64
{A56A2029-79B8-492A-ABE5-D2BFE05801BD}.Release Installer and Portable|arm64.ActiveCfg = Release Portable|arm64
{A56A2029-79B8-492A-ABE5-D2BFE05801BD}.Release Installer and Portable|arm64.Build.0 = Release Portable|arm64
{A56A2029-79B8-492A-ABE5-D2BFE05801BD}.Release Installer and Portable|x64.ActiveCfg = Release Portable|x64
{A56A2029-79B8-492A-ABE5-D2BFE05801BD}.Release Installer and Portable|x64.Build.0 = Release Portable|x64
{A56A2029-79B8-492A-ABE5-D2BFE05801BD}.Release Self-Contained|arm64.ActiveCfg = Release Self-Contained|arm64
{A56A2029-79B8-492A-ABE5-D2BFE05801BD}.Release Self-Contained|arm64.Build.0 = Release Self-Contained|arm64
{A56A2029-79B8-492A-ABE5-D2BFE05801BD}.Release Self-Contained|x64.ActiveCfg = Release Self-Contained|x64
{A56A2029-79B8-492A-ABE5-D2BFE05801BD}.Release Self-Contained|x64.Build.0 = Release Self-Contained|x64
{A56A2029-79B8-492A-ABE5-D2BFE05801BD}.Release|arm64.ActiveCfg = Release|arm64
{A56A2029-79B8-492A-ABE5-D2BFE05801BD}.Release|arm64.Build.0 = Release|arm64
{A56A2029-79B8-492A-ABE5-D2BFE05801BD}.Release|x64.ActiveCfg = Release|x64
@@ -47,10 +47,10 @@ Global
{5718734C-03AC-4954-89B1-1723CF03AF10}.Debug|arm64.Build.0 = Debug|Any CPU
{5718734C-03AC-4954-89B1-1723CF03AF10}.Debug|x64.ActiveCfg = Debug|Any CPU
{5718734C-03AC-4954-89B1-1723CF03AF10}.Debug|x64.Build.0 = Debug|Any CPU
{5718734C-03AC-4954-89B1-1723CF03AF10}.Release Installer and Portable|arm64.ActiveCfg = Release|Any CPU
{5718734C-03AC-4954-89B1-1723CF03AF10}.Release Installer and Portable|arm64.Build.0 = Release|Any CPU
{5718734C-03AC-4954-89B1-1723CF03AF10}.Release Installer and Portable|x64.ActiveCfg = Release|Any CPU
{5718734C-03AC-4954-89B1-1723CF03AF10}.Release Installer and Portable|x64.Build.0 = Release|Any CPU
{5718734C-03AC-4954-89B1-1723CF03AF10}.Release Self-Contained|arm64.ActiveCfg = Release|Any CPU
{5718734C-03AC-4954-89B1-1723CF03AF10}.Release Self-Contained|arm64.Build.0 = Release|Any CPU
{5718734C-03AC-4954-89B1-1723CF03AF10}.Release Self-Contained|x64.ActiveCfg = Release|Any CPU
{5718734C-03AC-4954-89B1-1723CF03AF10}.Release Self-Contained|x64.Build.0 = Release|Any CPU
{5718734C-03AC-4954-89B1-1723CF03AF10}.Release|arm64.ActiveCfg = Release|Any CPU
{5718734C-03AC-4954-89B1-1723CF03AF10}.Release|arm64.Build.0 = Release|Any CPU
{5718734C-03AC-4954-89B1-1723CF03AF10}.Release|x64.ActiveCfg = Release|Any CPU

View File

@@ -24,7 +24,6 @@ namespace mRemoteNG.App
public static class ProgramRoot
{
private static Mutex? _mutex;
private static FrmSplashScreenNew _frmSplashScreen = null;
private static string customResourcePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Languages");
private static System.Threading.Thread? _wpfSplashThread;
@@ -41,6 +40,9 @@ namespace mRemoteNG.App
{
AppDomain.CurrentDomain.AssemblyResolve += OnAssemblyResolve;
#if !SELF_CONTAINED
// Runtime checks only needed for framework-dependent deployments
// Self-contained builds include the runtime, so no check is needed
string? installedVersion = DotNetRuntimeCheck.GetLatestDotNetRuntimeVersion();
//installedVersion = ""; // Force check for testing purposes
@@ -106,6 +108,7 @@ namespace mRemoteNG.App
{
Environment.Exit(0);
}
#endif
Lazy<bool> singleInstanceOption = new(() => Properties.OptionsStartupExitPage.Default.SingleInstance);
if (singleInstanceOption.Value)

View File

@@ -121,7 +121,11 @@ namespace mRemoteNG.App
private static void RunUpdateFile()
{
if (UpdatePending)
{
// Validate the update file path to prevent command injection
Tools.PathValidator.ValidateExecutablePathOrThrow(_updateFilePath, nameof(_updateFilePath));
Process.Start(new ProcessStartInfo(_updateFilePath) { UseShellExecute = true });
}
}
}
}

View File

@@ -465,4 +465,4 @@ namespace mRemoteNG.Connection
#endregion
}
}
}

View File

@@ -211,7 +211,17 @@ namespace mRemoteNG.Connection.Protocol.AnyDesk
// Username field is optional and not used in the CLI (reserved for future use)
// Password field is piped via stdin when --with-password flag is used
string anydeskId = _connectionInfo.Hostname.Trim();
string arguments = $"{anydeskId}";
// Validate AnyDesk ID to prevent command injection
// AnyDesk IDs are numeric or alphanumeric with @ and - characters for aliases
if (!IsValidAnydeskId(anydeskId))
{
Runtime.MessageCollector?.AddMessage(MessageClass.ErrorMsg,
"Invalid AnyDesk ID format. Only alphanumeric characters, @, -, _, and . are allowed.", true);
return false;
}
string arguments = anydeskId;
// Add --with-password flag if password is provided
bool hasPassword = !string.IsNullOrEmpty(_connectionInfo.Password);
@@ -242,27 +252,45 @@ namespace mRemoteNG.Connection.Protocol.AnyDesk
return false;
}
}
private bool IsValidAnydeskId(string anydeskId)
{
if (string.IsNullOrWhiteSpace(anydeskId))
return false;
// AnyDesk IDs can be:
// - Pure numeric (e.g., 123456789)
// - Alphanumeric with @ for aliases (e.g., alias@ad)
// - May contain hyphens and underscores in aliases
// Reject any characters that could be used for command injection
foreach (char c in anydeskId)
{
if (!char.IsLetterOrDigit(c) && c != '@' && c != '-' && c != '_' && c != '.')
{
return false;
}
}
return true;
}
private bool StartAnydeskWithPassword(string anydeskPath, string arguments)
{
try
{
// Use PowerShell to pipe the password to AnyDesk
// This is the recommended way according to AnyDesk documentation
string escapedPassword = _connectionInfo.Password.Replace("'", "''");
string powershellCommand = $"echo '{escapedPassword}' | & '{anydeskPath}' {arguments}";
// Start AnyDesk and pipe the password directly to stdin
// This avoids command injection vulnerabilities from using PowerShell
_process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = "powershell.exe",
Arguments = $"-WindowStyle Hidden -Command \"{powershellCommand}\"",
FileName = anydeskPath,
Arguments = arguments,
UseShellExecute = false,
CreateNoWindow = true,
CreateNoWindow = false,
RedirectStandardOutput = false,
RedirectStandardError = false,
RedirectStandardInput = false
RedirectStandardInput = true
},
EnableRaisingEvents = true
};
@@ -270,8 +298,23 @@ namespace mRemoteNG.Connection.Protocol.AnyDesk
_process.Exited += ProcessExited;
_process.Start();
// Write the password to stdin and close the stream
// AnyDesk expects the password on stdin when --with-password is used
try
{
if (_process.StandardInput != null)
{
_process.StandardInput.WriteLine(_connectionInfo.Password);
_process.StandardInput.Close();
}
}
catch (Exception ex)
{
Runtime.MessageCollector?.AddMessage(MessageClass.WarningMsg,
$"Failed to send password to AnyDesk: {ex.Message}", true);
}
// Wait for the AnyDesk window to appear
// Note: The window belongs to the AnyDesk process, not PowerShell
if (!WaitForAnydeskWindow())
{
Runtime.MessageCollector?.AddMessage(MessageClass.WarningMsg,

View File

@@ -92,7 +92,7 @@ namespace mRemoteNG.Connection
#region IComponent
[Browsable(false)]
public ISite Site
public ISite? Site
{
get => new PropertyGridCommandSite(this);
set => throw (new NotImplementedException());
@@ -103,7 +103,7 @@ namespace mRemoteNG.Connection
Disposed?.Invoke(this, EventArgs.Empty);
}
public event EventHandler Disposed;
public event EventHandler? Disposed;
#endregion
}

View File

@@ -2559,4 +2559,16 @@ Nightly Channel includes Alphas, Betas &amp; Release Candidates.</value>
<data name="VaultOpenbaoSecretEngineSSHOTP" xml:space="preserve">
<value>SSH engine OTP mode</value>
</data>
<data name="_Sessions" xml:space="preserve">
<value>&amp;Sessions</value>
</data>
<data name="NextSession" xml:space="preserve">
<value>Next Session</value>
</data>
<data name="PreviousSession" xml:space="preserve">
<value>Previous Session</value>
</data>
<data name="JumpToSession" xml:space="preserve">
<value>Jump to Session {0}</value>
</data>
</root>

View File

@@ -145,7 +145,7 @@ namespace mRemoteNG.Properties {
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("False")]
[global::System.Configuration.DefaultSettingValueAttribute("True")]
public bool PopupMessageWriterWriteErrorMsgs {
get {
return ((bool)(this["PopupMessageWriterWriteErrorMsgs"]));

View File

@@ -33,7 +33,7 @@
<Value Profile="(Default)">False</Value>
</Setting>
<Setting Name="PopupMessageWriterWriteErrorMsgs" Type="System.Boolean" Scope="User">
<Value Profile="(Default)">False</Value>
<Value Profile="(Default)">True</Value>
</Setting>
<Setting Name="LogToApplicationDirectory" Type="System.Boolean" Scope="User">
<Value Profile="(Default)">True</Value>

View File

@@ -46,17 +46,19 @@ namespace mRemoteNG.Tools
return Status;
}
//X509Certificate2 certificate2 = new(X509Certificate.CreateFromSignedFile(FilePath));
#pragma warning disable SYSLIB0057
using X509Certificate cert = X509Certificate.CreateFromSignedFile(FilePath);
byte[] certData = cert.GetRawCertData();
#pragma warning restore SYSLIB0057
X509Certificate2 certificate2 = X509CertificateLoader.LoadCertificate(certData);
_thumbprint = certificate2.Thumbprint;
X509Certificate2 certificate2 = new(X509Certificate.CreateFromSignedFile(FilePath));
_thumbprint = certificate2.Thumbprint;
if (_thumbprint != ThumbprintToMatch)
{
Status = StatusValue.ThumbprintNotMatch;
return Status;
}
}
if (_thumbprint != ThumbprintToMatch)
{
Status = StatusValue.ThumbprintNotMatch;
return Status;
}
}
NativeMethods.WINTRUST_FILE_INFO trustFileInfo = new() { pcwszFilePath = FilePath};
trustFileInfoPointer = Marshal.AllocCoTaskMem(Marshal.SizeOf(trustFileInfo));

View File

@@ -42,17 +42,17 @@ namespace mRemoteNG.Tools
return result;
}
public override StandardValuesCollection GetStandardValues(ITypeDescriptorContext context)
public override StandardValuesCollection GetStandardValues(ITypeDescriptorContext? context)
{
return new StandardValuesCollection(SshTunnels);
}
public override bool GetStandardValuesExclusive(ITypeDescriptorContext context)
public override bool GetStandardValuesExclusive(ITypeDescriptorContext? context)
{
return true;
}
public override bool GetStandardValuesSupported(ITypeDescriptorContext context)
public override bool GetStandardValuesSupported(ITypeDescriptorContext? context)
{
return true;
}

View File

@@ -27,7 +27,7 @@ namespace mRemoteNG.UI.Controls
private MrngLabel label2;
private MrngLabel label3;
private ToolTip toolTip1;
private System.ComponentModel.IContainer components;
private System.ComponentModel.Container components;
/* Sets and Gets the tooltiptext on toolTip1 */
public string ToolTipText
@@ -46,7 +46,7 @@ namespace mRemoteNG.UI.Controls
}
/* Set or Get the string that represents the value in the box */
public override string? Text
public override string Text
{
get => (Octet1.Text ?? string.Empty) + @"." + (Octet2.Text ?? string.Empty) + @"." + (Octet3.Text ?? string.Empty) + @"." + (Octet4.Text ?? string.Empty);
set
@@ -119,9 +119,7 @@ namespace mRemoteNG.UI.Controls
{
if (disposing)
{
// ReSharper disable once UseNullPropagation
if (components != null)
components.Dispose();
components?.Dispose();
}
base.Dispose(disposing);

View File

@@ -1,4 +1,5 @@
using System.Diagnostics;
using System;
using System.Diagnostics;
using System.Windows.Forms;
using mRemoteNG.App.Info;
using mRemoteNG.Themes;
@@ -8,6 +9,8 @@ using mRemoteNG.Properties;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using mRemoteNG.UI.Window;
using mRemoteNG.App;
using mRemoteNG.Messages;
namespace mRemoteNG.UI.Forms
{
@@ -41,7 +44,7 @@ namespace mRemoteNG.UI.Forms
[Conditional("PORTABLE")]
private void AddPortableString() => lblTitle.Text += $@" {Language.PortableEdition}";
private void ApplyTheme()
private new void ApplyTheme()
{
if (!ThemeManager.getInstance().ThemingActive) return;
if (!ThemeManager.getInstance().ActiveAndExtended) return;
@@ -59,47 +62,110 @@ namespace mRemoteNG.UI.Forms
private void llLicense_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
{
OpenUrl("https://raw.githubusercontent.com/mRemoteNG/mRemoteNG/v" + Assembly.GetExecutingAssembly().GetName().Version.ToString().Substring(0, Assembly.GetExecutingAssembly().GetName().Version.ToString().Length - 2) + "-" + Properties.OptionsUpdatesPage.Default.CurrentUpdateChannelType + "/COPYING.txt");
var version = Assembly.GetExecutingAssembly().GetName().Version;
var updateChannel = Properties.OptionsUpdatesPage.Default.CurrentUpdateChannelType;
if (version != null && updateChannel != null)
{
var versionString = version.ToString();
OpenUrl("https://raw.githubusercontent.com/mRemoteNG/mRemoteNG/v" + versionString[..^2] + "-" + updateChannel + "/COPYING.txt");
}
Close();
}
private void llChangelog_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
{
OpenUrl("https://raw.githubusercontent.com/mRemoteNG/mRemoteNG/v" + Assembly.GetExecutingAssembly().GetName().Version.ToString().Substring(0, Assembly.GetExecutingAssembly().GetName().Version.ToString().Length - 2) + "-" + Properties.OptionsUpdatesPage.Default.CurrentUpdateChannelType + "/CHANGELOG.md");
var version = Assembly.GetExecutingAssembly().GetName().Version;
var updateChannel = Properties.OptionsUpdatesPage.Default.CurrentUpdateChannelType;
if (version != null && updateChannel != null)
{
var versionString = version.ToString();
OpenUrl("https://raw.githubusercontent.com/mRemoteNG/mRemoteNG/v" + versionString[..^2] + "-" + updateChannel + "/CHANGELOG.md");
}
Close();
}
private void llCredits_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
{
OpenUrl("https://raw.githubusercontent.com/mRemoteNG/mRemoteNG/v" + Assembly.GetExecutingAssembly().GetName().Version.ToString().Substring(0, Assembly.GetExecutingAssembly().GetName().Version.ToString().Length - 2) + "-" + Properties.OptionsUpdatesPage.Default.CurrentUpdateChannelType + "/CREDITS.md");
var version = Assembly.GetExecutingAssembly().GetName().Version;
var updateChannel = Properties.OptionsUpdatesPage.Default.CurrentUpdateChannelType;
if (version != null && updateChannel != null)
{
var versionString = version.ToString();
OpenUrl("https://raw.githubusercontent.com/mRemoteNG/mRemoteNG/v" + versionString[..^2] + "-" + updateChannel + "/CREDITS.md");
}
Close();
}
private void OpenUrl(string url)
private static void OpenUrl(string url)
{
// Validate URL format to prevent injection
if (string.IsNullOrWhiteSpace(url))
return;
// Basic URL validation - ensure it starts with http:// or https://
if (!url.StartsWith("http://", StringComparison.OrdinalIgnoreCase) &&
!url.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
{
// Invalid URL format - don't try to open it
return;
}
try
{
Process.Start(url);
// Use the standard .NET approach for opening URLs securely
// UseShellExecute=true delegates to the OS default handler
var startInfo = new ProcessStartInfo
{
FileName = url,
UseShellExecute = true
};
Process.Start(startInfo);
}
catch
{
// hack because of this: https://github.com/dotnet/corefx/issues/10361
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
// Fallback for older .NET Core versions with bug: https://github.com/dotnet/corefx/issues/10361
// Use platform-specific URL launchers
try
{
url = url.Replace("&", "^&");
Process.Start(new ProcessStartInfo("cmd", $"/c start {url}") { CreateNoWindow = true });
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
// Use rundll32 with url.dll as fallback
var startInfo = new ProcessStartInfo
{
FileName = "rundll32.exe",
UseShellExecute = false,
CreateNoWindow = true
};
startInfo.ArgumentList.Add("url.dll,FileProtocolHandler");
startInfo.ArgumentList.Add(url);
Process.Start(startInfo);
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
var startInfo = new ProcessStartInfo
{
FileName = "xdg-open",
UseShellExecute = false
};
startInfo.ArgumentList.Add(url);
Process.Start(startInfo);
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
var startInfo = new ProcessStartInfo
{
FileName = "open",
UseShellExecute = false
};
startInfo.ArgumentList.Add(url);
Process.Start(startInfo);
}
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
catch
{
Process.Start("xdg-open", url);
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
Process.Start("open", url);
}
else
{
throw;
// Unable to open URL - notify the user
Runtime.MessageCollector?.AddMessage(MessageClass.WarningMsg,
"Unable to open URL in browser. Please open manually: " + url, true);
}
}
}

View File

@@ -44,6 +44,8 @@ namespace mRemoteNG.UI.Forms
this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F));
this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle());
this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle());
this.tableLayoutPanel1.AutoSize = true;
this.tableLayoutPanel1.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowAndShrink;
this.tableLayoutPanel1.Controls.Add(this._Ok, 1, 2);
this.tableLayoutPanel1.Controls.Add(this.buttonCancel, 2, 2);
this.tableLayoutPanel1.Controls.Add(this.textBox, 0, 1);
@@ -51,11 +53,12 @@ namespace mRemoteNG.UI.Forms
this.tableLayoutPanel1.Dock = System.Windows.Forms.DockStyle.Fill;
this.tableLayoutPanel1.Location = new System.Drawing.Point(0, 0);
this.tableLayoutPanel1.Name = "tableLayoutPanel1";
this.tableLayoutPanel1.Padding = new System.Windows.Forms.Padding(8);
this.tableLayoutPanel1.RowCount = 3;
this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 24F));
this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 24F));
this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 24F));
this.tableLayoutPanel1.Size = new System.Drawing.Size(284, 81);
this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.AutoSize));
this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 28F));
this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 35F));
this.tableLayoutPanel1.Size = new System.Drawing.Size(284, 90);
this.tableLayoutPanel1.TabIndex = 0;
//
// _Ok
@@ -103,12 +106,14 @@ namespace mRemoteNG.UI.Forms
this.label.TabIndex = 3;
this.label.Text = "Label";
this.label.TextAlign = System.Drawing.ContentAlignment.MiddleLeft;
//
//
// FrmInputBox
//
//
this.AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi;
this.ClientSize = new System.Drawing.Size(284, 81);
this.AutoSize = true;
this.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowAndShrink;
this.ClientSize = new System.Drawing.Size(300, 95);
this.ControlBox = false;
this.Controls.Add(this.tableLayoutPanel1);
this.DoubleBuffered = true;
@@ -116,6 +121,7 @@ namespace mRemoteNG.UI.Forms
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
this.MaximizeBox = false;
this.MinimizeBox = false;
this.MinimumSize = new System.Drawing.Size(300, 95);
this.Name = "FrmInputBox";
this.ShowIcon = false;
this.ShowInTaskbar = false;

View File

@@ -91,7 +91,12 @@ namespace mRemoteNG.UI.Forms
private void buttonCreateBug_Click(object sender, EventArgs e)
{
Process.Start(GeneralAppInfo.UrlBugs);
var startInfo = new ProcessStartInfo
{
FileName = GeneralAppInfo.UrlBugs,
UseShellExecute = true
};
Process.Start(startInfo);
}
}
}

View File

@@ -7,6 +7,7 @@ using mRemoteNG.App;
using mRemoteNG.Config.Settings.Registry;
using mRemoteNG.Properties;
using mRemoteNG.Resources.Language;
using mRemoteNG.Tools;
namespace mRemoteNG.UI.Forms.OptionsPages
{
@@ -342,8 +343,17 @@ namespace mRemoteNG.UI.Forms.OptionsPages
{
try
{
// Validate path to prevent command injection
PathValidator.ValidatePathOrThrow(path, nameof(path));
// Open the file using the default application associated with its file type based on the user's preference
Process.Start(path);
// Use ProcessStartInfo with UseShellExecute for better control
var startInfo = new ProcessStartInfo
{
FileName = path,
UseShellExecute = true
};
Process.Start(startInfo);
return true;
}
catch
@@ -362,9 +372,19 @@ namespace mRemoteNG.UI.Forms.OptionsPages
{
try
{
// Validate path to prevent command injection
PathValidator.ValidatePathOrThrow(path, nameof(path));
// Open it in "Notepad" (Windows default editor).
// Usually available on all Windows systems
Process.Start("notepad.exe", path);
// Use ProcessStartInfo with ArgumentList for better security
var startInfo = new ProcessStartInfo
{
FileName = "notepad.exe",
UseShellExecute = false
};
startInfo.ArgumentList.Add(path);
Process.Start(startInfo);
return true;
}
catch
@@ -383,11 +403,22 @@ namespace mRemoteNG.UI.Forms.OptionsPages
{
try
{
// Validate path to prevent command injection
PathValidator.ValidatePathOrThrow(path, nameof(path));
// when all fails open filelocation to logfile...
// Open Windows Explorer to the directory containing the file
Process.Start("explorer.exe", $"/select,\"{path}\"");
return true;
}
// Explorer expects /select,"path" as a single argument
var startInfo = new ProcessStartInfo
{
FileName = "explorer.exe",
UseShellExecute = false
};
startInfo.ArgumentList.Add("/select,");
startInfo.ArgumentList.Add(path);
Process.Start(startInfo);
return true;
}
catch
{
// If necessary, the error can be logged here.

View File

@@ -38,6 +38,7 @@ namespace mRemoteNG.UI.Forms
this.pnlDock = new WeifenLuo.WinFormsUI.Docking.DockPanel();
this.msMain = new System.Windows.Forms.MenuStrip();
this.fileMenu = new mRemoteNG.UI.Menu.FileMenu();
this.sessionsMenu = new mRemoteNG.UI.Menu.SessionsMenu();
this.viewMenu = new mRemoteNG.UI.Menu.ViewMenu();
this.toolsMenu = new mRemoteNG.UI.Menu.ToolsMenu();
this.helpMenu = new mRemoteNG.UI.Menu.HelpMenu();
@@ -75,13 +76,14 @@ namespace mRemoteNG.UI.Forms
this.msMain.GripStyle = System.Windows.Forms.ToolStripGripStyle.Visible;
this.msMain.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.fileMenu,
this.sessionsMenu,
this.viewMenu,
this.toolsMenu,
this.helpMenu});
this.msMain.Location = new System.Drawing.Point(3, 0);
this.msMain.Name = "msMain";
this.msMain.Padding = new System.Windows.Forms.Padding(0, 0, 1, 0);
this.msMain.Size = new System.Drawing.Size(151, 25);
this.msMain.Size = new System.Drawing.Size(212, 25);
this.msMain.Stretch = false;
this.msMain.TabIndex = 0;
this.msMain.Text = "Main Toolbar";
@@ -94,6 +96,13 @@ namespace mRemoteNG.UI.Forms
this.fileMenu.Text = "&File";
this.fileMenu.TreeWindow = null;
//
// sessionsMenu
//
this.sessionsMenu.Margin = new System.Windows.Forms.Padding(0, 3, 0, 3);
this.sessionsMenu.Name = "mMenSessions";
this.sessionsMenu.Size = new System.Drawing.Size(61, 19);
this.sessionsMenu.Text = "&Sessions";
//
// viewMenu
//
this.viewMenu.FullscreenHandler = null;
@@ -253,6 +262,7 @@ namespace mRemoteNG.UI.Forms
internal System.Windows.Forms.ToolStripSeparator mMenSep3;
private System.ComponentModel.IContainer components;
private Menu.FileMenu fileMenu;
private Menu.SessionsMenu sessionsMenu;
private Menu.ViewMenu viewMenu;
private Menu.ToolsMenu toolsMenu;
private Menu.HelpMenu helpMenu;

View File

@@ -292,6 +292,7 @@ namespace mRemoteNG.UI.Forms
private void ApplyLanguage()
{
fileMenu.ApplyLanguage();
sessionsMenu.ApplyLanguage();
viewMenu.ApplyLanguage();
toolsMenu.ApplyLanguage();
helpMenu.ApplyLanguage();
@@ -398,6 +399,11 @@ namespace mRemoteNG.UI.Forms
private async void FrmMain_Shown(object sender, EventArgs e)
{
// Bring the main window to the front after splash screen closes
Activate();
BringToFront();
NativeMethods.SetForegroundWindow(Handle);
PromptForUpdatesPreference();
await CheckForUpdates();
}

View File

@@ -188,19 +188,29 @@ namespace mRemoteNG.UI.Menu
}
}
private void mMenInfoHelp_Click(object? sender, EventArgs e) => Process.Start("explorer.exe", GeneralAppInfo.UrlDocumentation);
private void mMenInfoHelp_Click(object? sender, EventArgs e) => OpenUrl(GeneralAppInfo.UrlDocumentation);
private void mMenInfoForum_Click(object? sender, EventArgs e) => Process.Start("explorer.exe", GeneralAppInfo.UrlForum);
private void mMenInfoForum_Click(object? sender, EventArgs e) => OpenUrl(GeneralAppInfo.UrlForum);
private void mMenInfoChat_Click(object? sender, EventArgs e) => Process.Start("explorer.exe", GeneralAppInfo.UrlChat);
private void mMenInfoChat_Click(object? sender, EventArgs e) => OpenUrl(GeneralAppInfo.UrlChat);
private void mMenInfoCommunity_Click(object? sender, EventArgs e) => Process.Start("explorer.exe", GeneralAppInfo.UrlCommunity);
private void mMenInfoCommunity_Click(object? sender, EventArgs e) => OpenUrl(GeneralAppInfo.UrlCommunity);
private void mMenInfoBug_Click(object? sender, EventArgs e) => Process.Start("explorer.exe", GeneralAppInfo.UrlBugs);
private void mMenInfoBug_Click(object? sender, EventArgs e) => OpenUrl(GeneralAppInfo.UrlBugs);
private void mMenInfoWebsite_Click(object? sender, EventArgs e) => Process.Start("explorer.exe", GeneralAppInfo.UrlHome);
private void mMenInfoWebsite_Click(object? sender, EventArgs e) => OpenUrl(GeneralAppInfo.UrlHome);
private void mMenInfoDonate_Click(object? sender, EventArgs e) => Process.Start("explorer.exe", GeneralAppInfo.UrlDonate);
private void mMenInfoDonate_Click(object? sender, EventArgs e) => OpenUrl(GeneralAppInfo.UrlDonate);
private static void OpenUrl(string url)
{
var startInfo = new ProcessStartInfo
{
FileName = url,
UseShellExecute = true
};
Process.Start(startInfo);
}
private void mMenInfoAbout_Click(object? sender, EventArgs e)
{

View File

@@ -0,0 +1,150 @@
using System;
using System.Windows.Forms;
using mRemoteNG.UI.Window;
using mRemoteNG.Resources.Language;
using System.Runtime.Versioning;
using mRemoteNG.UI.Forms;
namespace mRemoteNG.UI.Menu
{
[SupportedOSPlatform("windows")]
public class SessionsMenu : ToolStripMenuItem
{
private ToolStripMenuItem _mMenSessionsNextSession;
private ToolStripMenuItem _mMenSessionsPreviousSession;
private ToolStripSeparator _mMenSessionsSep1;
private readonly ToolStripMenuItem[] _sessionNumberItems = new ToolStripMenuItem[9];
public SessionsMenu()
{
Initialize();
}
private void Initialize()
{
_mMenSessionsNextSession = new ToolStripMenuItem();
_mMenSessionsPreviousSession = new ToolStripMenuItem();
_mMenSessionsSep1 = new ToolStripSeparator();
// Initialize session number menu items (Ctrl+1 through Ctrl+9)
for (int i = 0; i < 9; i++)
{
_sessionNumberItems[i] = new ToolStripMenuItem();
}
//
// mMenSessions
//
DropDownItems.Add(_mMenSessionsNextSession);
DropDownItems.Add(_mMenSessionsPreviousSession);
DropDownItems.Add(_mMenSessionsSep1);
for (int i = 0; i < 9; i++)
{
DropDownItems.Add(_sessionNumberItems[i]);
}
Name = "mMenSessions";
Size = new System.Drawing.Size(61, 20);
Text = Language._Sessions;
//
// mMenSessionsNextSession
//
_mMenSessionsNextSession.Name = "mMenSessionsNextSession";
_mMenSessionsNextSession.ShortcutKeys = Keys.Control | Keys.Right;
_mMenSessionsNextSession.Size = new System.Drawing.Size(230, 22);
_mMenSessionsNextSession.Text = Language.NextSession;
_mMenSessionsNextSession.Click += mMenSessionsNextSession_Click;
//
// mMenSessionsPreviousSession
//
_mMenSessionsPreviousSession.Name = "mMenSessionsPreviousSession";
_mMenSessionsPreviousSession.ShortcutKeys = Keys.Control | Keys.Left;
_mMenSessionsPreviousSession.Size = new System.Drawing.Size(230, 22);
_mMenSessionsPreviousSession.Text = Language.PreviousSession;
_mMenSessionsPreviousSession.Click += mMenSessionsPreviousSession_Click;
//
// mMenSessionsSep1
//
_mMenSessionsSep1.Name = "mMenSessionsSep1";
_mMenSessionsSep1.Size = new System.Drawing.Size(227, 6);
// Initialize session number items (Ctrl+1 through Ctrl+9)
for (int i = 0; i < 9; i++)
{
int sessionNumber = i + 1;
_sessionNumberItems[i].Name = $"mMenSessionsSession{sessionNumber}";
_sessionNumberItems[i].ShortcutKeys = Keys.Control | (Keys.D1 + i);
_sessionNumberItems[i].Size = new System.Drawing.Size(230, 22);
_sessionNumberItems[i].Text = string.Format(Language.JumpToSession, sessionNumber);
int capturedIndex = i; // Capture the index for the lambda
_sessionNumberItems[i].Click += (s, e) => JumpToSessionNumber(capturedIndex);
}
// Hook up the dropdown opening event to update enabled state
DropDownOpening += SessionsMenu_DropDownOpening;
}
public void ApplyLanguage()
{
Text = Language._Sessions;
_mMenSessionsNextSession.Text = Language.NextSession;
_mMenSessionsPreviousSession.Text = Language.PreviousSession;
for (int i = 0; i < 9; i++)
{
_sessionNumberItems[i].Text = string.Format(Language.JumpToSession, i + 1);
}
}
private void SessionsMenu_DropDownOpening(object sender, EventArgs e)
{
// Update enabled state of menu items based on active sessions
var connectionWindow = GetActiveConnectionWindow();
bool hasMultipleSessions = false;
int sessionCount = 0;
if (connectionWindow != null)
{
var documents = connectionWindow.GetDocuments();
sessionCount = documents.Length;
hasMultipleSessions = sessionCount > 1;
}
_mMenSessionsNextSession.Enabled = hasMultipleSessions;
_mMenSessionsPreviousSession.Enabled = hasMultipleSessions;
// Enable/disable session number items based on session count
for (int i = 0; i < 9; i++)
{
_sessionNumberItems[i].Enabled = (i < sessionCount);
}
}
private void mMenSessionsNextSession_Click(object sender, EventArgs e)
{
var connectionWindow = GetActiveConnectionWindow();
connectionWindow?.NavigateToNextTab();
}
private void mMenSessionsPreviousSession_Click(object sender, EventArgs e)
{
var connectionWindow = GetActiveConnectionWindow();
connectionWindow?.NavigateToPreviousTab();
}
private void JumpToSessionNumber(int index)
{
var connectionWindow = GetActiveConnectionWindow();
connectionWindow?.NavigateToTab(index);
}
private ConnectionWindow GetActiveConnectionWindow()
{
return FrmMain.Default.pnlDock?.ActiveDocument as ConnectionWindow;
}
}
}

View File

@@ -40,7 +40,7 @@ namespace mRemoteNG.UI.TaskDialog
//--------------------------------------------------------------------------------
// Override this to make sure the control is invalidated (repainted) when 'Text' is changed
public override string Text
public override string? Text
{
get => base.Text;
set
@@ -109,13 +109,16 @@ namespace mRemoteNG.UI.TaskDialog
//--------------------------------------------------------------------------------
string GetLargeText()
{
if (string.IsNullOrEmpty(Text))
return string.Empty;
string[] lines = Text.Split('\n');
return lines[0];
}
string GetSmallText()
{
if (Text.IndexOf('\n') < 0)
if (string.IsNullOrEmpty(Text) || Text.IndexOf('\n') < 0)
return "";
string s = Text;

View File

@@ -387,6 +387,34 @@ namespace mRemoteNG.UI.Window
}
}
internal void NavigateToTab(int index)
{
try
{
var documents = connDock.DocumentsToArray();
if (index < 0 || index >= documents.Length) return;
documents[index].DockHandler.Activate();
}
catch (Exception ex)
{
Runtime.MessageCollector.AddExceptionMessage("NavigateToTab (UI.Window.ConnectionWindow) failed", ex);
}
}
internal IDockContent[] GetDocuments()
{
try
{
return connDock.DocumentsToArray();
}
catch (Exception ex)
{
Runtime.MessageCollector.AddExceptionMessage("GetDocuments (UI.Window.ConnectionWindow) failed", ex);
return Array.Empty<IDockContent>();
}
}
#endregion
#region Events

View File

@@ -99,7 +99,12 @@ namespace mRemoteNG.UI.Window
return;
}
Process.Start(linkUri.ToString());
var startInfo = new ProcessStartInfo
{
FileName = linkUri.ToString(),
UseShellExecute = true
};
Process.Start(startInfo);
}
#endregion

View File

@@ -3,6 +3,7 @@
<PropertyGroup>
<Platforms>x64;arm64</Platforms>
<RuntimeIdentifiers>win-x64;win-arm64</RuntimeIdentifiers>
<NoWarn>$(NoWarn);NU1701</NoWarn>
</PropertyGroup>
<PropertyGroup>
<OutputType>WinExe</OutputType>
@@ -15,13 +16,13 @@
<PackageLicenseFile>COPYING.TXT</PackageLicenseFile>
<PackageProjectUrl>https://mremoteng.org/</PackageProjectUrl>
<Platforms>x64;arm64</Platforms>
<Configurations>Debug;Release;Debug Portable;Release Portable;Release Installer;Deploy to github</Configurations>
<Configurations>Debug;Release;Release Self-Contained;Deploy to github</Configurations>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<LangVersion>latest</LangVersion>
<PackageIcon>Header_dark.png</PackageIcon>
<ProjectTypeGuids>{60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
<UseWPF>True</UseWPF>
<SupportedOSPlatformVersion>10.0.26100.0</SupportedOSPlatformVersion>
<SupportedOSPlatformVersion>10.0.17763.0</SupportedOSPlatformVersion>
<SignAssembly>False</SignAssembly>
<Title>Multi-Remote Next Generation Connection Manager</Title>
<RepositoryUrl>https://github.com/mRemoteNG/mRemoteNG.git</RepositoryUrl>
@@ -69,55 +70,40 @@
<WarningLevel>8</WarningLevel>
<ResolveComReferenceSilent>True</ResolveComReferenceSilent>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug Portable|x64'">
<DefineConstants>DEBUG;PORTABLE</DefineConstants>
<Optimize>False</Optimize>
<OutputPath>bin\x64\Debug Portable\</OutputPath>
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
<WarningsAsErrors />
<GenerateAssemblyInfo>False</GenerateAssemblyInfo>
<WarningLevel>8</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug Portable|arm64'">
<DefineConstants>DEBUG;PORTABLE</DefineConstants>
<Optimize>False</Optimize>
<OutputPath>bin\arm64\Debug Portable\</OutputPath>
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
<WarningsAsErrors />
<GenerateAssemblyInfo>False</GenerateAssemblyInfo>
<WarningLevel>8</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release Portable|x64'">
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release Self-Contained|x64'">
<DefineConstants>PORTABLE</DefineConstants>
<Optimize>True</Optimize>
<OutputPath>bin\x64\Release Portable\</OutputPath>
<PublishDir>bin\x64\Publish Self-Contained\</PublishDir>
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
<WarningsAsErrors />
<GenerateAssemblyInfo>False</GenerateAssemblyInfo>
<WarningLevel>8</WarningLevel>
<ResolveComReferenceSilent>True</ResolveComReferenceSilent>
<SelfContained>true</SelfContained>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<PublishReadyToRun>true</PublishReadyToRun>
<CopyBuildOutputToPublishDirectory>true</CopyBuildOutputToPublishDirectory>
<CopyOutputSymbolsToPublishDirectory>false</CopyOutputSymbolsToPublishDirectory>
<RunCommand></RunCommand>
<RunArguments></RunArguments>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release Portable|arm64'">
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release Self-Contained|arm64'">
<DefineConstants>PORTABLE</DefineConstants>
<Optimize>True</Optimize>
<OutputPath>bin\arm64\Release Portable\</OutputPath>
<OutputPath>bin\arm64\Release Self-Contained\</OutputPath>
<PublishDir>bin\arm64\Publish Self-Contained\</PublishDir>
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
<WarningsAsErrors />
<GenerateAssemblyInfo>False</GenerateAssemblyInfo>
<WarningLevel>8</WarningLevel>
<ResolveComReferenceSilent>True</ResolveComReferenceSilent>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release Installer|x64'">
<GenerateAssemblyInfo>False</GenerateAssemblyInfo>
<Optimize>True</Optimize>
<WarningLevel>8</WarningLevel>
<ResolveComReferenceSilent>True</ResolveComReferenceSilent>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release Installer|arm64'">
<GenerateAssemblyInfo>False</GenerateAssemblyInfo>
<Optimize>True</Optimize>
<WarningLevel>8</WarningLevel>
<ResolveComReferenceSilent>True</ResolveComReferenceSilent>
<SelfContained>true</SelfContained>
<RuntimeIdentifier>win-arm64</RuntimeIdentifier>
<PublishReadyToRun>true</PublishReadyToRun>
<CopyBuildOutputToPublishDirectory>true</CopyBuildOutputToPublishDirectory>
<CopyOutputSymbolsToPublishDirectory>false</CopyOutputSymbolsToPublishDirectory>
<RunCommand></RunCommand>
<RunArguments></RunArguments>
</PropertyGroup>
<ItemGroup>
<None Remove="buildenv.tmp" />
@@ -149,59 +135,16 @@
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" />
<PackageReference Include="Microsoft.NETCore.Platforms" />
<PackageReference Include="Microsoft.NETCore.Targets" />
<PackageReference Include="Microsoft.VisualStudio.TextTemplating.VSHost" />
<PackageReference Include="Microsoft.Web.WebView2" />
<PackageReference Include="Microsoft.Xaml.Behaviors.Wpf" />
<PackageReference Include="MySql.Data" />
<PackageReference Include="NETStandard.Library" />
<PackageReference Include="Newtonsoft.Json" />
<PackageReference Include="Newtonsoft.Json.Schema" />
<PackageReference Include="OpenCover" />
<PackageReference Include="Renci.SshNet.Async" />
<PackageReference Include="ReportGenerator" />
<PackageReference Include="runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl" />
<PackageReference Include="runtime.fedora.23-x64.runtime.native.System.Security.Cryptography.OpenSsl" />
<PackageReference Include="runtime.fedora.24-x64.runtime.native.System.Security.Cryptography.OpenSsl" />
<PackageReference Include="runtime.native.System" />
<PackageReference Include="runtime.native.System.IO.Compression" />
<PackageReference Include="runtime.native.System.Net.Http" />
<PackageReference Include="runtime.native.System.Security.Cryptography.OpenSsl" />
<PackageReference Include="runtime.opensuse.42.1-x64.runtime.native.System.Security.Cryptography.OpenSsl" />
<PackageReference Include="runtime.ubuntu.14.04-x64.runtime.native.System.Security.Cryptography.OpenSsl" />
<PackageReference Include="runtime.ubuntu.16.10-x64.runtime.native.System.Security.Cryptography.OpenSsl" />
<PackageReference Include="SSH.NET" />
<PackageReference Include="System.Collections.Immutable" />
<PackageReference Include="System.Configuration.ConfigurationManager" />
<PackageReference Include="System.Console" />
<PackageReference Include="System.Diagnostics.DiagnosticSource" />
<PackageReference Include="System.Diagnostics.EventLog" />
<PackageReference Include="System.DirectoryServices" />
<PackageReference Include="System.Drawing.Common" />
<PackageReference Include="System.Formats.Asn1" />
<PackageReference Include="System.IO.Pipelines" />
<PackageReference Include="System.Management" />
<PackageReference Include="System.Net.Primitives" />
<PackageReference Include="System.Net.Sockets" />
<PackageReference Include="System.Reflection.Emit" />
<PackageReference Include="System.Reflection.Emit.ILGeneration" />
<PackageReference Include="System.Reflection.Emit.Lightweight" />
<PackageReference Include="System.Reflection.Metadata" />
<PackageReference Include="System.Resources.ResourceManager" />
<PackageReference Include="System.Runtime.Extensions" />
<PackageReference Include="System.Security.AccessControl" />
<PackageReference Include="System.Security.Cryptography.Algorithms" />
<PackageReference Include="System.Security.Cryptography.Cng" />
<PackageReference Include="System.Security.Cryptography.OpenSsl" />
<PackageReference Include="System.Security.Cryptography.ProtectedData" />
<PackageReference Include="System.Security.Cryptography.X509Certificates" />
<PackageReference Include="System.Security.Permissions" />
<PackageReference Include="System.Security.Principal.Windows" />
<PackageReference Include="System.Text.Encoding.CodePages" />
<PackageReference Include="System.Text.Json" />
<PackageReference Include="System.Text.RegularExpressions" />
<PackageReference Include="System.Threading.Tasks.Extensions" />
<PackageReference Include="System.Windows.Extensions" />
<PackageReference Include="System.Xml.ReaderWriter" />
<PackageReference Include="VncSharpCore" />
<PackageReference Include="ZstdSharp.Port" />
</ItemGroup>
@@ -601,10 +544,10 @@
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<PackageReference Update="chromiumembeddedframework.runtime.win-x64" Version="143.0.9" />
<PackageReference Update="chromiumembeddedframework.runtime.win-x64" Version="144.0.12" />
</ItemGroup>
<ItemGroup Condition="'$(Platform)'=='arm64'">
<PackageReference Update="chromiumembeddedframework.runtime.win-arm64" Version="143.0.9" />
<PackageReference Update="chromiumembeddedframework.runtime.win-arm64" Version="144.0.12" />
</ItemGroup>
<ItemGroup>
<Service Include="{508349b6-6b84-4df5-91f0-309beebad82d}" />
@@ -634,4 +577,14 @@
<!-- Disable CET compatibility check in .NET 9 AppHost -->
<CETCompat>false</CETCompat>
</PropertyGroup>
<Target Name="PublishAfterBuild" AfterTargets="Build" Condition="'$(Configuration)' == 'Release Self-Contained'">
<Message Text="Publishing self-contained application for $(Platform)..." Importance="high" />
<MSBuild Projects="$(MSBuildProjectFile)" Targets="Publish" Properties="Configuration=$(Configuration);Platform=$(Platform);NoBuild=true;NoRestore=true" />
<PropertyGroup>
<TempBuildDir>$([System.IO.Path]::GetFullPath('$(OutputPath)'))</TempBuildDir>
</PropertyGroup>
<Message Text="Cleaning up temporary build directory: $(TempBuildDir)" Importance="high" />
<RemoveDir Directories="$(TempBuildDir)" />
<RemoveDir Directories="$(ProjectDir)bin\x64\Release Self-Contained" />
</Target>
</Project>

View File

@@ -73,3 +73,35 @@ Connections
- Move Up
* - Ctrl+Down
- Move Down
Sessions
========
.. list-table::
:widths: 30 70
:header-rows: 1
* - Keybinding
- Action
* - Ctrl+Right
- Next Session/Tab
* - Ctrl+Left
- Previous Session/Tab
* - Ctrl+1
- Jump to Session 1
* - Ctrl+2
- Jump to Session 2
* - Ctrl+3
- Jump to Session 3
* - Ctrl+4
- Jump to Session 4
* - Ctrl+5
- Jump to Session 5
* - Ctrl+6
- Jump to Session 6
* - Ctrl+7
- Jump to Session 7
* - Ctrl+8
- Jump to Session 8
* - Ctrl+9
- Jump to Session 9

View File

@@ -37,7 +37,7 @@ Popups settings
.. figure:: /images/notifications_popup.png
When items are selected here you will recieve a popup on the error that occurrs
When items are selected here you will receive a popup on the error that occurs
based on level chosen in settings here.
This can be useful if you do not want to use the notification area
and only get a popup if error occurs. (**default**: all off)
and only get a popup if error occurs. (**default**: Errors enabled, Debug/Info/Warning disabled)

View File

@@ -2,12 +2,14 @@
using System.Management;
using System.Collections;
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace CustomActions
{
public class InstalledWindowsUpdateChecker
{
private readonly ManagementScope _managementScope;
private static readonly Regex KbPattern = new Regex(@"^(KB)?\d+$", RegexOptions.IgnoreCase | RegexOptions.Compiled);
public InstalledWindowsUpdateChecker()
{
@@ -61,12 +63,45 @@ namespace CustomActions
var counter = 0;
foreach (var kb in kbList)
{
var sanitizedKb = SanitizeKbId(kb);
if (string.IsNullOrEmpty(sanitizedKb))
continue; // Skip invalid KB IDs
if (counter > 0)
whereClause += " OR ";
whereClause += $"HotFixID='{kb}'";
whereClause += $"HotFixID='{sanitizedKb}'";
counter++;
}
return whereClause;
}
/// <summary>
/// Sanitizes a KB ID to prevent WQL injection attacks.
/// KB IDs must match the pattern: optional "KB" prefix followed by digits,
/// or just digits. Any other characters are rejected.
/// </summary>
/// <param name="kbId">The KB ID to sanitize</param>
/// <returns>The sanitized KB ID, or empty string if invalid</returns>
private string SanitizeKbId(string kbId)
{
if (string.IsNullOrWhiteSpace(kbId))
return string.Empty;
// KB IDs should match the pattern: optional KB prefix followed by digits
// (e.g., KB1234567 or 1234567)
// Trim whitespace and check if it matches the expected pattern
var trimmedKb = kbId.Trim();
if (!KbPattern.IsMatch(trimmedKb))
return string.Empty;
// Normalize to uppercase
var normalizedKb = trimmedKb.ToUpperInvariant();
// Ensure KB prefix is present (Win32_QuickFixEngineering always uses the KB prefix)
if (!normalizedKb.StartsWith("KB"))
normalizedKb = "KB" + normalizedKb;
return normalizedKb;
}
}
}

View File

@@ -0,0 +1,117 @@
using System;
using System.Linq;
using mRemoteNG.App;
using mRemoteNG.Connection;
using mRemoteNG.Connection.Protocol;
using mRemoteNG.Messages;
using mRemoteNG.Resources.Language;
using NUnit.Framework;
namespace mRemoteNGTests.Connection
{
[TestFixture]
public class ConnectionInitiatorTests
{
private ConnectionInitiator _connectionInitiator;
private MessageCollector _messageCollector;
[SetUp]
public void Setup()
{
_connectionInitiator = new ConnectionInitiator();
_messageCollector = Runtime.MessageCollector;
_messageCollector.ClearMessages();
}
[TearDown]
public void Teardown()
{
_messageCollector?.ClearMessages();
}
[Test]
public void OpenConnection_WithEmptyHostname_AddsErrorMessage()
{
// Arrange
var connectionInfo = new ConnectionInfo
{
Name = "Test Connection",
Hostname = "", // Empty hostname
Protocol = ProtocolType.RDP // RDP doesn't support blank hostname
};
// Act
_connectionInitiator.OpenConnection(connectionInfo);
// Assert - poll for message with timeout
var foundMessage = WaitForMessage(MessageClass.ErrorMsg, timeoutMs: 1000);
Assert.That(foundMessage, Is.Not.Null, "Expected an error message to be added");
Assert.That(foundMessage.Text, Is.EqualTo(Language.ConnectionOpenFailedNoHostname));
}
[Test]
public void OpenConnection_WithNullHostname_AddsErrorMessage()
{
// Arrange
var connectionInfo = new ConnectionInfo
{
Name = "Test Connection",
Hostname = null, // Null hostname
Protocol = ProtocolType.SSH2 // SSH doesn't support blank hostname
};
// Act
_connectionInitiator.OpenConnection(connectionInfo);
// Assert - poll for message with timeout
var foundMessage = WaitForMessage(MessageClass.ErrorMsg, timeoutMs: 1000);
Assert.That(foundMessage, Is.Not.Null, "Expected an error message to be added");
Assert.That(foundMessage.Text, Is.EqualTo(Language.ConnectionOpenFailedNoHostname));
}
[Test]
public void OpenConnection_WithValidHostname_DoesNotAddHostnameError()
{
// Arrange
var connectionInfo = new ConnectionInfo
{
Name = "Test Connection",
Hostname = "192.168.1.1", // Valid hostname
Protocol = ProtocolType.RDP
};
// Act
_connectionInitiator.OpenConnection(connectionInfo);
// Give a moment for any potential async operations
System.Threading.Thread.Sleep(200);
// Assert
var hostnameErrors = _messageCollector.Messages
.Where(m => m.Text == Language.ConnectionOpenFailedNoHostname)
.ToList();
Assert.That(hostnameErrors, Is.Empty,
"Should not have hostname error when hostname is provided");
}
/// <summary>
/// Polls the message collector for a message of the specified class
/// </summary>
private IMessage WaitForMessage(MessageClass messageClass, int timeoutMs = 1000)
{
var startTime = DateTime.Now;
while ((DateTime.Now - startTime).TotalMilliseconds < timeoutMs)
{
var message = _messageCollector.Messages
.FirstOrDefault(m => m.Class == messageClass);
if (message != null)
return message;
System.Threading.Thread.Sleep(50); // Poll every 50ms
}
return null;
}
}
}

View File

@@ -0,0 +1,204 @@
using System;
using System.Reflection;
using mRemoteNG.Connection;
using mRemoteNG.Connection.Protocol.AnyDesk;
using NUnit.Framework;
namespace mRemoteNGTests.Connection.Protocol;
public class ProtocolAnyDeskTests
{
private ProtocolAnyDesk _protocolAnyDesk;
private ConnectionInfo _connectionInfo;
[SetUp]
public void Setup()
{
_connectionInfo = new ConnectionInfo();
_protocolAnyDesk = new ProtocolAnyDesk(_connectionInfo);
}
[TearDown]
public void Teardown()
{
_protocolAnyDesk?.Close();
_protocolAnyDesk = null;
_connectionInfo = null;
}
#region IsValidAnydeskId Tests
[Test]
public void IsValidAnydeskId_NumericId_ReturnsTrue()
{
// Valid numeric AnyDesk ID
bool result = InvokeIsValidAnydeskId("123456789");
Assert.That(result, Is.True);
}
[Test]
public void IsValidAnydeskId_AlphanumericWithAt_ReturnsTrue()
{
// Valid alias format: alias@ad
bool result = InvokeIsValidAnydeskId("myalias@ad");
Assert.That(result, Is.True);
}
[Test]
public void IsValidAnydeskId_WithHyphen_ReturnsTrue()
{
// Valid ID with hyphen
bool result = InvokeIsValidAnydeskId("my-alias@ad");
Assert.That(result, Is.True);
}
[Test]
public void IsValidAnydeskId_WithUnderscore_ReturnsTrue()
{
// Valid ID with underscore
bool result = InvokeIsValidAnydeskId("my_alias@ad");
Assert.That(result, Is.True);
}
[Test]
public void IsValidAnydeskId_WithDot_ReturnsTrue()
{
// Valid ID with dot
bool result = InvokeIsValidAnydeskId("alias.name@ad");
Assert.That(result, Is.True);
}
[Test]
public void IsValidAnydeskId_WithSemicolon_ReturnsFalse()
{
// Command injection attempt with semicolon
bool result = InvokeIsValidAnydeskId("123456789; calc.exe");
Assert.That(result, Is.False);
}
[Test]
public void IsValidAnydeskId_WithAmpersand_ReturnsFalse()
{
// Command injection attempt with ampersand
bool result = InvokeIsValidAnydeskId("123456789 & calc.exe");
Assert.That(result, Is.False);
}
[Test]
public void IsValidAnydeskId_WithPipe_ReturnsFalse()
{
// Command injection attempt with pipe
bool result = InvokeIsValidAnydeskId("123456789 | calc.exe");
Assert.That(result, Is.False);
}
[Test]
public void IsValidAnydeskId_WithRedirection_ReturnsFalse()
{
// Command injection attempt with redirection
bool result = InvokeIsValidAnydeskId("123456789 > output.txt");
Assert.That(result, Is.False);
}
[Test]
public void IsValidAnydeskId_WithBacktick_ReturnsFalse()
{
// PowerShell escape character
bool result = InvokeIsValidAnydeskId("123456789`calc");
Assert.That(result, Is.False);
}
[Test]
public void IsValidAnydeskId_WithDollarSign_ReturnsFalse()
{
// PowerShell variable indicator
bool result = InvokeIsValidAnydeskId("123456789$var");
Assert.That(result, Is.False);
}
[Test]
public void IsValidAnydeskId_WithParentheses_ReturnsFalse()
{
// Command substitution
bool result = InvokeIsValidAnydeskId("123456789(calc)");
Assert.That(result, Is.False);
}
[Test]
public void IsValidAnydeskId_WithNewline_ReturnsFalse()
{
// Newline injection
bool result = InvokeIsValidAnydeskId("123456789\ncalc");
Assert.That(result, Is.False);
}
[Test]
public void IsValidAnydeskId_WithCarriageReturn_ReturnsFalse()
{
// Carriage return injection
bool result = InvokeIsValidAnydeskId("123456789\rcalc");
Assert.That(result, Is.False);
}
[Test]
public void IsValidAnydeskId_WithQuotes_ReturnsFalse()
{
// Quote escape attempt
bool result = InvokeIsValidAnydeskId("123456789\"calc\"");
Assert.That(result, Is.False);
}
[Test]
public void IsValidAnydeskId_WithSingleQuotes_ReturnsFalse()
{
// Single quote escape attempt
bool result = InvokeIsValidAnydeskId("123456789'calc'");
Assert.That(result, Is.False);
}
[Test]
public void IsValidAnydeskId_EmptyString_ReturnsFalse()
{
// Empty string
bool result = InvokeIsValidAnydeskId("");
Assert.That(result, Is.False);
}
[Test]
public void IsValidAnydeskId_Whitespace_ReturnsFalse()
{
// Only whitespace
bool result = InvokeIsValidAnydeskId(" ");
Assert.That(result, Is.False);
}
[Test]
public void IsValidAnydeskId_Null_ReturnsFalse()
{
// Null string
bool result = InvokeIsValidAnydeskId(null);
Assert.That(result, Is.False);
}
#endregion
#region Helper Methods
/// <summary>
/// Uses reflection to invoke the private IsValidAnydeskId method
/// </summary>
private bool InvokeIsValidAnydeskId(string anydeskId)
{
var method = typeof(ProtocolAnyDesk).GetMethod("IsValidAnydeskId",
BindingFlags.NonPublic | BindingFlags.Instance);
if (method == null)
{
throw new Exception("IsValidAnydeskId method not found. The method may have been renamed or removed.");
}
return (bool)method.Invoke(_protocolAnydesk, new object[] { anydeskId });
}
#endregion
}

View File

@@ -0,0 +1,208 @@
using NUnit.Framework;
using System;
using System.Reflection;
namespace mRemoteNGTests.Installer
{
[TestFixture]
public class InstalledWindowsUpdateCheckerTests
{
private CustomActions.InstalledWindowsUpdateChecker _checker;
private MethodInfo _sanitizeKbIdMethod;
private MethodInfo _buildWhereClauseMethod;
[SetUp]
public void Setup()
{
_checker = new CustomActions.InstalledWindowsUpdateChecker();
// Use reflection to access private methods for testing
var type = typeof(CustomActions.InstalledWindowsUpdateChecker);
_sanitizeKbIdMethod = type.GetMethod("SanitizeKbId", BindingFlags.NonPublic | BindingFlags.Instance);
_buildWhereClauseMethod = type.GetMethod("BuildWhereClauseFromKbList", BindingFlags.NonPublic | BindingFlags.Instance);
}
#region SanitizeKbId Tests
[Test]
public void SanitizeKbId_ValidKbWithPrefix_ReturnsUppercased()
{
var result = InvokeSanitizeKbId("KB1234567");
Assert.That(result, Is.EqualTo("KB1234567"));
}
[Test]
public void SanitizeKbId_ValidKbLowercase_ReturnsUppercased()
{
var result = InvokeSanitizeKbId("kb1234567");
Assert.That(result, Is.EqualTo("KB1234567"));
}
[Test]
public void SanitizeKbId_ValidKbMixedCase_ReturnsUppercased()
{
var result = InvokeSanitizeKbId("Kb1234567");
Assert.That(result, Is.EqualTo("KB1234567"));
}
[Test]
public void SanitizeKbId_ValidNumberOnly_ReturnsWithKbPrefix()
{
var result = InvokeSanitizeKbId("1234567");
Assert.That(result, Is.EqualTo("KB1234567"));
}
[Test]
public void SanitizeKbId_WithWhitespace_ReturnsTrimmedAndUppercased()
{
var result = InvokeSanitizeKbId(" KB1234567 ");
Assert.That(result, Is.EqualTo("KB1234567"));
}
[Test]
public void SanitizeKbId_SqlInjectionAttempt_ReturnsEmpty()
{
var result = InvokeSanitizeKbId("KB1234' OR '1'='1");
Assert.That(result, Is.Empty);
}
[Test]
public void SanitizeKbId_WqlInjectionWithSemicolon_ReturnsEmpty()
{
var result = InvokeSanitizeKbId("KB1234; DROP TABLE");
Assert.That(result, Is.Empty);
}
[Test]
public void SanitizeKbId_WithSpecialCharacters_ReturnsEmpty()
{
var result = InvokeSanitizeKbId("KB1234@#$");
Assert.That(result, Is.Empty);
}
[Test]
public void SanitizeKbId_NullInput_ReturnsEmpty()
{
var result = InvokeSanitizeKbId(null);
Assert.That(result, Is.Empty);
}
[Test]
public void SanitizeKbId_EmptyString_ReturnsEmpty()
{
var result = InvokeSanitizeKbId("");
Assert.That(result, Is.Empty);
}
[Test]
public void SanitizeKbId_WhitespaceOnly_ReturnsEmpty()
{
var result = InvokeSanitizeKbId(" ");
Assert.That(result, Is.Empty);
}
[Test]
public void SanitizeKbId_OnlyKbPrefix_ReturnsEmpty()
{
var result = InvokeSanitizeKbId("KB");
Assert.That(result, Is.Empty);
}
[Test]
public void SanitizeKbId_WithDashes_ReturnsEmpty()
{
var result = InvokeSanitizeKbId("KB-1234567");
Assert.That(result, Is.Empty);
}
[Test]
public void SanitizeKbId_WithUnderscores_ReturnsEmpty()
{
var result = InvokeSanitizeKbId("KB_1234567");
Assert.That(result, Is.Empty);
}
#endregion
#region BuildWhereClauseFromKbList Tests
[Test]
public void BuildWhereClause_SingleValidKb_ReturnsCorrectClause()
{
var result = InvokeBuildWhereClause(new[] { "KB1234567" });
Assert.That(result, Is.EqualTo("HotFixID='KB1234567'"));
}
[Test]
public void BuildWhereClause_MultipleValidKbs_ReturnsOrClause()
{
var result = InvokeBuildWhereClause(new[] { "KB1234567", "KB7654321" });
Assert.That(result, Is.EqualTo("HotFixID='KB1234567' OR HotFixID='KB7654321'"));
}
[Test]
public void BuildWhereClause_InvalidKb_SkipsInvalid()
{
var result = InvokeBuildWhereClause(new[] { "KB1234567", "KB1234'; DROP--", "KB7654321" });
Assert.That(result, Is.EqualTo("HotFixID='KB1234567' OR HotFixID='KB7654321'"));
}
[Test]
public void BuildWhereClause_AllInvalidKbs_ReturnsEmpty()
{
var result = InvokeBuildWhereClause(new[] { "'; DROP TABLE", "OR 1=1--" });
Assert.That(result, Is.Empty);
}
[Test]
public void BuildWhereClause_EmptyList_ReturnsEmpty()
{
var result = InvokeBuildWhereClause(Array.Empty<string>());
Assert.That(result, Is.Empty);
}
[Test]
public void BuildWhereClause_NullValues_SkipsNulls()
{
var result = InvokeBuildWhereClause(new[] { "KB1234567", null, "KB7654321" });
Assert.That(result, Is.EqualTo("HotFixID='KB1234567' OR HotFixID='KB7654321'"));
}
[Test]
public void BuildWhereClause_MixedCaseKbs_NormalizesToUppercase()
{
var result = InvokeBuildWhereClause(new[] { "kb1234567", "KB7654321" });
Assert.That(result, Is.EqualTo("HotFixID='KB1234567' OR HotFixID='KB7654321'"));
}
[Test]
public void BuildWhereClause_DigitOnlyKb_AddsKbPrefix()
{
var result = InvokeBuildWhereClause(new[] { "1234567" });
Assert.That(result, Is.EqualTo("HotFixID='KB1234567'"));
}
[Test]
public void BuildWhereClause_MixedPrefixedAndDigitOnly_NormalizesAll()
{
var result = InvokeBuildWhereClause(new[] { "1234567", "KB7654321", "kb9999999" });
Assert.That(result, Is.EqualTo("HotFixID='KB1234567' OR HotFixID='KB7654321' OR HotFixID='KB9999999'"));
}
#endregion
#region Helper Methods
private string InvokeSanitizeKbId(string input)
{
return (string)_sanitizeKbIdMethod.Invoke(_checker, new object[] { input });
}
private string InvokeBuildWhereClause(string[] kbList)
{
return (string)_buildWhereClauseMethod.Invoke(_checker, new object[] { kbList });
}
#endregion
}
}

View File

@@ -69,6 +69,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\mRemoteNG\mRemoteNG.csproj" />
<ProjectReference Include="..\mRemoteNGInstaller\CustomActions\CustomActions.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Update="Properties\Resources.Designer.cs">