adsedare 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -14,49 +14,49 @@ require_relative "../logging"
14
14
 
15
15
  module Starship
16
16
  class Error < StandardError; end
17
-
17
+
18
18
  # AuthHelper handles authentication with Apple"s developer portal
19
19
  class AuthHelper
20
20
  include Logging
21
21
 
22
22
  attr_reader :session, :csrf, :csrf_ts, :session_data
23
-
23
+
24
24
  AUTH_ENDPOINT = "https://idmsa.apple.com/appleauth/auth"
25
25
  WIDGET_KEY_URL = "https://appstoreconnect.apple.com/olympus/v1/app/config?hostname=itunesconnect.apple.com"
26
-
26
+
27
27
  def initialize(two_factor_provider = nil)
28
28
  # Create session directory if it doesn't exist
29
- @session_directory = File.expand_path('~/.starship')
29
+ @session_directory = File.expand_path("~/.starship")
30
30
  FileUtils.mkdir_p(@session_directory)
31
-
31
+
32
32
  @widget_key = nil
33
33
  @csrf = nil
34
34
  @csrf_ts = nil
35
35
  @email = nil
36
36
  @session_data = {}
37
37
  @two_factor_provider = two_factor_provider || Starship::ManualTwoFactorProvider.new
38
-
38
+
39
39
  # Initialize Faraday with cookie jar
40
40
  @session = Faraday.new do |builder|
41
41
  builder.use :cookie_jar, jar: HTTP::CookieJar.new
42
42
  builder.adapter Faraday.default_adapter
43
43
  end
44
44
  end
45
-
45
+
46
46
  # Sign in to Apple Developer Portal
47
47
  # @return [Boolean] Whether authentication was successful
48
48
  def sign_in
49
49
  email = ENV["APPLE_DEVELOPER_USERNAME"]
50
50
  password = ENV["APPLE_DEVELOPER_PASSWORD"]
51
-
51
+
52
52
  if !email || !password
53
53
  raise Error, "Email and password are required. Set APPLE_ID and APPLE_PASSWORD environment variables."
54
54
  end
55
-
55
+
56
56
  @email = email
57
57
  @client_id = generate_session_id(email)
58
58
  cookie_path, session_path = get_paths(email)
59
-
59
+
60
60
  # Try to load existing session
61
61
  if File.exist?(session_path)
62
62
  load_session
@@ -64,13 +64,13 @@ module Starship
64
64
  return true
65
65
  end
66
66
  end
67
-
67
+
68
68
  logger.warn "Session invalid or expired. Starting authentication from scratch..."
69
69
  @session_data = { "client_id" => @client_id, "email" => email }
70
-
70
+
71
71
  # Start authentication process
72
72
  auth_result = authenticate_with_srp(email, password)
73
-
73
+
74
74
  if auth_result == :two_factor_required
75
75
  logger.info "Two-factor authentication required. Requesting verification code..."
76
76
 
@@ -80,44 +80,44 @@ module Starship
80
80
  response = @session.get("https://developer.apple.com/account")
81
81
  if response.status == 200
82
82
  extract_csrf_tokens(response)
83
-
83
+
84
84
  if @csrf && @csrf_ts
85
85
  save_session
86
86
  return true
87
87
  end
88
88
  end
89
89
  end
90
-
90
+
91
91
  return auth_result
92
92
  end
93
-
93
+
94
94
  # Check if the current session is valid
95
95
  # @return [Boolean] Whether the session is valid
96
96
  def validate_token
97
97
  return false unless @session_data["session_id"] && @session_data["scnt"]
98
-
98
+
99
99
  begin
100
100
  headers = {
101
101
  "Accept" => "application/json, text/plain, */*",
102
102
  "Content-Type" => "application/vnd.api+json",
103
103
  "X-Requested-With" => "XMLHttpRequest",
104
104
  "X-Apple-ID-Session-Id" => @session_data["session_id"],
105
- "scnt" => @session_data["scnt"]
105
+ "scnt" => @session_data["scnt"],
106
106
  }
107
-
107
+
108
108
  response = @session.get(
109
- "https://developer.apple.com/services-account/v1/certificates",
110
- nil,
109
+ "https://developer.apple.com/services-account/v1/certificates",
110
+ nil,
111
111
  headers
112
112
  )
