Only from PHP: Unable to connect APNS gateway.push.apple.com:2195

12,518

I've found the issue and fixed it.

The problem was in .pem certificate. Somehow there were two certificates in one file for both production and development .pem files. The same .pem file with two certificates was in the repo for a long time but APNs stopped to work only few months ago. Maybe something was upgraded/changed on the Apple's side.

I assume the Ruby code somehow removes certificate duplication or maybe it took only first certificate, so it worked in Ruby.

However, the solution was to remove the second certificate from the .pem file. After that APNs started to work and they work now (I received some just yesterday).

Share:
12,518
gorodezkiy
Author by

gorodezkiy

Updated on June 07, 2022

Comments

  • gorodezkiy
    gorodezkiy almost 2 years

    It's a late night. I just spent 10 hours in google/stackoverflow search and experiments. And seems I hate Apple Push Notifications. I'm totally frustrated and will appreciate any help.

    Thank you.

    The problem:

    The PHP code for sending Apple Push Notifications, which successfully worked two weeks ago stopped to work now and throws following errors:

    PHP Warning:  stream_socket_client(): Failed to enable crypto in /home/...
    PHP Warning:  stream_socket_client(): unable to connect to ssl://gateway.push.apple.com:2195 (Unknown error) in /home/...
    

    It stopped to work on two separate servers, which are using separate scripts for APNs sending.

    Environment:

    Servers: CentOS 6.5 with PHP 5.4.32 and Ubuntu 14.04.3 with PHP 5.5.9

    APN: In production mode

    Certificates: tested with 700+ push notifications.

    One of the servers is using https://github.com/immobiliare/ApnsPHP, other - https://github.com/antongorodezkiy/wp-apn, on localhost I tested simple file without using any third party code.

    Investigation:

    For all cases below I used the same active device token and the same production PEM certificate.

    php

    However, even this simple code doesn't work on both servers and localhost and return the same error as above:

    $ctx = stream_context_create();
    stream_context_set_option($ctx, 'ssl', 'local_cert', '/absolute/path/to/apn_prod.pem');
    
    // Open a connection to the APNS server
    $fp = stream_socket_client(
        'ssl://gateway.push.apple.com:2195', $err,
        $errstr, 60, STREAM_CLIENT_CONNECT|STREAM_CLIENT_PERSISTENT, $ctx);
    

    I also tried to play with stream_context_set_option() options, include entrust_2048_ca.cer, etc, and even some options from this article. Although provided code worked without any modifications before August 2015.

    openssl

    Connection worked with openssl (link):

    openssl s_client -connect gateway.push.apple.com:2195 -cert /absolute/path/to/apn_prod.pem -debug -showcerts -CAfile /absolute/path/to/server-ca-cert.pem
    

    And got with CONNECTED(00000003) and Verify return code: 0 ( ok ).

    telnet

    Connection worked with telnet:

    -sh-4.1$ telnet gateway.push.apple.com 2195
    Trying 17.172.233.150...
    Connected to gateway.push.apple.com.
    

    pecl apn

    It didn't send push notification. I just tried to use adaptation of sample code, but got the error Invalid token. The token is active and same token I used everywhere and for Houston and Ruby too.

    houston

    It worked with Houston

    apn push "0346a53f...231d9d6abe11" -c /absolute/path/to/apn_prod.pem -m "Hello from the command line!" -e "production"
    

    ruby

    I'm not a Ruby programmer (yet at least), but after success with Houston, I found and adapted Ruby code without Houston dependency.

    And it worked:

    #!/usr/bin/env ruby
    
    require 'openssl'
    require 'socket'
    require 'json'
    
    token = "0346a53f...231d9d6abe11"
    cert = File.read("/absolute/path/to/apn_prod.pem")
    ctx = OpenSSL::SSL::SSLContext.new
    ctx.key = OpenSSL::PKey::RSA.new(cert, '') #set passphrase here, if any
    ctx.cert = OpenSSL::X509::Certificate.new(cert)
    
    sock = TCPSocket.new('gateway.push.apple.com', 2195) #development gateway
    ssl = OpenSSL::SSL::SSLSocket.new(sock, ctx)
    ssl.connect
    
    payload = {"aps" => {"alert" => "Oh hai!", "badge" => 1, "sound" => 'default'}}
    json = payload.to_json()
    token =  [token.delete(' ')].pack('H*') #something like 2c0cad 01d1465 346786a9 3a07613f2 b03f0b94b6 8dde3993 d9017224 ad068d36
    apnsMessage = "\0\0 #{token}\0#{json.length.chr}#{json}"
    ssl.write(apnsMessage)
    
    ssl.close
    sock.close
    
    puts "End"
    

    Questions:

    1. What's wrong with PHP? Is there some bug related to this issue? (I didn't find bug report though)
    2. Any ideas how to solve this issue?
    3. Any ideas what could be the difference in PHP and Ruby cases (I assume that Python or Perl could work fine too)? I even tried to read PHP sources, but without success to understand how stream_socket_client() implemented.

    Please help.