GithubHelp home page GithubHelp logo

timehop / apns Goto Github PK

View Code? Open in Web Editor NEW
184.0 27.0 47.0 99 KB

A Go package to interface with the Apple Push Notification Service

Home Page: https://godoc.org/github.com/timehop/apns

License: MIT License

Go 100.00%
backend-dependency timehop

apns's People

Contributors

bdotdub avatar biasedbit avatar d1str0 avatar nathany avatar sneuspiel avatar st3fan avatar taylortrimble avatar themartorana avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

apns's Issues

Failing client_test - a little help?

@bdotdub So far I haven't been able to figure out why the two tests on Travis CI are failing. They are both tests that have a bad push.

client_test.go:

  • bad push
  • good, bad, good, requeue of last good

https://travis-ci.org/timehop/apns

I added some logging into RunLoop where the 1 second sleep is. As it turns out, neither of these tests hit that (several other tests did log errors here, probably intentionally - invalid cert, etc).

Tests for errors from Apple

I'd like some tests for errors from Apple but without actually hitting Apple's servers in the test suite.

Status code Description
0 No errors encountered
1 Processing error
2 Missing device token
3 Missing topic
4 Missing payload
5 Invalid token size
6 Invalid topic size
7 Invalid payload size
8 Invalid token
10 Shutdown
255 None (unknown)

I'm thinking about mockConn having a special device token that results in 8 Invalid token, or that sort of thing. A bit like test credit cards.

My thought is to only do this for tests within the apns package, not impacting the callers of this library in any way.

Using the Feedback Service

Hey guys, thanks for this awesome piece of work, this is definitely one of the best APNS packages out there for go.

I'm not really sure if I'm using the feedback service correctly because it sort of never prints any errors, even if I test it with broken/custom device tokens. I would love to detect expired tokens and delete them to keep my data clean and only send pushes to valid devices.

This is how I use it:

func (n *NotificationAgent) sendPushMessages(devices []model.Device,
    t NotificationType, args NotificationArguments, payload Payload) {

    for _, device := range devices {
        if *device.Type == model.DeviceTypeIos {
            p := apns.NewPayload()
            p.APS.Alert.Body = n.getLocalizedPushText(t, args)
            p.APS.Badge.Set(1)
            p.SetCustomValue("type", t)
            p.SetCustomValue("data", payload)

            m := apns.NewNotification()
            m.Payload = p
            m.DeviceToken = *device.Token
            m.Priority = apns.PriorityImmediate

            if e := n.apns.Send(m); e != nil {
                log.Println("Error: " + e.Error())
            }
        }
    }
    for ft := range n.apnsf.Receive() {
        log.Println("Feedback for token:" + ft.DeviceToken)
    }
}

As mentioned above I tried to use random device tokens in the belief that it would print something, but nothing happens. I'm not even sure if I'm calling the feedback service correctly because the sending is asynchronous and maybe my feedback loop is called too early... if so, where and how should I call it?

Oh and, yes n.apnsf is correctly initialized and throws no errors. I just wanted to keep the code small.

Thanks,
codingrogue

No way to close the client

If I have something like:

client := [...]
go func() {
    for f := range client.FailedNotifs {
        [...]
    }
}()
for [...] {
    [...]
    [...] client.Send(n) [...]
    [...]
}
// done, now what?

How do I clean up? I don't see any code that closes client.FailedNotifs, so that goroutine won't stop unless I signal it myself (if I even know it exists). It's also strange to be expected to close client.Conn yourself; this requires intimate familiarity with the client implementation instead of just its interface.

It would be great if Client had a Close() error method, or something comparable.

Provide Feedback Service Errors to Clients.

Similar to, but not a duplicate of, #3. #3 is about not knowing the reason a client has disconnected; this issue is about not knowing the reason the feedback service disconnected.

I'm currently experiencing issues connecting to the feedback service, but my gateway is working fine. I haven't been able to inspect the reason from the lib.

What happens if key file is encrypted with a pass phares?

Hey there,

Sorry if this is a stupid question but I did not see anywhere where I can enter the pass phrase for the key file.

Without it when I try to use NewClientWithFiles method, it just gives me the following error.

