Skip to content

Commit 2c96e37

Browse files
authored
Merge pull request #5 from Freedom-Club-FC/feat/settings-menu
feat: settings menu, and proxy support
2 parents 22b1b60 + 7b8595e commit 2c96e37

File tree

10 files changed

+392
-66
lines changed

10 files changed

+392
-66
lines changed

assets/icons/settings_icon.png

775 Bytes
Loading

core/requests.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,56 @@
11
from urllib import request
22
import json
33

4+
_ORIGINAL_SOCKET = None
5+
6+
def socks_monkey_patch(proxy_info: dict = None):
7+
import socks
8+
import socket
9+
10+
if proxy_info["username"] and proxy_info["password"]:
11+
socks.set_default_proxy(
12+
socks.SOCKS5 if proxy_info["type"] == "SOCKS5" else socks.SOCKS4,
13+
proxy_info["host"],
14+
proxy_info["port"],
15+
username=proxy_info["username"],
16+
password=proxy_info["password"]
17+
)
18+
else:
19+
socks.set_default_proxy(
20+
socks.SOCKS5 if proxy_info["type"] == "SOCKS5" else socks.SOCKS4,
21+
proxy_info["host"],
22+
proxy_info["port"],
23+
)
24+
25+
_ORIGINAL_SOCKET = socket.socket # save our socket before patching monkey patching socks
26+
socket.socket = socks.socksocket
27+
28+
29+
def http_monkey_patch(proxy_info: dict = None):
30+
if proxy_info and proxy_info["type"] == "HTTP":
31+
proxy_str = f"{proxy_info['host']}:{proxy_info['port']}"
32+
if proxy_info["username"] and proxy_info["password"]:
33+
proxy_str = f"{proxy_info['username']}:{proxy_info['password']}@{proxy_str}"
34+
35+
proxy_handler = request.ProxyHandler({
36+
'http': 'http://' + proxy_str,
37+
'https': 'http://' + proxy_str
38+
})
39+
40+
opener = request.build_opener(proxy_handler)
41+
request.install_opener(opener)
42+
43+
44+
def undo_monkey_patching():
45+
# This undos the custom opener for urllib
46+
request.install_opener(request.build_opener())
47+
48+
# This tries to undo the monkey patching we did using Pysocks
49+
if _ORIGINAL_SOCKET:
50+
import socket
51+
socket.socket = _ORIGINAL_SOCKET
52+
53+
454
def http_request(url: str, method: str, auth_token: str = None, payload: dict = None, longpoll: int = -1) -> dict:
555
if payload:
656
payload = json.dumps(payload).encode()

logic/authentication.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ def authenticate_account(user_data: dict) -> dict:
1616
if not 'challenge' in response:
1717
raise ValueError("Server did not give authenticatation challenge! Are you sure this is a Coldwire server ?")
1818
except Exception:
19-
if 'proxy_info' in user_data:
19+
if user_data["settings"]["proxy_info"] is not None:
2020
raise ValueError("Could not connect to server! Are you sure your proxy settings are valid ?")
2121
else:
2222
raise ValueError("Could not connect to server! Are you sure the URL is valid ?")

logic/message.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ def generate_and_send_pads(user_data, user_data_lock, contact_id: str, ui_queue)
4646

4747
contact_kyber_public_key = user_data["contacts"][contact_id]["ephemeral_keys"]["contact_public_key"]
4848
our_lt_private_key = user_data["contacts"][contact_id]["lt_sign_keys"]["our_keys"]["private_key"]
49-
49+
5050

5151
ciphertext_blob, pads = generate_kyber_shared_secrets(contact_kyber_public_key)
5252

logic/storage.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ def check_account_file() -> bool:
1616

1717
def load_account_data(password = None) -> dict:
1818
user_data = None
19-
if not password:
19+
if password is None:
2020
with open(ACCOUNT_FILE_PATH, "r", encoding="utf-8") as f:
2121
user_data = json.load(f)
2222
else:
@@ -35,7 +35,8 @@ def load_account_data(password = None) -> dict:
3535

3636
user_data["tmp"] = {
3737
"ephemeral_key_send_lock": {},
38-
"pfs_do_not_inform": {}
38+
"pfs_do_not_inform": {},
39+
"password": password
3940
}
4041

4142

@@ -99,8 +100,8 @@ def save_account_data(user_data: dict, user_data_lock, password = None) -> None:
99100
with user_data_lock:
100101
user_data = copy.deepcopy(user_data)
101102

