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.',
|
||||
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 '../../../session/session_manager.dart';
|
||||
import '../../api_response.dart';
|
||||
import '../../demo/demo_mode.dart';
|
||||
import '../../errors/network_exception.dart';
|
||||
|
||||
abstract class WebdavApi<T> {
|
||||
T genericParams;
|
||||
@@ -18,6 +20,14 @@ abstract class WebdavApi<T> {
|
||||
/// changes (app password minted/renewed, account switch) so it never keeps
|
||||
/// authenticating with stale credentials.
|
||||
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
|
||||
// minted), and the client would keep the previous login name.
|
||||
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_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:jiffy/jiffy.dart';
|
||||
import 'package:loader_overlay/loader_overlay.dart';
|
||||
import 'package:path_provider/path_provider.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/marianumconnect/auth/session_validator.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');
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
SessionManager().markUiEngine();
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'dart:developer';
|
||||
|
||||
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_response.dart';
|
||||
import '../../../../../api/marianumcloud/talk/chat/long_poll_chat.dart';
|
||||
@@ -262,7 +263,7 @@ class ChatBloc
|
||||
}
|
||||
|
||||
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
|
||||
// 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.
|
||||
|
||||
@@ -6,6 +6,7 @@ import '../../share_intent/remote_file_ref.dart';
|
||||
import '../../utils/downloads/download_job.dart';
|
||||
import '../../utils/downloads/download_manager.dart';
|
||||
import '../confirm_dialog.dart';
|
||||
import '../demo_restricted.dart';
|
||||
|
||||
/// 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,
|
||||
}) async {
|
||||
final path = downloadRemotePath;
|
||||
if (path == null) return;
|
||||
if (path == null || guardDemoAction(context)) return;
|
||||
final job = await DownloadManager.instance.start(
|
||||
remotePath: path,
|
||||
name: name,
|
||||
|
||||
Reference in New Issue
Block a user