2016/01/13 11:16:32 Could not create clientcrypto/tls: failed to parse private key

I tried my cert and key file with openssl client and they worked fine. I am also using the same files on the production with my api (php).

New, TLSv1/SSLv3, Cipher is AES256-SHA
Server public key is 2048 bit
Secure Renegotiation IS supported
Compression: NONE
Expansion: NONE
SSL-Session:
    Protocol  : TLSv1
    Cipher    : AES256-SHA
    Session-ID:
    Session-ID-ctx:
    Master-Key: 0C21677365B5EE5EA621FE5C3464C8B750A574E1ED632D34AF683121305054F4C12409D672C1B9F4BFCD89685671AA13
    Key-Arg   : None
    Start Time: 1452678448
    Timeout   : 300 (sec)
    Verify return code: 0 (ok)

---
asdasdasd
closed

[feature request] Notification of success

When receiving an error-response packet with a status code of 0 (No errors encountered), does it just ignore that?

I'm thinking that I'd like to send "success" over FaildNotifs (probably renamed). Essentially, I'm trying to wrap a synchronous API around this low-level socket stuff so that my REST API can return an appropriate status code.

Right now I'd be waiting on FailedNotif with a timeout, and if nothing shows up, assume success.

I'd probably keep that behaviour, but if I get back 0 (No errors encountered), I can stop waiting for the timeout and just return 200 OK right away.

What do you think?

Asynchronous send results in exiting before all notifications are sent

It's clear that:

  • Reliable delivery (and its limitations) is not generally well understood by users of the library
  • Error handling / reporting should be more clearly exposed
  • Some clients will benefit from a Nagle's-like or TCP_CORK-like implementation of sending APNS messages

In my mind, these are all closely intertwined with throughput. Any of these can be solved very easily at the expense of throughput. With high throughput in mind, these become more challenging to address. :)

This issue is a teaser. I have some ideas, but I'm going to wait until we're finished nailing down #31 before opening that bucket of worms. That way we can avoid the API moving every which way at once.

Forgive me,

@tylrtrmbl

fatal error: all goroutines are asleep - deadlock!

package main

import (
  "fmt"
  "github.com/timehop/apns"
)

func main() {
     c, _ := apns.NewClient("gateway.sandbox.push.apple.com:2195", "dev_ck.pem", "dev_pk.pem")

    p := apns.NewPayload()
    p.APS.Alert.Body = "I am a push notification!"
    p.APS.Badge.Set(5)
    p.APS.Sound = "turn_down_for_what.aiff"

    m := apns.NewNotification()
    m.Payload = p
    m.DeviceToken = "A_DEVICE_TOKEN"
    m.Priority = apns.PriorityImmediate

    resp := c.Send(m)
    fmt.Printf("%v\n",resp)
}





fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send (nil chan)]:
main.main()
D:/go/bin/project/src/applePush/applyPush.go:21 +0x1b2
exit status 2
``

Handling timeouts/disconnects, dropping some pushes?

Since we switched to using this library in production, we've had many reports of dropped push notifications. I think it's because of the persistent connection, which may at times error and then disconnect. Sounds like they're trying to handle this in https://github.com/pranavraja/apns, though that looks like less mature.

Any thoughts on how to detect/avoid this, or any recommendations? Appreciate the library and the help.

Thanks,
Ben

Hanging on send()

c.Send(m) isn't returning and I'm not sure why. I created a client without error and am not receiving any errors on sent PNs.

    c, err := apns.NewClientWithFiles(apns.SandboxGateway, CERT_PEM, KEY_PEM)
    if err != nil {
        log.Println("Client error: ", err.Error())
    }

    go func() {
        for f := range c.FailedNotifs {
            log.Println("Notif", f.Notif.ID, " failed with ", f.Err.Error())
        }
    }()

    p := apns.NewPayload()
    p.APS.Alert.Body = content
    badge := 1
    p.APS.Badge = &badge
    p.APS.Sound = "bingbong.aiff"

    m := apns.NewNotification()
    m.Payload = p
    m.DeviceToken = author.PnId
    m.Priority = apns.PriorityImmediate
    m.ID = content

    log.Println("---")
    c.Send(m)
    log.Println("...")

No mockable client interface

