Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(nextcloud): add http.Client that handles the CSRF token for webd… #1853

Merged
merged 2 commits into from
Apr 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 17 additions & 54 deletions packages/nextcloud/lib/src/webdav/client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import 'dart:typed_data';

import 'package:dynamite_runtime/http_client.dart';
import 'package:http/http.dart' as http;
import 'package:nextcloud/src/webdav/csrf_client.dart';
import 'package:nextcloud/src/webdav/models.dart';
import 'package:nextcloud/src/webdav/path_uri.dart';
import 'package:nextcloud/src/webdav/props.dart';
Expand All @@ -14,55 +15,17 @@ import 'package:nextcloud/src/webdav/webdav.dart';
import 'package:nextcloud/utils.dart';
import 'package:universal_io/io.dart' show File, FileStat;

// ignore: do_not_use_environment
const bool _kIsWeb = bool.fromEnvironment('dart.library.js_util');

/// WebDavClient class
class WebDavClient {
// ignore: public_member_api_docs
WebDavClient(this.rootClient);
WebDavClient(this.rootClient) : csrfClient = WebDavCSRFClient(rootClient);

// ignore: public_member_api_docs
final DynamiteClient rootClient;

String? _token;

Future<http.StreamedResponse> _send(
http.BaseRequest request,
) async {
// On web we need to send a CSRF token because we also send the cookies. In theory this should not be required as
// long as we send the OCS-APIRequest header, but the server has a bug that only triggers when you also send the
// cookies. On non-web platforms we don't send the cookies so we are fine, but on web the browser always does it
// and therefore we need this workaround.
// TODO: Fix this bug in server.
if (_kIsWeb) {
if (_token == null) {
final response = await rootClient.get(Uri.parse('${rootClient.baseURL}/index.php'));
if (response.statusCode >= 300) {
throw DynamiteStatusCodeException(
response.statusCode,
);
}

_token = RegExp('data-requesttoken="([^"]*)"').firstMatch(response.body)!.group(1);
}

request.headers.addAll({
'OCS-APIRequest': 'true',
'requesttoken': _token!,
});
}

final response = await rootClient.send(request);

if (response.statusCode >= 300) {
throw DynamiteStatusCodeException(
response.statusCode,
);
}

return response;
}
/// {@macro WebDavCSRFClient}
// TODO: Fix this bug in server.
final WebDavCSRFClient csrfClient;

Uri _constructUri([PathUri? path]) => constructUri(rootClient.baseURL, path);

Expand All @@ -84,7 +47,7 @@ class WebDavClient {
Future<WebDavOptions> options() async {
final request = options_Request();

final response = await _send(request);
final response = await csrfClient.send(request);
return parseWebDavOptions(response.headers);
}

Expand All @@ -107,7 +70,7 @@ class WebDavClient {
Future<http.StreamedResponse> mkcol(PathUri path) {
final request = mkcol_Request(path);

return _send(request);
return csrfClient.send(request);
}

/// Request to delete the resource at [path].
Expand All @@ -129,7 +92,7 @@ class WebDavClient {
Future<http.StreamedResponse> delete(PathUri path) {
final request = delete_Request(path);

return _send(request);
return csrfClient.send(request);
}

/// Request to put a new file at [path] with [localData] as content.
Expand Down Expand Up @@ -174,7 +137,7 @@ class WebDavClient {
created: created,
);

return _send(request);
return csrfClient.send(request);
}

/// Request to put a new file at [path] with [localData] as content.
Expand Down Expand Up @@ -244,7 +207,7 @@ class WebDavClient {
onProgress: onProgress,
);

return _send(request);
return csrfClient.send(request);
}

/// Request to put a new file at [path] with [file] as content.
Expand Down Expand Up @@ -296,7 +259,7 @@ class WebDavClient {
onProgress: onProgress,
);

return _send(request);
return csrfClient.send(request);
}

/// Request to get the content of the file at [path].
Expand All @@ -320,7 +283,7 @@ class WebDavClient {
}) {
final request = get_Request(path);
// ignore: discarded_futures
final response = _send(request);
final response = csrfClient.send(request);
final controller = StreamController<List<int>>();

unawaited(
Expand Down Expand Up @@ -413,7 +376,7 @@ class WebDavClient {
depth: depth,
);

final response = await _send(request);
final response = await csrfClient.send(request);
return const WebDavResponseConverter().convert(response);
}

Expand Down Expand Up @@ -454,7 +417,7 @@ class WebDavClient {
prop: prop,
);

final response = await _send(request);
final response = await csrfClient.send(request);
return const WebDavResponseConverter().convert(response);
}

Expand Down Expand Up @@ -497,7 +460,7 @@ class WebDavClient {
remove: remove,
);

final response = await _send(request);
final response = await csrfClient.send(request);
final data = await const WebDavResponseConverter().convert(response);
for (final a in data.responses) {
for (final b in a.propstats) {
Expand Down Expand Up @@ -546,7 +509,7 @@ class WebDavClient {
overwrite: overwrite,
);

return _send(request);
return csrfClient.send(request);
}

/// Request to copy the resource from [sourcePath] to [destinationPath].
Expand Down Expand Up @@ -586,7 +549,7 @@ class WebDavClient {
overwrite: overwrite,
);

return _send(request);
return csrfClient.send(request);
}

void _addBaseHeaders(http.BaseRequest request) {
Expand Down
48 changes: 48 additions & 0 deletions packages/nextcloud/lib/src/webdav/csrf_client.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import 'package:dynamite_runtime/http_client.dart';
import 'package:http/http.dart' as http;

/// A [http.Client] that sends the Nextcloud CSRF token.
///
/// {@template WebDavCSRFClient}
/// When sending a request with cookies a CSRF token is also needed. In theory this should not be required as
/// long as we send the OCS-APIRequest header, but the server has a bug that only triggers when you also send the
/// cookies.
/// {@endtemplate}
final class WebDavCSRFClient with http.BaseClient {
/// Creates a new CSRF client that executes requests through the given [DynamiteClient].
WebDavCSRFClient(this._inner);

final DynamiteClient _inner;

/// The request token sent by the [WebDavCSRFClient].
String? _token;

@override
Future<http.StreamedResponse> send(http.BaseRequest request) async {
if (_token == null) {
final response = await _inner.get(Uri.parse('${_inner.baseURL}/index.php'));
if (response.statusCode >= 300) {
throw DynamiteStatusCodeException(
response.statusCode,
);
}

_token = RegExp('data-requesttoken="([^"]*)"').firstMatch(response.body)!.group(1);
}

request.headers.addAll({
'OCS-APIRequest': 'true',
'requesttoken': _token!,
});

final response = await _inner.sendWithCookies(request);

if (response.statusCode >= 300) {
throw DynamiteStatusCodeException(
response.statusCode,
);
}

return response;
}
}
Loading