feat: implement web service

Signed-off-by: The one with the braid <info@braid.business>
This commit is contained in:
The one with the braid 2023-11-26 19:42:29 +01:00
parent 44494eaa90
commit 6e7f19a764
26 changed files with 331 additions and 512 deletions

View file

@ -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';

View file

@ -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 {

View file

@ -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.
///

View file

@ -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

View file

@ -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();

View file

@ -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';

View file

@ -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);
}
}

View file

@ -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<Uint8List?> 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();
}

View file

@ -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');
}