修复偏移问题,修复代理问题

This commit is contained in:
2026-04-15 00:22:12 +08:00
parent 570af7c225
commit f0f9d658eb
33 changed files with 1621 additions and 1763 deletions

View File

@@ -7,6 +7,8 @@ import requests
from urllib.parse import urlparse
from app.models import SystemConfig, db
from app.utils.octoprint_client import OctoPrintClient
from app.models import PrintFile
import os
printer_bp = Blueprint('printer', __name__, url_prefix='/printer')
@@ -35,20 +37,69 @@ def status():
return render_template('printer/status.html', status=status_data, job=job_data, error=error)
def get_gcode_dir():
conf = SystemConfig.query.filter_by(key='gcode_upload_folder').first()
if conf and conf.value and os.path.exists(conf.value):
return conf.value
return current_app.config['UPLOAD_FOLDER']
@printer_bp.route('/prepare')
@login_required
def prepare():
client = get_octo_client()
from app.models import PrintFile
import os
# Query only the sliced GCode files belonging to the current user
user_files = PrintFile.query.filter_by(user_id=current_user.id, status='sliced').order_by(PrintFile.created_at.desc()).all()
files = []
error = None
gcode_dir = get_gcode_dir()
client = get_octo_client()
octo_files_dict = {}
if client:
try:
res = client.get_files()
files = res.get('files', [])
octo_resp = client.get_files()
for item in octo_resp.get('files', []):
octo_files_dict[item.get('name')] = item
except Exception as e:
error = str(e)
else:
pass
for f in user_files:
gcode_filename = f.filename.rsplit('.', 1)[0] + '.gcode'
gcode_path = os.path.join(gcode_dir, gcode_filename)
size = 0
if os.path.exists(gcode_path):
size = os.path.getsize(gcode_path)
# Upload to OctoPrint if not found but exists locally
if client and gcode_filename not in octo_files_dict and size > 0:
try:
resp = client.upload_file('local', gcode_path, gcode_filename)
uploaded_loc = resp.get('files', {}).get('local', {})
if gcode_filename in uploaded_loc:
octo_files_dict[gcode_filename] = uploaded_loc[gcode_filename]
except Exception as e:
pass
octo_info = octo_files_dict.get(gcode_filename, {})
analysis = octo_info.get('gcodeAnalysis', None)
files.append({
'id': f.id,
'name': f.original_filename.rsplit('.', 1)[0] + '.gcode',
'type': 'machinecode',
'size': size,
'origin': 'local',
'path': gcode_filename,
'gcodeAnalysis': analysis
})
error = None
if not get_octo_client():
error = "OctoPrint is not configured."
return render_template('printer/prepare.html', files=files, error=error)
@printer_bp.route('/api/print_file', methods=['POST'])
@@ -98,6 +149,54 @@ def api_command():
return jsonify({"success": False, "error": str(e)})
return jsonify({"success": False, "error": "Invalid client or command"})
@printer_bp.route('/api/upload_gcode', methods=['POST'])
@login_required
def upload_gcode():
from app.models import PrintFile
import os
import uuid
from werkzeug.utils import secure_filename
if 'file' not in request.files:
return jsonify({"success": False, "error": "No file part"}), 400
file = request.files['file']
if file.filename == '':
return jsonify({"success": False, "error": "No selected file"}), 400
if not file.filename.lower().endswith(('.gcode', '.gco', '.g')):
return jsonify({"success": False, "error": "Only standard .gcode files are supported."}), 400
sec_name = secure_filename(file.filename)
random_prefix = uuid.uuid4().hex[:8]
# We save pseudo-STL filename so that our conventional parser works (replaces .ext with .gcode)
# i.e., "some_file.gcode" -> pseudo .stl tracker
pseudo_stl_filename = f"{random_prefix}_{sec_name.rsplit('.', 1)[0]}.stl"
gcode_filename = f"{random_prefix}_{sec_name.rsplit('.', 1)[0]}.gcode"
gcode_path = os.path.join(get_gcode_dir(), gcode_filename)
file.save(gcode_path)
print_file = PrintFile(
user_id=current_user.id,
original_filename=file.filename, # keep original GCode name
filename=pseudo_stl_filename,
file_type='stl',
status='sliced'
)
db.session.add(print_file)
db.session.commit()
client = get_octo_client()
if client:
try:
client.upload_file('local', gcode_path, gcode_filename)
except Exception:
pass
return jsonify({"success": True})
@printer_bp.route('/octo_config', methods=['GET', 'POST'])
@login_required
def octo_config():
@@ -141,9 +240,9 @@ def octo_embed():
embed_url = url_for('printer.octo_proxy') if url and url.value else None
return render_template('printer/octo_embed.html', embed_url=embed_url)
@printer_bp.route('/proxy', defaults={'path': ''}, websocket=True)
@printer_bp.route('/proxy', defaults={'path': ''}, websocket=True, strict_slashes=False)
@printer_bp.route('/proxy/<path:path>', websocket=True)
@printer_bp.route('/proxy', defaults={'path': ''}, methods=['GET', 'POST', 'PUT', 'DELETE', 'PATCH'])
@printer_bp.route('/proxy', defaults={'path': ''}, methods=['GET', 'POST', 'PUT', 'DELETE', 'PATCH'], strict_slashes=False)
@printer_bp.route('/proxy/<path:path>', methods=['GET', 'POST', 'PUT', 'DELETE', 'PATCH'])
@login_required
def octo_proxy(path):
@@ -159,7 +258,7 @@ def octo_proxy(path):
# print("----- REQUEST HEADERS -----")
# for k, v in request.headers:
# print(f"{k}: {v}")
# print("----- END HEADERS -----")
# print("----- END REQUEST HEADERS -----")
# --- WebSocket Proxy Logic ---
if request.headers.get('Upgrade', '').lower() == 'websocket':
@@ -278,17 +377,26 @@ def octo_proxy(path):
# Build headers for reverse proxy based on nginx config reference
parsed_base = urlparse(base_url)
headers = {k: v for k, v in request.headers if k.lower() not in ['host', 'content-length']}
headers = {k: v for k, v in request.headers if k.lower() not in ['host', 'content-length', 'origin', 'referer']}
# NGINX equivalent proxy headers
# print(f"Proxying to: {target_url}")
# Spoof Host, Origin, and Referer to match the backend URL completely
# This prevents Tornado's strict Origin vs Host CSRF/CORS validation from failing
# headers['Host'] = parsed_base.netloc
headers['Host'] = request.host
if 'Origin' in request.headers:
headers['Origin'] = base_url
if 'Referer' in request.headers:
headers['Referer'] = f"{base_url}/{path}"
headers['X-Real-IP'] = request.remote_addr
headers['X-Real-Port'] = str(request.environ.get('REMOTE_PORT', ''))
forwarded_for = request.headers.get('X-Forwarded-For', '')
headers['X-Forwarded-For'] = f"{forwarded_for}, {request.remote_addr}" if forwarded_for else request.remote_addr
headers['X-Forwarded-Protocol'] = request.scheme
headers['X-Forwarded-Proto'] = request.scheme
headers['X-Script-Name'] = "/printer/proxy"
headers['X-Forwarded-Host'] = request.host
headers['X-Forwarded-Port'] = str(request.environ.get('SERVER_PORT', '80'))
@@ -299,6 +407,27 @@ def octo_proxy(path):
if request.headers.get('Connection'):
headers['Connection'] = request.headers.get('Connection')
# OctoPrint requires an X-CSRF-Token header matching the csrf_token_* cookie for POST/PUT/DELETE
if request.method not in ['GET', 'HEAD', 'OPTIONS'] and 'X-CSRF-Token' not in request.headers:
for cookie_name, cookie_value in request.cookies.items():
if cookie_name.startswith('csrf_token_'):
headers['X-CSRF-Token'] = cookie_value
break
# if path == 'api/login':
# print("----- SEND HEADERS -----")
# for k, v in headers.items():
# if k in request.headers.keys():
# print(f"{k} :(from request): {request.headers[k]} (to send): {v}")
# else:
# print(f"{k} :(from request): None (to send): {v}")
# for k,v in request.headers.items():
# if k not in headers.keys():
# print(f"{k} :(from request): {request.headers[k]} (to send): None")
# print("----- END SEND HEADERS -----")
try:
# proxy_connect_timeout 60s, proxy_read_timeout 600s
resp = requests.request(
@@ -316,7 +445,15 @@ def octo_proxy(path):
# Strip headers that might break the iframe or framing
excluded_headers = ['content-encoding', 'content-length', 'transfer-encoding', 'connection', 'x-frame-options', 'content-security-policy']
response_headers = [(name, value) for (name, value) in resp.headers.items() if name.lower() not in excluded_headers]
# We must use raw headers to prevent requests from joining multiple Set-Cookie headers with a comma
# Joining Set-Cookie with a comma breaks standard cookie parsing in the browser due to commas in dates
response_headers = [(name, value) for name, value in resp.raw.headers.items()
if name.lower() not in excluded_headers and name.lower() != 'set-cookie']
for cookie in resp.raw.headers.get_all('set-cookie', []):
# ensure we preserve the proxy path override
response_headers.append(('Set-Cookie', cookie))
def generate():
for chunk in resp.iter_content(chunk_size=8192):