Skip to content

Commit

Permalink
feat(nextcloud): add custom http date parser to support rfc822 time z…
Browse files Browse the repository at this point in the history
…one offsets

Signed-off-by: Nikolas Rimikis <leptopoda@users.noreply.github.com>
  • Loading branch information
Leptopoda committed Mar 13, 2024
1 parent ea177b1 commit 6ab8f5d
Show file tree
Hide file tree
Showing 10 changed files with 570 additions and 10 deletions.
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

0 comments on commit 6ab8f5d

Please sign in to comment.