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

refactor(neon_http_client): use nextcloud csrf request #2502

Merged
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
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
import 'dart:convert';
import 'dart:io';

import 'package:http/http.dart' as http;
import 'package:interceptor_http_client/interceptor_http_client.dart';
import 'package:logging/logging.dart';
import 'package:meta/meta.dart';
import 'package:nextcloud/nextcloud.dart';
import 'package:nextcloud/core.dart' as core;
import 'package:nextcloud/webdav.dart' as webdav;

/// A HttpInterceptor that works around a Nextcloud CSRF bug when cookies are sent.
Expand All @@ -26,10 +23,15 @@ final class CSRFInterceptor implements HttpInterceptor {
CSRFInterceptor({
required http.Client client,
required Uri baseURL,
}) : _client = client,
}) : _client = core
.$Client(
baseURL,
httpClient: client,
)
.csrfToken,
_baseURL = baseURL;

final http.Client _client;
final core.$CsrfTokenClient _client;

final Uri _baseURL;

Expand All @@ -39,9 +41,6 @@ final class CSRFInterceptor implements HttpInterceptor {

static final _log = Logger('CSRFInterceptor');

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

@override
bool shouldInterceptRequest(http.BaseRequest request) {
if (request.url.host != _baseURL.host || !request.url.path.startsWith('${_baseURL.path}${webdav.webdavBase}')) {
Expand All @@ -58,19 +57,11 @@ final class CSRFInterceptor implements HttpInterceptor {
'Request should not be intercepted.',
);

if (!_kIsWeb) {
return request..headers.remove(HttpHeaders.cookieHeader);
}

if (token == null) {
_log.fine('Acquiring new CSRF token for WebDAV');

final response = await _client.get(Uri.parse('$_baseURL/index.php/csrftoken'));
if (response.statusCode >= 300) {
throw DynamiteStatusCodeException(response);
}

token = (json.decode(response.body) as Map<String, dynamic>)['token']! as String;
final response = await _client.index();
token = response.body.token;
}

request.headers.addAll({
Expand All @@ -83,7 +74,7 @@ final class CSRFInterceptor implements HttpInterceptor {

@override
bool shouldInterceptResponse(http.StreamedResponse response) {
return _kIsWeb && response.statusCode == 401;
return response.statusCode == 401;
}

@override
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'dart:io';

import 'package:http/http.dart';
import 'package:http/testing.dart';
import 'package:mocktail/mocktail.dart';
Expand Down Expand Up @@ -54,199 +56,113 @@ void main() {
expect(interceptor.shouldInterceptRequest(request), isFalse);
});

test(
'removes cookie header on dart vm',
() async {
final interceptor = CSRFInterceptor(
client: _FakeClient(),
baseURL: Uri.https('example.com', '/nextcloud'),
test('requests and attaches a new token', () async {
final mockedClient = MockClient((request) async {
return Response(
'{"token":"token"}',
200,
headers: {
HttpHeaders.contentTypeHeader: 'application/json',
},
);
});

final request = Request('GET', Uri.https('example.com', '/nextcloud/remote.php/webdav'))
..headers['cookie'] = 'key=value';

expect(
interceptor.interceptRequest(request: request),
completion(
isA<Request>().having(
(r) => r.headers,
'headers',
isNot(contains('cookie')),
),
),
);
final interceptor = CSRFInterceptor(
client: mockedClient,
baseURL: Uri.https('example.com', '/nextcloud'),
);

expect(interceptor.token, isNull);
},
onPlatform: const {
'browser': [Skip()],
},
);

test(
'requests and attaches a new token on web ',
() async {
final mockedClient = MockClient((request) async {
return Response('{"token":"token"}', 200);
});

final interceptor = CSRFInterceptor(
client: mockedClient,
baseURL: Uri.https('example.com', '/nextcloud'),
);
final request = Request('GET', Uri.https('example.com', '/nextcloud/remote.php/webdav'));

final request = Request('GET', Uri.https('example.com', '/nextcloud/remote.php/webdav'));
await interceptor.interceptRequest(request: request);

await interceptor.interceptRequest(request: request);
expect(
request.headers,
equals({
'OCS-APIRequest': 'true',
'requesttoken': 'token',
}),
);
expect(interceptor.token, equals('token'));
});

expect(
request.headers,
equals({
'OCS-APIRequest': 'true',
'requesttoken': 'token',
}),
);
expect(interceptor.token, equals('token'));
},
onPlatform: const {
'dart-vm': [Skip()],
},
);

test(
'attaches cached token on web',
() async {
final interceptor = CSRFInterceptor(
client: _FakeClient(),
baseURL: Uri.https('example.com', '/nextcloud'),
)..token = 'token';

final request = Request('GET', Uri.https('example.com', '/nextcloud/remote.php/webdav'));

await interceptor.interceptRequest(request: request);

expect(
request.headers,
equals({
'OCS-APIRequest': 'true',
'requesttoken': 'token',
}),
);
expect(interceptor.token, equals('token'));
},
onPlatform: const {
'dart-vm': [Skip()],
},
);

test(
'throws DynamiteStatusCodeException when token request status code >=300',
() async {
final mockedClient = MockClient((request) async {
return Response('', 404);
});

final interceptor = CSRFInterceptor(
client: mockedClient,
baseURL: Uri.https('example.com', '/nextcloud'),
);
test('attaches cached token', () async {
final interceptor = CSRFInterceptor(
client: _FakeClient(),
baseURL: Uri.https('example.com', '/nextcloud'),
)..token = 'token';

final request = Request('GET', Uri.https('example.com', '/nextcloud/remote.php/webdav'));
final request = Request('GET', Uri.https('example.com', '/nextcloud/remote.php/webdav'));

expect(
interceptor.interceptRequest(request: request),
throwsA(isA<DynamiteStatusCodeException>()),
);
},
onPlatform: const {
'dart-vm': [Skip()],
},
);

test(
'does intercept response on web with 401 response',
() async {
final interceptor = CSRFInterceptor(
client: _FakeClient(),
baseURL: Uri.https('example.com', '/nextcloud'),
);
await interceptor.interceptRequest(request: request);

final response = StreamedResponse(const Stream.empty(), 401);
expect(interceptor.shouldInterceptResponse(response), isTrue);
},
onPlatform: const {
'dart-vm': [Skip()],
},
);

test(
'does not intercept response on web with non 401 response',
() {
final interceptor = CSRFInterceptor(
client: _FakeClient(),
baseURL: Uri.https('example.com', '/nextcloud'),
);
expect(
request.headers,
equals({
'OCS-APIRequest': 'true',
'requesttoken': 'token',
}),
);
expect(interceptor.token, equals('token'));
});

final response = StreamedResponse(const Stream.empty(), 200);
expect(interceptor.shouldInterceptResponse(response), isFalse);
expect(
() => interceptor.interceptResponse(response: response, url: Uri()),
throwsA(isA<AssertionError>()),
);
},
onPlatform: const {
'dart-vm': [Skip()],
},
);

test(
'does not intercept response on vm',
() {
final interceptor = CSRFInterceptor(
client: _FakeClient(),
baseURL: Uri.https('example.com', '/nextcloud'),
);
test('throws DynamiteStatusCodeException when token request status code >=300', () async {
final mockedClient = MockClient((request) async {
return Response('', 404);
});

var response = StreamedResponse(const Stream.empty(), 401);
expect(interceptor.shouldInterceptResponse(response), isFalse);
expect(
() => interceptor.interceptResponse(response: response, url: Uri()),
throwsA(isA<AssertionError>()),
);
final interceptor = CSRFInterceptor(
client: mockedClient,
baseURL: Uri.https('example.com', '/nextcloud'),
);

response = StreamedResponse(const Stream.empty(), 200);
expect(interceptor.shouldInterceptResponse(response), isFalse);
expect(
() => interceptor.interceptResponse(response: response, url: Uri()),
throwsA(isA<AssertionError>()),
);
},
onPlatform: const {
'browser': [Skip()],
},
);

test(
'clears token on web with a 401 ',
() {
final interceptor = CSRFInterceptor(
client: _FakeClient(),
baseURL: Uri.https('example.com', '/nextcloud'),
);
final request = Request('GET', Uri.https('example.com', '/nextcloud/remote.php/webdav'));

final response = StreamedResponse(const Stream.empty(), 401);
expect(
interceptor.interceptResponse(response: response, url: Uri()),
equals(response),
);
expect(
interceptor.token,
isNull,
);
},
onPlatform: const {
'dart-vm': [Skip()],
},
);
expect(
interceptor.interceptRequest(request: request),
throwsA(isA<DynamiteStatusCodeException>()),
);
});

test('does intercept response with 401 response', () async {
final interceptor = CSRFInterceptor(
client: _FakeClient(),
baseURL: Uri.https('example.com', '/nextcloud'),
);

final response = StreamedResponse(const Stream.empty(), 401);
expect(interceptor.shouldInterceptResponse(response), isTrue);
});

test('does not intercept response with non 401 response', () {
final interceptor = CSRFInterceptor(
client: _FakeClient(),
baseURL: Uri.https('example.com', '/nextcloud'),
);

final response = StreamedResponse(const Stream.empty(), 200);
expect(interceptor.shouldInterceptResponse(response), isFalse);
expect(
() => interceptor.interceptResponse(response: response, url: Uri()),
throwsA(isA<AssertionError>()),
);
});

test('clears token on with a 401 ', () {
final interceptor = CSRFInterceptor(
client: _FakeClient(),
baseURL: Uri.https('example.com', '/nextcloud'),
);

final response = StreamedResponse(const Stream.empty(), 401);
expect(
interceptor.interceptResponse(response: response, url: Uri()),
equals(response),
);
expect(
interceptor.token,
isNull,
);
});
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@ final class FixtureInterceptor implements HttpInterceptor {

@override
bool shouldInterceptRequest(http.BaseRequest request) {
// TODO: use resetFixture and intercept all requests
return request.url.path != '/index.php/csrftoken';
return true;
}

@override
Expand Down
Loading