Skip to content

Commit

Permalink
v1.0.0-alpha2
Browse files Browse the repository at this point in the history
  • Loading branch information
justdmitry committed Oct 3, 2019
1 parent a302a5c commit 77c5858
Show file tree
Hide file tree
Showing 7 changed files with 106 additions and 25 deletions.
12 changes: 7 additions & 5 deletions PassKitHelper.Tests/PassKitMiddlewareTests_Passes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,9 @@ public async Task DoNothingWithWrongAuth()
[Fact]
public async Task ServiceIsCalled()
{
var obj = JObject.Parse("{ \"ok\": 1 }");
var obj = new MemoryStream();
obj.Write(new byte[] { 40, 41, 42 }, 0, 3);
obj.Position = 0;

passkitServiceMock
.Setup(x => x.GetPassAsync(passType, passSerial, authToken, null))
Expand All @@ -100,13 +102,13 @@ public async Task ServiceIsCalled()
Assert.Equal(200, httpContext.Response.StatusCode);
mocks.Verify();

Assert.Equal(PassInfoBuilder.MimeContentType, httpContext.Response.ContentType);
Assert.Equal(PassPackageBuilder.PkpassMimeContentType, httpContext.Response.ContentType);

var bytes = new byte[3];
httpContext.Response.Body.Position = 0;
using var sr = new StreamReader(httpContext.Response.Body);
var resp = await sr.ReadToEndAsync();
await httpContext.Response.Body.ReadAsync(bytes, 0, 3);

Assert.Equal("{\"ok\":1}", resp);
Assert.Equal(new byte[] { 40, 41, 42 }, bytes);
}

[Fact]
Expand Down
9 changes: 9 additions & 0 deletions PassKitHelper/Extensions/PassPackageBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@

