push

package
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 3, 2026 License: MIT Imports: 12 Imported by: 0

Documentation

Overview

Package push wakes managed devices through APNs: a Pusher sends one MDM push per Target, Notifier looks targets up in storage, sends, and publishes events, Coalescer collapses bursts, and CertStore supplies the push certificate per topic.

Why

An MDM server cannot talk to a device; it can only ask APNs to wake it, after which the device connects and asks for work. Phase 3 of the plan of record needs that wake-up path to be reliable under bursts and honest about failure: a 410 from APNs marks the token invalid and publishes PushTokenInvalid instead of retrying forever, a burst of changes for one enrollment becomes one push, and a rotated push certificate is picked up without a restart (decision records 0007 and 0015). StoreCertStore reloads from storage.PushCertStore on version change; StaticCertStore is for tests and single-tenant deployments.

Result.Outcome is what a caller acts on. It separates a token APNs says is dead (410, and only 410) from a request APNs refused — a wrong topic, a mismatched or expired certificate, the sandbox environment — because the second is normally true of every device at once and must not be read as a fleet that has gone quiet (decision record 0042).

The HTTP/2 client that actually talks to Apple is push/apns, certificate parsing is push/pushcert (standard library only, to avoid an import cycle with storage), and fakes are push/pushtest.

References

Index

Constants

View Source
const DefaultCertTTL = 30 * time.Second

DefaultCertTTL is how long a StoreCertStore trusts a cached certificate before asking the store whether its Version moved.

Variables

View Source
var (
	ErrNoCertificate = errors.New("push: no push certificate for topic")
	ErrCertExpired   = errors.New("push: push certificate expired")
	ErrInvalidToken  = errors.New("push: device token invalid")
	ErrRejected      = errors.New("push: APNs rejected the request")
	ErrRateLimited   = errors.New("push: rate limited")
	ErrUpstream      = errors.New("push: APNs error")
)

Errors returned by this package.

View Source
var ErrCoalesced = errors.New("push: coalesced with a recent push")

ErrCoalesced marks a push that was skipped because one was sent recently.

Outcomes lists every outcome, for exhaustiveness tests and label sets.

Functions

func ExpiringCerts

func ExpiringCerts(ctx context.Context, s storage.PushCertStore, now time.Time, within time.Duration) ([]storage.PushCert, error)

ExpiringCerts lists the stored certificates whose NotAfter is within `within` of now, or already past, so a deployment can schedule its own renewal check without a timer inside the library. The records carry no key material. An empty store yields an empty, non-nil slice.

Types

type CertStore

type CertStore interface {
	// PushCertificate returns the certificate with its private key. The
	// certificate must contain the topic as its UID.
	PushCertificate(ctx context.Context, topic string) (tls.Certificate, error)
}

CertStore provides the APNs push certificate for a topic.

type CertStoreOption

type CertStoreOption func(*StoreCertStore)

CertStoreOption configures a StoreCertStore.

func WithCertClock

func WithCertClock(cl clock.Clock) CertStoreOption

WithCertClock sets the clock used for the TTL (tests).

func WithCertTTL

func WithCertTTL(d time.Duration) CertStoreOption

WithCertTTL sets how long a cached certificate is served before its Version is checked again (default DefaultCertTTL). A non-positive TTL revalidates on every call, which reproduces a per-push staleness check.

type Coalescer

type Coalescer struct {
	// contains filtered or unexported fields
}

Coalescer drops repeated pushes to the same enrollment inside a window: a device that was just woken will fetch every queued command anyway.

func Coalesce

func Coalesce(next Pusher, window time.Duration, c clock.Clock) *Coalescer

Coalesce wraps a Pusher.

func (*Coalescer) Push

func (c *Coalescer) Push(ctx context.Context, targets []Target) (map[mdm.EnrollmentID]Result, error)

Push implements Pusher.

type Notifier

type Notifier struct {
	Store  storage.PushStore
	Pusher Pusher
	Bus    *event.Bus
	Clock  clock.Clock
}

Notifier pushes enrollments by id: it looks push info up in storage, sends through the Pusher, and publishes PushTokenInvalid for tokens APNs rejected.

func (*Notifier) Notify

func (n *Notifier) Notify(ctx context.Context, ids []mdm.EnrollmentID) (map[mdm.EnrollmentID]Result, error)

Notify pushes the given enrollments. Enrollments without push info (disabled or unknown) are skipped and reported with Err set.

type Outcome

type Outcome string

Outcome classifies what happened to one push. It is a closed set, so a caller may switch on it exhaustively and use it as a metric label.

