-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathclient.go
More file actions
184 lines (166 loc) · 4.62 KB
/
Copy pathclient.go
File metadata and controls
184 lines (166 loc) · 4.62 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
package main
import (
"bytes"
"crypto/rand"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"time"
)
const (
defaultBaseURL = "https://p68-maildomainws.icloud.com"
clientBuildNumber = "2608Build39"
clientMasteringNumber = "2608Build39"
userAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko)"
)
// Client talks to the iCloud Hide My Email API.
type Client struct {
BaseURL string
Cookies string
DSID string
ClientID string
HTTPClient *http.Client
}
// NewClient creates a Client from a cookie string.
func NewClient(cookies string) (*Client, error) {
dsid, err := ExtractDSID(cookies)
if err != nil {
return nil, err
}
return &Client{
BaseURL: defaultBaseURL,
Cookies: cookies,
DSID: dsid,
ClientID: generateClientID(),
HTTPClient: &http.Client{
Timeout: 30 * time.Second,
},
}, nil
}
// generateClientID produces a random UUID v4 client identifier per session.
func generateClientID() string {
var b [16]byte
rand.Read(b[:])
b[6] = (b[6] & 0x0f) | 0x40 // version 4
b[8] = (b[8] & 0x3f) | 0x80 // variant 2
return fmt.Sprintf("%08X-%04X-%04X-%04X-%012X",
b[0:4], b[4:6], b[6:8], b[8:10], b[10:16])
}
// queryParams returns the standard query parameters for all API calls.
func (c *Client) queryParams() url.Values {
return url.Values{
"clientBuildNumber": {clientBuildNumber},
"clientMasteringNumber": {clientMasteringNumber},
"clientId": {c.ClientID},
"dsid": {c.DSID},
}
}
// setHeaders applies the required headers to a request.
func (c *Client) setHeaders(req *http.Request) {
req.Header.Set("Cookie", c.Cookies)
req.Header.Set("Origin", "https://www.icloud.com")
req.Header.Set("Referer", "https://www.icloud.com/")
req.Header.Set("User-Agent", userAgent)
req.Header.Set("Accept", "application/json")
if req.Method == http.MethodPost {
req.Header.Set("Content-Type", "application/json")
}
}
// doRequest executes a request and decodes the JSON response.
func (c *Client) doRequest(req *http.Request, out interface{}) error {
c.setHeaders(req)
resp, err := c.HTTPClient.Do(req)
if err != nil {
return fmt.Errorf("could not reach iCloud: %w", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(io.LimitReader(resp.Body, 10*1024*1024))
if err != nil {
return fmt.Errorf("reading response: %w", err)
}
switch resp.StatusCode {
case http.StatusOK:
// continue to decode
case http.StatusUnauthorized, 421:
return fmt.Errorf("cookies expired. Run 'hme auth' to refresh")
case http.StatusTooManyRequests:
return fmt.Errorf("rate limited. Wait a few minutes")
default:
return fmt.Errorf("unexpected status %d from iCloud", resp.StatusCode)
}
if err := json.Unmarshal(body, out); err != nil {
return fmt.Errorf("decoding response: %w", err)
}
return nil
}
// Generate requests a new random HME email address.
func (c *Client) Generate() (string, error) {
u := c.BaseURL + "/v1/hme/generate?" + c.queryParams().Encode()
req, err := http.NewRequest(http.MethodPost, u, bytes.NewReader([]byte("{}")))
if err != nil {
return "", err
}
var resp GenerateResponse
if err := c.doRequest(req, &resp); err != nil {
return "", err
}
if !resp.Success {
return "", apiErrorToError(resp.Error)
}
if resp.Result.Hme == "" {
return "", fmt.Errorf("API returned success but no email address")
}
return resp.Result.Hme, nil
}
// Reserve confirms (activates) a generated HME email with a label and optional note.
func (c *Client) Reserve(hme, label, note string) error {
body, _ := json.Marshal(ReserveRequest{
Hme: hme,
Label: label,
Note: note,
})
u := c.BaseURL + "/v1/hme/reserve?" + c.queryParams().Encode()
req, err := http.NewRequest(http.MethodPost, u, bytes.NewReader(body))
if err != nil {
return err
}
var resp ReserveResponse
if err := c.doRequest(req, &resp); err != nil {
return err
}
if !resp.Success {
return apiErrorToError(resp.Error)
}
return nil
}
// List fetches all HME email aliases.
func (c *Client) List() ([]HmeEmail, error) {
u := c.BaseURL + "/v2/hme/list?" + c.queryParams().Encode()
req, err := http.NewRequest(http.MethodGet, u, nil)
if err != nil {
return nil, err
}
var resp ListResponse
if err := c.doRequest(req, &resp); err != nil {
return nil, err
}
if !resp.Success {
return nil, apiErrorToError(resp.Error)
}
return resp.Result.HmeEmails, nil
}
func apiErrorToError(e *APIError) error {
if e == nil {
return fmt.Errorf("unknown API error")
}
msg := e.ErrorMessage
if msg == "" {
msg = e.Reason
}
if msg == "" {
msg = fmt.Sprintf("error code %d", e.ErrorCode)
}
return fmt.Errorf("%s", msg)
}