In order to test code that uses this client, I would have to mock net.Conn, if I understand correctly. This is onerous; mocking the client's API is much easier. Exporting a mockable client interface, and an implementation of it, helps with this.

Something along these lines...

type Client interface {
    Send(n Notification) error
    Conn() *Conn
    FailedNotifs() chan NotificationResult
}

type client struct {[...]} // implements Client

func NewClient(gw string, cert string, key string) (Client, error) {...}

so we can test code like this:

func Foo(client apns.Client) error {
    [...]
    if err := client.Send([...]); err != nil {
        return err
    }
    [...]
}

like this:

func TestFoo(t *testing.T) {
    mymock := MyMock{} // has type apns.Client
    mymock.When("Send", apnsClient.Notification{[...]}).Return(errors.New("test"))
    if Foo(mymock) == nil {
        t.Error("Foo should return apns error")
    }
}

Expose mockTLSServer?

It would be pretty sweet if I could write "integration" tests for my own code that don't hit Apple's API.

Thoughts?

Connection Error Feedback

Is there a provision for the apns.Client to notify the application when an APNS connection has been lost? I'm thinking I have a bug where I cannot connect to the APNS gateway, but I'm unable to get any visibility into the package if the connection is succeeding or not.

Graceful shutdown

I noticed the run loop seems to be an infinite loop. Is there anyway we can send a command to gracefully shutdown the server?

Additional maintainers?

@bdotdub Would it be possible to add some additional maintainers to this project?
#14 Has been open since December 10th. It has tests, it received code review and updates based on review. While it changes behaviour in certain instances, I don't think it would be considered a breaking change.

It's difficult to contribute further while these pull requests remain open.

Revisit Error Types

There were some comments from #41 that hinted we may wish to revisit error types. I don't have anything specific in mind today, but recommend we review error handling specifically after some of the more major API changes we're considering.

Failed to parse certificate PEM data

Excuse my ignorance.. I'm new to the iOS world.

I'm getting the error: "crypto/tls: failed to parse certificate PEM data" from this code:

    c, err := apns.NewClient(apns.SandboxGateway, CERT_PEM, KEY_PEM)
    if err != nil {
        println("ERROR!!!!: ", err.Error())
    }

    p := apns.NewPayload()
    p.APS.Alert.Body = content
    badge := 1
    p.APS.Badge = &badge
    p.APS.Sound = "bingbong.aiff"

    m := apns.NewNotification()
    m.Payload = p
    m.DeviceToken = author.PnId
    m.Priority = apns.PriorityImmediate

    c.Send(m)

where CERT_PEM ("devcert.pem") and KEY_PEM ("devkey.pem") are the filenames. The code above is being called from a directory below the files:

    project/
        folder/
            sendAPN.go
        devcert.pem
        devkey.pem

I created the CERT_PEM by using this command:

     openssl x509 -in aps_development.cer -inform der -out devcert.pem

and I created KEY_PEM using this command:

     openssl pkcs12 -nocerts -out devkey.pem -in key.p12

I successfully tested these by using this command:

     openssl s_client -connect gateway.sandbox.push.apple.com:2195 
     -cert devcert.pem -key devkey.pem

What am I doing wrong here? Thanks a bunch for the library.

Failednotifs

When Failednotifs has objects, do I have to use the send command to attempt to resend?

Alert is serialized to JSON even if all fields are blank.

I think it's because it's a struct. One thought is to make it a pointer, but that adds some complexity to the user.

If we wanted to avoid complexity for the user, we could do our own serialization entirely to make sure the alert isn't serialized unless it needs to be. We could probably do that as a method on the Alert itself, which would be neat. That adds complexity to the lib though.

sent 1000 push notes and it failed to send because of too many connections

Is there a batch limit I need to adher to?

I sent it to 1000 folks, each push note was c.Send and it processed them quite fast which was rad but at around 500 it just stopped sending. I then tried processing without a c.Send and it went through all the device tokens and complained at the end that there were too many connections.

Can not run google app engine

I can run it correctly in local app engine environment, but when I deployed on the Google app engine, it won't work. Any idea?

Using Nagle's algorithm?

https://developer.apple.com/library/mac/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Chapters/CommunicatingWIthAPS.html states in part:

