I have to send push notifications to specific iOS devices with my .Net Core WebAPI that will be executed on a Windows 2008 Server R2. The server itself should not be the problem because it is working with a node.js library. But I want it to work with an WepAPI in ASP .Net Core 2.1 which is self hosted with the inbuilt Kestrel Server. Maybe you've got an idea how to solve this problem.
My Code:
// This will encode the jason web token apns needs for the authorization
// get the base64 private key of the .p8 file from apple
string p8File = System.IO.File.ReadAllText(Settings.Apn.PrivateKey);
p8File = p8File.Replace("-----BEGIN PRIVATE KEY-----", string.Empty);
p8File = p8File.Replace("-----END PRIVATE KEY-----", string.Empty);
p8File = p8File.Replace(" ", string.Empty);
byte[] keyData = Convert.FromBase64String(p8File);
ECDsa key = new ECDsaCng(CngKey.Import(keyData, CngKeyBlobFormat.Pkcs8PrivateBlob));
ECDsaSecurityKey securityKey = new ECDsaSecurityKey(key) { KeyId = Settings.Apn.KeyId };
SigningCredentials credentials = new SigningCredentials(securityKey, "ES256");
SecurityTokenDescriptor descriptor =
new SecurityTokenDescriptor
{
IssuedAt = DateTime.Now,
Issuer = Settings.Apn.TeamId,
SigningCredentials = credentials
};
JwtSecurityTokenHandler jwtHandler = new JwtSecurityTokenHandler();
string encodedToken = jwtHandler.CreateEncodedJwt(descriptor);
this.log?.LogInformation($"Created JWT: {encodedToken}");
// The hostname is: https://api.development.push.apple.com:443
HttpClient client = new HttpClient { BaseAddress = new Uri(Settings.Apn.Hostname) };
client.DefaultRequestHeaders.Clear();
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
this.log?.LogInformation("Initialized new HttpClient.");
// payload content for the apns
JObject payloadData = new JObject
{
new JProperty("alert", data.Message),
new JProperty("badge", 2),
new JProperty("sound", "default")
};
JObject payload = new JObject
{
new JProperty("aps", payloadData)
};
this.log?.LogInformation($"Setup payload: {payload}");
// HttpRequestMessage that should be send
HttpRequestMessage request = new HttpRequestMessage(
HttpMethod.Post,
$"{Settings.Apn.Hostname}/3/device/{data.DeviceId}")
{
Content = new StringContent(JsonConvert.SerializeObject(payload), Encoding.UTF8, "application/json")
};
this.log?.LogInformation("Setup HttpRequestMessage.");
// Setup the header
request.Headers.Add("Authorization", $"Bearer {encodedToken}");
request.Headers.Add("apns-id", Guid.NewGuid().ToString());
request.Headers.Add("apns-expiration", DateTime.Now.AddDays(1).ToString(CultureInfo.InvariantCulture));
request.Headers.Add("apns-priority", "10");
request.Headers.Add("apns-topic", "de.gefasoft-engineering.FabChat");
// Debug logging
this.log.LogDebug(request.ToString());
this.log.LogDebug(await request.Content.ReadAsStringAsync());
this.log.LogDebug(request.RequestUri.Host + request.RequestUri.Port);
// Send request
var result = await client.SendAsync(request);
this.log?.LogInformation("Sent request.");
this.log?.LogInformation(await result.Content.ReadAsStringAsync());
I always get following Exception thrown:
System.Net.Http.HttpRequestException: The SSL connection could not be
established, see inner exception. --->
System.Security.Authentication.AuthenticationException: Authentication
failed, see inner exception. --->
System.ComponentModel.Win32Exception: The message received was
unexpected or badly formatted --- End of inner exception stack
trace ---
Use CorePush lib
It's very lightweight. I use it across all my projects to send Firebase Android/WebPush and Apple iOS push notifications. Useful links:
NuGet package
Documentation
The interface is very simple and minimalistic:
Send APN message:
var apn = new ApnSender(settings, httpClient);
await apn.SendAsync(notification, deviceToken);
It can also send Android FCM message if needed:
var fcm = new FcmSender(settings, httpClient);
await fcm.SendAsync(deviceToken, notification);
can you try adding version information to your request after the apns-topic line as below? It ran to completion and I got a "bad device token" error for the first time after adding the following line.
request.Version = new Version(2, 0);
System.Net.ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12 | SecurityProtocolType.Tls11 | SecurityProtocolType.Tls;
I saw the version setting command at the post below.
How to implement apple token based push notifications (using p8 file) in C#?
I've already commented on the answer from #civilator. But I think, that some people read over it, so I'm posting it again.
This is the code that worked for me. Sorry for the late answer!
private readonly string hostname = "gateway.sandbox.push.apple.com";
private readonly int port = 2195;
public async Task<RestResult<JObject>> SendPushNotification(string deviceToken, string message)
{
this.log?.LogInformation("Trying to send push notification.");
X509Certificate2Collection certificatesCollection;
// Setup and read the certificate
// NOTE: You should get the certificate from your apple developer account.
try
{
string certificatePath = Settings.Apn.Certificate;
X509Certificate2 clientCertificate = new X509Certificate2(
File.ReadAllBytes(certificatePath),
Settings.Apn.Password);
certificatesCollection = new X509Certificate2Collection(clientCertificate);
this.log?.LogInformation("Setup certificates.");
}
catch (Exception e)
{
this.log?.LogError(e.ToString());
return new RestResult<JObject> { Result = "exception", Message = "Failed to setup certificates." };
}
// Setup a tcp connection to the apns
TcpClient client = new TcpClient(AddressFamily.InterNetwork);
this.log?.LogInformation("Created new TcpClient.");
try
{
IPHostEntry host = Dns.GetHostEntry(this.hostname);
await client.ConnectAsync(host.AddressList[0], this.port);
this.log?.LogInformation($"Opened connection to {this.hostname}:{this.port}.");
}
catch (Exception e)
{
this.log?.LogError("Failed to open tcp connection to the apns.");
this.log?.LogError(e.ToString());
}
// Validate the Certificate you get from the APN (for more information read the documentation:
// https://developer.apple.com/library/archive/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/APNSOverview.html#//apple_ref/doc/uid/TP40008194-CH8-SW1).
SslStream sslStream = new SslStream(
client.GetStream(),
false,
new RemoteCertificateValidationCallback(this.ValidateServerCertificate),
null);
try
{
await sslStream.AuthenticateAsClientAsync(this.hostname, certificatesCollection, SslProtocols.Tls, false);
MemoryStream memoryStream = new MemoryStream();
BinaryWriter writer = new BinaryWriter(memoryStream);
writer.Write((byte)0);
writer.Write((byte)0);
writer.Write((byte)32);
writer.Write(HexStringToByteArray(deviceToken.ToUpper()));
// Creating an payload object to send key values to the apns
JObject aps = new JObject
{
new JProperty("alert", message),
new JProperty("badge", 0),
new JProperty("sound", "default")
};
JObject payload = new JObject
{
new JProperty("aps", aps)
};
string payloadString = JsonConvert.SerializeObject(payload);
writer.Write((byte)0);
writer.Write((byte)payloadString.Length);
byte[] b1 = System.Text.Encoding.UTF8.GetBytes(payloadString);
writer.Write(b1);
writer.Flush();
byte[] array = memoryStream.ToArray();
sslStream.Write(array);
sslStream.Flush();
client.Dispose();
}
catch (AuthenticationException ex)
{
this.log?.LogError(ex.ToString());
client.Dispose();
return new RestResult<JObject> { Result = "exception", Message = "Authentication Exception." };
}
catch (Exception e)
{
this.log?.LogError(e.ToString());
client.Dispose();
return new RestResult<JObject> { Result = "exception", Message = "Exception was thrown." };
}
this.log?.LogInformation("Notification sent.");
return new RestResult<JObject> { Result = "success", Message = "Notification sent. Check your device." };
}
#region Helper methods
private static byte[] HexStringToByteArray(string hex)
{
return Enumerable.Range(0, hex.Length)
.Where(x => x % 2 == 0)
.Select(x => Convert.ToByte(hex.Substring(x, 2), 16))
.ToArray();
}
// The following method is invoked by the RemoteCertificateValidationDelegate.
private bool ValidateServerCertificate(
object sender,
X509Certificate certificate,
X509Chain chain,
SslPolicyErrors sslPolicyErrors)
{
if (sslPolicyErrors == SslPolicyErrors.None)
{
this.log?.LogInformation("Server Certificate validated.");
return true;
}
this.log?.LogError($"Server Certificate error: {sslPolicyErrors}");
// Do not allow this client to communicate with unauthenticated servers.
return false;
}
#endregion
Related
I am using WPF(Net6.0) in one of my projects and I am making some requests. To achieve this, I created a typed http client and try to login using username and password to get data like the following:
If the code is difficult to understand just use this general approach:
1-Logins to api system using username and password
2-Sends the command to get specific data.
3-Receives the desired data.
4-Logouts from api
public static async Task<TcpClient> CreateConnectionAsync(int user, string password, string host, int port)
{
var TARGETURL = $"http://{host}";
var targetUri = new Uri(TARGETURL);
IPHostEntry hostEntry = Dns.GetHostEntry(host, AddressFamily.InterNetwork);
UriBuilder uriBuilder = new UriBuilder();
uriBuilder.Host = host;
HttpResponseMessage? httpResponseMessage;
try
{
// Create a Handler for the URL.
var socketsHttpHandler = new SocketsHttpHandler();
socketsHttpHandler.PreAuthenticate = true;
socketsHttpHandler.ConnectCallback = async (context, cancellationToken) =>
{
//var ipAddress = Dns.GetHostEntry(host).AddressList[0];
Socket socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
socket.NoDelay = true;
try
{
var entry = await Dns.GetHostEntryAsync(context.DnsEndPoint.Host, AddressFamily.InterNetwork, cancellationToken);
socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true);
socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveTime, 5);
socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveInterval, 5);
socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveRetryCount, 5);
socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.HeaderIncluded, true);
await socket.ConnectAsync(entry.AddressList, port, cancellationToken);
return new NetworkStream(socket, true);
}
catch (Exception ex)
{
socket.Dispose();
throw;
}
};
// Create a request for the URL.
HttpClient httpClient = new HttpClient(socketsHttpHandler);
httpClient.DefaultRequestHeaders.Accept.Clear();
httpClient.DefaultRequestHeaders.Accept.Add(
new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json"));
string myUser = user.ToString();
string myPassword = password;
string userAndPasswordToken =
Convert.ToBase64String(Encoding.UTF8.GetBytes(myUser + ":" + myPassword));
httpClient.DefaultRequestHeaders.TryAddWithoutValidation("Authorization",
$"Basic {userAndPasswordToken}");
httpResponseMessage = await httpClient.GetAsync(targetUri);
httpResponseMessage.EnsureSuccessStatusCode();
// Receive ack.
var response = httpResponseMessage.Content;
}
catch (Exception exec)
{
throw;
}
return new TcpClient()
{
// Client = client
};
}
Expected: Connection should keep in live
and no error
Error InnerException = {"Unable to read data from the transport connection: Eine vorhandene Verbindung wurde vom Remotehost geschlossen.."}
I have an Alexa skill and a winform on a windows 10 device. I'm sending a message from the winform to Alexa using the Skill Messaging API. I've got the access token, sent the message and received a 202 status code to say the 'message has been successfully accepted, and will be sent to the skill' so I believe everything on the winform side is okay.
The code for it;
var handler = new HttpClientHandler();
handler.ServerCertificateCustomValidationCallback = (requestMessage, certificate, chain, policyErrors) => true;
using (var httpClient = new HttpClient(handler))
{
// Obtain skill messaging token
using (var requestToken = new HttpRequestMessage(new HttpMethod("POST"), "https://api.amazon.com/auth/O2/token"))
{
requestToken.Content = new StringContent("grant_type=client_credentials&scope=alexa:skill_messaging&client_id=amzn1.application-oa2-client.************&client_secret=************");
requestToken.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/x-www-form-urlencoded");
var responseToken = httpClient.SendAsync(requestToken);
Response r = JsonConvert.DeserializeObject<Response>(responseToken.Result.Content.ReadAsStringAsync().Result);
// Send message
using (var requestMessage = new HttpRequestMessage(new HttpMethod("POST"), "https://api.eu.amazonalexa.com/v1/skillmessages/users/" + strUserId))
{
requestMessage.Headers.TryAddWithoutValidation("Authorization", "Bearer " + r.access_token);
requestMessage.Content = new StringContent("{ \"data\" : { \"message\" : \"Hi pickle\" }}");
requestMessage.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json");
var responseMessage = httpClient.SendAsync(requestMessage);
MessageBox.Show(responseMessage.Result.ToString());
}
}
}
How do I capture the incoming message event on the skill though?
Looking at the docs I need to handle an incoming request of type Messaging.MessageReceived? Is that correct?
I tried something like that in the skills FunctionHandler but didn't have any luck.
public SkillResponse FunctionHandler(SkillRequest input, ILambdaContext context)
{
// Initialise response
skillResponse = new SkillResponse
{
Version = "1.0",
Response = new ResponseBody()
};
ssmlResponse = new SsmlOutputSpeech();
if (input.GetRequestType() == typeof(LaunchRequest))
{
LaunchRequestHandler(input, context);
}
else if (input.GetRequestType() == typeof(IntentRequest))
{
IntentRequestHandler(input, context);
}
else if (input.GetRequestType() == typeof(SessionEndedRequest))
{
SessionEndedRequestHandler(input, context);
}
else if(input.GetRequestType().Equals("Messaging.MessageReceived"))
{
ssmlResponse.Ssml = "<speak>" + input.Request.Type + "</speak>";
}
skillResponse.Response.OutputSpeech = ssmlResponse;
return skillResponse;
}
How do I react to the message? Is it permissions I need to set up? Does the incoming message not trigger the functionhandler the same way the echo device does?
Thanks.
I'm trying to retrieve and read emails from my Outlook mail. Unfortunately my mailbox uses Authenticity, which I need to deal with. I have tried a mailbox that does not use Authenticity and the code works. I followed the instructions here https://www.emailarchitect.net/eagetmail/ex/c/22.aspx
(I used the library to read a mailbox that does not use OAuth). So I registered my application on Microsoft Azure as instructed (except for authentication, which was the last step). Unfortunately I get this error System.ComponentModel.Win32Exception
HResult=0x80004005 Message=System cannot find the specified file.
Source=System.Diagnostics.Process
I also tried another library
https://afterlogic.com/mailbee-net/docs/OAuth2MicrosoftRegularAccountsInstalledApps.html
But with the same result
It is larger project, so I will post method where I am getting the error. I will paste more code, if you will need it.
Feel free to ask.
Thanks for any advice.(The documentation is great, so I didn't want to change it)
const string clientID = "Client ID";
const string clientSecret = "client Secret";
const string scope = "https://outlook.office.com/IMAP.AccessAsUser.All%20https://outlook.office.com/POP.AccessAsUser.All%20offline_access%20email%20openid";
const string authUri = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize";
const string tokenUri = "https://login.microsoftonline.com/common/oauth2/v2.0/token";
static int GetRandomUnusedPort()
{
var listener = new TcpListener(IPAddress.Loopback, 0);
listener.Start();
var port = ((IPEndPoint)listener.LocalEndpoint).Port;
listener.Stop();
return port;
}
async void DoOauthAndRetrieveEmail()
{
// Creates a redirect URI using an available port on the loopback address.
string redirectUri = string.Format("http://127.0.0.1:{0}/", GetRandomUnusedPort());
Console.WriteLine("redirect URI: " + redirectUri);
// Creates an HttpListener to listen for requests on that redirect URI.
var http = new HttpListener();
http.Prefixes.Add(redirectUri);
Console.WriteLine("Listening ...");
http.Start();
// Creates the OAuth 2.0 authorization request.
string authorizationRequest = string.Format("{0}?response_type=code&scope={1}&redirect_uri={2}&client_id={3}&prompt=login",
authUri,
scope,
Uri.EscapeDataString(redirectUri),
clientID
);
// Opens request in the browser.
//There is issue
System.Diagnostics.Process.Start(authorizationRequest);
// Waits for the OAuth authorization response.
var context = await http.GetContextAsync();
// Brings the Console to Focus.
BringConsoleToFront();
// Sends an HTTP response to the browser.
var response = context.Response;
string responseString = string.Format("<html><head></head><body>Please return to the app and close current window.</body></html>");
var buffer = Encoding.UTF8.GetBytes(responseString);
response.ContentLength64 = buffer.Length;
var responseOutput = response.OutputStream;
Task responseTask = responseOutput.WriteAsync(buffer, 0, buffer.Length).ContinueWith((task) =>
{
responseOutput.Close();
http.Stop();
Console.WriteLine("HTTP server stopped.");
});
// Checks for errors.
if (context.Request.QueryString.Get("error") != null)
{
Console.WriteLine(string.Format("OAuth authorization error: {0}.", context.Request.QueryString.Get("error")));
return;
}
if (context.Request.QueryString.Get("code") == null)
{
Console.WriteLine("Malformed authorization response. " + context.Request.QueryString);
return;
}
// extracts the code
var code = context.Request.QueryString.Get("code");
Console.WriteLine("Authorization code: " + code);
string responseText = await RequestAccessToken(code, redirectUri);
Console.WriteLine(responseText);
OAuthResponseParser parser = new OAuthResponseParser();
parser.Load(responseText);
var user = parser.EmailInIdToken;
var accessToken = parser.AccessToken;
Console.WriteLine("User: {0}", user);
Console.WriteLine("AccessToken: {0}", accessToken);
RetrieveMailWithXOAUTH2(user, accessToken);
}
I'm trying to send a PushNotification Message to a list of iOS device.
My constraints are :
code is inside a webapi controller
web server is a Windows 2012 R2
using asp.net / .net core (actually 3.1)
First, I decided to use p8 file instead of p12 (it seems Apple prefere it).
I crawled many SO questions, trying about 5-6 solutions, but i still get same results :
An unhandled exception occurred while processing the request.
Win32Exception: Le message reçu était inattendu ou formaté de façon
incorrecte. Unknown location
AuthenticationException: Authentication failed, see inner exception.
System.Net.Security.SslStream.StartSendAuthResetSignal(ProtocolToken
message, AsyncProtocolRequest asyncRequest, ExceptionDispatchInfo
exception)
HttpRequestException: The SSL connection could not be established, see
inner exception.
System.Net.Http.ConnectHelper.EstablishSslConnectionAsyncCore(Stream
stream, SslClientAuthenticationOptions sslOptions, CancellationToken
cancellationToken)
From french "Win32Exception: Le message reçu était inattendu ou formaté de façon incorrecte", in English would be "Win32Exception: The message received was unexpected or badly formatted"
To format the JWT, i've used the solution here from Bourne Koloh.
I've also tried PushSharp and CoreSharp with same error.
When I extract data from the p8 file, i have all the file without first and last line, and without linebreak.
When i use the Push Notifications Tester application, it works, the message is delivered to the device.
By the way, i've added the both p12 certificate (dev and prod) to the Windows 2012 R2 server
I probably miss something important but still don't know what.
Since the message say it's a problem of authentication, I guess it's around the JWT and the the p8 file.
How I extract the p8 file data :
var data = System.IO.File.ReadAllText("AuthKey.p8");
var list = data.Split('\n').ToList();
var prk = list.Where((s, i) => i != 0 && i != list.Count - 1).Aggregate((agg, s) => agg + s);
var key = new ECDsaCng(CngKey.Import(Convert.FromBase64String(prk), CngKeyBlobFormat.Pkcs8PrivateBlob));
Il also tried this way (with BouncyCastle ):
using (var reader = System.IO.File.OpenText("AuthKey.p8"))
{
var ecPrivateKeyParameters = (ECPrivateKeyParameters)new PemReader(reader).ReadObject();
var x = ecPrivateKeyParameters.Parameters.G.AffineXCoord.GetEncoded();
var y = ecPrivateKeyParameters.Parameters.G.AffineYCoord.GetEncoded();
var d = ecPrivateKeyParameters.D.ToByteArrayUnsigned();
var key = ECDsaCng(EccKey.New(x, y, d));
}
And now how I build the JWT :
try #1 : using Jose package.
private string GetProviderToken(ECDsaCng key)
{
var epochNow = (int)DateTime.UtcNow.Subtract(new DateTime(1970,1,1)).TotalSeconds;
var payload = new Dictionary<string, object>()
{
{ "iss", "THETEAMID" },
{ "iat", epochNow }
};
var extraHeaders = new Dictionary<string, object>()
{
{ "kid", "THEKEYID"}
};
return JWT.Encode(payload, key, JwsAlgorithm.ES256, extraHeaders);
}
var token = GetProviderToken(key);
try #2 :
private string CreateToken(ECDsa key, string keyID, string teamID)
{
var securityKey = new ECDsaSecurityKey(key) { KeyId = keyID };
var credentials = new SigningCredentials(securityKey, "ES256");
var descriptor = new SecurityTokenDescriptor
{
IssuedAt = DateTime.Now,
Issuer = teamID,
SigningCredentials = credentials
};
descriptor.Expires = null;
descriptor.NotBefore = null;
var handler = new JwtSecurityTokenHandler();
var encodedToken = handler.CreateEncodedJwt(descriptor);
return encodedToken;
}
var token = CreateToken(key, "THEKEYID", "THETEAMID");
try #3
private string SignES256(string privateKey, string header, string payload)
{
CngKey key = CngKey.Import(Convert.FromBase64String(privateKey), CngKeyBlobFormat.Pkcs8PrivateBlob);
using (ECDsaCng dsa = new ECDsaCng(key))
{
dsa.HashAlgorithm = CngAlgorithm.Sha256;
var unsignedJwtData = Microsoft.AspNetCore.WebUtilities.WebEncoders.Base64UrlEncode(System.Text.Encoding.UTF8.GetBytes(header)) + "." + Microsoft.AspNetCore.WebUtilities.WebEncoders.Base64UrlEncode(System.Text.Encoding.UTF8.GetBytes(payload));
var signature = dsa.SignData(System.Text.Encoding.UTF8.GetBytes(unsignedJwtData));
return unsignedJwtData + "." + Microsoft.AspNetCore.WebUtilities.WebEncoders.Base64UrlEncode(signature);
}
}
var token = SignES256(prk, "{\"alg\":\"ES256\" ,\"kid\":\"THEKEYID\"}", "{ \"iss\": \"THETEAMID\",\"iat\":" + (int)DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1)).TotalSeconds + "\" }");
If I'm not using a library (CorePush, etc) for sending the http message, i do this way :
var url = string.Format("https://api.sandbox.push.apple.com/3/device/{0}", deviceToken);
var request = new HttpRequestMessage(HttpMethod.Post, url);
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
request.Headers.TryAddWithoutValidation("apns-push-type", "alert"); // or background
request.Headers.TryAddWithoutValidation("apns-id", Guid.NewGuid().ToString("D"));
request.Headers.TryAddWithoutValidation("apns-expiration", Convert.ToString(0));
request.Headers.TryAddWithoutValidation("apns-priority", Convert.ToString(10));
request.Headers.TryAddWithoutValidation("apns-topic", "com.company.project");
request.Content = new StringContent("{\"aps\":{\"alert\":\"Hello\"},\"yourCustomKey\":\"1\"}");
// also tried without yourcustomkey
request.Version = new Version(2, 0); // tried directly with System.Net.HttpVersion.Version20;
var handler = new HttpClientHandler();
handler.SslProtocols = SslProtocols.Tls12 | SslProtocols.Tls11 | SslProtocols.Tls; // Tried with only Tls12
handler.ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => true;
using (HttpClient client = new HttpClient(handler))
{
HttpResponseMessage resp = await client.SendAsync(request).ContinueWith(responseTask =>
{
return responseTask.Result; // line of error
});
if (resp != null)
{
string apnsResponseString = await resp.Content.ReadAsStringAsync();
handler.Dispose();
//ALL GOOD ....
return Ok(apnsResponseString);
}
handler.Dispose();
}
I'm trying to send push notifications from C#/dotnetcore and I'm running into an issue when serialising and sending my payload to APNS.
If you look at the code sample below, the iosPayload object has one commented out property. Without it in the payload, the notification is received successfully, with it, the notification does not reach the device.
The error is System.FormatException with the message Additional non-parsable characters are at the end of the string. I'm actually sending from within an Azure Webjob and it's only there that I get the error message, running locally with a simple console application doesn't show any error, but also never reaches the device.
public void SendNotification(string deviceToken)
{
int port = 2195;
string hostname = "gateway.sandbox.push.apple.com";
var iosPayload = new {
aps = new {
alert = "The title",
sound = "default"
},
app_group_id = 1,
notification_id = "notification_id",
campaignName = "Campaign Name",
push_title = "Campaign Title",
push_message = "The main body",
type = "sdkNotification",
push_on_click_behaviour = "1"//,
//another_property = "4"
};
string certificatePath = #"./com.myCompany.sampleIOS.DEV.p12";
X509Certificate2 clientCertificate = new X509Certificate2(File.ReadAllBytes(certificatePath), "");
X509Certificate2Collection certificatesCollection = new X509Certificate2Collection(clientCertificate);
TcpClient tcpClient = new TcpClient(hostname, port);
SslStream sslStream = new SslStream(tcpClient.GetStream(), false, new RemoteCertificateValidationCallback(ValidateServerCertificate), null);
try
{
sslStream.AuthenticateAsClient(hostname, certificatesCollection, SslProtocols.Tls, false);
MemoryStream memoryStream = new MemoryStream();
BinaryWriter writer = new BinaryWriter(memoryStream);
writer.Write((byte)0);
writer.Write((byte)0);
writer.Write((byte)32);
writer.Write(HexStringToByteArray(deviceToken.ToUpper()));
var payload = JsonConvert.SerializeObject(iosPayload);
writer.Write((byte)0);
writer.Write((byte)payload.Length);
byte[] payloadBytes = System.Text.Encoding.UTF8.GetBytes(payload);
writer.Write(payloadBytes);
writer.Flush();
byte[] memoryStreamAsBytes = memoryStream.ToArray();
sslStream.Write(memoryStreamAsBytes);
sslStream.Flush();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
tcpClient.Close();
}
}
private byte[] HexStringToByteArray(string hexString)
{
return Enumerable.Range(0, hexString.Length)
.Where(x => x % 2 == 0)
.Select(x => Convert.ToByte(hexString.Substring(x, 2), 16))
.ToArray();
}
private bool ValidateServerCertificate(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
{
if (sslPolicyErrors == SslPolicyErrors.None) return true;
return false;
}
Edit: strangely I can send my full payload from Pusher...
It turned out that I was calling the Legacy Binary Provider API which the first byte of the sequence determines the command. I was sending through 0 as the command and the max payload size of that API is 256 bytes. Obviously my payload was slightly larger than that and was being rejected..
I then sent 2 as the command and built up the necessary frame data.