mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-10-23 04:03:27 +00:00
fix: warn about insecure custom API server connections (#16616)
* fix: warn about insecure custom API server connections Signed-off-by: fufesou <linlong1266@gmail.com> * fix: limit API server warning to HTTP addresses Signed-off-by: fufesou <linlong1266@gmail.com> * fix: resolve API warnings against effective server configuration Signed-off-by: fufesou <linlong1266@gmail.com> * fix: reuse common sync for API server previews Signed-off-by: fufesou <linlong1266@gmail.com> * fix: keep server settings usable on API preview errors Signed-off-by: fufesou <linlong1266@gmail.com> --------- Signed-off-by: fufesou <linlong1266@gmail.com>
This commit is contained in:
1 parent
7f5a401947
commit
c7f212df2c
59 files changed
+291
-2
No files matched your search
@@ -0,0 +1,72 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/models/platform_model.dart';
|
||||
|
||||
String _resolveApiServer({required String api, required String custom}) {
|
||||
final key = 'resolve-api-server:${jsonEncode([api, custom])}';
|
||||
final response = jsonDecode(bind.mainGetCommonSync(key: key));
|
||||
if (response['error'] != null) {
|
||||
throw FormatException('Failed to resolve API server: ${response['error']}');
|
||||
}
|
||||
return response['url'] as String;
|
||||
}
|
||||
|
||||
bool shouldWarnApiServer(String? apiServer) {
|
||||
final uri = Uri.tryParse(apiServer?.trim() ?? '');
|
||||
if (uri == null || uri.host.isEmpty || uri.scheme != 'http') {
|
||||
return false;
|
||||
}
|
||||
return !const {'localhost', '127.0.0.1', '::1'}.contains(uri.host);
|
||||
}
|
||||
|
||||
class ApiServerWarning extends StatelessWidget {
|
||||
final TextEditingController controller;
|
||||
final TextEditingController idController;
|
||||
final String message;
|
||||
final String emptyMessage;
|
||||
final String Function({required String api, required String custom})
|
||||
resolveApiServer;
|
||||
|
||||
const ApiServerWarning({
|
||||
super.key,
|
||||
required this.controller,
|
||||
required this.idController,
|
||||
required this.message,
|
||||
required this.emptyMessage,
|
||||
this.resolveApiServer = _resolveApiServer,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListenableBuilder(
|
||||
listenable: Listenable.merge([controller, idController]),
|
||||
builder: (context, child) {
|
||||
final apiServer = controller.text;
|
||||
final idServer = idController.text;
|
||||
final String effectiveApiServer;
|
||||
try {
|
||||
effectiveApiServer =
|
||||
resolveApiServer(api: apiServer.trim(), custom: idServer.trim());
|
||||
} catch (error, stack) {
|
||||
FlutterError.reportError(FlutterErrorDetails(
|
||||
exception: error,
|
||||
stack: stack,
|
||||
context: ErrorDescription('while resolving the API server warning'),
|
||||
));
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
if (!shouldWarnApiServer(effectiveApiServer)) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return Align(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: Text(
|
||||
controller.text.trim().isEmpty ? emptyMessage : message,
|
||||
style: TextStyle(color: Colors.orange.shade700),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/common/widgets/api_server_warning.dart';
|
||||
import 'package:flutter_hbb/common/widgets/setting_widgets.dart';
|
||||
import 'package:flutter_hbb/common/widgets/toolbar.dart';
|
||||
import 'package:get/get.dart';
|
||||
@@ -69,6 +70,8 @@ void showServerSettingsWithValue(
|
||||
ServerConfig serverConfig,
|
||||
OverlayDialogManager dialogManager,
|
||||
void Function(VoidCallback)? upSetState) async {
|
||||
const labelWidth = 120.0;
|
||||
const labelSpacing = 8.0;
|
||||
var isInProgress = false;
|
||||
final idCtrl = TextEditingController(text: serverConfig.idServer);
|
||||
final relayCtrl = TextEditingController(text: serverConfig.relayServer);
|
||||
@@ -112,10 +115,10 @@ void showServerSettingsWithValue(
|
||||
return Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 120,
|
||||
width: labelWidth,
|
||||
child: Text(label),
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
SizedBox(width: labelSpacing),
|
||||
Expanded(
|
||||
child: serverSettingsTextFormField(
|
||||
label: label,
|
||||
@@ -175,6 +178,17 @@ void showServerSettingsWithValue(
|
||||
return null;
|
||||
},
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsetsDirectional.only(
|
||||
start: isDesktop || isWeb ? labelWidth + labelSpacing : 0,
|
||||
),
|
||||
child: ApiServerWarning(
|
||||
controller: apiCtrl,
|
||||
idController: idCtrl,
|
||||
message: translate('api_server_connection_tip'),
|
||||
emptyMessage: translate('api_server_empty_tip'),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
buildField('Key', keyCtrl, ''),
|
||||
if (isInProgress)
|
||||
|
||||
@@ -1928,6 +1928,11 @@ class RustdeskImpl {
|
||||
}
|
||||
|
||||
String mainGetCommonSync({required String key, dynamic hint}) {
|
||||
const prefix = 'resolve-api-server:';
|
||||
if (key.startsWith(prefix)) {
|
||||
return js.context
|
||||
.callMethod('getByName', ['api_server', key.substring(prefix.length)]);
|
||||
}
|
||||
throw UnimplementedError("mainGetCommonSync");
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:flutter_hbb/common/widgets/api_server_warning.dart';
|
||||
|
||||
void main() {
|
||||
test('warns only for remote HTTP endpoints', () {
|
||||
final cases = <String?, bool>{
|
||||
null: false,
|
||||
'': false,
|
||||
'https://api.example.com': false,
|
||||
'http://api.example.com': true,
|
||||
'http://192.168.1.13:13000': true,
|
||||
'http://localhost:21114': false,
|
||||
'http://127.0.0.1:21114': false,
|
||||
'http://[::1]:21114': false,
|
||||
};
|
||||
for (final entry in cases.entries) {
|
||||
expect(shouldWarnApiServer(entry.key), entry.value, reason: entry.key);
|
||||
}
|
||||
});
|
||||
|
||||
testWidgets('warning follows API and ID changes using the resolved endpoint',
|
||||
(tester) async {
|
||||
final api = TextEditingController();
|
||||
final id = TextEditingController(text: 'id.example.com');
|
||||
addTearDown(api.dispose);
|
||||
addTearDown(id.dispose);
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
home: ApiServerWarning(
|
||||
controller: api,
|
||||
idController: id,
|
||||
message: 'HTTP warning',
|
||||
emptyMessage: 'Default HTTP warning',
|
||||
resolveApiServer: ({required api, required custom}) =>
|
||||
api.isNotEmpty ? api : 'http://$custom',
|
||||
),
|
||||
));
|
||||
expect(find.text('Default HTTP warning'), findsOneWidget);
|
||||
|
||||
id.text = 'localhost';
|
||||
await tester.pump();
|
||||
expect(find.text('Default HTTP warning'), findsNothing);
|
||||
|
||||
api.text = 'http://api.example.com';
|
||||
await tester.pump();
|
||||
expect(find.text('HTTP warning'), findsOneWidget);
|
||||
|
||||
api.text = 'https://api.example.com';
|
||||
await tester.pump();
|
||||
expect(find.text('HTTP warning'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('resolver failures are reported without breaking the server form',
|
||||
(tester) async {
|
||||
final api = TextEditingController();
|
||||
final id = TextEditingController();
|
||||
addTearDown(api.dispose);
|
||||
addTearDown(id.dispose);
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
home: Scaffold(
|
||||
body: Column(children: [
|
||||
TextField(controller: api),
|
||||
Expanded(
|
||||
child: ApiServerWarning(
|
||||
controller: api,
|
||||
idController: id,
|
||||
message: 'HTTP warning',
|
||||
emptyMessage: 'Default HTTP warning',
|
||||
resolveApiServer: ({required api, required custom}) =>
|
||||
throw const FormatException('API preview failed'),
|
||||
)),
|
||||
]),
|
||||
),
|
||||
));
|
||||
expect(tester.takeException(), isA<FormatException>());
|
||||
expect(find.byType(ErrorWidget), findsNothing);
|
||||
expect(find.text('Default HTTP warning'), findsNothing);
|
||||
expect(find.byType(TextField), findsOneWidget);
|
||||
});
|
||||
}
|
||||
@@ -2763,6 +2763,16 @@ pub fn main_get_common(key: String) -> String {
|
||||
}
|
||||
|
||||
pub fn main_get_common_sync(key: String) -> SyncReturn<String> {
|
||||
if let Some(args) = key.strip_prefix("resolve-api-server:") {
|
||||
let response = match serde_json::from_str::<(String, String)>(args) {
|
||||
Ok((api, custom)) => serde_json::json!({"url": crate::get_api_server(api, custom)}),
|
||||
Err(err) => {
|
||||
log::trace!("Invalid API server preview arguments: {err}");
|
||||
serde_json::json!({"error": err.to_string()})
|
||||
}
|
||||
};
|
||||
return SyncReturn(response.to_string());
|
||||
}
|
||||
// Metadata verification performs blocking network requests. Keep this key on
|
||||
// the asynchronous FFI path to avoid blocking the calling Flutter isolate.
|
||||
if key.starts_with("verified-download-url-") {
|
||||
|
||||
@@ -813,5 +813,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("shortcut-key-not-supported", ""),
|
||||
("On", ""),
|
||||
("Off", ""),
|
||||
("api_server_connection_tip", ""),
|
||||
("api_server_empty_tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -813,5 +813,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("shortcut-key-not-supported", ""),
|
||||
("On", ""),
|
||||
("Off", ""),
|
||||
("api_server_connection_tip", ""),
|
||||
("api_server_empty_tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -813,5 +813,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("shortcut-key-not-supported", ""),
|
||||
("On", ""),
|
||||
("Off", ""),
|
||||
("api_server_connection_tip", ""),
|
||||
("api_server_empty_tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -813,5 +813,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("shortcut-key-not-supported", ""),
|
||||
("On", ""),
|
||||
("Off", ""),
|
||||
("api_server_connection_tip", ""),
|
||||
("api_server_empty_tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -813,5 +813,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("shortcut-key-not-supported", ""),
|
||||
("On", ""),
|
||||
("Off", ""),
|
||||
("api_server_connection_tip", ""),
|
||||
("api_server_empty_tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -813,5 +813,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("shortcut-key-not-supported", "“{}” 不能用作快捷键。"),
|
||||
("On", "开"),
|
||||
("Off", "关"),
|
||||
("api_server_connection_tip", "自定义服务器正在使用未加密的 HTTP。请联系管理员启用 HTTPS。"),
|
||||
("api_server_empty_tip", "API 服务器留空时,自定义服务器默认使用 HTTP。请联系管理员启用 HTTPS。"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -813,5 +813,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("shortcut-key-not-supported", ""),
|
||||
("On", ""),
|
||||
("Off", ""),
|
||||
("api_server_connection_tip", ""),
|
||||
("api_server_empty_tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -813,5 +813,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("shortcut-key-not-supported", ""),
|
||||
("On", ""),
|
||||
("Off", ""),
|
||||
("api_server_connection_tip", ""),
|
||||
("api_server_empty_tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -813,5 +813,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("shortcut-key-not-supported", ""),
|
||||
("On", ""),
|
||||
("Off", ""),
|
||||
("api_server_connection_tip", ""),
|
||||
("api_server_empty_tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -813,5 +813,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("shortcut-key-not-supported", ""),
|
||||
("On", ""),
|
||||
("Off", ""),
|
||||
("api_server_connection_tip", ""),
|
||||
("api_server_empty_tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -288,5 +288,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("shortcut-already-bound-to", "Already bound to"),
|
||||
("shortcut-mobile-physical-keyboard-tip", "Recording requires a physical keyboard. Soft keyboards are not supported."),
|
||||
("shortcut-key-not-supported", "\"{}\" can't be used as a shortcut."),
|
||||
("api_server_connection_tip", "Your custom server uses unencrypted HTTP. Contact your administrator to enable HTTPS."),
|
||||
("api_server_empty_tip", "With API Server empty, your custom server defaults to HTTP. Contact your administrator to enable HTTPS."),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -813,5 +813,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("shortcut-key-not-supported", ""),
|
||||
("On", ""),
|
||||
("Off", ""),
|
||||
("api_server_connection_tip", ""),
|
||||
("api_server_empty_tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -813,5 +813,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("shortcut-key-not-supported", ""),
|
||||
("On", ""),
|
||||
("Off", ""),
|
||||
("api_server_connection_tip", ""),
|
||||
("api_server_empty_tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -813,5 +813,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("shortcut-key-not-supported", ""),
|
||||
("On", ""),
|
||||
("Off", ""),
|
||||
("api_server_connection_tip", ""),
|
||||
("api_server_empty_tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -813,5 +813,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("shortcut-key-not-supported", ""),
|
||||
("On", ""),
|
||||
("Off", ""),
|
||||
("api_server_connection_tip", ""),
|
||||
("api_server_empty_tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -813,5 +813,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("shortcut-key-not-supported", ""),
|
||||
("On", ""),
|
||||
("Off", ""),
|
||||
("api_server_connection_tip", ""),
|
||||
("api_server_empty_tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -813,5 +813,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("shortcut-key-not-supported", ""),
|
||||
("On", ""),
|
||||
("Off", ""),
|
||||
("api_server_connection_tip", ""),
|
||||
("api_server_empty_tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -813,5 +813,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("shortcut-key-not-supported", ""),
|
||||
("On", ""),
|
||||
("Off", ""),
|
||||
("api_server_connection_tip", ""),
|
||||
("api_server_empty_tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -813,5 +813,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("shortcut-key-not-supported", ""),
|
||||
("On", ""),
|
||||
("Off", ""),
|
||||
("api_server_connection_tip", ""),
|
||||
("api_server_empty_tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -813,5 +813,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("shortcut-key-not-supported", ""),
|
||||
("On", ""),
|
||||
("Off", ""),
|
||||
("api_server_connection_tip", ""),
|
||||
("api_server_empty_tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -813,5 +813,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("shortcut-key-not-supported", ""),
|
||||
("On", ""),
|
||||
("Off", ""),
|
||||
("api_server_connection_tip", ""),
|
||||
("api_server_empty_tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -813,5 +813,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("shortcut-key-not-supported", ""),
|
||||
("On", ""),
|
||||
("Off", ""),
|
||||
("api_server_connection_tip", ""),
|
||||
("api_server_empty_tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -813,5 +813,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("shortcut-key-not-supported", ""),
|
||||
("On", ""),
|
||||
("Off", ""),
|
||||
("api_server_connection_tip", ""),
|
||||
("api_server_empty_tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -813,5 +813,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("shortcut-key-not-supported", ""),
|
||||
("On", ""),
|
||||
("Off", ""),
|
||||
("api_server_connection_tip", ""),
|
||||
("api_server_empty_tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -813,5 +813,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("shortcut-key-not-supported", ""),
|
||||
("On", ""),
|
||||
("Off", ""),
|
||||
("api_server_connection_tip", ""),
|
||||
("api_server_empty_tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -813,5 +813,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("shortcut-key-not-supported", ""),
|
||||
("On", ""),
|
||||
("Off", ""),
|
||||
("api_server_connection_tip", ""),
|
||||
("api_server_empty_tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -813,5 +813,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("shortcut-key-not-supported", "'{}' non può essere usato come scorciatoia."),
|
||||
("On", "ON"),
|
||||
("Off", "OFF"),
|
||||
("api_server_connection_tip", ""),
|
||||
("api_server_empty_tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -813,5 +813,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("shortcut-key-not-supported", ""),
|
||||
("On", ""),
|
||||
("Off", ""),
|
||||
("api_server_connection_tip", ""),
|
||||
("api_server_empty_tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -813,5 +813,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("shortcut-key-not-supported", ""),
|
||||
("On", ""),
|
||||
("Off", ""),
|
||||
("api_server_connection_tip", ""),
|
||||
("api_server_empty_tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -813,5 +813,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("shortcut-key-not-supported", ""),
|
||||
("On", ""),
|
||||
("Off", ""),
|
||||
("api_server_connection_tip", ""),
|
||||
("api_server_empty_tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -813,5 +813,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("shortcut-key-not-supported", ""),
|
||||
("On", ""),
|
||||
("Off", ""),
|
||||
("api_server_connection_tip", ""),
|
||||
("api_server_empty_tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -813,5 +813,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("shortcut-key-not-supported", ""),
|
||||
("On", ""),
|
||||
("Off", ""),
|
||||
("api_server_connection_tip", ""),
|
||||
("api_server_empty_tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -813,5 +813,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("shortcut-key-not-supported", ""),
|
||||
("On", ""),
|
||||
("Off", ""),
|
||||
("api_server_connection_tip", ""),
|
||||
("api_server_empty_tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -813,5 +813,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("shortcut-key-not-supported", ""),
|
||||
("On", ""),
|
||||
("Off", ""),
|
||||
("api_server_connection_tip", ""),
|
||||
("api_server_empty_tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -813,5 +813,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("shortcut-key-not-supported", "Sneltoets wordt niet ondersteund"),
|
||||
("On", "Aan"),
|
||||
("Off", "Uit"),
|
||||
("api_server_connection_tip", ""),
|
||||
("api_server_empty_tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -813,5 +813,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("shortcut-key-not-supported", ""),
|
||||
("On", ""),
|
||||
("Off", ""),
|
||||
("api_server_connection_tip", ""),
|
||||
("api_server_empty_tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -813,5 +813,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("shortcut-key-not-supported", "Não é possível usar \"{}\" como atalho."),
|
||||
("On", "Ativado"),
|
||||
("Off", "Desativado"),
|
||||
("api_server_connection_tip", ""),
|
||||
("api_server_empty_tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -813,5 +813,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("shortcut-key-not-supported", ""),
|
||||
("On", ""),
|
||||
("Off", ""),
|
||||
("api_server_connection_tip", ""),
|
||||
("api_server_empty_tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -813,5 +813,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("shortcut-key-not-supported", ""),
|
||||
("On", ""),
|
||||
("Off", ""),
|
||||
("api_server_connection_tip", ""),
|
||||
("api_server_empty_tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -813,5 +813,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("shortcut-key-not-supported", "\"{}\" невозможно использовать как сочетание клавиш."),
|
||||
("On", "вкл"),
|
||||
("Off", "откл"),
|
||||
("api_server_connection_tip", ""),
|
||||
("api_server_empty_tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -813,5 +813,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("shortcut-key-not-supported", ""),
|
||||
("On", ""),
|
||||
("Off", ""),
|
||||
("api_server_connection_tip", ""),
|
||||
("api_server_empty_tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -813,5 +813,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("shortcut-key-not-supported", ""),
|
||||
("On", ""),
|
||||
("Off", ""),
|
||||
("api_server_connection_tip", ""),
|
||||
("api_server_empty_tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -813,5 +813,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("shortcut-key-not-supported", ""),
|
||||
("On", ""),
|
||||
("Off", ""),
|
||||
("api_server_connection_tip", ""),
|
||||
("api_server_empty_tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -813,5 +813,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("shortcut-key-not-supported", ""),
|
||||
("On", ""),
|
||||
("Off", ""),
|
||||
("api_server_connection_tip", ""),
|
||||
("api_server_empty_tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -813,5 +813,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("shortcut-key-not-supported", ""),
|
||||
("On", ""),
|
||||
("Off", ""),
|
||||
("api_server_connection_tip", ""),
|
||||
("api_server_empty_tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -813,5 +813,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("shortcut-key-not-supported", ""),
|
||||
("On", ""),
|
||||
("Off", ""),
|
||||
("api_server_connection_tip", ""),
|
||||
("api_server_empty_tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -813,5 +813,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("shortcut-key-not-supported", ""),
|
||||
("On", ""),
|
||||
("Off", ""),
|
||||
("api_server_connection_tip", ""),
|
||||
("api_server_empty_tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -813,5 +813,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("shortcut-key-not-supported", ""),
|
||||
("On", ""),
|
||||
("Off", ""),
|
||||
("api_server_connection_tip", ""),
|
||||
("api_server_empty_tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -813,5 +813,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("shortcut-key-not-supported", ""),
|
||||
("On", ""),
|
||||
("Off", ""),
|
||||
("api_server_connection_tip", ""),
|
||||
("api_server_empty_tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -813,5 +813,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("shortcut-key-not-supported", ""),
|
||||
("On", ""),
|
||||
("Off", ""),
|
||||
("api_server_connection_tip", ""),
|
||||
("api_server_empty_tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -813,5 +813,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("shortcut-key-not-supported", ""),
|
||||
("On", ""),
|
||||
("Off", ""),
|
||||
("api_server_connection_tip", "自訂伺服器正在使用未加密的 HTTP。請聯絡管理員啟用 HTTPS。"),
|
||||
("api_server_empty_tip", "API 伺服器留空時,自訂伺服器預設使用 HTTP。請聯絡管理員啟用 HTTPS。"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -813,5 +813,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("shortcut-key-not-supported", ""),
|
||||
("On", ""),
|
||||
("Off", ""),
|
||||
("api_server_connection_tip", ""),
|
||||
("api_server_empty_tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -813,6 +813,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("shortcut-key-not-supported", ""),
|
||||
("On", ""),
|
||||
("Off", ""),
|
||||
("api_server_connection_tip", ""),
|
||||
("api_server_empty_tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -813,5 +813,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("shortcut-key-not-supported", ""),
|
||||
("On", ""),
|
||||
("Off", ""),
|
||||
("api_server_connection_tip", ""),
|
||||
("api_server_empty_tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
Reference in new issue
Block a user