diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ef00706 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.pyc +cookies.txt diff --git a/README.md b/README.md index aa7afc6..9866fb0 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,18 @@ # huggingface-proxy -Host a proxy server on Huggingface Spaces \ No newline at end of file +_Host a proxy server on Huggingface Spaces._ + +### Setup +#### Space +1. Create a new Space using `./space/Dockerfile`. +2. Upload your rathole client config to the space under the path `./rathole-client.toml` +3. Upload `./space/server.py` and `./space/supervisord.conf` +4. "Factory Reboot" the space to get it to rebuild. + + +#### Keep Alive +On a local machine, run the keep alive script to prevent Huggingface from stopping your space due to "inactivity". You'll have to create a write API key on your account and save your cookies to `cookies.txt`. + +``` +python3 ./keepalive/keepalive.py --token hf_XXX --username XXX --cookies ./cookies.txt --headless +``` diff --git a/keepalive/hf-upkeep.service b/keepalive/hf-upkeep.service new file mode 100644 index 0000000..aa0b69a --- /dev/null +++ b/keepalive/hf-upkeep.service @@ -0,0 +1,15 @@ +[Unit] +Description=Huggingface Proxy Upkeep Daemon +After=network.target +StartLimitIntervalSec=0 + +[Service] +User=server +Group=server +Type=simple +ExecStart=/srv/server/huggingface-proxy/venv/bin/python3 /srv/server/huggingface-proxy/keepalive.py --token hf_XXX --username example --cookies /srv/server/huggingface-proxy/cookies.txt --headless +Restart=always +RestartSec=3 + +[Install] +WantedBy=multi-user.target diff --git a/keepalive/keepalive.py b/keepalive/keepalive.py new file mode 100644 index 0000000..03f4ca0 --- /dev/null +++ b/keepalive/keepalive.py @@ -0,0 +1,81 @@ +import argparse +import random +import time +from pathlib import Path + +from huggingface_hub import HfApi +from selenium.webdriver.common.by import By +from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.support.ui import WebDriverWait + +from lib.hf import list_spaces, login +from lib.webdriver import create_driver, load_cookies, save_cookies + +parser = argparse.ArgumentParser() +parser.add_argument("--cookies", required=True, help="Path to the file containing your HF cookies") +parser.add_argument("--token", required=True, help="Your HF API token") +parser.add_argument("--username", required=True, help="Your HF username") +parser.add_argument("--password", required=True, help="Your HF password") +parser.add_argument("--headless", action='store_true', help="Run Selinium in headless mode") +args = parser.parse_args() + +cookies_file = Path(args.cookies).resolve().expanduser().absolute() + +login(args.token) + +driver = create_driver(args.headless) + +driver.get(f'https://huggingface.co') +if cookies_file.exists(): + load_cookies(cookies_file) + +driver.get('https://huggingface.co/settings/profile') +if driver.current_url != 'https://huggingface.co/settings/profile': + print('Not logged in!') + username = WebDriverWait(driver, 10).until(EC.presence_of_element_located((By.NAME, "username"))) + username.send_keys(args.username) + password = driver.find_element(By.NAME, "password") + password.send_keys(args.password) + login_button = WebDriverWait(driver, 10).until(EC.presence_of_element_located((By.XPATH, '//button[@type="submit"]'))) + login_button.click() + save_cookies(driver, cookies_file) + print('Manually logged in.') +else: + print('Auth is correct') + +while True: + our_spaces = [x.id for x in list_spaces(args.username) if 'proxy' in x.id] + random.shuffle(our_spaces) + + driver.get(f'https://huggingface.co') + time.sleep(5) + for space in our_spaces: + print(space) + driver.get(f'https://huggingface.co/spaces/{space}') + time.sleep(100) + + # try: + if len(driver.find_elements(By.ID, 'captcha-container')): + print('CAPTCHA!!!') + else: + for iframe in driver.find_elements(By.TAG_NAME, 'iframe'): + src = iframe.get_attribute('src') + if 'hf.space' in src and '?__sign' in src: + print('Space is online') + break + else: + api = HfApi() + try: + api.restart_space(repo_id=space) + print('Restarted space') + except Exception as e: + print('Failed to restart space!', e) + + time.sleep(30) + + # Avoid the captcha + driver.get('https://huggingface.co') + time.sleep(30) + sleep_time = 300 + print(f'Sleeping {sleep_time} seconds...') + time.sleep(sleep_time) diff --git a/keepalive/lib/__init__.py b/keepalive/lib/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/keepalive/lib/hf.py b/keepalive/lib/hf.py new file mode 100644 index 0000000..2387726 --- /dev/null +++ b/keepalive/lib/hf.py @@ -0,0 +1,14 @@ +from huggingface_hub import login as hf_login +from huggingface_hub import HfApi + + +def login(token: str): + return hf_login(token) + + +def list_spaces(author: str): + api = HfApi() + result = [] + for space in api.list_spaces(author=author): + result.append(space) + return result diff --git a/keepalive/lib/webdriver.py b/keepalive/lib/webdriver.py new file mode 100644 index 0000000..fbc0bfa --- /dev/null +++ b/keepalive/lib/webdriver.py @@ -0,0 +1,46 @@ +import http.cookiejar + +import undetected_chromedriver as uc +from selenium.common import InvalidSessionIdException +from selenium.webdriver.chrome.options import Options +from selenium.webdriver.chrome.webdriver import WebDriver +from webdriver_manager.chrome import ChromeDriverManager + +driver: WebDriver = None + + +def create_driver(headless=False): + global driver + if driver: + try: + driver.close() + except InvalidSessionIdException: + pass + + options = Options() + if headless: + options.add_argument('--headless') + options.add_argument("--window-size=1920,1200") + options.add_argument("--incognito") + driver = uc.Chrome(driver_executable_path=ChromeDriverManager().install(), options=options, use_subprocess=False) + return driver + + +def load_cookies(file): + cookie_jar = http.cookiejar.MozillaCookieJar() + cookie_jar.load(file, ignore_discard=True, ignore_expires=True) + for cookie in cookie_jar: + driver.add_cookie({"name": cookie.name, "value": cookie.value, 'domain': cookie.domain}) + + +def save_cookies(driver, location): + with open(location, 'w') as file: + file.write('# Netscape HTTP Cookie File\n') + for cookie in driver.get_cookies(): + file.write(f"{cookie['domain']}\t" + f"{'TRUE' if cookie['domain'].startswith('.') else 'FALSE'}\t" + f"{cookie['path']}\t" + f"{'TRUE' if 'secure' in cookie else 'FALSE'}\t" + f"{str(int(cookie['expiry'])) if 'expiry' in cookie else '0'}\t" + f"{cookie['name']}\t" + f"{cookie['value']}\n") diff --git a/keepalive/requirements.txt b/keepalive/requirements.txt new file mode 100644 index 0000000..ddae3ac --- /dev/null +++ b/keepalive/requirements.txt @@ -0,0 +1,5 @@ +undetected-chromedriver +selenium==4.9.1 +huggingface_hub +webdriver_manager +pyvirtualdisplay diff --git a/keepalive/restart-spaces.py b/keepalive/restart-spaces.py new file mode 100644 index 0000000..61dc49d --- /dev/null +++ b/keepalive/restart-spaces.py @@ -0,0 +1,24 @@ +import argparse +import random + +from huggingface_hub import HfApi + +from lib.hf import list_spaces, login + +parser = argparse.ArgumentParser() +parser.add_argument("--token", required=True, help="Your HF API token") +parser.add_argument("--username", required=True, help="Your HF username") +args = parser.parse_args() + +login(args.token) + +our_spaces = [x.id for x in list_spaces(args.username) if 'proxy' in x.id] +random.shuffle(our_spaces) + +for space in our_spaces: + api = HfApi() + try: + api.restart_space(repo_id=space) + print(space) + except Exception as e: + print('Failed to restart space!', e) diff --git a/space/Dockerfile b/space/Dockerfile new file mode 100644 index 0000000..bd3f71f --- /dev/null +++ b/space/Dockerfile @@ -0,0 +1,25 @@ +FROM debian:stable +RUN apt-get update && \ + apt-get install -y git wget python3 python3-pip unzip && \ + rm -rf /var/cache/apt/archives /var/lib/apt/lists/* + +RUN mkdir -p /var/log/app + +RUN git clone "https://git.evulid.cc/cyberes/huggingface-proxy.git" /app/huggingface + +COPY ./rathole-client.toml /app/huggingface/rathole-client.toml +COPY ./server.py /app/huggingface/space/server.py + +RUN wget https://github.com/rapiz1/rathole/releases/download/v0.5.0/rathole-x86_64-unknown-linux-gnu.zip -O /tmp/rathole.zip +RUN unzip /tmp/rathole.zip -d /app + +RUN pip install --break-system-packages supervisor requests flask proxy.py "httpx[http2]" + +RUN adduser --disabled-password --gecos '' --uid 1000 newuser + +RUN chown -R newuser:newuser /app + +EXPOSE 7860 +USER newuser + +CMD cd /app && /usr/local/bin/supervisord -c /app/huggingface/space/supervisord.conf diff --git a/space/server.py b/space/server.py new file mode 100644 index 0000000..80b943e --- /dev/null +++ b/space/server.py @@ -0,0 +1,14 @@ +import requests +from flask import Flask, Response + +app = Flask(__name__) + + +@app.route('/') +def home(): + r = requests.get('https://api.ipify.org') + return Response(r.text, mimetype='text/plain') + + +if __name__ == '__main__': + app.run(host="0.0.0.0", port=7860) diff --git a/space/supervisord.conf b/space/supervisord.conf new file mode 100644 index 0000000..2410c0d --- /dev/null +++ b/space/supervisord.conf @@ -0,0 +1,30 @@ +[supervisord] +nodaemon=true + +[program:rathole] +command=/bin/bash -c '/app/rathole -c /app/huggingface/rathole-client.toml 2>&1 | tee -a /app/rathole.log' +autostart=true +autorestart=true +stdout_logfile=/dev/fd/1 +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/fd/2 +stderr_logfile_maxbytes=0 + +[program:flask] +command=/usr/bin/python3 /app/huggingface/space/server.py +autostart=true +autorestart=true +stdout_logfile=/dev/fd/1 +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/fd/2 +stderr_logfile_maxbytes=0 + +[program:proxy] +command=proxy --hostname 0.0.0.0 --port 3128 --timeout 300 --plugins proxy.plugin.CloudflareDnsResolverPlugin +autostart=true +autorestart=true +stdout_logfile=/dev/fd/1 +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/fd/2 +stderr_logfile_maxbytes=0 +environment=PORT=3128