Sebebeb commited on
Commit
aae0beb
·
verified ·
1 Parent(s): bf5848f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +161 -107
app.py CHANGED
@@ -1,123 +1,177 @@
1
  import os
2
- from urllib.parse import urljoin, quote
3
 
4
- import requests
5
- from bs4 import BeautifulSoup
6
- from flask import Flask, request, Response
7
 
8
  app = Flask(__name__)
9
 
10
- DEFAULT_URL = "https://example.com"
11
- HEADERS = {
12
- "User-Agent": (
13
- "Mozilla/5.0 (compatible; HFSimpleBrowser/1.0; "
14
- "+https://huggingface.co/spaces)"
15
- )
16
- }
17
-
18
- TOPBAR_TEMPLATE = """
19
- <div id="__browser_topbar" style="
20
- position:sticky;top:0;z-index:999999;
21
- display:flex;gap:8px;align-items:center;
22
- padding:10px 14px;background:#1e1e1e;
23
- font-family:-apple-system,Helvetica,Arial,sans-serif;">
24
- <form action="/browse" method="get" style="display:flex;gap:8px;flex:1;">
25
- <input name="url" value="{url}" style="
26
- flex:1;padding:8px 10px;border-radius:6px;border:none;
27
- font-size:14px;" />
28
- <button type="submit" style="
29
- padding:8px 16px;border-radius:6px;border:none;
30
- background:#4f8cff;color:white;font-size:14px;cursor:pointer;">
31
- Go
32
- </button>
33
- </form>
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  </div>
35
- """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
 
37
- ERROR_TEMPLATE = """
38
- <div style="font-family:-apple-system,Helvetica,Arial,sans-serif;
39
- padding:24px;color:#c0392b;">
40
- <h3>Couldn't load that page</h3>
41
- <p>{message}</p>
42
- </div>
43
- """
44
-
45
- PROXY_ATTRS = [
46
- ("a", "href"),
47
- ("form", "action"),
48
- ]
49
-
50
- RESOURCE_ATTRS = [
51
- ("link", "href"),
52
- ("script", "src"),
53
- ("img", "src"),
54
- ("source", "src"),
55
- ]
56
-
57
-
58
- def render_home(url=DEFAULT_URL):
59
- return f"""<!DOCTYPE html>
60
- <html><head><meta charset="utf-8"><title>Simple Browser</title></head>
61
- <body style="margin:0;">
62
- {TOPBAR_TEMPLATE.format(url=url)}
63
- <iframe src="/browse?url={quote(url)}" style="
64
- width:100%;height:calc(100vh - 52px);border:none;"></iframe>
65
  </body></html>"""
66
 
67
 
68
  @app.route("/")
69
  def home():
70
- return render_home()
71
-
72
-
73
- @app.route("/browse")
74
- def browse():
75
- target = request.args.get("url", "").strip()
76
- if not target:
77
- return render_home()
78
- if not target.startswith(("http://", "https://")):
79
- target = "https://" + target
80
-
81
- try:
82
- resp = requests.get(target, headers=HEADERS, timeout=10)
83
- except requests.RequestException as e:
84
- body = ERROR_TEMPLATE.format(message=str(e))
85
- return f"""<!DOCTYPE html><html><body style="margin:0;">
86
- {TOPBAR_TEMPLATE.format(url=target)}{body}</body></html>"""
87
-
88
- content_type = resp.headers.get("Content-Type", "")
89
- if "text/html" not in content_type:
90
- # Non-HTML resource (image, css, etc requested directly) — pass through
91
- return Response(resp.content, content_type=content_type)
92
-
93
- soup = BeautifulSoup(resp.text, "html.parser")
94
- base_tag = soup.find("base")
95
- base_url = base_tag["href"] if base_tag and base_tag.get("href") else resp.url
96
-
97
- # Resources load straight from the origin site in the visitor's browser
98
- for tag_name, attr in RESOURCE_ATTRS:
99
- for tag in soup.find_all(tag_name):
100
- if tag.get(attr):
101
- tag[attr] = urljoin(base_url, tag[attr])
102
-
103
- # Links/forms route back through this server so navigation stays proxied
104
- for tag_name, attr in PROXY_ATTRS:
105
- for tag in soup.find_all(tag_name):
106
- if tag.get(attr):
107
- absolute = urljoin(base_url, tag[attr])
108
- if absolute.startswith(("http://", "https://")):
109
- tag[attr] = "/browse?url=" + quote(absolute, safe="")
110
- if tag.get("target"):
111
- del tag["target"]
112
-
113
- body_html = str(soup)
114
- page = f"""<!DOCTYPE html><html><body style="margin:0;">
115
- {TOPBAR_TEMPLATE.format(url=target)}
116
- <div>{body_html}</div>
117
- </body></html>"""
118
- return Response(page, content_type="text/html")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119
 
