-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathbot_cookie.go
339 lines (294 loc) · 9.57 KB
/
bot_cookie.go
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
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
package wee
import (
"encoding/json"
"errors"
"fmt"
"io/fs"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
"github.com/go-rod/rod/lib/proto"
"github.com/gookit/goutil/fsutil"
"github.com/jrefior/uncurl"
)
const (
_defaultCookieFolder = ".cookies"
// File permissions
_cookieFilePermissions fs.FileMode = 0644
// HTTP schemes
_schemeHTTP = "http"
_schemeHTTPS = "https"
// HTTP ports
_portHTTP = 80
_portHTTPS = 443
)
var ErrEmptyCookieStr = errors.New("no cookie str found")
// DumpCookies saves the current cookies of the bot's page to a file on disk.
//
// This function performs the following operations:
//
// 1. Ensures the cookie file exists:
// - If b.cookieFile is empty, it generates a filename based on the current URL.
// - Creates any necessary parent directories for the cookie file.
//
// 2. Retrieves and marshals cookies:
// - Calls b.page.MustCookies() to get all cookies from the current page.
// - Marshals these cookies into JSON format.
//
// 3. Writes cookies to file:
// - Opens or creates the cookie file with 0644 permissions (rw-r--r--).
// - Writes the JSON-encoded cookie data to the file.
//
// Returns:
// - string: The full path to the cookie file where the cookies were saved.
// - error: An error if any of the following occur:
// - Failure to ensure the cookie file (e.g., permission issues)
// - Failure to marshal cookies to JSON
// - Failure to write to the cookie file
//
// Usage:
//
// filepath, err := bot.DumpCookies()
// if err != nil {
// log.Fatalf("Failed to dump cookies: %v", err)
// }
// fmt.Printf("Cookies saved to: %s\n", filepath)
//
// Notes:
// - This function is useful for persisting session data between bot runs.
// - The saved cookies can be later loaded using the LoadCookies method.
// - If b.cookieFile is not set, the function will automatically generate a filename
// based on the current URL, stored in the default or specified cookie folder.
// - Ensure the bot has necessary permissions to write to the cookie directory.
func (b *Bot) DumpCookies() (string, error) {
err := b.ensureCookieFile()
if err != nil {
return "", err
}
content, err := json.Marshal(b.page.MustCookies())
if err != nil {
return "", fmt.Errorf("cannot marshal cookies: %w", err)
}
err = os.WriteFile(b.cookieFile, content, _cookieFilePermissions)
if err != nil {
return "", fmt.Errorf("cannot save file: %w", err)
}
return b.cookieFile, nil
}
// ensureCookieFile create cookie file if not existed.
func (b *Bot) ensureCookieFile() error {
if b.cookieFile == "" {
file, err := b.createCookieFilenameFromURL(b.CurrentURL())
if err != nil {
return err
}
b.cookieFile = file
}
if err := fsutil.MkParentDir(b.cookieFile); err != nil {
return err
}
return nil
}
func (b *Bot) getRawCookies(filepath string) ([]byte, error) {
// 1. check if copy as cURL
if len(b.copyAsCURLCookies) != 0 {
return b.copyAsCURLCookies, nil
}
// 2. filepath or auto get by URL
if filepath == "" {
v, err := b.createCookieFilenameFromURL(b.CurrentURL())
if err != nil {
return nil, err
}
filepath = v
}
if !fsutil.FileExist(filepath) {
return nil, ErrMissingCookieFile
}
return os.ReadFile(filepath)
}
// LoadCookies loads cookies from a file or from previously stored cURL data and converts them
// into a format usable by the bot.
//
// Parameters:
// - filepath: string - The path to the cookie file. If empty, the function will attempt to
// generate a filename based on the bot's current URL.
//
// Returns:
// - []*proto.NetworkCookieParam: A slice of NetworkCookieParam objects representing the loaded cookies.
// - error: An error if the loading process fails at any step.
//
// Function behavior:
// 1. Retrieves raw cookie data:
// - If copyAsCURLCookies is not empty, it uses this data.
// - Otherwise, it reads from the specified file or an auto-generated file based on the current URL.
//
// 2. Determines the format of the cookie data:
// - If the data is in JSON format, it parses it as proto.NetworkCookie objects.
// - If not JSON, it assumes cURL format and parses accordingly.
//
// 3. For JSON format:
// - Unmarshals the data into proto.NetworkCookie objects.
// - Converts these into proto.NetworkCookieParam objects, filling in additional fields.
//
// 4. For cURL format:
// - Calls ParseCURLCookies to handle the conversion.
//
// Usage:
//
// cookies, err := bot.LoadCookies("path/to/cookies.json")
// if err != nil {
// log.Fatalf("Failed to load cookies: %v", err)
// }
// // Use the cookies with the bot...
//
// Notes:
// - This function is flexible and can handle both JSON-formatted cookie files (typically saved
// by DumpCookies) and cURL-formatted cookie strings.
// - When loading from a file, ensure the bot has read permissions for the cookie file.
// - If filepath is empty and the bot can't determine the current URL, this will result in an error.
// - The returned cookies are in a format ready to be used with the bot's page or browser instance.
// - Any parsing or file reading errors will be returned, allowing the caller to handle them appropriately.
//
// See also:
// - DumpCookies: For saving cookies to a file.
// - ParseCURLCookies: For the specific handling of cURL-formatted cookie strings.
func (b *Bot) LoadCookies(filepath string) ([]*proto.NetworkCookieParam, error) {
raw, err := b.getRawCookies(filepath)
if err != nil {
return nil, fmt.Errorf("cannot get raw cookies: %w", err)
}
if !IsJSON(string(raw)) {
// non json format, means from cURL
// parse as cURL
return ParseCURLCookies(string(raw))
}
// if not copyAsCURL cookies, try parse with json format.
var (
cookies []proto.NetworkCookie
nodes []*proto.NetworkCookieParam
)
if err := json.Unmarshal(raw, &cookies); err != nil {
return nil, fmt.Errorf("cannot unmarshal to proto.cookie: %w", err)
}
for _, cookie := range cookies {
port := &cookie.SourcePort
nodes = append(nodes, &proto.NetworkCookieParam{
Name: cookie.Name,
Value: cookie.Value,
URL: b.CurrentURL(),
Domain: cookie.Domain,
Path: cookie.Path,
Secure: cookie.Secure,
HTTPOnly: cookie.HTTPOnly,
SameSite: cookie.SameSite,
Expires: cookie.Expires,
Priority: cookie.Priority,
SameParty: cookie.SameParty,
SourceScheme: cookie.SourceScheme,
SourcePort: port,
})
}
return nodes, nil
}
// createCookieFilenameFromURL generates a cookie filename based on the given URL.
// If no URL is provided, it uses the bot's current URL.
// Returns the full filepath for the cookie file and any error encountered.
func (b *Bot) createCookieFilenameFromURL(uri string) (string, error) {
if uri == "" {
uri = b.CurrentURL()
}
u, err := url.Parse(uri)
if err != nil {
return "", err
}
filename := fmt.Sprintf("%s_%s.cookies", u.Scheme, u.Host)
folder := StrAorB(b.cookieFolder, _defaultCookieFolder)
filepath := filepath.Join(folder, filename)
return filepath, nil
}
// ParseCURLCookiesFromFile reads a file containing cURL-formatted cookies,
// parses the content, and returns NetworkCookieParam objects.
// It returns an error if the file cannot be read or parsed.
func (b *Bot) ParseCURLCookiesFromFile(filePath string) ([]*proto.NetworkCookieParam, error) {
raw, err := os.ReadFile(filePath)
if err != nil {
return nil, err
}
return ParseCURLCookies(string(raw))
}
// ParseCURLCookies converts a cURL-formatted cookie string into NetworkCookieParam objects.
//
// It extracts cookies from the "Cookie" header in the cURL string, parses each cookie,
// and creates NetworkCookieParam objects with appropriate attributes based on the URL.
//
// Parameters:
// - raw: string - The raw cURL-formatted cookie string.
//
// Returns:
// - []*proto.NetworkCookieParam: Slice of parsed cookie objects.
// - error: Error if parsing fails or no cookies are found.
//
// Usage:
//
// cookies, err := ParseCURLCookies(curlString)
func ParseCURLCookies(raw string) ([]*proto.NetworkCookieParam, error) {
curl, err := uncurl.NewString(raw)
if err != nil {
return nil, fmt.Errorf("cannot new copy as curl: %w", err)
}
cookieArr := getHeader(curl.Header(), "Cookie")
if len(cookieArr) == 0 {
return nil, ErrEmptyCookieStr
}
uriQuery, err := url.Parse(curl.Target())
if err != nil {
return nil, err
}
var nodes []*proto.NetworkCookieParam
for _, pair := range strings.Split(cookieArr[0], "; ") {
pair = strings.TrimSpace(pair)
if pair == "" {
continue
}
arr := strings.Split(pair, "=")
httpOnly := true
port := _portHTTP
if uriQuery.Scheme == _schemeHTTPS {
httpOnly = false
port = _portHTTPS
}
ckObj := &proto.NetworkCookieParam{
Name: arr[0],
Value: arr[1],
Domain: uriQuery.Host,
Path: uriQuery.Path,
HTTPOnly: httpOnly,
SourcePort: &port,
}
nodes = append(nodes, ckObj)
}
return nodes, nil
}
// getHeader retrieves the values for a given header key from an http.Header object.
// It checks for both the original key and its lowercase version.
// Returns a slice of string values associated with the header key.
func getHeader(header http.Header, key string) []string {
cookieStr, b := header[key]
if b {
return cookieStr
}
return header[strings.ToLower(key)]
}
// flattenNodes converts a slice of NetworkCookieParam objects into a slice of strings,
// where each string is in the format "name=value".
// Returns a slice of strings representing the flattened cookie data.
func flattenNodes(nodes []*proto.NetworkCookieParam) []string {
got := []string{}
for _, node := range nodes {
got = append(got, fmt.Sprintf("%s=%s", node.Name, node.Value))
}
return got
}