113
113
 
114
- if response.status == 403
114
+ if response.status == 403
115
115
  # Fetch CSRF tokens after confirming session is valid
116
116
  csrf_response = @session.get("https://developer.apple.com/account/resources/certificates/list")
117
117
 
118
118
  if csrf_response.status == 200
119
119
  extract_csrf_tokens(csrf_response)
120
-
120
+
121
121
  if @csrf && @csrf_ts
122
122
  return true
123
123
  else
@@ -135,14 +135,14 @@ module Starship
135
135
  return false
136
136
  end
137
137
  end
138
-
138
+
139
139
  def request(endpoint, method: :get, params: nil, body: nil, headers: nil)
140
140
  default_headers = {
141
141
  "Accept" => "application/json, text/plain, */*",
142
142
  "X-Requested-With" => "XMLHttpRequest",
143
143
  "X-HTTP-Method-Override" => "GET",
144
144
  "csrf" => @csrf,
145
- "csrf_ts" => @csrf_ts
145
+ "csrf_ts" => @csrf_ts,
146
146
  }
147
147
 
148
148
  if headers
@@ -150,38 +150,38 @@ module Starship
150
150
  end
151
151
 
152
152
  response = case method
153
- when :get
154
- @session.get(endpoint, params, default_headers)
155
- when :post
156
- @session.post(endpoint, body, default_headers)
157
- when :put
158
- @session.put(endpoint, body, default_headers)
159
- when :delete
160
- @session.delete(endpoint, default_headers)
161
- when :patch
162
- @session.patch(endpoint, body, default_headers)
163
- end
164
-
153
+ when :get
154
+ @session.get(endpoint, params, default_headers)
155
+ when :post
156
+ @session.post(endpoint, body, default_headers)
157
+ when :put
158
+ @session.put(endpoint, body, default_headers)
159
+ when :delete
160
+ @session.delete(endpoint, default_headers)
161
+ when :patch
162
+ @session.patch(endpoint, body, default_headers)
163
+ end
164
+
165
165
  if response.status == 401 || response.status == 403
166
166
  logger.warn "Session invalid or expired. Starting authentication from scratch..."
167
167
 
168
168
  self.sign_in
169
-
169
+
170
170
  response = self.request(endpoint, method: method, params: params, body: body, headers: headers)
171
171
  end
172
-
172
+
173
173
  return response
174
174
  end
175
-
175
+
176
176
  private
177
-
177
+
178
178
  # Generate a consistent session ID from email
179
179
  # @param email [String] The email address
180
180
  # @return [String] The session ID
181
181
  def generate_session_id(email)
182
182
  "auth-#{Digest::SHA256.hexdigest(email)[0...8]}"
183
183
  end
184
-
184
+
185
185
  # Get the paths for cookie and session files
186
186
  # @param email [String] The email address
187
187
  # @return [Array<String>] The cookie path and session path
@@ -191,21 +191,21 @@ module Starship
191
191
  session_path = File.join(@session_directory, "#{session_id}.session")
192
192
  [cookie_path, session_path]
193
193
  end
194
-
194
+
195
195
  # Get the cookie jar path
196
196
  # @return [String] The cookie jar path
197
197
  def cookiejar_path
198
198
  raise Error, "Email not set" unless @email
199
199
  get_paths(@email)[0]
200
200
  end
201
-
201
+
202
202
  # Get the session path
203
203
  # @return [String] The session path
204
204
  def session_path
205
205
  raise Error, "Email not set" unless @email
206
206
  get_paths(@email)[1]
207
207
  end
208
-
208
+
209
209
  # Load session data from file
210
210
  # @return [Boolean] Whether the session was loaded successfully
211
211
  def load_session
@@ -216,7 +216,7 @@ module Starship
216
216
  # Create a new cookie jar for Faraday
217
217
  jar = HTTP::CookieJar.new
218
218
  jar.load(cookiejar_path, format: :cookiestxt)
219
-
219
+
220
220
  # Recreate the Faraday connection with the loaded cookies
221
221
  @session = Faraday.new do |builder|
222
222
  builder.use :cookie_jar, jar: jar
@@ -233,17 +233,17 @@ module Starship
233
233
  return false
234
234
  end
235
235
  end
236
-
236
+
237
237
  # Save session data to file
