webpush-client-go/sites/nicopush/client.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, false); 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
}