jpush

package module
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Jul 22, 2026 License: MIT Imports: 19 Imported by: 0

README

jpush-go

CI Go Reference

A minimal, receive-only Go client for the JiGuang / JPush (Aurora Mobile) device push service. Zero dependencies — standard library only.

Every published JPush SDK for Go is a server-side REST wrapper around api.jpush.cn (for sending pushes). This package is the opposite: it speaks the device-side JCore TCP protocol, so a headless program can act like an app instance and receive pushes — resolve a push server (SIS), register for a registration id, log in, set an alias, and hold the socket open to read incoming messages. It never sends pushes.

go get github.com/thibauddavid/jpush-go

Usage

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 (see below)
    Store:       jpush.FileStore{Path: "creds.json"}, // optional: persist/reuse the RID
})

if _, err := c.Register(ctx); err != nil {       // SIS lookup + register + login
    log.Fatal(err)
}
if err := c.SetAlias(ctx, "my-alias"); err != nil {
    log.Fatal(err)
}

push, err := c.WaitForPush(ctx, nil)             // blocks until a push arrives
fmt.Println(push.Content)                         // raw msgContent JSON

// or stream them:
// c.Run(ctx, func(p jpush.Push) error { … })

Set Config.Logger to print every frame (-> … / <- …) — invaluable when validating the wire format against a real packet capture.

Codecs

The connection loop and command orchestration are codec-agnostic; the wire format is pluggable via Config.Codec:

Codec Constructor Protocol
JHeadCodec (default) zero value Classic "JHead + TLV" plaintext binary protocol (JPush Android SDK v2.1.0).
JCore473Codec NewJCore473Codec() (Android) / NewJCore473CodecIOS() (iOS) Modern JCore 4.7.3 / iOS 2.4.0, with AES-256-CBC-encrypted bodies. Current JPush servers require this.
Modern protocol (JCore473Codec)
  • 24-byte request / 20-byte response header, big-endian: len | version | command | rid | seed | juid. The high bit of the length flags an encrypted body.
  • Body is AES-256-CBC / PKCS7, key = the 32 ASCII bytes of MD5("JCKP" + transform(seed)) (register keys off the header seed; later commands key off the juid). transform is the seed % 10 mixing from cn.jiguang.cn.g.
  • iOS register uses header version 0x19 and an APNs-token-carrying body; login and later commands use 0x18. Set-alias is a JSON control frame (cmd 29, {"platform":…,"op":"set","alias":…}).
Classic protocol (JHeadCodec)

Plaintext JHead + TLV strings (uint16 length + UTF-8), with the register → login → set-alias → heartbeat → push/ack command set, and UDP SIS discovery on port 19000. Legacy servers accept it; modern ones don't.

Status & caveats

register → login → set-alias → receive-push works against a live modern JPush server (iOS JCore path, verified end-to-end). The protocol was reconstructed from decompiled SDKs and real packet captures; a few fields are best-effort and overridable via Config (register clientInfo, the login ClientVersion integer, the push-ack body). If a capture shows a newer transport (e.g. Protocol Buffers bodies), implement the Codec interface and pass it via Config.Codec — no client changes needed.

Analyzing captures

The capture subpackage decodes raw JCore frames from a packet capture — decrypting a frame body and decoding register/login bodies and register responses. It's a reverse-engineering aid, kept out of the core package so the client API stays focused:

import "github.com/thibauddavid/jpush-go/capture"

info, _ := capture.InspectFrame(frameBytes)          // try all key derivations/offsets
fmt.Println(info.Command, info.Encrypted)
// capture.TryDecryptSeed / DecodeRegisterResponse / DecodeIOSRegisterBody

License

MIT — see LICENSE.

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
}

Index

Examples

Constants

View Source
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

type Ack struct {
	RequestCommand int
	Step           int
	Status         int
	STime          int64
}

Ack is a server acknowledgement (cmd 19) of a client request such as a heartbeat (RequestCommand 2) or a set-alias (RequestCommand 10).

func (*Ack) Command

func (*Ack) Command() int

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 New

func New(cfg Config) *Client

New builds a Client from cfg, applying defaults.

func (*Client) Close

func (c *Client) Close() error

Close shuts down the client and its connection.

func (*Client) RegID

func (c *Client) RegID() string

RegID returns the current registration id, or "" if not yet registered.

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

func (c *Client) Run(ctx context.Context, onPush func(Push) error) error

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

func (c *Client) SetAlias(ctx context.Context, alias string) error

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

func (c *Client) WaitForPush(ctx context.Context, match func(Push) bool) (Push, error)

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) Decode

func (c *JCore473Codec) Decode(f []byte) (Response, error)

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) Decode

func (JHeadCodec) Decode(f []byte) (Response, error)

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.

const (
	PlatformAndroid Platform = iota
	PlatformIOS
)

type Push

type Push struct {
	MsgType int
	MsgID   int64
	Content string
}

Push is an incoming push message (cmd 3). Content is the raw msgContent JSON.

func (*Push) Command

func (*Push) Command() int

func (Push) Fields

func (p Push) Fields() (map[string]any, error)

Fields decodes the push's msgContent JSON into a generic map. The schema is application-defined; common JPush keys include "title", "message", "extras", and "content_type".

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.

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.

Jump to

Keyboard shortcuts

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