238
238
  def save_session
239
239
  logger.info "Saving session to disk at #{session_path}"
240
240
  File.write(session_path, JSON.pretty_generate(@session_data))
241
-
241
+
242
242
  # Save cookies
243
243
  cookie_path = get_paths(@email)[0]
244
-
244
+
245
245
  # Get the cookie jar from Faraday
246
- jar = @session.builder.app.instance_variable_get('@jar')
246
+ jar = @session.builder.app.instance_variable_get("@jar")
247
247
  if jar
248
248
  # Save all cookies, even if they're marked as discardable or expired
249
249
  jar.save(cookie_path, format: :cookiestxt, session: true)
@@ -251,10 +251,10 @@ module Starship
251
251
  else
252
252
  logger.info "No cookies were found to save."
253
253
  end
254
-
254
+
255
255
  logger.info "Session saved successfully."
256
256
  end
257
-
257
+
258
258
  # Get the widget key used for authentication
259
259
  # @return [String] The widget key
260
260
  def widget_key
@@ -264,26 +264,26 @@ module Starship
264
264
  end
265
265
  @widget_key
266
266
  end
267
-
267
+
268
268
  # Get a cookie value by name
269
269
  # @param name [String] The cookie name
270
270
  # @return [String, nil] The cookie value or nil if not found
271
271
  def get_cookie_value(name)
272
272
  # Access the cookie jar middleware from Faraday
273
- jar = @session.builder.app.instance_variable_get('@jar')
273
+ jar = @session.builder.app.instance_variable_get("@jar")
274
274
  return nil unless jar
275
-
275
+
276
276
  # Find the cookie by name
277
277
  jar.cookies.find { |cookie| cookie.name == name }&.value
278
278
  end
279
-
279
+
280
280
  # Extract CSRF tokens from response
281
281
  # @param response [Faraday::Response] The response object
282
282
  def extract_csrf_tokens(response)
283
283
  # Try cookies first
284
284
  @csrf = get_cookie_value("csrf")
285
285
  @csrf_ts = get_cookie_value("csrf_ts")
286
-
286
+
287
287
  # If not in cookies, try response headers
288
288
  unless @csrf
289
289
  @csrf = response.headers["csrf"]
@@ -291,18 +291,18 @@ module Starship
291
291
  unless @csrf_ts
292
292
  @csrf_ts = response.headers["csrf_ts"]
293
293
  end
294
-
294
+
295
295
  # If still not found, try to extract from page content
296
296
  if !@csrf || !@csrf_ts
297
- if response.body =~ /csrf[""]\s*:\s*[""](.*?)[""]/
297
+ if response.body =~ /csrf[""]\s*:\s*[""](.*?)[""]/
298
298
  @csrf = $1
299
299
  end
300
- if response.body =~ /csrf_ts[""]\s*:\s*[""](.*?)[""]/
300
+ if response.body =~ /csrf_ts[""]\s*:\s*[""](.*?)[""]/
301
301
  @csrf_ts = $1
302
302
  end
303
303
  end
304
304
  end
305
-
305
+
306
306
  # Authenticate with Secure Remote Password protocol
307
307
  # @param email [String] The email address
308
308
  # @param password [String] The password
@@ -310,108 +310,108 @@ module Starship
310
310
  def authenticate_with_srp(email, password)
311
311
  # This is a simplified SRP implementation since Ruby doesn"t have a direct equivalent to Python"s srp library
312
312
  # In a real implementation, you would use a proper SRP library or implement the full protocol
313
-
313
+
314
314
  headers = {
315
315
  "Accept" => "application/json, text/javascript",
316
316
  "Content-Type" => "application/json",
317
317
  "X-Requested-With" => "XMLHttpRequest",
318
- "X-Apple-Widget-Key" => widget_key
318
+ "X-Apple-Widget-Key" => widget_key,
319
319
  }
320
-
320
+
321
321
  if @session_data["session_id"]
322
322
  headers.update({
323
323
  "X-Apple-ID-Session-Id" => @session_data["session_id"],
324
- "scnt" => @session_data["scnt"]
324
+ "scnt" => @session_data["scnt"],
325
325
  })
326
326
  end
327
-
327
+
328
328
  # For simplicity, we"ll use a direct password-based authentication approach
