Nginx Web 应用程序重定向在表单提交时失败(重定向到包含 %2C 的虚假 URL)

lph*_*lph 5 html python sqlalchemy nginx flask

首先,我对大部分内容都很陌生。我在远程(Linode)Ubuntu 服务器上运行 Flask 应用程序。该应用程序与 Gunicorn 和 Nginx 一起提供。

在最近的更新之后,当我提交表单时,重定向开始表现得很奇怪。

具体来说,我重定向到的 url 如下所示: mywebsite.com%2Cmywebsite.com/my/expected/redirect

(要明确,mywebsite.com是基本 url。在提交表单时,我希望被重定向到mywebsite.com/my/expected/redirect,但是我被重定向到mywebsite.com%2Cmywebsite.com/my/expected /重定向)

这仅发生在服务器上。在我的本地主机上,不存在此问题。

这仅在我提交表单(通过 wtf 表单)时发生。提交表单并获取虚假 url 后,如果我手动输入预期的 url,我会得到预期的页面 - 即表单提交成功的确认闪存消息,并且数据库包含通过表单提交的新数据。所以我猜 db.commit() 正在工作。

并非所有链接都已损坏,我可以成功地从一个页面导航到另一个页面。例如,我可以通过站点范围的 html 头文件中的这个链接从我的导航栏导航回家。

href="{{ url_for('home') }}"
Run Code Online (Sandbox Code Playgroud)

我可以通过这些按钮导航到其他页面(我知道我应该使用 url_for 而不是硬链接)

<button class="w3-bar-item w3-button w3-black" onclick="window.location.href = '/recipes';">Recipes</button>
<button class="w3-bar-item w3-button w3-yellow" onclick="window.location.href = '/newrecipe';">Add New Recipe</button>

Run Code Online (Sandbox Code Playgroud)

有三种不同的形式都会产生此错误。在下面你会看到它们是NewRecipe、NewBoilAddition、StartBoilTimer

我曾尝试将 _external=True 添加到 url_for 重定向,但是由于内部服务器错误而失败

从研究中,我知道 %2C 是一个未转义的逗号,因此我怀疑正在将 URL 列表传递给重定向,或者我遗漏了一些其他编码问题。我特别困惑,因为这个错误才刚刚出现,当时所有这些代码都可以正常工作。我已尝试回滚到我确定此错误不存在的提交,并且该错误在该提交中是可重现的。这让我怀疑是 gunicorn/nginx 负责,因为应用程序代码本身很好。

我超级困惑。任何想法表示赞赏!

初始化.py

from flask import Flask, render_template, request, g, redirect, url_for, flash
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.hybrid import hybrid_property

from forms import NewRecipe, NewBoilAddition, StartBoilTimer

# import sqlite3
import pandas as pd
import json
import sys
import os
import config

from datetime import datetime, timedelta

from flask_gtts import gtts

app = Flask(__name__)

app.config['SECRET_KEY'] = 'xxx'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///site.db'

db = SQLAlchemy(app)

class User(db.Model):

    __tablename__ = 'users'

    id          = db.Column(db.Integer, primary_key=True)
    username    = db.Column(db.String(20), unique=True, nullable=False)
    password    = db.Column(db.String(60), nullable=False)
    bevvys      = db.relationship('Bevvy_list', backref='brewer', lazy=True)

    def __repr__(self):
        return f"User('{self.id}', '{self.username}')"

class Bevvy_list(db.Model):

    __tablename__ = 'bevvy_list'

    id              = db.Column(db.Integer, primary_key=True)
    name            = db.Column(db.String(100), unique=True, nullable=False) #Optimise length of this string
    style           = db.Column(db.String(60), nullable=False) #TODO add in validated field, linking to style guide
    abbreviation    = db.Column(db.String(60), nullable=False)
    iteration       = db.Column(db.Integer, nullable=False)
    iteration_of    = db.Column(db.Integer, nullable=False) #id of parent beer, if iteration = 1 then this equals self.id
    batch_size      = db.Column(db.Integer, nullable=False)
    brewday_date    = db.Column(db.DateTime, nullable=False)
    url             = db.Column(db.String(20), nullable=True)
    user_id         = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
    boils           = db.relationship('Boil', backref='bevvy', lazy=True)

    def __repr__(self):
        return f"Bevvy_list('{self.id}', '{self.name}')"

