Skip to content

Commit 8ee8dd0

Browse files
author
danny
committed
Merge branch 'new_build' into dev
2 parents f82f5e0 + b3204fc commit 8ee8dd0

File tree

9 files changed

+159
-117
lines changed

9 files changed

+159
-117
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ Included projects are (Credits goes to them for the hard work):
148148

149149
Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.
150150

151-
[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)
151+
[![License: AGPL v3](https://img.shields.io/badge/License-AGPL_v3-blue.svg)](https://www.gnu.org/licenses/agpl-3.0)
152152

153153
## Disclaimer
154154

endpoint/mh_endpoint.py

Lines changed: 40 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,15 @@
55
import logging
66
import os
77
import ssl
8+
import sys
89
import time
910
from collections import OrderedDict
1011
from datetime import datetime, timezone
1112
from http.server import BaseHTTPRequestHandler, HTTPServer
1213

1314
import requests
1415

15-
import config
16+
import mh_config
1617
from register import apple_cryptography, pypush_gsa_icloud
1718

1819
logger = logging.getLogger()
@@ -29,9 +30,9 @@ def addCORSHeaders(self):
2930
self.send_header("Access-Control-Allow-Private-Network","true")
3031

3132
def authenticate(self):
32-
user = config.getEndpointUser()
33-
passw = config.getEndpointPass()
34-
if (user is None or user == "") and (passw is None or passw == ""):
33+
endpoint_user = mh_config.getEndpointUser()
34+
endpoint_pass = mh_config.getEndpointPass()
35+
if (endpoint_user is None or endpoint_user == "") and (endpoint_pass is None or endpoint_pass == ""):
3536
return True
3637

3738
auth_header = self.headers.get('authorization')
@@ -40,7 +41,7 @@ def authenticate(self):
4041
if auth_type.lower() == 'basic':
4142
auth_decoded = base64.b64decode(auth_encoded).decode('utf-8')
4243
username, password = auth_decoded.split(':', 1)
43-
if username == user and password == passw:
44+
if username == endpoint_user and password == endpoint_pass:
4445
return True
4546

4647
return False
@@ -122,7 +123,7 @@ def do_POST(self):
122123
responseBody = json.dumps(result)
123124
self.wfile.write(responseBody.encode())
124125
except requests.exceptions.ConnectTimeout:
125-
logger.error("Timeout to " + config.getAnisetteServer() +
126+
logger.error("Timeout to " + mh_config.getAnisetteServer() +
126127
", is your anisette running and accepting Connections?")
127128
self.send_response(504)
128129
except Exception as e:
@@ -136,46 +137,64 @@ def getCurrentTimes(self):
136137

137138

138139
def getAuth(regenerate=False, second_factor='sms'):
139-
if os.path.exists(config.getConfigFile()) and not regenerate:
140-
with open(config.getConfigFile(), "r") as f:
140+
if os.path.exists(mh_config.getConfigFile()) and not regenerate:
141+
with open(mh_config.getConfigFile(), "r") as f:
141142
j = json.load(f)
142143
else:
143-
mobileme = pypush_gsa_icloud.icloud_login_mobileme(username=config.USER, password=config.PASS)
144+
mobileme = pypush_gsa_icloud.icloud_login_mobileme(username=mh_config.USER, password=mh_config.PASS)
144145
logger.debug('Mobileme result: ' + mobileme)
145146
j = {'dsid': mobileme['dsid'], 'searchPartyToken': mobileme['delegates']
146147
['com.apple.mobileme']['service-data']['tokens']['searchPartyToken']}
147-
with open(config.getConfigFile(), "w") as f:
148+
with open(mh_config.getConfigFile(), "w") as f:
148149
json.dump(j, f)
149150
return j['dsid'], j['searchPartyToken']
150151

151152

153+
154+
def check_if_anisette_is_reachable(max_retries=3, retry_delay=10):
155+
server_url = mh_config.getAnisetteServer()
156+
logging.info(f'Checking if Anisette {server_url} is reachable')
157+
for attempt in range(max_retries):
158+
try:
159+
response = requests.get(server_url, timeout=5)
160+
response.raise_for_status()
161+
return
162+
except (requests.RequestException, requests.HTTPError) as e:
163+
logger.error(f"Attempt {attempt + 1} failed: {str(e)}")
164+
if attempt < max_retries - 1:
165+
logger.error(f"Retrying in {retry_delay} seconds...")
166+
time.sleep(retry_delay)
167+
logger.error(f"Max retries reached. Program will exit. Make sure your Anisette is reachable and start again with 'docker start -ai macless-haystack'")
168+
sys.exit()
169+
152170
if __name__ == "__main__":
153-
logging.debug(f'Searching for token at ' + config.getConfigFile())
154-
if not os.path.exists(config.getConfigFile()):
171+
check_if_anisette_is_reachable()
172+
logging.info(f'Searching for token at ' + mh_config.getConfigFile())
173+
if not os.path.exists(mh_config.getConfigFile()):
155174
logging.info(f'No auth-token found.')
156175
apple_cryptography.registerDevice()
157176

158177
Handler = ServerHandler
159178

160-
httpd = HTTPServer((config.getBindingAddress(), config.getPort()), Handler)
179+
httpd = HTTPServer((mh_config.getBindingAddress(), mh_config.getPort()), Handler)
161180
httpd.timeout = 30
162-
address = config.getBindingAddress() + ":" + str(config.getPort())
163-
if os.path.isfile(config.getCertFile()):
164-
logger.info("Certificate file " + config.getCertFile() +
181+
address = mh_config.getBindingAddress() + ":" + str(mh_config.getPort())
182+
if os.path.isfile(mh_config.getCertFile()):
183+
logger.info("Certificate file " + mh_config.getCertFile() +
165184
" exists, so using SSL")
166185
ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
167-
ssl_context.load_cert_chain(certfile=config.getCertFile(
168-
), keyfile=config.getKeyFile() if os.path.isfile(config.getKeyFile()) else None)
186+
ssl_context.load_cert_chain(certfile=mh_config.getCertFile(
187+
), keyfile=mh_config.getKeyFile() if os.path.isfile(mh_config.getKeyFile()) else None)
169188

170189
httpd.socket = ssl_context.wrap_socket(httpd.socket, server_side=True)
171190

172191
logger.info("serving at " + address + " over HTTPS")
173192
else:
174-
logger.info("Certificate file " + config.getCertFile() +
193+
logger.info("Certificate file " + mh_config.getCertFile() +
175194
" not found, so not using SSL")
176195
logger.info("serving at " + address + " over HTTP")
177-
user = config.getEndpointUser()
178-
passw = config.getEndpointPass()
196+
user = mh_config.getEndpointUser()
197+
passw = mh_config.getEndpointPass()
179198
if (user is None or user == "") and (passw is None or passw == ""):
180199
logger.warning("Endpoint is not protected by authentication")
181200
else:

endpoint/register/apple_cryptography.py

Lines changed: 13 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,18 @@
11
#!/usr/bin/env python
22

3-
import os
4-
import glob
5-
import datetime
6-
import argparse
7-
import base64
8-
import json
93
import hashlib
10-
import codecs
4+
import json
5+
import logging
6+
import os
117
import struct
12-
import requests
13-
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
14-
from cryptography.hazmat.primitives.asymmetric import ec
15-
from cryptography.hazmat.backends import default_backend
16-
import sqlite3
178
import sys
18-
from .pypush_gsa_icloud import icloud_login_mobileme, generate_anisette_headers
199

20-
import config
21-
import logging
10+
from cryptography.hazmat.backends import default_backend
11+
from cryptography.hazmat.primitives.ciphers import Cipher
12+
13+
from endpoint import mh_config
14+
from .pypush_gsa_icloud import icloud_login_mobileme
15+
2216
logger = logging.getLogger()
2317

2418

@@ -42,21 +36,21 @@ def decode_tag(data):
4236

4337

4438
def getAuth(regenerate=False):
45-
if os.path.exists(config.getConfigFile()) and not regenerate:
46-
with open(config.getConfigFile(), "r") as f:
39+
if os.path.exists(mh_config.getConfigFile()) and not regenerate:
40+
with open(mh_config.getConfigFile(), "r") as f:
4741
j = json.load(f)
4842
else:
4943
logger.info('Trying to login')
5044
mobileme = icloud_login_mobileme(
51-
username=config.getUser(), password=config.getPass())
45+
username=mh_config.getUser(), password=mh_config.getPass())
5246

5347
logger.debug('Answer from icloud login')
5448
logger.debug(mobileme)
5549
status = mobileme['delegates']['com.apple.mobileme']['status']
5650
if status == 0:
5751
j = {'dsid': mobileme['dsid'], 'searchPartyToken': mobileme['delegates']
5852
['com.apple.mobileme']['service-data']['tokens']['searchPartyToken']}
59-
with open(config.getConfigFile(), "w") as f:
53+
with open(mh_config.getConfigFile(), "w") as f:
6054
json.dump(j, f)
6155
else:
6256
msg = mobileme['delegates']['com.apple.mobileme']['status-message']

endpoint/register/pypush_gsa_icloud.py

Lines changed: 56 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import time
2+
13
import urllib3
24
from getpass import getpass
35
import plistlib as plist
@@ -16,7 +18,8 @@
1618
from cryptography.hazmat.primitives import padding
1719
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
1820
from Crypto.Hash import SHA256
19-
import config
21+
22+
from endpoint import mh_config
2023

2124
# Created here so that it is consistent
2225
USER_ID = uuid.uuid4()
@@ -37,6 +40,7 @@ def icloud_login_mobileme(username='', password=''):
3740
username = input('Apple ID: ')
3841
if not password:
3942
password = getpass('Password: ')
43+
4044
g = gsa_authenticate(username, password)
4145
pet = g["t"]["com.apple.gs.idms.pet"]["token"]
4246
adsid = g["adsid"]
@@ -71,10 +75,10 @@ def icloud_login_mobileme(username='', password=''):
7175
def gsa_authenticate(username, password):
7276
# Password is None as we'll provide it later
7377
usr = srp.User(username, bytes(), hash_alg=srp.SHA256, ng_type=srp.NG_2048)
74-
_, A = usr.start_authentication()
78+
_, a = usr.start_authentication()
7579
logger.info("Authentication request initialization")
7680
r = gsa_authenticated_request(
77-
{"A2k": A, "ps": ["s2k", "s2k_fo"], "u": username, "o": "init"})
81+
{"A2k": a, "ps": ["s2k", "s2k_fo"], "u": username, "o": "init"})
7882

7983
if r["sp"] not in ["s2k", "s2k_fo"]:
8084
logger.warning(f"This implementation only supports s2k and sk2_fo. Server returned {r['sp']}")
@@ -169,7 +173,7 @@ def generate_cpd():
169173

170174

171175
def generate_anisette_headers():
172-
h = json.loads(requests.get(config.getAnisetteServer(), timeout=5).text)
176+
h = json.loads(requests.get(mh_config.getAnisetteServer(), timeout=5).text)
173177
a = {"X-Apple-I-MD": h["X-Apple-I-MD"],
174178
"X-Apple-I-MD-M": h["X-Apple-I-MD-M"]}
175179
a.update(generate_meta_headers(user_id=USER_ID, device_id=DEVICE_ID))
@@ -219,15 +223,13 @@ def decrypt_cbc(usr, data):
219223
return padder.update(data) + padder.finalize()
220224

221225

226+
WAITING_TIME = 60
227+
228+
222229
def sms_second_factor(dsid, idms_token):
223230
identity_token = base64.b64encode(
224231
(dsid + ":" + idms_token).encode()).decode()
225232

226-
# TODO: Actually do this request to get user prompt data
227-
# a = requests.get("https://gsa.apple.com/auth", verify=False)
228-
# This request isn't strictly necessary though,
229-
# and most accounts should have their id 1 SMS, if not contribute ;)
230-
231233
headers = {
232234
"User-Agent": "Xcode",
233235
"Accept-Language": "en-us",
@@ -238,32 +240,46 @@ def sms_second_factor(dsid, idms_token):
238240
}
239241

240242
headers.update(generate_anisette_headers())
241-
243+
242244
# Extract the "boot_args" from the auth page to get the id of the trusted phone number
243245
pattern = r'<script.*class="boot_args">\s*(.*?)\s*</script>'
244246
auth = requests.get("https://gsa.apple.com/auth", headers=headers, verify=False)
245247
sms_id = 1
246248
match = re.search(pattern, auth.text, re.DOTALL)
247249
if match:
248250
boot_args = json.loads(match.group(1).strip())
249-
try:
251+
try:
250252
sms_id = boot_args["direct"]["phoneNumberVerification"]["trustedPhoneNumber"]["id"]
251-
except KeyError as e:
252-
logger.debug(match.group(1).strip())
253-
logger.error("Key for sms id not found. Using the first phone number")
253+
except KeyError as e:
254+
logger.debug(match.group(1).strip())
255+
logger.error("Key for sms id not found. Using the first phone number")
254256
else:
255-
logger.debug(auth.text)
256-
logger.error("Script for sms id not found. Using the first phone number")
257-
258-
logger.info(f"Using phone with id {sms_id} for SMS2FA")
259-
body = {"phoneNumber": {"id": sms_id }, "mode": "sms"}
260-
261-
# This will send the 2FA code to the user's phone over SMS
262-
# We don't care about the response, it's just some HTML with a form for entering the code
263-
# Easier to just use a text prompt
257+
logger.debug(auth.text)
258+
logger.error("Script for sms id not found. Using the first phone number")
264259

260+
logger.info(f"Using phone with id {sms_id} for SMS2FA")
261+
body = {"phoneNumber": {"id": sms_id}, "mode": "sms"}
262+
for handler in logger.handlers:
263+
handler.flush()
265264
# Prompt for the 2FA code. It's just a string like '123456', no dashes or spaces
266-
code = input("Enter SMS 2FA code: ")
265+
start_time = time.perf_counter()
266+
code = input(
267+
f"Enter SMS 2FA code (If you do not receive a code, wait {WAITING_TIME}s and press Enter. An attempt will be made to request the SMS in another way.): ")
268+
end_time = time.perf_counter()
269+
270+
if code == "":
271+
elapsed_time = int(end_time - start_time)
272+
if elapsed_time < WAITING_TIME:
273+
waiting_time = WAITING_TIME - elapsed_time
274+
logger.info(
275+
f"You only waited {elapsed_time} seconds. The next request will be started in {waiting_time} seconds")
276+
time.sleep(waiting_time)
277+
code = input(f"Enter SMS 2FA code if you have received it in the meantime, otherwise press Enter: ")
278+
279+
if code == "":
280+
code = request_code(headers)
281+
else:
282+
code = request_code(headers)
267283

268284
body['securityCode'] = {'code': code}
269285

@@ -287,3 +303,19 @@ def sms_second_factor(dsid, idms_token):
287303
else:
288304
raise Exception(
289305
"2FA unsuccessful. Maybe wrong code or wrong number. Check your account details.")
306+
307+
308+
def request_code(headers):
309+
# This will send the 2FA code to the user's phone over SMS
310+
# We don't care about the response, it's just some HTML with a form for entering the code
311+
# Easier to just use a text prompt
312+
body = {"phoneNumber": {"id": 1}, "mode": "sms"}
313+
requests.put(
314+
"https://gsa.apple.com/auth/verify/phone/",
315+
json=body,
316+
headers=headers,
317+
verify=False,
318+
timeout=5
319+
)
320+
code = input(f"Enter SMS 2FA code:")
321+
return code

macless_haystack/android/app/build.gradle

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
plugins {
2+
id "com.android.application"
3+
id "kotlin-android"
4+
id "dev.flutter.flutter-gradle-plugin"
5+
}
16
def localProperties = new Properties()
27
def localPropertiesFile = rootProject.file('local.properties')
38
if (localPropertiesFile.exists()) {
@@ -6,10 +11,6 @@ if (localPropertiesFile.exists()) {
611
}
712
}
813

9-
def flutterRoot = localProperties.getProperty('flutter.sdk')
10-
if (flutterRoot == null) {
11-
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
12-
}
1314

1415
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
1516
if (flutterVersionCode == null) {
@@ -21,9 +22,6 @@ if (flutterVersionName == null) {
2122
flutterVersionName = '1.0'
2223
}
2324

24-
apply plugin: 'com.android.application'
25-
apply plugin: 'kotlin-android'
26-
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
2725

2826
android {
2927
defaultConfig {
@@ -76,5 +74,5 @@ flutter {
7674
}
7775

7876
dependencies {
79-
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
77+
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.21"
8078
}

macless_haystack/android/build.gradle

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,3 @@
1-
buildscript {
2-
ext.kotlin_version = '1.8.21'
3-
repositories {
4-
google()
5-
mavenCentral()
6-
}
7-
8-
dependencies {
9-
classpath 'com.android.tools.build:gradle:8.6.1'
10-
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
11-
}
12-
}
13-
141
allprojects {
152
repositories {
163
google()

0 commit comments

Comments
 (0)