157 lines
4.0 KiB
Go
157 lines
4.0 KiB
Go
package nicopush
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/ecdh"
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
|
|
"git.min.rip/min/webpush-client-go/autopush"
|
|
"git.min.rip/min/webpush-client-go/rfc8291"
|
|
"git.min.rip/min/webpush-client-go/webpush"
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
type NicoPushClient struct {
|
|
autoPushClient *autopush.AutoPushClient
|
|
httpClient *http.Client
|
|
ece *rfc8291.RFC8291
|
|
|
|
vapidKey []byte
|
|
nicoPushEndpoint string
|
|
|
|
uaid string
|
|
channelIDs []string
|
|
authSecret []byte
|
|
privateKey *ecdh.PrivateKey
|
|
publicKey *ecdh.PublicKey
|
|
}
|
|
|
|
func NewNicoPushClient(
|
|
uaid string,
|
|
channelIDs []string,
|
|
authSecret []byte,
|
|
privateKey *ecdh.PrivateKey,
|
|
httpClient *http.Client,
|
|
) (*NicoPushClient, chan autopush.Notification, error) {
|
|
vapidKey, nicoPushEndpoint, err := getEndpointAndVapidKey()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
ap, ch := autopush.NewAutoPushClient()
|
|
|
|
client := &NicoPushClient{
|
|
autoPushClient: ap,
|
|
httpClient: httpClient,
|
|
ece: rfc8291.NewRFC8291(sha256.New),
|
|
|
|
vapidKey: vapidKey,
|
|
nicoPushEndpoint: nicoPushEndpoint,
|
|
|
|
uaid: uaid,
|
|
channelIDs: channelIDs,
|
|
authSecret: authSecret,
|
|
privateKey: privateKey,
|
|
publicKey: privateKey.PublicKey(),
|
|
}
|
|
|
|
return client, ch, nil
|
|
}
|
|
|
|
// Handshake performs a handshake with the AutoPush server and retrieves a UAID.
|
|
// The obtained UAID is used for client identification and should be saved.
|
|
func (c *NicoPushClient) Handshake() (uaid string, err error) {
|
|
if err := c.autoPushClient.Connect(autopush.MOZILLA_PUSH_SERVICE, 3, 2); err != nil {
|
|
return "", fmt.Errorf("failed to connect autopush server: %v", err)
|
|
}
|
|
|
|
data, err := c.autoPushClient.Hello(c.uaid, c.channelIDs)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to handshake autopush server: %v", err)
|
|
}
|
|
|
|
c.uaid = data.UAID
|
|
return c.uaid, nil
|
|
}
|
|
|
|
func (c *NicoPushClient) Register() (channelID string, err error) {
|
|
uuid, err := uuid.NewRandom()
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to generate UUIDv4: %v", err)
|
|
}
|
|
|
|
data, err := c.autoPushClient.Register(uuid.String(), base64.StdEncoding.EncodeToString(c.vapidKey))
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to register autopush: %v", err)
|
|
}
|
|
|
|
if err := c.registerToAppServer(data.PushEndpoint); err != nil {
|
|
return "", fmt.Errorf("failed to register nicopush: %v", err)
|
|
}
|
|
|
|
return uuid.String(), nil
|
|
}
|
|
|
|
func (c *NicoPushClient) Decrypt(data autopush.Notification) (*webpush.WebPushPayload, error) {
|
|
return c.autoPushClient.Decrypt(
|
|
c.authSecret,
|
|
c.privateKey,
|
|
data,
|
|
)
|
|
}
|
|
|
|
// Registration AutoPush's push-endpoint to NicoPush (Application Server)
|
|
func (c *NicoPushClient) registerToAppServer(pushEndpoint string) error {
|
|
payload, err := json.Marshal(Register{
|
|
DestApp: NICO_ACCOUNT_WEBPUSH,
|
|
Endpoint: Endpoint{
|
|
Endpoint: pushEndpoint,
|
|
Auth: base64.StdEncoding.EncodeToString(c.authSecret),
|
|
P256DH: base64.StdEncoding.EncodeToString(c.publicKey.Bytes()),
|
|
},
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to marshal register payload: %v", err)
|
|
}
|
|
|
|
req, err := http.NewRequest(http.MethodPost, c.nicoPushEndpoint, bytes.NewBuffer(payload))
|
|
if err != nil {
|
|
return fmt.Errorf("failed to bulid register request: %v", err)
|
|
}
|
|
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("X-Request-With", "https://account.nicovideo.jp/my/account")
|
|
req.Header.Set("X-Frontend-Id", "8")
|
|
|
|
res, err := c.httpClient.Do(req)
|
|
if err != nil {
|
|
return fmt.Errorf("nicopush register request failed: %v", err)
|
|
}
|
|
defer res.Body.Close()
|
|
|
|
if res.StatusCode != http.StatusOK {
|
|
return fmt.Errorf("invalid http status: %d %s", res.StatusCode, res.Status)
|
|
}
|
|
|
|
body, err := io.ReadAll(res.Body)
|
|
if err != nil {
|
|
return fmt.Errorf("read error: %v", err)
|
|
}
|
|
|
|
var data APIResponse
|
|
if err := json.Unmarshal(body, &data); err != nil {
|
|
return fmt.Errorf("failed to unmarshal api response: %v", err)
|
|
}
|
|
|
|
if data.Meta.Status != http.StatusOK {
|
|
return fmt.Errorf("invalid response status: %d", data.Meta.Status)
|
|
}
|
|
|
|
return nil
|
|
}
|