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.
Related
OCSP request does not fall into the Audit log table when I send it using Bouncy Castle library.
I don't understand why? What is wrong with my code and how to solve it?
I have a client certificate and issuer certificate. I create two client classes for this purpose. Each of them uses different libraries. One use Bouncy Castle another one uses Chilkat. Each of them validates the certificate correctly. But the problem is with the server side. The query does not fall into the Audit log table when I try Bouncy Castle version.
But when I use Chilkat API everything is ok.
Bouncy Castle version :
class TestOCSPClient
{
protected static Asn1Object GetExtensionValue(X509Certificate cert,
string oid)
{
if (cert == null)
{
return null;
}
byte[] bytes = cert.GetExtensionValue(new DerObjectIdentifier(oid)).GetOctets();
if (bytes == null)
{
return null;
}
Asn1InputStream aIn = new Asn1InputStream(bytes);
return aIn.ReadObject();
}
public static List<string> GetAuthorityInformationAccessOcspUrl(X509Certificate cert)
{
List<string> ocspUrls = new List<string>();
try
{
Asn1Object obj = GetExtensionValue(cert, X509Extensions.AuthorityInfoAccess.Id);
if (obj == null)
{
return null;
}
Asn1Sequence s = (Asn1Sequence)obj;
IEnumerator elements = s.GetEnumerator();
while (elements.MoveNext())
{
Asn1Sequence element = (Asn1Sequence)elements.Current;
DerObjectIdentifier oid = (DerObjectIdentifier)element[0];
if (oid.Id.Equals("1.3.6.1.5.5.7.48.1")) // Is Ocsp?
{
Asn1TaggedObject taggedObject = (Asn1TaggedObject)element[1];
GeneralName gn = (GeneralName)GeneralName.GetInstance(taggedObject);
ocspUrls.Add(((DerIA5String)DerIA5String.GetInstance(gn.Name)).GetString());
}
}
}
catch (Exception e)
{
throw new Exception("Error parsing AIA.", e);
}
return ocspUrls;
}
public CertificateStatusEnum ValidateOCSP(X509Certificate cert, X509Certificate cacert)
{
List<string> urls = GetAuthorityInformationAccessOcspUrl(cert);
if (urls.Count == 0)
{
throw new Exception("No OCSP url found in ee certificate.");
}
string url = urls[0];
Console.WriteLine("Sending to : '" + url + "'...");
byte[] packtosend = CreateOCSPPackage(cert, cacert);
byte[] response = PostRequest(url, packtosend, "Content-Type", "application/ocsp-request");
return VerifyResponse(response);
}
public byte[] ToByteArray(Stream stream)
{
byte[] buffer = new byte[4096 * 8];
MemoryStream ms = new MemoryStream();
int read = 0;
while ((read = stream.Read(buffer, 0, buffer.Length)) > 0)
{
ms.Write(buffer, 0, read);
}
return ms.ToArray();
}
public byte[] PostRequest(string url, byte[] data, string contentType, string accept)
{
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
request.Method = "POST";
request.ContentType = contentType;
request.ContentLength = data.Length;
request.Accept = accept;
Stream stream = request.GetRequestStream();
stream.Write(data, 0, data.Length);
stream.Close();
HttpWebResponse response = (HttpWebResponse)request.GetResponse();
Stream respStream = response.GetResponseStream();
Console.WriteLine(string.Format("HttpStatusCode : {0}", response.StatusCode.ToString()));
byte[] resp = ToByteArray(respStream);
respStream.Close();
return resp;
}
private CertificateStatusEnum VerifyResponse(byte[] response)
{
OcspResp r = new OcspResp(response);
CertificateStatusEnum cStatusEnum = CertificateStatusEnum.Unknown;
switch (r.Status)
{
case OcspRespStatus.Successful:
BasicOcspResp or = (BasicOcspResp)r.GetResponseObject();
//ValidateResponse(or, issuerCert);
Console.WriteLine(or.Responses.Length);
if (or.Responses.Length == 1)
{
SingleResp resp = or.Responses[0];
Object certificateStatus = resp.GetCertStatus();
//this part returns null actually
if (certificateStatus == null)
{
Console.WriteLine("Status is null ! ");
}
if (certificateStatus==null || certificateStatus == Org.BouncyCastle.Ocsp.CertificateStatus.Good)
{
cStatusEnum = CertificateStatusEnum.Good;
}
else if (certificateStatus is Org.BouncyCastle.Ocsp.RevokedStatus)
{
cStatusEnum = CertificateStatusEnum.Revoked;
}
else if (certificateStatus is Org.BouncyCastle.Ocsp.UnknownStatus)
{
cStatusEnum = CertificateStatusEnum.Unknown;
}
}
break;
default:
throw new Exception("Unknow status '" + r.Status + "'.");
}
return cStatusEnum;
}
private static byte[] CreateOCSPPackage(X509Certificate cert, X509Certificate cacert)
{
OcspReqGenerator gen = new OcspReqGenerator();
try
{
CertificateID certId = new CertificateID(CertificateID.HashSha1, cacert, cert.SerialNumber);
gen.AddRequest(certId);
gen.SetRequestExtensions(CreateExtension());
OcspReq req;
req = gen.Generate();
return req.GetEncoded();
}
catch (OcspException e)
{
Console.WriteLine(e.StackTrace);
}
catch (IOException e)
{
Console.WriteLine(e.StackTrace);
}
return null;
}
private static X509Extensions CreateExtension()
{
byte[] nonce = new byte[16];
Hashtable exts = new Hashtable();
BigInteger nc = BigInteger.ValueOf(DateTime.Now.Ticks);
X509Extension nonceext = new X509Extension(false, new DerOctetString(nc.ToByteArray()));
exts.Add(OcspObjectIdentifiers.PkixOcspNonce, nonceext);
return new X509Extensions(exts);
}
}
Chilkat Version :
public static class ChilCatOCSP
{
public static void Validate( string cetpat )
{
Chilkat.Global glob = new Chilkat.Global();
bool chksuccesss = glob.UnlockBundle("Anything for 30-day trial");
if (chksuccesss != true)
{
Console.WriteLine(glob.LastErrorText);
return;
}
Chilkat.Cert cert = new Chilkat.Cert();
bool success = cert.LoadFromFile(cetpat);
if (success != true)
{
Console.WriteLine(cert.LastErrorText);
return;
}
string ocspUrl = cert.OcspUrl;
// Build the JSON that will be the OCSP request.
Chilkat.Prng prng = new Chilkat.Prng();
Chilkat.JsonObject json = new Chilkat.JsonObject();
json.EmitCompact = false;
json.UpdateString("extensions.ocspNonce", prng.GenRandom(36, "base64"));
json.I = 0;
json.UpdateString("request[i].cert.hashAlg", "sha1");
json.UpdateString("request[i].cert.issuerNameHash", cert.HashOf("IssuerDN", "sha1", "base64"));
json.UpdateString("request[i].cert.issuerKeyHash", cert.HashOf("IssuerPublicKey", "sha1", "base64"));
json.UpdateString("request[i].cert.serialNumber", cert.SerialNumber);
Console.WriteLine(json.Emit());
Chilkat.BinData ocspRequest = new Chilkat.BinData();
Chilkat.Http http = new Chilkat.Http();
// Convert our JSON to a binary (ASN.1) OCSP request
http.CreateOcspRequest(json, ocspRequest);
// Send the OCSP request to the OCSP server
Chilkat.HttpResponse resp = http.PBinaryBd("POST", ocspUrl, ocspRequest, "application/ocsp-request", false, false);
if (http.LastMethodSuccess != true)
{
Console.WriteLine(http.LastErrorText);
return;
}
// Get the binary (ASN.1) OCSP reply
Chilkat.BinData ocspReply = new Chilkat.BinData();
resp.GetBodyBd(ocspReply);
// Convert the binary reply to JSON.
// Also returns the overall OCSP response status.
Chilkat.JsonObject jsonReply = new Chilkat.JsonObject();
int ocspStatus = http.ParseOcspReply(ocspReply, jsonReply);
// The ocspStatus can have one of these values:
// -1: The ARG1 does not contain a valid OCSP reply.
// 0: Successful - Response has valid confirmations..
// 1: Malformed request - Illegal confirmation request.
// 2: Internal error - Internal error in issuer.
// 3: Try later - Try again later.
// 4: Not used - This value is never returned.
// 5: Sig required - Must sign the request.
// 6: Unauthorized - Request unauthorized.
if (ocspStatus < 0)
{
Console.WriteLine("Invalid OCSP reply.");
return;
}
Console.WriteLine("Overall OCSP Response Status: " + Convert.ToString(ocspStatus));
// Let's examine the OCSP response (in JSON).
jsonReply.EmitCompact = false;
Console.WriteLine(jsonReply.Emit());
// The JSON reply looks like this:
// (Use the online tool at https://tools.chilkat.io/jsonParse.cshtml
// to generate JSON parsing code.)
// {
// "responseStatus": 0,
// "responseTypeOid": "1.3.6.1.5.5.7.48.1.1",
// "responseTypeName": "ocspBasic",
// "response": {
// "responderIdChoice": "KeyHash",
// "responderKeyHash": "d8K4UJpndnaxLcKG0IOgfqZ+uks=",
// "dateTime": "20180803193937Z",
// "cert": [
// {
// "hashOid": "1.3.14.3.2.26",
// "hashAlg": "SHA-1",
// "issuerNameHash": "9u2wY2IygZo19o11oJ0CShGqbK0=",
// "issuerKeyHash": "d8K4UJpndnaxLcKG0IOgfqZ+uks=",
// "serialNumber": "6175535D87BF94B6",
// "status": 0,
// "thisUpdate": "20180803193937Z",
// "nextUpdate": "20180810193937Z"
// }
// ]
// }
// }
//
// The certificate status:
int certStatus = json.IntOf("response.cert[0].status");
// Possible certStatus values are:
// 0: Good
// 1: Revoked
// 2: Unknown.
Console.WriteLine("Certificate Status: " + Convert.ToString(certStatus));
}
}
Also, I've found an online tool that works ok. here
I think problem is that you use a wrong accept type in your code. You may omit accept type if it is not necessary. You should consider
adding a nonce to your request too.
public CertificateStatusEnum ValidateOCSP(X509Certificate cert, X509Certificate cacert)
{
List<string> urls = GetAuthorityInformationAccessOcspUrl(cert);
if (urls.Count == 0)
{
throw new Exception("No OCSP url found in ee certificate.");
}
string url = urls[0];
Console.WriteLine("Sending to : '" + url + "'...");
byte[] packtosend = CreateOCSPPackage(cert, cacert);
byte[] response = PostRequest(url, packtosend, **"application/ocsp-request"**, **"application/ocsp-response"**);
return VerifyResponse(response);
}
Problem:
I'm making a small app in which, when ran it has a login page requiring the user to enter username and password. When these are entered, the information goes to the server via TCP using sockets. However, from what I found online, in order to do so you need to specify a length of bytes in order to receive the information (see code below). The problem is that when I specify the length the rest of the string becomes \0\0\0 until all the byte slots are filled which causes problems later in the process.
What I tried:
I have tried removing the part "\0\0\0.." from the string but it failed as the program kept failing on finding the character "\". I'm not if I'm using the correct protocol or method for this but any advice is welcomed.
NetworkStream stream = client.GetStream(); //Gets input stream
byte[] receivedBuffer = new byte[100];
stream.Read(receivedBuffer, 0, receivedBuffer.Length);
string msg = Encoding.ASCII.GetString(receivedBuffer,0,receivedBuffer.Length); //translates msg
if(msg.Contains("|")) //if the msg contains "|" = log-in
{
bool cr1 = false;
bool cr2 = false;
string[] cre = msg.Split("|");
if(cre[0] == "admin") //the whole checking system will be made properly and I know this is wrong but its for testing
{
cr1 = true;
}
if (cre[1] == "pass")
{
cr2 = true;
}
if (cr1 == true && cr2 == true)
{
string answer = "True";
Tosend(answer); //Sends response to client
}
else
{
string answer = "False";
Tosend(answer);
}
}
Class to send things:
static void Tosend(string msg)
{
string ip3 = "localhost";
TcpClient client = new TcpClient(ip3, 8081);
int bc = Encoding.ASCII.GetByteCount(msg);
byte[] sd = new byte[bc];
sd = Encoding.ASCII.GetBytes(msg);
NetworkStream st = client.GetStream();
st.Write(sd, 0, sd.Length);
st.Close();
client.Close();
}
EXAMPLE
What I get:
Input: user|pass => to bytes => Sends bytes => Bytes received => Bytes translated => msg = user|pass\0\0\0\0\0\0\0\0...
Expectation:
Input: user|pass => to bytes => Sends bytes from client => Bytes received by server => Bytes translated => msg = user|pass
NetworkStream.Read will return the number of bytes read. You can use that to pull out only the actual data.
int receivedBytes = stream.Read(receivedBuffer, 0, receivedBuffer.Length);
string msg = Encoding.ASCII.GetString(receivedBuffer,0,receivedBytes);
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
I'm creating one web api application, and I want to send notification to iOS devices.
I've tried with pushsharp, but it works fine with few devices only, while sending it to multiple devices with expired/invalid tokens its not sending notifications to all devices.
So, I'm going with following code:
public async Task pushMessage(string deviceToken, string message)
{
int port = 2195;
String hostname = "gateway.sandbox.push.apple.com";
String certificatePath = "~/test.p12" // my certificate path
X509Certificate2 clientCertificate = new X509Certificate2(System.IO.File.ReadAllBytes(certificatePath), CommonSource.GetAppleCertificatePassword(), X509KeyStorageFlags.MachineKeySet);
X509Certificate2Collection certificatesCollection = new X509Certificate2Collection(clientCertificate);
TcpClient client = new TcpClient(hostname, port);
SslStream sslStream = new SslStream(client.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()));
String payload = message.ToString();
writer.Write((byte)0);
writer.Write((byte)payload.Length);
byte[] b1 = System.Text.Encoding.UTF8.GetBytes(payload);
writer.Write(b1);
writer.Flush();
byte[] array = memoryStream.ToArray();
sslStream.Write(array);
sslStream.Flush();
// Read message from the server.
//byte[] response = ReadMessage(sslStream);
client.Close();
}
catch (System.Security.Authentication.AuthenticationException ex)
{
LogError(ex.Message);
client.Close();
}
catch (Exception e)
{
LogError(e.Message);
client.Close();
}
}
This code works exactly what I wanted.
But now I want to check response from APNS server. So I've checked with byte[] response = ReadMessage(sslStream);
And ReadMessage method looks like:
private byte[] ReadMessage(SslStream sslStream)
{
MemoryStream ms = new MemoryStream();
byte[] buffer = new byte[1024];
int bytes = -1;
do
{
bytes = sslStream.Read(buffer, 0, buffer.Length);
ms.Write(buffer, 0, bytes);
} while (bytes != 0);
return ms.ToArray();
}
But when I run this, it stuck at bytes = sslStream.Read(buffer, 0, buffer.Length);
I'm not able to figure out the issue.
Can anyone tell me, what is the issue with my code?
According to this article
The problem with Apples Push Notification Service… Solutions and Workarounds…
Apple never sends a response for messages that succeeded.
An error response may not immediately be returned before you have a chance to send more notifications to the stream, but before Apple closes the connection. These notifications that might still ‘make it through’ are left in limbo. They are never acknowledged or delivered, and never have error responses returned for them.
So in a case where your request works as intended there will be no response. That would mean that your attempt to read a response will not get anything and this is what you are experiencing when you can't get past the read line. It is waiting for a response that might never come.
It seems everyone uses PushSharp for sending push notifications to iOS devices from C#. But that library has a queue it uses instead of sending the notification directly, which then means you need a Windows Service or something to host it properly (per its own documentation) which is overkill for me. I have an incoming web request to my ASP.NET web service and as part of handling that, I want to immediately send a push notification. Simple as that.
Can anyone tell me either how to use PushSharp to send one immediately (bypassing its queue mechanism) or how to properly send the push notification myself? I already have the code that formulates the JSON message, but I don't know how to apply the .p12 file to the request. I can't find any Apple documentation for how to do that.
This is a old question, but the answer is not complete.
Here my code:
// private fields
private static readonly string _apnsHostName = ConfigurationManager.AppSettings["APNS:HostName"];
private static readonly int _apnsPort = int.Parse(ConfigurationManager.AppSettings["APNS:Port"]);
private static readonly string _apnsCertPassword = ConfigurationManager.AppSettings["APNS:CertPassword"];
private static readonly string _apnsCertSubject = ConfigurationManager.AppSettings["APNS:CertSubject"];
private static readonly string _apnsCertPath = ConfigurationManager.AppSettings["APNS:CertPath"];
private readonly ILogger _log;
private X509Certificate2Collection _certificatesCollection;
ctor <TAB key>(ILogger log)
{
_log = log ?? throw new ArgumentNullException(nameof(log));
// load .p12 certificate in the collection
var cert = new X509Certificate2(_apnsCertPath, _apnsCertPassword);
_certificatesCollection = new X509Certificate2Collection(cert);
}
public async Task SendAppleNativeNotificationAsync(string payload, Registration registration)
{
try
{
// handle is the iOS device Token
var handle = registration.Handle;
// instantiate new TcpClient with ApnsHostName and Port
var client = new TcpClient(_apnsHostName, _apnsPort);
// add fake validation
var sslStream = new SslStream(client.GetStream(), false, new RemoteCertificateValidationCallback(ValidateServerCertificate), null);
try
{
// authenticate ssl stream on ApnsHostName with your .p12 certificate
sslStream.AuthenticateAsClient(_apnsHostName, _certificatesCollection, SslProtocols.Tls, false);
var memoryStream = new MemoryStream();
var writer = new BinaryWriter(memoryStream);
// command
writer.Write((byte)0);
// first byte of the deviceId length (big-endian first byte)
writer.Write((byte)0);
// deviceId length (big-endian second byte)
writer.Write((byte)32);
// deviceId data (byte[])
writer.Write(HexStringToByteArray(handle.ToUpper()));
// first byte of payload length; (big-endian first byte)
writer.Write((byte)0);
// payload length (big-endian second byte)
writer.Write((byte)Encoding.UTF8.GetByteCount(payload));
byte[] b1 = Encoding.UTF8.GetBytes(payload);
// payload data (byte[])
writer.Write(b1);
writer.Flush();
byte[] array = memoryStream.ToArray();
await sslStream.WriteAsync(array, 0, array.Length);
// TIP: do not wait a response from APNS because APNS return a response only when an error occurs;
// so if you wait the response your code will remain stuck here.
// await ReadTcpResponse();
sslStream.Flush();
// close client
client.Close();
}
catch (AuthenticationException ex)
{
_log.Error($"Error sending APNS notification. Exception: {ex}");
client.Close();
}
catch (Exception ex)
{
_log.Error($"Error sending APNS notification. Exception: {ex}");
client.Close();
}
}
catch (Exception ex)
{
_log.Error($"Error sending APNS notification. Exception: {ex}");
}
}
private static byte[] HexStringToByteArray(string hex)
{
if (hex == null)
{
return null;
}
// added for newest devices (>= iPhone 8)
if (hex.Length % 2 == 1)
{
hex = '0' + hex;
}
return Enumerable.Range(0, hex.Length)
.Where(x => x % 2 == 0)
.Select(x => Convert.ToByte(hex.Substring(x, 2), 16))
.ToArray();
}
private static bool ValidateServerCertificate(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
{
return true;
//if (sslPolicyErrors == SslPolicyErrors.None)
// return true;
//// do not allow this client to communicate with unauthenticated servers.
//return false;
}
private async Task<byte[]> ReadTcpResponse(SslStream sslStream)
{
MemoryStream ms = new MemoryStream();
byte[] buffer = new byte[2048];
int bytes = -1;
do
{
bytes = await sslStream.ReadAsync(buffer, 0, buffer.Length);
await ms.WriteAsync(buffer, 0, bytes);
} while (bytes != 0);
return ms.ToArray();
}
TIP: with iOS13, device token is received differently.
> iOS 12 (deviceToken as NSData).description -> "< your_token_here >"
> iOS 13 (deviceToken as NSData).description -> "{ length = 32, bytes = 0x321e1ba1c1ba...token_in_bytes }"
With iOS13 you must convert token to string or skip the method
'HexStringToByteArray' because you already have a byte[].
If you have question, I'm glad to answer.
I spent many hours trying to find a way to push notifications, then I found a piece of code that did it for me.
First of all make sure that you installed the certificates correctly, here is a link which will help you.
https://arashnorouzi.wordpress.com/2011/04/13/sending-apple-push-notifications-in-asp-net-%E2%80%93-part-3-apns-certificates-registration-on-windows/
Here is a code I used to push notifications:
public static bool ConnectToAPNS(string deviceId, string message)
{
X509Certificate2Collection certs = new X509Certificate2Collection();
// Add the Apple cert to our collection
certs.Add(getServerCert());
// Apple development server address
string apsHost;
/*
if (getServerCert().ToString().Contains("Production"))
apsHost = "gateway.push.apple.com";
else*/
apsHost = "gateway.sandbox.push.apple.com";
// Create a TCP socket connection to the Apple server on port 2195
TcpClient tcpClient = new TcpClient(apsHost, 2195);
// Create a new SSL stream over the connection
SslStream sslStream1 = new SslStream(tcpClient.GetStream());
// Authenticate using the Apple cert
sslStream1.AuthenticateAsClient(apsHost, certs, SslProtocols.Default, false);
PushMessage(deviceId, message, sslStream1);
return true;
}
private static X509Certificate getServerCert()
{
X509Certificate test = new X509Certificate();
//Open the cert store on local machine
X509Store store = new X509Store(StoreLocation.CurrentUser);
if (store != null)
{
// store exists, so open it and search through the certs for the Apple Cert
store.Open(OpenFlags.ReadOnly);
X509Certificate2Collection certs = store.Certificates;
if (certs.Count > 0)
{
int i;
for (i = 0; i < certs.Count; i++)
{
X509Certificate2 cert = certs[i];
if (cert.FriendlyName.Contains("Apple Development IOS Push Services"))
{
//Cert found, so return it.
Console.WriteLine("Found It!");
return certs[i];
}
}
}
return test;
}
return test;
}
private static byte[] HexToData(string hexString)
{
if (hexString == null)
return null;
if (hexString.Length % 2 == 1)
hexString = '0' + hexString; // Up to you whether to pad the first or last byte
byte[] data = new byte[hexString.Length / 2];
for (int i = 0; i < data.Length; i++)
data[i] = Convert.ToByte(hexString.Substring(i * 2, 2), 16);
return data;
}
Note that this code is for development certificates "Apple Development IOS Push Services".