Documentation
¶
Overview ¶
Package jpush is a minimal, receive-only client for the JiGuang / JPush (Aurora Mobile) device push service.
Unlike the official JPush SDKs — which are all server-side REST wrappers around api.jpush.cn — this package speaks the *device-side* JCore TCP protocol: it resolves a push server via the UDP "SIS" lookup, registers to obtain a registration id (RID) and numeric user id (juid), logs in, sets an alias, and then holds the connection open (with heartbeats) to receive pushes addressed to that alias. It never sends pushes.
Protocol provenance and scope ¶
Two wire codecs are provided, both selectable via Config.Codec:
- JHeadCodec (the default): JPush's classic "JHead + TLV" plaintext binary protocol, reconstructed from the decompiled JPush Android SDK v2.1.0 (cn.jpush.proto.common.commands.*).
- JCore473Codec: the modern JCore protocol (Android JCore 4.7.3 / iOS 2.4.0), whose bodies are AES-256-CBC encrypted under a key derived from the register seed / juid. Current JPush servers require this. Construct it with NewJCore473Codec (Android) or NewJCore473CodecIOS (iOS).
The framing, connection loop, and command orchestration are codec-agnostic, so a future transport (e.g. Protocol Buffers bodies inside libjcore.so) only needs a new Codec — not a rewrite of the client.
Typical use ¶
c := jpush.New(jpush.Config{AppKey: "…", SDKVersion: "2.1.0"})
if _, err := c.Register(ctx); err != nil { … } // SIS + register + login
if err := c.SetAlias(ctx, "myalias"); err != nil { … }
push, err := c.WaitForPush(ctx, nil) // blocks until one push arrives
The package is dependency-free (standard library only) so it builds anywhere Go does, including gomobile and wasm targets.
Example ¶
Example shows the typical receive-only flow: register (SIS discovery + login), bind an alias, then block for a push. It talks to a live JPush server, so it is shown for documentation and not executed by `go test`.
package main
import (
"context"
"fmt"
jpush "github.com/thibauddavid/jpush-go"
)
func main() {
c := jpush.New(jpush.Config{
AppKey: "0123456789abcdef01234567", // your 24-hex JPush AppKey
PackageName: "com.example.app", // the app id bound to the AppKey
Codec: jpush.NewJCore473Codec(), // modern encrypted protocol
})
ctx := context.Background()
if _, err := c.Register(ctx); err != nil { // SIS lookup + register + login
panic(err)
}
defer c.Close()
if err := c.SetAlias(ctx, "my-alias"); err != nil {
panic(err)
}
push, err := c.WaitForPush(ctx, nil) // blocks until a push arrives
if err != nil {
panic(err)
}
fmt.Println(push.Content) // raw msgContent JSON
}
Output:
Index ¶
- Constants
- type Ack
- type Client
- func (c *Client) Close() error
- func (c *Client) RegID() string
- func (c *Client) Register(ctx context.Context) (*Credentials, error)
- func (c *Client) Run(ctx context.Context, onPush func(Push) error) error
- func (c *Client) SetAlias(ctx context.Context, alias string) error
- func (c *Client) WaitForPush(ctx context.Context, match func(Push) bool) (Push, error)
- type Codec
- type Config
- type Credentials
- type FileStore
- type JCore473Codec
- func (c *JCore473Codec) Decode(f []byte) (Response, error)
- func (c *JCore473Codec) EncodeHeartbeat(rid, juid int64, sid int) []byte
- func (c *JCore473Codec) EncodeLogin(rid, juid int64, p LoginParams) []byte
- func (c *JCore473Codec) EncodePushAck(rid, juid int64, sid, code, msgType int, msgID int64) []byte
- func (c *JCore473Codec) EncodeRegister(rid int64, p RegisterParams) []byte
- func (c *JCore473Codec) EncodeSetAlias(rid, juid int64, sid int, appKey, action string) []byte
- type JHeadCodec
- func (JHeadCodec) Decode(f []byte) (Response, error)
- func (JHeadCodec) EncodeHeartbeat(rid, juid int64, sid int) []byte
- func (JHeadCodec) EncodeLogin(rid, juid int64, p LoginParams) []byte
- func (JHeadCodec) EncodePushAck(rid, juid int64, sid, code, msgType int, msgID int64) []byte
- func (JHeadCodec) EncodeRegister(rid int64, p RegisterParams) []byte
- func (JHeadCodec) EncodeSetAlias(rid, juid int64, sid int, appKey, action string) []byte
- type LoginParams
- type LoginResult
- type Platform
- type Push
- type RegisterParams
- type RegisterResult
- type Response
- type Store
Examples ¶
Constants ¶
const ( DefaultSDKVersion = "2.1.0" DefaultAppVersion = "1.0.0" DefaultHeartbeatInterval = 15 * time.Second )
Default configuration values.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Ack ¶
Ack is a server acknowledgement (cmd 19) of a client request such as a heartbeat (RequestCommand 2) or a set-alias (RequestCommand 10).
type Client ¶
type Client struct {
// contains filtered or unexported fields
}
Client is a JPush receive-only push client. It is safe for a single caller to drive sequentially: Register, then SetAlias / WaitForPush / Run. Concurrent writes to the socket are serialized internally.
func (*Client) Register ¶
func (c *Client) Register(ctx context.Context) (*Credentials, error)
Register establishes the push connection and authenticates. If a Store holds valid credentials it reuses them (skipping the register round-trip); otherwise it runs SIS discovery, registers to obtain a fresh RID/juid, and persists the result. On success the background heartbeat and receive loops are running.
func (*Client) Run ¶
Run delivers each received push to onPush until it returns an error, the context is cancelled, or the connection drops. It requires a prior successful Register. Pushes already received when the connection drops are still delivered first.
func (*Client) SetAlias ¶
SetAlias binds this registration to alias, so pushes addressed to the alias reach this client. It waits for the server's acknowledgement.
func (*Client) WaitForPush ¶
WaitForPush blocks until a push satisfying match arrives (match nil accepts the first push), the context ends, or the connection drops. A push already buffered when the connection drops is still returned.
type Codec ¶
type Codec interface {
EncodeRegister(rid int64, p RegisterParams) []byte
EncodeLogin(rid, juid int64, p LoginParams) []byte
EncodeSetAlias(rid, juid int64, sid int, appKey, action string) []byte
EncodeHeartbeat(rid, juid int64, sid int) []byte
EncodePushAck(rid, juid int64, sid, code, msgType int, msgID int64) []byte
// Decode parses a whole length-prefixed response frame (prefix included).
Decode(frame []byte) (Response, error)
}
Codec encodes JCore request bodies and decodes response frames. The default implementation, JHeadCodec, speaks the JPush SDK-2.1.0 "JHead + TLV" wire format. Supply a different Codec via Config to target a newer JCore build (e.g. one with Protocol Buffers bodies) without changing the client.
type Config ¶
type Config struct {
AppKey string // the 24-hex JPush application key (required)
SDKVersion string // reported SDK version; drives the SIS blob (default "2.1.0")
AppVersion string // host app version reported at register (default "1.0.0")
Platform Platform // only PlatformAndroid is supported
// PackageName is the app's package/bundle id, bound to AppKey by the JPush
// portal — it MUST match what the app was registered with or the server rejects
// registration (codes 1005/1015). Required for registration to succeed.
PackageName string
// Channel is the JPush channel string reported in clientInfo (default
// "developer-default").
Channel string
// DeviceModel and ClientInfo populate the register clientInfo field. If
// ClientInfo is set it is used verbatim; otherwise a "$$"-delimited blob is
// synthesized (osVer,api$$model$$baseband$$device$$channel$$sdkVer$$sysInstall$$WxH)
// using DeviceModel (default "Pixel 5").
DeviceModel string
ClientInfo string
// ClientVersion is the integer SDK version sent in the login body. If 0 it is
// derived from SDKVersion. The exact derivation is unverified — override if a
// capture shows a different value.
ClientVersion int
// RegBusiness is the register "business" module bitmask (bx/b.java b()). It is
// server-stored metadata, not validated at register; defaults to 0.
RegBusiness int
Store Store // optional credential persistence; nil => in-memory
SISHosts []string // optional SIS host override
Codec Codec // wire codec; nil => JHeadCodec{}
// Servers, if non-empty, are "ip:port" push connections tried in order,
// bypassing SIS discovery entirely (useful to pin a server or reuse cached
// ones). SIS is only consulted when this is empty.
Servers []string
// Dial opens the push TCP connection. nil => a plain net.Dialer. Supply this to
// wrap with TLS (for ssl_ips servers) or to inject a fake conn in tests.
Dial func(ctx context.Context, network, addr string) (net.Conn, error)
// HeartbeatInterval is how often to ping after login (default 15s).
HeartbeatInterval time.Duration
// Logger, if set, receives frame-level diagnostics. Invaluable while validating
// the wire format against a live server.
Logger func(format string, args ...any)
}
Config configures a Client. AppKey is required; everything else has a default.
type Credentials ¶
type Credentials struct {
JUID int64 `json:"juid"`
Password string `json:"password"` // register credential; login sends MD5 of it
RegID string `json:"reg_id"` // the registration id
DeviceID string `json:"device_id"`
// UDID and AndroidID are the client-generated device identifiers sent in the
// register key/keyExt fields (JPush derives these from the real device; a
// headless client synthesizes and persists them so they stay stable).
UDID string `json:"udid,omitempty"`
AndroidID string `json:"android_id,omitempty"`
// DeviceToken is the iOS APNs device token sent (as "<token>$$") in the
// register body. A real device gets this from Apple; a headless client can't,
// so it synthesizes a stable 64-hex value. The register appears to require a
// non-empty token to provision a login-able session — an empty "$$" token
// registers (returns a juid) but the follow-up login is rejected with 1015.
DeviceToken string `json:"device_token,omitempty"`
}
Credentials are the durable identity a client obtains at registration, plus the synthesized device ids it registers with. Persisting them (via a Store) lets a client skip re-registration on subsequent runs and keep a stable RegID/device id.
type FileStore ¶
type FileStore struct{ Path string }
FileStore persists Credentials as JSON at Path — a ready-made Store for desktop use. Load returns (nil, nil) when the file does not exist yet.
func (FileStore) Load ¶
func (f FileStore) Load() (*Credentials, error)
func (FileStore) Save ¶
func (f FileStore) Save(c *Credentials) error
type JCore473Codec ¶
type JCore473Codec struct {
// contains filtered or unexported fields
}
JCore473Codec implements Codec for the modern JCore 4.7.3 encrypted protocol. It is stateful: it remembers the register seed and, once the juid is known, keys all traffic from it. Use NewJCore473Codec; safe for the client's sequential use.
func NewJCore473Codec ¶
func NewJCore473Codec() *JCore473Codec
NewJCore473Codec returns a modern Android (JCore 4.7.3) codec.
func NewJCore473CodecIOS ¶
func NewJCore473CodecIOS() *JCore473Codec
NewJCore473CodecIOS returns a modern iOS (JCore 2.4.0) codec: register head version 0x19 and the iOS register body (deviceToken/advertisingId/buildType/apsType).
func (*JCore473Codec) EncodeHeartbeat ¶
func (c *JCore473Codec) EncodeHeartbeat(rid, juid int64, sid int) []byte
func (*JCore473Codec) EncodeLogin ¶
func (c *JCore473Codec) EncodeLogin(rid, juid int64, p LoginParams) []byte
func (*JCore473Codec) EncodePushAck ¶
func (c *JCore473Codec) EncodePushAck(rid, juid int64, sid, code, msgType int, msgID int64) []byte
func (*JCore473Codec) EncodeRegister ¶
func (c *JCore473Codec) EncodeRegister(rid int64, p RegisterParams) []byte
func (*JCore473Codec) EncodeSetAlias ¶
func (c *JCore473Codec) EncodeSetAlias(rid, juid int64, sid int, appKey, action string) []byte
EncodeSetAlias builds the modern JCore set-alias frame (cmd 29). action is the full JSON control blob (e.g. {"platform":"i","op":"set","alias":"<md5>"}) — the caller builds it so the platform is correct. Byte-verified against a captured app alias: no appKey field (the session's juid is already bound to it), the SID rides in the head [12..16] field, and the head version byte is 2. Server replies {"code":0}.
type JHeadCodec ¶
type JHeadCodec struct{}
JHeadCodec implements Codec with the classic JHead+TLV binary format.
func (JHeadCodec) EncodeHeartbeat ¶
func (JHeadCodec) EncodeHeartbeat(rid, juid int64, sid int) []byte
func (JHeadCodec) EncodeLogin ¶
func (JHeadCodec) EncodeLogin(rid, juid int64, p LoginParams) []byte
func (JHeadCodec) EncodePushAck ¶
func (JHeadCodec) EncodePushAck(rid, juid int64, sid, code, msgType int, msgID int64) []byte
EncodePushAck builds a push-received acknowledgement (cmd 4). The 2.1.0 SDK emits these bytes from native code; the body layout (code/msgType/msgId) is a best effort and acking is not required to receive the first push — see the package docs. Failure to ack simply risks server-side redelivery.
func (JHeadCodec) EncodeRegister ¶
func (JHeadCodec) EncodeRegister(rid int64, p RegisterParams) []byte
EncodeRegister uses only the classic 2.1.0 fields (iOS extras ignored).
func (JHeadCodec) EncodeSetAlias ¶
func (JHeadCodec) EncodeSetAlias(rid, juid int64, sid int, appKey, action string) []byte
type LoginParams ¶
type LoginParams struct {
PasswordMD5 string
AppKey string
ClientVersion int // 2.1.0 / Android login body
VersionString string // iOS: "<jcoreVerInt>|<jpushVerInt>||||", e.g. "132096|197632||||"
DeviceHash string // iOS: md5(bundleId+appKey+uuid) (same value as the register extKey hash)
}
LoginParams holds the fields of a login request.
type LoginResult ¶
type LoginResult struct {
Code int
Error string
SID int
ServerVersion int
SessionKey string
ServerTime int
}
LoginResult is the body of a login response (cmd 1). SID must be echoed in the headers of subsequent requests.
func (*LoginResult) Command ¶
func (*LoginResult) Command() int
type Platform ¶
type Platform int
Platform identifies the registering device kind. Both register without a working APNs token: Android needs none, and iOS JCore registers before APNs delivers one (sending an empty device token), so a headless client can complete either.
type RegisterParams ¶
type RegisterParams struct {
Key string
APKVersion string
ClientInfo string
KeyExt string
RegBusiness int
AccountID string
// iOS-only (JCORERegister.bodyData inserts these between clientInfo and byte0):
DeviceToken string // "$$" when APNs+VoIP tokens are both empty
AdvertisingID string // " " when IDFA is unavailable
BuildType int // 1=dev, 2=prod
ApsType int // 1=dev, 2=prod, 0xff if no mobileprovision
}
RegisterParams holds the fields of a register request. The iOS-only fields (DeviceToken, AdvertisingID, BuildType, ApsType) are ignored for Android.
type RegisterResult ¶
type RegisterResult struct {
Code int
Error string
JUID int64
Password string // the login credential; login sends MD5(Password)
RegID string // the registration id (RID)
DeviceID string
}
RegisterResult is the body of a register response (cmd 0). On success Code is 0 and RegID/JUID/Password are populated.
func (*RegisterResult) Command ¶
func (*RegisterResult) Command() int
type Response ¶
type Response interface {
// Command returns the JCore opcode (cmdRegister, cmdLogin, …).
Command() int
}
Response is a decoded server-to-client packet. Concrete types are *RegisterResult, *LoginResult, *Push, and *Ack.
type Store ¶
type Store interface {
Load() (*Credentials, error)
Save(*Credentials) error
}
Store persists Credentials across process runs. Implementations are caller-supplied (a JSON file on desktop, Keychain/Keystore on mobile, …). A nil Store means credentials live only in memory for the client's lifetime.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package capture provides offline analysis of captured JCore frames: it decrypts a frame body and decodes register/login bodies and register responses.
|
Package capture provides offline analysis of captured JCore frames: it decrypts a frame body and decodes register/login bodies and register responses. |