120
 
121
  if __name__ == "__main__":
122
  port = int(os.environ.get("PORT", 7860))
123
- app.run(host="0.0.0.0", port=port)
 
1
  import os
2
+ import threading
3
 
4
+ from flask import Flask, request, Response, jsonify
5
+ from playwright.sync_api import sync_playwright
 
6
 
7
  app = Flask(__name__)
8
 
9
+ VIEWPORT = {"width": 1280, "height": 800}
10
+
11
+ _lock = threading.Lock()
12
+ _state = {"playwright": None, "browser": None, "page": None}
13
+
14
+
15
+ def get_page():
16
+ if _state["page"] is None:
17
+ _state["playwright"] = sync_playwright().start()
18
+ _state["browser"] = _state["playwright"].chromium.launch(
19
+ headless=True,
20
+ args=["--no-sandbox", "--disable-dev-shm-usage"],
21
+ )
22
+ context = _state["browser"].new_context(viewport=VIEWPORT)
23
+ _state["page"] = context.new_page()
24
+ _state["page"].goto("https://example.com", wait_until="domcontentloaded")
25
+ return _state["page"]
26
+
27
+
28
+ HOME = """<!DOCTYPE html>
29
+ <html><head><meta charset="utf-8"><title>Remote Browser</title>
30
+ <style>
31
+ html,body{margin:0;height:100%;background:#111;
32
+ font-family:-apple-system,Helvetica,Arial,sans-serif;}
33
+ #bar{display:flex;gap:8px;padding:10px 14px;background:#1e1e1e;}
34
+ #bar input{flex:1;padding:8px 10px;border-radius:6px;border:none;font-size:14px;}
35
+ #bar button{padding:8px 16px;border-radius:6px;border:none;
36
+ background:#4f8cff;color:#fff;font-size:14px;cursor:pointer;}
37
+ #stage{width:100%;height:calc(100vh - 52px);overflow:auto;background:#000;
38
+ display:flex;justify-content:center;}
39
+ #view{cursor:pointer;max-width:100%;display:block;}
40
+ </style></head>
41
+ <body>
42
+ <div id="bar">
43
+ <input id="url" value="https://example.com" />
44
+ <button onclick="go()">Go</button>
45
+ <button onclick="back()">&#8592;</button>
46
  </div>
47
+ <div id="stage"><img id="view" src="/screenshot?t=0" /></div>
48
+ <script>
49
+ const img = document.getElementById('view');
50
+ const urlBox = document.getElementById('url');
51
+ const vw = __VW__, vh = __VH__;
52
+
53
+ function refresh() { img.src = '/screenshot?t=' + Date.now(); }
54
+
55
+ async function go() {
56
+ await fetch('/go', {method:'POST', headers:{'Content-Type':'application/json'},
57
+ body: JSON.stringify({url: urlBox.value})});
58
+ setTimeout(refresh, 400);
59
+ }
60
+ async function back() {
61
+ await fetch('/back', {method:'POST'});
62
+ setTimeout(refresh, 400);
63
+ }
64
 
65
+ img.addEventListener('click', async (e) => {
66
+ const rect = img.getBoundingClientRect();
67
+ const x = (e.clientX - rect.left) / rect.width * vw;
68
+ const y = (e.clientY - rect.top) / rect.height * vh;
69
+ await fetch('/click', {method:'POST', headers:{'Content-Type':'application/json'},
70
+ body: JSON.stringify({x, y})});
71
+ setTimeout(refresh, 300);
72
+ });
73
+
74
+ document.addEventListener('keydown', async (e) => {
75
+ if (document.activeElement === urlBox) return;
76
+ e.preventDefault();
77
+ await fetch('/key', {method:'POST', headers:{'Content-Type':'application/json'},
78
+ body: JSON.stringify({key: e.key})});
79
+ refresh();
80
+ });
81
+
82
+ document.getElementById('stage').addEventListener('wheel', async (e) => {
83
+ await fetch('/scroll', {method:'POST', headers:{'Content-Type':'application/json'},
84
+ body: JSON.stringify({dy: e.deltaY})});
85
+ refresh();
86
+ });
87
+
88
+ setInterval(refresh, 1000);
89
+ </script>
 
 
 
90
  </body></html>"""
