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 custom http date parser to support rfc822 time zone offsets #1763

Merged
merged 1 commit into from
Mar 13, 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
1 change: 1 addition & 0 deletions .cspell/misc.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
asctime
browsable
cleartext
codegen
Expand Down
5 changes: 2 additions & 3 deletions packages/neon_framework/lib/src/utils/request_manager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@ import 'package:built_value/serializer.dart';
import 'package:dynamite_runtime/http_client.dart';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;
import 'package:http_parser/http_parser.dart';
import 'package:logging/logging.dart';
import 'package:meta/meta.dart';
import 'package:neon_framework/models.dart';
import 'package:neon_framework/src/bloc/result.dart';
import 'package:neon_framework/src/models/account.dart';
import 'package:neon_framework/storage.dart';
import 'package:nextcloud/nextcloud.dart';
import 'package:nextcloud/utils.dart';
import 'package:rxdart/rxdart.dart';
import 'package:timezone/timezone.dart' as tz;
import 'package:xml/xml.dart' as xml;
Expand Down Expand Up @@ -368,8 +368,7 @@ class CacheParameters {
factory CacheParameters.parseHeaders(Map<String, dynamic> headers) {
tz.TZDateTime? expiry;
if (headers.containsKey('expires')) {
final parsed = parseHttpDate(headers['expires']! as String);
expiry = tz.TZDateTime.from(parsed, tz.UTC);
expiry = parseHttpDate(headers['expires']! as String);
}

return CacheParameters(
Expand Down
1 change: 0 additions & 1 deletion packages/neon_framework/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ dependencies:
flutter_zxing: ^1.0.0
go_router: ^13.0.0
http: ^1.0.0
http_parser: ^4.0.0
image: ^4.0.0
intersperse: ^2.0.0
intl: ^0.18.0
Expand Down
2 changes: 1 addition & 1 deletion packages/neon_framework/test/request_manager_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ import 'package:built_value/serializer.dart';
import 'package:dynamite_runtime/http_client.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:http/http.dart';
import 'package:http_parser/http_parser.dart';
import 'package:mocktail/mocktail.dart';
import 'package:neon_framework/src/bloc/result.dart';
import 'package:neon_framework/src/utils/request_manager.dart';
import 'package:neon_framework/testing.dart';
import 'package:nextcloud/utils.dart';
import 'package:rxdart/rxdart.dart';
import 'package:timezone/timezone.dart' as tz;

Expand Down
174 changes: 174 additions & 0 deletions packages/nextcloud/lib/src/utils/http_date_parser.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

// ignore_for_file: unnecessary_raw_strings, cascade_invocations, parameter_assignments, always_put_control_body_on_new_line

import 'package:string_scanner/string_scanner.dart';
import 'package:timezone/timezone.dart' as tz;

const _weekdays = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
const _months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];

final _shortWeekdayRegExp = RegExp(r'Mon|Tue|Wed|Thu|Fri|Sat|Sun');
final _longWeekdayRegExp = RegExp(r'Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday');
final _monthRegExp = RegExp(r'Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec');
final _digitRegExp = RegExp(r'\d+');
final _zoneRegExp = RegExp(r'[+|-]');

/// Return a HTTP-formatted string representation of [date].
///
/// This follows [RFC 822](http://tools.ietf.org/html/rfc822) as updated by
/// [RFC 1123](http://tools.ietf.org/html/rfc1123).
String formatHttpDate(DateTime date) {
date = date.toUtc();
final buffer = StringBuffer()
..write(_weekdays[date.weekday - 1])
..write(', ')
..write(date.day <= 9 ? '0' : '')
..write(date.day.toString())
..write(' ')
..write(_months[date.month - 1])
..write(' ')
..write(date.year.toString())
..write(date.hour <= 9 ? ' 0' : ' ')
..write(date.hour.toString())
..write(date.minute <= 9 ? ':0' : ':')
..write(date.minute.toString())
..write(date.second <= 9 ? ':0' : ':')
..write(date.second.toString())
..write(' GMT');
return buffer.toString();
}

/// Parses an HTTP-formatted date into a UTC [DateTime].
///
/// This follows [RFC 2616](http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.3).
/// It will throw a [FormatException] if [date] is invalid.
tz.TZDateTime parseHttpDate(String date) {
final scanner = StringScanner(date);

if (scanner.scan(_longWeekdayRegExp)) {
// RFC 850 starts with a long weekday.
scanner.expect(', ');
final day = _parseInt(scanner, 2);
scanner.expect('-');
final month = _parseMonth(scanner);
scanner.expect('-');
final year = 1900 + _parseInt(scanner, 2);
scanner.expect(' ');
final time = _parseTime(scanner);
scanner.expect(' GMT');
scanner.expectDone();

return _makeDateTime(year, month, day, time);
}

// RFC 1123 and asctime both start with a short weekday.
scanner.expect(_shortWeekdayRegExp);
if (scanner.scan(', ')) {
// RFC 1123 follows the weekday with a comma.
final day = _parseInt(scanner, 2);
scanner.expect(' ');
final month = _parseMonth(scanner);
scanner.expect(' ');
final year = _parseInt(scanner, 4);
scanner.expect(' ');
final time = _parseTime(scanner);
scanner.expect(' ');
final offset = _parseLocation(scanner);
scanner.expectDone();

return _makeDateTime(year, month, day, time, offset);
}

// asctime follows the weekday with a space.
scanner.expect(' ');
final month = _parseMonth(scanner);
scanner.expect(' ');
final day = scanner.scan(' ') ? _parseInt(scanner, 1) : _parseInt(scanner, 2);
scanner.expect(' ');
final time = _parseTime(scanner);
scanner.expect(' ');
final year = _parseInt(scanner, 4);
scanner.expectDone();

return _makeDateTime(year, month, day, time);
}

/// Parses a short-form month name to a form accepted by [DateTime].
int _parseMonth(StringScanner scanner) {
scanner.expect(_monthRegExp);
// DateTime uses 1-indexed months.
return _months.indexOf(scanner.lastMatch![0]!) + 1;
}

/// Parses an int an enforces that it has exactly [digits] digits.
int _parseInt(StringScanner scanner, int digits) {
scanner.expect(_digitRegExp);
if (scanner.lastMatch![0]!.length != digits) {
scanner.error('expected a $digits-digit number.');
}

return int.parse(scanner.lastMatch![0]!);
}

/// Parses an timestamp of the form "HH:MM:SS" on a 24-hour clock.
tz.TZDateTime _parseTime(StringScanner scanner) {
final hours = _parseInt(scanner, 2);
if (hours >= 24) scanner.error('hours may not be greater than 24.');
scanner.expect(':');

final minutes = _parseInt(scanner, 2);
if (minutes >= 60) scanner.error('minutes may not be greater than 60.');
scanner.expect(':');

final seconds = _parseInt(scanner, 2);
if (seconds >= 60) scanner.error('seconds may not be greater than 60.');

return tz.TZDateTime.utc(1, 1, 1, hours, minutes, seconds);
}

Duration? _parseLocation(StringScanner scanner) {
if (scanner.scan('GMT')) {
return null;
} else if (scanner.scan(_zoneRegExp)) {
final modifier = scanner.lastMatch![0]!;

scanner.expect(RegExp(r'\d{2}'));
final hours = int.parse(scanner.lastMatch![0]!);

scanner.expect(RegExp(r'\d{2}'));
final minutes = int.parse(scanner.lastMatch![0]!);

if (hours >= 99 && minutes > 59) {
throw FormatException("invalid timezone offset '$hours$minutes'.");
}

var offset = Duration(hours: hours, minutes: minutes);
if (modifier == '-') {
offset *= -1;
}

return offset;
} else {
throw const FormatException('Parsing timezone can not be done unambiguously.');
}
}

/// Returns a UTC [tz.TZDateTime] from the given components.
///
/// Validates that [day] is a valid day for [month]. If it's not, throws a
/// [FormatException].
tz.TZDateTime _makeDateTime(int year, int month, int day, tz.TZDateTime time, [Duration? offset]) {
var dateTime = tz.TZDateTime(tz.UTC, year, month, day, time.hour, time.minute, time.second);
if (offset != null) {
dateTime = dateTime.add(offset);
}

// If [day] was too large, it will cause [month] to overflow.
if (dateTime.month != month) {
throw FormatException("invalid day '$day' for month '$month'.");
}
return dateTime;
}
5 changes: 2 additions & 3 deletions packages/nextcloud/lib/src/webdav/file.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import 'package:http_parser/http_parser.dart';
import 'package:nextcloud/src/utils/date_time.dart';
import 'package:nextcloud/src/utils/http_date_parser.dart';
import 'package:nextcloud/src/webdav/client.dart';
import 'package:nextcloud/src/webdav/path_uri.dart';
import 'package:nextcloud/src/webdav/props.dart';
Expand Down Expand Up @@ -65,8 +65,7 @@ class WebDavFile {
/// Last modified date of the file
late final tz.TZDateTime? lastModified = () {
if (props.davgetlastmodified != null) {
final parsed = parseHttpDate(props.davgetlastmodified!);
return tz.TZDateTime.from(parsed, tz.UTC);
return parseHttpDate(props.davgetlastmodified!);
}
return null;
}();
Expand Down
1 change: 1 addition & 0 deletions packages/nextcloud/lib/utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
library;

export 'src/utils/date_time.dart';
export 'src/utils/http_date_parser.dart';
2 changes: 1 addition & 1 deletion packages/nextcloud/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ dependencies:
crypton: ^2.0.0
dynamite_runtime: ^0.2.0
http: ^1.2.0
http_parser: ^4.0.0
json_annotation: ^4.8.1
meta: ^1.0.0
string_scanner: ^1.1.0
timezone: ^0.9.2
universal_io: ^2.0.0
uri: ^1.0.0
Expand Down
Loading