class Boil(db.Model):

    __tablename__ = 'boil'

    id              = db.Column(db.Integer, primary_key=True)
    description     = db.Column(db.String(100), unique=False, nullable=False)
    time            = db.Column(db.Integer, unique=False, nullable=False)
    end_datetime    = db.Column(db.DateTime, unique=False, nullable=True)
    brew_id         = db.Column(db.Integer, db.ForeignKey('bevvy_list.id'), nullable=False)

    def __repr__(self):
        return f"Boil('{self.id}', '{self.description}', '{self.time}', '{self.end_datetime}')"

##########################################
#          Build app
##########################################

# home navigation
@app.route("/")
def index():
    return render_template("home.html")

@app.route("/recipes")
def recipes():
    bevs_data = Bevvy_list.query.all()
    return render_template("recipes.html", bevs_data=bevs_data)

@app.route("/edit/<int:recipe_id>", methods=['GET', 'POST'])
def edit(recipe_id):
    form = NewBoilAddition(brew_id=recipe_id)
    if form.validate_on_submit():
        boil_addition = Boil(description=form.description.data, \
                            time=form.time.data, \
                            brew_id=recipe_id,\
                            end_datetime=None)
        db.session.add(boil_addition)
        db.session.commit()
        flash(f'Boil Addition {form.description.data} successfully added ', 'success_boil')
        return redirect(url_for('edit', recipe_id=recipe_id))

    start_timer_form = StartBoilTimer()
    boil_additions = Boil.query.filter(Boil.brew_id == recipe_id).all()
    if start_timer_form.validate_on_submit():
        end_all_timers = datetime.now() + timedelta(minutes=max_value(Boil.query.filter(Boil.brew_id == recipe_id).with_entities(Boil.time).all()))
        for addition in boil_additions:
            addition_end_datetime = end_all_timers - timedelta(minutes=addition.time)
            db.session.query(Boil).filter(Boil.id == addition.id).update({'end_datetime':addition_end_datetime})
            db.session.commit()
        return redirect(url_for('edit', recipe_id=recipe_id))

    recipe = Bevvy_list.query.filter(Bevvy_list.id == recipe_id).all()

    return render_template("edit.html", recipe=recipe[0], form=form, \
        boil_additions=boil_additions, start_timer_form=start_timer_form)

# add a new recipe
@app.route("/newrecipe", methods=['GET', 'POST'])
def new_recipe():
    form = NewRecipe()
    recipe_list = Bevvy_list.query.all()
    list_recipes =[]
    for recipe in recipe_list:
        row = [recipe.id, recipe.name, recipe.brewday_date.strftime("%-d %b %y")]
        list_recipes.append(row)
    if form.validate_on_submit():
        recipe = Bevvy_list(name=form.name.data, style=form.style.data, abbreviation=form.abbreviation.data, iteration=form.iteration.data, \
            iteration_of=form.iteration_of.data, batch_size=form.batch_size.data, brewday_date=form.brewday_date.data, user_id=form.user_id.data)
        db.session.add(recipe)
        href = "/edit/" + str(db.session.query(Bevvy_list).order_by(Bevvy_list.id.desc()).first().id)
        db.session.query(Bevvy_list).order_by(Bevvy_list.id.desc()).first().url = href
        db.session.commit()
        flash(f'Recipe for {form.name.data} successfully added', 'success')
        return redirect(url_for('home'), _external=True)

    return render_template("new_recipe.html", title="New Recipe", form=form, modal=list_recipes)

if __name__ == "__main__":
    app.run()
Run Code Online (Sandbox Code Playgroud)

new_recipe.html

<!DOCTYPE html>

{% extends "layouts.html" %}

{% block content %}

  <!-- Modal -->  