The binary interface employs a plain TCP socket for binary content that is streaming in nature. For optimum performance, batch multiple notifications in a single transmission over the interface, either explicitly or using a TCP/IP Nagle algorithm.

Was net.TCPConn.SetNoDelay= false considered? The quotation seems to indicate doing this would yield better performance.

Some tests time out after 1 second

The tests seem to be flaky. Before I made any changes, a few tests would sometimes time out, but other times succeed. There should be a longer test time out than 1 second, it seems.

What is the lifetime of the client instance?

As the title says, since I've read from the docs that it might periodically disconnect any idle APN client. Is this managed automatically and we should only create one client? Or should we create new client each time we have a payload to deliver?

Same goes for the feedback loop. Do we just fire one loop? Or do we have to continually fire one as we create new client?

Could not create feedback crypto/tls: failed to parse certificate PEM data

Send ok.
feedback error:Could not create feedback crypto/tls: failed to parse certificate PEM data

code

client, err := apns.NewClientWithFiles(apns.ProductionGateway, "resource/cer.pem", "resource/key.unencrypted.pem")
    if nil != err {
        util.NewLog().Info(err.Error())
    }
    //设置样式
    payload := apns.NewPayload()
    payload.APS.Alert.Body = "hello,world2222"
    payload.APS.Sound = "bingbong.aiff"
    m := apns.NewNotification()
    m.Payload = payload
    m.DeviceToken = "00d7c5d3d764b4eef67096f562748c2ee11939bddffed866f453ca745249fdaa"
    client.Send(m)

    f, err := apns.NewFeedback(apns.ProductionFeedbackGateway,"resource/cer.pem", "resource/key.unencrypted.pem")
    if err != nil {
        fmt.Println("Could not create feedback", err.Error())
    }
    for ft := range f.Receive() {
        fmt.Println("Feedback for token:", ft.DeviceToken)
    }

Report Error for Malformed Device Token

I just spent a long time tracking down a bug where I was passing a byte slice for the device identifier instead of a hex encoded string. I took an awfully long time, since I didn't have an error come out of Client.FailedNotifs.

We should do that. 😊

How do I simulate FailedNotifs?

I'm currently working on a REST API wrapper around APNs. I've been trying to figure out how to handle the FailedNotifs channel, but I'm finding it difficult to send a request that causes an error.

I've tried fiddling around in ToBinary to send an invalid notification, but it ends up with an err.Identifier that doesn't match the notification identifier (err.Identifier just binary gibberish).

I'm wondering how you use FailedNotifs in production? Any suggestions?

What is the reason for APS.MarshalJSON?

I don't understand why APS.MarshalJSON is defined:

func (aps APS) MarshalJSON() ([]byte, error) {
    data := make(map[string]interface{})

    if !aps.Alert.isZero() {
        data["alert"] = aps.Alert
    }
    if aps.Badge != nil {
        data["badge"] = aps.Badge
    }
    if aps.Sound != "" {
        data["sound"] = aps.Sound
    }
    if aps.ContentAvailable != 0 {
        data["content-available"] = aps.ContentAvailable
    }
    if aps.Category != "" {
        data["category"] = aps.Category
    }
    if aps.URLArgs != nil && len(aps.URLArgs) != 0 {
        data["url-args"] = aps.URLArgs
    }

    return json.Marshal(data)
}

This appears to almost duplicate the default marshaling behavior for json struct tags. The only difference I can see is with Alert, where Alert.Title/Action don't affect whether Alert is included, and I don't understand why. Perhaps I'm missing something?

Connection Ideas

Hi all,

I've shared these ideas with @bdotdub and gone over parts with @nathany now as well. I just found my notes and figured I'd type them here on GitHub as well.

The ideas I will lay out below was guided by some factors from the community:

  • We've seen some demand for a more mockable components, especially connections
  • We've seen some demand for greater configuration of the connection
    • Example: Support connection-level Nagle's algorithm
    • Example: Support custom TLS configuration

Here's what I came up with:

The current Connection struct in master is simply a net.Conn wrapper with some configuration. This isn't the same as a mockable interface, and is probably insufficient. In develop, Connection handles three things:

  • Read, write, and close (via io.ReadWriteCloser)
  • Reconnection
  • One configuration parameter: read deadline

