ai-devsec-gateway 1.2.1__py3-none-any.whl
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.
- ai_blocker/__init__.py +56 -0
- ai_blocker/__main__.py +112 -0
- ai_blocker/block_actions.py +137 -0
- ai_blocker/config.py +85 -0
- ai_blocker/constants.py +93 -0
- ai_blocker/gateway.py +55 -0
- ai_blocker/i18n.py +141 -0
- ai_blocker/system_utils.py +84 -0
- ai_blocker/tray.py +177 -0
- ai_blocker/ui.py +1254 -0
- ai_devsec_gateway-1.2.1.dist-info/METADATA +340 -0
- ai_devsec_gateway-1.2.1.dist-info/RECORD +15 -0
- ai_devsec_gateway-1.2.1.dist-info/WHEEL +5 -0
- ai_devsec_gateway-1.2.1.dist-info/licenses/LICENSE +33 -0
- ai_devsec_gateway-1.2.1.dist-info/top_level.txt +1 -0
ai_blocker/__init__.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# ruff: noqa: F401
|
|
3
|
+
__version__ = "1.2.1"
|
|
4
|
+
APP_VERSION = __version__
|
|
5
|
+
|
|
6
|
+
# Expose everything to maintain backwards compatibility and test stability
|
|
7
|
+
from ai_blocker.block_actions import activate_block, deactivate_block, detect_running_ai_editors, force_close_processes
|
|
8
|
+
from ai_blocker.config import (
|
|
9
|
+
SENSITIVE_CONFIG_KEYS,
|
|
10
|
+
get_config_path,
|
|
11
|
+
get_windows_autostart,
|
|
12
|
+
load_config,
|
|
13
|
+
save_config,
|
|
14
|
+
set_windows_autostart,
|
|
15
|
+
)
|
|
16
|
+
from ai_blocker.constants import (
|
|
17
|
+
BLOCKLIST,
|
|
18
|
+
CATEGORY_ICONS,
|
|
19
|
+
COL_BASE,
|
|
20
|
+
COL_BLUE,
|
|
21
|
+
COL_GREEN,
|
|
22
|
+
COL_MAUVE,
|
|
23
|
+
COL_RED,
|
|
24
|
+
COL_SUBTEXT,
|
|
25
|
+
COL_SURFACE0,
|
|
26
|
+
COL_SURFACE1,
|
|
27
|
+
COL_TEXT,
|
|
28
|
+
COL_YELLOW,
|
|
29
|
+
COMMENT_TAG,
|
|
30
|
+
CURRENT_OS,
|
|
31
|
+
HOSTS_PATH,
|
|
32
|
+
PROCESS_LIST,
|
|
33
|
+
UI_FONT,
|
|
34
|
+
_get_ui_font,
|
|
35
|
+
)
|
|
36
|
+
from ai_blocker.gateway import GatewayHandler
|
|
37
|
+
from ai_blocker.i18n import (
|
|
38
|
+
CATEGORY_TRANSLATIONS,
|
|
39
|
+
LANG_CODE_MAP,
|
|
40
|
+
LANG_DISPLAY_MAP,
|
|
41
|
+
STRINGS,
|
|
42
|
+
detect_system_language,
|
|
43
|
+
load_translations,
|
|
44
|
+
)
|
|
45
|
+
from ai_blocker.system_utils import (
|
|
46
|
+
_get_subprocess_kwargs,
|
|
47
|
+
count_total_domains,
|
|
48
|
+
flush_dns,
|
|
49
|
+
get_hosts_status,
|
|
50
|
+
is_admin,
|
|
51
|
+
relaunch_as_admin,
|
|
52
|
+
)
|
|
53
|
+
from ai_blocker.ui import AIBlockerApp
|
|
54
|
+
|
|
55
|
+
if CURRENT_OS == "Windows":
|
|
56
|
+
from ai_blocker.tray import WindowsTrayIcon
|
ai_blocker/__main__.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import ctypes
|
|
3
|
+
import sys
|
|
4
|
+
import tkinter as tk
|
|
5
|
+
from tkinter import messagebox
|
|
6
|
+
|
|
7
|
+
from ai_blocker.constants import CURRENT_OS
|
|
8
|
+
from ai_blocker.i18n import STRINGS, detect_system_language
|
|
9
|
+
from ai_blocker.system_utils import get_hosts_status, is_admin, relaunch_as_admin
|
|
10
|
+
from ai_blocker.ui import AIBlockerApp
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def acquire_single_instance_lock():
|
|
14
|
+
if CURRENT_OS == "Windows":
|
|
15
|
+
mutex_name = "Global\\AIBlocker_SingleInstance_Mutex"
|
|
16
|
+
mutex = ctypes.windll.kernel32.CreateMutexW(None, False, mutex_name)
|
|
17
|
+
if ctypes.windll.kernel32.GetLastError() == 183: # ERROR_ALREADY_EXISTS
|
|
18
|
+
return False, None
|
|
19
|
+
return True, mutex
|
|
20
|
+
else:
|
|
21
|
+
try:
|
|
22
|
+
import fcntl
|
|
23
|
+
lock_file = "/tmp/ai_blocker.lock"
|
|
24
|
+
fp = open(lock_file, "w")
|
|
25
|
+
fcntl.lockf(fp, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
26
|
+
return True, fp
|
|
27
|
+
except (IOError, OSError, ImportError):
|
|
28
|
+
return False, None
|
|
29
|
+
|
|
30
|
+
def main():
|
|
31
|
+
parser = argparse.ArgumentParser(description="AI Network Blocker & DevSec Gateway CLI")
|
|
32
|
+
parser.add_argument("--block", choices=["work", "personal", "free"], help="Activate blocking for the specified profile")
|
|
33
|
+
parser.add_argument("--unblock", action="store_true", help="Deactivate all AI domain blocks")
|
|
34
|
+
parser.add_argument("--status", action="store_true", help="Show current blocking status and active editors")
|
|
35
|
+
|
|
36
|
+
args, unknown = parser.parse_known_args()
|
|
37
|
+
|
|
38
|
+
# CLI execution path
|
|
39
|
+
if args.block or args.unblock or args.status:
|
|
40
|
+
if args.status:
|
|
41
|
+
is_blocked, count = get_hosts_status()
|
|
42
|
+
print(f"Status: {'PROTECTED (Blocking active)' if is_blocked else 'EXPOSED (No protection)'}")
|
|
43
|
+
print(f"Blocked domains: {count}")
|
|
44
|
+
|
|
45
|
+
from ai_blocker.block_actions import detect_running_ai_editors
|
|
46
|
+
editors = detect_running_ai_editors()
|
|
47
|
+
if editors:
|
|
48
|
+
print(f"Active AI editors detected: {', '.join(editors)}")
|
|
49
|
+
else:
|
|
50
|
+
print("No active AI editors detected.")
|
|
51
|
+
sys.exit(0)
|
|
52
|
+
|
|
53
|
+
# For block/unblock, verify admin privileges
|
|
54
|
+
if not is_admin():
|
|
55
|
+
print("Error: Administrator/root privileges are required for this action.")
|
|
56
|
+
if CURRENT_OS == "Windows":
|
|
57
|
+
print("Requesting Administrator elevation...")
|
|
58
|
+
relaunch_as_admin()
|
|
59
|
+
sys.exit(1)
|
|
60
|
+
|
|
61
|
+
from ai_blocker.block_actions import activate_block, deactivate_block
|
|
62
|
+
|
|
63
|
+
if args.unblock:
|
|
64
|
+
ok, msg = deactivate_block("en")
|
|
65
|
+
print(msg.strip())
|
|
66
|
+
sys.exit(0 if ok else 1)
|
|
67
|
+
|
|
68
|
+
if args.block:
|
|
69
|
+
from ai_blocker.constants import BLOCKLIST
|
|
70
|
+
if args.block == "work":
|
|
71
|
+
cats = list(BLOCKLIST.keys())
|
|
72
|
+
elif args.block == "personal":
|
|
73
|
+
cats = [c for c in BLOCKLIST.keys() if "copilot" in c.lower()]
|
|
74
|
+
else: # free
|
|
75
|
+
cats = []
|
|
76
|
+
|
|
77
|
+
if not cats:
|
|
78
|
+
ok, msg = deactivate_block("en")
|
|
79
|
+
else:
|
|
80
|
+
ok, msg = activate_block("en", cats)
|
|
81
|
+
print(msg.strip())
|
|
82
|
+
sys.exit(0 if ok else 1)
|
|
83
|
+
|
|
84
|
+
# GUI execution path
|
|
85
|
+
ok, lock_ref = acquire_single_instance_lock()
|
|
86
|
+
if not ok:
|
|
87
|
+
print("AI Network Blocker is already running.")
|
|
88
|
+
sys.exit(0)
|
|
89
|
+
|
|
90
|
+
if not is_admin():
|
|
91
|
+
relaunch_as_admin()
|
|
92
|
+
|
|
93
|
+
detected_lang = detect_system_language()
|
|
94
|
+
s = STRINGS[detected_lang]
|
|
95
|
+
|
|
96
|
+
root_temp = tk.Tk()
|
|
97
|
+
root_temp.withdraw()
|
|
98
|
+
messagebox.showerror(
|
|
99
|
+
s["admin_required_title"],
|
|
100
|
+
s["admin_required_msg"]
|
|
101
|
+
)
|
|
102
|
+
root_temp.destroy()
|
|
103
|
+
sys.exit(1)
|
|
104
|
+
|
|
105
|
+
root = tk.Tk()
|
|
106
|
+
AIBlockerApp(root)
|
|
107
|
+
if "--minimized" in sys.argv and CURRENT_OS == "Windows":
|
|
108
|
+
root.withdraw()
|
|
109
|
+
root.mainloop()
|
|
110
|
+
|
|
111
|
+
if __name__ == "__main__":
|
|
112
|
+
main()
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
import os
|
|
3
|
+
import subprocess
|
|
4
|
+
|
|
5
|
+
from ai_blocker.constants import BLOCKLIST, COMMENT_TAG, CURRENT_OS, HOSTS_PATH, PROCESS_LIST
|
|
6
|
+
from ai_blocker.i18n import STRINGS
|
|
7
|
+
from ai_blocker.system_utils import _get_subprocess_kwargs, flush_dns
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def force_close_processes():
|
|
11
|
+
closed = []
|
|
12
|
+
kwargs = _get_subprocess_kwargs()
|
|
13
|
+
|
|
14
|
+
try:
|
|
15
|
+
if CURRENT_OS == "Windows":
|
|
16
|
+
args = ["taskkill", "/F"]
|
|
17
|
+
for proc in PROCESS_LIST:
|
|
18
|
+
args.extend(["/IM", proc])
|
|
19
|
+
result = subprocess.run(args, capture_output=True, text=True, **kwargs)
|
|
20
|
+
out_lower = result.stdout.lower()
|
|
21
|
+
for proc in PROCESS_LIST:
|
|
22
|
+
if f'"{proc.lower()}"' in out_lower or f'{proc.lower()}' in out_lower:
|
|
23
|
+
closed.append(proc.replace(".exe", ""))
|
|
24
|
+
else:
|
|
25
|
+
active = detect_running_ai_editors()
|
|
26
|
+
if active:
|
|
27
|
+
args = ["killall"] + active
|
|
28
|
+
subprocess.run(args, capture_output=True, text=True, **kwargs)
|
|
29
|
+
closed = active
|
|
30
|
+
except Exception:
|
|
31
|
+
pass
|
|
32
|
+
return closed
|
|
33
|
+
|
|
34
|
+
def detect_running_ai_editors():
|
|
35
|
+
running = []
|
|
36
|
+
kwargs = _get_subprocess_kwargs()
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
if CURRENT_OS == "Windows":
|
|
40
|
+
result = subprocess.run(["tasklist", "/NH"], capture_output=True, text=True, **kwargs)
|
|
41
|
+
out_lower = result.stdout.lower()
|
|
42
|
+
for proc in PROCESS_LIST:
|
|
43
|
+
if proc.lower() in out_lower:
|
|
44
|
+
running.append(proc.replace(".exe", ""))
|
|
45
|
+
else:
|
|
46
|
+
result = subprocess.run(["ps", "-A", "-o", "comm="], capture_output=True, text=True, **kwargs)
|
|
47
|
+
out_lines = result.stdout.splitlines()
|
|
48
|
+
active = set()
|
|
49
|
+
for line in out_lines:
|
|
50
|
+
active.add(os.path.basename(line.strip()).lower())
|
|
51
|
+
|
|
52
|
+
for proc in PROCESS_LIST:
|
|
53
|
+
if proc.lower() in active:
|
|
54
|
+
running.append(proc)
|
|
55
|
+
except Exception:
|
|
56
|
+
pass
|
|
57
|
+
return running
|
|
58
|
+
|
|
59
|
+
def activate_block(lang, categories_to_block=None):
|
|
60
|
+
if categories_to_block is None:
|
|
61
|
+
categories_to_block = list(BLOCKLIST.keys())
|
|
62
|
+
|
|
63
|
+
closed_list = force_close_processes()
|
|
64
|
+
s = STRINGS[lang]
|
|
65
|
+
|
|
66
|
+
try:
|
|
67
|
+
existing_lines = []
|
|
68
|
+
if os.path.exists(HOSTS_PATH):
|
|
69
|
+
with open(HOSTS_PATH, "r", encoding="utf-8") as f:
|
|
70
|
+
existing_lines = f.readlines()
|
|
71
|
+
|
|
72
|
+
cleaned_lines = [l for l in existing_lines if COMMENT_TAG not in l]
|
|
73
|
+
|
|
74
|
+
if cleaned_lines and not cleaned_lines[-1].endswith("\n"):
|
|
75
|
+
cleaned_lines[-1] += "\n"
|
|
76
|
+
|
|
77
|
+
new_entries = []
|
|
78
|
+
added_count = 0
|
|
79
|
+
|
|
80
|
+
domains_to_block = []
|
|
81
|
+
for cat in categories_to_block:
|
|
82
|
+
if cat in BLOCKLIST:
|
|
83
|
+
for domain in BLOCKLIST[cat]:
|
|
84
|
+
if domain not in domains_to_block:
|
|
85
|
+
domains_to_block.append(domain)
|
|
86
|
+
|
|
87
|
+
for domain in domains_to_block:
|
|
88
|
+
entry = f"127.0.0.1 {domain} {COMMENT_TAG}\n"
|
|
89
|
+
new_entries.append(entry)
|
|
90
|
+
added_count += 1
|
|
91
|
+
|
|
92
|
+
with open(HOSTS_PATH, "w", encoding="utf-8") as f:
|
|
93
|
+
f.writelines(cleaned_lines + new_entries)
|
|
94
|
+
|
|
95
|
+
flush_dns()
|
|
96
|
+
|
|
97
|
+
if closed_list:
|
|
98
|
+
process_details = f"{s['closed_processes_prefix']}{', '.join(closed_list)}"
|
|
99
|
+
else:
|
|
100
|
+
process_details = s["no_processes_detected"]
|
|
101
|
+
|
|
102
|
+
msg = s["block_success_msg"].format(
|
|
103
|
+
added_count=added_count,
|
|
104
|
+
process_details=process_details
|
|
105
|
+
)
|
|
106
|
+
return True, msg
|
|
107
|
+
|
|
108
|
+
except PermissionError:
|
|
109
|
+
return False, s["hosts_write_error_msg"]
|
|
110
|
+
except Exception as e:
|
|
111
|
+
return False, s["unexpected_error_msg"].format(error=str(e))
|
|
112
|
+
|
|
113
|
+
def deactivate_block(lang):
|
|
114
|
+
s = STRINGS[lang]
|
|
115
|
+
try:
|
|
116
|
+
if not os.path.exists(HOSTS_PATH):
|
|
117
|
+
return True, s["unblock_success_msg"].format(removed=0)
|
|
118
|
+
|
|
119
|
+
with open(HOSTS_PATH, "r", encoding="utf-8") as f:
|
|
120
|
+
lines = f.readlines()
|
|
121
|
+
|
|
122
|
+
original_count = len(lines)
|
|
123
|
+
cleaned = [l for l in lines if COMMENT_TAG not in l]
|
|
124
|
+
removed = original_count - len(cleaned)
|
|
125
|
+
|
|
126
|
+
with open(HOSTS_PATH, "w", encoding="utf-8") as f:
|
|
127
|
+
f.writelines(cleaned)
|
|
128
|
+
|
|
129
|
+
flush_dns()
|
|
130
|
+
|
|
131
|
+
msg = s["unblock_success_msg"].format(removed=removed)
|
|
132
|
+
return True, msg
|
|
133
|
+
|
|
134
|
+
except PermissionError:
|
|
135
|
+
return False, s["hosts_write_error_msg"]
|
|
136
|
+
except Exception as e:
|
|
137
|
+
return False, s["unexpected_error_msg"].format(error=str(e))
|
ai_blocker/config.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
from ai_blocker.constants import CURRENT_OS
|
|
7
|
+
|
|
8
|
+
SENSITIVE_CONFIG_KEYS = {"openai_key"}
|
|
9
|
+
|
|
10
|
+
def get_config_path():
|
|
11
|
+
if CURRENT_OS == "Windows":
|
|
12
|
+
base_dir = os.environ.get("APPDATA", os.path.expanduser("~"))
|
|
13
|
+
else:
|
|
14
|
+
base_dir = os.path.expanduser("~/.config")
|
|
15
|
+
app_dir = os.path.join(base_dir, "AI-Blocker")
|
|
16
|
+
try:
|
|
17
|
+
os.makedirs(app_dir, exist_ok=True)
|
|
18
|
+
except Exception:
|
|
19
|
+
pass
|
|
20
|
+
return os.path.join(app_dir, "config.json")
|
|
21
|
+
|
|
22
|
+
def load_config():
|
|
23
|
+
path = get_config_path()
|
|
24
|
+
if os.path.exists(path):
|
|
25
|
+
try:
|
|
26
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
27
|
+
data = json.load(f)
|
|
28
|
+
for key in SENSITIVE_CONFIG_KEYS:
|
|
29
|
+
data.pop(key, None)
|
|
30
|
+
return data
|
|
31
|
+
except Exception:
|
|
32
|
+
pass
|
|
33
|
+
return {}
|
|
34
|
+
|
|
35
|
+
def save_config(config_data):
|
|
36
|
+
path = get_config_path()
|
|
37
|
+
try:
|
|
38
|
+
safe_config = dict(config_data)
|
|
39
|
+
for key in SENSITIVE_CONFIG_KEYS:
|
|
40
|
+
safe_config.pop(key, None)
|
|
41
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
42
|
+
json.dump(safe_config, f, indent=4, ensure_ascii=False)
|
|
43
|
+
except Exception:
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
def set_windows_autostart(enabled=True):
|
|
47
|
+
if CURRENT_OS != "Windows":
|
|
48
|
+
return False
|
|
49
|
+
import winreg
|
|
50
|
+
key_path = r"Software\Microsoft\Windows\CurrentVersion\Run"
|
|
51
|
+
try:
|
|
52
|
+
# Determine script/executable command line
|
|
53
|
+
if getattr(sys, 'frozen', False):
|
|
54
|
+
cmd = f'"{sys.executable}" --minimized'
|
|
55
|
+
else:
|
|
56
|
+
# When modularized, the root file is still ai_blocker.py
|
|
57
|
+
# But let's check: if we are running as module, we need to run main file
|
|
58
|
+
root_file = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "ai_blocker.py"))
|
|
59
|
+
cmd = f'"{sys.executable}" "{root_file}" --minimized'
|
|
60
|
+
|
|
61
|
+
key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, key_path, 0, winreg.KEY_SET_VALUE)
|
|
62
|
+
if enabled:
|
|
63
|
+
winreg.SetValueEx(key, "AIBlocker", 0, winreg.REG_SZ, cmd)
|
|
64
|
+
else:
|
|
65
|
+
try:
|
|
66
|
+
winreg.DeleteValue(key, "AIBlocker")
|
|
67
|
+
except FileNotFoundError:
|
|
68
|
+
pass
|
|
69
|
+
winreg.CloseKey(key)
|
|
70
|
+
return True
|
|
71
|
+
except Exception:
|
|
72
|
+
return False
|
|
73
|
+
|
|
74
|
+
def get_windows_autostart():
|
|
75
|
+
if CURRENT_OS != "Windows":
|
|
76
|
+
return False
|
|
77
|
+
import winreg
|
|
78
|
+
key_path = r"Software\Microsoft\Windows\CurrentVersion\Run"
|
|
79
|
+
try:
|
|
80
|
+
key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, key_path, 0, winreg.KEY_READ)
|
|
81
|
+
val, _ = winreg.QueryValueEx(key, "AIBlocker")
|
|
82
|
+
winreg.CloseKey(key)
|
|
83
|
+
return True
|
|
84
|
+
except Exception:
|
|
85
|
+
return False
|
ai_blocker/constants.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
import os
|
|
3
|
+
import platform
|
|
4
|
+
|
|
5
|
+
CURRENT_OS = platform.system() # 'Windows', 'Linux', 'Darwin'
|
|
6
|
+
|
|
7
|
+
BLOCKLIST = {
|
|
8
|
+
"OpenAI": [
|
|
9
|
+
"api.openai.com", "chatgpt.com", "chat.openai.com",
|
|
10
|
+
"platform.openai.com", "openai.com", "auth.openai.com",
|
|
11
|
+
"oaistatic.com", "oaiusercontent.com", "labs.openai.com",
|
|
12
|
+
],
|
|
13
|
+
"Anthropic": [
|
|
14
|
+
"api.anthropic.com", "claude.ai", "anthropic.com",
|
|
15
|
+
"claudeusercontent.com",
|
|
16
|
+
],
|
|
17
|
+
"GitHub Copilot": [
|
|
18
|
+
"api.githubcopilot.com", "copilot.github.com",
|
|
19
|
+
"githubcopilot.com", "api.individual.githubcopilot.com",
|
|
20
|
+
],
|
|
21
|
+
"Google AI": [
|
|
22
|
+
"generativelanguage.googleapis.com", "aistudio.google.com",
|
|
23
|
+
"gemini.google.com", "ai.google.dev",
|
|
24
|
+
],
|
|
25
|
+
"Meta AI": [
|
|
26
|
+
"meta.ai", "ai.meta.com",
|
|
27
|
+
],
|
|
28
|
+
"Mistral AI": [
|
|
29
|
+
"api.mistral.ai", "mistral.ai",
|
|
30
|
+
],
|
|
31
|
+
"Microsoft Copilot": [
|
|
32
|
+
"copilot.microsoft.com", "bing.com", "edgeservices.bing.com",
|
|
33
|
+
],
|
|
34
|
+
"DeepSeek": [
|
|
35
|
+
"deepseek.com", "api.deepseek.com",
|
|
36
|
+
],
|
|
37
|
+
"xAI": [
|
|
38
|
+
"api.x.ai", "grok.x.ai", "x.ai",
|
|
39
|
+
],
|
|
40
|
+
"Otros": [
|
|
41
|
+
"api.perplexity.ai", "perplexity.ai",
|
|
42
|
+
"app.wordware.ai",
|
|
43
|
+
],
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
CATEGORY_ICONS = {
|
|
47
|
+
"OpenAI": "🟢", "Anthropic": "🟠", "GitHub Copilot": "🐙",
|
|
48
|
+
"Google AI": "🔵", "Meta AI": "🔷", "Mistral AI": "🌊",
|
|
49
|
+
"Microsoft Copilot": "🟦", "DeepSeek": "🔮", "xAI": "🤖", "Otros": "📦",
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if CURRENT_OS == "Windows":
|
|
53
|
+
PROCESS_LIST = [
|
|
54
|
+
"Code.exe", "Cursor.exe", "Windsurf.exe", "Claude.exe",
|
|
55
|
+
"Trae.exe", "Cline.exe", "Roo.exe", "Augment.exe",
|
|
56
|
+
]
|
|
57
|
+
else:
|
|
58
|
+
PROCESS_LIST = [
|
|
59
|
+
"code", "cursor", "windsurf", "claude",
|
|
60
|
+
"trae", "cline", "roo", "augment",
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
COMMENT_TAG = "# AI-Block"
|
|
64
|
+
|
|
65
|
+
if CURRENT_OS == "Windows":
|
|
66
|
+
HOSTS_PATH = os.path.join(
|
|
67
|
+
os.environ.get("SystemRoot", r"C:\Windows"),
|
|
68
|
+
r"System32\drivers\etc\hosts",
|
|
69
|
+
)
|
|
70
|
+
else:
|
|
71
|
+
HOSTS_PATH = "/etc/hosts"
|
|
72
|
+
|
|
73
|
+
# Catppuccin Mocha colors
|
|
74
|
+
COL_BASE = "#1E1E2E"
|
|
75
|
+
COL_SURFACE0 = "#313244"
|
|
76
|
+
COL_SURFACE1 = "#45475A"
|
|
77
|
+
COL_TEXT = "#CDD6F4"
|
|
78
|
+
COL_SUBTEXT = "#A6ADC8"
|
|
79
|
+
COL_RED = "#F38BA8"
|
|
80
|
+
COL_GREEN = "#A6E3A1"
|
|
81
|
+
COL_YELLOW = "#F9E2AF"
|
|
82
|
+
COL_BLUE = "#89B4FA"
|
|
83
|
+
COL_MAUVE = "#CBA6F7"
|
|
84
|
+
|
|
85
|
+
def _get_ui_font():
|
|
86
|
+
if CURRENT_OS == "Windows":
|
|
87
|
+
return "Segoe UI"
|
|
88
|
+
elif CURRENT_OS == "Darwin":
|
|
89
|
+
return "Helvetica Neue"
|
|
90
|
+
else:
|
|
91
|
+
return "DejaVu Sans"
|
|
92
|
+
|
|
93
|
+
UI_FONT = _get_ui_font()
|
ai_blocker/gateway.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
import urllib.error
|
|
3
|
+
import urllib.request
|
|
4
|
+
from http.server import BaseHTTPRequestHandler
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class GatewayHandler(BaseHTTPRequestHandler):
|
|
8
|
+
def do_GET(self):
|
|
9
|
+
self._proxy_request("GET")
|
|
10
|
+
|
|
11
|
+
def do_POST(self):
|
|
12
|
+
self._proxy_request("POST")
|
|
13
|
+
|
|
14
|
+
def do_OPTIONS(self):
|
|
15
|
+
self._proxy_request("OPTIONS")
|
|
16
|
+
|
|
17
|
+
def _proxy_request(self, method):
|
|
18
|
+
target = self.server.target_url.rstrip("/") + self.path
|
|
19
|
+
headers = {}
|
|
20
|
+
for k, v in self.headers.items():
|
|
21
|
+
if k.lower() not in ['host', 'accept-encoding']:
|
|
22
|
+
headers[k] = v
|
|
23
|
+
|
|
24
|
+
data = None
|
|
25
|
+
if method in ['POST', 'PUT', 'PATCH']:
|
|
26
|
+
content_length = int(self.headers.get('Content-Length', 0))
|
|
27
|
+
if content_length > 0:
|
|
28
|
+
data = self.rfile.read(content_length)
|
|
29
|
+
|
|
30
|
+
req = urllib.request.Request(target, data=data, headers=headers, method=method)
|
|
31
|
+
try:
|
|
32
|
+
with urllib.request.urlopen(req, timeout=30) as response:
|
|
33
|
+
self.send_response(response.status)
|
|
34
|
+
for k, v in response.headers.items():
|
|
35
|
+
if k.lower() not in ['transfer-encoding']:
|
|
36
|
+
self.send_header(k, v)
|
|
37
|
+
self.end_headers()
|
|
38
|
+
|
|
39
|
+
while True:
|
|
40
|
+
chunk = response.read(1024)
|
|
41
|
+
if not chunk:
|
|
42
|
+
break
|
|
43
|
+
self.wfile.write(chunk)
|
|
44
|
+
self.wfile.flush()
|
|
45
|
+
except urllib.error.HTTPError as e:
|
|
46
|
+
self.send_response(e.code)
|
|
47
|
+
for k, v in e.headers.items():
|
|
48
|
+
if k.lower() not in ['transfer-encoding']:
|
|
49
|
+
self.send_header(k, v)
|
|
50
|
+
self.end_headers()
|
|
51
|
+
self.wfile.write(e.read())
|
|
52
|
+
except Exception as e:
|
|
53
|
+
self.send_response(502)
|
|
54
|
+
self.end_headers()
|
|
55
|
+
self.wfile.write(f"Gateway Error: {e}".encode())
|
ai_blocker/i18n.py
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
import json
|
|
3
|
+
import locale
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
from ai_blocker.constants import CURRENT_OS
|
|
8
|
+
|
|
9
|
+
if CURRENT_OS == "Windows":
|
|
10
|
+
import ctypes
|
|
11
|
+
|
|
12
|
+
LANG_DISPLAY_MAP = {
|
|
13
|
+
"English": "en",
|
|
14
|
+
"Español": "es",
|
|
15
|
+
"Português": "pt",
|
|
16
|
+
"Français": "fr",
|
|
17
|
+
"Deutsch": "de",
|
|
18
|
+
"Italiano": "it",
|
|
19
|
+
"Русский": "ru",
|
|
20
|
+
"中文 (简体)": "zh",
|
|
21
|
+
"日本語": "ja",
|
|
22
|
+
"한국어": "ko"
|
|
23
|
+
}
|
|
24
|
+
LANG_CODE_MAP = {v: k for k, v in LANG_DISPLAY_MAP.items()}
|
|
25
|
+
|
|
26
|
+
CATEGORY_TRANSLATIONS = {}
|
|
27
|
+
STRINGS = {}
|
|
28
|
+
|
|
29
|
+
def load_translations():
|
|
30
|
+
global CATEGORY_TRANSLATIONS, STRINGS
|
|
31
|
+
try:
|
|
32
|
+
# Load relative to package or executable directory
|
|
33
|
+
if getattr(sys, 'frozen', False):
|
|
34
|
+
base_dir = os.path.dirname(sys.executable)
|
|
35
|
+
else:
|
|
36
|
+
base_dir = os.path.dirname(os.path.abspath(__file__))
|
|
37
|
+
|
|
38
|
+
trans_path = os.path.join(base_dir, "translations.json")
|
|
39
|
+
|
|
40
|
+
# Fallback to parent directory if in development subfolder
|
|
41
|
+
if not os.path.exists(trans_path):
|
|
42
|
+
trans_path = os.path.join(os.path.dirname(base_dir), "translations.json")
|
|
43
|
+
|
|
44
|
+
# Try PyInstaller temporary folder fallback (if bundled as data file)
|
|
45
|
+
if not os.path.exists(trans_path) and hasattr(sys, '_MEIPASS'):
|
|
46
|
+
trans_path = os.path.join(sys._MEIPASS, "translations.json")
|
|
47
|
+
|
|
48
|
+
# Fallback if still not found
|
|
49
|
+
if not os.path.exists(trans_path):
|
|
50
|
+
trans_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "translations.json")
|
|
51
|
+
|
|
52
|
+
with open(trans_path, "r", encoding="utf-8") as f:
|
|
53
|
+
data = json.load(f)
|
|
54
|
+
CATEGORY_TRANSLATIONS = data["category_translations"]
|
|
55
|
+
STRINGS = data["strings"]
|
|
56
|
+
except Exception as e:
|
|
57
|
+
print(f"Warning: Failed to load translations.json: {e}")
|
|
58
|
+
# Minimal hardcoded fallback
|
|
59
|
+
CATEGORY_TRANSLATIONS = {"en": {"OpenAI": "OpenAI", "Anthropic": "Anthropic", "GitHub Copilot": "GitHub Copilot", "Google AI": "Google AI", "Meta AI": "Meta AI", "Mistral AI": "Mistral AI", "Microsoft Copilot": "Microsoft Copilot", "DeepSeek": "DeepSeek", "xAI": "xAI", "Otros": "Others"}}
|
|
60
|
+
STRINGS = {"en": {
|
|
61
|
+
"protected_title": "PROTECTED — Blocking active",
|
|
62
|
+
"protected_desc": "{count} domains redirected to 127.0.0.1",
|
|
63
|
+
"exposed_title": "EXPOSED — No protection",
|
|
64
|
+
"exposed_desc": "Your network traffic to AI is open",
|
|
65
|
+
"btn_block": "🔒 BLOCK AI",
|
|
66
|
+
"btn_unblock": "🔓 UNBLOCK AI",
|
|
67
|
+
"busy_text": "⏳ Processing...",
|
|
68
|
+
"categories_title": "Blocked Categories",
|
|
69
|
+
"domains_label": "{total} domains",
|
|
70
|
+
"running_warning": "⚠ Detected: {editors}",
|
|
71
|
+
"hosts_write_error_title": "Permission Error",
|
|
72
|
+
"hosts_write_error_msg": "Could not write to the hosts file.\nPlease run the application as Administrator.",
|
|
73
|
+
"unexpected_error_title": "Unexpected Error",
|
|
74
|
+
"unexpected_error_msg": "An error occurred:\n{error}",
|
|
75
|
+
"block_success_title": "AI Blocking Active",
|
|
76
|
+
"block_success_msg": "Block successfully activated!\n\n✓ {added_count} domains blocked in hosts file.\n{process_details}\n✓ DNS cache flushed.",
|
|
77
|
+
"unblock_success_title": "AI Blocking Disabled",
|
|
78
|
+
"unblock_success_msg": "Block successfully deactivated!\n\n✓ {removed} entries removed from hosts file.\n✓ DNS cache flushed.\n\nAll AI tools can access the network again.",
|
|
79
|
+
"closed_processes_prefix": "✓ Closed processes: ",
|
|
80
|
+
"no_processes_detected": "• No open AI editors detected.",
|
|
81
|
+
"admin_required_title": "Access Denied",
|
|
82
|
+
"admin_required_msg_windows": "Administrator privileges are required.\n\nRight-click → 'Run as administrator'.",
|
|
83
|
+
"admin_required_msg_unix": "Root privileges are required.\n\nRun with: sudo python3 ai_blocker.py",
|
|
84
|
+
"profile_work": "Work",
|
|
85
|
+
"profile_personal": "Personal",
|
|
86
|
+
"profile_free": "Free",
|
|
87
|
+
"profile_custom": "Custom"
|
|
88
|
+
}}
|
|
89
|
+
|
|
90
|
+
# Resolve platform specific strings
|
|
91
|
+
for lang in STRINGS:
|
|
92
|
+
if CURRENT_OS == "Windows":
|
|
93
|
+
STRINGS[lang]["admin_required_msg"] = STRINGS[lang].get("admin_required_msg_windows", "")
|
|
94
|
+
else:
|
|
95
|
+
STRINGS[lang]["admin_required_msg"] = STRINGS[lang].get("admin_required_msg_unix", "")
|
|
96
|
+
|
|
97
|
+
def detect_system_language():
|
|
98
|
+
# 1. Environment variables
|
|
99
|
+
for env_var in ('LANGUAGE', 'LC_ALL', 'LC_CTYPE', 'LANG'):
|
|
100
|
+
val = os.environ.get(env_var)
|
|
101
|
+
if val:
|
|
102
|
+
code = val.split('_')[0].split('.')[0].lower()
|
|
103
|
+
if code in STRINGS:
|
|
104
|
+
return code
|
|
105
|
+
|
|
106
|
+
# 2. locale.getlocale()
|
|
107
|
+
try:
|
|
108
|
+
if hasattr(locale, 'getlocale'):
|
|
109
|
+
lang, _ = locale.getlocale()
|
|
110
|
+
if lang:
|
|
111
|
+
code = lang.split('_')[0].lower()
|
|
112
|
+
if code in STRINGS:
|
|
113
|
+
return code
|
|
114
|
+
except Exception:
|
|
115
|
+
pass
|
|
116
|
+
|
|
117
|
+
# Windows LCID query
|
|
118
|
+
if CURRENT_OS == "Windows":
|
|
119
|
+
try:
|
|
120
|
+
lcid = ctypes.windll.kernel32.GetUserDefaultUILanguage()
|
|
121
|
+
lcid_map = {
|
|
122
|
+
1034: "es", 2058: "es", 3082: "es", 4106: "es", 5130: "es", 6154: "es",
|
|
123
|
+
1046: "pt", 2070: "pt",
|
|
124
|
+
1036: "fr", 2060: "fr", 3084: "fr",
|
|
125
|
+
1031: "de", 2055: "de",
|
|
126
|
+
1040: "it",
|
|
127
|
+
1049: "ru",
|
|
128
|
+
2052: "zh", 1028: "zh", 3076: "zh",
|
|
129
|
+
1041: "ja",
|
|
130
|
+
1042: "ko",
|
|
131
|
+
}
|
|
132
|
+
for k, v in lcid_map.items():
|
|
133
|
+
if lcid == k or (lcid & 0x3FF) == (k & 0x3FF):
|
|
134
|
+
return v
|
|
135
|
+
except Exception:
|
|
136
|
+
pass
|
|
137
|
+
|
|
138
|
+
return "en"
|
|
139
|
+
|
|
140
|
+
# Run initial load on import
|
|
141
|
+
load_translations()
|