Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 4 additions & 23 deletions apps/webserver/reverse_proxy/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,29 +28,10 @@ python3 multi_reverse_proxy.py \
포트 번호가 기록되며, 이후 줄에는 각 포트 매핑이 나열됩니다. 같은 정보는 `--status-port`
옵션으로 지정한 포트에서 제공되는 웹 페이지에서도 확인할 수 있습니다.

`--status-user` 와 `--status-pass` 옵션을 함께 지정하면 상태 페이지에 접속했을 때 사용자 이름과 비밀번호를 입력할 수 있는 간단한 로그인 화면이 나타납니다. 올바른 값을 입력하면 현재 프록시 상태 정보를 볼 수 있습니다.

로그인 성공 후에는 `http://***.iptime.org`의 `--redirect-port` 값으로 설정된 포트로 리다이렉트됩니다.

### 로그인 테스트 예제

`login_client.py` 스크립트는 상태 페이지에 POST 요청을 보내 로그인 과정을 확인하는 간단한 예제입니다.

```bash
python3 login_client.py --base-url http://localhost:9000 --username admin --password secret
```

실행하면 다음과 같이 첫 단계에서 `/login` 경로로 POST 요청을 보내고, 응답 코드와 리다이렉트 위치, 쿠키 값을 출력합니다.

```python
login_url = f'{base_url}/login'
login_data = {
'username': username,
'password': password
}
print(f"[1] POST to {login_url}")
response = session.post(login_url, data=login_data, allow_redirects=False)
```
`--status-user` 와 `--status-pass` 옵션을 지정하면 `/cgi-bin/status_login.py` CGI
스크립트에서 입력 값과 비교하여 인증을 수행합니다. 웹 브라우저에서 해당 CGI
경로에 접속하여 사용자 이름과 비밀번호를 입력하면 올바른 경우 `status.txt`
파일의 내용이 출력됩니다.

### status.txt 조회 스크립트

Expand Down
38 changes: 38 additions & 0 deletions apps/webserver/reverse_proxy/cgi-bin/status_login.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Simple CGI login script to show status.txt on success."""
import cgi
import os

USER = os.environ.get("STATUS_USER", "admin")
PASS = os.environ.get("STATUS_PASS", "secret")
STATUS_FILE = os.environ.get("STATUS_FILE", "status.txt")

form = cgi.FieldStorage()
username = form.getfirst("username")
password = form.getfirst("password")

print("Content-Type: text/html\n")

if username is None or password is None:
print("<html><body>")
print("<h1>Login</h1>")
print("<form method='post'>")
print("Username: <input type='text' name='username'><br>")
print("Password: <input type='password' name='password'><br>")
print("<input type='submit' value='Login'>")
print("</form>")
print("</body></html>")
else:
if username == USER and password == PASS:
print("<pre>")
try:
with open(STATUS_FILE, "r", encoding="utf-8") as f:
print(cgi.escape(f.read()))
except Exception as e:
print(f"Error reading {STATUS_FILE}: {e}")
print("</pre>")
else:
print("<p style='color:red'>Invalid credentials</p>")
print("<a href=''>Try again</a>")
print("</body></html>")
77 changes: 18 additions & 59 deletions apps/webserver/reverse_proxy/multi_reverse_proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import urllib.request
import urllib.parse
import threading
import os


# hold mapping information (out_port -> target url) for status display
Expand Down Expand Up @@ -55,26 +56,11 @@ class ThreadingHTTPServer(socketserver.ThreadingMixIn, http.server.HTTPServer):
daemon_threads = True


class StatusHTTPRequestHandler(http.server.BaseHTTPRequestHandler):
protocol_version = 'HTTP/1.1'
class StatusHTTPRequestHandler(http.server.CGIHTTPRequestHandler):
"""Handler that serves proxy status and executes CGI scripts."""

def _show_login(self, invalid=False):
html = '<html><body><h1>Login</h1>'
if invalid:
html += '<p style="color:red">Invalid credentials</p>'
html += (
'<form method="post" action="/login">'
'Username: <input type="text" name="username"><br>'
'Password: <input type="password" name="password"><br>'
'<input type="submit" value="Login">'
'</form></body></html>'
)
body = html.encode('utf-8')
self.send_response(200)
self.send_header('Content-Type', 'text/html; charset=utf-8')
self.send_header('Content-Length', str(len(body)))
self.end_headers()
self.wfile.write(body)
protocol_version = 'HTTP/1.1'
cgi_directories = ['/cgi-bin']


def _show_status(self):
Expand All @@ -93,52 +79,28 @@ def _show_status(self):


def do_GET(self):
if getattr(self.server, 'requires_auth', False):
cookie = self.headers.get('Cookie', '')
if 'auth=1' not in cookie:
self._show_login()
return
if self.path.startswith('/cgi-bin/'):
return http.server.CGIHTTPRequestHandler.do_GET(self)
self._show_status()

def do_POST(self):

if self.path == '/login' and getattr(self.server, 'requires_auth', False):
length = int(self.headers.get('Content-Length', '0'))
body = self.rfile.read(length).decode('utf-8')
params = urllib.parse.parse_qs(body)
user = params.get('username', [''])[0]
pw = params.get('password', [''])[0]
if user == self.server.auth_user and pw == self.server.auth_pass:
# After successful login redirect the browser to the
# external address which is also stored in the cookie
port = getattr(self.server, 'redirect_port', 2281)
redirect_url = f'http://bigsoft.iptime.org:{port}'
self.send_response(303)
self.send_header('Set-Cookie', 'auth=1; Path=/')
self.send_header('Location', redirect_url)
self.send_header('Content-Length', '0')
self.end_headers()
return

else:
self._show_login(invalid=True)
else:
self.send_error(404)
if self.path.startswith('/cgi-bin/'):
return http.server.CGIHTTPRequestHandler.do_POST(self)
self.send_error(404)


def start_status_server(port, data, auth_user=None, auth_pass=None, redirect_port=2281):
def start_status_server(port, data, status_file, auth_user=None, auth_pass=None):
"""Start HTTP server for status page with optional CGI login."""
handler = StatusHTTPRequestHandler
server = ThreadingHTTPServer(('', port), handler)
server.data = data
server.status_port = port

server.redirect_port = redirect_port
if auth_user and auth_pass:
server.auth_user = auth_user
server.auth_pass = auth_pass
server.requires_auth = True
else:
server.requires_auth = False
os.environ['STATUS_FILE'] = status_file
if auth_user:
os.environ['STATUS_USER'] = auth_user
if auth_pass:
os.environ['STATUS_PASS'] = auth_pass

thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
Expand Down Expand Up @@ -185,8 +147,6 @@ def main():
help='username for status page basic auth')
parser.add_argument('--status-pass', dest='status_pass', default=None,
help='password for status page basic auth')
parser.add_argument('--redirect-port', type=int, default=2281,
help='port used to build login redirect URL')
args = parser.parse_args()

servers = []
Expand All @@ -204,10 +164,9 @@ def main():
status_srv = start_status_server(
args.status_port,
status_data,
args.status_file,
auth_user=args.status_user,
auth_pass=args.status_pass,

redirect_port=args.redirect_port,
)

try:
Expand Down
Loading