├── .dockerignore ├── .env.example ├── .gitignore ├── CHANGELOG.md ├── Dockerfile ├── README.md ├── docker-compose.yml ├── hosts ├── __init__.py ├── _example.py ├── default.py └── tests.py ├── proxy.py ├── proxy.sh ├── requirements.txt └── tests ├── default.py └── tests.py /.dockerignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | .git 3 | .DS_Store 4 | .vscode 5 | .idea 6 | .pytest_cache -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | production=false 2 | 3 | authentification=false 4 | username="thomas" 5 | password="123456" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | .idea 3 | .vscode 4 | .DS_Store 5 | .env 6 | .pytest_cache 7 | .certs 8 | pytest.ini 9 | 10 | hosts/* 11 | !hosts/__init__.py 12 | !hosts/default.py 13 | !hosts/_example.py 14 | !hosts/tests.py 15 | 16 | tests/* 17 | !tests/default.py 18 | !tests/tests.py -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [0.0.3] - 2022-12-11 4 | 5 | ### Changed 6 | 7 | - Déplacement de la logiqe de chargement des modules dynamiquement de __init__ vers proxy.py 8 | - Remplacement de nodemon par nodemon-py-simple 9 | - Remplacement de l'image docker nikolaik/python-nodejs par python:3.9-slim. Afin de prendre en charge les processeurs arm mais aussi optimiser le temps de build 10 | 11 | 12 | ## [0.0.2] - 2022-12-06 13 | 14 | ### Added 15 | 16 | - Ajout des tests 17 | 18 | ### Changed 19 | 20 | - Déplacement du répertoires des certificats dans .certs 21 | - Ajout du fichier requirements.txt pour les dépendances python 22 | 23 | ## [0.0.1] - 2022-12-06 24 | 25 | ### Added 26 | 27 | - Changelog.md pour garder les mises à jours 28 | - .env afin d'avoir des variables plus globales 29 | - Ajout de la possiblité de mettre une authentification 30 | 31 | ### Changed 32 | 33 | - Changement du point d'entrée, docker-entrypoint.sh -> proxy.sh 34 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.9-slim 2 | 3 | EXPOSE 8301 4 | EXPOSE 8302 5 | 6 | VOLUME /proxy 7 | WORKDIR /proxy 8 | 9 | ADD requirements.txt /proxy/ 10 | 11 | RUN apt update 12 | RUN pip install --no-cache --upgrade pip setuptools 13 | RUN pip install -r requirements.txt 14 | 15 | 16 | ENTRYPOINT ["./proxy.sh"] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Proxeditor 2 | ### Proxy qui permet de supprimer les pubs et de modifier les requêtes des sites facilement 3 | 4 | À la base j'avais crée ce proxy pour mes parents, je m'étais rendu compte que pour des personnes n'utilisant pas forcément beaucoup la technologie. Toutes les popups, demandes de cookies, d'authentification... Rendait l'usage d'une tablette très difficile ou même des sites en général. 5 | 6 | Personnellement, j'ai mis en place ce proxy sur un raspberry local avec pas mal d'utilisations diverses : 7 | 8 | - Avoir des applications en version premium 9 | - Modifier les réponses de certains sites pour ajouter des balises *script* afin d'améliorer la navigation et par example automatiquement se connecter aux comptes, rediriger vers la bonne page, supprimer des éléments supperflus... 10 | - Également, modifier les réponses des sites pour ajouter des balises *style* pour rendre les sites plus ergonomiques (augmenter les polices, darkmode, viewport...) 11 | 12 | Afin de respecter les sites en question, je ne publierais pas les sites modifiés et le type de modification dans le répertoire **hosts/**. 13 | 14 | Mais le fichier _example.py est assez explicite et simple à comprendre, c'est la base que j'utilise pour chaque site. 15 | 16 |
17 | 18 | 1. La commande pour lancer docker : 19 | ``` 20 | docker compose up 21 | OU 22 | docker compose up -d // En background 23 | ``` 24 | 25 | ⭐️ Le docker intègre les DNS de [AdGuard](https://adguard-dns.io/) pour supprimer toutes les pubs, même dans les applications 26 | 27 | Ou alors le lancer directement : 28 | ``` 29 | pip install mitmproxy 30 | pip install tldextract 31 | 32 | chmod a+x proxy.sh 33 | ./proxy.sh 34 | ``` 35 | 36 | Les ports accessibles : 37 | 38 | - Le proxy : **8302** 39 | - L'interface en mode développement : **8301** 40 | 41 | 2. Se rendre dans les paramètres de son device et mettre l'ip du serveur ainsi que le port. 42 | 43 | 3. Se rendre sur http://mitm.it/ et récupérer le certificat 44 | 45 | 4. Ajouter le certificat dans les paramètres pour lui donner autorité. 46 | 47 | - IOS : Réglages > Général > VPN et gestion de l'appareil > mitmproxy > Installer. 48 | 49 | - Réglages > Général > Informations > Réglages des certificats (tout en bas) > Cocher mitmproxy > Continuer. 50 | 51 | - Android : Paramètres > Sécurité > Chiffrement et identifiants > Installer un certicat > Certificat CA > Installer quand même. 52 | 53 | # Sécurité 54 | 55 | 1. Renommer le fichier *.env.example* -> *.env* 56 | 2. Passer l'*authentification=true* et modifier la valeur de *username* et *password* 57 | ```sh 58 | authentification=true 59 | username="thomas" 60 | password="123456" 61 | ``` 62 | 63 | 64 | # Processus 65 | 66 | 1. Passer le proxy en mode ouvert (c'est à dire qu'il va tout intercepter au lieu de n'intercepter que les domaines définis) 67 | ``` 68 | // hosts/default.py:18 69 | 70 | Remplacer 71 | data.ignore_connection = True 72 | 73 | Par 74 | data.ignore_connection = False 75 | ``` 76 | 77 | 2. Copier le fichier **_example.py** dans le répertoire **hosts/** 78 | 79 | 3. Renommer la classe (Du même nom que le fichier pour s'y retrouver) 80 | 81 | 4. Pour le debug le process 82 | - Passer l'environnement en mode *production=false* dans le fichier *.env* 83 | - Se rendre sur l'interface http://127.0.0.1:8301 84 | - Et pour rendre ça encore plus agréable j'utlise [cette extension](https://chrome.google.com/webstore/detail/user-javascript-and-css/nbhcbdghjpllgmfilhnhkllmkecfmpld) 85 | - Chargée avec ce script qui permet de recharger la page à chaque fois que le proxy redémarre (il est moche mais il marche) : 86 | 87 | ```js 88 | const ws = new WebSocket(`ws://${window.location.host}/updates`); 89 | let inReload = false; 90 | 91 | ws.addEventListener('open', () => { 92 | setTimeout(() => { 93 | document.querySelectorAll('.nav-tabs a')[2].click(); 94 | setTimeout(() => { 95 | if(!document.querySelectorAll('.menu-content input')[3].checked) { 96 | document.querySelectorAll('.menu-content input')[3].click(); 97 | setTimeout(() => { 98 | document.querySelectorAll('.eventlog .btn-primary').forEach((btn) => btn.click()); 99 | document.querySelectorAll('.eventlog .btn')[4].click(); 100 | }, 10) 101 | } 102 | }, 10); 103 | }, 100); 104 | }); 105 | 106 | ws.addEventListener('close', () => { 107 | setInterval(() => { 108 | new WebSocket(`ws://${window.location.host}/updates`).addEventListener('open', () => reload()); 109 | }, 100); 110 | }); 111 | 112 | const reload = () => { 113 | if(inReload) return; 114 | inReload = true; 115 | window.location.href = "http://" + window.location.host; 116 | } 117 | ``` 118 | 119 | 120 | 5. Envoyer des messages de sortie avec *logging.error()* depuis le proxy pour bien les discerner car la console défile trop vite 121 | 122 | 6. Ne pas oublier de refaire l'étape 1 dans le sens inverse sinon certains domaines sécurisés avec du SSL Pinning ne pourront pas charger 123 | 124 | --- 125 | 126 | *Je ne suis pas responsable de l'utilisation de ce proxy. Ce proxy est uniquement à usage personnel pour mes parents. Ils sont seuls responsables de son utilisation et s'engagent à ne l'utiliser qu'à des fins légitimes et en conformité avec toutes les lois et réglementations applicables. Toute utilisation non autorisée ou illégale de ce proxy sera de votre seule responsabilité. Je décline toute responsabilité en ce qui concerne l'utilisation de ce proxy. Si vous avez des doutes sur l'utilisation légale de ce proxy, veuillez consulter un avocat qualifié.* -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | proxy: 4 | build: . 5 | 6 | working_dir: /proxy 7 | 8 | volumes: 9 | - ./:/proxy/ 10 | 11 | ports: 12 | - 8302:8302 13 | - 8301:8301 14 | 15 | dns: 16 | - 94.140.14.15 17 | - 94.140.14.16 -------------------------------------------------------------------------------- /hosts/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomasync/proxeditor/0c2be170a14c3621d3a069e8df81eb2247468450/hosts/__init__.py -------------------------------------------------------------------------------- /hosts/_example.py: -------------------------------------------------------------------------------- 1 | import mitmproxy 2 | import json 3 | from tldextract import extract 4 | import logging 5 | 6 | HOSTS = [ 7 | 'example.fr', 8 | 'example.org' 9 | ] 10 | 11 | SCRIPT = """ 12 | 19 | """ 20 | 21 | STYLE = """ 22 | 27 | """ 28 | 29 | class Example: 30 | 31 | # Give access to the request 32 | @staticmethod 33 | def tls_clienthello(data: mitmproxy.proxy.layers.tls.ClientHelloData) -> None: 34 | domain = extract(data.context.server.address[0]).registered_domain 35 | if domain in HOSTS: 36 | data.ignore_connection = False 37 | 38 | # Event called when a request is sent 39 | @staticmethod 40 | def request(flow: mitmproxy.http.HTTPFlow) -> None: 41 | domain = extract(flow.request.host).registered_domain 42 | if domain not in HOSTS: 43 | return 44 | 45 | if "verify_premium" in flow.request.path: 46 | flow.kill() 47 | logging.error("example debug message") 48 | 49 | # Event called when a response is received 50 | @staticmethod 51 | def response(flow: mitmproxy.http.HTTPFlow) -> None: 52 | domain = extract(flow.request.host).registered_domain 53 | if domain not in HOSTS or flow.response.content == '': 54 | return 55 | 56 | if "article" in flow.request.path: 57 | response = json.loads(flow.response.content) 58 | response["response"]["article"]["unlocked"] = "1" 59 | flow.response.content = json.dumps(response).encode() 60 | 61 | elif "user" in flow.request.path: 62 | response = json.loads(flow.response.content) 63 | response["response"]["user"]["category"] = "premium" 64 | response["response"]["user"]["subscription"]["isPremium"] = "true" 65 | flow.response.content = json.dumps(response).encode() 66 | 67 | elif "login" in flow.request.path: 68 | flow.response.content += (STYLE + SCRIPT).encode(); 69 | -------------------------------------------------------------------------------- /hosts/default.py: -------------------------------------------------------------------------------- 1 | import mitmproxy 2 | from tldextract import extract 3 | 4 | 5 | HOSTS_ADS = ['googleadservices.com', 'taboola.com', 'batch.com', 'googlesyndication.com', 'doubleclick.net', 'xiti.com'] 6 | 7 | class Default: 8 | 9 | @staticmethod 10 | def tls_clienthello(data: mitmproxy.proxy.layers.tls.ClientHelloData) -> None: 11 | domain = extract(data.context.server.address[0]).registered_domain 12 | 13 | if domain in HOSTS_ADS: 14 | data.ignore_connection = False 15 | else: 16 | # Set to false for debug 17 | data.ignore_connection = True 18 | 19 | @staticmethod 20 | def request(flow: mitmproxy.http.HTTPFlow) -> None: 21 | for host in HOSTS_ADS: 22 | domain = extract(flow.request.host).registered_domain 23 | if host in domain: 24 | flow.kill() 25 | -------------------------------------------------------------------------------- /hosts/tests.py: -------------------------------------------------------------------------------- 1 | import mitmproxy 2 | from tldextract import extract 3 | import logging 4 | import re 5 | 6 | HOSTS = [ 7 | 'neverssl.com', 8 | 'github.com' 9 | ] 10 | 11 | PAGE = """ 12 | 13 | 14 | Proxy Works 15 | 16 | 17 |

Proxy Works

18 | 19 | 20 | """ 21 | 22 | class Tests: 23 | 24 | # Give access to the request 25 | @staticmethod 26 | def tls_clienthello(data: mitmproxy.proxy.layers.tls.ClientHelloData) -> None: 27 | domain = extract(data.context.server.address[0]).registered_domain 28 | if domain in HOSTS: 29 | data.ignore_connection = False 30 | 31 | # Event called when a response is received 32 | @staticmethod 33 | def response(flow: mitmproxy.http.HTTPFlow) -> None: 34 | domain = extract(flow.request.host).registered_domain 35 | 36 | if domain not in HOSTS or "proxeditor_pytest" not in flow.request.path: 37 | return 38 | 39 | logging.error("Domain: " + domain) 40 | 41 | if "neverssl.com" == domain and "online/create" in flow.request.path: 42 | flow.response.status_code = 200 43 | flow.response.content = PAGE.encode() 44 | 45 | elif "neverssl.com" == domain and "online" in flow.request.path: 46 | flow.response.content = flow.response.content.replace(b'42C0FD', b'43b045') 47 | 48 | elif "neverssl.com" == domain and "redirect" in flow.request.path: 49 | flow.response.status_code = 302 50 | flow.response.headers['Location'] = 'https://neverssl.com/online/' 51 | 52 | elif "github.com"== domain and "thomasync" in flow.request.path: 53 | content = flow.response.content.decode() 54 | content = re.sub(r'(.*?)', r'\1 (Proxy Works!)', content, 0, re.DOTALL) 55 | flow.response.content = content.encode() 56 | -------------------------------------------------------------------------------- /proxy.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | import inspect 3 | import os 4 | 5 | # Get the names of the files in the "hosts" directory 6 | filenames = os.listdir("hosts") 7 | 8 | # Get only the names of the files that end with ".py" 9 | # to keep only the modules 10 | module_filenames = [filename for filename in filenames if filename.endswith(".py") and not filename.startswith("_")] 11 | 12 | # Load the modules from their names 13 | # and reload the modules with importlib.reload() 14 | modules = [] 15 | for filename in module_filenames: 16 | module_name = filename[:-3] # Remove the ".py" extension from the file name 17 | module = importlib.import_module('hosts.' + module_name) 18 | importlib.reload(module) 19 | 20 | if module_name == "default": 21 | modules.insert(0, module) 22 | else: 23 | modules.append(module) 24 | 25 | # Get all the classes in the loaded modules 26 | classes = [] 27 | for module in modules: 28 | # Get all the members (attributes and functions) in the module 29 | members = inspect.getmembers(module) 30 | 31 | # Get only the members that are classes 32 | # using the isclass() function from inspect 33 | class_members = [member for member in members if inspect.isclass(member[1])] 34 | 35 | # Add the classes to the classes list 36 | classes.extend(class_members) 37 | 38 | # Add all the classes to the addons variable 39 | addons = [cls[1]() for cls in classes] -------------------------------------------------------------------------------- /proxy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | export TERM=xterm 4 | 5 | if [ -f ".env" ]; then 6 | export $(grep -v '^#' .env | xargs) 7 | else 8 | echo ".env not found." 9 | production="true" 10 | authentification="false" 11 | fi 12 | 13 | if test "$authentification" = "true"; then 14 | proxyauth="--proxyauth $username:$password" 15 | else 16 | proxyauth="" 17 | fi 18 | 19 | 20 | if test "$production" = "true"; then 21 | echo "Run in production mode." 22 | mitmdump -s proxy.py --listen-host 0.0.0.0 --listen-port 8302 --ssl-insecure --set confdir=./.certs --set block_global=false $proxyauth 23 | else 24 | echo "Run in development mode." 25 | nodemon-py-simple -c hosts -m proxy.py "mitmweb -s proxy.py --listen-host 0.0.0.0 --listen-port 8302 --web-host 0.0.0.0 --web-port 8301 --ssl-insecure --set confdir=./.certs --no-web-open-browser --set block_global=false $proxyauth" 26 | fi -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | mitmproxy 2 | tldextract 3 | selenium 4 | webdriver-manager 5 | pytest 6 | nodemon-py-simple -------------------------------------------------------------------------------- /tests/default.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from selenium import webdriver 3 | from selenium.webdriver.chrome.service import Service 4 | from webdriver_manager.chrome import ChromeDriverManager 5 | 6 | # Before all tests 7 | @pytest.fixture(autouse=True) 8 | def pytest_constructor(): 9 | options = webdriver.ChromeOptions() 10 | options.add_argument('--proxy-server=127.0.0.1:8302') 11 | options.add_argument('--ignore-ssl-errors=yes') 12 | options.add_argument('--ignore-certificate-errors') 13 | options.headless = True 14 | 15 | driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()), options=options) 16 | pytest._driver = driver 17 | 18 | # Verify that the proxy does not require authentication 19 | def test_authentification(): 20 | pytest._driver.get("http://mitm.it") 21 | assert "Proxy Authentication Required" not in pytest._driver.title -------------------------------------------------------------------------------- /tests/tests.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | def test_neverssl_online(): 4 | pytest._driver.get("http://neverssl.com/online/?proxeditor_pytest") 5 | assert "43b045" in pytest._driver.page_source 6 | 7 | def test_neverssl_online_create(): 8 | pytest._driver.get("http://neverssl.com/online/create?proxeditor_pytest") 9 | assert "Proxy Works" in pytest._driver.page_source 10 | 11 | def test_neverssl_redirect(): 12 | pytest._driver.get("http://neverssl.com/redirect?proxeditor_pytest") 13 | assert "https://neverssl.com/online/" in pytest._driver.current_url 14 | 15 | def test_neverssl_online(): 16 | pytest._driver.get("http://neverssl.com/online/?proxeditor_pytest") 17 | assert "43b045" in pytest._driver.page_source 18 | 19 | def test_github_thomasync(): 20 | pytest._driver.get("https://github.com/thomasync?proxeditor_pytest") 21 | assert "Proxy Works" in pytest._driver.title --------------------------------------------------------------------------------