<div class="modal fade" id="exampleModalLong" tabindex="-1" role="dialog" aria-labelledby="exampleModalLongTitle" aria-hidden="true">
  <div class="modal-dialog" role="document">
    <div class="modal-content">
      <div class="modal-header">
        <h5 class="modal-title" id="exampleModalLongTitle">All Recipes</h5>
        <button type="button" class="close" data-dismiss="modal" aria-label="Close">
          <span aria-hidden="true">&times;</span>
        </button>
      </div>
      <div class="modal-body">
        <table class="table table-striped table-hover table-sm">
          <thead class="thead-dark">
            <tr>
              <th scope="col">ID</th>
              <th scope="col">Name</th>
              <th scope="col">Brewday Date</th>
            </tr>
          </thead>
          <tbody>
            {% for list in modal %}
              <tr>
              {% for item in list %}
                <td>{{ item }}</td>
              {% endfor %}
              </tr>
            {% endfor %}
          </tbody>
        </table>
      </div>
      <div class="modal-footer">
        <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
      </div>
    </div>
  </div>
</div>

<div class="content-section">
  <form method="POST" action="">
    {{  form.hidden_tag() }}
    <fieldset class="form-group">
      <legend class="border-bottom mb-4">Add New Recipe</legend>
      <div class="form-group"> 
        {{ form.name.label(class="form-control-label") }}
        {% if form.name.errors %}
          {{ form.name(class="form-control form-control-lg is-invalid") }}
          <div class="invalid-feedback">
            {% for error in form.name.errors%}
              <span>{{ error }}</span>
            {% endfor %}
          </div>
        {% else %}
          {{ form.name(class="form-control form-control-lg") }}
        {% endif %}

      </div>
      <div class="form-group"> 
        {{ form.style.label(class="form-control-label") }}
        {% if form.style.errors %}
          {{ form.style(class="form-control form-control-lg is-invalid") }}
          <div class="invalid-feedback">
            {% for error in form.style.errors%}
              <span>{{ error }}</span>
            {% endfor %}
          </div>
        {% else %}
          {{ form.style(class="form-control form-control-lg") }}
        {% endif %}
      </div>
      <div class="form-group"> 
        {{ form.abbreviation.label(class="form-control-label") }}
        {% if form.abbreviation.errors %}
          {{ form.abbreviation(class="form-control form-control-lg is-invalid") }}
          <div class="invalid-feedback">
            {% for error in form.abbreviation.errors%}
              <span>{{ error }}</span>
            {% endfor %}
          </div>
        {% else %}
          {{ form.abbreviation(class="form-control form-control-lg") }}
        {% endif %}
      </div>
      <div class="form-group"> 
        {{ form.iteration.label(class="form-control-label") }}
        {% if form.iteration.errors %}
          {{ form.iteration(class="form-control form-control-lg is-invalid") }}
          <div class="invalid-feedback">
            {% for error in form.iteration.errors%}
              <span>{{ error }}</span>
            {% endfor %}
          </div>
        {% else %}
          {{ form.iteration(class="form-control form-control-lg") }}
        {% endif %}
      </div>
      <div class="form-group"> 
        {{ form.iteration_of.label(class="form-control-label") }}
        {% if form.iteration_of.errors %}
          {{ form.iteration_of(class="form-control form-control-lg is-invalid") }}
          <div class="invalid-feedback">
            {% for error in form.iteration_of.errors%}
              <span>{{ error }}</span>
            {% endfor %}
          </div>
        {% else %}
          {{ form.iteration_of(class="form-control form-control-lg") }}
        {% endif %}
        <button type="button" class="btn btn-primary" data-toggle="modal" data-target="#exampleModalLong">
          Show recipe list for reference
        </button>
      </div>
      <div class="form-group"> 
        {{ form.batch_size.label(class="form-control-label") }}
        {% if form.batch_size.errors %}
          {{ form.batch_size(class="form-control form-control-lg is-invalid") }}
          <div class="invalid-feedback">
            {% for error in form.batch_size.errors%}
              <span>{{ error }}</span>
            {% endfor %}
          </div>
        {% else %}
          {{ form.batch_size(class="form-control form-control-lg") }}
        {% endif %} 
      </div>
      <div class="form-group"> 
        {{ form.brewday_date.label(class="form-control-label") }}
        {% if form.brewday_date.errors %}
          {{ form.brewday_date(class="form-control form-control-lg is-invalid") }}
          <div class="invalid-feedback">
            {% for error in form.brewday_date.errors%}
              <span>{{ error }}</span>
            {% endfor %}
          </div>
        {% else %}
          {{ form.brewday_date(class="form-control form-control-lg") }}
        {% endif %}
      </div>
      <div class="form-check"> 
        {% if form.user_id.errors %}
          {% for subfield in form.user_id %}
          <table>
            <td>
                <tr>{{ subfield(class="form-check-input is-invalid") }}</tr>
                <tr>{{ subfield.label(class="form-check-label") }}</tr><br>
            </td>
          </table>
          {% endfor %}
          <div class="invalid-feedback">
            {% for error in form.user_id.errors%}
              <span>{{ error }}</span>
            {% endfor %}
          </div>
        {% else %}
          {% for subfield in form.user_id %}
            <table>
              <td>
                  <tr>{{ subfield(class="form-check-input") }}</tr>
                  <tr>{{ subfield.label(class="form-check-label") }}</tr><br>
              </td>
            </table>
          {% endfor %}
        {% endif %}
      </div>
    </fieldset>
    <div class="form-group"> 
      {{ form.submit(class="btn btn-outline-info")  }}
    </div>
  </form>
