blocked all nextcloud requests in demo mode to stop fake-credential logins tripping brute-force protection
This commit is contained in:
@@ -0,0 +1,28 @@
|
|||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
import 'package:http/io_client.dart';
|
||||||
|
|
||||||
|
import '../../model/endpoint_data.dart';
|
||||||
|
import '../errors/network_exception.dart';
|
||||||
|
import 'demo_mode.dart';
|
||||||
|
|
||||||
|
/// Demo sessions carry made-up credentials. Every request they sent to the
|
||||||
|
/// real Nextcloud would count as a failed login there and, in bulk, trip the
|
||||||
|
/// server's brute-force throttle for the whole IP (HTTP 429 for everyone
|
||||||
|
/// behind it). Installed app-wide via [http.runWithClient].
|
||||||
|
class DemoHttpClient extends http.BaseClient {
|
||||||
|
final http.Client _inner = IOClient();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<http.StreamedResponse> send(http.BaseRequest request) {
|
||||||
|
if (DemoMode.active && isNextcloudHost(request.url)) {
|
||||||
|
return Future.error(NetworkException.demoBlocked(request.url));
|
||||||
|
}
|
||||||
|
return _inner.send(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void close() => _inner.close();
|
||||||
|
|
||||||
|
static bool isNextcloudHost(Uri url) =>
|
||||||
|
url.host == EndpointData().nextcloud().domain;
|
||||||
|
}
|
||||||
@@ -13,4 +13,10 @@ class NetworkException extends AppException {
|
|||||||
'Der Server hat zu lange gebraucht. Bitte versuche es erneut.',
|
'Der Server hat zu lange gebraucht. Bitte versuche es erneut.',
|
||||||
technicalDetails: technicalDetails,
|
technicalDetails: technicalDetails,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/// A request a demo session must never send (see `DemoHttpClient`).
|
||||||
|
factory NetworkException.demoBlocked(Uri url) => NetworkException(
|
||||||
|
userMessage: 'Im Demo-Modus nicht verfügbar.',
|
||||||
|
technicalDetails: 'demo mode blocked $url',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import 'package:nextcloud/nextcloud.dart';
|
|||||||
import '../../../model/endpoint_data.dart';
|
import '../../../model/endpoint_data.dart';
|
||||||
import '../../../session/session_manager.dart';
|
import '../../../session/session_manager.dart';
|
||||||
import '../../api_response.dart';
|
import '../../api_response.dart';
|
||||||
|
import '../../demo/demo_mode.dart';
|
||||||
|
import '../../errors/network_exception.dart';
|
||||||
|
|
||||||
abstract class WebdavApi<T> {
|
abstract class WebdavApi<T> {
|
||||||
T genericParams;
|
T genericParams;
|
||||||
@@ -18,6 +20,14 @@ abstract class WebdavApi<T> {
|
|||||||
/// changes (app password minted/renewed, account switch) so it never keeps
|
/// changes (app password minted/renewed, account switch) so it never keeps
|
||||||
/// authenticating with stale credentials.
|
/// authenticating with stale credentials.
|
||||||
static Future<WebDavClient> get webdav {
|
static Future<WebDavClient> get webdav {
|
||||||
|
// The WebDAV client talks dart:io directly, past DemoHttpClient.
|
||||||
|
if (DemoMode.active) {
|
||||||
|
return Future.error(
|
||||||
|
NetworkException.demoBlocked(
|
||||||
|
Uri.parse(EndpointData().nextcloud().origin()),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
// Keyed by user too: two accounts may share a password (no app password
|
// Keyed by user too: two accounts may share a password (no app password
|
||||||
// minted), and the client would keep the previous login name.
|
// minted), and the client would keep the previous login name.
|
||||||
final nextcloud = SessionManager().requireNextcloud();
|
final nextcloud = SessionManager().requireNextcloud();
|
||||||
|
|||||||
+7
-1
@@ -11,11 +11,13 @@ import 'package:flutter/scheduler.dart' show timeDilation;
|
|||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
import 'package:jiffy/jiffy.dart';
|
import 'package:jiffy/jiffy.dart';
|
||||||
import 'package:loader_overlay/loader_overlay.dart';
|
import 'package:loader_overlay/loader_overlay.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart';
|
import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart';
|
||||||
|
|
||||||
|
import 'api/demo/demo_http_client.dart';
|
||||||
import 'api/marianumcloud/webdav/queries/list_files/list_files_cache.dart';
|
import 'api/marianumcloud/webdav/queries/list_files/list_files_cache.dart';
|
||||||
import 'api/marianumconnect/auth/session_validator.dart';
|
import 'api/marianumconnect/auth/session_validator.dart';
|
||||||
import 'api/marianumconnect/marianumconnect_endpoint.dart';
|
import 'api/marianumconnect/marianumconnect_endpoint.dart';
|
||||||
@@ -117,7 +119,11 @@ void _installErrorHandlers() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> main() async {
|
// Everything runs inside the zone so every package:http client created by the
|
||||||
|
// app (Talk, avatars, image caches) goes through DemoHttpClient.
|
||||||
|
Future<void> main() => http.runWithClient(_main, DemoHttpClient.new);
|
||||||
|
|
||||||
|
Future<void> _main() async {
|
||||||
log('MarianumMobile started');
|
log('MarianumMobile started');
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
SessionManager().markUiEngine();
|
SessionManager().markUiEngine();
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'dart:developer';
|
|||||||
|
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
import '../../../../../api/demo/demo_mode.dart';
|
||||||
import '../../../../../api/marianumcloud/talk/chat/get_chat_history.dart';
|
import '../../../../../api/marianumcloud/talk/chat/get_chat_history.dart';
|
||||||
import '../../../../../api/marianumcloud/talk/chat/get_chat_response.dart';
|
import '../../../../../api/marianumcloud/talk/chat/get_chat_response.dart';
|
||||||
import '../../../../../api/marianumcloud/talk/chat/long_poll_chat.dart';
|
import '../../../../../api/marianumcloud/talk/chat/long_poll_chat.dart';
|
||||||
@@ -262,7 +263,7 @@ class ChatBloc
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _startLongPoll(String token) {
|
void _startLongPoll(String token) {
|
||||||
if (!_appResumed) return;
|
if (!_appResumed || DemoMode.active) return;
|
||||||
// A load chain may finish after the user switched chats (A→B); without this
|
// A load chain may finish after the user switched chats (A→B); without this
|
||||||
// guard the stale chain hijacks the long-poll back to A and merges A's
|
// guard the stale chain hijacks the long-poll back to A and merges A's
|
||||||
// messages into B's state while B never receives live updates.
|
// messages into B's state while B never receives live updates.
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import '../../share_intent/remote_file_ref.dart';
|
|||||||
import '../../utils/downloads/download_job.dart';
|
import '../../utils/downloads/download_job.dart';
|
||||||
import '../../utils/downloads/download_manager.dart';
|
import '../../utils/downloads/download_manager.dart';
|
||||||
import '../confirm_dialog.dart';
|
import '../confirm_dialog.dart';
|
||||||
|
import '../demo_restricted.dart';
|
||||||
|
|
||||||
/// Shared download trigger logic for the Files list and Talk chat bubbles.
|
/// Shared download trigger logic for the Files list and Talk chat bubbles.
|
||||||
///
|
///
|
||||||
@@ -71,7 +72,7 @@ mixin DownloadTrigger<T extends StatefulWidget> on State<T> {
|
|||||||
RemoteFileRef? remoteFile,
|
RemoteFileRef? remoteFile,
|
||||||
}) async {
|
}) async {
|
||||||
final path = downloadRemotePath;
|
final path = downloadRemotePath;
|
||||||
if (path == null) return;
|
if (path == null || guardDemoAction(context)) return;
|
||||||
final job = await DownloadManager.instance.start(
|
final job = await DownloadManager.instance.start(
|
||||||
remotePath: path,
|
remotePath: path,
|
||||||
name: name,
|
name: name,
|
||||||
|
|||||||
Reference in New Issue
Block a user