mirror of
https://git.mirrors.martin98.com/https://github.com/langgenius/dify.git
synced 2025-04-22 21:59:55 +08:00

Signed-off-by: yihong0618 <zouzou0208@gmail.com> Signed-off-by: -LAN- <laipz8200@outlook.com> Signed-off-by: xhe <xw897002528@gmail.com> Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: takatost <takatost@gmail.com> Co-authored-by: kurokobo <kuro664@gmail.com> Co-authored-by: Novice Lee <novicelee@NoviPro.local> Co-authored-by: zxhlyh <jasonapring2015@outlook.com> Co-authored-by: AkaraChen <akarachen@outlook.com> Co-authored-by: Yi <yxiaoisme@gmail.com> Co-authored-by: Joel <iamjoel007@gmail.com> Co-authored-by: JzoNg <jzongcode@gmail.com> Co-authored-by: twwu <twwu@dify.ai> Co-authored-by: Hiroshi Fujita <fujita-h@users.noreply.github.com> Co-authored-by: AkaraChen <85140972+AkaraChen@users.noreply.github.com> Co-authored-by: NFish <douxc512@gmail.com> Co-authored-by: Wu Tianwei <30284043+WTW0313@users.noreply.github.com> Co-authored-by: 非法操作 <hjlarry@163.com> Co-authored-by: Novice <857526207@qq.com> Co-authored-by: Hiroki Nagai <82458324+nagaihiroki-git@users.noreply.github.com> Co-authored-by: Gen Sato <52241300+halogen22@users.noreply.github.com> Co-authored-by: eux <euxuuu@gmail.com> Co-authored-by: huangzhuo1949 <167434202+huangzhuo1949@users.noreply.github.com> Co-authored-by: huangzhuo <huangzhuo1@xiaomi.com> Co-authored-by: lotsik <lotsik@mail.ru> Co-authored-by: crazywoola <100913391+crazywoola@users.noreply.github.com> Co-authored-by: nite-knite <nkCoding@gmail.com> Co-authored-by: Jyong <76649700+JohnJyong@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: gakkiyomi <gakkiyomi@aliyun.com> Co-authored-by: CN-P5 <heibai2006@gmail.com> Co-authored-by: CN-P5 <heibai2006@qq.com> Co-authored-by: Chuehnone <1897025+chuehnone@users.noreply.github.com> Co-authored-by: yihong <zouzou0208@gmail.com> Co-authored-by: Kevin9703 <51311316+Kevin9703@users.noreply.github.com> Co-authored-by: -LAN- <laipz8200@outlook.com> Co-authored-by: Boris Feld <lothiraldan@gmail.com> Co-authored-by: mbo <himabo@gmail.com> Co-authored-by: mabo <mabo@aeyes.ai> Co-authored-by: Warren Chen <warren.chen830@gmail.com> Co-authored-by: JzoNgKVO <27049666+JzoNgKVO@users.noreply.github.com> Co-authored-by: jiandanfeng <chenjh3@wangsu.com> Co-authored-by: zhu-an <70234959+xhdd123321@users.noreply.github.com> Co-authored-by: zhaoqingyu.1075 <zhaoqingyu.1075@bytedance.com> Co-authored-by: 海狸大師 <86974027+yenslife@users.noreply.github.com> Co-authored-by: Xu Song <xusong.vip@gmail.com> Co-authored-by: rayshaw001 <396301947@163.com> Co-authored-by: Ding Jiatong <dingjiatong@gmail.com> Co-authored-by: Bowen Liang <liangbowen@gf.com.cn> Co-authored-by: JasonVV <jasonwangiii@outlook.com> Co-authored-by: le0zh <newlight@qq.com> Co-authored-by: zhuxinliang <zhuxinliang@didiglobal.com> Co-authored-by: k-zaku <zaku99@outlook.jp> Co-authored-by: luckylhb90 <luckylhb90@gmail.com> Co-authored-by: hobo.l <hobo.l@binance.com> Co-authored-by: jiangbo721 <365065261@qq.com> Co-authored-by: 刘江波 <jiangbo721@163.com> Co-authored-by: Shun Miyazawa <34241526+miya@users.noreply.github.com> Co-authored-by: EricPan <30651140+Egfly@users.noreply.github.com> Co-authored-by: crazywoola <427733928@qq.com> Co-authored-by: sino <sino2322@gmail.com> Co-authored-by: Jhvcc <37662342+Jhvcc@users.noreply.github.com> Co-authored-by: lowell <lowell.hu@zkteco.in> Co-authored-by: Boris Polonsky <BorisPolonsky@users.noreply.github.com> Co-authored-by: Ademílson Tonato <ademilsonft@outlook.com> Co-authored-by: Ademílson Tonato <ademilson.tonato@refurbed.com> Co-authored-by: IWAI, Masaharu <iwaim.sub@gmail.com> Co-authored-by: Yueh-Po Peng (Yabi) <94939112+y10ab1@users.noreply.github.com> Co-authored-by: Jason <ggbbddjm@gmail.com> Co-authored-by: Xin Zhang <sjhpzx@gmail.com> Co-authored-by: yjc980121 <3898524+yjc980121@users.noreply.github.com> Co-authored-by: heyszt <36215648+hieheihei@users.noreply.github.com> Co-authored-by: Abdullah AlOsaimi <osaimiacc@gmail.com> Co-authored-by: Abdullah AlOsaimi <189027247+osaimi@users.noreply.github.com> Co-authored-by: Yingchun Lai <laiyingchun@apache.org> Co-authored-by: Hash Brown <hi@xzd.me> Co-authored-by: zuodongxu <192560071+zuodongxu@users.noreply.github.com> Co-authored-by: Masashi Tomooka <tmokmss@users.noreply.github.com> Co-authored-by: aplio <ryo.091219@gmail.com> Co-authored-by: Obada Khalili <54270856+obadakhalili@users.noreply.github.com> Co-authored-by: Nam Vu <zuzoovn@gmail.com> Co-authored-by: Kei YAMAZAKI <1715090+kei-yamazaki@users.noreply.github.com> Co-authored-by: TechnoHouse <13776377+deephbz@users.noreply.github.com> Co-authored-by: Riddhimaan-Senapati <114703025+Riddhimaan-Senapati@users.noreply.github.com> Co-authored-by: MaFee921 <31881301+2284730142@users.noreply.github.com> Co-authored-by: te-chan <t-nakanome@sakura-is.co.jp> Co-authored-by: HQidea <HQidea@users.noreply.github.com> Co-authored-by: Joshbly <36315710+Joshbly@users.noreply.github.com> Co-authored-by: xhe <xw897002528@gmail.com> Co-authored-by: weiwenyan-dev <154779315+weiwenyan-dev@users.noreply.github.com> Co-authored-by: ex_wenyan.wei <ex_wenyan.wei@tcl.com> Co-authored-by: engchina <12236799+engchina@users.noreply.github.com> Co-authored-by: engchina <atjapan2015@gmail.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: 呆萌闷油瓶 <253605712@qq.com> Co-authored-by: Kemal <kemalmeler@outlook.com> Co-authored-by: Lazy_Frog <4590648+lazyFrogLOL@users.noreply.github.com> Co-authored-by: Yi Xiao <54782454+YIXIAO0@users.noreply.github.com> Co-authored-by: Steven sun <98230804+Tuyohai@users.noreply.github.com> Co-authored-by: steven <sunzwj@digitalchina.com> Co-authored-by: Kalo Chin <91766386+fdb02983rhy@users.noreply.github.com> Co-authored-by: Katy Tao <34019945+KatyTao@users.noreply.github.com> Co-authored-by: depy <42985524+h4ckdepy@users.noreply.github.com> Co-authored-by: 胡春东 <gycm520@gmail.com> Co-authored-by: Junjie.M <118170653@qq.com> Co-authored-by: MuYu <mr.muzea@gmail.com> Co-authored-by: Naoki Takashima <39912547+takatea@users.noreply.github.com> Co-authored-by: Summer-Gu <37869445+gubinjie@users.noreply.github.com> Co-authored-by: Fei He <droxer.he@gmail.com> Co-authored-by: ybalbert001 <120714773+ybalbert001@users.noreply.github.com> Co-authored-by: Yuanbo Li <ybalbert@amazon.com> Co-authored-by: douxc <7553076+douxc@users.noreply.github.com> Co-authored-by: liuzhenghua <1090179900@qq.com> Co-authored-by: Wu Jiayang <62842862+Wu-Jiayang@users.noreply.github.com> Co-authored-by: Your Name <you@example.com> Co-authored-by: kimjion <45935338+kimjion@users.noreply.github.com> Co-authored-by: AugNSo <song.tiankai@icloud.com> Co-authored-by: llinvokerl <38915183+llinvokerl@users.noreply.github.com> Co-authored-by: liusurong.lsr <liusurong.lsr@alibaba-inc.com> Co-authored-by: Vasu Negi <vasu-negi@users.noreply.github.com> Co-authored-by: Hundredwz <1808096180@qq.com> Co-authored-by: Xiyuan Chen <52963600+GareArc@users.noreply.github.com>
312 lines
9.5 KiB
Python
312 lines
9.5 KiB
Python
import json
|
|
import logging
|
|
import random
|
|
import re
|
|
import string
|
|
import subprocess
|
|
import time
|
|
import uuid
|
|
from collections.abc import Generator, Mapping
|
|
from datetime import datetime
|
|
from hashlib import sha256
|
|
from typing import TYPE_CHECKING, Any, Optional, Union, cast
|
|
from zoneinfo import available_timezones
|
|
|
|
from flask import Response, stream_with_context
|
|
from flask_restful import fields # type: ignore
|
|
|
|
from configs import dify_config
|
|
from core.app.features.rate_limiting.rate_limit import RateLimitGenerator
|
|
from core.file import helpers as file_helpers
|
|
from extensions.ext_redis import redis_client
|
|
|
|
if TYPE_CHECKING:
|
|
from models.account import Account
|
|
|
|
|
|
def run(script):
|
|
return subprocess.getstatusoutput("source /root/.bashrc && " + script)
|
|
|
|
|
|
class AppIconUrlField(fields.Raw):
|
|
def output(self, key, obj):
|
|
if obj is None:
|
|
return None
|
|
|
|
from models.model import App, IconType, Site
|
|
|
|
if isinstance(obj, dict) and "app" in obj:
|
|
obj = obj["app"]
|
|
|
|
if isinstance(obj, App | Site) and obj.icon_type == IconType.IMAGE.value:
|
|
return file_helpers.get_signed_file_url(obj.icon)
|
|
return None
|
|
|
|
|
|
class AvatarUrlField(fields.Raw):
|
|
def output(self, key, obj):
|
|
if obj is None:
|
|
return None
|
|
|
|
from models.account import Account
|
|
|
|
if isinstance(obj, Account) and obj.avatar is not None:
|
|
return file_helpers.get_signed_file_url(obj.avatar)
|
|
return None
|
|
|
|
|
|
class TimestampField(fields.Raw):
|
|
def format(self, value) -> int:
|
|
return int(value.timestamp())
|
|
|
|
|
|
def email(email):
|
|
# Define a regex pattern for email addresses
|
|
pattern = r"^[\w\.!#$%&'*+\-/=?^_`{|}~]+@([\w-]+\.)+[\w-]{2,}$"
|
|
# Check if the email matches the pattern
|
|
if re.match(pattern, email) is not None:
|
|
return email
|
|
|
|
error = "{email} is not a valid email.".format(email=email)
|
|
raise ValueError(error)
|
|
|
|
|
|
def uuid_value(value):
|
|
if value == "":
|
|
return str(value)
|
|
|
|
try:
|
|
uuid_obj = uuid.UUID(value)
|
|
return str(uuid_obj)
|
|
except ValueError:
|
|
error = "{value} is not a valid uuid.".format(value=value)
|
|
raise ValueError(error)
|
|
|
|
|
|
def alphanumeric(value: str):
|
|
# check if the value is alphanumeric and underlined
|
|
if re.match(r"^[a-zA-Z0-9_]+$", value):
|
|
return value
|
|
|
|
raise ValueError(f"{value} is not a valid alphanumeric value")
|
|
|
|
|
|
def timestamp_value(timestamp):
|
|
try:
|
|
int_timestamp = int(timestamp)
|
|
if int_timestamp < 0:
|
|
raise ValueError
|
|
return int_timestamp
|
|
except ValueError:
|
|
error = "{timestamp} is not a valid timestamp.".format(timestamp=timestamp)
|
|
raise ValueError(error)
|
|
|
|
|
|
class StrLen:
|
|
"""Restrict input to an integer in a range (inclusive)"""
|
|
|
|
def __init__(self, max_length, argument="argument"):
|
|
self.max_length = max_length
|
|
self.argument = argument
|
|
|
|
def __call__(self, value):
|
|
length = len(value)
|
|
if length > self.max_length:
|
|
error = "Invalid {arg}: {val}. {arg} cannot exceed length {length}".format(
|
|
arg=self.argument, val=value, length=self.max_length
|
|
)
|
|
raise ValueError(error)
|
|
|
|
return value
|
|
|
|
|
|
class FloatRange:
|
|
"""Restrict input to an float in a range (inclusive)"""
|
|
|
|
def __init__(self, low, high, argument="argument"):
|
|
self.low = low
|
|
self.high = high
|
|
self.argument = argument
|
|
|
|
def __call__(self, value):
|
|
value = _get_float(value)
|
|
if value < self.low or value > self.high:
|
|
error = "Invalid {arg}: {val}. {arg} must be within the range {lo} - {hi}".format(
|
|
arg=self.argument, val=value, lo=self.low, hi=self.high
|
|
)
|
|
raise ValueError(error)
|
|
|
|
return value
|
|
|
|
|
|
class DatetimeString:
|
|
def __init__(self, format, argument="argument"):
|
|
self.format = format
|
|
self.argument = argument
|
|
|
|
def __call__(self, value):
|
|
try:
|
|
datetime.strptime(value, self.format)
|
|
except ValueError:
|
|
error = "Invalid {arg}: {val}. {arg} must be conform to the format {format}".format(
|
|
arg=self.argument, val=value, format=self.format
|
|
)
|
|
raise ValueError(error)
|
|
|
|
return value
|
|
|
|
|
|
def _get_float(value):
|
|
try:
|
|
return float(value)
|
|
except (TypeError, ValueError):
|
|
raise ValueError("{} is not a valid float".format(value))
|
|
|
|
|
|
def timezone(timezone_string):
|
|
if timezone_string and timezone_string in available_timezones():
|
|
return timezone_string
|
|
|
|
error = "{timezone_string} is not a valid timezone.".format(timezone_string=timezone_string)
|
|
raise ValueError(error)
|
|
|
|
|
|
def generate_string(n):
|
|
letters_digits = string.ascii_letters + string.digits
|
|
result = ""
|
|
for i in range(n):
|
|
result += random.choice(letters_digits)
|
|
|
|
return result
|
|
|
|
|
|
def extract_remote_ip(request) -> str:
|
|
if request.headers.get("CF-Connecting-IP"):
|
|
return cast(str, request.headers.get("Cf-Connecting-Ip"))
|
|
elif request.headers.getlist("X-Forwarded-For"):
|
|
return cast(str, request.headers.getlist("X-Forwarded-For")[0])
|
|
else:
|
|
return cast(str, request.remote_addr)
|
|
|
|
|
|
def generate_text_hash(text: str) -> str:
|
|
hash_text = str(text) + "None"
|
|
return sha256(hash_text.encode()).hexdigest()
|
|
|
|
|
|
def compact_generate_response(response: Union[Mapping, Generator, RateLimitGenerator]) -> Response:
|
|
if isinstance(response, dict):
|
|
return Response(response=json.dumps(response), status=200, mimetype="application/json")
|
|
else:
|
|
|
|
def generate() -> Generator:
|
|
yield from response
|
|
|
|
return Response(stream_with_context(generate()), status=200, mimetype="text/event-stream")
|
|
|
|
|
|
class TokenManager:
|
|
@classmethod
|
|
def generate_token(
|
|
cls,
|
|
token_type: str,
|
|
account: Optional["Account"] = None,
|
|
email: Optional[str] = None,
|
|
additional_data: Optional[dict] = None,
|
|
) -> str:
|
|
if account is None and email is None:
|
|
raise ValueError("Account or email must be provided")
|
|
|
|
account_id = account.id if account else None
|
|
account_email = account.email if account else email
|
|
|
|
if account_id:
|
|
old_token = cls._get_current_token_for_account(account_id, token_type)
|
|
if old_token:
|
|
if isinstance(old_token, bytes):
|
|
old_token = old_token.decode("utf-8")
|
|
cls.revoke_token(old_token, token_type)
|
|
|
|
token = str(uuid.uuid4())
|
|
token_data = {"account_id": account_id, "email": account_email, "token_type": token_type}
|
|
if additional_data:
|
|
token_data.update(additional_data)
|
|
|
|
expiry_minutes = dify_config.model_dump().get(f"{token_type.upper()}_TOKEN_EXPIRY_MINUTES")
|
|
if expiry_minutes is None:
|
|
raise ValueError(f"Expiry minutes for {token_type} token is not set")
|
|
token_key = cls._get_token_key(token, token_type)
|
|
expiry_time = int(expiry_minutes * 60)
|
|
redis_client.setex(token_key, expiry_time, json.dumps(token_data))
|
|
|
|
if account_id:
|
|
cls._set_current_token_for_account(account_id, token, token_type, expiry_minutes)
|
|
|
|
return token
|
|
|
|
@classmethod
|
|
def _get_token_key(cls, token: str, token_type: str) -> str:
|
|
return f"{token_type}:token:{token}"
|
|
|
|
@classmethod
|
|
def revoke_token(cls, token: str, token_type: str):
|
|
token_key = cls._get_token_key(token, token_type)
|
|
redis_client.delete(token_key)
|
|
|
|
@classmethod
|
|
def get_token_data(cls, token: str, token_type: str) -> Optional[dict[str, Any]]:
|
|
key = cls._get_token_key(token, token_type)
|
|
token_data_json = redis_client.get(key)
|
|
if token_data_json is None:
|
|
logging.warning(f"{token_type} token {token} not found with key {key}")
|
|
return None
|
|
token_data: Optional[dict[str, Any]] = json.loads(token_data_json)
|
|
return token_data
|
|
|
|
@classmethod
|
|
def _get_current_token_for_account(cls, account_id: str, token_type: str) -> Optional[str]:
|
|
key = cls._get_account_token_key(account_id, token_type)
|
|
current_token: Optional[str] = redis_client.get(key)
|
|
return current_token
|
|
|
|
@classmethod
|
|
def _set_current_token_for_account(
|
|
cls, account_id: str, token: str, token_type: str, expiry_hours: Union[int, float]
|
|
):
|
|
key = cls._get_account_token_key(account_id, token_type)
|
|
expiry_time = int(expiry_hours * 60 * 60)
|
|
redis_client.setex(key, expiry_time, token)
|
|
|
|
@classmethod
|
|
def _get_account_token_key(cls, account_id: str, token_type: str) -> str:
|
|
return f"{token_type}:account:{account_id}"
|
|
|
|
|
|
class RateLimiter:
|
|
def __init__(self, prefix: str, max_attempts: int, time_window: int):
|
|
self.prefix = prefix
|
|
self.max_attempts = max_attempts
|
|
self.time_window = time_window
|
|
|
|
def _get_key(self, email: str) -> str:
|
|
return f"{self.prefix}:{email}"
|
|
|
|
def is_rate_limited(self, email: str) -> bool:
|
|
key = self._get_key(email)
|
|
current_time = int(time.time())
|
|
window_start_time = current_time - self.time_window
|
|
|
|
redis_client.zremrangebyscore(key, "-inf", window_start_time)
|
|
attempts = redis_client.zcard(key)
|
|
|
|
if attempts and int(attempts) >= self.max_attempts:
|
|
return True
|
|
return False
|
|
|
|
def increment_rate_limit(self, email: str):
|
|
key = self._get_key(email)
|
|
current_time = int(time.time())
|
|
|
|
redis_client.zadd(key, {current_time: current_time})
|
|
redis_client.expire(key, self.time_window * 2)
|