From 6e7f19a764c711c17bf0de95e29a19bff919dd9c Mon Sep 17 00:00:00 2001 From: The one with the braid Date: Sun, 26 Nov 2023 19:42:29 +0100 Subject: [PATCH] feat: implement web service Signed-off-by: The one with the braid --- .gitignore | 1 + .idea/.gitignore | 3 - .idea/libraries/Dart_Packages.xml | 444 ------------------ .idea/libraries/Dart_SDK.xml | 29 -- .idea/misc.xml | 6 - .idea/modules.xml | 8 - .idea/vcs.xml | 6 - README.md | 2 +- lib/pkpass.dart | 16 +- lib/{src => pkpass}/error.dart | 0 lib/{src => pkpass}/models/barcode.dart | 0 lib/{src => pkpass}/models/beacon.dart | 0 lib/{src => pkpass}/models/location.dart | 0 lib/{src => pkpass}/models/pass.dart | 2 +- .../models/pass_structure_dictionary.dart | 2 +- .../models/pass_web_service.dart | 2 - lib/{src => pkpass}/pass_file.dart | 4 +- lib/{src => pkpass}/utils/color_helper.dart | 0 lib/{src => pkpass}/utils/file_matcher.dart | 0 lib/{src => pkpass}/utils/lproj_parser.dart | 0 lib/{src => pkpass}/utils/mabe_decode.dart | 0 lib/pkpass_web_service.dart | 9 + lib/pkpass_web_wervice/utils/http_date.dart | 219 +++++++++ lib/pkpass_web_wervice/web_service.dart | 69 +++ lib/pkpass_web_wervice/web_service_error.dart | 19 + test/pkpass_test.dart | 2 +- 26 files changed, 331 insertions(+), 512 deletions(-) delete mode 100644 .idea/.gitignore delete mode 100644 .idea/libraries/Dart_Packages.xml delete mode 100644 .idea/libraries/Dart_SDK.xml delete mode 100644 .idea/misc.xml delete mode 100644 .idea/modules.xml delete mode 100644 .idea/vcs.xml rename lib/{src => pkpass}/error.dart (100%) rename lib/{src => pkpass}/models/barcode.dart (100%) rename lib/{src => pkpass}/models/beacon.dart (100%) rename lib/{src => pkpass}/models/location.dart (100%) rename lib/{src => pkpass}/models/pass.dart (99%) rename lib/{src => pkpass}/models/pass_structure_dictionary.dart (99%) rename lib/{src => pkpass}/models/pass_web_service.dart (95%) rename lib/{src => pkpass}/pass_file.dart (97%) rename lib/{src => pkpass}/utils/color_helper.dart (100%) rename lib/{src => pkpass}/utils/file_matcher.dart (100%) rename lib/{src => pkpass}/utils/lproj_parser.dart (100%) rename lib/{src => pkpass}/utils/mabe_decode.dart (100%) create mode 100644 lib/pkpass_web_service.dart create mode 100644 lib/pkpass_web_wervice/utils/http_date.dart create mode 100644 lib/pkpass_web_wervice/web_service.dart create mode 100644 lib/pkpass_web_wervice/web_service_error.dart diff --git a/.gitignore b/.gitignore index 3cceda5..5ed2e6d 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ # Avoid committing pubspec.lock for library packages; see # https://dart.dev/guides/libraries/private-files#pubspeclock. pubspec.lock +.idea diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 26d3352..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml diff --git a/.idea/libraries/Dart_Packages.xml b/.idea/libraries/Dart_Packages.xml deleted file mode 100644 index a2c1ccc..0000000 --- a/.idea/libraries/Dart_Packages.xml +++ /dev/null @@ -1,444 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/libraries/Dart_SDK.xml b/.idea/libraries/Dart_SDK.xml deleted file mode 100644 index 3c407c5..0000000 --- a/.idea/libraries/Dart_SDK.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index 639900d..0000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index 4f2d084..0000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 94a25f7..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/README.md b/README.md index 5fb5c48..3c161bc 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ Some parts of the PkPass specification are either not yet implemented, or not pl - `signature`: The detached PKCS #7 signature using Apple certificates of the manifest. Note: Checksums _are_ checked. - Not planned, feel free to contribute. - `nfc`: Card payment information for Apple Pay. - Not planned, feel free to contribute. -- `webService`: Information used to update passes using the web service. - Planned, feel free to contribute. +- `webService`: Only pull to refresh supported. Push service not implemented yet. - Planned, feel free to contribute. ## Localizations diff --git a/lib/pkpass.dart b/lib/pkpass.dart index c5ffa37..7a56dd9 100644 --- a/lib/pkpass.dart +++ b/lib/pkpass.dart @@ -3,11 +3,11 @@ /// More dartdocs go here. library pkpass; -export 'src/error.dart'; -export 'src/models/barcode.dart'; -export 'src/models/beacon.dart'; -export 'src/models/location.dart'; -export 'src/models/pass.dart'; -export 'src/models/pass_structure_dictionary.dart'; -export 'src/models/pass_web_service.dart'; -export 'src/pass_file.dart'; +export 'pkpass/error.dart'; +export 'pkpass/models/barcode.dart'; +export 'pkpass/models/beacon.dart'; +export 'pkpass/models/location.dart'; +export 'pkpass/models/pass.dart'; +export 'pkpass/models/pass_structure_dictionary.dart'; +export 'pkpass/models/pass_web_service.dart'; +export 'pkpass/pass_file.dart'; diff --git a/lib/src/error.dart b/lib/pkpass/error.dart similarity index 100% rename from lib/src/error.dart rename to lib/pkpass/error.dart diff --git a/lib/src/models/barcode.dart b/lib/pkpass/models/barcode.dart similarity index 100% rename from lib/src/models/barcode.dart rename to lib/pkpass/models/barcode.dart diff --git a/lib/src/models/beacon.dart b/lib/pkpass/models/beacon.dart similarity index 100% rename from lib/src/models/beacon.dart rename to lib/pkpass/models/beacon.dart diff --git a/lib/src/models/location.dart b/lib/pkpass/models/location.dart similarity index 100% rename from lib/src/models/location.dart rename to lib/pkpass/models/location.dart diff --git a/lib/src/models/pass.dart b/lib/pkpass/models/pass.dart similarity index 99% rename from lib/src/models/pass.dart rename to lib/pkpass/models/pass.dart index 6e94f82..7c2fb38 100644 --- a/lib/src/models/pass.dart +++ b/lib/pkpass/models/pass.dart @@ -1,7 +1,7 @@ import 'package:intl/locale.dart'; import 'package:pkpass/pkpass.dart'; -import 'package:pkpass/src/utils/mabe_decode.dart'; +import 'package:pkpass/pkpass/utils/mabe_decode.dart'; /// Information that is required for all passes. class PassMetadata { diff --git a/lib/src/models/pass_structure_dictionary.dart b/lib/pkpass/models/pass_structure_dictionary.dart similarity index 99% rename from lib/src/models/pass_structure_dictionary.dart rename to lib/pkpass/models/pass_structure_dictionary.dart index 6cd3693..2ffe1e6 100644 --- a/lib/src/models/pass_structure_dictionary.dart +++ b/lib/pkpass/models/pass_structure_dictionary.dart @@ -1,7 +1,7 @@ import 'package:intl/locale.dart'; import 'package:pkpass/pkpass.dart'; -import 'package:pkpass/src/utils/mabe_decode.dart'; +import 'package:pkpass/pkpass/utils/mabe_decode.dart'; /// Keys that define the structure of the pass. /// diff --git a/lib/src/models/pass_web_service.dart b/lib/pkpass/models/pass_web_service.dart similarity index 95% rename from lib/src/models/pass_web_service.dart rename to lib/pkpass/models/pass_web_service.dart index bf6472f..52c664c 100644 --- a/lib/src/models/pass_web_service.dart +++ b/lib/pkpass/models/pass_web_service.dart @@ -1,5 +1,3 @@ -/// TODO: implement PassKit Web Service Reference -/// /// Metadata required for Pass Web Service /// /// https://developer.apple.com/library/archive/documentation/PassKit/Reference/PassKit_WebService/WebService.html#//apple_ref/doc/uid/TP40011988 diff --git a/lib/src/pass_file.dart b/lib/pkpass/pass_file.dart similarity index 97% rename from lib/src/pass_file.dart rename to lib/pkpass/pass_file.dart index acaa061..86df73e 100644 --- a/lib/src/pass_file.dart +++ b/lib/pkpass/pass_file.dart @@ -6,8 +6,8 @@ import 'package:crypto/crypto.dart'; import 'package:intl/locale.dart'; import 'package:pkpass/pkpass.dart'; -import 'package:pkpass/src/utils/file_matcher.dart'; -import 'package:pkpass/src/utils/lproj_parser.dart'; +import 'package:pkpass/pkpass/utils/file_matcher.dart'; +import 'package:pkpass/pkpass/utils/lproj_parser.dart'; final _utf8codec = Utf8Codec(); final _jsonCodec = JsonCodec(); diff --git a/lib/src/utils/color_helper.dart b/lib/pkpass/utils/color_helper.dart similarity index 100% rename from lib/src/utils/color_helper.dart rename to lib/pkpass/utils/color_helper.dart diff --git a/lib/src/utils/file_matcher.dart b/lib/pkpass/utils/file_matcher.dart similarity index 100% rename from lib/src/utils/file_matcher.dart rename to lib/pkpass/utils/file_matcher.dart diff --git a/lib/src/utils/lproj_parser.dart b/lib/pkpass/utils/lproj_parser.dart similarity index 100% rename from lib/src/utils/lproj_parser.dart rename to lib/pkpass/utils/lproj_parser.dart diff --git a/lib/src/utils/mabe_decode.dart b/lib/pkpass/utils/mabe_decode.dart similarity index 100% rename from lib/src/utils/mabe_decode.dart rename to lib/pkpass/utils/mabe_decode.dart diff --git a/lib/pkpass_web_service.dart b/lib/pkpass_web_service.dart new file mode 100644 index 0000000..a1d5ff2 --- /dev/null +++ b/lib/pkpass_web_service.dart @@ -0,0 +1,9 @@ +/// The PkPass Web Service. +/// +/// So far only supports update check but no push service. +/// +/// https://developer.apple.com/library/archive/documentation/PassKit/Reference/PassKit_WebService/WebService.html#//apple_ref/doc/uid/TP40011988 +library pkpass_web_service; + +export 'pkpass_web_wervice/web_service.dart'; +export 'pkpass_web_wervice/web_service_error.dart'; diff --git a/lib/pkpass_web_wervice/utils/http_date.dart b/lib/pkpass_web_wervice/utils/http_date.dart new file mode 100644 index 0000000..78615f1 --- /dev/null +++ b/lib/pkpass_web_wervice/utils/http_date.dart @@ -0,0 +1,219 @@ +// Copyright (c) 2012, 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. + +// copied from dart:io + +/// Utility functions for working with dates with HTTP specific date +/// formats. +class HttpDate { + // From RFC-2616 section "3.3.1 Full Date", + // http://tools.ietf.org/html/rfc2616#section-3.3.1 + // + // HTTP-date = rfc1123-date | rfc850-date | asctime-date + // rfc1123-date = wkday "," SP date1 SP time SP "GMT" + // rfc850-date = weekday "," SP date2 SP time SP "GMT" + // asctime-date = wkday SP date3 SP time SP 4DIGIT + // date1 = 2DIGIT SP month SP 4DIGIT + // ; day month year (e.g., 02 Jun 1982) + // date2 = 2DIGIT "-" month "-" 2DIGIT + // ; day-month-year (e.g., 02-Jun-82) + // date3 = month SP ( 2DIGIT | ( SP 1DIGIT )) + // ; month day (e.g., Jun 2) + // time = 2DIGIT ":" 2DIGIT ":" 2DIGIT + // ; 00:00:00 - 23:59:59 + // wkday = "Mon" | "Tue" | "Wed" + // | "Thu" | "Fri" | "Sat" | "Sun" + // weekday = "Monday" | "Tuesday" | "Wednesday" + // | "Thursday" | "Friday" | "Saturday" | "Sunday" + // month = "Jan" | "Feb" | "Mar" | "Apr" + // | "May" | "Jun" | "Jul" | "Aug" + // | "Sep" | "Oct" | "Nov" | "Dec" + + /// Format a date according to + /// [RFC-1123](http://tools.ietf.org/html/rfc1123 "RFC-1123"), + /// e.g. `Thu, 1 Jan 1970 00:00:00 GMT`. + static String format(DateTime date) { + const List wkday = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]; + const List month = [ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec", + ]; + + DateTime d = date.toUtc(); + StringBuffer sb = StringBuffer() + ..write(wkday[d.weekday - 1]) + ..write(", ") + ..write(d.day <= 9 ? "0" : "") + ..write(d.day.toString()) + ..write(" ") + ..write(month[d.month - 1]) + ..write(" ") + ..write(d.year.toString()) + ..write(d.hour <= 9 ? " 0" : " ") + ..write(d.hour.toString()) + ..write(d.minute <= 9 ? ":0" : ":") + ..write(d.minute.toString()) + ..write(d.second <= 9 ? ":0" : ":") + ..write(d.second.toString()) + ..write(" GMT"); + return sb.toString(); + } + + /// Parse a date string in either of the formats + /// [RFC-1123](http://tools.ietf.org/html/rfc1123 "RFC-1123"), + /// [RFC-850](http://tools.ietf.org/html/rfc850 "RFC-850") or + /// ANSI C's asctime() format. These formats are listed here. + /// + /// Thu, 1 Jan 1970 00:00:00 GMT + /// Thursday, 1-Jan-1970 00:00:00 GMT + /// Thu Jan 1 00:00:00 1970 + /// + /// For more information see [RFC-2616 section + /// 3.1.1](http://tools.ietf.org/html/rfc2616#section-3.3.1 + /// "RFC-2616 section 3.1.1"). + static DateTime parse(String date) { + final int sp = 32; + const List wkdays = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]; + const List weekdays = [ + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday", + "Sunday", + ]; + const List months = [ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec", + ]; + + final int formatRfc1123 = 0; + final int formatRfc850 = 1; + final int formatAsctime = 2; + + int index = 0; + String tmp; + + void expect(String s) { + if (date.length - index < s.length) { + throw Exception("Invalid HTTP date $date"); + } + String tmp = date.substring(index, index + s.length); + if (tmp != s) { + throw Exception("Invalid HTTP date $date"); + } + index += s.length; + } + + int expectWeekday() { + int weekday; + // The formatting of the weekday signals the format of the date string. + int pos = date.indexOf(",", index); + if (pos == -1) { + int pos = date.indexOf(" ", index); + if (pos == -1) throw Exception("Invalid HTTP date $date"); + tmp = date.substring(index, pos); + index = pos + 1; + weekday = wkdays.indexOf(tmp); + if (weekday != -1) { + return formatAsctime; + } + } else { + tmp = date.substring(index, pos); + index = pos + 1; + weekday = wkdays.indexOf(tmp); + if (weekday != -1) { + return formatRfc1123; + } + weekday = weekdays.indexOf(tmp); + if (weekday != -1) { + return formatRfc850; + } + } + throw Exception("Invalid HTTP date $date"); + } + + int expectMonth(String separator) { + int pos = date.indexOf(separator, index); + if (pos - index != 3) throw Exception("Invalid HTTP date $date"); + tmp = date.substring(index, pos); + index = pos + 1; + int month = months.indexOf(tmp); + if (month != -1) return month; + throw Exception("Invalid HTTP date $date"); + } + + int expectNum(String separator) { + int pos; + if (separator.isNotEmpty) { + pos = date.indexOf(separator, index); + } else { + pos = date.length; + } + String tmp = date.substring(index, pos); + index = pos + separator.length; + try { + int value = int.parse(tmp); + return value; + } on FormatException { + throw Exception("Invalid HTTP date $date"); + } + } + + void expectEnd() { + if (index != date.length) { + throw Exception("Invalid HTTP date $date"); + } + } + + int format = expectWeekday(); + int year; + int month; + int day; + int hours; + int minutes; + int seconds; + if (format == formatAsctime) { + month = expectMonth(" "); + if (date.codeUnitAt(index) == sp) index++; + day = expectNum(" "); + hours = expectNum(":"); + minutes = expectNum(":"); + seconds = expectNum(" "); + year = expectNum(""); + } else { + expect(" "); + day = expectNum(format == formatRfc1123 ? " " : "-"); + month = expectMonth(format == formatRfc1123 ? " " : "-"); + year = expectNum(" "); + hours = expectNum(":"); + minutes = expectNum(":"); + seconds = expectNum(" "); + expect("GMT"); + } + expectEnd(); + return DateTime.utc(year, month + 1, day, hours, minutes, seconds, 0); + } +} diff --git a/lib/pkpass_web_wervice/web_service.dart b/lib/pkpass_web_wervice/web_service.dart new file mode 100644 index 0000000..78eb4d0 --- /dev/null +++ b/lib/pkpass_web_wervice/web_service.dart @@ -0,0 +1,69 @@ +import 'dart:typed_data'; + +import 'package:http/http.dart'; + +import 'package:pkpass/pkpass.dart'; +import 'package:pkpass/pkpass_web_wervice/utils/http_date.dart'; +import 'web_service_error.dart'; + +/// PassKit WebService implementation +/// +/// A Representational State Transfer (REST)–style web service protocol is used +/// to communicate with your server about changes to passes, and to fetch the +/// latest version of a pass when it has changed. +/// +/// https://developer.apple.com/library/archive/documentation/PassKit/Reference/PassKit_WebService/WebService.html#//apple_ref/doc/uid/TP40011988 +class PkPassWebService { + static const _apiVersion = 'v1'; + static Client? _client; + + /// The [PassMetadata] to check for updates. + final PassMetadata metadata; + + /// An optional [Client] used for any http requests + final Client? client; + + const PkPassWebService(this.metadata, {this.client}); + + PassWebService get webService { + final service = metadata.webService; + if (service == null) noWebServiceProvided(); + return service; + } + + Client get httpClient { + final client = this.client; + if (client != null) return client; + return _client ??= Client(); + } + + /// Getting the Latest Version of a Pass + /// + /// Requests the latest version of the current PkPass file + /// + /// [modifiedSince] should be provided in order to support "304 Not Modified" + Future getLatestVersion([DateTime? modifiedSince]) async { + final identifier = metadata.passTypeIdentifier; + final serial = metadata.serialNumber; + final endpoint = '/$_apiVersion/passes/$identifier/$serial'; + + final response = await httpClient.get( + Uri.parse(webService.webServiceURL.toString() + endpoint), + headers: { + if (modifiedSince != null) + 'If-Modified-Since': HttpDate.format(modifiedSince), + 'Authorization': 'ApplePass ${webService.authenticationToken}', + }, + ); + switch (response.statusCode) { + case 200: + return response.bodyBytes; + case 304: + return null; + default: + throw WebServiceResponseError(response); + } + } + + Never noWebServiceProvided() => throw WebServiceUnavailable(); +} diff --git a/lib/pkpass_web_wervice/web_service_error.dart b/lib/pkpass_web_wervice/web_service_error.dart new file mode 100644 index 0000000..58f3b72 --- /dev/null +++ b/lib/pkpass_web_wervice/web_service_error.dart @@ -0,0 +1,19 @@ +import 'package:http/http.dart'; + +import 'package:pkpass/pkpass.dart'; + +abstract class PkPassWebServiceError extends PKPassError { + PkPassWebServiceError({required super.message}); +} + +class WebServiceUnavailable extends PkPassWebServiceError { + WebServiceUnavailable() + : super(message: 'The PkPass file does not contain any web service.'); +} + +class WebServiceResponseError extends PkPassWebServiceError { + final Response response; + + WebServiceResponseError(this.response) + : super(message: 'Unexpected response from web service'); +} diff --git a/test/pkpass_test.dart b/test/pkpass_test.dart index 5031fdc..9c3c3d5 100644 --- a/test/pkpass_test.dart +++ b/test/pkpass_test.dart @@ -5,7 +5,7 @@ import 'package:intl/locale.dart'; import 'package:test/test.dart'; import 'package:pkpass/pkpass.dart'; -import 'package:pkpass/src/utils/file_matcher.dart'; +import 'package:pkpass/pkpass/utils/file_matcher.dart'; void main() { final archive = [