91
 
92
 
93
  @app.route("/")
94
  def home():
95
+ html = HOME.replace("__VW__", str(VIEWPORT["width"])).replace(
96
+ "__VH__", str(VIEWPORT["height"])
97
+ )
98
+ return html
99
+
100
+
101
+ @app.route("/screenshot")
102
+ def screenshot():
103
+ with _lock:
104
+ page = get_page()
105
+ try:
106
+ data = page.screenshot(type="jpeg", quality=60)
107
+ except Exception:
108
+ data = b""
109
+ return Response(data, content_type="image/jpeg")
110
+
111
+
112
+ @app.route("/go", methods=["POST"])
113
+ def go():
114
+ url = (request.json or {}).get("url", "").strip()
115
+ if not url:
116
+ return jsonify(ok=False)
117
+ if not url.startswith(("http://", "https://")):
118
+ url = "https://" + url
119
+ with _lock:
120
+ page = get_page()
121
+ try:
122
+ page.goto(url, wait_until="domcontentloaded", timeout=20000)
123
+ except Exception as e:
124
+ return jsonify(ok=False, error=str(e))
125
+ return jsonify(ok=True)
126
+
127
+
128
+ @app.route("/back", methods=["POST"])
129
+ def back():
130
+ with _lock:
131
+ page = get_page()
132
+ try:
133
+ page.go_back(timeout=10000)
134
+ except Exception:
135
+ pass
136
+ return jsonify(ok=True)
137
+
138
+
139
+ @app.route("/click", methods=["POST"])
140
+ def click():
141
+ data = request.json or {}
142
+ with _lock:
143
+ page = get_page()
144
+ try:
145
+ page.mouse.click(data.get("x", 0), data.get("y", 0))
146
+ except Exception:
147
+ pass
148
+ return jsonify(ok=True)
149
+
150
+
151
+ @app.route("/key", methods=["POST"])
152
+ def key():
153
+ data = request.json or {}
154
+ with _lock:
155
+ page = get_page()
156
+ try:
157
+ page.keyboard.press(data.get("key", ""))
158
+ except Exception:
159
+ pass
160
+ return jsonify(ok=True)
161
+
162
+
163
+ @app.route("/scroll", methods=["POST"])
164
+ def scroll():
165
+ data = request.json or {}
166
+ with _lock:
167
+ page = get_page()
168
+ try:
169
+ page.mouse.wheel(0, data.get("dy", 0))
170
+ except Exception:
171
+ pass
172
+ return jsonify(ok=True)
173
 
174
 
175
  if __name__ == "__main__":
176
  port = int(os.environ.get("PORT", 7860))
177
+ app.run(host="0.0.0.0", port=port, threaded=False)