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 ¶
- Decision record 0007: docs/research/decisions/0007-apns-push.md
- Decision record 0015: docs/research/decisions/0015-push-cert-store.md
- Decision record 0042: docs/research/decisions/0042-push-failure-classification.md
- Plan of record: docs/research/implementation_plan.md (phase 3)
- Threat model: docs/security/threat-model.md (Push rows)
- End-to-end scenarios: docs/testing/e2e-scenarios.md (E2E-006, E2E-007)
- Apple: https://developer.apple.com/documentation/devicemanagement/setting-up-push-notifications-for-your-device-management-customers
- Apple: https://developer.apple.com/documentation/devicemanagement/dealing-with-inactive-managed-devices-and-invalid-push-tokens
- Schema: third_party/device-management/mdm/checkin/tokenupdate.yaml (Topic, PushMagic, Token)
Index ¶
Constants ¶
const DefaultCertTTL = 30 * time.Second
DefaultCertTTL is how long a StoreCertStore trusts a cached certificate before asking the store whether its Version moved.
Variables ¶
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.
var ErrCoalesced = errors.New("push: coalesced with a recent push")
ErrCoalesced marks a push that was skipped because one was sent recently.
var Outcomes = []Outcome{ OutcomeSent, OutcomeInvalidToken, OutcomeRejected, OutcomeRateLimited, OutcomeUnavailable, OutcomeSkipped, }
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.
type Notifier ¶
Notifier pushes enrollments by id: it looks push info up in storage, sends through the Pusher, and publishes PushTokenInvalid for tokens APNs rejected.
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" // 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) TokenInvalid ¶
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.
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. |