diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..7430307 --- /dev/null +++ b/.env.example @@ -0,0 +1,3 @@ +PROXY=http://nathanwoodburn:5000/ +TLD=woodburn +RESTRICTED=["admin"] \ No newline at end of file diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml new file mode 100644 index 0000000..03769fb --- /dev/null +++ b/.gitea/workflows/build.yml @@ -0,0 +1,38 @@ +name: Build Docker +run-name: Build Docker Images +on: + push: + +jobs: + Build Docker: + runs-on: [ubuntu-latest, amd] # Add amd to require amd64 + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Install Docker + run : | + echo "Updating apt sources" + echo "deb http://ftp.au.debian.org/debian buster main" > /etc/apt/sources.list + apt-get update --allow-unauthenticated --allow-insecure-repositories + apt-get install docker.io -y + - name: Build Docker image + run : | + echo "${{ secrets.DOCKERGIT_TOKEN }}" | docker login git.woodburn.au -u nathanwoodburn --password-stdin + echo "branch=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}" + tag=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}} + tag=${tag//\//-} + tag_num=${GITHUB_RUN_NUMBER} + echo "tag_num=$tag_num" + + if [[ "$tag" == "main" ]]; then + tag="latest" + else + tag_num="${tag}-${tag_num}" + fi + + + docker build -t tld_restricted_proxy:$tag_num . + docker tag tld_restricted_proxy:$tag_num git.woodburn.au/nathanwoodburn/tld_restricted_proxy:$tag_num + docker push git.woodburn.au/nathanwoodburn/tld_restricted_proxy:$tag_num + docker tag tld_restricted_proxy:$tag_num git.woodburn.au/nathanwoodburn/tld_restricted_proxy:$tag + docker push git.woodburn.au/nathanwoodburn/tld_restricted_proxy:$tag \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..242fca1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ + +.env + +__pycache__/ + +cookies.json diff --git a/404.html b/404.html new file mode 100644 index 0000000..e69de29 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..725a071 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,17 @@ +FROM --platform=$BUILDPLATFORM python:3.10-alpine AS builder + +WORKDIR /app + +COPY requirements.txt /app +RUN --mount=type=cache,target=/root/.cache/pip \ + pip3 install -r requirements.txt + +COPY . /app + +# Add mount point for data volume +# VOLUME /data + +ENTRYPOINT ["python3"] +CMD ["server.py"] + +FROM builder as dev-envs \ No newline at end of file diff --git a/README.md b/README.md index b054ffd..764d037 100644 --- a/README.md +++ b/README.md @@ -1 +1,7 @@ -# g-user-proxy +# TLD Restricted Proxy + +Environment variables: +- `PROXY` - The proxy to use for restricted TLDs. Example: `http://localhost:3128` +- `TLD` - The TLD to restrict. Example: `g` +- `RESTRICTED` - List of restricted paths. Example: `["path1", "path2"]` will require the user to have a .g domain to access `path1/*`, `path2/*` + diff --git a/main.py b/main.py new file mode 100644 index 0000000..357d6ad --- /dev/null +++ b/main.py @@ -0,0 +1,130 @@ +from flask import Flask, make_response, redirect, request, jsonify, render_template, send_from_directory, render_template_string +import os +import dotenv +import requests +import datetime +import json +import secrets +import threading + +app = Flask(__name__) +dotenv.load_dotenv() +URL = os.getenv('PROXY') +RESTRICTED = os.getenv('RESTRICTED') +RESTRICTED = json.loads(RESTRICTED) +RESTRICTED = [f'{i.lower()}/' for i in RESTRICTED] +TLD = os.getenv('TLD') + +# Load cookies +cookies = [] + +if os.path.isfile('cookies.json'): + with open('cookies.json') as file: + cookies = json.load(file) +else: + with open('cookies.json', 'w') as file: + json.dump(cookies, file) + + +# region Auth +@app.route('/auth', methods=['POST']) +def auth(): + global cookies + auth = login(request) + if auth == False: + return render_template_string("Failed to authenticate") + + # Make sure user has a correct domain + if not auth.endswith(f'.{TLD}'): + return render_template_string(f"You need to have a domain on .{TLD} to access this content.") + + resp = make_response(render_template_string("Success")) + # Gen cookie + auth_cookie = secrets.token_hex(12 // 2) + cookies.append({'name': auth, 'cookie': auth_cookie}) + + with open('cookies.json', 'w') as file: + json.dump(cookies, file) + + resp.set_cookie('auth', auth_cookie, max_age=60*60*24*30) + return resp + +@app.route('/logout') +def logout(): + global cookies + resp = make_response(redirect('/')) + resp.set_cookie('auth', '', expires=0) + + if 'auth' not in request.cookies: + return resp + cookies = [i for i in cookies if i['cookie'] != request.cookies['auth']] + with open('cookies.json', 'w') as file: + json.dump(cookies, file) + + return resp + +def login(request): + dict = request.form.to_dict() + keys = dict.keys() + keys = list(keys)[0] + keys = json.loads(keys) + auth_request = keys['request'] + # return login(auth_request) + r = requests.get(f'https://auth.varo.domains/verify/{auth_request}') + r = r.json() + if r['success'] == False: + return False + + if 'data' in r: + data = r['data'] + if 'name' in data: + return data['name'] + return False + +# endregion + +# Catch all +@app.route('/', defaults={'path': ''}) +@app.route('/', methods=['GET', 'POST']) +def catch_all(path): + for i in RESTRICTED: + if path.lower().startswith(i) or path.lower() == i: + # Check if user is logged in + if 'auth' not in request.cookies: + return render_template('auth.html', year=datetime.datetime.now().year,redirect=request.url,tld=TLD) + auth = request.cookies['auth'] + if not any(i['cookie'] == auth for i in cookies): + return render_template('auth.html', year=datetime.datetime.now().year,redirect=request.url,tld=TLD) + break + + + + res = requests.request( + method = request.method, + url = request.url.replace(request.host_url, f'{URL}/'), + headers = {k:v for k,v in request.headers if k.lower() != 'host'}, + data = request.get_data(), + cookies = request.cookies, + allow_redirects = False, + ) + excluded_headers = ['content-encoding', 'content-length', 'transfer-encoding', 'connection'] + headers = [ + (k,v) for k,v in res.raw.headers.items() + if k.lower() not in excluded_headers + ] + + # Replace all instances of the proxy URL with the local URL + # If content type is html + if 'text/html' in res.headers['Content-Type']: + content = res.content.decode('utf-8') + content = content.replace(URL, request.host_url) + # TMP: Replace other domains + content = content.replace('https://alee.freeconcept/', request.host_url) + response = make_response(content, res.status_code, headers) + return response + + response = make_response(res.content, res.status_code, headers) + return response + +if __name__ == '__main__': + app.run(debug=True, port=5000, host='0.0.0.0') \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..f640785 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +flask +python-dotenv +gunicorn +requests \ No newline at end of file diff --git a/server.py b/server.py new file mode 100644 index 0000000..a8cb325 --- /dev/null +++ b/server.py @@ -0,0 +1,33 @@ +from flask import Flask +from main import app +import main +from gunicorn.app.base import BaseApplication + + +class GunicornApp(BaseApplication): + def __init__(self, app, options=None): + self.options = options or {} + self.application = app + super().__init__() + + def load_config(self): + for key, value in self.options.items(): + if key in self.cfg.settings and value is not None: + self.cfg.set(key.lower(), value) + + def load(self): + return self.application + +if __name__ == '__main__': + workers = 1 + threads = 2 + workers = int(workers) + threads = int(threads) + options = { + 'bind': '0.0.0.0:5000', + 'workers': workers, + 'threads': threads, + } + gunicorn_app = GunicornApp(app, options) + print('Starting server with ' + str(workers) + ' workers and ' + str(threads) + ' threads', flush=True) + gunicorn_app.run() diff --git a/templates/auth.html b/templates/auth.html new file mode 100644 index 0000000..cbef74f --- /dev/null +++ b/templates/auth.html @@ -0,0 +1,82 @@ + + + + + + + Login + + + + + + + + +
+

+ {{error}} +

+

This content is protected

+

Please verify you own a .{{tld}} domain to access this content

+ +
+ +
+ +
+ + + \ No newline at end of file