I posit that if we move the responsibility for reconnection into the Client, we can get rid of the Connection altogether and simply use io.ReadWriteCloser. It would work this way:

  • When the Client detects a connection (io.ReadWriteCloser) has gone sour, it gets a new one from a provider function
  • We supply a default provider function that makes a connection configured with sensible defaults for a given gateway and tls.Certificate

Example

type ConnectionProvider func(gateway string, cert tls.Certificate) (io.ReadWriteCloser, error)

type Client struct {
    ...
    Gateway       string
    Cert          tls.Certificate
    NewConnection ConnectionProvider
    ...
}

func DefaultConnectionProvider(gateway string, cert tls.Certificate) (tls.Conn, err Error) {
    gatewayParts := strings.Split(gw, ":")
    config := tls.Config{
        Certificates:       []tls.Certificate{cert},
        ServerName:         gatewayParts[0],
    }

    return tls.Dial("tcp", gateway, config)
}

func NewClient(gateway, cert tls.Config) *Client{
    return &Client{
        Gateway: gateway,
        Cert: cert,
        NewConnection: DefaultConnectionProvider,
    }
}

func Example() {
    cert, err := tls.LoadX509KeyPair(certFile, keyFile)
    if err != nil {
        panic("cannot load APNS cert")
    }

    client := NewClient(ProductionGateway, cert)
    ...
}

With this scheme, the Client is able to make a new connection whenever it needs one. The user is able to replace NewConnection to provide a custom-configured connection, as well as replace NewConnection to provide a mock connection. DefaultConnectionProvider is exported so it can be used if the user wants override only a few settings from the default connection.

Let me know what you guys think!

Related projects

For a little inspiration, I'm taking a look at other Go projects for APNS, starting with the one @finkel mentioned in #47 and then the one @themartorana mentioned (#38). @willfaught mentioned anachronistic in #26 (comment).

apns by Pranav Raja @pranavraja https://github.com/pranavraja/apns

  • Exposes a queue (a slice) for sending multiple notifications at once. Timehop has an internal circular buffer rather than exposing a queue. Currently neither implementation batches multiple notifications into a single frame.
  • It uses the synchronous approach of sending a batch, then reading with a timeout. Timehop does the read on a separate goroutine. Based on Apple's documentation I believe the separate goroutine to be more responsive, though reading with a timeout does make for a nice and simple synchronous API.
  • Rather than resend failed messages internally, it reports what was unsent and reslices the queue for resending (less magic).
  • There is a timeout parameter for sending. 👍
  • Exposes a lower-level API to read errors from the socket and decode the Apple format. This is an interesting alternative to passing those errors over a channel. Timehop does a blocking read in a goroutine and sends errors over FailedNotifs, but a blocking read in a goroutine could be part of the client code or a high-level API.
  • Offers a lower-level interface where you can make a notification before sending it.
  • Notification is a separate package. 👍
  • MakeNotification takes a string rather than defining the structs for JSON serialization.

SendOne, MakeNotification, and ReadInvalid make for a nice lower-level API that other things can be built on. I'm not sure how I feel about the queue. If I was fanning-out the same/similar message to a number of devices or using it as a mechanism for batch & send, maybe it could work, but managing multiple queues at once (requeuing on errors while sending more messages) could become cumbersome for the client.

go-libapns by Karl Kirch @joekarl https://github.com/joekarl/go-libapns

  • Batches push notifications up to 64K (TCP packet size) and flushes every 10ms. This is accomplished with an internal inFlightFrameByteBuffer.
  • Doesn't use Nagel #32 in order to have a shorter timeout length (REAMDE includes explanation)
  • Provides two functions for creating connections, one takes an existing tcp net.Conn for use on Google App Engine, etc.
  • API uses channels (SendChannel) rather than a Send() method (Timehop currently puts a notification into an internal channel). Also uses a runLoop called sendListener.
  • When an error occurs, it sends the notification that failed and all the unset notifications over a CloseChannel.
  • To clear badges, it was relying on negative numbers (instead of 0), but now uses BadgeNumber from @themartorana. See #38.
  • It will clip the alert body if the payload is over Apple's 2K limit.