102-
if password == None and "password" in user_data:
103-
password = user_data["password"]
103+
if (password is None) and (user_data["tmp"]["password"] is not None):
104+
password = user_data["tmp"]["password"]
104105

105106
del user_data["tmp"]
106107

@@ -157,7 +158,7 @@ def save_account_data(user_data: dict, user_data_lock, password = None) -> None:
157158

158159

159160

160-
if not password:
161+
if password is None:
161162
with open(ACCOUNT_FILE_PATH, "w", encoding="utf-8") as f:
162163
json.dump(user_data, f, indent=2)
163164
else:

logic/user.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
def build_initial_user_data() -> dict:
2+
return {
3+
"server_url": None,
4+
"contacts": {},
5+
"tmp": {},
6+
"settings": {
7+
"proxy_info": None,
8+
"ignore_new_contacts_smp": False,
9+
}
10+
}

ui/connect_window.py

Lines changed: 47 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
from ui.password_window import PasswordWindow
33
from logic.storage import save_account_data
44
from logic.authentication import authenticate_account
5+
from core.requests import socks_monkey_patch, http_monkey_patch, undo_monkey_patching
6+
from logic.user import build_initial_user_data
57
from core.crypto import generate_sign_keys
68
from urllib.parse import urlparse
79
import tkinter as tk
@@ -20,7 +22,6 @@ def __init__(self, master):
2022
self.configure(bg="black")
2123
self.resizable(False, False)
2224

23-
# Server input
2425
self.label = tk.Label(self, text="Enter server URL:", fg="white", bg="black")
2526
self.label.pack(pady=(20, 5))
2627

@@ -31,7 +32,6 @@ def __init__(self, master):
3132

3233
enhanced_entry(self.server_url, placeholder="I.e. example.com ...")
3334

34-
# Use Proxy
3535
self.use_proxy_var = tk.IntVar()
3636
self.proxy_check = tk.Checkbutton(
3737
self,
@@ -46,18 +46,14 @@ def __init__(self, master):
4646
)
4747
self.proxy_check.pack(pady=(10, 0), anchor="center")
4848

49-
# Proxy Fields Frame (hidden initially)
5049
self.proxy_fields_frame = tk.Frame(self, bg="black")
5150

52-
# Proxy row (type + address)
5351
self.proxy_row = tk.Frame(self.proxy_fields_frame, bg="black")
5452

55-
self.proxy_type_var = tk.StringVar(value="SOCKS5")
53+
self.proxy_type_var = tk.StringVar(value="HTTP")
5654
self.proxy_type_var.trace_add("write", self.update_auth_visibility)
5755

58-
self.proxy_type_menu = tk.OptionMenu(
59-
self.proxy_row, self.proxy_type_var, "HTTP", "SOCKS4", "SOCKS5"
60-
)
56+
self.proxy_type_menu = tk.OptionMenu(self.proxy_row, self.proxy_type_var, "HTTP", "SOCKS4", "SOCKS5")
6157
self.proxy_type_menu.config(bg="gray15", fg="white", highlightthickness=0, width=7)
6258
self.proxy_type_menu.pack(side="left", padx=(0, 5))
6359

@@ -69,7 +65,6 @@ def __init__(self, master):
6965

7066
self.proxy_row.pack(pady=5)
7167

72-
# Auth row (username + password)
7368
self.auth_frame = tk.Frame(self.proxy_fields_frame, bg="black")
7469

