├── app ├── routes.py ├── __init__.py ├── templates │ └── index.html └── static │ ├── css │ └── index.css │ └── js │ └── index.js ├── tests └── test_routes.py ├── .gitattributes ├── requirements.txt ├── Dockerfile ├── docker-compose.yaml ├── .gitignore ├── utils.py ├── README.md ├── main.py ├── core.py └── LICENSE /app/routes.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/test_routes.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | uvicorn==0.29.0 2 | fastapi==0.110.0 3 | jinja2==3.1.3 4 | python-multipart==0.0.9 5 | websockets==12.0 6 | Faker==23.2.1 7 | loguru==0.7.2 8 | starlette==0.36.3 9 | PySocks==1.7.1 -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.11.4-slim-bullseye 2 | 3 | WORKDIR /GetGrassMultiple 4 | 5 | COPY requirements.txt requirements.txt 6 | 7 | RUN pip3 install -r requirements.txt 8 | 9 | COPY . . 10 | 11 | CMD [ "python3", "main.py"] -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3.0" 2 | services: 3 | grass_webui: 4 | build: 5 | dockerfile: Dockerfile 6 | context: . 7 | restart: always 8 | container_name: getgrass_webui 9 | network_mode: bridge 10 | ports: 11 | - "8000:8000" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | .venv 3 | env/ 4 | venv/ 5 | *.pyc 6 | __pycache__ 7 | 8 | 9 | build 10 | .build/ 11 | dist 12 | *.egg-info 13 | 14 | .cache/ 15 | compliance/reports/ 16 | .eggs 17 | .vscode 18 | 19 | # PyCharm 20 | .idea/ 21 | 22 | # Sphinx documentation 23 | docs/build/ -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from urllib.parse import urlparse 3 | 4 | 5 | class Status(Enum): 6 | disconnect = 0 7 | connecting = 1 8 | connected = 2 9 | 10 | 11 | def parse_proxy_url(proxy_url): 12 | parsed_url = urlparse(proxy_url) 13 | 14 | scheme = parsed_url.scheme 15 | host = parsed_url.hostname 16 | port = parsed_url.port 17 | auth = None 18 | 19 | if parsed_url.username and parsed_url.password: 20 | auth = (parsed_url.username, parsed_url.password) 21 | 22 | return scheme, host, port, auth 23 | 24 | 25 | def parse_line(line): 26 | line = line.strip() 27 | if not line: 28 | return None, None 29 | if "==" in line: 30 | user_id, proxy_url = line.split('==') 31 | else: 32 | user_id, proxy_url = line, None 33 | return user_id, proxy_url or None 34 | 35 | 36 | -------------------------------------------------------------------------------- /app/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | GetGrass WebUI 9 | 10 | 11 | 17 | 18 |
19 |
20 |
21 | 22 | 23 | 24 |
25 |
26 | 27 |
28 |
29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 |
NumaraKullanıcı IDVekilDurumİşlem
43 |
44 |
45 |
46 | 47 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /app/static/css/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | max-width: 1100px; 3 | margin: 0 auto; 4 | padding: 20px; 5 | background-color: #f5f5f5; 6 | color: #333333; 7 | } 8 | 9 | .title { 10 | display: flex; 11 | justify-content: space-between; 12 | align-items: center; 13 | padding: 10px; 14 | color: #333333; 15 | } 16 | 17 | button { 18 | max-height: 35px; 19 | color: #ffffff; 20 | background-color: #4caf50; 21 | border: none; 22 | border-radius: 5px; 23 | cursor: pointer; 24 | transition: background-color 0.3s ease; 25 | } 26 | 27 | button:hover { 28 | background-color: #388e3c; 29 | } 30 | 31 | table { 32 | width: 100%; 33 | color: #333333; 34 | background-color: #ffffff; 35 | border-collapse: collapse; 36 | } 37 | 38 | th, td { 39 | text-align: center; 40 | padding: 10px; 41 | border: 1px solid #dddddd; 42 | } 43 | 44 | th { 45 | background-color: #4caf50; 46 | color: #ffffff; 47 | } 48 | 49 | td.proxy { 50 | text-align: left; 51 | } 52 | 53 | .status-0 { color: #f44336; } 54 | .status-1 { color: #ff9800; } 55 | .status-2 { color: #4caf50; } 56 | .status-3 { color: #2196f3; } 57 | 58 | .modal { 59 | display: none; 60 | position: fixed; 61 | z-index: 1; 62 | left: 0; 63 | top: 0; 64 | width: 100%; 65 | height: 100%; 66 | overflow: auto; 67 | background-color: rgba(0, 0, 0, 0.8); 68 | } 69 | 70 | .modal-content { 71 | color: #333333; 72 | background-color: #ffffff; 73 | margin: 15% auto; 74 | padding: 20px; 75 | border-radius: 10px; 76 | box-shadow: 0 4px 8px rgba(0, 0, 0, 0.4); 77 | } 78 | 79 | .close { 80 | color: #333333; 81 | float: right; 82 | font-size: 30px; 83 | font-weight: bold; 84 | cursor: pointer; 85 | } 86 | 87 | .close:hover { 88 | color: #888888; 89 | } 90 | 91 | .loading { 92 | display: none; 93 | position: fixed; 94 | top: 0; 95 | left: 0; 96 | width: 100%; 97 | height: 100%; 98 | background: rgba(255, 255, 255, 0.8); 99 | color: #333333; 100 | text-align: center; 101 | z-index: 9999; 102 | } 103 | 104 | .loading-text { 105 | border-radius: 20px; 106 | color: #000000; 107 | background: #ffffff; 108 | padding: 35px; 109 | display: inline-block; 110 | font-size: 24px; 111 | } 112 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Get Grass With WebUI 2 | 3 | Welcome to GetGrass WebUI, a powerful Python tool designed to help you accumulate grass score across multiple accounts effortlessly. 4 | 5 | ## Features 6 | 7 | - Supports multiple accounts 8 | - Easy to use WebUI 9 | - Proxy support for each account 10 | - Docker support for easy deployment 11 | 12 | ## Running the Application 13 | 14 | ### Directly 15 | 16 | 1. Install the required Python packages: 17 | ```bash 18 | pip3 install -r requirements.txt 19 | ``` 20 | 2. Start the application: 21 | ```bash 22 | python3 main.py 23 | ``` 24 | 3. Open your web browser and visit `http://127.0.0.1:8000`. 25 | 4. Click on "Upload File" and upload your pre-configured `account.txt` file. 26 | 27 | ### With Docker Compose 28 | 29 | 1. Clone the project: 30 | ```bash 31 | git clone https://github.com/Confusion-ymc/GetGrassWebUI.git 32 | ``` 33 | 2. Start the application with Docker Compose: 34 | ```bash 35 | docker compose up --build -d 36 | ``` 37 | 3. Open your web browser and visit `http://{container_ip}:8000`. 38 | 4. Click on "Upload File" and upload your pre-configured `account.txt` file. 39 | 40 | ## `account.txt` File Format 41 | 42 | - Without Proxy Configuration 43 | - Each line represents one account configuration. 44 | - If there's no proxy, the format is simply `user_id` on one line, separated by a dash (`-----`). 45 | - With Proxy Configuration 46 | - If using a proxy, append `==proxy_address` at the end of the line. The format is `user_id==socks5://proxy.com:1080`. 47 | 48 | - For example: 49 | ```text 50 | d1b88fee-8078-46bc-aaed-df7196183f5f==http://proxy.com:8080 51 | d1b88fee-8078-46bc-aaed-df7196183f5f 52 | d1b88fee-8078-46bc-aaed-df7196183f5f==socks5://proxy.com:1080 53 | ``` 54 | 55 | ## Join Us 56 | 57 | Ready to start your journey with GetGrass WebUI? [Sign up](https://app.getgrass.io/register/?referralCode=I03C7kFL7tLZUH6) using our referral link and let's grow together! 58 | 59 | ## Support Us 60 | 61 | Love our work? You can support us by sending your donations to the following Ethereum address: `0xE0CF9bc10C3589A4713A6Ff10e34964c089B0b52`. Your support is greatly appreciated! 62 | 63 | This README provides more detailed information about your project, including how to run the application and the format of the `account.txt` file. It also includes a call to action for users to sign up using your referral link and a request for donations to your Ethereum address. -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import uuid 3 | 4 | import uvicorn 5 | from typing import Dict, Optional 6 | 7 | from fastapi import FastAPI, APIRouter, UploadFile, BackgroundTasks 8 | from fastapi.templating import Jinja2Templates 9 | from loguru import logger 10 | from starlette.requests import Request 11 | from starlette.responses import HTMLResponse 12 | from starlette.staticfiles import StaticFiles 13 | 14 | from utils import parse_line 15 | from core import AsyncGrassWs 16 | 17 | app = FastAPI() 18 | 19 | templates = Jinja2Templates(directory="app/templates") 20 | app.mount("/static", StaticFiles(directory="app/static"), name="static") 21 | 22 | client_router = APIRouter(prefix='/client') 23 | 24 | all_client: Dict[str, AsyncGrassWs] = {} 25 | all_client_ids = [] 26 | 27 | CLIENT_INDEX = 0 28 | 29 | # Or if there are multiple tasks 30 | background_tasks = set() 31 | 32 | 33 | def run_client(client_id): 34 | task = asyncio.create_task(all_client[client_id].run()) 35 | 36 | # Add the task to the set to keep a strong reference: 37 | background_tasks.add(task) 38 | 39 | # To prevent the task from being held in memory forever and unable to be garbage collected 40 | # Let each task remove itself from the set after it finishes: 41 | task.add_done_callback(background_tasks.discard) 42 | return client_id 43 | 44 | 45 | def add_client(grass_client: AsyncGrassWs): 46 | client_id = uuid.uuid4().__str__() 47 | all_client[client_id] = grass_client 48 | all_client_ids.append(client_id) 49 | return client_id 50 | 51 | 52 | async def delete_client(client_id): 53 | logger.info(f'[Exit] {all_client[client_id].user_id}') 54 | await all_client[client_id].stop() 55 | del all_client[client_id] 56 | all_client_ids.remove(client_id) 57 | 58 | 59 | def load_file_clients(data): 60 | new_clients = [] 61 | index = 0 62 | for line in data.split('\n'): 63 | user_id, proxy_url = parse_line(line) 64 | if not user_id: 65 | continue 66 | index += 1 67 | client = AsyncGrassWs(user_id=user_id, proxy_url=proxy_url) 68 | new_clients.append(add_client(client)) 69 | return new_clients 70 | 71 | 72 | async def threading_run_clients(clients): 73 | for client_id in clients: 74 | run_client(client_id) 75 | 76 | 77 | @app.get("/", response_class=HTMLResponse) 78 | async def read_item(request: Request): 79 | return templates.TemplateResponse(request=request, name="index.html") 80 | 81 | 82 | @client_router.get("/{client_id}") 83 | def find_one(client_id: str): 84 | client = all_client.get(client_id) 85 | data = { 86 | 'data': { 87 | 'status': None, 88 | "proxy_url": None, 89 | "logs": [] 90 | }, 91 | 'message': "failed" 92 | } 93 | if client is not None: 94 | data = { 95 | 'data': { 96 | 'status': client.status, 97 | "proxy_url": client.proxy_url, 98 | "logs": list(reversed(client.logs[-50:])) 99 | }, 100 | 'message': "success" 101 | } 102 | return data 103 | 104 | 105 | @client_router.get("/") 106 | def find_all(): 107 | data = [] 108 | for client_id in all_client_ids: 109 | try: 110 | data.append({ 111 | 'id': client_id, 112 | 'user_id': all_client[client_id].user_id, 113 | 'status': all_client[client_id].status, 114 | "proxy_url": all_client[client_id].proxy_url 115 | }) 116 | except: 117 | continue 118 | return { 119 | 'data': data, 120 | 'message': "success" 121 | } 122 | 123 | 124 | @client_router.post("/") 125 | async def add(user_id: str, proxy_url: Optional[str] = None): 126 | client = AsyncGrassWs(user_id=user_id, proxy_url=proxy_url or None) 127 | client_id = add_client(client) 128 | run_client(client_id) 129 | return {'data': client_id, 'message': 'create success'} 130 | 131 | 132 | @client_router.delete("/{user_id}") 133 | async def delete_one(user_id: str): 134 | await delete_client(user_id) 135 | return {'data': user_id, 'message': 'success'} 136 | 137 | 138 | @client_router.delete("/") 139 | async def delete_all(): 140 | all_client_ids_copy = all_client_ids[::] 141 | for client_id in all_client_ids_copy: 142 | await delete_client(client_id) 143 | return {'data': [], 'message': 'success'} 144 | 145 | 146 | @app.post("/upload/") 147 | async def run_by_file(file: UploadFile, background_task: BackgroundTasks): 148 | data = (await file.read()).decode() 149 | new_clients = load_file_clients(data) 150 | background_task.add_task(threading_run_clients, new_clients) 151 | return {"data": None, 'message': 'success'} 152 | 153 | 154 | app.include_router(client_router) 155 | 156 | if __name__ == '__main__': 157 | uvicorn.run("main:app", host='0.0.0.0') 158 | -------------------------------------------------------------------------------- /app/static/js/index.js: -------------------------------------------------------------------------------- 1 | // Function to show logs in a modal 2 | function showLogs (userId) { 3 | fetch(`/client/${userId}`) 4 | .then(response => response.json()) 5 | .then(data => { 6 | const logs = data.data.logs 7 | const logText = logs.map(log => `[${log[0]}] -- ${log[1]}`).join('
') 8 | document.getElementById('logText').innerHTML = logText 9 | document.getElementById('logsModal').style.display = 'block' 10 | }) 11 | .catch(error => console.error('Error:', error)) 12 | } 13 | 14 | // Update the statistics of online connections 15 | function updateOnlineCount (onlineCount, allCount) { 16 | // Get the elements to display the connection count 17 | const onlineCountElement = document.getElementById('onlineCount') 18 | const allCountElement = document.getElementById('allCount') 19 | onlineCountElement.textContent = onlineCount 20 | allCountElement.textContent = allCount 21 | } 22 | 23 | // Prompt dialog to input user_id and proxy 24 | function addUser () { 25 | const userId = prompt('Please enter UserID:') 26 | if (!userId) { 27 | return 28 | } 29 | const proxy = prompt('Set proxy (click cancel if not using proxy)') 30 | if (userId) { 31 | // Construct URL with parameters 32 | const baseUrl = '/client/' 33 | const url = new URL(baseUrl, window.location.href) 34 | url.searchParams.append('user_id', userId) 35 | if (proxy) { 36 | url.searchParams.append('proxy_url', proxy) 37 | } 38 | fetch(url.href, { 39 | method: 'POST', 40 | headers: { 41 | 'Content-Type': 'application/json' 42 | } 43 | }) 44 | .then(response => { 45 | if (response.ok) { 46 | alert('User added successfully') 47 | fetchData() 48 | } else { 49 | alert('Failed to add user') 50 | } 51 | }) 52 | .catch(error => { 53 | alert('Request error:', error) 54 | }) 55 | } 56 | } 57 | 58 | // Function to delete a user 59 | function deleteUser (userId) { 60 | fetch(`/client/${userId}`, { 61 | method: 'DELETE' 62 | }) 63 | .then(response => response.json()) 64 | .then(data => { 65 | if (data.message === 'success') { 66 | alert('Delete successful') 67 | // If deletion is successful, reload all data 68 | fetchData() 69 | } 70 | }) 71 | .catch(error => console.error('Error:', error)) 72 | } 73 | 74 | // Function to delete all users 75 | function deleteAllUser () { 76 | showLoading() 77 | fetch('/client/', { 78 | method: 'DELETE' 79 | }) 80 | .then(response => response.json()) 81 | .then(data => { 82 | if (data.message === 'success') { 83 | alert('Delete successful') 84 | // If deletion is successful, reload all data 85 | fetchData() 86 | } 87 | hideLoading() 88 | }) 89 | .catch(error => { 90 | console.error('Error:', error) 91 | hideLoading() 92 | }) 93 | } 94 | 95 | // Function to upload a file 96 | function uploadFile () { 97 | const input = document.createElement('input') 98 | input.type = 'file' 99 | input.onchange = function () { 100 | const file = input.files[0] 101 | const formData = new FormData() 102 | formData.append('file', file) 103 | showLoading() 104 | fetch('/upload/', { 105 | method: 'POST', 106 | body: formData 107 | }) 108 | .then(response => { 109 | if (response.ok) { 110 | alert('File uploaded successfully') 111 | fetchData() 112 | } else { 113 | alert('Failed to upload file!') 114 | } 115 | hideLoading() 116 | }) 117 | .catch(error => { 118 | console.error('Error:', error) 119 | hideLoading() 120 | }) 121 | } 122 | input.click() 123 | } 124 | 125 | // Function to close the modal 126 | function closeModal () { 127 | document.getElementById('logsModal').style.display = 'none' 128 | } 129 | 130 | // Function to fetch data (simulate data request) 131 | function fetchData () { 132 | return new Promise((resolve, reject) => { 133 | // Make a GET request to the API endpoint 134 | fetch('/client/') 135 | .then(response => response.json()) 136 | .then(data => { 137 | const statusMap = { 138 | 0: 'Not connected', 139 | 1: 'Connecting', 140 | 2: 'Connected', 141 | 3: 'Stopped' 142 | } 143 | let counter = 0 // Initial number is 1 144 | let onlineCounter = 0 145 | const tableBody = document.querySelector('#data-table tbody') 146 | // Clear existing data in the table 147 | tableBody.innerHTML = '' 148 | 149 | data.data.forEach(item => { 150 | const row = document.createElement('tr') 151 | row.innerHTML = ` 152 | ${counter + 1} 153 | ${item.user_id} 154 | ${item.proxy_url || ''} 155 | ${statusMap[item.status]} 156 | 157 | 158 | 159 | 160 | ` 161 | tableBody.appendChild(row) 162 | counter++ 163 | if (item.status == 2) { 164 | onlineCounter++ 165 | } 166 | }) 167 | updateOnlineCount(onlineCounter, counter) 168 | resolve() 169 | }) 170 | .catch(error => console.error('Error:', error)) 171 | }) 172 | } 173 | 174 | // Function to fetch data at intervals 175 | function fetchDataInterval () { 176 | fetchData().then(() => { 177 | // Wait for 5 seconds after data request is completed before initiating the next request 178 | setTimeout(fetchDataInterval, 5000) 179 | }) 180 | } 181 | 182 | // Function to show loading indicator 183 | function showLoading () { 184 | document.getElementById('loading').style.display = 'block' 185 | } 186 | 187 | // Function to hide loading indicator 188 | function hideLoading () { 189 | document.getElementById('loading').style.display = 'none' 190 | } 191 | 192 | // Initiate fetching data at intervals 193 | fetchDataInterval() 194 | -------------------------------------------------------------------------------- /core.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import json 3 | import ssl 4 | import sys 5 | import time 6 | import uuid 7 | from datetime import datetime 8 | from typing import Optional 9 | 10 | import socks 11 | import websockets 12 | from faker import Faker 13 | from loguru import logger 14 | from websockets import WebSocketCommonProtocol 15 | 16 | from utils import parse_proxy_url, Status 17 | 18 | logger.remove() # Remove the default console output handler 19 | 20 | logger.add(sys.stdout, level="INFO") # Add a new console output handler 21 | 22 | INFO = 'INFO' 23 | DEBUG = 'DEBUG' 24 | 25 | 26 | class AsyncGrassWs: 27 | def __init__(self, user_id, proxy_url=None): 28 | self.user_id = user_id 29 | self.user_agent = Faker().chrome() 30 | self.device_id = str(uuid.uuid3(uuid.NAMESPACE_DNS, proxy_url or "")) 31 | self.proxy_url = proxy_url 32 | self.ws: Optional[WebSocketCommonProtocol] = None 33 | self.status: Status = Status.disconnect 34 | self._stop = False 35 | self._stopped = False 36 | self._ping_stopped = False 37 | self.server_hostname = "proxy.wynd.network" 38 | self.server_port = 4444 39 | self.server_url = f"wss://{self.server_hostname}:{self.server_port}/" 40 | self.proxy_timeout = 60 41 | 42 | self.logs = [] 43 | 44 | def log(self, level, message): 45 | logger.log(logger.level(level).name, message) 46 | self.logs.append((datetime.now().strftime("%Y-%m-%d %H:%M:%S"), message)) 47 | if len(self.logs) >= 100: 48 | self.logs = self.logs[-100:] 49 | 50 | async def send_ping(self): 51 | await asyncio.sleep(5) 52 | while not self._stop: 53 | try: 54 | send_message = json.dumps( 55 | {"id": str(uuid.uuid4()), "version": "1.0.0", "action": "PING", "data": {}}) 56 | if self.ws: 57 | self.log(DEBUG, f'[Sending Message] [{self.user_id}] [{self.proxy_url}] [{send_message}]') 58 | await self.ws.send(send_message) 59 | except Exception as e: 60 | self.log(DEBUG, f'[PING Error] {e}') 61 | for i in range(20): 62 | if self._stop: 63 | break 64 | await asyncio.sleep(1) 65 | self._ping_stopped = True 66 | 67 | def auth_response(self, message): 68 | return { 69 | "id": message["id"], 70 | "origin_action": "AUTH", 71 | "result": { 72 | "browser_id": self.device_id, 73 | "user_id": self.user_id, 74 | "user_agent": self.user_agent, 75 | "timestamp": int(time.time()), 76 | "device_type": "extension", 77 | "version": "3.3.2" 78 | } 79 | } 80 | 81 | async def run(self): 82 | self.log(INFO, f'[Start] [{self.user_id}] [{self.proxy_url}]') 83 | asyncio.create_task(self.send_ping()) 84 | loop = asyncio.get_event_loop() 85 | while True: 86 | ws_proxy = None 87 | try: 88 | self.status = Status.connecting 89 | if self.proxy_url: 90 | proxy_type, http_proxy_host, http_proxy_port, http_proxy_auth = parse_proxy_url(self.proxy_url) 91 | if http_proxy_auth: 92 | username, password = http_proxy_auth[0], http_proxy_auth[1] 93 | else: 94 | username = password = None 95 | # Initialize the connection to the server through the proxy 96 | self.log(DEBUG, f'[Connecting Proxy] [{self.user_id}] [{self.proxy_url}]') 97 | ws_proxy = socks.socksocket() 98 | ws_proxy.set_proxy(socks.PROXY_TYPES[proxy_type.upper()], http_proxy_host, http_proxy_port, 99 | username=username, password=password) 100 | async with asyncio.timeout(self.proxy_timeout): 101 | await loop.run_in_executor(None, ws_proxy.connect, (self.server_hostname, self.server_port)) 102 | self.log(DEBUG, f'[Proxy Connection Successful] [{self.user_id}] [{self.proxy_url}]') 103 | ssl_context = ssl.create_default_context() 104 | ssl_context.check_hostname = False 105 | ssl_context.verify_mode = ssl.CERT_NONE 106 | custom_headers = { 107 | "User-Agent": self.user_agent 108 | } 109 | self.log(DEBUG, f'[Connecting Server] [{self.user_id}] [{self.proxy_url}]') 110 | self.ws = await websockets.connect( 111 | self.server_url, 112 | ssl=ssl_context, 113 | sock=ws_proxy, 114 | extra_headers=custom_headers, 115 | server_hostname=self.server_hostname, 116 | open_timeout=60 117 | ) 118 | 119 | self.log(DEBUG, f'[Server Connection Successful] [{self.user_id}] [{self.proxy_url}]') 120 | while True: 121 | response = await self.ws.recv() 122 | message = json.loads(response) 123 | self.log(DEBUG, f'[Received Message] [{self.user_id}] [{self.proxy_url}] [{message}]') 124 | if message.get("action") == "AUTH": 125 | auth_response = self.auth_response(message) 126 | self.log(DEBUG, f'[Sending Message] [{self.user_id}] [{self.proxy_url}] [{auth_response}]') 127 | await self.ws.send(json.dumps(auth_response)) 128 | self.status = Status.connected 129 | self.log(INFO, f'[Online] [{self.user_id}] [{self.proxy_url}]') 130 | elif message.get("action") == "PONG": 131 | pong_response = {"id": message["id"], "origin_action": "PONG"} 132 | self.log(DEBUG, f'[Sending Message] [{self.user_id}] [{self.proxy_url}] [{pong_response}]') 133 | await self.ws.send(json.dumps(pong_response)) 134 | except TimeoutError as e: 135 | self.log(INFO, f'[Connection Timeout] [{self.user_id}] [{self.proxy_url}] {e}') 136 | except Exception as e: 137 | self.log(INFO, f'[Connection Closed] [{self.user_id}] [{self.proxy_url}] {e}') 138 | self.status = Status.disconnect 139 | if not self._stop: 140 | self.log(DEBUG, f'[Reconnecting] [{self.user_id}] [{self.proxy_url}]') 141 | try: 142 | ws_proxy.close() 143 | except: 144 | pass 145 | else: 146 | while not self._ping_stopped: 147 | await asyncio.sleep(1) 148 | self.log(INFO, f'Manually Exiting [{self.user_id}] [{self.proxy_url}]') 149 | self._stopped = True 150 | break 151 | await asyncio.sleep(5) 152 | 153 | async def stop(self): 154 | self._stop = True 155 | if self.ws: 156 | await self.ws.close() 157 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [2024] [confusion] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------