apns by @anachronistic https://github.com/anachronistic/apns

  • Alert uses an interface that can be either a string or an AlertDictionary. Timehop currently checks if only Alert.Body is set and serializes the JSON in the more efficient way, but the API still requires an Alert struct.
  • It has a PayloadString() for serializing the JSON without doing the binary encoding or sending it.
  • It connects, writes and waits for a response (with a timeout) all in one step. This may be fine for very low-volume push notifications, but Apple treats rapid connection and disconnection as a denial-of-service attack.
  • Overrides Badge 0 to -1 (not sure the point, vs just not using omitempty). Not sure if it's possible to leave badge as-is (omitempty)?
  • Does a check for MaxPayloadSizeBytes and returns an error. Timehop does not.
  • Generates a pseudo-random identifier up to 9999 rather than incrementing a counter or requesting it from the caller.

apns by Arash Payan @arashpayan https://github.com/arashpayan/apns

  • Uses a pseudo-random identifier like anachronistic.
  • Uses a slice to store the last 25 notifications (rather than a list). Hm.
  • Uses an invalidTokenHandler callback
  • Exponential backoff for reconnects.

apns by Ryan Slade @ryanslade https://github.com/ryanslade/apns

  • Has a shortcut for pushing a message with just an alert (PushMessage).
  • Uses an idChan to generate incrementing identifiers
  • Launches a go routine to resend payloads on failure.
  • Holds on to a slice of payloads and cleans them up after 5 minutes rather than using a circular list that limits the amount.

apns on App Engine by Siong @siong1987 https://github.com/siong1987/apns

  • This uses appengine.Context and appengine/socket.
  • It has it's own PEM loader that supports keys with a passphrase.
  • Uses the synchronous read after writing method.
  • Has a connection pool (10 connections to Apple).
  • Uses a pseudo-random identifier like anachronistic.

hermes APNS/GCM by Paul Karadimas @pkar https://github.com/pkar/hermes

Go-APNs by Matthew Price @mattprice https://github.com/mattprice/Go-APNs

apns-go by @Kwik-messenger https://github.com/Kwik-messenger/apns-go

go_apns by @MessageDream https://github.com/MessageDream/go_apns

Go-Apns by @virushuo Ju Huo https://github.com/virushuo/Go-Apns

  • The example is interesting in that mutates an existing payload, yet Send takes a pointer to a notification. Hm.
  • Overall a similar architecture with a sendLoop and reconnecting. It also uses an errorChan for reporting errors (but of type error) and a concrete NotificationError struct with an OtherError for storing read errors, etc.

APNs by Cornel Damian @corneldamian https://github.com/corneldamian/APNs

APNs by Matt DuVall @mduvall https://github.com/mduvall/go-apns

go-apns by Fabrizio Milo @Mistobaan https://github.com/Mistobaan/go-apns

Apns by @houxiaobei https://github.com/houxiaobei/Apns

Not sending FailedNotif if identifier not specified.

This relates to #10, as I try to build a synchronous API around Send and FailedNotif.

To get an error from Apple, I am changing a single digit in an otherwise valid device token. I've added logging to timehop/apns.

On this push (with invalid data) nothing comes back from Apple:

2014/12/17 11:32:26 Apple gateway at gateway.sandbox.push.apple.com:2195
2014/12/17 11:32:28 pushing payload...
2014/12/17 11:32:28 json: {"aps":{"alert":{"body":"hello robots"}}}
2014/12/17 11:32:29 Timed out waiting for errors (FailedNotifs). Must be good.

That's not great, but okay. On this push, errrors are coming back from Apple, but the identifiers don't match up so FailedNotifs are never sent.

2014/12/17 11:32:31 pushing payload...
2014/12/17 11:32:31 json: {"aps":{"alert":{"body":"hello robots"}}}
2014/12/17 11:32:31 handleError n.Identifier=0 err.Identifier=2 err=&apns.Error{Command:0x8, Status:0x8, Identifier:0x2, ErrStr:"Invalid token"}
2014/12/17 11:32:31 handleError n.Identifier=0 err.Identifier=2 err=&apns.Error{Command:0x8, Status:0x8, Identifier:0x2, ErrStr:"Invalid token"}
2014/12/17 11:32:32 Timed out waiting for errors (FailedNotifs). Must be good.

