megatrump commited on
Commit
c4edceb
·
verified ·
1 Parent(s): 8c984b6

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +81 -43
app.py CHANGED
@@ -1,6 +1,6 @@
1
  import re
2
- import logging
3
  import os
 
4
  from flask import Flask, request, Response
5
  import requests
6
  from urllib.parse import urlparse, unquote
@@ -9,15 +9,17 @@ from urllib.parse import urlparse, unquote
9
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
10
  app = Flask(__name__)
11
 
12
- # --- Authentication Configuration ---
13
- # Read the secret key from an environment variable.
14
- # The service will not start if this key is not set for security reasons.
15
- SECRET_KEY = os.environ.get('PROXY_SECRET_KEY')
16
- if not SECRET_KEY:
17
- logging.critical("FATAL: Environment variable PROXY_SECRET_KEY is not set. Service cannot start.")
18
- exit("Error: The PROXY_SECRET_KEY environment variable must be set.")
19
 
20
  # --- Whitelisted URL Patterns for GitHub ---
 
 
21
  ALLOWED_PATTERNS = [
22
  # Repositories: releases, archives, blobs, raw content
23
  re.compile(r'^https://github\.com/[^/]+/[^/]+/(?:releases|archive)/.*$', re.IGNORECASE),
@@ -40,8 +42,8 @@ ALLOWED_PATTERNS = [
40
  re.compile(r'^https://github\.com/[^/]+/[^/]+/?$', re.IGNORECASE),
41
  ]
42
 
43
- # --- Custom Index Page (Updated with Authentication Info) ---
44
- INDEX_PAGE_HTML = f"""
45
  <!DOCTYPE html>
46
  <html lang="en">
47
  <head>
@@ -49,25 +51,32 @@ INDEX_PAGE_HTML = f"""
49
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
50
  <title>Private GitHub Proxy</title>
51
  <style>
52
- body {{ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; line-height: 1.6; color: #333; max-width: 800px; margin: 40px auto; padding: 20px; }}
53
- .container {{ border: 1px solid #ddd; border-radius: 8px; padding: 20px 40px; background-color: #f9f9f9; }}
54
- .warning {{ color: #856404; background-color: #fff3cd; border: 1px solid #ffeeba; padding: 15px; border-radius: 4px; margin-bottom: 20px; }}
55
- h1, h2 {{ border-bottom: 1px solid #eaecef; padding-bottom: 0.3em; }}
56
- code {{ background-color: #eef; padding: 2px 4px; border-radius: 3px; font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace; }}
 
57
  </style>
58
  </head>
59
  <body>
60
  <div class="container">
61
  <h1>Private GitHub Reverse Proxy</h1>
 
 
 
62
  <div class="warning">
63
- <strong>Authentication Required:</strong> This is a private proxy. You must include your secret key in the URL to access content.
 
64
  </div>
65
  <h2>How to Use</h2>
66
- <p>To access GitHub content, prepend your secret key to the GitHub URL.</p>
67
  <p>For example, to clone a repository:</p>
68
- <code>git clone {{YOUR_PROXY_URL}}/{SECRET_KEY}/https://github.com/owner/repo.git</code>
69
  <p>Or to view a repository page:</p>
70
- <code>{{YOUR_PROXY_URL}}/{SECRET_KEY}/https://github.com/owner/repo</code>
 
 
71
  </div>
72
  </body>
73
  </html>
@@ -80,40 +89,61 @@ def is_url_allowed(url):
80
  return True
81
  return False
82
 
83
- # --- Core Proxy Logic (Updated with Authentication) ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
  @app.route('/', defaults={'path': ''}, methods=['GET', 'POST', 'PUT', 'DELETE'])
85
  @app.route('/<path:path>', methods=['GET', 'POST', 'PUT', 'DELETE'])
86
  def proxy(path):
87
  """
88
- Authenticates the request via a secret key in the path, then proxies
89
- the request to GitHub after validating it against a whitelist.
90
  """
91
- # Split the path to separate the secret key from the target URL.
92
- # e.g., "mysecretkey/https://github.com/user/repo" -> ["mysecretkey", "https://..."]
93
- path_parts = path.split('/', 1)
94
-
95
- # --- Authentication Check ---
96
- # The path must contain a key. If not, or if the key is wrong, deny access.
97
- if len(path_parts) < 1 or path_parts[0] != SECRET_KEY:
98
- logging.warning(f"Authentication failed for request from {request.remote_addr}. Path: '{path}'")
99
- return "<h1>401 Unauthorized</h1><p>A valid secret key is required in the URL path.</p>", 401
100
-
101
- # If the key is correct but there is no target URL, show the index page.
102
- # This happens when accessing /<secret_key>/
103
- if len(path_parts) == 1 or not path_parts[1]:
104
  return INDEX_PAGE_HTML, 200
105
 
106
- target_path = unquote(path_parts[1])
 
 
 
 
 
 
107
 
 
 
 
 
108
  # Prepend 'https://' if the scheme is missing.
109
- if not target_path.startswith(('http://', 'https://')):
110
- target_url = 'https://' + target_path
111
  else:
112
- target_url = target_path
113
 
114
  # Security check: Ensure the URL is in the whitelist.
115
  if not is_url_allowed(target_url):
116
- logging.warning(f"URL Denied! Key was correct, but pattern not matched: {target_url}")
117
  return "<h1>403 Forbidden</h1><p>Request blocked by proxy security policy.</p>", 403
118
 
119
  try:
@@ -129,19 +159,27 @@ def proxy(path):
129
  headers['Host'] = target_host
130
 
131
  try:
 
 
 
 
132
  resp = requests.request(
133
  method=request.method,
134
  url=target_url,
135
  headers=headers,
136
  data=request.get_data(),
137
  cookies=request.cookies,
138
- allow_redirects=False,
139
  stream=True,
140
- timeout=30
141
  )
 
 
142
  excluded_headers = ['content-encoding', 'content-length', 'transfer-encoding', 'connection']
143
  response_headers = [(name, value) for (name, value) in resp.raw.headers.items()
144
  if name.lower() not in excluded_headers]
 
 
145
  return Response(resp.iter_content(chunk_size=8192), resp.status_code, response_headers)
146
 
147
  except requests.exceptions.RequestException as e:
@@ -149,4 +187,4 @@ def proxy(path):
149
  return "An error occurred while proxying the request.", 502
150
 
151
  if __name__ == '__main__':
152
- app.run(host='0.0.0.0', port=7860)
 
1
  import re
 
2
  import os
3
+ import logging
4
  from flask import Flask, request, Response
5
  import requests
6
  from urllib.parse import urlparse, unquote
 
9
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
10
  app = Flask(__name__)
11
 
12
+ # --- Load Secret Key from Environment ---
13
+ PROXY_SECRET_KEY = os.environ.get('PROXY_SECRET_KEY')
14
+ if not PROXY_SECRET_KEY:
15
+ logging.error("PROXY_SECRET_KEY environment variable is not set!")
16
+ exit(1)
17
+
18
+ logging.info(f"Proxy service initialized with secret key authentication")
19
 
20
  # --- Whitelisted URL Patterns for GitHub ---
21
+ # This list defines all URL patterns that are permitted to be proxied.
22
+ # It's a security measure to prevent the proxy from being used to access unintended domains.
23
  ALLOWED_PATTERNS = [
24
  # Repositories: releases, archives, blobs, raw content
25
  re.compile(r'^https://github\.com/[^/]+/[^/]+/(?:releases|archive)/.*$', re.IGNORECASE),
 
42
  re.compile(r'^https://github\.com/[^/]+/[^/]+/?$', re.IGNORECASE),
43
  ]
44
 
45
+ # --- Custom Index Page ---
46
+ INDEX_PAGE_HTML = """
47
  <!DOCTYPE html>
48
  <html lang="en">
49
  <head>
 
51
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
52
  <title>Private GitHub Proxy</title>
53
  <style>
54
+ body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; line-height: 1.6; color: #333; max-width: 800px; margin: 40px auto; padding: 20px; }
55
+ .container { border: 1px solid #ddd; border-radius: 8px; padding: 20px 40px; background-color: #f9f9f9; }
56
+ .warning { color: #856404; background-color: #fff3cd; border: 1px solid #ffeeba; padding: 15px; border-radius: 4px; margin-bottom: 20px; }
57
+ .security { color: #721c24; background-color: #f8d7da; border: 1px solid #f5c6cb; padding: 15px; border-radius: 4px; margin-bottom: 20px; }
58
+ h1, h2 { border-bottom: 1px solid #eaecef; padding-bottom: 0.3em; }
59
+ code { background-color: #eef; padding: 2px 4px; border-radius: 3px; }
60
  </style>
61
  </head>
62
  <body>
63
  <div class="container">
64
  <h1>Private GitHub Reverse Proxy</h1>
65
+ <div class="security">
66
+ <strong>Security Notice:</strong> This is a private proxy service that requires authentication.
67
+ </div>
68
  <div class="warning">
69
+ <strong>Warning:</strong> You are accessing GitHub content through a reverse proxy.
70
+ All content served from this page is provided directly by GitHub.
71
  </div>
72
  <h2>How to Use</h2>
73
+ <p>To access GitHub content, you need to include the secret key in the URL path:</p>
74
  <p>For example, to clone a repository:</p>
75
+ <code>git clone {YOUR_PROXY_URL}/&lt;SECRET_KEY&gt;/https://github.com/owner/repo.git</code>
76
  <p>Or to view a repository page:</p>
77
+ <code>{YOUR_PROXY_URL}/&lt;SECRET_KEY&gt;/https://github.com/owner/repo</code>
78
+ <h2>Authentication Required</h2>
79
+ <p>This proxy requires a valid secret key to access. Contact the administrator for access credentials.</p>
80
  </div>
81
  </body>
82
  </html>
 
89
  return True
90
  return False
91
 
92
+ def extract_secret_and_url(path):
93
+ """Extract secret key and target URL from the path."""
94
+ # Remove leading slash if present
95
+ if path.startswith('/'):
96
+ path = path[1:]
97
+
98
+ # Split the path to extract secret key and URL
99
+ parts = path.split('/', 1)
100
+ if len(parts) < 2:
101
+ return None, None
102
+
103
+ secret_key = parts[0]
104
+ target_url = parts[1]
105
+
106
+ return secret_key, target_url
107
+
108
+ # --- Core Proxy Logic ---
109
  @app.route('/', defaults={'path': ''}, methods=['GET', 'POST', 'PUT', 'DELETE'])
110
  @app.route('/<path:path>', methods=['GET', 'POST', 'PUT', 'DELETE'])
111
  def proxy(path):
112
  """
113
+ Proxies requests to GitHub after validating secret key and URL whitelist.
114
+ It streams the response back to the client.
115
  """
116
+ # The full path might be URL-encoded, so we decode it.
117
+ target_path = unquote(request.full_path)
118
+ if target_path.startswith('/'):
119
+ target_path = target_path[1:]
120
+
121
+ # For the root path, display the custom warning page.
122
+ # Handle both '' (for a request to '/') and '?' (for a request to '/?').
123
+ if not target_path or target_path == '?':
 
 
 
 
 
124
  return INDEX_PAGE_HTML, 200
125
 
126
+ # Extract secret key and target URL
127
+ provided_secret, target_url_path = extract_secret_and_url(target_path)
128
+
129
+ # Validate secret key
130
+ if not provided_secret or provided_secret != PROXY_SECRET_KEY:
131
+ logging.warning(f"Access denied: Invalid or missing secret key from IP: {request.remote_addr}")
132
+ return "<h1>403 Forbidden</h1><p>Access denied: Invalid or missing authentication key.</p>", 403
133
 
134
+ if not target_url_path:
135
+ logging.warning(f"Access denied: No target URL provided from IP: {request.remote_addr}")
136
+ return "<h1>400 Bad Request</h1><p>No target URL provided.</p>", 400
137
+
138
  # Prepend 'https://' if the scheme is missing.
139
+ if not target_url_path.startswith(('http://', 'https://')):
140
+ target_url = 'https://' + target_url_path
141
  else:
142
+ target_url = target_url_path
143
 
144
  # Security check: Ensure the URL is in the whitelist.
145
  if not is_url_allowed(target_url):
146
+ logging.warning(f"URL Denied! No pattern matched: {target_url} from IP: {request.remote_addr}")
147
  return "<h1>403 Forbidden</h1><p>Request blocked by proxy security policy.</p>", 403
148
 
149
  try:
 
159
  headers['Host'] = target_host
160
 
161
  try:
162
+ # Log successful proxy request
163
+ logging.info(f"Proxying request to: {target_url} from IP: {request.remote_addr}")
164
+
165
+ # Stream the request to handle large files efficiently.
166
  resp = requests.request(
167
  method=request.method,
168
  url=target_url,
169
  headers=headers,
170
  data=request.get_data(),
171
  cookies=request.cookies,
172
+ allow_redirects=False, # Redirects are handled by the client.
173
  stream=True,
174
+ timeout=30 # Timeout for the connection.
175
  )
176
+
177
+ # Exclude headers that can interfere with streaming.
178
  excluded_headers = ['content-encoding', 'content-length', 'transfer-encoding', 'connection']
179
  response_headers = [(name, value) for (name, value) in resp.raw.headers.items()
180
  if name.lower() not in excluded_headers]
181
+
182
+ # Stream the response back to the original client.
183
  return Response(resp.iter_content(chunk_size=8192), resp.status_code, response_headers)
184
 
185
  except requests.exceptions.RequestException as e:
 
187
  return "An error occurred while proxying the request.", 502
188
 
189
  if __name__ == '__main__':
190
+ app.run(host='0.0.0.0', port=7860)