7570
self.proxy_user_entry = tk.Entry(
@@ -86,16 +81,15 @@ def __init__(self, master):
8681

8782
self.proxy_fields_frame.pack_forget()
8883

89-
# Status + Connect
9084
self.status_label = tk.Label(self, text="", fg="red", bg="black")
9185
self.status_label.pack(pady=5)
9286

9387
self.connect_button = tk.Button(self, text="Connect", command=self.on_connect, bg="gray25", fg="white")
9488
self.connect_button.pack(pady=10)
9589

96-
# Shrink to fit default
90+
# Shrink to fit default, autosize.
9791
self.update_idletasks()
98-
self.geometry("") # Autosize
92+
self.geometry("")
9993

10094
def toggle_proxy_fields(self):
10195
if self.use_proxy_var.get():
@@ -105,11 +99,11 @@ def toggle_proxy_fields(self):
10599
self.proxy_fields_frame.pack_forget()
106100

107101
self.update_idletasks()
108-
self.geometry("") # Resize window
102+
self.geometry("") # Resize again
109103

110104
def update_auth_visibility(self, *args):
111-
proxy_type = self.proxy_type_var.get().lower()
112-
if proxy_type in ["http", "socks5"]:
105+
proxy_type = self.proxy_type_var.get()
106+
if proxy_type in ["HTTP", "SOCKS5"]:
113107
self.auth_frame.pack(pady=5)
114108
else:
115109
self.auth_frame.pack_forget()
@@ -119,11 +113,14 @@ def update_auth_visibility(self, *args):
119113

120114

121115
def password_callback(self, password):
122-
# We save the password (if any) in the user data for ease of access to simplify development
123-
self.user_data = {"server_url": self.server_url_fixed, "password": password, "contacts": {}, "tmp": {}, "proxy_info": None}
116+
# We save the password (if any) in the user data tmp dict for ease of access across codebase (i.e. saving)
117+
self.user_data = build_initial_user_data()
118+
self.user_data["server_url"] = self.server_url_fixed
119+
self.user_data["tmp"]["password"] = password
120+
124121
proxy_info = self.get_proxy_info()
125122
if proxy_info:
126-
self.user_data["proxy_info": proxy_info]
123+
self.user_data["settings"]["proxy_info"] = proxy_info
127124

128125
private_key, public_key = generate_sign_keys()
129126

@@ -137,6 +134,22 @@ def password_callback(self, password):
137134
self.connect_to_server()
138135

139136
def connect_to_server(self):
137+
if self.user_data["settings"]["proxy_info"]:
138+
if self.user_data["settings"]["proxy_info"]["type"] in ["SOCKS5", "SOCKS4"]:
139+
try:
140+
import socks
141+
except ImportError:
142+
logger.error("SOCKS proxy set and we could not find PySocks. WARNING before you install PySocks: PySocks is largely unmaintained. It's highly recommended you use proxychains instead")
143+
self.status_label.config(text="You need to install PySocks to enable SOCKS proxy support!")
144+
return
145+
146+
socks_monkey_patch(self.user_data["settings"]["proxy_info"])
147+
else:
148+
http_monkey_patch(self.user_data["settings"]["proxy_info"])
149+
else:
150+
undo_monkey_patching()
151+
152+
140153
try:
141154
self.user_data = authenticate_account(self.user_data)
142155
except ValueError as e:
@@ -145,31 +158,36 @@ def connect_to_server(self):
145158

146159
save_account_data(self.user_data, self.master.user_data_lock)
147160
self.destroy()
148-
self.master.ready_to_authenticate_callback(self.user_data["password"])
161+
self.master.ready_to_authenticate_callback(self.user_data["tmp"]["password"], already_authenticated = True)
149162

150163

151164
def get_proxy_info(self):
152165
proxy_info = None
153166
if self.use_proxy_var.get():
154-
proxy_type = self.proxy_type_var.get().lower()
155-
proxy_addr = self.proxy_addr_entry.get().strip()
167+
proxy_type = self.proxy_type_var.get()
168+
proxy_addr = self.proxy_addr_entry.get().strip()
169+
username = self.proxy_user_entry.get().strip()
170+
password = self.proxy_pass_entry.get().strip()
171+
156172
if not proxy_addr or ':' not in proxy_addr:
157173
self.status_label.config(text="Invalid proxy address.")
158174
return
159175
host, port = proxy_addr.split(':', 1)
160176

177+
try:
178+
port = int(port)
179+
except ValueError:
180+
self.status_label.config(text="Invalid proxy address port!")
181+
return
182+
161183
proxy_info = {
162184
"type": proxy_type,
163185
"host": host,
164-
"port": int(port)
186+
"port": port,
187+
"username": username,
188+
"password": password
165189
}
166190

167-
username = self.proxy_user_entry.get().strip()
168-
password = self.proxy_pass_entry.get().strip()
169-
if username and password:
170-
proxy_info["username"] = username
171-
proxy_info["password"] = password
172-
173191
if proxy_info:
174192
logger.info("Using proxy: %s", json.dumps(proxy_info, indent=2))
175193
return proxy_info
@@ -207,4 +225,4 @@ def on_connect(self, event=None):
207225
PasswordWindow(self, self.password_callback)
208226
else:
209227
self.status_label.config(text="")
210-
self.password_callback(self.user_data["password"])
228+
self.password_callback(self.user_data["tmp"]["password"])

0 commit comments

Comments
 (0)