If Identifier is set, then I do see an identifier match and the error is sent via FailedNotif:

2014/12/17 11:35:02 pushing payload...
2014/12/17 11:35:02 json: {"aps":{"alert":{"body":"hello robots"}}}
2014/12/17 11:35:02 handleError n.Identifier=1234 err.Identifier=1234 err=&apns.Error{Command:0x8, Status:0x8, Identifier:0x4d2, ErrStr:"Invalid token"}
2014/12/17 11:35:02 reportFailedPush apns.Notification{ID:"user:timestamp", DeviceToken:"30c4afb...", Identifier:0x4d2, Expiration:(*time.Time)(0xc2080bc018), Priority:10, Payload:(*apns.Payload)(0xc2082488c0)} true
2014/12/17 11:35:02 Notif user:timestamp failed with {8 8 1234 Invalid token}

TCP Keepalive option

It would be useful to have the possibility of setting some socket options from the application while initializing a Client.
For example I had to deploy a low-traffic notification gateway on Google Cloud Platform. All the instances have a private IP, NATed on a public one. The NAT timeouts after 10 mins of TCP inactivity.
I forked your repo, please see what I mean here: zaninime@5f8cba1

Client and Session separation

Hey y'all!

Sorry for the radio silence on a bunch of the issues & PRs. As mentioned in #17, I've been thinking about how to make it a bit more synchronous to make the data flow so that the error reporting and handling isn't as roundabout. This feels like the root of the problem for #4, #19, and #25.

In the current implementation, it feels like I've conflated multiple concepts into a single Client struct. Right now, the Client maintains the whole lifecycle of the interactions with Apple (connection, reconnection, requeueing, etc). While Client has an arguably convenient interface, it doesn't allow for granular control for what to do for connection retry strategies, reconnection behavior (ie. do we automatically requeue all failed notifs?), etc.

On the branch I've been working on (BranchComparison to master), I've added a separate Session concept. The state of the branch is that most of the code is there, and I'm now just filling in the rest of the tests.

At a high level, here is what changed:

  • Conn – connects to Apple's gateways and holds the TLS config. The main change here is that Conn is now an interface so that it can be mockable.
  • Session – this represents a single stateful lifecycle of Connect > Send notifs > Disconnect with the APNS servers. Once it disconnects (explicit via the caller, or implicit via APN errors), it can no longer be revived and reused. What this allows us to do is to allow a way to pull out the requeueble notifications and decide what to do with it – that is in contrast to what happens now (automatically requeues them on reconnection). This, again, is also an interface for mockability.
  • Client – largely the same interface from before. It is meant to still be really convenient for the caller. The difference in implementation is that the grisly bits of a single session to APNS is now in Session. Client is now only responsible for holding a Conn and then create Sessions to interact with. Client holds the higher level logic of creating a new Session on disconnect and requeueing undelivered notifs. By extracting out the Session bits, it'll be much easier to be able to provide ways to change reconnection policies, strategies for how to deal with undelivered notifs, etc

In theory, you could easily create your own Client with your own behavior by using Session if you didn't like the one in this lib.

These changes have also helped the testability and brittleness of the previous tests. They should now be running consistently on Travis.

I'd love to get all of your thoughts on this!

cc @brunodecarvalho @tylrtrmbl @nathany @willfaught

In the meantime, I'll take a look at the PRs currently open

New HTTP/2 Push Notification Provider API from Apple

Apple announced a new provider API coming later in 2015 that is based on HTTP/2.
https://developer.apple.com/videos/wwdc/2015/?id=720 (at about 20 minutes)

This uses a request/response cycle with standard http status codes. The connection is left open after a notification failed, and there is no need to resend notifications. There is also no need for a separate feedback service.

There will be no need to keep the existing APNS provider API as the new API applies to older devices running existing versions of iOS and OS X. The video mentions some other changes, such as 4K payloads and ~100-character device tokens.

How to Batch send push

when we send to many devices with the same message, is there a method to make it called once ?

Wish list