public static class PassPackageBuilderExtensions
{
/// <summary>
/// Sets the <see cref="PassPackageBuilder.AutoDisposeOnBuild"/> property value.
/// </summary>
public static PassPackageBuilder AutoDisposeOnBuild(this PassPackageBuilder builder, bool value)
{
builder.AutoDisposeOnBuild(value);
return builder;
}

/// <summary>
/// The image displayed as the background of the front of the pass.
/// </summary>
Expand Down
5 changes: 3 additions & 2 deletions PassKitHelper/IPassKitService.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
namespace PassKitHelper
{
using System;
using System.IO;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;

Expand Down Expand Up @@ -62,12 +63,12 @@ public interface IPassKitService
/// <param name="authorizationToken">Pass’s authorization token as specified in the pass.</param>
/// <param name="ifModifiedSince">Return HTTP status code 304 if the pass has not changed.</param>
/// <returns>
/// If request is authorized, returns HTTP status 200 with a payload of the pass data.
/// If request is authorized, returns HTTP status 200 with a payload of the pass data (signed and built pass package).
/// If the request is not authorized, returns HTTP status 401 (and null as pass data).
/// If no data has changed since <see cref="ifModifiedSince"/> - return HTTP status code 304 (and null as pass data).
/// Otherwise, returns the appropriate standard HTTP status (and null as pass data).
/// </returns>
Task<(int statusCode, JObject? passData)> GetPassAsync(string passTypeIdentifier, string serialNumber, string authorizationToken, DateTimeOffset? ifModifiedSince);
Task<(int statusCode, MemoryStream? passData)> GetPassAsync(string passTypeIdentifier, string serialNumber, string authorizationToken, DateTimeOffset? ifModifiedSince);

/// <summary>
/// Logging Errors. Log messages contain a description of the error in a human-readable format.
Expand Down
2 changes: 0 additions & 2 deletions PassKitHelper/PassInfoBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@

public class PassInfoBuilder
{
public const string MimeContentType = "application/json";

public static readonly JsonSerializerSettings JsonSettings = new JsonSerializerSettings
{
ContractResolver = new DefaultContractResolver() { NamingStrategy = new CamelCaseNamingStrategy() },
Expand Down
2 changes: 1 addition & 1 deletion PassKitHelper/PassKitHelper.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
<RepositoryType>git</RepositoryType>
<PackageProjectUrl>https://github.com/justdmitry/PassKitHelper</PackageProjectUrl>
<RepositoryUrl>https://github.com/justdmitry/PassKitHelper.git</RepositoryUrl>
<Version>1.0.0-alpha1</Version>
<Version>1.0.0-alpha2</Version>
<Description>Helper library for all your Apple PassKit (Apple Wallet, Apple Passbook) needs: create passes, sign pass packages, receive webhooks into your aspnetcore app. Apple Developer Account required!</Description>
<PackageTags>apple passkit passbook pass</PackageTags>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
Expand Down
8 changes: 5 additions & 3 deletions PassKitHelper/PassKitMiddleware.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
/// </remarks>
public class PassKitMiddleware
{
private const string JsonMimeContentType = "application/json";

private readonly RequestDelegate next;

private readonly ILogger logger;
Expand Down Expand Up @@ -167,7 +169,7 @@ public async Task InvokeDevicesAsync(HttpContext context, PathString devicesRema
serialNumbers = passes,
};

context.Response.ContentType = PassInfoBuilder.MimeContentType;
context.Response.ContentType = JsonMimeContentType;
await context.Response.WriteAsync(JsonConvert.SerializeObject(data, Formatting.None));
}
else
Expand Down Expand Up @@ -233,8 +235,8 @@ public async Task InvokePassesAsync(HttpContext context, PathString passesRemain
throw new Exception("GetPassAsync() must return non-null 'pass' when 'status' == 200");
}

context.Response.ContentType = PassInfoBuilder.MimeContentType;
await context.Response.WriteAsync(pass.ToString(Formatting.None));
context.Response.ContentType = PassPackageBuilder.PkpassMimeContentType;
await pass.CopyToAsync(context.Response.Body);
}
else
{
Expand Down
93 changes: 81 additions & 12 deletions PassKitHelper/PassPackageBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,27 +12,46 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

public class PassPackageBuilder
public class PassPackageBuilder : IDisposable
{
public const string MimeContentType = "application/vnd.apple.pkpass";
/// <summary>
/// Content-Type value for *.pkpass files.
/// </summary>
public const string PkpassMimeContentType = "application/vnd.apple.pkpass";

private readonly JObject passInfo;

private readonly IDictionary<string, object> files;

private bool disposed = false;

public PassPackageBuilder(JObject passInfo)
{
this.passInfo = passInfo;
files = new Dictionary<string, object>();
}

/// <summary>
/// If <b>true</b> (default) - will dispose itself (and all files added as streams) when you call <see cref="SignAndBuildAsync(byte[], byte[], string)"/> or <see cref="SignAndBuildAsync(Stream, Stream, string)"/>.
/// </summary>
public bool AutoDisposeOnBuild { get; set; } = true;

public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}

public void AddFile(string name, byte[] content)
{
CheckDisposed();
files[name] = content;
}

public void AddFile(string name, Stream content)
{
CheckDisposed();

if (!content.CanSeek)
{
throw new ArgumentException(nameof(content), "Stream must support seeking (CanSeek == true)");
Expand All @@ -43,20 +62,37 @@ public void AddFile(string name, Stream content)

public async Task<MemoryStream> SignAndBuildAsync(Stream appleCertificate, Stream passCertificate, string? passCertificatePassword = null)
{
CheckDisposed();

var appleBytes = await StreamToBytesAsync(appleCertificate);
var passBytes = await StreamToBytesAsync(passCertificate);
return await SignAndBuildAsync(appleBytes, passBytes, passCertificatePassword);
}

public async Task<MemoryStream> SignAndBuildAsync(byte[] appleCertificate, byte[] passCertificate, string? passCertificatePassword = null)
public Task<MemoryStream> SignAndBuildAsync(byte[] appleCertificate, byte[] passCertificate, string? passCertificatePassword = null)
{
CheckDisposed();

using var apple509cert = new X509Certificate2(appleCertificate);

using var pass509cert = string.IsNullOrEmpty(passCertificatePassword)
? new X509Certificate2(passCertificate)
: new X509Certificate2(passCertificate, passCertificatePassword);

return SignAndBuildAsync(apple509cert, pass509cert);
}

public async Task<MemoryStream> SignAndBuildAsync(X509Certificate2 appleCertificate, X509Certificate2 passCertificate)
{
CheckDisposed();

AddFile("pass.json", Serialize(passInfo));

var manifest = CreateManifestFile();
var manifestStream = Serialize(manifest);
AddFile("manifest.json", manifestStream);

var signature = CreateSignature(manifestStream, appleCertificate, passCertificate, passCertificatePassword);
var signature = CreateSignature(manifestStream, appleCertificate, passCertificate);
AddFile("signature", signature);

var ms = new MemoryStream();
Expand All @@ -82,6 +118,11 @@ public async Task<MemoryStream> SignAndBuildAsync(byte[] appleCertificate, byte[
}
}

if (AutoDisposeOnBuild)
{
Dispose();
}

ms.Position = 0;
return ms;
}
Expand Down Expand Up @@ -110,28 +151,48 @@ protected JObject CreateManifestFile()
return JObject.FromObject(hashes);
}

protected byte[] CreateSignature(MemoryStream manifest, byte[] appleCertificate, byte[] passCertificate, string? passCertificatePassword = null)
protected byte[] CreateSignature(MemoryStream manifest, X509Certificate2 appleCertificate, X509Certificate2 passCertificate)
{
var pass509cert = string.IsNullOrEmpty(passCertificatePassword)
? new X509Certificate2(passCertificate)
: new X509Certificate2(passCertificate, passCertificatePassword);

var content = new SignedCms(new ContentInfo(manifest.ToArray()), true);

var signer = new CmsSigner(SubjectIdentifierType.SubjectKeyIdentifier, pass509cert)
var signer = new CmsSigner(SubjectIdentifierType.SubjectKeyIdentifier, passCertificate)
{
IncludeOption = X509IncludeOption.None,
};

signer.Certificates.Add(new X509Certificate2(appleCertificate));
signer.Certificates.Add(pass509cert);
signer.Certificates.Add(appleCertificate);
signer.Certificates.Add(passCertificate);
signer.SignedAttributes.Add(new Pkcs9SigningTime());

content.ComputeSignature(signer);

return content.Encode();
}

protected virtual void Dispose(bool disposing)
{
if (disposed)
{
return;
}

if (disposing)
{
if (files != null)
{
foreach (var file in files)
{
if (file.Value is Stream stream)
{
stream.Dispose();
}
}
}
}

disposed = true;
}

private static MemoryStream Serialize(JObject source)
{
var ms = new MemoryStream();
Expand All @@ -158,5 +219,13 @@ private static async Task<byte[]> StreamToBytesAsync(Stream stream)
ms2.Position = 0;
return ms2.ToArray();
}

private void CheckDisposed()
{
if (disposed)
{
throw new ObjectDisposedException(nameof(PassPackageBuilder));
}
}
}
}

0 comments on commit 77c5858

Please sign in to comment.