</div>

{% endblock content %}
Run Code Online (Sandbox Code Playgroud)

编辑:

/etc/systemd/system/MYAPP.service

[Unit]
Description=Gunicorn instance to serve zym_app
After=network.target

[Service]
User=root
Group=www-data
WorkingDirectory=/home/lph/zym_app/zym
Environment="PATH=/home/lph/zym_app/zym/zym_app_env/bin"
ExecStart=/home/lph/zym_app/zym/zym_app_env/bin/gunicorn --workers 3 --bind unix:zym_app.sock -m 007 wsgi:app

[Install]
WantedBy=multi-user.target


Run Code Online (Sandbox Code Playgroud)

和 /etc/nginx/sites-available/MYAPP

server {
    listen 80;
    server_name mywebsite.com www.mywebsite.com;

    location / {
        include proxy_params;
        proxy_pass http://unix:/home/lph/zym_app/zym/zym_app.sock;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}



Run Code Online (Sandbox Code Playgroud)

编辑 2:看起来我错过了 2 并直接进入了 3 ..

编辑 3:解决了这个问题后,我现在认为这是 nginx 配置的问题。我在服务器上测试了烧瓶应用程序

gunicorn --bind 0.0.0.0:5000 wsgi:app
Run Code Online (Sandbox Code Playgroud)

这解决了观察到的重定向问题,应用程序按预期工作。因此,烧瓶和 gunicorn 部分工作正常 - 只留下 nginx。

我还从 .validate_on_submit() 条件中删除了重定向行,这也解决了通过 nginx 运行时的问题。新的表单数据已成功发布到数据库,但是由于缺少重定向,我不得不在提交后手动刷新页面。

这是我的 nginx 配置文件。/etc/nginx/sites-available/zym_app

server {
    listen 80;    
    server_name mywebsite.com www.mywebsite.com;

    location / {
        include proxy_params;
        proxy_pass http://unix:/home/ZYM/zym/zym_app.sock;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_headers_hash_max_size 512;
        proxy_headers_hash_bucket_size 128;
    }
}
Run Code Online (Sandbox Code Playgroud)

编辑 4:在尝试了许多 nginx 配置参数更改后,我注释掉了有问题的重定向行,现在当我单击导航栏链接到主页时出现问题。这是该链接中的 html。

href="{{ url_for('home') }}"
Run Code Online (Sandbox Code Playgroud)