There are a number of things I would like to see:

  • Separate loading certificates from configuring the Client or Feedback service, and add support for *.p12 DER certificates as exported from the Apple keychain. #37

  • Separate the serialization and binary encoding of payloads from actual sending.

  • Provide (optional) validation that should reduce the chance of error responses from Apple.

  • Mock tests for when Apple does have an error response, #40.

  • Change ErrProcessing, etc to errors instead of strings. Error Response shouldn't need Command (8) or Status.

  • Move away from using pointers for omitempty when serializing JSON #38

  • Handle retry logic differently for cases like revoked or invalid certificates (if those can be checked before connecting, that would be great too).

  • Allow closing the client #25, for example if the certificate expired and a new connection is needed (though in that case it may close due to an error).

  • Reevaluate how notifications are stored and resent. Currently there is a buffer of 50 messages, which may not be enough. Rather than a maximum, another option would be to assume success after a given time period (or some combination with a much higher maximum). Also, messages aren't requeued until the next Send (on develop).

    There is also a cleanup thread running that constantly checks the oldest notification in the ‘Sent’ queue to see if it’s older than a few seconds and if so, it is assumed to have been successfully sent and is removed from the ‘Sent’ queue. - The Problem With Apples Push Notification Service

    It can take a while for the dropped connection to make its way from APNs back to your server just because of normal latency. It's possible to send over 500 notifications before a write fails because of the connection being dropped. Around 1,700 notifications writes can fail just because the pipe is full, so just retry in that case once the stream is ready for writing again. - Push Notification Throughput and Error Checking

    Maybe storing/resending notifications needs to be extracted and optional so that in-memory isn't the only option.

  • Remove determineIdentifier and require ID to be set explicitly by the caller #19. Or provide an identifier generator that must be called explicitly rather than passing no ID.

  • Ensure caller is aware of Error responses even if it can't be mapped to a recently sent Notification (SessionError).

Timed out test

Hi,

When I ran the tests for 135f567, I saw this

$ go test
Running Suite: Apns Suite
=========================
Random Seed: 1431423856
Will run 72 of 72 specs

••••••••••••••••••••••••••••••••••••••••••••
------------------------------
•... Timeout [1.000 seconds]
Client
/home/mark/workspace/src/apns/client_test.go:384
  #Send
  /home/mark/workspace/src/apns/client_test.go:383
    bad push
    /home/mark/workspace/src/apns/client_test.go:181
      should not return an error [It]
      /home/mark/workspace/src/apns/client_test.go:180

      Timed out

      /home/mark/workspace/src/apns/client_test.go:180
------------------------------
••
------------------------------
•... Timeout [1.000 seconds]
Client
/home/mark/workspace/src/apns/client_test.go:384
  #Send
  /home/mark/workspace/src/apns/client_test.go:383
    good, bad, good, requeue of last good
    /home/mark/workspace/src/apns/client_test.go:382
      should not return an error [It]
      /home/mark/workspace/src/apns/client_test.go:381

      Timed out

      /home/mark/workspace/src/apns/client_test.go:381
------------------------------
••••••••••••••••••••••••

Summarizing 2 Failures:

[Timeout...] Client #Send bad push [It] should not return an error 
/home/mark/workspace/src/apns/client_test.go:180

[Timeout...] Client #Send good, bad, good, requeue of last good [It] should not return an error 
/home/mark/workspace/src/apns/client_test.go:381

Ran 72 of 72 Specs in 5.489 seconds
FAIL! -- 70 Passed | 2 Failed | 0 Pending | 0 Skipped --- FAIL: TestApns (5.49s)
FAIL
exit status 1
FAIL    apns    6.554s

For the first case ("#Send -> bad push -> should not return an error), I guess it's because this line block: https://github.com/timehop/apns/blob/master/client_test.go#L169

It's probably something wrong with my setup. Have you seen this before?

improve Notification.ToBinary() to reduce memory copy

var placeholder = [5]byte{}

func (n Notification) ToBinary() ([]byte, error) {
   ....
   /* write header place holder*/
   buf.Write(placeholder[:])

   /* write fields: token,payload... etc */
   ...

   /* rewrite header */
   b = buf.Bytes()
   b[0] = commandID
   binary.BigEndian.PutUint32(b[1:5], uint32(len(b)-5))
   return b, nil
}

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.