329
329
  # In a real implementation, you would implement the full SRP protocol
330
330
  auth_data = {
331
331
  "accountName" => email,
332
332
  "password" => password,
333
- "rememberMe" => true
333
+ "rememberMe" => true,
334
334
  }
335
-
335
+
336
336
  logger.info "Initializing authentication request to Apple ID..."
337
337
  response = @session.post(
338
338
  "#{AUTH_ENDPOINT}/signin",
339
339
  auth_data.to_json,
340
340
  headers
341
341
  )
342
-
342
+
343
343
  # Handle 409 response (2FA required)
344
344
  if response.status == 409
345
345
  session_id = response.headers["X-Apple-ID-Session-Id"]
346
346
  scnt = response.headers["scnt"]
347
-
347
+
348
348
  if session_id && scnt
349
349
  @session_data.update({
350
350
  "session_id" => session_id,
351
351
  "scnt" => scnt,
352
352
  "client_id" => @client_id,
353
- "email" => email
353
+ "email" => email,
354
354
  })
355
355
  save_session
356
356
  end
357
-
357
+
358
358
  return :two_factor_required
359
359
  end
360
-
360
+
361
361
  # Handle successful authentication
362
362
  if response.status == 200 || response.status == 302
363
363
  session_id = response.headers["X-Apple-ID-Session-Id"]
364
364
  scnt = response.headers["scnt"]
365
-
365
+
366
366
  if session_id && scnt
367
367
  @session_data.update({
368
368
  "session_id" => session_id,
369
369
  "scnt" => scnt,
370
370
  "client_id" => @client_id,
371
- "email" => email
371
+ "email" => email,
372
372
  })
373
373
  save_session
374
374
  logger.info "Session data saved after basic authentication."
375
375
  return true
376
376
  end
377
377
  end
378
-
378
+
379
379
  return false
380
380
  end
381
-
381
+
382
382
  # Handle two-factor authentication
383
383
  # @return [Boolean] Whether 2FA was successful
384
384
  def handle_two_factor_auth
385
385
  session_id = @session_data["session_id"]
386
386
  scnt = @session_data["scnt"]
387
-
387
+
388
388
  unless session_id && scnt
389
389
  logger.error "Missing session data. Cannot continue two-factor authentication."
390
390
  return false
391
391
  end
392
-
392
+
393
393
  begin
394
394
  # Get 2FA code from provider
395
395
  code = @two_factor_provider.get_code(session_id, scnt)
396
-
396
+
397
397
  verify_headers = {
398
398
  "Accept" => "application/json, text/javascript",
399
399
  "Content-Type" => "application/json",
400
400
  "X-Requested-With" => "XMLHttpRequest",
401
401
  "X-Apple-ID-Session-Id" => session_id,
402
402
  "scnt" => scnt,
403
- "X-Apple-Widget-Key" => widget_key
403
+ "X-Apple-Widget-Key" => widget_key,
404
404
  }
405
-
405
+
406
406
  verify_data = { "securityCode" => { "code" => code.strip } }
407
-
407
+
408
408
  # First verify the security code
409
409
  verify_response = @session.post(
410
410
  "#{AUTH_ENDPOINT}/verify/trusteddevice/securitycode",
411
411
  verify_data.to_json,
412
412
  verify_headers
413
413
  )
414
-
414
+
415
415
  if verify_response.status == 204
416
416
  logger.info "Two-factor code verified successfully."
417
417
  logger.info "Trusting the session after 2FA verification..."
@@ -422,15 +422,14 @@ module Starship
422
422
  nil,
423
423
  verify_headers
424
424
  )
425
-
426
-
425
+
427
426
  if trust_response.status == 204
428
427
  # Store all relevant session data
429
428
  @session_data.update({
430
429
  "session_id" => session_id,
431
430
  "scnt" => scnt,
432
431
  "client_id" => @client_id,
433
- "email" => @email
432
+ "email" => @email,
434
433
  })
435
434
  logger.info "Session trusted and fully authenticated. Saving final session data."
436
435
  save_session
@@ -443,7 +442,7 @@ module Starship
443
442
  rescue => e
444
443
  logger.error "Two-factor verification failed: #{e.message}"
445
444
  end
446
-
445
+
447
446
  return false
448
447
  end
449
448
  end