mirror of
https://github.com/mRemoteNG/mRemoteNG.git
synced 2026-02-17 22:11:48 +08:00
Compare commits
102 Commits
copilot/fi
...
copilot/ad
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9ba3cf0727 | ||
|
|
71911bba7b | ||
|
|
e9f94cbe31 | ||
|
|
db1496d4a2 | ||
|
|
a8b12c9ba1 | ||
|
|
a2b408e537 | ||
|
|
a871624ab7 | ||
|
|
e40a800bc4 | ||
|
|
48cb1ce770 | ||
|
|
6733d758aa | ||
|
|
f78b9bf51c | ||
|
|
213ea6a4d3 | ||
|
|
ac3d7e6366 | ||
|
|
9fae2e066e | ||
|
|
cf66e84d31 | ||
|
|
f7326aff62 | ||
|
|
d4bca6b03d | ||
|
|
1d86015f9d | ||
|
|
5efcc653eb | ||
|
|
333588e101 | ||
|
|
4046681fc5 | ||
|
|
91c7df22b2 | ||
|
|
173b208eb1 | ||
|
|
50de37c3a4 | ||
|
|
380e91de07 | ||
|
|
ba97933f33 | ||
|
|
539b761199 | ||
|
|
aaff6e4548 | ||
|
|
a1e3b34580 | ||
|
|
276585e379 | ||
|
|
9242dc0faf | ||
|
|
14d08d8d62 | ||
|
|
421d8eb581 | ||
|
|
aed0006f1d | ||
|
|
86d986a633 | ||
|
|
5163aeb4d2 | ||
|
|
a103939c64 | ||
|
|
bcb8e05698 | ||
|
|
2e74313f07 | ||
|
|
c9791454ec | ||
|
|
b1c1696acb | ||
|
|
ab668ac677 | ||
|
|
1777c4840a | ||
|
|
90fcd672d8 | ||
|
|
4226396cbf | ||
|
|
3591ca0f4c | ||
|
|
09cbcccf30 | ||
|
|
4e36b5666e | ||
|
|
308253a325 | ||
|
|
30bb4016b4 | ||
|
|
e616ae16e1 | ||
|
|
469528a07a | ||
|
|
c7f831e9f9 | ||
|
|
dd9922be45 | ||
|
|
d0a468c22b | ||
|
|
ff5dbc88fe | ||
|
|
3a4ae9b098 | ||
|
|
2de24e534c | ||
|
|
59412a65e1 | ||
|
|
adedb6962f | ||
|
|
d3fa608ae9 | ||
|
|
3159903875 | ||
|
|
31c28c4917 | ||
|
|
2c13f7c3a7 | ||
|
|
725ee92147 | ||
|
|
14406f79a2 | ||
|
|
3e202c3a19 | ||
|
|
412c727e4c | ||
|
|
31e7b9e443 | ||
|
|
a18e292765 | ||
|
|
258ea87f90 | ||
|
|
fd9eabe1e6 | ||
|
|
a9dd06df45 | ||
|
|
dfc24b0cb2 | ||
|
|
63f5325f29 | ||
|
|
6f7949214b | ||
|
|
6cfec060a0 | ||
|
|
db733424ca | ||
|
|
49eab4d377 | ||
|
|
53e5396031 | ||
|
|
99d01130bf | ||
|
|
62ce6cd6e7 | ||
|
|
35c66b0e4a | ||
|
|
dafc05dc42 | ||
|
|
458a05ea5f | ||
|
|
38acc1e960 | ||
|
|
f83209a2b9 | ||
|
|
9d1546c8b7 | ||
|
|
dca2517cf0 | ||
|
|
ff54ca9015 | ||
|
|
76cb0a1e0b | ||
|
|
c683854678 | ||
|
|
e9d0a8aa69 | ||
|
|
30cb1de711 | ||
|
|
699b93e175 | ||
|
|
c7df6f3715 | ||
|
|
f1d1a19779 | ||
|
|
469ca48592 | ||
|
|
208ce663b2 | ||
|
|
843243c75e | ||
|
|
b7ed5a300d | ||
|
|
415a649a76 |
242
.github/copilot-instructions.md
vendored
Normal file
242
.github/copilot-instructions.md
vendored
Normal 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
|
||||
177
.github/workflows/Build_mR-NB.yml
vendored
177
.github/workflows/Build_mR-NB.yml
vendored
@@ -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
167
DEPLOYMENT_OPTIONS.md
Normal 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
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -465,4 +465,4 @@ namespace mRemoteNG.Connection
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -2559,4 +2559,16 @@ Nightly Channel includes Alphas, Betas & Release Candidates.</value>
|
||||
<data name="VaultOpenbaoSecretEngineSSHOTP" xml:space="preserve">
|
||||
<value>SSH engine OTP mode</value>
|
||||
</data>
|
||||
<data name="_Sessions" xml:space="preserve">
|
||||
<value>&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>
|
||||
@@ -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"]));
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
20
mRemoteNG/UI/Forms/FrmInputBox.Designer.cs
generated
20
mRemoteNG/UI/Forms/FrmInputBox.Designer.cs
generated
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
12
mRemoteNG/UI/Forms/frmMain.Designer.cs
generated
12
mRemoteNG/UI/Forms/frmMain.Designer.cs
generated
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
150
mRemoteNG/UI/Menu/msMain/SessionsMenu.cs
Normal file
150
mRemoteNG/UI/Menu/msMain/SessionsMenu.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
117
mRemoteNGTests/Connection/ConnectionInitiatorTests.cs
Normal file
117
mRemoteNGTests/Connection/ConnectionInitiatorTests.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
204
mRemoteNGTests/Connection/Protocol/ProtocolAnydeskTests.cs
Normal file
204
mRemoteNGTests/Connection/Protocol/ProtocolAnydeskTests.cs
Normal 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
|
||||
}
|
||||
208
mRemoteNGTests/Installer/InstalledWindowsUpdateCheckerTests.cs
Normal file
208
mRemoteNGTests/Installer/InstalledWindowsUpdateCheckerTests.cs
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user