mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-07-10 01:05:08 +03:00
Compare commits
17 Commits
9c831dc59b
...
webrtc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c19698fa52 | ||
|
|
1040df0399 | ||
|
|
03cbf609f6 | ||
|
|
0e0ec5a551 | ||
|
|
c50e7d078d | ||
|
|
3d6b06e854 | ||
|
|
472c4fc03a | ||
|
|
9f8f726f12 | ||
|
|
701a9c6cdc | ||
|
|
0d40cf2101 | ||
|
|
dd265dadd7 | ||
|
|
fe5a8cb2ad | ||
|
|
b6caa1a7b2 | ||
|
|
55c9707639 | ||
|
|
d8808baa83 | ||
|
|
1978020d27 | ||
|
|
0e4b91b8d7 |
@@ -47,7 +47,7 @@ screencapturekit = ["cpal/screencapturekit"]
|
||||
[dependencies]
|
||||
async-trait = "0.1"
|
||||
scrap = { path = "libs/scrap", features = ["wayland"] }
|
||||
hbb_common = { path = "libs/hbb_common" }
|
||||
hbb_common = { path = "libs/hbb_common", features = ["webrtc"] }
|
||||
serde_derive = "1.0"
|
||||
serde = "1.0"
|
||||
serde_json = "1.0"
|
||||
|
||||
@@ -16,6 +16,12 @@ import 'package:get/get.dart';
|
||||
|
||||
bool isEditOsPassword = false;
|
||||
|
||||
// macOS privacy mode blacks out all online displays, so switching the remote
|
||||
// display does not weaken the local privacy protection.
|
||||
bool allowDisplaySwitchInPrivacyMode(PeerInfo pi) {
|
||||
return pi.platform == kPeerPlatformMacOS;
|
||||
}
|
||||
|
||||
class TTextMenu {
|
||||
final Widget child;
|
||||
final VoidCallback? onPressed;
|
||||
@@ -684,8 +690,9 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
|
||||
child: Text(translate('Lock after session end'))));
|
||||
}
|
||||
|
||||
final privacyModeState = PrivacyModeState.find(id);
|
||||
if (pi.isSupportMultiDisplay &&
|
||||
PrivacyModeState.find(id).isEmpty &&
|
||||
(privacyModeState.isEmpty || allowDisplaySwitchInPrivacyMode(pi)) &&
|
||||
pi.displaysCount.value > 1 &&
|
||||
bind.mainGetUserDefaultOption(key: kKeyShowMonitorsToolbar) == 'Y') {
|
||||
final value =
|
||||
@@ -776,7 +783,8 @@ List<TToggleMenu> toolbarPrivacyMode(
|
||||
onChanged: enabled
|
||||
? (value) {
|
||||
if (value == null) return;
|
||||
if (ffiModel.pi.currentDisplay != 0 &&
|
||||
if (!allowDisplaySwitchInPrivacyMode(pi) &&
|
||||
ffiModel.pi.currentDisplay != 0 &&
|
||||
ffiModel.pi.currentDisplay != kAllDisplayValue) {
|
||||
msgBox(
|
||||
sessionId,
|
||||
|
||||
@@ -376,7 +376,8 @@ class _RemoteToolbarState extends State<RemoteToolbar> {
|
||||
}
|
||||
|
||||
toolbarItems.add(Obx(() {
|
||||
if (PrivacyModeState.find(widget.id).isEmpty &&
|
||||
if ((PrivacyModeState.find(widget.id).isEmpty ||
|
||||
allowDisplaySwitchInPrivacyMode(pi)) &&
|
||||
pi.displaysCount.value > 1) {
|
||||
return _MonitorMenu(
|
||||
id: widget.id,
|
||||
|
||||
@@ -593,13 +593,13 @@ class _DesktopTabState extends State<DesktopTab>
|
||||
}
|
||||
|
||||
Widget _buildBar() {
|
||||
final isIncomingHomePage = bind.isIncomingOnly() && isInHomePage();
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
// custom double tap handler
|
||||
onTap: !(bind.isIncomingOnly() && isInHomePage()) &&
|
||||
showMaximize
|
||||
onTap: !isIncomingHomePage && showMaximize
|
||||
? () {
|
||||
final current = DateTime.now().millisecondsSinceEpoch;
|
||||
final elapsed = current - _lastClickTime;
|
||||
@@ -610,7 +610,7 @@ class _DesktopTabState extends State<DesktopTab>
|
||||
.then((value) => stateGlobal.setMaximized(value));
|
||||
}
|
||||
}
|
||||
: null,
|
||||
: (isIncomingHomePage ? () {} : null), // Keep tap recognizer for Windows touch.
|
||||
onPanStart: (_) => startDragging(isMainWindow),
|
||||
onPanCancel: () {
|
||||
// We want to disable dragging of the tab area in the tab bar.
|
||||
|
||||
Submodule libs/hbb_common updated: 42af0f0aed...5a78ec4230
@@ -31,17 +31,17 @@ LExit:
|
||||
return WcaFinalize(er);
|
||||
}
|
||||
|
||||
// Helper function to safely delete a file or directory using handle-based deletion.
|
||||
// This avoids TOCTOU (Time-Of-Check-Time-Of-Use) race conditions.
|
||||
// Helper function to safely delete a file using handle-based deletion.
|
||||
// Directories are refused after opening the handle.
|
||||
BOOL SafeDeleteItem(LPCWSTR fullPath)
|
||||
{
|
||||
// Open the file/directory with DELETE access and FILE_FLAG_OPEN_REPARSE_POINT
|
||||
// Open the file/directory with delete and attribute-read access plus FILE_FLAG_OPEN_REPARSE_POINT
|
||||
// to prevent following symlinks.
|
||||
// Use shared access to allow deletion even when other processes have the file open.
|
||||
DWORD flags = FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT;
|
||||
HANDLE hFile = CreateFileW(
|
||||
fullPath,
|
||||
DELETE,
|
||||
DELETE | FILE_READ_ATTRIBUTES,
|
||||
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, // Allow shared access
|
||||
NULL,
|
||||
OPEN_EXISTING,
|
||||
@@ -55,6 +55,21 @@ BOOL SafeDeleteItem(LPCWSTR fullPath)
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
BY_HANDLE_FILE_INFORMATION fileInfo;
|
||||
if (FALSE == GetFileInformationByHandle(hFile, &fileInfo))
|
||||
{
|
||||
WcaLog(LOGMSG_STANDARD, "SafeDeleteItem: Failed to inspect '%ls'. Error: %lu", fullPath, GetLastError());
|
||||
CloseHandle(hFile);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
if (fileInfo.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
|
||||
{
|
||||
WcaLog(LOGMSG_STANDARD, "SafeDeleteItem: Refusing to delete directory '%ls'.", fullPath);
|
||||
CloseHandle(hFile);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
// Use SetFileInformationByHandle to mark for deletion.
|
||||
// The file will be deleted when the handle is closed.
|
||||
FILE_DISPOSITION_INFO dispInfo;
|
||||
@@ -77,98 +92,74 @@ BOOL SafeDeleteItem(LPCWSTR fullPath)
|
||||
return result;
|
||||
}
|
||||
|
||||
// Helper function to recursively delete a directory's contents with detailed logging.
|
||||
void RecursiveDelete(LPCWSTR path)
|
||||
BOOL PathEndsWithSlash(LPCWSTR path)
|
||||
{
|
||||
// Ensure the path is not empty or null.
|
||||
if (path == NULL || path[0] == L'\0')
|
||||
size_t length = 0;
|
||||
HRESULT hr = StringCchLengthW(path, MAX_PATH, &length);
|
||||
if (FAILED(hr) || length == 0)
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
WCHAR last = path[length - 1];
|
||||
return last == L'\\' || last == L'/';
|
||||
}
|
||||
|
||||
void ClearReadOnlyAttribute(LPCWSTR fullPath, DWORD attributes)
|
||||
{
|
||||
if (!(attributes & FILE_ATTRIBUTE_READONLY))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Extra safety: never operate directly on a root path.
|
||||
if (PathIsRootW(path))
|
||||
DWORD writableAttributes = attributes & ~FILE_ATTRIBUTE_READONLY;
|
||||
if (writableAttributes == 0)
|
||||
{
|
||||
WcaLog(LOGMSG_STANDARD, "RecursiveDelete: refusing to operate on root path '%ls'.", path);
|
||||
writableAttributes = FILE_ATTRIBUTE_NORMAL;
|
||||
}
|
||||
|
||||
if (SetFileAttributesW(fullPath, writableAttributes))
|
||||
{
|
||||
WcaLog(LOGMSG_STANDARD, "Runtime cleanup cleared read-only attribute for '%ls'.", fullPath);
|
||||
return;
|
||||
}
|
||||
|
||||
// MAX_PATH is enough here since the installer should not be using longer paths.
|
||||
// No need to handle extended-length paths (\\?\) in this context.
|
||||
WCHAR searchPath[MAX_PATH];
|
||||
HRESULT hr = StringCchPrintfW(searchPath, MAX_PATH, L"%s\\*", path);
|
||||
if (FAILED(hr)) {
|
||||
WcaLog(LOGMSG_STANDARD, "RecursiveDelete: Path too long to enumerate: %ls", path);
|
||||
return;
|
||||
WcaLog(LOGMSG_STANDARD, "Runtime cleanup failed to clear read-only attribute for '%ls'. Error: %lu", fullPath, GetLastError());
|
||||
}
|
||||
|
||||
BOOL DeleteRuntimeGeneratedFile(LPCWSTR installFolder, LPCWSTR fileName)
|
||||
{
|
||||
WCHAR fullPath[MAX_PATH];
|
||||
LPCWSTR separator = PathEndsWithSlash(installFolder) ? L"" : L"\\";
|
||||
HRESULT hr = StringCchPrintfW(fullPath, MAX_PATH, L"%s%s%s", installFolder, separator, fileName);
|
||||
if (FAILED(hr))
|
||||
{
|
||||
WcaLog(LOGMSG_STANDARD, "Runtime cleanup path is too long for '%ls'.", fileName);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
WIN32_FIND_DATAW findData;
|
||||
HANDLE hFind = FindFirstFileW(searchPath, &findData);
|
||||
|
||||
if (hFind == INVALID_HANDLE_VALUE)
|
||||
DWORD attributes = GetFileAttributesW(fullPath);
|
||||
if (attributes == INVALID_FILE_ATTRIBUTES)
|
||||
{
|
||||
// This can happen if the directory is empty or doesn't exist, which is not an error in our case.
|
||||
WcaLog(LOGMSG_STANDARD, "RecursiveDelete: Failed to enumerate directory '%ls'. It may be missing or inaccessible. Error: %lu", path, GetLastError());
|
||||
return;
|
||||
DWORD error = GetLastError();
|
||||
if (error == ERROR_FILE_NOT_FOUND || error == ERROR_PATH_NOT_FOUND)
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
WcaLog(LOGMSG_STANDARD, "Runtime cleanup cannot stat '%ls'. Error: %lu", fullPath, error);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
do
|
||||
if (attributes & FILE_ATTRIBUTE_DIRECTORY)
|
||||
{
|
||||
// Skip '.' and '..' directories.
|
||||
if (wcscmp(findData.cFileName, L".") == 0 || wcscmp(findData.cFileName, L"..") == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// MAX_PATH is enough here since the installer should not be using longer paths.
|
||||
// No need to handle extended-length paths (\\?\) in this context.
|
||||
WCHAR fullPath[MAX_PATH];
|
||||
hr = StringCchPrintfW(fullPath, MAX_PATH, L"%s\\%s", path, findData.cFileName);
|
||||
if (FAILED(hr)) {
|
||||
WcaLog(LOGMSG_STANDARD, "RecursiveDelete: Path too long for item '%ls' in '%ls', skipping.", findData.cFileName, path);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Before acting, ensure the read-only attribute is not set.
|
||||
if (findData.dwFileAttributes & FILE_ATTRIBUTE_READONLY)
|
||||
{
|
||||
if (FALSE == SetFileAttributesW(fullPath, findData.dwFileAttributes & ~FILE_ATTRIBUTE_READONLY))
|
||||
{
|
||||
WcaLog(LOGMSG_STANDARD, "RecursiveDelete: Failed to remove read-only attribute. Error: %lu", GetLastError());
|
||||
}
|
||||
}
|
||||
|
||||
if (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
|
||||
{
|
||||
// Check for reparse points (symlinks/junctions) to prevent directory traversal attacks.
|
||||
// Do not follow reparse points, only remove the link itself.
|
||||
if (findData.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT)
|
||||
{
|
||||
WcaLog(LOGMSG_STANDARD, "RecursiveDelete: Not recursing into reparse point (symlink/junction), deleting link itself: %ls", fullPath);
|
||||
SafeDeleteItem(fullPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Recursively delete directory contents first
|
||||
RecursiveDelete(fullPath);
|
||||
// Then delete the directory itself
|
||||
SafeDeleteItem(fullPath);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Delete file using safe handle-based deletion
|
||||
SafeDeleteItem(fullPath);
|
||||
}
|
||||
} while (FindNextFileW(hFind, &findData) != 0);
|
||||
|
||||
DWORD lastError = GetLastError();
|
||||
if (lastError != ERROR_NO_MORE_FILES)
|
||||
{
|
||||
WcaLog(LOGMSG_STANDARD, "RecursiveDelete: FindNextFileW failed with error %lu", lastError);
|
||||
WcaLog(LOGMSG_STANDARD, "Runtime cleanup skipped directory '%ls'.", fullPath);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
FindClose(hFind);
|
||||
ClearReadOnlyAttribute(fullPath, attributes);
|
||||
WcaLog(LOGMSG_STANDARD, "Runtime cleanup deleting '%ls'.", fullPath);
|
||||
return SafeDeleteItem(fullPath);
|
||||
}
|
||||
|
||||
// See `Package.wxs` for the sequence of this custom action.
|
||||
@@ -178,13 +169,13 @@ void RecursiveDelete(LPCWSTR path)
|
||||
// 2. RemoveExistingProducts
|
||||
// ├─ TerminateProcesses
|
||||
// ├─ TryStopDeleteService
|
||||
// ├─ RemoveInstallFolder - <-- Here
|
||||
// ├─ RemoveRuntimeGeneratedFiles - <-- Here
|
||||
// └─ RemoveFiles
|
||||
// 3. InstallValidate
|
||||
// 4. InstallFiles
|
||||
// 5. InstallExecute
|
||||
// 6. InstallFinalize
|
||||
UINT __stdcall RemoveInstallFolder(
|
||||
UINT __stdcall RemoveRuntimeGeneratedFiles(
|
||||
__in MSIHANDLE hInstall)
|
||||
{
|
||||
HRESULT hr = S_OK;
|
||||
@@ -194,7 +185,7 @@ UINT __stdcall RemoveInstallFolder(
|
||||
LPWSTR pwz = NULL;
|
||||
LPWSTR pwzData = NULL;
|
||||
|
||||
hr = WcaInitialize(hInstall, "RemoveInstallFolder");
|
||||
hr = WcaInitialize(hInstall, "RemoveRuntimeGeneratedFiles");
|
||||
ExitOnFailure(hr, "Failed to initialize");
|
||||
|
||||
hr = WcaGetProperty(L"CustomActionData", &pwzData);
|
||||
@@ -202,24 +193,20 @@ UINT __stdcall RemoveInstallFolder(
|
||||
|
||||
pwz = pwzData;
|
||||
hr = WcaReadStringFromCaData(&pwz, &installFolder);
|
||||
ExitOnFailure(hr, "failed to read database key from custom action data: %ls", pwz);
|
||||
ExitOnFailure(hr, "failed to read install folder from custom action data: %ls", pwz);
|
||||
|
||||
if (installFolder == NULL || installFolder[0] == L'\0') {
|
||||
WcaLog(LOGMSG_STANDARD, "Install folder path is empty, skipping recursive delete.");
|
||||
WcaLog(LOGMSG_STANDARD, "Install folder path is empty, skipping runtime cleanup.");
|
||||
goto LExit;
|
||||
}
|
||||
|
||||
if (PathIsRootW(installFolder)) {
|
||||
WcaLog(LOGMSG_STANDARD, "Refusing to recursively delete root folder '%ls'.", installFolder);
|
||||
WcaLog(LOGMSG_STANDARD, "Refusing runtime cleanup in root folder '%ls'.", installFolder);
|
||||
goto LExit;
|
||||
}
|
||||
|
||||
WcaLog(LOGMSG_STANDARD, "Attempting to recursively delete contents of install folder: %ls", installFolder);
|
||||
|
||||
RecursiveDelete(installFolder);
|
||||
|
||||
// The standard MSI 'RemoveFolders' action will take care of removing the (now empty) directories.
|
||||
// We don't need to call RemoveDirectoryW on installFolder itself, as it might still be in use by the installer.
|
||||
WcaLog(LOGMSG_STANDARD, "Removing runtime-generated files from install folder: %ls", installFolder);
|
||||
DeleteRuntimeGeneratedFile(installFolder, L"RuntimeBroker_rustdesk.exe");
|
||||
|
||||
LExit:
|
||||
ReleaseStr(pwzData);
|
||||
|
||||
@@ -2,7 +2,7 @@ LIBRARY "CustomActions"
|
||||
|
||||
EXPORTS
|
||||
CustomActionHello
|
||||
RemoveInstallFolder
|
||||
RemoveRuntimeGeneratedFiles
|
||||
TerminateProcesses
|
||||
AddFirewallRules
|
||||
SetPropertyIsServiceRunning
|
||||
|
||||
@@ -16,8 +16,15 @@
|
||||
<!-- If a command line value was stored, restore it after the registry search has been performed -->
|
||||
<SetProperty Action="RestoreSavedInstallFolderValue" Id="INSTALLFOLDER" Value="[SavedInstallFolderCmdLineValue]" After="AppSearch" Sequence="first" Condition="SavedInstallFolderCmdLineValue" />
|
||||
|
||||
<!-- If a command line value or registry value was set, update the main properties with the value -->
|
||||
<SetProperty Id="INSTALLFOLDER_INNER" Value="[INSTALLFOLDER]" After="RestoreSavedInstallFolderValue" Sequence="first" Condition="INSTALLFOLDER" />
|
||||
<!-- Normalize INSTALLFOLDER from the command line or registry before assigning INSTALLFOLDER_INNER. -->
|
||||
<!-- Case 1: already ends with \$(var.Product)\, keep it unchanged. -->
|
||||
<SetProperty Action="SetInstallFolderInnerFromProductDir" Id="INSTALLFOLDER_INNER" Value="[INSTALLFOLDER]" After="RestoreSavedInstallFolderValue" Sequence="first" Condition="INSTALLFOLDER AND INSTALLFOLDER ~>> "\$(var.Product)\"" />
|
||||
<!-- Case 2: already ends with \$(var.Product) but has no trailing slash, add the slash. -->
|
||||
<SetProperty Action="SetInstallFolderInnerFromProductDirNoSlash" Id="INSTALLFOLDER_INNER" Value="[INSTALLFOLDER]\" After="RestoreSavedInstallFolderValue" Sequence="first" Condition="INSTALLFOLDER AND INSTALLFOLDER ~>> "\$(var.Product)"" />
|
||||
<!-- Case 3: ends with a slash but not \$(var.Product)\, append $(var.Product)\. -->
|
||||
<SetProperty Action="SetInstallFolderInnerAppendProduct" Id="INSTALLFOLDER_INNER" Value="[INSTALLFOLDER]$(var.Product)\" After="RestoreSavedInstallFolderValue" Sequence="first" Condition="INSTALLFOLDER AND INSTALLFOLDER ~>> "\" AND NOT (INSTALLFOLDER ~>> "\$(var.Product)\" OR INSTALLFOLDER ~>> "\$(var.Product)")" />
|
||||
<!-- Case 4: has no trailing slash and does not end with \$(var.Product), append \$(var.Product)\. -->
|
||||
<SetProperty Action="SetInstallFolderInnerAppendSlashProduct" Id="INSTALLFOLDER_INNER" Value="[INSTALLFOLDER]\$(var.Product)\" After="RestoreSavedInstallFolderValue" Sequence="first" Condition="INSTALLFOLDER AND NOT INSTALLFOLDER ~>> "\" AND NOT (INSTALLFOLDER ~>> "\$(var.Product)\" OR INSTALLFOLDER ~>> "\$(var.Product)")" />
|
||||
|
||||
<!-- INSTALLFOLDER_INNER is defined for compatibility with previous versions of the installer. -->
|
||||
<!-- Because we need to use INSTALLFOLDER as the command line argument. -->
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
</Component>
|
||||
</DirectoryRef>
|
||||
|
||||
<CustomAction Id="RemoveInstallFolder.SetParam" Return="check" Property="RemoveInstallFolder" Value="[INSTALLFOLDER_INNER]" />
|
||||
<CustomAction Id="RemoveRuntimeGeneratedFiles.SetParam" Return="check" Property="RemoveRuntimeGeneratedFiles" Value="[INSTALLFOLDER_INNER]" />
|
||||
<CustomAction Id="AddFirewallRules.SetParam" Return="check" Property="AddFirewallRules" Value="1[INSTALLFOLDER_INNER]$(var.Product).exe" />
|
||||
<CustomAction Id="RemoveFirewallRules.SetParam" Return="check" Property="RemoveFirewallRules" Value="0[INSTALLFOLDER_INNER]$(var.Product).exe" />
|
||||
<CustomAction Id="CreateStartService.SetParam" Return="check" Property="CreateStartService" Value="$(var.Product);"[INSTALLFOLDER_INNER]$(var.Product).exe" --service" />
|
||||
@@ -77,21 +77,21 @@
|
||||
|
||||
<Custom Action="AddRegSoftwareSASGeneration" Before="InstallFinalize" Condition="NOT (Installed AND REMOVE AND NOT UPGRADINGPRODUCTCODE) AND (NOT CC_CONNECTION_TYPE="outgoing")"/>
|
||||
|
||||
<Custom Action="RemoveInstallFolder" Before="RemoveFiles"/>
|
||||
<Custom Action="RemoveInstallFolder.SetParam" Before="RemoveInstallFolder"/>
|
||||
<Custom Action="TryStopDeleteService" Before="RemoveInstallFolder.SetParam" />
|
||||
<Custom Action="RemoveRuntimeGeneratedFiles" Before="RemoveFiles" Condition="Installed AND (REMOVE="ALL" OR UPGRADINGPRODUCTCODE)"/>
|
||||
<Custom Action="RemoveRuntimeGeneratedFiles.SetParam" Before="RemoveRuntimeGeneratedFiles" Condition="Installed AND (REMOVE="ALL" OR UPGRADINGPRODUCTCODE)"/>
|
||||
<Custom Action="TryStopDeleteService" Before="RemoveRuntimeGeneratedFiles.SetParam" />
|
||||
<Custom Action="TryStopDeleteService.SetParam" Before="TryStopDeleteService" />
|
||||
|
||||
<Custom Action="RemoveFirewallRules" Before="RemoveFiles"/>
|
||||
<Custom Action="RemoveFirewallRules.SetParam" Before="RemoveFirewallRules"/>
|
||||
|
||||
<Custom Action="UninstallPrinter" Before="RemoveInstallFolder" Condition="VersionNT >= 603" />
|
||||
<Custom Action="UninstallPrinter" Before="RemoveRuntimeGeneratedFiles" Condition="VersionNT >= 603" />
|
||||
|
||||
<Custom Action="TerminateProcesses" Before="RemoveInstallFolder"/>
|
||||
<Custom Action="TerminateProcesses" Before="RemoveRuntimeGeneratedFiles"/>
|
||||
<Custom Action="TerminateProcesses.SetParam" Before="TerminateProcesses"/>
|
||||
<Custom Action="TerminateBrokers" Before="RemoveInstallFolder"/>
|
||||
<Custom Action="TerminateBrokers" Before="RemoveRuntimeGeneratedFiles"/>
|
||||
<Custom Action="TerminateBrokers.SetParam" Before="TerminateBrokers"/>
|
||||
<Custom Action="RemoveAmyuniIdd" Before="RemoveInstallFolder"/>
|
||||
<Custom Action="RemoveAmyuniIdd" Before="RemoveRuntimeGeneratedFiles"/>
|
||||
<Custom Action="RemoveAmyuniIdd.SetParam" Before="RemoveAmyuniIdd"/>
|
||||
</InstallExecuteSequence>
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<Binary Id="Custom_Actions_Dll" SourceFile="$(var.CustomActions.TargetDir)$(var.CustomActions.TargetName).dll" />
|
||||
|
||||
<CustomAction Id="CustomActionHello" DllEntry="CustomActionHello" Impersonate="yes" Execute="immediate" Return="ignore" BinaryRef="Custom_Actions_Dll"/>
|
||||
<CustomAction Id="RemoveInstallFolder" DllEntry="RemoveInstallFolder" Impersonate="no" Execute="deferred" Return="ignore" BinaryRef="Custom_Actions_Dll"/>
|
||||
<CustomAction Id="RemoveRuntimeGeneratedFiles" DllEntry="RemoveRuntimeGeneratedFiles" Impersonate="no" Execute="deferred" Return="ignore" BinaryRef="Custom_Actions_Dll"/>
|
||||
<CustomAction Id="TerminateProcesses" DllEntry="TerminateProcesses" Impersonate="yes" Execute="immediate" Return="ignore" BinaryRef="Custom_Actions_Dll"/>
|
||||
<CustomAction Id="TerminateBrokers" DllEntry="TerminateProcesses" Impersonate="yes" Execute="immediate" Return="ignore" BinaryRef="Custom_Actions_Dll"/>
|
||||
<CustomAction Id="AddFirewallRules" DllEntry="AddFirewallRules" Impersonate="no" Execute="deferred" Return="ignore" BinaryRef="Custom_Actions_Dll"/>
|
||||
|
||||
@@ -23,12 +23,13 @@ Patch dialog sequence:
|
||||
-->
|
||||
|
||||
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs" xmlns:ui="http://wixtoolset.org/schemas/v4/wxs/ui">
|
||||
<?include ../Includes.wxi?>
|
||||
<?foreach WIXUIARCH in X86;X64;A64 ?>
|
||||
<Fragment>
|
||||
<UI Id="UI_MyInstallDialog_$(WIXUIARCH)">
|
||||
<Publish Dialog="LicenseAgreementDlg" Control="Print" Event="DoAction" Value="WixUIPrintEula_$(WIXUIARCH)" />
|
||||
<Publish Dialog="BrowseDlg" Control="OK" Event="DoAction" Value="WixUIValidatePath_$(WIXUIARCH)" Order="3" Condition="NOT WIXUI_DONTVALIDATEPATH" />
|
||||
<Publish Dialog="MyInstallDirDlg" Control="Next" Event="DoAction" Value="WixUIValidatePath_$(WIXUIARCH)" Order="2" Condition="NOT WIXUI_DONTVALIDATEPATH" />
|
||||
<Publish Dialog="MyInstallDirDlg" Control="Next" Event="DoAction" Value="WixUIValidatePath_$(WIXUIARCH)" Order="5" Condition="NOT WIXUI_DONTVALIDATEPATH" />
|
||||
</UI>
|
||||
|
||||
<UIRef Id="UI_MyInstallDialog" />
|
||||
@@ -64,9 +65,16 @@ Patch dialog sequence:
|
||||
<Publish Dialog="LicenseAgreementDlg" Control="Next" Event="NewDialog" Value="MyInstallDirDlg" Condition="LicenseAccepted = "1"" />
|
||||
|
||||
<Publish Dialog="MyInstallDirDlg" Control="Back" Event="NewDialog" Value="LicenseAgreementDlg" />
|
||||
<Publish Dialog="MyInstallDirDlg" Control="Next" Event="SetTargetPath" Value="[WIXUI_INSTALLDIR]" Order="1" />
|
||||
<Publish Dialog="MyInstallDirDlg" Control="Next" Event="SpawnDialog" Value="InvalidDirDlg" Order="3" Condition="NOT WIXUI_DONTVALIDATEPATH AND WIXUI_INSTALLDIR_VALID<>"1"" />
|
||||
<Publish Dialog="MyInstallDirDlg" Control="Next" Event="NewDialog" Value="VerifyReadyDlg" Order="4" Condition="WIXUI_DONTVALIDATEPATH OR WIXUI_INSTALLDIR_VALID="1"" />
|
||||
<!-- Normalize INSTALLFOLDER_INNER before SetTargetPath and WixUIValidatePath run. -->
|
||||
<!-- UI case 1: already ends with \$(var.Product) but has no trailing slash, add the slash. -->
|
||||
<Publish Dialog="MyInstallDirDlg" Control="Next" Property="INSTALLFOLDER_INNER" Value="[INSTALLFOLDER_INNER]\" Order="1" Condition="INSTALLFOLDER_INNER AND INSTALLFOLDER_INNER ~>> "\$(var.Product)"" />
|
||||
<!-- UI case 2: ends with a slash but not \$(var.Product)\, append $(var.Product)\. -->
|
||||
<Publish Dialog="MyInstallDirDlg" Control="Next" Property="INSTALLFOLDER_INNER" Value="[INSTALLFOLDER_INNER]$(var.Product)\" Order="2" Condition="INSTALLFOLDER_INNER AND INSTALLFOLDER_INNER ~>> "\" AND NOT (INSTALLFOLDER_INNER ~>> "\$(var.Product)\" OR INSTALLFOLDER_INNER ~>> "\$(var.Product)")" />
|
||||
<!-- UI case 3: has no trailing slash and does not end with \$(var.Product), append \$(var.Product)\. -->
|
||||
<Publish Dialog="MyInstallDirDlg" Control="Next" Property="INSTALLFOLDER_INNER" Value="[INSTALLFOLDER_INNER]\$(var.Product)\" Order="3" Condition="INSTALLFOLDER_INNER AND NOT INSTALLFOLDER_INNER ~>> "\" AND NOT (INSTALLFOLDER_INNER ~>> "\$(var.Product)\" OR INSTALLFOLDER_INNER ~>> "\$(var.Product)")" />
|
||||
<Publish Dialog="MyInstallDirDlg" Control="Next" Event="SetTargetPath" Value="[WIXUI_INSTALLDIR]" Order="4" />
|
||||
<Publish Dialog="MyInstallDirDlg" Control="Next" Event="SpawnDialog" Value="InvalidDirDlg" Order="6" Condition="NOT WIXUI_DONTVALIDATEPATH AND WIXUI_INSTALLDIR_VALID<>"1"" />
|
||||
<Publish Dialog="MyInstallDirDlg" Control="Next" Event="NewDialog" Value="VerifyReadyDlg" Order="7" Condition="WIXUI_DONTVALIDATEPATH OR WIXUI_INSTALLDIR_VALID="1"" />
|
||||
<Publish Dialog="MyInstallDirDlg" Control="ChangeFolder" Property="_BrowseProperty" Value="[WIXUI_INSTALLDIR]" Order="1" />
|
||||
<Publish Dialog="MyInstallDirDlg" Control="ChangeFolder" Event="SpawnDialog" Value="BrowseDlg" Order="2" />
|
||||
<Publish Dialog="VerifyReadyDlg" Control="Back" Event="NewDialog" Value="MyInstallDirDlg" Order="1" Condition="NOT Installed" />
|
||||
|
||||
21
src/cli.rs
21
src/cli.rs
@@ -3,7 +3,7 @@ use async_trait::async_trait;
|
||||
use hbb_common::{
|
||||
config::PeerConfig,
|
||||
config::READ_TIMEOUT,
|
||||
futures::{SinkExt, StreamExt},
|
||||
futures::StreamExt,
|
||||
log,
|
||||
message_proto::*,
|
||||
protobuf::Message as _,
|
||||
@@ -46,6 +46,7 @@ impl Session {
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
session
|
||||
}
|
||||
@@ -53,7 +54,7 @@ impl Session {
|
||||
|
||||
#[async_trait]
|
||||
impl Interface for Session {
|
||||
fn get_login_config_handler(&self) -> Arc<RwLock<LoginConfigHandler>> {
|
||||
fn get_lch(&self) -> Arc<RwLock<LoginConfigHandler>> {
|
||||
return self.lc.clone();
|
||||
}
|
||||
|
||||
@@ -61,14 +62,20 @@ impl Interface for Session {
|
||||
match msgtype {
|
||||
"input-password" => {
|
||||
self.sender
|
||||
.send(Data::Login((self.password.clone(), true)))
|
||||
.send(Data::Login((
|
||||
String::new(),
|
||||
String::new(),
|
||||
self.password.clone(),
|
||||
true,
|
||||
)))
|
||||
.ok();
|
||||
}
|
||||
"re-input-password" => {
|
||||
log::error!("{}: {}", title, text);
|
||||
match rpassword::prompt_password("Enter password: ") {
|
||||
Ok(password) => {
|
||||
let login_data = Data::Login((password, true));
|
||||
let login_data =
|
||||
Data::Login((String::new(), String::new(), password, true));
|
||||
self.sender.send(login_data).ok();
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -93,6 +100,8 @@ impl Interface for Session {
|
||||
self.lc.write().unwrap().handle_peer_info(&pi);
|
||||
}
|
||||
|
||||
fn set_multiple_windows_session(&self, _sessions: Vec<WindowsSession>) {}
|
||||
|
||||
async fn handle_hash(&self, pass: &str, hash: Hash, peer: &mut Stream) {
|
||||
log::info!(
|
||||
"password={}",
|
||||
@@ -137,8 +146,8 @@ pub async fn connect_test(id: &str, key: String, token: String) {
|
||||
Err(err) => {
|
||||
log::error!("Failed to connect {}: {}", &id, err);
|
||||
}
|
||||
Ok((mut stream, direct)) => {
|
||||
log::info!("direct: {}", direct);
|
||||
Ok(((mut stream, _direct, _secure, _kcp, _typ), direct)) => {
|
||||
log::info!("direct: {:?}", direct);
|
||||
// rpassword::prompt_password("Input anything to exit").ok();
|
||||
loop {
|
||||
tokio::select! {
|
||||
|
||||
247
src/client.rs
247
src/client.rs
@@ -65,11 +65,12 @@ use hbb_common::{
|
||||
self,
|
||||
net::UdpSocket,
|
||||
sync::{
|
||||
mpsc::{unbounded_channel, UnboundedReceiver},
|
||||
mpsc::{error::TryRecvError, unbounded_channel, UnboundedReceiver},
|
||||
oneshot,
|
||||
},
|
||||
time::{interval, Duration, Instant},
|
||||
},
|
||||
webrtc::WebRTCStream,
|
||||
AddrMangle, ResultType, Stream,
|
||||
};
|
||||
pub use helper::*;
|
||||
@@ -330,6 +331,19 @@ impl Client {
|
||||
} else {
|
||||
(None, None)
|
||||
};
|
||||
let ipv6 = if crate::get_ipv6_punch_enabled() {
|
||||
crate::get_ipv6_socket().await
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let webrtc_offerer =
|
||||
match WebRTCStream::new("", interface.is_force_relay(), CONNECT_TIMEOUT).await {
|
||||
Ok(stream) => Some(stream),
|
||||
Err(err) => {
|
||||
log::warn!("webrtc offerer setup failed: {}", err);
|
||||
None
|
||||
}
|
||||
};
|
||||
let fut = Self::_start_inner(
|
||||
peer.to_owned(),
|
||||
key.to_owned(),
|
||||
@@ -338,6 +352,8 @@ impl Client {
|
||||
interface.clone(),
|
||||
udp.clone(),
|
||||
Some(stop_udp_tx),
|
||||
ipv6,
|
||||
webrtc_offerer,
|
||||
rendezvous_server.clone(),
|
||||
servers.clone(),
|
||||
contained,
|
||||
@@ -355,6 +371,8 @@ impl Client {
|
||||
interface,
|
||||
(None, None),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
rendezvous_server,
|
||||
servers,
|
||||
contained,
|
||||
@@ -366,6 +384,67 @@ impl Client {
|
||||
}
|
||||
}
|
||||
|
||||
fn is_expected_webrtc_ice_candidate(ice: &IceCandidate, session_key: &str) -> bool {
|
||||
!session_key.is_empty() && ice.session_key == session_key && !ice.candidate.is_empty()
|
||||
}
|
||||
|
||||
fn spawn_webrtc_ice_bridge(
|
||||
mut socket: Stream,
|
||||
mut local_ice_rx: Option<UnboundedReceiver<String>>,
|
||||
webrtc: WebRTCStream,
|
||||
peer: String,
|
||||
session_key: String,
|
||||
) -> oneshot::Sender<()> {
|
||||
let (stop_tx, mut stop_rx) = oneshot::channel::<()>();
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
match stop_rx.try_recv() {
|
||||
Ok(_) | Err(tokio::sync::oneshot::error::TryRecvError::Closed) => break,
|
||||
Err(tokio::sync::oneshot::error::TryRecvError::Empty) => {}
|
||||
}
|
||||
|
||||
if let Some(rx) = local_ice_rx.as_mut() {
|
||||
loop {
|
||||
match rx.try_recv() {
|
||||
Ok(candidate) => {
|
||||
let mut msg = RendezvousMessage::new();
|
||||
msg.set_ice_candidate(IceCandidate {
|
||||
id: peer.clone(),
|
||||
session_key: session_key.clone(),
|
||||
candidate,
|
||||
..Default::default()
|
||||
});
|
||||
if let Err(err) = socket.send(&msg).await {
|
||||
log::warn!("failed to send WebRTC ICE candidate: {}", err);
|
||||
return;
|
||||
}
|
||||
}
|
||||
Err(TryRecvError::Empty) => break,
|
||||
Err(TryRecvError::Disconnected) => {
|
||||
local_ice_rx = None;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(msg_in) =
|
||||
crate::get_next_nonkeyexchange_msg(&mut socket, Some(100)).await
|
||||
{
|
||||
if let Some(rendezvous_message::Union::IceCandidate(ice)) = msg_in.union {
|
||||
if Self::is_expected_webrtc_ice_candidate(&ice, &session_key) {
|
||||
if let Err(err) = webrtc.add_remote_ice_candidate(&ice.candidate).await
|
||||
{
|
||||
log::warn!("failed to add WebRTC ICE candidate: {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
stop_tx
|
||||
}
|
||||
|
||||
async fn _start_inner(
|
||||
peer: String,
|
||||
key: String,
|
||||
@@ -374,6 +453,8 @@ impl Client {
|
||||
interface: impl Interface,
|
||||
mut udp: (Option<Arc<UdpSocket>>, Option<Arc<Mutex<u16>>>),
|
||||
stop_udp_tx: Option<oneshot::Sender<()>>,
|
||||
mut ipv6: Option<(Arc<UdpSocket>, bytes::Bytes)>,
|
||||
mut webrtc_offerer: Option<WebRTCStream>,
|
||||
mut rendezvous_server: String,
|
||||
servers: Vec<String>,
|
||||
contained: bool,
|
||||
@@ -446,14 +527,20 @@ impl Client {
|
||||
// Stop UDP NAT test task if still running
|
||||
stop_udp_tx.map(|tx| tx.send(()));
|
||||
let mut msg_out = RendezvousMessage::new();
|
||||
let mut ipv6 = if crate::get_ipv6_punch_enabled() {
|
||||
if let Some((socket, addr)) = crate::get_ipv6_socket().await {
|
||||
(Some(socket), Some(addr))
|
||||
} else {
|
||||
(None, None)
|
||||
let mut ipv6 = ipv6
|
||||
.take()
|
||||
.map(|(socket, addr)| (Some(socket), Some(addr)))
|
||||
.unwrap_or((None, None));
|
||||
let webrtc_sdp_offer = if let Some(webrtc) = webrtc_offerer.as_ref() {
|
||||
match webrtc.get_local_endpoint().await {
|
||||
Ok(endpoint) => endpoint,
|
||||
Err(err) => {
|
||||
log::warn!("failed to read local WebRTC offer: {}", err);
|
||||
String::new()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
(None, None)
|
||||
String::new()
|
||||
};
|
||||
let udp_nat_port = udp.1.map(|x| *x.lock().unwrap()).unwrap_or(0);
|
||||
let punch_type = if udp_nat_port > 0 { "UDP" } else { "TCP" };
|
||||
@@ -467,9 +554,16 @@ impl Client {
|
||||
udp_port: udp_nat_port as _,
|
||||
force_relay: interface.is_force_relay(),
|
||||
socket_addr_v6: ipv6.1.unwrap_or_default(),
|
||||
webrtc_sdp_offer: webrtc_sdp_offer.clone(),
|
||||
..Default::default()
|
||||
});
|
||||
for i in 1..=3 {
|
||||
let webrtc_session_key = webrtc_offerer
|
||||
.as_ref()
|
||||
.map(|webrtc| webrtc.session_key().to_owned())
|
||||
.unwrap_or_default();
|
||||
let mut webrtc_sdp_answer = String::new();
|
||||
let mut pending_webrtc_ice = Vec::<String>::new();
|
||||
'punch_attempts: for i in 1..=3 {
|
||||
log::info!(
|
||||
"#{} {} punch attempt with {}, id: {}",
|
||||
i,
|
||||
@@ -479,9 +573,20 @@ impl Client {
|
||||
);
|
||||
socket.send(&msg_out).await?;
|
||||
// below timeout should not bigger than hbbs's connection timeout.
|
||||
if let Some(msg_in) =
|
||||
crate::get_next_nonkeyexchange_msg(&mut socket, Some(i * 3000)).await
|
||||
{
|
||||
let attempt_deadline = Instant::now() + Duration::from_millis((i * 3000) as u64);
|
||||
loop {
|
||||
let remaining = attempt_deadline.saturating_duration_since(Instant::now());
|
||||
if remaining.is_zero() {
|
||||
break;
|
||||
}
|
||||
let timeout_ms = remaining
|
||||
.as_millis()
|
||||
.clamp(1, u64::MAX as u128) as u64;
|
||||
let Some(msg_in) =
|
||||
crate::get_next_nonkeyexchange_msg(&mut socket, Some(timeout_ms)).await
|
||||
else {
|
||||
break;
|
||||
};
|
||||
match msg_in.union {
|
||||
Some(rendezvous_message::Union::PunchHoleResponse(ph)) => {
|
||||
if ph.socket_addr.is_empty() {
|
||||
@@ -510,6 +615,7 @@ impl Client {
|
||||
relay_server = ph.relay_server;
|
||||
peer_addr = AddrMangle::decode(&ph.socket_addr);
|
||||
feedback = ph.feedback;
|
||||
webrtc_sdp_answer = ph.webrtc_sdp_answer;
|
||||
let s = udp.0.take();
|
||||
if ph.is_udp && s.is_some() {
|
||||
if let Some(s) = s {
|
||||
@@ -528,7 +634,7 @@ impl Client {
|
||||
}
|
||||
}
|
||||
log::info!("{} Hole Punched {} = {}", punch_type, peer, peer_addr);
|
||||
break;
|
||||
break 'punch_attempts;
|
||||
}
|
||||
}
|
||||
Some(rendezvous_message::Union::RelayResponse(rr)) => {
|
||||
@@ -549,6 +655,38 @@ impl Client {
|
||||
}
|
||||
}
|
||||
signed_id_pk = rr.pk().into();
|
||||
let mut webrtc_bridge_stop = None;
|
||||
let mut webrtc_for_connect = None;
|
||||
if !rr.webrtc_sdp_answer.is_empty() {
|
||||
if let Some(webrtc) = webrtc_offerer.take() {
|
||||
if let Err(err) =
|
||||
webrtc.set_remote_endpoint(&rr.webrtc_sdp_answer).await
|
||||
{
|
||||
log::warn!("failed to set WebRTC relay answer: {}", err);
|
||||
} else {
|
||||
for candidate in pending_webrtc_ice.drain(..) {
|
||||
if let Err(err) =
|
||||
webrtc.add_remote_ice_candidate(&candidate).await
|
||||
{
|
||||
log::warn!(
|
||||
"failed to add buffered WebRTC ICE candidate: {}",
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
let session_key = webrtc.session_key().to_owned();
|
||||
let local_ice_rx = webrtc.take_local_ice_rx();
|
||||
webrtc_bridge_stop = Some(Self::spawn_webrtc_ice_bridge(
|
||||
socket,
|
||||
local_ice_rx,
|
||||
webrtc.clone(),
|
||||
peer.clone(),
|
||||
session_key,
|
||||
));
|
||||
webrtc_for_connect = Some(webrtc);
|
||||
}
|
||||
}
|
||||
}
|
||||
let fut = Self::create_relay(
|
||||
&peer,
|
||||
rr.uuid,
|
||||
@@ -564,30 +702,81 @@ impl Client {
|
||||
}
|
||||
.boxed(),
|
||||
);
|
||||
if let Some(mut webrtc) = webrtc_for_connect {
|
||||
connect_futures.push(
|
||||
async move {
|
||||
webrtc.wait_connected(CONNECT_TIMEOUT).await?;
|
||||
Ok((Stream::WebRTC(webrtc), None, "WebRTC"))
|
||||
}
|
||||
.boxed(),
|
||||
);
|
||||
}
|
||||
// Run all connection attempts concurrently, return the first successful one
|
||||
let (conn, kcp, typ) = match select_ok(connect_futures).await {
|
||||
Ok(conn) => (Ok(conn.0 .0), conn.0 .1, conn.0 .2),
|
||||
|
||||
Err(e) => (Err(e), None, ""),
|
||||
};
|
||||
if let Some(stop) = webrtc_bridge_stop {
|
||||
let _ = stop.send(());
|
||||
}
|
||||
let mut conn = conn?;
|
||||
feedback = rr.feedback;
|
||||
log::info!("{:?} used to establish {typ} connection", start.elapsed());
|
||||
let pk =
|
||||
Self::secure_connection(&peer, signed_id_pk, &key, &mut conn).await?;
|
||||
return Ok((
|
||||
(conn, typ == "IPv6", pk, kcp, typ),
|
||||
(conn, typ == "IPv6" || typ == "WebRTC", pk, kcp, typ),
|
||||
(feedback, rendezvous_server),
|
||||
false,
|
||||
));
|
||||
}
|
||||
Some(rendezvous_message::Union::IceCandidate(ice)) => {
|
||||
if Self::is_expected_webrtc_ice_candidate(&ice, &webrtc_session_key) {
|
||||
pending_webrtc_ice.push(ice.candidate);
|
||||
} else {
|
||||
log::debug!(
|
||||
"dropping ICE candidate for unexpected WebRTC session key {}",
|
||||
ice.session_key,
|
||||
);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
log::error!("Unexpected protobuf msg received: {:?}", msg_in);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
drop(socket);
|
||||
let mut webrtc_bridge_stop = None;
|
||||
let mut webrtc_for_connect = None;
|
||||
if !webrtc_sdp_answer.is_empty() {
|
||||
if let Some(webrtc) = webrtc_offerer.take() {
|
||||
if let Err(err) = webrtc.set_remote_endpoint(&webrtc_sdp_answer).await {
|
||||
log::warn!("failed to set WebRTC answer: {}", err);
|
||||
drop(socket);
|
||||
} else {
|
||||
for candidate in pending_webrtc_ice.drain(..) {
|
||||
if let Err(err) = webrtc.add_remote_ice_candidate(&candidate).await {
|
||||
log::warn!("failed to add buffered WebRTC ICE candidate: {}", err);
|
||||
}
|
||||
}
|
||||
let session_key = webrtc.session_key().to_owned();
|
||||
let local_ice_rx = webrtc.take_local_ice_rx();
|
||||
webrtc_bridge_stop = Some(Self::spawn_webrtc_ice_bridge(
|
||||
socket,
|
||||
local_ice_rx,
|
||||
webrtc.clone(),
|
||||
peer.clone(),
|
||||
session_key,
|
||||
));
|
||||
webrtc_for_connect = Some(webrtc);
|
||||
}
|
||||
} else {
|
||||
drop(socket);
|
||||
}
|
||||
} else {
|
||||
drop(socket);
|
||||
}
|
||||
if peer_addr.port() == 0 {
|
||||
bail!("Failed to connect via rendezvous server");
|
||||
}
|
||||
@@ -621,6 +810,8 @@ impl Client {
|
||||
interface,
|
||||
udp.0,
|
||||
ipv6.0,
|
||||
webrtc_for_connect,
|
||||
webrtc_bridge_stop,
|
||||
punch_type,
|
||||
)
|
||||
.await?,
|
||||
@@ -647,6 +838,8 @@ impl Client {
|
||||
interface: impl Interface,
|
||||
udp_socket_nat: Option<Arc<UdpSocket>>,
|
||||
udp_socket_v6: Option<Arc<UdpSocket>>,
|
||||
webrtc_offerer: Option<WebRTCStream>,
|
||||
webrtc_bridge_stop: Option<oneshot::Sender<()>>,
|
||||
punch_type: &str,
|
||||
) -> ResultType<(
|
||||
Stream,
|
||||
@@ -705,11 +898,23 @@ impl Client {
|
||||
if let Some(udp_socket_v6) = udp_socket_v6 {
|
||||
connect_futures.push(udp_nat_connect(udp_socket_v6, "IPv6", connect_timeout).boxed());
|
||||
}
|
||||
if let Some(mut webrtc) = webrtc_offerer {
|
||||
connect_futures.push(
|
||||
async move {
|
||||
webrtc.wait_connected(connect_timeout).await?;
|
||||
Ok((Stream::WebRTC(webrtc), None, "WebRTC"))
|
||||
}
|
||||
.boxed(),
|
||||
);
|
||||
}
|
||||
// Run all connection attempts concurrently, return the first successful one
|
||||
let (mut conn, kcp, mut typ) = match select_ok(connect_futures).await {
|
||||
Ok(conn) => (Ok(conn.0 .0), conn.0 .1, conn.0 .2),
|
||||
Err(e) => (Err(e), None, ""),
|
||||
};
|
||||
if let Some(stop) = webrtc_bridge_stop {
|
||||
let _ = stop.send(());
|
||||
}
|
||||
|
||||
let mut direct = !conn.is_err();
|
||||
if interface.is_force_relay() || conn.is_err() {
|
||||
@@ -4120,8 +4325,22 @@ pub mod peer_online {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::client::Client;
|
||||
use hbb_common::rendezvous_proto::IceCandidate;
|
||||
use hbb_common::tokio;
|
||||
|
||||
#[test]
|
||||
fn accepts_webrtc_ice_by_session_key_only() {
|
||||
let ice = IceCandidate {
|
||||
session_key: "session-a".to_owned(),
|
||||
candidate: "candidate-json".to_owned(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
assert!(Client::is_expected_webrtc_ice_candidate(&ice, "session-a"));
|
||||
assert!(!Client::is_expected_webrtc_ice_candidate(&ice, "session-b"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_query_onlines() {
|
||||
super::query_online_states(
|
||||
|
||||
@@ -627,6 +627,98 @@ pub fn core_main() -> Option<Vec<String>> {
|
||||
println!("Installation and administrative privileges required!");
|
||||
}
|
||||
return None;
|
||||
} else if args[0] == "--deploy" {
|
||||
if config::Config::no_register_device() {
|
||||
println!("Cannot deploy an unregistrable device!");
|
||||
} else if crate::platform::is_installed() && is_root() {
|
||||
let max = args.len() - 1;
|
||||
let pos = args.iter().position(|x| x == "--token").unwrap_or(max);
|
||||
if pos >= max {
|
||||
println!("--token is required!");
|
||||
return None;
|
||||
}
|
||||
let token = args[pos + 1].to_owned();
|
||||
let get_value = |c: &str| {
|
||||
let pos = args.iter().position(|x| x == c).unwrap_or(max);
|
||||
if pos < max {
|
||||
Some(args[pos + 1].to_owned())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
let new_id = get_value("--id");
|
||||
let local_id = crate::ipc::get_id();
|
||||
let id_to_deploy = new_id.clone().unwrap_or_else(|| local_id.clone());
|
||||
let uuid = crate::encode64(hbb_common::get_uuid());
|
||||
let pk = crate::encode64(
|
||||
hbb_common::config::Config::get_key_pair().1,
|
||||
);
|
||||
let body = serde_json::json!({
|
||||
"id": id_to_deploy,
|
||||
"uuid": uuid,
|
||||
"pk": pk,
|
||||
});
|
||||
let header = "Authorization: Bearer ".to_owned() + &token;
|
||||
let url = crate::ui_interface::get_api_server() + "/api/devices/deploy";
|
||||
match crate::post_request_sync(url, body.to_string(), &header) {
|
||||
Err(err) => {
|
||||
println!("Request failed: {}", err);
|
||||
std::process::exit(1);
|
||||
}
|
||||
Ok(text) => {
|
||||
let parsed: serde_json::Value =
|
||||
serde_json::from_str(&text).unwrap_or(serde_json::Value::Null);
|
||||
let result = parsed["result"].as_str().unwrap_or("");
|
||||
match result {
|
||||
"OK" => {
|
||||
if let Some(ref new_id) = new_id {
|
||||
if *new_id != local_id {
|
||||
if let Err(err) =
|
||||
crate::ipc::set_config("id", new_id.clone())
|
||||
{
|
||||
println!(
|
||||
"Failed to persist deployed id locally: {}",
|
||||
err
|
||||
);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Err(err) = crate::ipc::notify_deployed() {
|
||||
log::warn!("Failed to notify deployed state: {}", err);
|
||||
}
|
||||
println!("Device deployed.");
|
||||
}
|
||||
"NOT_ENABLED" => {
|
||||
println!("Server does not require deployment.");
|
||||
std::process::exit(3);
|
||||
}
|
||||
"INVALID_INPUT" => {
|
||||
println!("Invalid input.");
|
||||
std::process::exit(5);
|
||||
}
|
||||
"ID_TAKEN" => {
|
||||
println!(
|
||||
"Id `{}` is already used by another machine on the server.",
|
||||
id_to_deploy
|
||||
);
|
||||
std::process::exit(6);
|
||||
}
|
||||
_ => {
|
||||
if text.is_empty() {
|
||||
println!("Unknown response.");
|
||||
} else {
|
||||
println!("{}", text);
|
||||
}
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println!("Installation and administrative privileges required!");
|
||||
}
|
||||
return None;
|
||||
} else if args[0] == "--check-hwcodec-config" {
|
||||
#[cfg(feature = "hwcodec")]
|
||||
crate::ipc::hwcodec_process();
|
||||
|
||||
12
src/ipc.rs
12
src/ipc.rs
@@ -312,6 +312,7 @@ pub enum Data {
|
||||
ClipboardNonFile(Option<(String, Vec<ClipboardNonFile>)>),
|
||||
PrivacyModeState((i32, PrivacyModeState, String)),
|
||||
TestRendezvousServer,
|
||||
Deployed,
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
Keyboard(DataKeyboard),
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
@@ -929,6 +930,10 @@ async fn handle(data: Data, stream: &mut Connection) {
|
||||
Data::TestRendezvousServer => {
|
||||
crate::test_rendezvous_server();
|
||||
}
|
||||
Data::Deployed => {
|
||||
crate::rendezvous_mediator::NEEDS_DEPLOY.store(false, Ordering::SeqCst);
|
||||
crate::rendezvous_mediator::RendezvousMediator::restart();
|
||||
}
|
||||
#[cfg(feature = "flutter")]
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
Data::SwitchSidesRequest(id) => {
|
||||
@@ -1737,6 +1742,13 @@ pub async fn test_rendezvous_server() -> ResultType<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::main(flavor = "current_thread")]
|
||||
pub async fn notify_deployed() -> ResultType<()> {
|
||||
let mut c = connect(1000, "").await?;
|
||||
c.send(&Data::Deployed).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::main(flavor = "current_thread")]
|
||||
pub async fn send_url_scheme(url: String) -> ResultType<()> {
|
||||
connect(1_000, "_url")
|
||||
|
||||
@@ -208,7 +208,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Closed manually by the peer", "Cerrado manualmente por el par"),
|
||||
("Enable remote configuration modification", "Habilitar modificación remota de configuración"),
|
||||
("Run without install", "Ejecutar sin instalar"),
|
||||
("Connect via relay", ""),
|
||||
("Connect via relay", "Conectar a través de relay"),
|
||||
("Always connect via relay", "Conéctese siempre a través de relay"),
|
||||
("whitelist_tip", "Solo las direcciones IP autorizadas pueden conectarse a este escritorio"),
|
||||
("Login", "Iniciar sesión"),
|
||||
@@ -228,7 +228,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Username missed", "Olvidó su nombre de usuario"),
|
||||
("Password missed", "Olvidó su contraseña"),
|
||||
("Wrong credentials", "Credenciales incorrectas"),
|
||||
("The verification code is incorrect or has expired", ""),
|
||||
("The verification code is incorrect or has expired", "El código de verificación es incorrecto o ha caducado"),
|
||||
("Edit Tag", "Editar tag"),
|
||||
("Forget Password", "Olvidar contraseña"),
|
||||
("Favorites", "Favoritos"),
|
||||
@@ -302,8 +302,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Keep RustDesk background service", "Dejar RustDesk como Servicio en 2do plano"),
|
||||
("Ignore Battery Optimizations", "Ignorar optimizacioens de bateria"),
|
||||
("android_open_battery_optimizations_tip", "Si deseas deshabilitar esta característica, por favor, ve a la página siguiente de ajustes, busca y entra en [Batería] y desmarca [Sin restricción]"),
|
||||
("Start on boot", ""),
|
||||
("Start the screen sharing service on boot, requires special permissions", ""),
|
||||
("Start on boot", "Iniciar al arrancar"),
|
||||
("Start the screen sharing service on boot, requires special permissions", "Iniciar el servicio de pantalla compartida al arrancar, requiere permisos especiales"),
|
||||
("Connection not allowed", "Conexión no disponible"),
|
||||
("Legacy mode", "Modo heredado"),
|
||||
("Map mode", "Modo mapa"),
|
||||
@@ -326,8 +326,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Ratio", "Relación"),
|
||||
("Image Quality", "Calidad de imagen"),
|
||||
("Scroll Style", "Estilo de desplazamiento"),
|
||||
("Show Toolbar", ""),
|
||||
("Hide Toolbar", ""),
|
||||
("Show Toolbar", "Mostrar herramientas"),
|
||||
("Hide Toolbar", "Ocultar herramientas"),
|
||||
("Direct Connection", "Conexión directa"),
|
||||
("Relay Connection", "Conexión Relay"),
|
||||
("Secure Connection", "Conexión segura"),
|
||||
@@ -338,7 +338,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Security", "Seguridad"),
|
||||
("Theme", "Tema"),
|
||||
("Dark Theme", "Tema Oscuro"),
|
||||
("Light Theme", ""),
|
||||
("Light Theme", "Tema claro"),
|
||||
("Dark", "Oscuro"),
|
||||
("Light", "Claro"),
|
||||
("Follow System", "Tema del sistema"),
|
||||
@@ -355,12 +355,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Audio Input Device", "Dispositivo de entrada de audio"),
|
||||
("Use IP Whitelisting", "Usar lista de IPs admitidas"),
|
||||
("Network", "Red"),
|
||||
("Pin Toolbar", ""),
|
||||
("Unpin Toolbar", ""),
|
||||
("Pin Toolbar", "Anclar herramientas"),
|
||||
("Unpin Toolbar", "Desanclar herramientas"),
|
||||
("Recording", "Grabando"),
|
||||
("Directory", "Directorio"),
|
||||
("Automatically record incoming sessions", "Grabación automática de sesiones entrantes"),
|
||||
("Automatically record outgoing sessions", ""),
|
||||
("Automatically record outgoing sessions", "Grabación automática de sesiones salientes"),
|
||||
("Change", "Cambiar"),
|
||||
("Start session recording", "Comenzar grabación de sesión"),
|
||||
("Stop session recording", "Detener grabación de sesión"),
|
||||
@@ -368,7 +368,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Enable LAN discovery", "Habilitar descubrimiento de LAN"),
|
||||
("Deny LAN discovery", "Denegar descubrimiento de LAN"),
|
||||
("Write a message", "Escribir un mensaje"),
|
||||
("Prompt", ""),
|
||||
("Prompt", "Solicitud"),
|
||||
("Please wait for confirmation of UAC...", "Por favor, espera confirmación de UAC"),
|
||||
("elevated_foreground_window_tip", "La ventana actual del escritorio remoto necesita privilegios elevados para funcionar, así que no puedes usar ratón y teclado temporalmente. Puedes solicitar al usuario remoto que minimize la ventana actual o hacer clic en el botón de elevación de la ventana de gestión de conexión. Para evitar este problema, se recomienda instalar el programa en el dispositivo remto."),
|
||||
("Disconnected", "Desconectado"),
|
||||
@@ -616,9 +616,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("During service is on", "Mientras el servicio está activo"),
|
||||
("Capture screen using DirectX", "Capturar pantalla con DirectX"),
|
||||
("Back", "Atrás"),
|
||||
("Apps", ""),
|
||||
("Volume up", "Bajar volumen"),
|
||||
("Volume down", "Subir volumen"),
|
||||
("Apps", "Aplicaciones"),
|
||||
("Volume up", "Subir volumen"),
|
||||
("Volume down", "Bajar volumen"),
|
||||
("Power", "Encendido"),
|
||||
("Telegram bot", "Bot de Telegram"),
|
||||
("enable-bot-tip", "Si activas esta característica puedes recibir código 2FA de tu bot. También puede funcionar como notificación de conexión."),
|
||||
@@ -651,7 +651,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Update client clipboard", "Actualizar portapapeles del cliente"),
|
||||
("Untagged", "Sin itiquetar"),
|
||||
("new-version-of-{}-tip", "Hay una nueva versión de {} disponible"),
|
||||
("Accessible devices", ""),
|
||||
("Accessible devices", "Dispositivos accesibles"),
|
||||
("upgrade_remote_rustdesk_client_to_{}_tip", "Por favor, actualiza el cliente RustDesk a la versión {} o superior en el lado remoto"),
|
||||
("d3d_render_tip", "Al activar el renderizado D3D, la pantalla de control remoto puede verse negra en algunos equipos."),
|
||||
("Use D3D rendering", "Usar renderizado D3D"),
|
||||
@@ -689,9 +689,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Use WebSocket", "Usar WebSocket"),
|
||||
("Trackpad speed", "Velocidad de trackpad"),
|
||||
("Default trackpad speed", "Velocidad predeterminada de trackpad"),
|
||||
("Numeric one-time password", ""),
|
||||
("Enable IPv6 P2P connection", ""),
|
||||
("Enable UDP hole punching", ""),
|
||||
("Numeric one-time password", "Contraseña numérica de un solo uso"),
|
||||
("Enable IPv6 P2P connection", "Habilitar conexión IPv6 P2P"),
|
||||
("Enable UDP hole punching", "Habilitar perforación de agujero UDP"),
|
||||
("View camera", "Ver cámara"),
|
||||
("Enable camera", "Habilitar cámara"),
|
||||
("No cameras", "No hay cámaras"),
|
||||
@@ -708,8 +708,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Failed to check if the user is an administrator.", "No se ha podido comprobar si el usuario es un administrador."),
|
||||
("Supported only in the installed version.", "Soportado solo en la versión instalada."),
|
||||
("elevation_username_tip", "Introduzca el nombre de usuario o dominio\\NombreDeUsuario"),
|
||||
("Preparing for installation ...", ""),
|
||||
("Show my cursor", ""),
|
||||
("Preparing for installation ...", "Preparando instlación..."),
|
||||
("Show my cursor", "Mostrar mi cursor"),
|
||||
("Scale custom", "Escala personalizada"),
|
||||
("Custom scale slider", "Control deslizante de escala personalizada"),
|
||||
("Decrease", "Disminuir"),
|
||||
@@ -721,28 +721,28 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Show virtual joystick", "Mostrar joystick virtual"),
|
||||
("Edit note", "Editar nota"),
|
||||
("Alias", ""),
|
||||
("ScrollEdge", ""),
|
||||
("Allow insecure TLS fallback", ""),
|
||||
("allow-insecure-tls-fallback-tip", ""),
|
||||
("Disable UDP", ""),
|
||||
("disable-udp-tip", ""),
|
||||
("server-oss-not-support-tip", ""),
|
||||
("input note here", ""),
|
||||
("note-at-conn-end-tip", ""),
|
||||
("Show terminal extra keys", ""),
|
||||
("Relative mouse mode", ""),
|
||||
("rel-mouse-not-supported-peer-tip", ""),
|
||||
("rel-mouse-not-ready-tip", ""),
|
||||
("rel-mouse-lock-failed-tip", ""),
|
||||
("rel-mouse-exit-{}-tip", ""),
|
||||
("rel-mouse-permission-lost-tip", ""),
|
||||
("Changelog", ""),
|
||||
("keep-awake-during-outgoing-sessions-label", ""),
|
||||
("keep-awake-during-incoming-sessions-label", ""),
|
||||
("ScrollEdge", "Desplazamiento de pantalla"),
|
||||
("Allow insecure TLS fallback", "Permitir conexión TLS insegura de respaldo"),
|
||||
("allow-insecure-tls-fallback-tip", "De forma predeterminada, RustDesk verifica el certificado de servidor para protocolos que usen TLS.\nCon esta opción habilitada, Rustdesk volverá al paso de omisión de verificación y procederá en caso de fallo de verificación."),
|
||||
("Disable UDP", "Inhabilitar UDP"),
|
||||
("disable-udp-tip", "Controla si se usa TCP solamente.\nCuando esta opción está activa, RustDesk no usará más el puerto UDP 21116, en su lugar se usará el TCP 21116."),
|
||||
("server-oss-not-support-tip", "NOTA: El servidor RustDesk OSS no incluye esta característica."),
|
||||
("input note here", "Introducir nota aquí"),
|
||||
("note-at-conn-end-tip", "Pedir nota al finalizar la conexión"),
|
||||
("Show terminal extra keys", "Mostrar teclas extra del terminal"),
|
||||
("Relative mouse mode", "Modo de ratón relativo"),
|
||||
("rel-mouse-not-supported-peer-tip", "El modo relativo de ratón no está soportado por el par."),
|
||||
("rel-mouse-not-ready-tip", "El modo relativo de ratón aún no está preparado. Por favor, inténtalo de nuevo."),
|
||||
("rel-mouse-lock-failed-tip", "Ha fallado el bloqueo del cursor. El modo relativo del ratón ha sido inhabilitado."),
|
||||
("rel-mouse-exit-{}-tip", "Pulsa {} para salir."),
|
||||
("rel-mouse-permission-lost-tip", "Permiso de teclado revocado. El modo relativo del ratón ha sido inhabilitado."),
|
||||
("Changelog", "Registro de cambios"),
|
||||
("keep-awake-during-outgoing-sessions-label", "Mantener la pantalla activa durante sesiones salientes"),
|
||||
("keep-awake-during-incoming-sessions-label", "Mantener la pantalla activa durante sesiones entrantes"),
|
||||
("Continue with {}", "Continuar con {}"),
|
||||
("Display Name", ""),
|
||||
("password-hidden-tip", ""),
|
||||
("preset-password-in-use-tip", ""),
|
||||
("Enable privacy mode", ""),
|
||||
("Display Name", "Nombre de pantalla"),
|
||||
("password-hidden-tip", "La contraseña permanente está ajustada a (oculta)."),
|
||||
("preset-password-in-use-tip", "Se está usando la contraseña predeterminada."),
|
||||
("Enable privacy mode", "Habilitar modo privado"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -743,6 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Display Name", "Kijelző név"),
|
||||
("password-hidden-tip", "Állandó jelszó lett beállítva (rejtett)."),
|
||||
("preset-password-in-use-tip", "Jelenleg az alapértelmezett jelszót használja."),
|
||||
("Enable privacy mode", ""),
|
||||
("Enable privacy mode", "Adatvédelmi mód aktiválása"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -743,6 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Display Name", "Naam Weergeven"),
|
||||
("password-hidden-tip", "Er is een permanent wachtwoord ingesteld (verborgen)."),
|
||||
("preset-password-in-use-tip", "Het basis wachtwoord is momenteel in gebruik."),
|
||||
("Enable privacy mode", ""),
|
||||
("Enable privacy mode", "Privacymodus inschakelen"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
81
src/main.rs
81
src/main.rs
@@ -38,49 +38,68 @@ fn main() {
|
||||
if !common::global_init() {
|
||||
return;
|
||||
}
|
||||
use clap::App;
|
||||
use clap::{Arg, ArgAction, Command};
|
||||
use hbb_common::log;
|
||||
let args = format!(
|
||||
"-p, --port-forward=[PORT-FORWARD-OPTIONS] 'Format: remote-id:local-port:remote-port[:remote-host]'
|
||||
-c, --connect=[REMOTE_ID] 'test only'
|
||||
-k, --key=[KEY] ''
|
||||
-s, --server=[] 'Start server'",
|
||||
);
|
||||
let matches = App::new("rustdesk")
|
||||
let matches = Command::new("rustdesk")
|
||||
.version(crate::VERSION)
|
||||
.author("Purslane Ltd<info@rustdesk.com>")
|
||||
.about("RustDesk command line tool")
|
||||
.args_from_usage(&args)
|
||||
.arg(
|
||||
Arg::new("port-forward")
|
||||
.short('p')
|
||||
.long("port-forward")
|
||||
.value_name("PORT-FORWARD-OPTIONS")
|
||||
.help("Format: remote-id:local-port:remote-port[:remote-host]"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("connect")
|
||||
.short('c')
|
||||
.long("connect")
|
||||
.value_name("REMOTE_ID")
|
||||
.help("test only"),
|
||||
)
|
||||
.arg(Arg::new("key").short('k').long("key").value_name("KEY"))
|
||||
.arg(
|
||||
Arg::new("server")
|
||||
.short('s')
|
||||
.long("server")
|
||||
.action(ArgAction::SetTrue)
|
||||
.help("Start server"),
|
||||
)
|
||||
.get_matches();
|
||||
use hbb_common::{config::LocalConfig, env_logger::*};
|
||||
init_from_env(Env::default().filter_or(DEFAULT_FILTER_ENV, "info"));
|
||||
if let Some(p) = matches.value_of("port-forward") {
|
||||
let options: Vec<String> = p.split(":").map(|x| x.to_owned()).collect();
|
||||
if let Some(p) = matches.get_one::<String>("port-forward") {
|
||||
let options: Vec<String> = p.split(':').map(|x| x.to_owned()).collect();
|
||||
if options.len() < 3 {
|
||||
log::error!("Wrong port-forward options");
|
||||
return;
|
||||
}
|
||||
let mut port = 0;
|
||||
if let Ok(v) = options[1].parse::<i32>() {
|
||||
port = v;
|
||||
} else {
|
||||
log::error!("Wrong local-port");
|
||||
return;
|
||||
}
|
||||
let mut remote_port = 0;
|
||||
if let Ok(v) = options[2].parse::<i32>() {
|
||||
remote_port = v;
|
||||
} else {
|
||||
log::error!("Wrong remote-port");
|
||||
return;
|
||||
}
|
||||
let port = match options[1].parse::<i32>() {
|
||||
Ok(v) => v,
|
||||
Err(_) => {
|
||||
log::error!("Wrong local-port");
|
||||
return;
|
||||
}
|
||||
};
|
||||
let remote_port = match options[2].parse::<i32>() {
|
||||
Ok(v) => v,
|
||||
Err(_) => {
|
||||
log::error!("Wrong remote-port");
|
||||
return;
|
||||
}
|
||||
};
|
||||
let mut remote_host = "localhost".to_owned();
|
||||
if options.len() > 3 {
|
||||
remote_host = options[3].clone();
|
||||
}
|
||||
common::test_rendezvous_server();
|
||||
common::test_nat_type();
|
||||
let key = matches.value_of("key").unwrap_or("").to_owned();
|
||||
let key = matches
|
||||
.get_one::<String>("key")
|
||||
.map(String::as_str)
|
||||
.unwrap_or("")
|
||||
.to_owned();
|
||||
let token = LocalConfig::get_option("access_token");
|
||||
cli::start_one_port_forward(
|
||||
options[0].clone(),
|
||||
@@ -90,13 +109,17 @@ fn main() {
|
||||
key,
|
||||
token,
|
||||
);
|
||||
} else if let Some(p) = matches.value_of("connect") {
|
||||
} else if let Some(p) = matches.get_one::<String>("connect") {
|
||||
common::test_rendezvous_server();
|
||||
common::test_nat_type();
|
||||
let key = matches.value_of("key").unwrap_or("").to_owned();
|
||||
let key = matches
|
||||
.get_one::<String>("key")
|
||||
.map(String::as_str)
|
||||
.unwrap_or("")
|
||||
.to_owned();
|
||||
let token = LocalConfig::get_option("access_token");
|
||||
cli::connect_test(p, key, token);
|
||||
} else if let Some(p) = matches.value_of("server") {
|
||||
} else if matches.get_flag("server") {
|
||||
log::info!("id={}", hbb_common::config::Config::get_id());
|
||||
crate::start_server(true, false);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ use super::{linux::*, ResultType};
|
||||
use crate::client::{
|
||||
LOGIN_MSG_DESKTOP_NO_DESKTOP, LOGIN_MSG_DESKTOP_SESSION_ANOTHER_USER,
|
||||
LOGIN_MSG_DESKTOP_SESSION_NOT_READY, LOGIN_MSG_DESKTOP_XORG_NOT_FOUND,
|
||||
LOGIN_MSG_DESKTOP_XSESSION_FAILED,
|
||||
LOGIN_MSG_DESKTOP_XSESSION_FAILED, LOGIN_MSG_PASSWORD_WRONG,
|
||||
};
|
||||
use hbb_common::{
|
||||
allow_err, bail, log,
|
||||
@@ -94,6 +94,49 @@ fn detect_headless() -> Option<&'static str> {
|
||||
None
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||
enum XSessionStartErrorKind {
|
||||
Auth,
|
||||
Env,
|
||||
}
|
||||
|
||||
const XSESSION_AUTH_FAILURE_DETAIL: &str = "authentication failed";
|
||||
|
||||
#[derive(Debug)]
|
||||
struct XSessionStartError {
|
||||
kind: XSessionStartErrorKind,
|
||||
detail: String,
|
||||
}
|
||||
|
||||
impl XSessionStartError {
|
||||
fn auth(detail: String) -> Self {
|
||||
Self {
|
||||
kind: XSessionStartErrorKind::Auth,
|
||||
detail,
|
||||
}
|
||||
}
|
||||
|
||||
fn env(detail: String) -> Self {
|
||||
Self {
|
||||
kind: XSessionStartErrorKind::Env,
|
||||
detail,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for XSessionStartError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.detail)
|
||||
}
|
||||
}
|
||||
|
||||
fn map_xsession_start_error_to_login_msg(kind: XSessionStartErrorKind) -> &'static str {
|
||||
match kind {
|
||||
XSessionStartErrorKind::Auth => LOGIN_MSG_PASSWORD_WRONG,
|
||||
XSessionStartErrorKind::Env => LOGIN_MSG_DESKTOP_XSESSION_FAILED,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_start_desktop(_username: &str, _passsword: &str) -> String {
|
||||
debug_assert!(crate::is_server());
|
||||
if _username.is_empty() {
|
||||
@@ -136,14 +179,21 @@ pub fn try_start_desktop(_username: &str, _passsword: &str) -> String {
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Failed to start xsession {}", e);
|
||||
LOGIN_MSG_DESKTOP_XSESSION_FAILED.to_owned()
|
||||
match e.kind {
|
||||
XSessionStartErrorKind::Auth => {
|
||||
log::warn!("Failed to authenticate xsession user {}", e);
|
||||
}
|
||||
XSessionStartErrorKind::Env => {
|
||||
log::error!("Failed to start xsession {}", e);
|
||||
}
|
||||
}
|
||||
map_xsession_start_error_to_login_msg(e.kind).to_owned()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn try_start_x_session(username: &str, password: &str) -> ResultType<(String, bool)> {
|
||||
fn try_start_x_session(username: &str, password: &str) -> Result<(String, bool), XSessionStartError> {
|
||||
let mut desktop_manager = DESKTOP_MANAGER.lock().unwrap();
|
||||
if let Some(desktop_manager) = &mut (*desktop_manager) {
|
||||
if let Some(seat0_username) = desktop_manager.get_supported_display_seat0_username() {
|
||||
@@ -161,7 +211,9 @@ fn try_start_x_session(username: &str, password: &str) -> ResultType<(String, bo
|
||||
desktop_manager.is_running(),
|
||||
))
|
||||
} else {
|
||||
bail!(crate::client::LOGIN_MSG_DESKTOP_NOT_INITED);
|
||||
Err(XSessionStartError::env(
|
||||
crate::client::LOGIN_MSG_DESKTOP_NOT_INITED.to_owned(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,10 +299,15 @@ impl DesktopManager {
|
||||
self.is_child_running.load(Ordering::SeqCst)
|
||||
}
|
||||
|
||||
fn try_start_x_session(&mut self, username: &str, password: &str) -> ResultType<()> {
|
||||
fn try_start_x_session(
|
||||
&mut self,
|
||||
username: &str,
|
||||
password: &str,
|
||||
) -> Result<(), XSessionStartError> {
|
||||
match get_user_by_name(username) {
|
||||
Some(userinfo) => {
|
||||
let mut client = pam::Client::with_password(&pam_get_service_name())?;
|
||||
let mut client = pam::Client::with_password(&pam_get_service_name())
|
||||
.map_err(|e| XSessionStartError::env(format!("failed to init pam client, {}", e)))?;
|
||||
client
|
||||
.conversation_mut()
|
||||
.set_credentials(username, password);
|
||||
@@ -267,17 +324,24 @@ impl DesktopManager {
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
bail!("failed to start x session, {}", e);
|
||||
Err(XSessionStartError::env(format!(
|
||||
"failed to start x session, {}",
|
||||
e
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
bail!("failed to check user pass for {}, {}", username, e);
|
||||
Err(_e) => {
|
||||
Err(XSessionStartError::auth(
|
||||
XSESSION_AUTH_FAILURE_DETAIL.to_owned(),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
bail!("failed to get userinfo of {}", username);
|
||||
Err(XSessionStartError::auth(
|
||||
XSESSION_AUTH_FAILURE_DETAIL.to_owned(),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
net::SocketAddr,
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
@@ -21,8 +22,13 @@ use hbb_common::{
|
||||
rendezvous_proto::*,
|
||||
sleep,
|
||||
socket_client::{self, connect_tcp, is_ipv4, new_direct_udp_for, new_udp_for},
|
||||
tokio::{self, select, sync::Mutex, time::interval},
|
||||
tokio::{
|
||||
self, select,
|
||||
sync::{mpsc, Mutex},
|
||||
time::interval,
|
||||
},
|
||||
udp::FramedSocket,
|
||||
webrtc::WebRTCStream,
|
||||
AddrMangle, IntoTargetAddr, ResultType, Stream, TargetAddr,
|
||||
};
|
||||
|
||||
@@ -32,15 +38,41 @@ use crate::{
|
||||
};
|
||||
|
||||
type Message = RendezvousMessage;
|
||||
type RendezvousSender = mpsc::UnboundedSender<Message>;
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref SOLVING_PK_MISMATCH: Mutex<String> = Default::default();
|
||||
static ref LAST_MSG: Mutex<(SocketAddr, Instant)> = Mutex::new((SocketAddr::new([0; 4].into(), 0), Instant::now()));
|
||||
static ref LAST_RELAY_MSG: Mutex<(SocketAddr, Instant)> = Mutex::new((SocketAddr::new([0; 4].into(), 0), Instant::now()));
|
||||
static ref WEBRTC_ICE_TXS: Mutex<HashMap<String, mpsc::UnboundedSender<String>>> = Default::default();
|
||||
}
|
||||
static SHOULD_EXIT: AtomicBool = AtomicBool::new(false);
|
||||
static MANUAL_RESTARTED: AtomicBool = AtomicBool::new(false);
|
||||
static SENT_REGISTER_PK: AtomicBool = AtomicBool::new(false);
|
||||
pub(crate) static NEEDS_DEPLOY: AtomicBool = AtomicBool::new(false);
|
||||
// register_pk retry interval (ms) when device is awaiting deployment
|
||||
const DEPLOY_RETRY_INTERVAL: i64 = 30_000;
|
||||
lazy_static::lazy_static! {
|
||||
static ref LAST_NOT_DEPLOYED_REGISTER: Mutex<Option<Instant>> = Mutex::new(None);
|
||||
}
|
||||
|
||||
// Single source of truth for the "awaiting deployment" backoff. The server has
|
||||
// already told us this device is not in its db; until the operator runs
|
||||
// `rustdesk --deploy --token <api_token>` there is no point re-running the
|
||||
// register path more often than DEPLOY_RETRY_INTERVAL. Gating in the timer
|
||||
// loops (rather than only inside register_pk) also avoids the
|
||||
// last_register_sent / fails / latency / UDP-rebind churn the loop would
|
||||
// otherwise spin on while no response ever comes back.
|
||||
async fn deploy_register_throttled() -> bool {
|
||||
if !NEEDS_DEPLOY.load(Ordering::SeqCst) {
|
||||
return false;
|
||||
}
|
||||
LAST_NOT_DEPLOYED_REGISTER
|
||||
.lock()
|
||||
.await
|
||||
.map(|t| (t.elapsed().as_millis() as i64) < DEPLOY_RETRY_INTERVAL)
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RendezvousMediator {
|
||||
@@ -48,6 +80,7 @@ pub struct RendezvousMediator {
|
||||
host: String,
|
||||
host_prefix: String,
|
||||
keep_alive: i32,
|
||||
rz_sender: RendezvousSender,
|
||||
}
|
||||
|
||||
impl RendezvousMediator {
|
||||
@@ -158,11 +191,13 @@ impl RendezvousMediator {
|
||||
let host = check_port(&host, RENDEZVOUS_PORT);
|
||||
log::info!("start udp: {host}");
|
||||
let (mut socket, mut addr) = new_udp_for(&host, CONNECT_TIMEOUT).await?;
|
||||
let (rz_sender, mut rz_out_rx) = mpsc::unbounded_channel::<Message>();
|
||||
let mut rz = Self {
|
||||
addr: addr.clone(),
|
||||
host: host.clone(),
|
||||
host_prefix: Self::get_host_prefix(&host),
|
||||
keep_alive: crate::DEFAULT_KEEP_ALIVE,
|
||||
rz_sender,
|
||||
};
|
||||
|
||||
let mut timer = crate::rustdesk_interval(interval(crate::TIMER_OUT));
|
||||
@@ -222,10 +257,21 @@ impl RendezvousMediator {
|
||||
},
|
||||
}
|
||||
},
|
||||
Some(msg_out) = rz_out_rx.recv() => {
|
||||
Sink::Framed(&mut socket, &addr).send(&msg_out).await?;
|
||||
},
|
||||
_ = timer.tick() => {
|
||||
if SHOULD_EXIT.load(Ordering::SeqCst) {
|
||||
break;
|
||||
}
|
||||
// The server already told us this device is not deployed. Skip
|
||||
// the whole register / fails / latency / UDP-rebind path until
|
||||
// DEPLOY_RETRY_INTERVAL elapses, otherwise the loop spins every
|
||||
// few seconds (log spam + misapplied network-recovery rebind)
|
||||
// until the operator runs `rustdesk --deploy`.
|
||||
if deploy_register_throttled().await {
|
||||
continue;
|
||||
}
|
||||
let now = Some(Instant::now());
|
||||
let expired = last_register_resp.map(|x| x.elapsed().as_millis() as i64 >= REG_INTERVAL).unwrap_or(true);
|
||||
let timeout = last_register_sent.map(|x| x.elapsed().as_millis() as i64 >= reg_timeout).unwrap_or(false);
|
||||
@@ -289,10 +335,22 @@ impl RendezvousMediator {
|
||||
Config::set_key_confirmed(true);
|
||||
Config::set_host_key_confirmed(&self.host_prefix, true);
|
||||
*SOLVING_PK_MISMATCH.lock().await = "".to_owned();
|
||||
NEEDS_DEPLOY.store(false, Ordering::SeqCst);
|
||||
}
|
||||
Ok(register_pk_response::Result::UUID_MISMATCH) => {
|
||||
self.handle_uuid_mismatch(sink).await?;
|
||||
}
|
||||
Ok(register_pk_response::Result::NOT_DEPLOYED) => {
|
||||
if !NEEDS_DEPLOY.load(Ordering::SeqCst) {
|
||||
log::warn!("Server requires deployment. Run `rustdesk --deploy --token <api_token>` on this device.");
|
||||
}
|
||||
NEEDS_DEPLOY.store(true, Ordering::SeqCst);
|
||||
// Clear key_confirmed so the UI reflects the truth: this device is
|
||||
// not currently registered. Covers the case where an online device
|
||||
// was deleted by an admin while running.
|
||||
Config::set_key_confirmed(false);
|
||||
Config::set_host_key_confirmed(&self.host_prefix, false);
|
||||
}
|
||||
_ => {
|
||||
log::error!("unknown RegisterPkResponse");
|
||||
}
|
||||
@@ -323,6 +381,17 @@ impl RendezvousMediator {
|
||||
allow_err!(rz.handle_intranet(fla, server).await);
|
||||
});
|
||||
}
|
||||
Some(rendezvous_message::Union::IceCandidate(ice)) => {
|
||||
let tx = WEBRTC_ICE_TXS.lock().await.get(&ice.session_key).cloned();
|
||||
if let Some(tx) = tx {
|
||||
let _ = tx.send(ice.candidate);
|
||||
} else {
|
||||
log::debug!(
|
||||
"dropping ICE candidate for unknown WebRTC session key {}",
|
||||
ice.session_key
|
||||
);
|
||||
}
|
||||
}
|
||||
Some(rendezvous_message::Union::ConfigureUpdate(cu)) => {
|
||||
let v0 = Config::get_rendezvous_servers();
|
||||
Config::set_option(
|
||||
@@ -345,11 +414,13 @@ impl RendezvousMediator {
|
||||
let mut conn = connect_tcp(host.clone(), CONNECT_TIMEOUT).await?;
|
||||
let key = crate::get_key(true).await;
|
||||
crate::secure_tcp(&mut conn, &key).await?;
|
||||
let (rz_sender, mut rz_out_rx) = mpsc::unbounded_channel::<Message>();
|
||||
let mut rz = Self {
|
||||
addr: conn.local_addr().into_target_addr()?,
|
||||
host: host.clone(),
|
||||
host_prefix: Self::get_host_prefix(&host),
|
||||
keep_alive: crate::DEFAULT_KEEP_ALIVE,
|
||||
rz_sender,
|
||||
};
|
||||
let mut timer = crate::rustdesk_interval(interval(crate::TIMER_OUT));
|
||||
let mut last_register_sent: Option<Instant> = None;
|
||||
@@ -377,6 +448,9 @@ impl RendezvousMediator {
|
||||
let msg = Message::parse_from_bytes(&bytes)?;
|
||||
rz.handle_resp(msg.union, Sink::Stream(&mut conn), &server, &mut update_latency).await?
|
||||
}
|
||||
Some(msg_out) = rz_out_rx.recv() => {
|
||||
Sink::Stream(&mut conn).send(&msg_out).await?;
|
||||
}
|
||||
_ = timer.tick() => {
|
||||
if SHOULD_EXIT.load(Ordering::SeqCst) {
|
||||
break;
|
||||
@@ -428,6 +502,7 @@ impl RendezvousMediator {
|
||||
rr.secure,
|
||||
false,
|
||||
Default::default(),
|
||||
String::new(),
|
||||
rr.control_permissions.clone().into_option(),
|
||||
)
|
||||
.await
|
||||
@@ -442,6 +517,7 @@ impl RendezvousMediator {
|
||||
secure: bool,
|
||||
initiate: bool,
|
||||
socket_addr_v6: bytes::Bytes,
|
||||
webrtc_sdp_answer: String,
|
||||
control_permissions: Option<ControlPermissions>,
|
||||
) -> ResultType<()> {
|
||||
let peer_addr = AddrMangle::decode(&socket_addr);
|
||||
@@ -460,6 +536,7 @@ impl RendezvousMediator {
|
||||
socket_addr: socket_addr.into(),
|
||||
version: crate::VERSION.to_owned(),
|
||||
socket_addr_v6,
|
||||
webrtc_sdp_answer,
|
||||
..Default::default()
|
||||
};
|
||||
if initiate {
|
||||
@@ -527,6 +604,7 @@ impl RendezvousMediator {
|
||||
true,
|
||||
true,
|
||||
socket_addr_v6,
|
||||
String::new(),
|
||||
fla.control_permissions.into_option(),
|
||||
)
|
||||
.await
|
||||
@@ -569,6 +647,81 @@ impl RendezvousMediator {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn spawn_webrtc_answerer(
|
||||
&self,
|
||||
ph: &PunchHole,
|
||||
force_relay: bool,
|
||||
server: ServerPtr,
|
||||
peer_addr: SocketAddr,
|
||||
control_permissions: Option<ControlPermissions>,
|
||||
) -> ResultType<String> {
|
||||
let mut stream =
|
||||
WebRTCStream::new(&ph.webrtc_sdp_offer, force_relay, CONNECT_TIMEOUT).await?;
|
||||
let answer = stream.get_local_endpoint().await?;
|
||||
let session_key = stream.session_key().to_owned();
|
||||
let return_route = ph.socket_addr.clone();
|
||||
|
||||
let (remote_ice_tx, mut remote_ice_rx) = mpsc::unbounded_channel::<String>();
|
||||
WEBRTC_ICE_TXS
|
||||
.lock()
|
||||
.await
|
||||
.insert(session_key.clone(), remote_ice_tx);
|
||||
|
||||
let stream_for_remote_ice = stream.clone();
|
||||
tokio::spawn(async move {
|
||||
while let Some(candidate) = remote_ice_rx.recv().await {
|
||||
if let Err(err) = stream_for_remote_ice.add_remote_ice_candidate(&candidate).await
|
||||
{
|
||||
log::warn!("failed to add remote WebRTC ICE candidate: {}", err);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(mut local_ice_rx) = stream.take_local_ice_rx() {
|
||||
let sender = self.rz_sender.clone();
|
||||
let socket_addr = return_route.clone();
|
||||
let session_key_for_ice = session_key.clone();
|
||||
tokio::spawn(async move {
|
||||
while let Some(candidate) = local_ice_rx.recv().await {
|
||||
let mut msg = Message::new();
|
||||
msg.set_ice_candidate(IceCandidate {
|
||||
socket_addr: socket_addr.clone(),
|
||||
session_key: session_key_for_ice.clone(),
|
||||
candidate,
|
||||
..Default::default()
|
||||
});
|
||||
let _ = sender.send(msg);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let session_key_for_cleanup = session_key.clone();
|
||||
tokio::spawn(async move {
|
||||
let result = stream.wait_connected(CONNECT_TIMEOUT).await;
|
||||
WEBRTC_ICE_TXS
|
||||
.lock()
|
||||
.await
|
||||
.remove(&session_key_for_cleanup);
|
||||
if let Err(err) = result {
|
||||
log::warn!("webrtc wait_connected failed: {}", err);
|
||||
return;
|
||||
}
|
||||
if let Err(err) = crate::server::create_tcp_connection(
|
||||
server,
|
||||
Stream::WebRTC(stream),
|
||||
peer_addr,
|
||||
true,
|
||||
control_permissions,
|
||||
)
|
||||
.await
|
||||
{
|
||||
log::warn!("failed to create WebRTC server connection: {}", err);
|
||||
}
|
||||
});
|
||||
|
||||
Ok(answer)
|
||||
}
|
||||
|
||||
async fn handle_punch_hole(&self, ph: PunchHole, server: ServerPtr) -> ResultType<()> {
|
||||
let mut peer_addr = AddrMangle::decode(&ph.socket_addr);
|
||||
let last = *LAST_MSG.lock().await;
|
||||
@@ -580,7 +733,23 @@ impl RendezvousMediator {
|
||||
let peer_addr_v6 = hbb_common::AddrMangle::decode(&ph.socket_addr_v6);
|
||||
let relay = use_ws() || Config::is_proxy() || ph.force_relay;
|
||||
let mut socket_addr_v6 = Default::default();
|
||||
let control_permissions = ph.control_permissions.into_option();
|
||||
let control_permissions = ph.control_permissions.clone().into_option();
|
||||
let webrtc_sdp_answer = if !ph.webrtc_sdp_offer.is_empty() {
|
||||
self.spawn_webrtc_answerer(
|
||||
&ph,
|
||||
relay,
|
||||
server.clone(),
|
||||
peer_addr,
|
||||
control_permissions.clone(),
|
||||
)
|
||||
.await
|
||||
.unwrap_or_else(|err| {
|
||||
log::warn!("failed to create WebRTC answer: {}", err);
|
||||
String::new()
|
||||
})
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
if peer_addr_v6.port() > 0 && !relay {
|
||||
socket_addr_v6 = start_ipv6(
|
||||
peer_addr_v6,
|
||||
@@ -607,6 +776,7 @@ impl RendezvousMediator {
|
||||
true,
|
||||
true,
|
||||
socket_addr_v6.clone(),
|
||||
webrtc_sdp_answer.clone(),
|
||||
control_permissions,
|
||||
)
|
||||
.await;
|
||||
@@ -620,6 +790,7 @@ impl RendezvousMediator {
|
||||
nat_type: nat_type.into(),
|
||||
version: crate::VERSION.to_owned(),
|
||||
socket_addr_v6,
|
||||
webrtc_sdp_answer,
|
||||
..Default::default()
|
||||
};
|
||||
if ph.udp_port > 0 {
|
||||
@@ -678,6 +849,21 @@ impl RendezvousMediator {
|
||||
}
|
||||
|
||||
async fn register_pk(&mut self, socket: Sink<'_>) -> ResultType<()> {
|
||||
// Throttle register_pk when the device is awaiting deployment: server
|
||||
// already told us we're not in its db; sending more often than every
|
||||
// DEPLOY_RETRY_INTERVAL ms is wasted traffic until the operator runs
|
||||
// `rustdesk --deploy --token <api_token>`.
|
||||
if NEEDS_DEPLOY.load(Ordering::SeqCst) {
|
||||
let mut last = LAST_NOT_DEPLOYED_REGISTER.lock().await;
|
||||
if let Some(t) = *last {
|
||||
if (t.elapsed().as_millis() as i64) < DEPLOY_RETRY_INTERVAL {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
*last = Some(Instant::now());
|
||||
} else {
|
||||
*LAST_NOT_DEPLOYED_REGISTER.lock().await = None;
|
||||
}
|
||||
let mut msg_out = Message::new();
|
||||
let pk = Config::get_key_pair().1;
|
||||
let uuid = hbb_common::get_uuid();
|
||||
|
||||
@@ -67,6 +67,7 @@ pub mod input_service {
|
||||
}
|
||||
|
||||
mod connection;
|
||||
mod login_failure_check;
|
||||
pub mod display_service;
|
||||
#[cfg(windows)]
|
||||
pub mod portable_service;
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
#[cfg(target_os = "windows")]
|
||||
use super::login_failure_check::try_acquire_os_credential_login_gate;
|
||||
use super::login_failure_check::{
|
||||
evaluate_os_credential_policy, record_os_credential_failure, FailureScope,
|
||||
};
|
||||
use super::{input_service::*, *};
|
||||
#[cfg(feature = "unix-file-copy-paste")]
|
||||
use crate::clipboard::try_empty_clipboard_files;
|
||||
@@ -82,6 +87,9 @@ lazy_static::lazy_static! {
|
||||
static ref PENDING_SWITCH_SIDES_UUID: Arc::<Mutex<HashMap<String, (Instant, uuid::Uuid)>>> = Default::default();
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
const TERMINAL_OS_LOGIN_FAILED_MSG: &str = "Incorrect username or password.";
|
||||
|
||||
fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
|
||||
if a.len() != b.len() {
|
||||
return false;
|
||||
@@ -94,6 +102,32 @@ fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
|
||||
x == 0
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn should_check_linux_headless_os_auth_before_desktop_start(
|
||||
is_headless_allowed: bool,
|
||||
username: &str,
|
||||
) -> bool {
|
||||
is_headless_allowed
|
||||
&& !username.trim().is_empty()
|
||||
&& linux_desktop_manager::get_username().is_empty()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn should_record_linux_headless_os_auth_failure(
|
||||
is_headless_allowed: bool,
|
||||
username: &str,
|
||||
err_msg: &str,
|
||||
) -> bool {
|
||||
is_headless_allowed
|
||||
&& !username.trim().is_empty()
|
||||
&& err_msg == crate::client::LOGIN_MSG_PASSWORD_WRONG
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
fn should_use_terminal_os_login_scope(is_terminal: bool, os_login_username: &str) -> bool {
|
||||
cfg!(target_os = "windows") && is_terminal && !os_login_username.trim().is_empty()
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "windows", target_os = "linux"))]
|
||||
lazy_static::lazy_static! {
|
||||
static ref WALLPAPER_REMOVER: Arc<Mutex<Option<WallPaperRemover>>> = Default::default();
|
||||
@@ -1497,6 +1531,9 @@ impl Connection {
|
||||
// Keep the connection alive so the client can continue with 2FA.
|
||||
return true;
|
||||
}
|
||||
if let Some(keep_alive) = self.prepare_terminal_login_for_authorization().await {
|
||||
return keep_alive;
|
||||
}
|
||||
if !self.connect_port_forward_if_needed().await {
|
||||
return false;
|
||||
}
|
||||
@@ -2376,33 +2413,6 @@ impl Connection {
|
||||
o.terminal_persistent.enum_value() == Ok(BoolOption::Yes);
|
||||
}
|
||||
self.terminal_service_id = terminal.service_id;
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
if let Some(msg) =
|
||||
self.fill_terminal_user_token(&lr.os_login.username, &lr.os_login.password)
|
||||
{
|
||||
self.send_login_error(msg).await;
|
||||
sleep(1.).await;
|
||||
return false;
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
if let Some(is_user) =
|
||||
terminal_service::is_service_specified_user(&self.terminal_service_id)
|
||||
{
|
||||
if let Some(user_token) = &self.terminal_user_token {
|
||||
let has_service_token =
|
||||
user_token.to_terminal_service_token().is_some();
|
||||
if is_user != has_service_token {
|
||||
// This occurs when the service id (in the configuration) is manually changed by the user, causing a mismatch in validation.
|
||||
log::error!("Terminal service user mismatch detected. The service ID may have been manually changed in the configuration, causing validation to fail.");
|
||||
// No need to translate the following message, because it is in an abnormal case.
|
||||
self.send_login_error("Terminal service user mismatch detected.")
|
||||
.await;
|
||||
sleep(1.).await;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(login_request::Union::PortForward(mut pf)) => {
|
||||
if !Self::permission(keys::OPTION_ENABLE_TUNNEL, &self.control_permissions) {
|
||||
@@ -2420,8 +2430,43 @@ impl Connection {
|
||||
}
|
||||
}
|
||||
|
||||
if !hbb_common::is_ip_str(&lr.username)
|
||||
&& !hbb_common::is_domain_port_str(&lr.username)
|
||||
&& lr.username != Config::get_id()
|
||||
{
|
||||
self.send_login_error(crate::client::LOGIN_MSG_OFFLINE)
|
||||
.await;
|
||||
return false;
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
if self.terminal
|
||||
&& lr.os_login.username.trim().is_empty()
|
||||
&& crate::platform::is_prelogin()
|
||||
{
|
||||
self.send_login_error(
|
||||
"No active console user logged on, please connect and logon first.",
|
||||
)
|
||||
.await;
|
||||
sleep(1.).await;
|
||||
return false;
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
self.try_start_cm_ipc();
|
||||
if !should_use_terminal_os_login_scope(self.terminal, &lr.os_login.username) {
|
||||
self.try_start_cm_ipc();
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
if should_check_linux_headless_os_auth_before_desktop_start(
|
||||
self.linux_headless_handle.is_headless_allowed,
|
||||
&lr.os_login.username,
|
||||
) {
|
||||
let (_failure, res) = self.check_failure(0).await;
|
||||
if !res {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
let err_msg = "".to_owned();
|
||||
@@ -2433,6 +2478,18 @@ impl Connection {
|
||||
// If err is LOGIN_MSG_DESKTOP_SESSION_NOT_READY, just keep this msg and go on checking password.
|
||||
if !err_msg.is_empty() && err_msg != crate::client::LOGIN_MSG_DESKTOP_SESSION_NOT_READY
|
||||
{
|
||||
#[cfg(target_os = "linux")]
|
||||
if should_record_linux_headless_os_auth_failure(
|
||||
self.linux_headless_handle.is_headless_allowed,
|
||||
&lr.os_login.username,
|
||||
&err_msg,
|
||||
) {
|
||||
let (failure, res) = self.check_failure(0).await;
|
||||
if !res {
|
||||
return true;
|
||||
}
|
||||
self.update_failure(failure, false, 0);
|
||||
}
|
||||
self.send_login_error(err_msg).await;
|
||||
return true;
|
||||
}
|
||||
@@ -2461,17 +2518,16 @@ impl Connection {
|
||||
crate::get_builtin_option(keys::OPTION_ALLOW_LOGON_SCREEN_PASSWORD) == "Y"
|
||||
&& is_logon();
|
||||
|
||||
if !hbb_common::is_ip_str(&lr.username)
|
||||
&& !hbb_common::is_domain_port_str(&lr.username)
|
||||
&& lr.username != Config::get_id()
|
||||
{
|
||||
self.send_login_error(crate::client::LOGIN_MSG_OFFLINE)
|
||||
.await;
|
||||
return false;
|
||||
} else if (password::approve_mode() == ApproveMode::Click
|
||||
&& !allow_logon_screen_password)
|
||||
if (password::approve_mode() == ApproveMode::Click && !allow_logon_screen_password)
|
||||
|| password::approve_mode() == ApproveMode::Both && !password::has_valid_password()
|
||||
{
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
if should_use_terminal_os_login_scope(self.terminal, &lr.os_login.username) {
|
||||
if let Some(keep_alive) = self.prepare_terminal_login_for_authorization().await
|
||||
{
|
||||
return keep_alive;
|
||||
}
|
||||
}
|
||||
self.try_start_cm(lr.my_id, lr.my_name, false);
|
||||
if hbb_common::get_version_number(&lr.version)
|
||||
>= hbb_common::get_version_number("1.2.0")
|
||||
@@ -2493,6 +2549,14 @@ impl Connection {
|
||||
}
|
||||
} else if lr.password.is_empty() {
|
||||
if err_msg.is_empty() {
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
if should_use_terminal_os_login_scope(self.terminal, &lr.os_login.username) {
|
||||
if let Some(keep_alive) =
|
||||
self.prepare_terminal_login_for_authorization().await
|
||||
{
|
||||
return keep_alive;
|
||||
}
|
||||
}
|
||||
self.try_start_cm(lr.my_id, lr.my_name, false);
|
||||
} else {
|
||||
self.send_login_error(
|
||||
@@ -2506,7 +2570,7 @@ impl Connection {
|
||||
return true;
|
||||
}
|
||||
if !self.validate_password(allow_logon_screen_password) {
|
||||
self.update_failure(failure, false, 0);
|
||||
self.update_failure_with_scope(failure, false, 0, FailureScope::Default);
|
||||
self.check_update_temporary_password(false);
|
||||
if err_msg.is_empty() {
|
||||
self.send_login_error(crate::client::LOGIN_MSG_PASSWORD_WRONG)
|
||||
@@ -2519,7 +2583,7 @@ impl Connection {
|
||||
.await;
|
||||
}
|
||||
} else {
|
||||
self.update_failure(failure, true, 0);
|
||||
self.update_failure_with_scope(failure, true, 0, FailureScope::Default);
|
||||
if err_msg.is_empty() {
|
||||
#[cfg(target_os = "linux")]
|
||||
self.linux_headless_handle.wait_desktop_cm_ready().await;
|
||||
@@ -3484,16 +3548,16 @@ impl Connection {
|
||||
self.terminal_user_token = Some(TerminalUserToken::SelfUser);
|
||||
None
|
||||
} else {
|
||||
Some("The user is not an administrator.")
|
||||
Some(TERMINAL_OS_LOGIN_FAILED_MSG)
|
||||
}
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
log::error!("Failed to check if the user is an administrator: {}", e);
|
||||
Some("Failed to check if the user is an administrator.")
|
||||
Some(TERMINAL_OS_LOGIN_FAILED_MSG)
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Failed to get logon user token: {}", e);
|
||||
Some("Incorrect username or password.")
|
||||
Some(TERMINAL_OS_LOGIN_FAILED_MSG)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3529,6 +3593,146 @@ impl Connection {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
async fn prepare_terminal_login_for_authorization(&mut self) -> Option<bool> {
|
||||
if !self.terminal || self.terminal_user_token.is_some() {
|
||||
return None;
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
enum TerminalAuthorizationMode {
|
||||
OsLogin {
|
||||
failure: ((i32, i32, i32), i32),
|
||||
scope: FailureScope,
|
||||
},
|
||||
SessionUser,
|
||||
}
|
||||
|
||||
let normalized_username = self.lr.os_login.username.trim().to_owned();
|
||||
let auth_mode = if should_use_terminal_os_login_scope(self.terminal, &normalized_username) {
|
||||
// Check failure state
|
||||
let failure_scope = FailureScope::TerminalOsLogin;
|
||||
let (failure, res) = self.check_failure_with_scope(0, failure_scope).await;
|
||||
if !res {
|
||||
log::warn!(
|
||||
"OS credential login blocked by failure policy: ip={} conn_id={} scope={:?}",
|
||||
self.ip,
|
||||
self.inner.id(),
|
||||
failure_scope
|
||||
);
|
||||
// Terminal OS login is sensitive. Close this connection instead of keeping it
|
||||
// alive for retries on the same socket after a rate-limit block.
|
||||
return Some(false);
|
||||
}
|
||||
TerminalAuthorizationMode::OsLogin {
|
||||
failure,
|
||||
scope: failure_scope,
|
||||
}
|
||||
} else {
|
||||
TerminalAuthorizationMode::SessionUser
|
||||
};
|
||||
|
||||
let is_terminal_os_login = matches!(auth_mode, TerminalAuthorizationMode::OsLogin { .. });
|
||||
let failure_scope = match auth_mode {
|
||||
TerminalAuthorizationMode::OsLogin { scope, .. } => scope,
|
||||
TerminalAuthorizationMode::SessionUser => FailureScope::Default,
|
||||
};
|
||||
|
||||
let username = normalized_username;
|
||||
let password = self.lr.os_login.password.clone();
|
||||
let terminal_login_error = {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
// Concurrency gate for terminal OS login with credentials, to prevent brute-force attacks.
|
||||
let _os_login_concurrency_guard = if is_terminal_os_login {
|
||||
let guard = try_acquire_os_credential_login_gate();
|
||||
if guard.is_err() {
|
||||
log::warn!(
|
||||
"OS credential login blocked by concurrency gate: ip={} conn_id={} scope={:?}",
|
||||
self.ip,
|
||||
self.inner.id(),
|
||||
failure_scope
|
||||
);
|
||||
self.send_login_error("Please try 1 minute later").await;
|
||||
sleep(1.).await;
|
||||
Self::post_alarm_audit(
|
||||
AlarmAuditType::TerminalOsLoginConcurrency,
|
||||
json!({
|
||||
"ip": self.ip,
|
||||
"id": self.lr.my_id.clone(),
|
||||
"name": self.lr.my_name.clone(),
|
||||
}),
|
||||
);
|
||||
return Some(false);
|
||||
}
|
||||
guard.ok()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
self.fill_terminal_user_token(&username, &password)
|
||||
}
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
self.fill_terminal_user_token(&username, &password)
|
||||
}
|
||||
};
|
||||
if let Some(msg) = terminal_login_error {
|
||||
if let TerminalAuthorizationMode::OsLogin { failure, scope } = auth_mode {
|
||||
self.update_failure_with_scope(failure, false, 0, scope);
|
||||
}
|
||||
let auth_context = if is_terminal_os_login {
|
||||
"OS credential login verification"
|
||||
} else {
|
||||
"Terminal session-user authorization"
|
||||
};
|
||||
log::warn!(
|
||||
"{} failed: ip={} conn_id={} scope={:?} msg='{}'",
|
||||
auth_context,
|
||||
self.ip,
|
||||
self.inner.id(),
|
||||
failure_scope,
|
||||
msg
|
||||
);
|
||||
self.send_login_error(msg).await;
|
||||
sleep(1.).await;
|
||||
return Some(false);
|
||||
}
|
||||
if let TerminalAuthorizationMode::OsLogin { failure, scope } = auth_mode {
|
||||
self.update_failure_with_scope(failure, true, 0, scope);
|
||||
}
|
||||
|
||||
if let Some(is_user) =
|
||||
terminal_service::is_service_specified_user(&self.terminal_service_id)
|
||||
{
|
||||
if let Some(user_token) = &self.terminal_user_token {
|
||||
let has_service_token = user_token.to_terminal_service_token().is_some();
|
||||
if is_user != has_service_token {
|
||||
log::error!(
|
||||
"Terminal service user mismatch: ip={} conn_id={} service_is_user={} has_service_token={}. The service ID may have been manually changed in the configuration, causing validation to fail.",
|
||||
self.ip,
|
||||
self.inner.id(),
|
||||
is_user,
|
||||
has_service_token
|
||||
);
|
||||
// No need to translate the following message, because it is in an abnormal case.
|
||||
self.send_login_error("Terminal service user mismatch detected.")
|
||||
.await;
|
||||
sleep(1.).await;
|
||||
return Some(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
if is_terminal_os_login {
|
||||
self.try_start_cm_ipc();
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||
async fn prepare_terminal_login_for_authorization(&mut self) -> Option<bool> {
|
||||
None
|
||||
}
|
||||
|
||||
// Try to parse connection IP as IPv6 address, returning /64, /56, and /48 prefixes.
|
||||
// Parsing an IPv4 address just returns None.
|
||||
// note: we specifically don't use hbb_common::is_ipv6_str to avoid divergence issues
|
||||
@@ -3555,18 +3759,37 @@ impl Connection {
|
||||
Some((p64, p56, p48))
|
||||
}
|
||||
|
||||
fn update_failure(&self, (failure, time): ((i32, i32, i32), i32), remove: bool, i: usize) {
|
||||
fn bump(mut cur: (i32, i32, i32), time: i32) -> (i32, i32, i32) {
|
||||
if cur.0 == time {
|
||||
cur.1 += 1;
|
||||
cur.2 += 1;
|
||||
} else {
|
||||
cur.0 = time;
|
||||
cur.1 = 1;
|
||||
cur.2 += 1;
|
||||
}
|
||||
cur
|
||||
fn bump_failure_entry(mut cur: (i32, i32, i32), time: i32) -> (i32, i32, i32) {
|
||||
if cur.0 == time {
|
||||
cur.1 += 1;
|
||||
cur.2 += 1;
|
||||
} else {
|
||||
cur.0 = time;
|
||||
cur.1 = 1;
|
||||
cur.2 += 1;
|
||||
}
|
||||
cur
|
||||
}
|
||||
|
||||
fn update_failure(&self, failure: ((i32, i32, i32), i32), remove: bool, i: usize) {
|
||||
self.update_failure_with_scope(failure, remove, i, FailureScope::Default);
|
||||
}
|
||||
|
||||
fn update_failure_with_scope(
|
||||
&self,
|
||||
(failure, time): ((i32, i32, i32), i32),
|
||||
remove: bool,
|
||||
i: usize,
|
||||
scope: FailureScope,
|
||||
) {
|
||||
let os_credential_scope = matches!(scope, FailureScope::TerminalOsLogin);
|
||||
if os_credential_scope {
|
||||
if !remove {
|
||||
record_os_credential_failure(scope);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let map_mutex = &LOGIN_FAILURES[i];
|
||||
if remove {
|
||||
if failure.0 != 0 {
|
||||
@@ -3587,14 +3810,15 @@ impl Connection {
|
||||
let mut m = map_mutex.lock().unwrap();
|
||||
for key in [p64, p56, p48] {
|
||||
let cur = m.get(&key).copied().unwrap_or((0, 0, 0));
|
||||
m.insert(key, bump(cur, time));
|
||||
m.insert(key, Self::bump_failure_entry(cur, time));
|
||||
}
|
||||
// Update full IP: bump from the *original* passed-in failure
|
||||
m.insert(self.ip.clone(), bump(failure, time));
|
||||
let current_ip = m.get(&self.ip).copied().unwrap_or((0, 0, 0));
|
||||
m.insert(self.ip.clone(), Self::bump_failure_entry(current_ip, time));
|
||||
} else {
|
||||
// Update full IP: bump from the *original* passed-in failure
|
||||
// Re-read the full IP bucket in case another failed attempt updated it.
|
||||
let mut m = map_mutex.lock().unwrap();
|
||||
m.insert(self.ip.clone(), bump(failure, time));
|
||||
let current_ip = m.get(&self.ip).copied().unwrap_or((0, 0, 0));
|
||||
m.insert(self.ip.clone(), Self::bump_failure_entry(current_ip, time));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3634,8 +3858,50 @@ impl Connection {
|
||||
}
|
||||
|
||||
async fn check_failure(&mut self, i: usize) -> (((i32, i32, i32), i32), bool) {
|
||||
self.check_failure_with_scope(i, FailureScope::Default)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn check_failure_with_scope(
|
||||
&mut self,
|
||||
i: usize,
|
||||
scope: FailureScope,
|
||||
) -> (((i32, i32, i32), i32), bool) {
|
||||
let time = (get_time() / 60_000) as i32;
|
||||
|
||||
if matches!(scope, FailureScope::TerminalOsLogin) {
|
||||
let decision = evaluate_os_credential_policy(scope, get_time());
|
||||
let res = if decision.allowed {
|
||||
true
|
||||
} else {
|
||||
log::warn!(
|
||||
"OS credential login blocked by policy: ip={} conn_id={} i={} msg='{}'",
|
||||
self.ip,
|
||||
self.inner.id(),
|
||||
i,
|
||||
decision.login_error.as_deref().unwrap_or("")
|
||||
);
|
||||
if let Some(login_error) = decision.login_error {
|
||||
// Rare branch and currently temporary response copy; translation can be added later if needed.
|
||||
self.send_login_error(login_error).await;
|
||||
}
|
||||
if let Some(audit) = decision.audit {
|
||||
// For OS blocked/backoff events, we currently emit one alarm report per blocked attempt.
|
||||
// TODO: Add unified cumulative/aggregation fields across alarm producers.
|
||||
Self::post_alarm_audit(
|
||||
audit,
|
||||
json!({
|
||||
"ip": self.ip,
|
||||
"id": self.lr.my_id.clone(),
|
||||
"name": self.lr.my_name.clone(),
|
||||
}),
|
||||
);
|
||||
}
|
||||
false
|
||||
};
|
||||
return (((0, 0, 0), time), res);
|
||||
}
|
||||
|
||||
// IPv6 addresses are cheap to make so we check prefix/netblock as well
|
||||
if let Some((p64, p56, p48)) = self.get_ipv6_prefixes() {
|
||||
if let Some(res) = self.check_failure_ipv6_prefix(i, time, &p64, 64, 60).await {
|
||||
@@ -5219,6 +5485,8 @@ pub enum AlarmAuditType {
|
||||
// MultipleLoginsAttemptsWithinOneMinute = 4,
|
||||
// MultipleLoginsAttemptsWithinOneHour = 5,
|
||||
ExceedIPv6PrefixAttempts = 6,
|
||||
TerminalOsLoginBackoff = 7,
|
||||
TerminalOsLoginConcurrency = 8,
|
||||
}
|
||||
|
||||
pub enum FileAuditType {
|
||||
|
||||
231
src/server/login_failure_check.rs
Normal file
231
src/server/login_failure_check.rs
Normal file
@@ -0,0 +1,231 @@
|
||||
use crate::AlarmAuditType;
|
||||
use hbb_common::get_time;
|
||||
#[cfg(target_os = "windows")]
|
||||
use hbb_common::tokio::sync::{Mutex as TokioMutex, OwnedMutexGuard};
|
||||
use std::sync::Mutex;
|
||||
#[cfg(target_os = "windows")]
|
||||
use std::sync::Arc;
|
||||
|
||||
const OS_CREDENTIAL_LOGIN_TOTAL_IDLE_RESET_MS: i64 = 120 * 60 * 1_000;
|
||||
const OS_CREDENTIAL_LOGIN_BACKOFF_BASE_SECONDS: i64 = 15;
|
||||
const OS_CREDENTIAL_LOGIN_BACKOFF_MAX_SECONDS: i64 = 30 * 60;
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||
pub(crate) enum FailureScope {
|
||||
Default,
|
||||
TerminalOsLogin,
|
||||
}
|
||||
|
||||
pub(crate) struct OsCredentialPolicyDecision {
|
||||
pub allowed: bool,
|
||||
pub login_error: Option<String>,
|
||||
pub audit: Option<AlarmAuditType>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Default)]
|
||||
struct OsCredentialFailureState {
|
||||
total_failures: i32,
|
||||
backoff_until_ms: Option<i64>,
|
||||
last_failure_ms: Option<i64>,
|
||||
}
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref OS_CREDENTIAL_LOGIN_FAILURE_STATE: Mutex<OsCredentialFailureState> =
|
||||
Mutex::new(OsCredentialFailureState::default());
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
lazy_static::lazy_static! {
|
||||
static ref OS_CREDENTIAL_LOGIN_MUTEX: Arc<TokioMutex<()>> = Arc::new(TokioMutex::new(()));
|
||||
}
|
||||
|
||||
fn is_os_credential_scope(scope: FailureScope) -> bool {
|
||||
matches!(scope, FailureScope::TerminalOsLogin)
|
||||
}
|
||||
|
||||
fn state_for_os_credential_scope(
|
||||
scope: FailureScope,
|
||||
) -> Option<&'static Mutex<OsCredentialFailureState>> {
|
||||
if is_os_credential_scope(scope) {
|
||||
Some(&OS_CREDENTIAL_LOGIN_FAILURE_STATE)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn backoff_audit_type_for_scope(scope: FailureScope) -> Option<AlarmAuditType> {
|
||||
match scope {
|
||||
FailureScope::TerminalOsLogin => Some(AlarmAuditType::TerminalOsLoginBackoff),
|
||||
FailureScope::Default => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn os_credential_login_backoff_seconds(total_failures: i32) -> i64 {
|
||||
if total_failures <= 2 {
|
||||
return 0;
|
||||
}
|
||||
let exp = (total_failures - 3).min(7);
|
||||
let seconds = OS_CREDENTIAL_LOGIN_BACKOFF_BASE_SECONDS * (1_i64 << exp);
|
||||
seconds.min(OS_CREDENTIAL_LOGIN_BACKOFF_MAX_SECONDS)
|
||||
}
|
||||
|
||||
fn normalize_backoff(state: &mut OsCredentialFailureState, now_ms: i64) {
|
||||
if let Some(until_ms) = state.backoff_until_ms {
|
||||
if until_ms <= now_ms {
|
||||
state.backoff_until_ms = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn reset_totals_on_idle(state: &mut OsCredentialFailureState, now_ms: i64) {
|
||||
if let Some(last_ms) = state.last_failure_ms {
|
||||
if now_ms.saturating_sub(last_ms) >= OS_CREDENTIAL_LOGIN_TOTAL_IDLE_RESET_MS {
|
||||
state.total_failures = 0;
|
||||
state.backoff_until_ms = None;
|
||||
state.last_failure_ms = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn allow_decision() -> OsCredentialPolicyDecision {
|
||||
OsCredentialPolicyDecision {
|
||||
allowed: true,
|
||||
login_error: None,
|
||||
audit: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn block_decision(
|
||||
login_error: String,
|
||||
alarm_type: Option<AlarmAuditType>,
|
||||
) -> OsCredentialPolicyDecision {
|
||||
OsCredentialPolicyDecision {
|
||||
allowed: false,
|
||||
login_error: Some(login_error),
|
||||
audit: alarm_type,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn evaluate_os_credential_policy(
|
||||
scope: FailureScope,
|
||||
now_ms: i64,
|
||||
) -> OsCredentialPolicyDecision {
|
||||
if !is_os_credential_scope(scope) {
|
||||
return allow_decision();
|
||||
}
|
||||
let Some(state_mutex) = state_for_os_credential_scope(scope) else {
|
||||
return allow_decision();
|
||||
};
|
||||
let mut state = state_mutex.lock().unwrap();
|
||||
reset_totals_on_idle(&mut state, now_ms);
|
||||
normalize_backoff(&mut state, now_ms);
|
||||
|
||||
if let Some(until_ms) = state.backoff_until_ms {
|
||||
let remaining_ms = (until_ms - now_ms).max(0);
|
||||
let remaining_seconds = ((remaining_ms + 999) / 1_000).max(1);
|
||||
let seconds_label = if remaining_seconds == 1 {
|
||||
"second"
|
||||
} else {
|
||||
"seconds"
|
||||
};
|
||||
block_decision(
|
||||
format!(
|
||||
"Please try again in {} {}.",
|
||||
remaining_seconds, seconds_label
|
||||
),
|
||||
backoff_audit_type_for_scope(scope),
|
||||
)
|
||||
} else {
|
||||
allow_decision()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn record_os_credential_failure(scope: FailureScope) {
|
||||
if !is_os_credential_scope(scope) {
|
||||
return;
|
||||
}
|
||||
let Some(state_mutex) = state_for_os_credential_scope(scope) else {
|
||||
return;
|
||||
};
|
||||
let mut state = state_mutex.lock().unwrap();
|
||||
let now_ms = get_time();
|
||||
reset_totals_on_idle(&mut state, now_ms);
|
||||
normalize_backoff(&mut state, now_ms);
|
||||
state.total_failures = state.total_failures.saturating_add(1);
|
||||
state.last_failure_ms = Some(now_ms);
|
||||
let backoff_seconds = os_credential_login_backoff_seconds(state.total_failures);
|
||||
if backoff_seconds > 0 {
|
||||
state.backoff_until_ms = Some(now_ms + backoff_seconds * 1_000);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub(crate) fn try_acquire_os_credential_login_gate() -> Result<OwnedMutexGuard<()>, ()> {
|
||||
OS_CREDENTIAL_LOGIN_MUTEX
|
||||
.clone()
|
||||
.try_lock_owned()
|
||||
.map_err(|_| ())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
static TEST_MUTEX: Mutex<()> = Mutex::new(());
|
||||
|
||||
fn clear_os_credential_failure_state(scope: FailureScope) {
|
||||
if let Some(state_mutex) = state_for_os_credential_scope(scope) {
|
||||
*state_mutex.lock().unwrap() = OsCredentialFailureState::default();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn os_credential_policy_prioritizes_backoff() {
|
||||
let _guard = TEST_MUTEX.lock().unwrap();
|
||||
clear_os_credential_failure_state(FailureScope::TerminalOsLogin);
|
||||
let now_ms = get_time();
|
||||
for _ in 0..3 {
|
||||
record_os_credential_failure(FailureScope::TerminalOsLogin);
|
||||
}
|
||||
let decision = evaluate_os_credential_policy(FailureScope::TerminalOsLogin, now_ms);
|
||||
assert!(!decision.allowed);
|
||||
assert!(decision.login_error.is_some());
|
||||
clear_os_credential_failure_state(FailureScope::TerminalOsLogin);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn os_credential_policy_idle_window_resets_total_counter() {
|
||||
let _guard = TEST_MUTEX.lock().unwrap();
|
||||
clear_os_credential_failure_state(FailureScope::TerminalOsLogin);
|
||||
for _ in 0..13 {
|
||||
record_os_credential_failure(FailureScope::TerminalOsLogin);
|
||||
}
|
||||
let blocked = evaluate_os_credential_policy(FailureScope::TerminalOsLogin, get_time());
|
||||
assert!(!blocked.allowed);
|
||||
|
||||
let after_failures_ms = get_time();
|
||||
let after_idle_ms = after_failures_ms + OS_CREDENTIAL_LOGIN_TOTAL_IDLE_RESET_MS + 1_000;
|
||||
let allowed = evaluate_os_credential_policy(FailureScope::TerminalOsLogin, after_idle_ms);
|
||||
assert!(allowed.allowed);
|
||||
clear_os_credential_failure_state(FailureScope::TerminalOsLogin);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn os_credential_policy_audits_every_backoff_block() {
|
||||
let _guard = TEST_MUTEX.lock().unwrap();
|
||||
clear_os_credential_failure_state(FailureScope::TerminalOsLogin);
|
||||
|
||||
for _ in 0..3 {
|
||||
record_os_credential_failure(FailureScope::TerminalOsLogin);
|
||||
}
|
||||
let now_ms = get_time();
|
||||
let first = evaluate_os_credential_policy(FailureScope::TerminalOsLogin, now_ms);
|
||||
let second = evaluate_os_credential_policy(FailureScope::TerminalOsLogin, now_ms + 1_000);
|
||||
assert!(!first.allowed);
|
||||
assert!(!second.allowed);
|
||||
assert!(first.audit.is_some());
|
||||
assert!(second.audit.is_some());
|
||||
|
||||
clear_os_credential_failure_state(FailureScope::TerminalOsLogin);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user