Skip to content
Closed
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
22 changes: 22 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: CI

on:
pull_request:
branches: [master]

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '9.x'

- name: Restore
run: dotnet restore CorePush.sln

- name: Build
run: dotnet build CorePush.sln --no-restore
28 changes: 28 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: Publish to NuGet

on:
release:
types: [published]

jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '9.x'

- name: Restore
run: dotnet restore CorePush.sln

- name: Build
run: dotnet build CorePush.sln --no-restore -c Release

- name: Pack CorePush
run: dotnet pack CorePush/CorePush.csproj --no-build -c Release -o out

- name: Push to NuGet
run: dotnet nuget push out/*.nupkg --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate
5 changes: 0 additions & 5 deletions .travis.yml

This file was deleted.

1 change: 0 additions & 1 deletion CorePush.Tester/CorePush.Tester.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@

<ItemGroup>
<PackageReference Include="FirebaseAdmin" Version="3.4.0" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.15.0" />
</ItemGroup>

</Project>
16 changes: 7 additions & 9 deletions CorePush.Tester/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,15 +50,13 @@ private static async Task SendApnNotificationAsync()
ServerType = apnServerType,
};

while (true)
{
var apn = new ApnSender(settings, http);
var payload = new AppleNotification(
Guid.NewGuid(),
"Hello World (Message)",
"Hello World (Title)");
var response = await apn.SendAsync(payload, apnDeviceToken);
}
var apn = new ApnSender(settings, http);
var payload = new AppleNotification(
Guid.NewGuid(),
"Hello World (Message)",
"Hello World (Title)");
var response = await apn.SendAsync(payload, apnDeviceToken);
Console.WriteLine($"APN Response: {response.StatusCode} - {response.Message}");
}

private static async Task SendFirebaseNotificationAsync()
Expand Down
19 changes: 13 additions & 6 deletions CorePush/Apple/ApnSender.cs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ public async Task<PushResult> SendAsync(
ApnPushType apnPushType = ApnPushType.Alert,
CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrWhiteSpace(deviceToken);

var path = $"/3/device/{deviceToken}";
var json = serializer.Serialize(notification);

Expand All @@ -102,19 +104,20 @@ public async Task<PushResult> SendAsync(
using var response = await http.SendAsync(message, cancellationToken);

var content = await response.Content.ReadAsStringAsync(cancellationToken);
var error = response.IsSuccessStatusCode
? null
: serializer.Deserialize<ApnsError>(content).Reason;
var error = response.IsSuccessStatusCode
? null
: serializer.Deserialize<ApnsError>(content)?.Reason;

return new PushResult((int)response.StatusCode, response.IsSuccessStatusCode, content, error);
}

private string GetJwtToken()
{
var (token, date) = tokens.GetOrAdd(settings.AppBundleIdentifier, _ => new Tuple<string, DateTime>(CreateJwtToken(), DateTime.UtcNow));
var cacheKey = $"{settings.AppBundleIdentifier}:{settings.P8PrivateKeyId}";
var (token, date) = tokens.GetOrAdd(cacheKey, _ => new Tuple<string, DateTime>(CreateJwtToken(), DateTime.UtcNow));
if (date < DateTime.UtcNow.AddMinutes(-tokenExpiresMinutes))
{
tokens.TryRemove(settings.AppBundleIdentifier, out _);
tokens.TryRemove(cacheKey, out _);
return GetJwtToken();
}

Expand Down Expand Up @@ -156,5 +159,9 @@ private static string Base64UrlEncode(string str)
return Base64UrlEncode(bytes);
}

private static string Base64UrlEncode(byte[] bytes) => Convert.ToBase64String(bytes);
private static string Base64UrlEncode(byte[] bytes) =>
Convert.ToBase64String(bytes)
.Replace('+', '-')
.Replace('/', '_')
.TrimEnd('=');
}
22 changes: 14 additions & 8 deletions CorePush/Firebase/FirebaseSender.cs
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ public async Task<PushResult> SendAsync(object payload, CancellationToken cancel
HttpMethod.Post,
$"https://fcm.googleapis.com/v1/projects/{settings.ProjectId}/messages:send");

var token = await GetJwtTokenAsync();
var token = await GetJwtTokenAsync(cancellationToken);

message.Headers.Add("Authorization", $"Bearer {token}");
message.Content = new StringContent(json, Encoding.UTF8, "application/json");
Expand All @@ -118,22 +118,22 @@ public async Task<PushResult> SendAsync(object payload, CancellationToken cancel
firebaseResponse.Error?.Status);
}

private async Task<string> GetJwtTokenAsync()
private async Task<string> GetJwtTokenAsync(CancellationToken cancellationToken)
{
if (firebaseToken != null && firebaseTokenExpiration > DateTime.UtcNow)
{
return firebaseToken.AccessToken;
}

using var message = new HttpRequestMessage(HttpMethod.Post, "https://oauth2.googleapis.com/token");
using var form = new MultipartFormDataContent();
var authToken = GetMasterToken();
form.Add(new StringContent(authToken), "assertion");
form.Add(new StringContent("urn:ietf:params:oauth:grant-type:jwt-bearer"), "grant_type");
message.Content = form;

using var response = await http.SendAsync(message);
var content = await response.Content.ReadAsStringAsync();
using var response = await http.SendAsync(message, cancellationToken);
var content = await response.Content.ReadAsStringAsync(cancellationToken);

if (!response.IsSuccessStatusCode)
{
Expand Down Expand Up @@ -163,8 +163,8 @@ private string GetMasterToken()
exp = CryptoHelper.GetEpochTimestamp() + 3600 /* has to be short lived */
});

var headerBase64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(header));
var payloadBase64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(payload));
var headerBase64 = Base64UrlEncode(Encoding.UTF8.GetBytes(header));
var payloadBase64 = Base64UrlEncode(Encoding.UTF8.GetBytes(payload));
var unsignedJwtData = $"{headerBase64}.{payloadBase64}";
var unsignedJwtBytes = Encoding.UTF8.GetBytes(unsignedJwtData);

Expand All @@ -174,11 +174,17 @@ private string GetMasterToken()
signer.BlockUpdate(unsignedJwtBytes, 0, unsignedJwtBytes.Length);

var signature = signer.GenerateSignature();
var signatureBase64 = Convert.ToBase64String(signature);
var signatureBase64 = Base64UrlEncode(signature);

return $"{unsignedJwtData}.{signatureBase64}";
}

private static string Base64UrlEncode(byte[] bytes) =>
Convert.ToBase64String(bytes)
.Replace('+', '-')
.Replace('/', '_')
.TrimEnd('=');

private static AsymmetricKeyParameter ParsePkcs8PrivateKeyPem(string key)
{
using var keyReader = new StringReader(key);
Expand Down