The distinction that matters is between OutcomeInvalidToken and OutcomeRejected. The first says this device will never receive a push again; the second says this request was wrong, which is usually a property of the topic, the certificate, or the environment rather than of the device, and so is usually true of every device at once. Collapsing them lets one misconfiguration read as a fleet that has gone silent.

const (
	// OutcomeSent: APNs accepted the notification.
	OutcomeSent Outcome = "sent"
	// OutcomeInvalidToken: this token will not work again. Apple states this
	// only for status 410 ("there is no need to send further pushes to the
	// same device token"), so only 410 produces it.
	OutcomeInvalidToken Outcome = "invalid-token"
	// OutcomeRejected: APNs refused the request and will refuse an identical
	// one, but said nothing about the device. A wrong topic, an expired or
	// mismatched push certificate, the sandbox environment, or a malformed
	// request all land here. It needs an operator, not a retry, and it is
	// not grounds for treating the enrollment as gone.
	OutcomeRejected Outcome = "rejected"
	// OutcomeRateLimited: APNs asked for a pause. RetryAfter carries what it
	// asked for, when it said.
	OutcomeRateLimited Outcome = "rate-limited"
	// OutcomeUnavailable: APNs or the network failed in a way that may
	// succeed on retry.
	OutcomeUnavailable Outcome = "unavailable"
	// OutcomeSkipped: nothing was sent to APNs, because the enrollment has
	// no usable push info or because a Coalescer dropped the push as a
	// duplicate. Err says which.
	OutcomeSkipped Outcome = "skipped"
)

Push outcomes.

type Pusher

type Pusher interface {
	// Push sends to every target and returns a Result per enrollment id.
	// The error is for failures that affect the whole batch (no
	// certificate, context cancelled); per-target failures are in Results.
	Push(ctx context.Context, targets []Target) (map[mdm.EnrollmentID]Result, error)
}

Pusher sends MDM pushes.

type Result

type Result struct {
	// Outcome classifies the result. The zero value is not a valid outcome;
	// a Pusher always sets it.
	Outcome Outcome
	// RetryAfter is what APNs asked to wait, and zero when it asked for
	// nothing. It is not a recommendation this package invents: a caller
	// that wants a floor applies its own, or apns.DefaultRetryAfter.
	RetryAfter time.Duration
	// Status and Reason are the APNs HTTP status and reason string. Reason
	// is one of the values in apns.Reasons when APNs sent one.
	Status int
	Reason string
	// APNSID is the apns-id header of an accepted push.
	APNSID string
	Err    error
}

Result is the outcome for one target.

func (Result) Sent

func (r Result) Sent() bool

Sent reports whether APNs accepted the notification.

func (Result) TokenInvalid

func (r Result) TokenInvalid() bool

TokenInvalid reports whether APNs said this token will never work again, which is the only outcome that justifies giving up on an enrollment.

type StaticCertStore

type StaticCertStore map[string]tls.Certificate

StaticCertStore serves fixed certificates by topic.

func (StaticCertStore) PushCertificate

func (s StaticCertStore) PushCertificate(_ context.Context, topic string) (tls.Certificate, error)

PushCertificate implements CertStore.

type StoreCertStore

type StoreCertStore struct {
	// contains filtered or unexported fields
}

StoreCertStore serves push certificates from a storage.PushCertStore with a per-topic cache revalidated against the stored Version once per TTL (decision record 0015). A renewal written through StorePushCert bumps the Version, so it is picked up within one TTL without a query per push, and a failed reload returns an error rather than silently keeping the old certificate.

func NewStoreCertStore

func NewStoreCertStore(s storage.PushCertStore, opts ...CertStoreOption) *StoreCertStore

NewStoreCertStore returns a CertStore backed by s.

func (*StoreCertStore) PushCertificate

func (c *StoreCertStore) PushCertificate(ctx context.Context, topic string) (tls.Certificate, error)

PushCertificate implements CertStore. A cached certificate is returned as is inside the TTL. After the TTL the stored Version is read: when it is unchanged the cache entry is kept for another TTL, otherwise the PEM pair is loaded and parsed again. A topic the store does not know maps to ErrNoCertificate. The mutex is held only around cache reads and writes, never across a storage call.

type Target

type Target struct {
	ID   mdm.EnrollmentID
	Push mdm.Push
}

Target is one push destination.

Directories

Path Synopsis
Package apns is the APNs HTTP/2 client for MDM pushes.
Package apns is the APNs HTTP/2 client for MDM pushes.
Package pushcert parses APNs push certificates and derives their topic.
Package pushcert parses APNs push certificates and derives their topic.
Package pushtest provides a scripted push.Pusher and an in-process APNs server so push behaviour is testable without Apple.
Package pushtest provides a scripted push.Pusher and an in-process APNs server so push behaviour is testable without Apple.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL