From 3f9cbc3d088c36e43bce6db0bc8905028195bdef Mon Sep 17 00:00:00 2001 From: yeongpin Date: Wed, 9 Apr 2025 20:49:39 +0800 Subject: [PATCH] Update version to 1.8.10, add user authorization check feature, and enhance localization support - Bumped version in .env file to 1.8.10. - Introduced a new script for checking user authorization with detailed feedback and error handling. - Updated CHANGELOG.md to include new entries for the user authorization feature and minor fixes. - Added localization strings for user authorization checks in English, Simplified Chinese, and Traditional Chinese. --- .env | 4 +- CHANGELOG.md | 4 + check_user_authorized.py | 214 +++++++++++++++++++++++++++++++++++++++ config.py | 2 +- locales/en.json | 31 +++++- locales/zh_cn.json | 31 +++++- locales/zh_tw.json | 31 +++++- main.py | 9 +- 8 files changed, 318 insertions(+), 8 deletions(-) create mode 100644 check_user_authorized.py diff --git a/.env b/.env index 58b7a07..58df688 100644 --- a/.env +++ b/.env @@ -1,2 +1,2 @@ -version=1.8.09 -VERSION=1.8.09 +version=1.8.10 +VERSION=1.8.10 diff --git a/CHANGELOG.md b/CHANGELOG.md index 9441fb3..d96c0c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Change Log +## v1.8.10 +1. Add: Check User Authorized | 添加檢查用戶授權 +2. Fix: Some Issues | 修復一些問題 + ## v1.8.09 1. Add: Force Update Config | 添加強制更新配置 2. Add: Multilanguage support for force update | 添加強制更新功能的多語言支持 diff --git a/check_user_authorized.py b/check_user_authorized.py new file mode 100644 index 0000000..de93aa6 --- /dev/null +++ b/check_user_authorized.py @@ -0,0 +1,214 @@ +import os +import requests +import time +import hashlib +import base64 +import struct +from colorama import Fore, Style, init + +# Initialize colorama +init() + +# Define emoji constants +EMOJI = { + "SUCCESS": "✅", + "ERROR": "❌", + "INFO": "ℹ️", + "WARNING": "⚠️", + "KEY": "🔑", + "CHECK": "🔍" +} + +def generate_hashed64_hex(input_str: str, salt: str = '') -> str: + """Generate a SHA-256 hash of input + salt and return as hex""" + hash_obj = hashlib.sha256() + hash_obj.update((input_str + salt).encode('utf-8')) + return hash_obj.hexdigest() + +def obfuscate_bytes(byte_array: bytearray) -> bytearray: + """Obfuscate bytes using the algorithm from utils.js""" + t = 165 + for r in range(len(byte_array)): + byte_array[r] = ((byte_array[r] ^ t) + (r % 256)) & 0xFF + t = byte_array[r] + return byte_array + +def generate_cursor_checksum(token: str, translator=None) -> str: + """Generate Cursor checksum from token using the algorithm""" + try: + # Clean the token + clean_token = token.strip() + + # Generate machineId and macMachineId + machine_id = generate_hashed64_hex(clean_token, 'machineId') + mac_machine_id = generate_hashed64_hex(clean_token, 'macMachineId') + + # Get timestamp and convert to byte array + timestamp = int(time.time() * 1000) // 1000000 + byte_array = bytearray(struct.pack('>Q', timestamp)[-6:]) # Take last 6 bytes + + # Obfuscate bytes and encode as base64 + obfuscated_bytes = obfuscate_bytes(byte_array) + encoded_checksum = base64.b64encode(obfuscated_bytes).decode('utf-8') + + # Combine final checksum + return f"{encoded_checksum}{machine_id}/{mac_machine_id}" + except Exception as e: + print(f"{Fore.RED}{EMOJI['ERROR']} {translator.get('auth_check.error_generating_checksum', error=str(e)) if translator else f'Error generating checksum: {str(e)}'}{Style.RESET_ALL}") + return "" + +def check_user_authorized(token: str, translator=None) -> bool: + """ + Check if the user is authorized with the given token + + Args: + token (str): The authorization token + translator: Optional translator for internationalization + + Returns: + bool: True if authorized, False otherwise + """ + try: + print(f"{Fore.CYAN}{EMOJI['CHECK']} {translator.get('auth_check.checking_authorization') if translator else 'Checking authorization...'}{Style.RESET_ALL}") + + # Clean the token + if token and '%3A%3A' in token: + token = token.split('%3A%3A')[1] + elif token and '::' in token: + token = token.split('::')[1] + + # Remove any whitespace + token = token.strip() + + if not token or len(token) < 10: # Add a basic validation for token length + print(f"{Fore.RED}{EMOJI['ERROR']} {translator.get('auth_check.invalid_token') if translator else 'Invalid token'}{Style.RESET_ALL}") + return False + + print(f"{Fore.CYAN}{EMOJI['INFO']} {translator.get('auth_check.token_length', length=len(token)) if translator else f'Token length: {len(token)} characters'}{Style.RESET_ALL}") + + # Try to get usage info using the DashboardService API + try: + # Generate checksum + checksum = generate_cursor_checksum(token, translator) + + # Create request headers + headers = { + 'accept-encoding': 'gzip', + 'authorization': f'Bearer {token}', + 'connect-protocol-version': '1', + 'content-type': 'application/proto', + 'user-agent': 'connect-es/1.6.1', + 'x-cursor-checksum': checksum, + 'x-cursor-client-version': '0.48.7', + 'x-cursor-timezone': 'Asia/Shanghai', + 'x-ghost-mode': 'false', + 'Host': 'api2.cursor.sh' + } + + print(f"{Fore.CYAN}{EMOJI['INFO']} {translator.get('auth_check.checking_usage_information') if translator else 'Checking usage information...'}{Style.RESET_ALL}") + + # Make the request - this endpoint doesn't need a request body + usage_response = requests.post( + 'https://api2.cursor.sh/aiserver.v1.DashboardService/GetUsageBasedPremiumRequests', + headers=headers, + data=b'', # Empty body + timeout=10 + ) + + print(f"{Fore.CYAN}{EMOJI['INFO']} {translator.get('auth_check.usage_response', response=usage_response.status_code) if translator else f'Usage response status: {usage_response.status_code}'}{Style.RESET_ALL}") + + if usage_response.status_code == 200: + print(f"{Fore.GREEN}{EMOJI['SUCCESS']} {translator.get('auth_check.user_authorized') if translator else 'User is authorized'}{Style.RESET_ALL}") + return True + elif usage_response.status_code == 401 or usage_response.status_code == 403: + print(f"{Fore.RED}{EMOJI['ERROR']} {translator.get('auth_check.user_unauthorized') if translator else 'User is unauthorized'}{Style.RESET_ALL}") + return False + else: + print(f"{Fore.YELLOW}{EMOJI['WARNING']} {translator.get('auth_check.unexpected_status_code', code=usage_response.status_code) if translator else f'Unexpected status code: {usage_response.status_code}'}{Style.RESET_ALL}") + + # If the token at least looks like a valid JWT, consider it valid + if token.startswith('eyJ') and '.' in token and len(token) > 100: + print(f"{Fore.YELLOW}{EMOJI['WARNING']} {translator.get('auth_check.jwt_token_warning') if translator else 'Token appears to be in JWT format, but API check returned an unexpected status code. The token might be valid but API access is restricted.'}{Style.RESET_ALL}") + return True + + return False + except Exception as e: + print(f"{Fore.YELLOW}{EMOJI['WARNING']} Error checking usage: {str(e)}{Style.RESET_ALL}") + + # If the token at least looks like a valid JWT, consider it valid even if the API check fails + if token.startswith('eyJ') and '.' in token and len(token) > 100: + print(f"{Fore.YELLOW}{EMOJI['WARNING']} {translator.get('auth_check.jwt_token_warning') if translator else 'Token appears to be in JWT format, but API check failed. The token might be valid but API access is restricted.'}{Style.RESET_ALL}") + return True + + return False + + except requests.exceptions.Timeout: + print(f"{Fore.RED}{EMOJI['ERROR']} {translator.get('auth_check.request_timeout') if translator else 'Request timed out'}{Style.RESET_ALL}") + return False + except requests.exceptions.ConnectionError: + print(f"{Fore.RED}{EMOJI['ERROR']} {translator.get('auth_check.connection_error') if translator else 'Connection error'}{Style.RESET_ALL}") + return False + except Exception as e: + print(f"{Fore.RED}{EMOJI['ERROR']} {translator.get('auth_check.check_error', error=str(e)) if translator else f'Error checking authorization: {str(e)}'}{Style.RESET_ALL}") + return False + +def run(translator=None): + """Run function to be called from main.py""" + try: + # Ask user if they want to get token from database or input manually + choice = input(f"{Fore.CYAN}{EMOJI['INFO']} {translator.get('auth_check.token_source') if translator else 'Get token from database or input manually? (d/m, default: d): '}{Style.RESET_ALL}").strip().lower() + + token = None + + # If user chooses database or default + if not choice or choice == 'd': + print(f"{Fore.CYAN}{EMOJI['INFO']} {translator.get('auth_check.getting_token_from_db') if translator else 'Getting token from database...'}{Style.RESET_ALL}") + + try: + # Import functions from cursor_acc_info.py + from cursor_acc_info import get_token + + # Get token using the get_token function + token = get_token() + + if token: + print(f"{Fore.GREEN}{EMOJI['SUCCESS']} {translator.get('auth_check.token_found_in_db') if translator else 'Token found in database'}{Style.RESET_ALL}") + else: + print(f"{Fore.YELLOW}{EMOJI['WARNING']} {translator.get('auth_check.token_not_found_in_db') if translator else 'Token not found in database'}{Style.RESET_ALL}") + except ImportError: + print(f"{Fore.YELLOW}{EMOJI['WARNING']} {translator.get('auth_check.cursor_acc_info_not_found') if translator else 'cursor_acc_info.py not found'}{Style.RESET_ALL}") + except Exception as e: + print(f"{Fore.YELLOW}{EMOJI['WARNING']} {translator.get('auth_check.error_getting_token_from_db', error=str(e)) if translator else f'Error getting token from database: {str(e)}'}{Style.RESET_ALL}") + + # If token not found in database or user chooses manual input + if not token: + # Try to get token from environment + token = os.environ.get('CURSOR_TOKEN') + + # If not in environment, ask user to input + if not token: + token = input(f"{Fore.CYAN}{EMOJI['KEY']} {translator.get('auth_check.enter_token') if translator else 'Enter your Cursor token: '}{Style.RESET_ALL}") + + # Check authorization + is_authorized = check_user_authorized(token, translator) + + if is_authorized: + print(f"{Fore.GREEN}{EMOJI['SUCCESS']} {translator.get('auth_check.authorization_successful') if translator else 'Authorization successful!'}{Style.RESET_ALL}") + else: + print(f"{Fore.RED}{EMOJI['ERROR']} {translator.get('auth_check.authorization_failed') if translator else 'Authorization failed!'}{Style.RESET_ALL}") + + return is_authorized + + except KeyboardInterrupt: + print(f"\n{Fore.YELLOW}{EMOJI['WARNING']} {translator.get('auth_check.operation_cancelled') if translator else 'Operation cancelled by user'}{Style.RESET_ALL}") + return False + except Exception as e: + print(f"{Fore.RED}{EMOJI['ERROR']} {translator.get('auth_check.unexpected_error', error=str(e)) if translator else f'Unexpected error: {str(e)}'}{Style.RESET_ALL}") + return False + +def main(translator=None): + """Main function to check user authorization""" + return run(translator) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/config.py b/config.py index d7b70f2..beb6c2f 100644 --- a/config.py +++ b/config.py @@ -54,7 +54,7 @@ def setup_config(translator=None): }, 'Utils': { 'enabled_update_check': 'True', - 'enabled_force_update': 'True', + 'enabled_force_update': 'False', 'enabled_account_info': 'True' } } diff --git a/locales/en.json b/locales/en.json index 5ffaaf3..79ce939 100644 --- a/locales/en.json +++ b/locales/en.json @@ -30,7 +30,8 @@ "continue_prompt": "Continue? (y/N): ", "operation_cancelled_by_user": "Operation cancelled by user", "exiting": "Exiting ……", - "bypass_version_check": "Bypass Cursor Version Check" + "bypass_version_check": "Bypass Cursor Version Check", + "check_user_authorized": "Check User Authorized" }, "languages": { "en": "English", @@ -698,5 +699,33 @@ "title": "Cursor Version Bypass Tool", "description": "This tool modifies Cursor's product.json to bypass version restrictions", "menu_option": "Bypass Cursor Version Check" + }, + "auth_check": { + "checking_authorization": "Checking authorization...", + "token_source": "Get token from database or input manually? (d/m, default: d)", + "getting_token_from_db": "Getting token from database...", + "token_found_in_db": "Token found in database", + "token_not_found_in_db": "Token not found in database", + "cursor_acc_info_not_found": "cursor_acc_info.py not found", + "error_getting_token_from_db": "Error getting token from database: {error}", + "enter_token": "Enter your Cursor token: ", + "token_length": "Token length: {length} characters", + "usage_response_status": "Usage response status: {response}", + "unexpected_status_code": "Unexpected status code: {code}", + "jwt_token_warning": "Token appears to be in JWT format, but API check returned an unexpected status code. The token might be valid but API access is restricted.", + "invalid_token": "Invalid token", + "user_authorized": "User is authorized", + "user_unauthorized": "User is unauthorized", + "request_timeout": "Request timed out", + "connection_error": "Connection error", + "check_error": "Error checking authorization: {error}", + "authorization_successful": "Authorization successful!", + "authorization_failed": "Authorization failed!", + "operation_cancelled": "Operation cancelled by user", + "unexpected_error": "Unexpected error: {error}", + "error_generating_checksum": "Error generating checksum: {error}", + "checking_usage_information": "Checking usage information...", + "check_usage_response": "Check usage response: {response}", + "usage_response": "Usage response: {response}" } } \ No newline at end of file diff --git a/locales/zh_cn.json b/locales/zh_cn.json index 2e4a90a..2e6e520 100644 --- a/locales/zh_cn.json +++ b/locales/zh_cn.json @@ -30,7 +30,8 @@ "continue_prompt": "继续?(y/N): ", "operation_cancelled_by_user": "操作被用户取消", "exiting": "退出中 ……", - "bypass_version_check": "绕过 Cursor 版本检查" + "bypass_version_check": "绕过 Cursor 版本检查", + "check_user_authorized": "检查用户授权" }, "languages": { "en": "英语", @@ -676,5 +677,33 @@ "title": "Cursor 版本绕过工具", "description": "此工具修改 Cursor 的 product.json 以绕过版本限制", "menu_option": "绕过 Cursor 版本检查" + }, + "auth_check": { + "checking_authorization": "检查授权...", + "token_source": "从数据库获取 token 或手动输入?(d/m, 默认: d)", + "getting_token_from_db": "从数据库获取 token...", + "token_found_in_db": "在数据库中找到 token", + "token_not_found_in_db": "在数据库中未找到 token", + "cursor_acc_info_not_found": "cursor_acc_info.py 未找到", + "error_getting_token_from_db": "从数据库获取 token 时出错: {error}", + "enter_token": "请输入您的 Cursor token: ", + "token_length": "token 长度: {length}", + "usage_response_status": "使用情况响应状态: {response}", + "unexpected_status_code": "意外状态码: {code}", + "jwt_token_warning": "token 似乎是 JWT 格式,但 API 检查返回意外状态码。token 可能有效但 API 访问受限。", + "invalid_token": "无效的 token", + "user_authorized": "用户已授权", + "user_unauthorized": "用户未授权", + "request_timeout": "请求超时", + "connection_error": "连接错误", + "check_error": "检查授权时出错: {error}", + "authorization_successful": "授权成功", + "authorization_failed": "授权失败", + "operation_cancelled": "操作已取消", + "unexpected_error": "意外错误: {error}", + "error_generating_checksum": "生成校验和时出错: {error}", + "checking_usage_information": "检查使用情况...", + "check_usage_response": "检查使用情况响应: {response}", + "usage_response": "使用情况响应: {response}" } } \ No newline at end of file diff --git a/locales/zh_tw.json b/locales/zh_tw.json index a748a2a..5e7c608 100644 --- a/locales/zh_tw.json +++ b/locales/zh_tw.json @@ -30,7 +30,8 @@ "continue_prompt": "繼續?(y/N): ", "operation_cancelled_by_user": "操作被使用者取消", "exiting": "退出中 ……", - "bypass_version_check": "繞過 Cursor 版本檢查" + "bypass_version_check": "繞過 Cursor 版本檢查", + "check_user_authorized": "檢查用戶授權" }, "languages": { "en": "英文", @@ -658,5 +659,33 @@ "title": "Cursor 版本繞過工具", "description": "此工具修改 Cursor 的 product.json 以繞過版本限制", "menu_option": "繞過 Cursor 版本檢查" + }, + "auth_check": { + "checking_authorization": "檢查授權...", + "token_source": "從資料庫獲取 token 或手動輸入?(d/m, 預設: d)", + "getting_token_from_db": "從資料庫獲取 token...", + "token_found_in_db": "在資料庫中找到 token", + "token_not_found_in_db": "在資料庫中未找到 token", + "cursor_acc_info_not_found": "cursor_acc_info.py 未找到", + "usage_response_status": "使用情況響應狀態: {response}", + "unexpected_status_code": "意外狀態碼: {code}", + "jwt_token_warning": "token 似乎是 JWT 格式,但 API 檢查返回意外狀態碼。token 可能有效但 API 訪問受限。", + "error_getting_token_from_db": "從資料庫獲取 token 時出錯: {error}", + "enter_token": "請輸入您的 Cursor token: ", + "token_length": "token 長度: {length}", + "invalid_token": "無效的 token", + "user_authorized": "用戶已授權", + "user_unauthorized": "用戶未授權", + "request_timeout": "請求超時", + "connection_error": "連接錯誤", + "check_error": "檢查授權時出錯: {error}", + "authorization_successful": "授權成功", + "authorization_failed": "授權失敗", + "operation_cancelled": "操作已取消", + "unexpected_error": "意外錯誤: {error}", + "error_generating_checksum": "生成校驗和時出錯: {error}", + "checking_usage_information": "檢查使用情況...", + "check_usage_response": "檢查使用情況響應: {response}", + "usage_response": "使用情況響應: {response}" } } \ No newline at end of file diff --git a/main.py b/main.py index a7f4e01..3d09097 100644 --- a/main.py +++ b/main.py @@ -287,7 +287,8 @@ def print_menu(): 12: f"{Fore.GREEN}12{Style.RESET_ALL}. {EMOJI['SETTINGS']} {translator.get('menu.config')}", 13: f"{Fore.GREEN}13{Style.RESET_ALL}. {EMOJI['SETTINGS']} {translator.get('menu.select_chrome_profile')}", 14: f"{Fore.GREEN}14{Style.RESET_ALL}. {EMOJI['ERROR']} {translator.get('menu.delete_google_account', fallback='Delete Cursor Google Account')}", - 15: f"{Fore.GREEN}15{Style.RESET_ALL}. {EMOJI['UPDATE']} {translator.get('menu.bypass_version_check', fallback='Bypass Cursor Version Check')}" + 15: f"{Fore.GREEN}15{Style.RESET_ALL}. {EMOJI['UPDATE']} {translator.get('menu.bypass_version_check', fallback='Bypass Cursor Version Check')}", + 16: f"{Fore.GREEN}16{Style.RESET_ALL}. {EMOJI['UPDATE']} {translator.get('menu.check_user_authorized', fallback='Check User Authorized')}" } # Automatically calculate the number of menu items in the left and right columns @@ -560,7 +561,7 @@ def main(): while True: try: - choice_num = 15 + choice_num = 16 choice = input(f"\n{EMOJI['ARROW']} {Fore.CYAN}{translator.get('menu.input_choice', choices=f'0-{choice_num}')}: {Style.RESET_ALL}") if choice == "0": @@ -630,6 +631,10 @@ def main(): import bypass_version bypass_version.main(translator) print_menu() + elif choice == "16": + import check_user_authorized + check_user_authorized.main(translator) + print_menu() else: print(f"{Fore.RED}{EMOJI['ERROR']} {translator.get('menu.invalid_choice')}{Style.RESET_ALL}") print_menu()