diff --git a/.env.example b/.env.example
index 68c958e4f..9c4eb1191 100644
--- a/.env.example
+++ b/.env.example
@@ -43,11 +43,13 @@ UVICORN_PORT = 8000
## If USE_CUSTOM_JSON_DEFAULT is set True, all following programs will use the JSON config
# USE_CUSTOM_JSON_FOR_V2RAYN=False
# USE_CUSTOM_JSON_FOR_V2RAYNG=True
+# USE_CUSTOM_JSON_FOR_STREISAND=False
## Set headers for subscription
# SUB_PROFILE_TITLE = "Susbcription"
# SUB_SUPPORT_URL = "https://t.me/support"
# SUB_UPDATE_INTERVAL = "12"
+# RANDOMIZE_SUBSCRIPTION_CONFIGS = True
# SQLALCHEMY_DATABASE_URL = "sqlite:///db.sqlite3"
diff --git a/app/dashboard/build/locales/en.json b/app/dashboard/build/locales/en.json
index 09f49343f..a81f47b59 100644
--- a/app/dashboard/build/locales/en.json
+++ b/app/dashboard/build/locales/en.json
@@ -105,6 +105,7 @@
"hostsDialog.host.wildcard": "Use * to generate a random string (works for wildcard domain names)",
"hostsDialog.sockopt": "Sockopt",
"hostsDialog.muxEnable": "Enable MUX",
+ "hostsDialog.randomUserAgent":"Use random user agent",
"hostsDialog.allowinsecure": "Allow Insecure",
"hostsDialog.fragment": "Fragment pattern",
"hostsDialog.fragment.info": "Correct pattern: length,interval,packets",
diff --git a/app/dashboard/build/locales/fa.json b/app/dashboard/build/locales/fa.json
index 57e79ace5..0b0fbd88a 100644
--- a/app/dashboard/build/locales/fa.json
+++ b/app/dashboard/build/locales/fa.json
@@ -106,6 +106,7 @@
"hostsDialog.host.wildcard": "از * برای ساخت عبارت تصادفی استفاده کنید (برای نامهای wildcard کار میکند)",
"hostsDialog.sockopt": "Sockopt",
"hostsDialog.muxEnable": "فعالسازی MUX",
+ "hostsDialog.randomUserAgent":"استفاده از User Agent تصادفی",
"hostsDialog.allowinsecure": "Allow Insecure",
"hostsDialog.fragment": "الگو فرگمنت",
"hostsDialog.fragment.info": "length,interval,packet (e.g. 10-100,100-200,tlshello)",
diff --git a/app/dashboard/build/locales/ru.json b/app/dashboard/build/locales/ru.json
index 7d3409aa0..6e52cec13 100644
--- a/app/dashboard/build/locales/ru.json
+++ b/app/dashboard/build/locales/ru.json
@@ -103,6 +103,7 @@
"hostsDialog.host.wildcard": "Используйте *, чтобы сгенерировать случайную строку (работает для wildcard доменов)",
"hostsDialog.sockopt": "Sockopt",
"hostsDialog.muxEnable": "Давать возможность MUX",
+ "hostsDialog.randomUserAgent":"Use random user agent",
"hostsDialog.allowinsecure": "Allow Insecure",
"hostsDialog.fragment": "Шаблон фрагмента",
"hostsDialog.fragment.info": "length,interval,packet (e.g. 10-100,100-200,tlshello)",
diff --git a/app/dashboard/build/locales/zh.json b/app/dashboard/build/locales/zh.json
index f9817e286..65789ad45 100644
--- a/app/dashboard/build/locales/zh.json
+++ b/app/dashboard/build/locales/zh.json
@@ -97,6 +97,7 @@
"hostsDialog.fingerprint": "指纹",
"hostsDialog.sockopt": "Sockopt",
"hostsDialog.muxEnable": "使能够 MUX",
+ "hostsDialog.randomUserAgent":"Use random user agent",
"hostsDialog.allowinsecure": "Allow Insecure",
"hostsDialog.fragment": "碎片图案",
"hostsDialog.fragment.info": "length,interval,packet (e.g. 10-100,100-200,tlshello)",
diff --git a/app/dashboard/public/locales/en.json b/app/dashboard/public/locales/en.json
index 09f49343f..a81f47b59 100644
--- a/app/dashboard/public/locales/en.json
+++ b/app/dashboard/public/locales/en.json
@@ -105,6 +105,7 @@
"hostsDialog.host.wildcard": "Use * to generate a random string (works for wildcard domain names)",
"hostsDialog.sockopt": "Sockopt",
"hostsDialog.muxEnable": "Enable MUX",
+ "hostsDialog.randomUserAgent":"Use random user agent",
"hostsDialog.allowinsecure": "Allow Insecure",
"hostsDialog.fragment": "Fragment pattern",
"hostsDialog.fragment.info": "Correct pattern: length,interval,packets",
diff --git a/app/dashboard/public/locales/fa.json b/app/dashboard/public/locales/fa.json
index 57e79ace5..0b0fbd88a 100644
--- a/app/dashboard/public/locales/fa.json
+++ b/app/dashboard/public/locales/fa.json
@@ -106,6 +106,7 @@
"hostsDialog.host.wildcard": "از * برای ساخت عبارت تصادفی استفاده کنید (برای نامهای wildcard کار میکند)",
"hostsDialog.sockopt": "Sockopt",
"hostsDialog.muxEnable": "فعالسازی MUX",
+ "hostsDialog.randomUserAgent":"استفاده از User Agent تصادفی",
"hostsDialog.allowinsecure": "Allow Insecure",
"hostsDialog.fragment": "الگو فرگمنت",
"hostsDialog.fragment.info": "length,interval,packet (e.g. 10-100,100-200,tlshello)",
diff --git a/app/dashboard/public/locales/ru.json b/app/dashboard/public/locales/ru.json
index 7d3409aa0..6e52cec13 100644
--- a/app/dashboard/public/locales/ru.json
+++ b/app/dashboard/public/locales/ru.json
@@ -103,6 +103,7 @@
"hostsDialog.host.wildcard": "Используйте *, чтобы сгенерировать случайную строку (работает для wildcard доменов)",
"hostsDialog.sockopt": "Sockopt",
"hostsDialog.muxEnable": "Давать возможность MUX",
+ "hostsDialog.randomUserAgent":"Use random user agent",
"hostsDialog.allowinsecure": "Allow Insecure",
"hostsDialog.fragment": "Шаблон фрагмента",
"hostsDialog.fragment.info": "length,interval,packet (e.g. 10-100,100-200,tlshello)",
diff --git a/app/dashboard/public/locales/zh.json b/app/dashboard/public/locales/zh.json
index f9817e286..65789ad45 100644
--- a/app/dashboard/public/locales/zh.json
+++ b/app/dashboard/public/locales/zh.json
@@ -97,6 +97,7 @@
"hostsDialog.fingerprint": "指纹",
"hostsDialog.sockopt": "Sockopt",
"hostsDialog.muxEnable": "使能够 MUX",
+ "hostsDialog.randomUserAgent":"Use random user agent",
"hostsDialog.allowinsecure": "Allow Insecure",
"hostsDialog.fragment": "碎片图案",
"hostsDialog.fragment.info": "length,interval,packet (e.g. 10-100,100-200,tlshello)",
diff --git a/app/dashboard/src/components/HostsDialog.tsx b/app/dashboard/src/components/HostsDialog.tsx
index 9eb8efad3..89d79d058 100644
--- a/app/dashboard/src/components/HostsDialog.tsx
+++ b/app/dashboard/src/components/HostsDialog.tsx
@@ -119,6 +119,7 @@ const hostsSchema = z.record(
allowinsecure: z.boolean().nullable().default(false),
is_disabled: z.boolean().default(true),
fragment_setting: z.string().nullable(),
+ random_user_agent: z.boolean().default(false),
security: z.string(),
alpn: z.string(),
fingerprint: z.string(),
@@ -175,6 +176,7 @@ const AccordionInbound: FC = ({
allowinsecure: false,
is_disabled: false,
fragment_setting: "",
+ random_user_agent: false,
security: "inbound_default",
alpn: "",
fingerprint: "",
@@ -920,6 +922,28 @@ const AccordionInbound: FC = ({
)}
+
+
+ {t("hostsDialog.randomUserAgent")}
+
+ {accordionErrors &&
+ accordionErrors[index]?.random_user_agent && (
+
+ {accordionErrors[index]?.random_user_agent?.message}
+
+ )}
+
diff --git a/app/db/crud.py b/app/db/crud.py
index cb2edb1d7..1affebb46 100644
--- a/app/db/crud.py
+++ b/app/db/crud.py
@@ -85,6 +85,7 @@ def update_hosts(db: Session, inbound_tag: str, modified_hosts: List[ProxyHostMo
is_disabled=host.is_disabled,
mux_enable=host.mux_enable,
fragment_setting=host.fragment_setting,
+ random_user_agent=host.random_user_agent,
) for host in modified_hosts
]
db.commit()
diff --git a/app/db/migrations/versions/305943d779c4_add_h3_to_alpn_enum.py b/app/db/migrations/versions/305943d779c4_add_h3_to_alpn_enum.py
new file mode 100644
index 000000000..6ff6d48e9
--- /dev/null
+++ b/app/db/migrations/versions/305943d779c4_add_h3_to_alpn_enum.py
@@ -0,0 +1,115 @@
+"""add h3 to alpn enum
+
+Revision ID: 305943d779c4
+Revises: 31f92220c0d0
+Create Date: 2024-07-03 19:27:15.282711
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = '305943d779c4'
+down_revision = '31f92220c0d0'
+branch_labels = None
+depends_on = None
+
+
+# Describing of enum
+enum_name = "alpn"
+temp_enum_name = f"temp_{enum_name}"
+old_values = ("none", "h2", "http/1.1", "h2,http/1.1")
+new_values = ("h3", "h3,h2", "h3,h2,http/1.1", *old_values)
+# on downgrade
+downgrade_from = ("h3", "h3,h2", "h3,h2,http/1.1", "")
+downgrade_to = "none"
+old_type = sa.Enum(*old_values, name=enum_name)
+new_type = sa.Enum(*new_values, name=enum_name)
+temp_type = sa.Enum(*new_values, name=temp_enum_name)
+
+
+# Describing of table
+table_name = "hosts"
+column_name = "alpn"
+temp_table = sa.sql.table(
+ table_name,
+ sa.Column(
+ column_name,
+ new_type,
+ nullable=False
+ )
+)
+
+
+def upgrade():
+ # temp type to use instead of old one
+ temp_type.create(op.get_bind(), checkfirst=False)
+
+ # changing of column type from old enum to new one.
+ # SQLite will create temp table for this
+ with op.batch_alter_table(table_name) as batch_op:
+ batch_op.alter_column(
+ column_name,
+ existing_type=old_type,
+ type_=temp_type,
+ existing_nullable=False,
+ postgresql_using=f"{column_name}::text::{temp_enum_name}"
+ )
+
+ # remove old enum, create new enum
+ old_type.drop(op.get_bind(), checkfirst=False)
+ new_type.create(op.get_bind(), checkfirst=False)
+
+ # changing of column type from temp enum to new one.
+ # SQLite will create temp table for this
+ with op.batch_alter_table(table_name) as batch_op:
+ batch_op.alter_column(
+ column_name,
+ existing_type=temp_type,
+ type_=new_type,
+ existing_nullable=False,
+ postgresql_using=f"{column_name}::text::{enum_name}"
+ )
+
+ # remove temp enum
+ temp_type.drop(op.get_bind(), checkfirst=False)
+
+
+def downgrade():
+ # old enum don't have new value anymore.
+ # before downgrading from new enum to old one,
+ # we should replace new value from new enum with
+ # somewhat of old values from old enum
+ update_query = (
+ temp_table
+ .update()
+ .where(temp_table.c.alpn.in_(downgrade_from))
+ .values(alpn=downgrade_to)
+ )
+ op.execute(update_query)
+
+ temp_type.create(op.get_bind(), checkfirst=False)
+
+ with op.batch_alter_table(table_name) as batch_op:
+ batch_op.alter_column(
+ column_name,
+ existing_type=new_type,
+ type_=temp_type,
+ existing_nullable=False,
+ postgresql_using=f"{column_name}::text::{temp_enum_name}"
+ )
+
+ new_type.drop(op.get_bind(), checkfirst=False)
+ old_type.create(op.get_bind(), checkfirst=False)
+
+ with op.batch_alter_table(table_name) as batch_op:
+ batch_op.alter_column(
+ column_name,
+ existing_type=temp_type,
+ type_=old_type,
+ existing_nullable=False,
+ postgresql_using=f"{column_name}::text::{enum_name}"
+ )
+
+ temp_type.drop(op.get_bind(), checkfirst=False)
diff --git a/app/db/migrations/versions/31f92220c0d0_add_support_random_user_agent.py b/app/db/migrations/versions/31f92220c0d0_add_support_random_user_agent.py
new file mode 100644
index 000000000..65c9b3412
--- /dev/null
+++ b/app/db/migrations/versions/31f92220c0d0_add_support_random_user_agent.py
@@ -0,0 +1,28 @@
+"""Add Support Random User-Agent
+
+Revision ID: 31f92220c0d0
+Revises: 4f045f53bef8
+Create Date: 2024-06-01 21:28:33.310627
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = '31f92220c0d0'
+down_revision = '4f045f53bef8'
+branch_labels = None
+depends_on = None
+
+
+def upgrade() -> None:
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.add_column('hosts', sa.Column('random_user_agent', sa.Boolean(), server_default='0', nullable=False))
+ # ### end Alembic commands ###
+
+
+def downgrade() -> None:
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.drop_column('hosts', 'random_user_agent')
+ # ### end Alembic commands ###
diff --git a/app/db/migrations/versions/51e941ed9018_deactive_user_status.py b/app/db/migrations/versions/51e941ed9018_deactive_user_status.py
index cbd4ed9a5..1a5193d2b 100644
--- a/app/db/migrations/versions/51e941ed9018_deactive_user_status.py
+++ b/app/db/migrations/versions/51e941ed9018_deactive_user_status.py
@@ -41,6 +41,9 @@
def upgrade():
+ status_enum = sa.Enum('active', 'limited', 'expired', name='status')
+ status_enum.create(op.get_bind())
+
# temp type to use instead of old one
temp_type.create(op.get_bind(), checkfirst=False)
diff --git a/app/db/migrations/versions/7cbe9d91ac11_proxyhost_security_added.py b/app/db/migrations/versions/7cbe9d91ac11_proxyhost_security_added.py
index 18b1c3e0e..02b6e5000 100644
--- a/app/db/migrations/versions/7cbe9d91ac11_proxyhost_security_added.py
+++ b/app/db/migrations/versions/7cbe9d91ac11_proxyhost_security_added.py
@@ -17,6 +17,9 @@
def upgrade() -> None:
+ proxyhostsecurity_enum = sa.Enum('inbound_default', 'none', 'tls', name='proxyhostsecurity')
+ proxyhostsecurity_enum.create(op.get_bind())
+
# ### commands auto generated by Alembic - please adjust! ###
op.add_column(
"hosts",
diff --git a/app/db/migrations/versions/c106bb40c861_alpn_fingerprint_hosts.py b/app/db/migrations/versions/c106bb40c861_alpn_fingerprint_hosts.py
index 55691911c..e713922b0 100644
--- a/app/db/migrations/versions/c106bb40c861_alpn_fingerprint_hosts.py
+++ b/app/db/migrations/versions/c106bb40c861_alpn_fingerprint_hosts.py
@@ -17,6 +17,11 @@
def upgrade() -> None:
+ proxyhostalpn_enum = sa.Enum('none', 'h2', 'http/1.1', 'h2,http/1.1', name='proxyhostalpn')
+ proxyhostalpn_enum.create(op.get_bind(), checkfirst=True)
+
+ proxyhostfingerprint_enum = sa.Enum('none', 'chrome', 'firefox', 'safari', 'ios', 'android', 'edge', '360', 'qq', 'random', 'randomized', name='proxyhostfingerprint')
+ proxyhostfingerprint_enum.create(op.get_bind(), checkfirst=True)
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('hosts', sa.Column('alpn', sa.Enum('none', 'h2', 'http/1.1', 'h2,http/1.1', name='proxyhostalpn'), server_default='none', nullable=False))
op.add_column('hosts', sa.Column('fingerprint', sa.Enum('none', 'chrome', 'firefox', 'safari', 'ios', 'android', 'edge', '360', 'qq', 'random', 'randomized', name='proxyhostfingerprint'), server_default='none', nullable=False))
diff --git a/app/db/migrations/versions/d02dcfbf1517_add_userusageresetlogs_model_and_data_.py b/app/db/migrations/versions/d02dcfbf1517_add_userusageresetlogs_model_and_data_.py
index c1bf6670c..5a204a730 100644
--- a/app/db/migrations/versions/d02dcfbf1517_add_userusageresetlogs_model_and_data_.py
+++ b/app/db/migrations/versions/d02dcfbf1517_add_userusageresetlogs_model_and_data_.py
@@ -17,6 +17,9 @@
def upgrade() -> None:
+ userdatalimitresetstrategy = sa.Enum('no_reset', 'day', 'week', 'month', 'year', name='userdatalimitresetstrategy')
+ userdatalimitresetstrategy.create(op.get_bind())
+
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('user_usage_logs',
sa.Column('id', sa.Integer(), nullable=False),
diff --git a/app/db/models.py b/app/db/models.py
index 65d94bf9d..0d20f834c 100644
--- a/app/db/models.py
+++ b/app/db/models.py
@@ -3,9 +3,10 @@
from sqlalchemy import (JSON, BigInteger, Boolean, Column, DateTime, Enum,
Float, ForeignKey, Integer, String, Table,
- UniqueConstraint)
+ UniqueConstraint, func)
+from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.orm import relationship
-from sqlalchemy.sql.expression import text
+from sqlalchemy.sql.expression import text, select
from app import xray
from app.db.base import Base
@@ -46,7 +47,7 @@ class User(Base):
nullable=False,
default=UserDataLimitResetStrategy.no_reset,
)
- usage_logs = relationship("UserUsageResetLogs", back_populates="user")
+ usage_logs = relationship("UserUsageResetLogs", back_populates="user") # maybe rename it to reset_usage_logs?
expire = Column(Integer, nullable=True)
admin_id = Column(Integer, ForeignKey("admins.id"))
admin = relationship("Admin", back_populates="users")
@@ -60,6 +61,18 @@ class User(Base):
on_hold_timeout = Column(DateTime, nullable=True, default=None)
edit_at = Column(DateTime, nullable=True, default=None)
+ @hybrid_property
+ def reseted_usage(self):
+ return sum([log.used_traffic_at_reset for log in self.usage_logs])
+
+ @reseted_usage.expression
+ def reseted_usage(self):
+ return (
+ select([func.sum(UserUsageResetLogs.used_traffic_at_reset)]).
+ where(UserUsageResetLogs.user_id == self.id).
+ label('reseted_usage')
+ )
+
@property
def lifetime_used_traffic(self):
return (
@@ -194,6 +207,7 @@ class ProxyHost(Base):
is_disabled = Column(Boolean, nullable=True, default=False)
mux_enable = Column(Boolean, nullable=False, default=False, server_default='0')
fragment_setting = Column(String(100), nullable=True)
+ random_user_agent = Column(Boolean, nullable=False, default=False, server_default='0')
class System(Base):
diff --git a/app/jobs/review_users.py b/app/jobs/review_users.py
index 6ceb8ce32..41aa96947 100644
--- a/app/jobs/review_users.py
+++ b/app/jobs/review_users.py
@@ -55,10 +55,8 @@ def review():
xray.operations.remove_user(user)
update_user_status(db, user, status)
- bg.add_task(
- report.status_change, username=user.username, status=status,
- user=UserResponse.from_orm(user), user_admin=user.admin
- )
+ report.status_change(username=user.username, status=status,
+ user=UserResponse.from_orm(user), user_admin=user.admin)
logger.info(f"User \"{user.username}\" status changed to {status}")
@@ -82,10 +80,9 @@ def review():
update_user_status(db, user, status)
start_user_expire(db, user)
- bg.add_task(
- report.status_change, username=user.username, status=status,
- user=UserResponse.from_orm(user), user_admin=user.admin
- )
+
+ report.status_change(username=user.username, status=status,
+ user=UserResponse.from_orm(user), user_admin=user.admin)
logger.info(f"User \"{user.username}\" status changed to {status}")
diff --git a/app/models/proxy.py b/app/models/proxy.py
index bef62f36e..c83ff9c21 100644
--- a/app/models/proxy.py
+++ b/app/models/proxy.py
@@ -149,6 +149,7 @@ class ProxyHost(BaseModel):
is_disabled: Union[bool, None] = None
mux_enable: Union[bool, None] = None
fragment_setting: Optional[str] = Field(None, nullable=True)
+ random_user_agent: Union[bool, None] = None
class Config:
orm_mode = True
diff --git a/app/subscription/clash.py b/app/subscription/clash.py
index e999055b7..c37f747ce 100644
--- a/app/subscription/clash.py
+++ b/app/subscription/clash.py
@@ -1,8 +1,16 @@
-import yaml
import json
-from app.templates import render_template
+from random import choice
-from config import CLASH_SUBSCRIPTION_TEMPLATE, MUX_TEMPLATE
+import yaml
+
+from app.subscription.funcs import get_grpc_gun
+from app.templates import render_template
+from config import (
+ CLASH_SUBSCRIPTION_TEMPLATE,
+ MUX_TEMPLATE,
+ USER_AGENT_TEMPLATE,
+ GRPC_USER_AGENT_TEMPLATE,
+)
class ClashConfiguration(object):
@@ -15,6 +23,21 @@ def __init__(self):
}
self.proxy_remarks = []
self.mux_template = render_template(MUX_TEMPLATE)
+ temp_user_agent_data = render_template(USER_AGENT_TEMPLATE)
+ user_agent_data = json.loads(temp_user_agent_data)
+
+ if 'list' in user_agent_data and isinstance(user_agent_data['list'], list):
+ self.user_agent_list = user_agent_data['list']
+ else:
+ self.user_agent_list = []
+
+ temp_grpc_user_agent_data = render_template(GRPC_USER_AGENT_TEMPLATE)
+ grpc_user_agent_data = json.loads(temp_grpc_user_agent_data)
+
+ if 'list' in grpc_user_agent_data and isinstance(grpc_user_agent_data['list'], list):
+ self.grpc_user_agent_data = grpc_user_agent_data['list']
+ else:
+ self.grpc_user_agent_data = []
def render(self):
return yaml.dump(
@@ -59,7 +82,11 @@ def make_node(self,
udp: bool = True,
alpn: str = '',
ais: bool = '',
- mux_enable: bool = False):
+ mux_enable: bool = False,
+ random_user_agent: bool = False):
+
+ if network in ["grpc", "gun"]:
+ path = get_grpc_gun(path)
if type == 'shadowsocks':
type = 'ss'
@@ -77,6 +104,14 @@ def make_node(self,
'udp': udp
}
+ if "?ed=" in path:
+ path, max_early_data = path.split("?ed=")
+ max_early_data, = max_early_data.split("/")
+ max_early_data = int(max_early_data)
+ early_data_header_name = "Sec-WebSocket-Protocol"
+ else:
+ max_early_data = None
+
if type == 'ss': # shadowsocks
return node
@@ -100,16 +135,31 @@ def make_node(self,
if host:
net_opts['method'] = 'GET'
net_opts['Host'] = host
+ if random_user_agent:
+ net_opts['header'] = {"User-Agent": choice(self.user_agent_list)}
- if network == 'ws':
+ if network == 'ws' or network == 'httpupgrade':
if path:
net_opts['path'] = path
- if host:
- net_opts['headers'] = {"Host": host}
-
- if network == 'grpc':
+ if host or random_user_agent:
+ net_opts['headers'] = {}
+ if host:
+ net_opts['headers']["Host"] = host
+ if random_user_agent:
+ net_opts['headers']["User-Agent"] = choice(self.user_agent_list)
+ if max_early_data:
+ net_opts['max-early-data'] = max_early_data
+ net_opts['early-data-header-name'] = early_data_header_name
+ if network == 'httpupgrade':
+ net_opts['v2ray-http-upgrade'] = True
+ if max_early_data:
+ net_opts['v2ray-http-upgrade-fast-open'] = True
+
+ if network == 'grpc' or network == 'gun':
if path:
net_opts['grpc-service-name'] = path
+ if random_user_agent:
+ net_opts['header'] = {"User-Agent": choice(self.user_agent_list)}
if network == 'h2':
if path:
@@ -149,7 +199,8 @@ def add(self, remark: str, address: str, inbound: dict, settings: dict):
udp=True,
alpn=inbound.get('alpn', ''),
ais=inbound.get('ais', ''),
- mux_enable=inbound.get('mux_enable', '')
+ mux_enable=inbound.get('mux_enable', ''),
+ random_user_agent=inbound.get("random_user_agent")
)
if inbound['protocol'] == 'vmess':
@@ -189,7 +240,8 @@ def make_node(self,
pbk: str = '',
sid: str = '',
ais: bool = '',
- mux_enable: bool = False):
+ mux_enable: bool = False,
+ random_user_agent: bool = False):
node = super().make_node(
name=name,
type=type,
@@ -204,7 +256,8 @@ def make_node(self,
udp=udp,
alpn=alpn,
ais=ais,
- mux_enable=mux_enable
+ mux_enable=mux_enable,
+ random_user_agent=random_user_agent
)
if fp:
node['client-fingerprint'] = fp
@@ -231,7 +284,8 @@ def add(self, remark: str, address: str, inbound: dict, settings: dict):
pbk=inbound.get('pbk', ''),
sid=inbound.get('sid', ''),
ais=inbound.get('ais', ''),
- mux_enable=inbound.get('mux_enable', '')
+ mux_enable=inbound.get('mux_enable', ''),
+ random_user_agent=inbound.get("random_user_agent")
)
if inbound['protocol'] == 'vmess':
@@ -244,7 +298,7 @@ def add(self, remark: str, address: str, inbound: dict, settings: dict):
if inbound['protocol'] == 'vless':
node['uuid'] = settings['id']
- if inbound['network'] in ('tcp', 'kcp') and inbound['header_type'] != 'http':
+ if inbound['network'] in ('tcp', 'kcp') and inbound['header_type'] != 'http' and inbound['tls'] != 'none':
node['flow'] = settings.get('flow', '')
self.data['proxies'].append(node)
diff --git a/app/subscription/funcs.py b/app/subscription/funcs.py
new file mode 100644
index 000000000..9fcc3d0de
--- /dev/null
+++ b/app/subscription/funcs.py
@@ -0,0 +1,20 @@
+def get_grpc_gun(path: str) -> str:
+ if not path.startswith("/"):
+ return path
+
+ servicename = path.rsplit("/", 1)[0]
+ streamname = path.rsplit("/", 1)[1].split("|")[0]
+
+ if streamname == "Tun":
+ return servicename[1:]
+
+ return "%s%s%s" % (servicename, "/", streamname)
+
+def get_grpc_multi(path: str) -> str:
+ if not path.startswith("/"):
+ return path
+
+ servicename = path.rsplit("/", 1)[0]
+ streamname = path.rsplit("/", 1)[1].split("|")[1]
+
+ return "%s%s%s" % (servicename, "/", streamname)
\ No newline at end of file
diff --git a/app/subscription/share.py b/app/subscription/share.py
index 832745d4f..a191e5620 100644
--- a/app/subscription/share.py
+++ b/app/subscription/share.py
@@ -1,6 +1,8 @@
import base64
import random
import secrets
+import yaml
+import json
from datetime import datetime as dt
from datetime import timedelta
from typing import TYPE_CHECKING, List, Literal, Union
@@ -17,7 +19,7 @@
from config import (ACTIVE_STATUS_TEXT, DISABLED_STATUS_TEXT,
EXPIRED_STATUS_TEXT, LIMITED_STATUS_TEXT,
- ONHOLD_STATUS_TEXT)
+ ONHOLD_STATUS_TEXT, RANDOMIZE_SUBSCRIPTION_CONFIGS)
SERVER_IP = get_public_ip()
SERVER_IPV6 = get_public_ipv6()
@@ -39,85 +41,10 @@
}
-def get_v2ray_link(remark: str, address: str, inbound: dict, settings: dict):
- if inbound["protocol"] == "vmess":
- return V2rayShareLink.vmess(
- remark=remark,
- address=address,
- port=inbound["port"],
- id=settings["id"],
- net=inbound["network"],
- tls=inbound["tls"],
- sni=inbound.get("sni", ""),
- fp=inbound.get("fp", ""),
- alpn=inbound.get("alpn", ""),
- pbk=inbound.get("pbk", ""),
- sid=inbound.get("sid", ""),
- spx=inbound.get("spx", ""),
- host=inbound["host"],
- path=inbound["path"],
- type=inbound["header_type"],
- ais=inbound.get("ais", ""),
- fs=inbound.get("fragment_setting", ""),
- )
-
- if inbound["protocol"] == "vless":
- return V2rayShareLink.vless(
- remark=remark,
- address=address,
- port=inbound["port"],
- id=settings["id"],
- flow=settings.get("flow", ""),
- net=inbound["network"],
- tls=inbound["tls"],
- sni=inbound.get("sni", ""),
- fp=inbound.get("fp", ""),
- alpn=inbound.get("alpn", ""),
- pbk=inbound.get("pbk", ""),
- sid=inbound.get("sid", ""),
- spx=inbound.get("spx", ""),
- host=inbound["host"],
- path=inbound["path"],
- type=inbound["header_type"],
- ais=inbound.get("ais", ""),
- fs=inbound.get("fragment_setting", ""),
- )
-
- if inbound["protocol"] == "trojan":
- return V2rayShareLink.trojan(
- remark=remark,
- address=address,
- port=inbound["port"],
- password=settings["password"],
- flow=settings.get("flow", ""),
- net=inbound["network"],
- tls=inbound["tls"],
- sni=inbound.get("sni", ""),
- fp=inbound.get("fp", ""),
- alpn=inbound.get("alpn", ""),
- pbk=inbound.get("pbk", ""),
- sid=inbound.get("sid", ""),
- spx=inbound.get("spx", ""),
- host=inbound["host"],
- path=inbound["path"],
- type=inbound["header_type"],
- ais=inbound.get("ais", ""),
- fs=inbound.get("fragment_setting", ""),
- )
-
- if inbound["protocol"] == "shadowsocks":
- return V2rayShareLink.shadowsocks(
- remark=remark,
- address=address,
- port=inbound["port"],
- password=settings["password"],
- method=settings["method"],
- )
-
-
def generate_v2ray_links(proxies: dict, inbounds: dict, extra_data: dict) -> list:
format_variables = setup_format_variables(extra_data)
- return process_inbounds_and_tags(inbounds, proxies, format_variables, mode="v2ray")
+ conf = V2rayShareLink()
+ return process_inbounds_and_tags(inbounds, proxies, format_variables, conf=conf)
def generate_clash_subscription(
@@ -130,7 +57,7 @@ def generate_clash_subscription(
format_variables = setup_format_variables(extra_data)
return process_inbounds_and_tags(
- inbounds, proxies, format_variables, mode="clash", conf=conf
+ inbounds, proxies, format_variables, conf=conf
)
@@ -141,7 +68,7 @@ def generate_singbox_subscription(
format_variables = setup_format_variables(extra_data)
return process_inbounds_and_tags(
- inbounds, proxies, format_variables, mode="sing-box", conf=conf
+ inbounds, proxies, format_variables, conf=conf
)
@@ -156,7 +83,7 @@ def generate_outline_subscription(
format_variables = setup_format_variables(extra_data)
return process_inbounds_and_tags(
- inbounds, proxies, format_variables, mode="outline", conf=conf
+ inbounds, proxies, format_variables, conf=conf
)
@@ -167,10 +94,43 @@ def generate_v2ray_json_subscription(
format_variables = setup_format_variables(extra_data)
return process_inbounds_and_tags(
- inbounds, proxies, format_variables, mode="v2ray-json", conf=conf
+ inbounds, proxies, format_variables, conf=conf
)
+def randomize_sub_config(
+ config: str, config_format: str
+) -> str:
+
+ if config_format == "v2ray":
+ config = config.split("\n")
+ random.shuffle(config)
+ config = "\n".join(config)
+
+ elif config_format in ("clash-meta", "clash"):
+ config = yaml.safe_load(config)
+ random.shuffle(config['proxies'])
+ for group in config['proxy-groups']:
+ if group['name'] == '♻️ Automatic':
+ group['proxies'] = [proxy['name'] for proxy in config['proxies']]
+ config = yaml.dump(config, allow_unicode=True, sort_keys=False)
+
+ elif config_format == "sing-box":
+ config = json.loads(config)
+ outbounds = config['outbounds']
+ main_outbounds = [ob for ob in outbounds if ob['type'] in {'selector', 'urltest'}]
+ other_outbounds = [ob for ob in outbounds if ob['type'] not in {'selector', 'urltest', 'direct', 'block', 'dns'}]
+ random.shuffle(other_outbounds)
+ proxy_names = [ob['tag'] for ob in other_outbounds]
+ for ob in main_outbounds:
+ ob['outbounds'] = ['Best Latency'] + proxy_names if ob['type'] == 'selector' else proxy_names
+ config['outbounds'] = main_outbounds + other_outbounds + [ob for ob in outbounds if ob['type'] in {'direct', 'block', 'dns'}]
+ config = json.dumps(config, indent=4)
+
+ elif config_format == "v2ray-json":
+ random.shuffle(config)
+ return config
+
def generate_subscription(
user: "UserResponse",
config_format: Literal["v2ray", "clash-meta", "clash", "sing-box", "outline", "v2ray-json"],
@@ -197,6 +157,9 @@ def generate_subscription(
else:
raise ValueError(f'Unsupported format "{config_format}"')
+ if RANDOMIZE_SUBSCRIPTION_CONFIGS is not False:
+ config = randomize_sub_config(config, config_format)
+
if as_base64:
config = base64.b64encode(config.encode()).decode()
@@ -232,6 +195,8 @@ def setup_format_variables(extra_data: dict) -> dict:
user_status = extra_data.get("status")
expire_timestamp = extra_data.get("expire")
on_hold_expire_duration = extra_data.get("on_hold_expire_duration")
+ now = dt.utcnow()
+ now_ts = now.timestamp()
if user_status != UserStatus.on_hold:
if expire_timestamp is not None and expire_timestamp >= 0:
@@ -241,8 +206,13 @@ def setup_format_variables(extra_data: dict) -> dict:
jalali_expire_date = jd.fromgregorian(
year=expire_date.year, month=expire_date.month, day=expire_date.day
).strftime("%Y-%m-%d")
- days_left = (expire_datetime - dt.utcnow()).days + 1
- time_left = format_time_left(seconds_left)
+ if now_ts < expire_timestamp:
+ days_left = (expire_datetime - dt.utcnow()).days + 1
+ time_left = format_time_left(seconds_left)
+ else:
+ days_left = "0"
+ time_left = "0"
+
else:
days_left = "∞"
time_left = "∞"
@@ -295,10 +265,8 @@ def process_inbounds_and_tags(
inbounds: dict,
proxies: dict,
format_variables: dict,
- mode: str = "v2ray",
conf=None,
) -> Union[List, str]:
- results = []
_inbounds = []
for protocol, tags in inbounds.items():
@@ -352,32 +320,19 @@ def process_inbounds_and_tags(
"ais": host["allowinsecure"]
or inbound.get("allowinsecure", ""),
"mux_enable": host["mux_enable"],
- "fragment_setting": host["fragment_setting"]
+ "fragment_setting": host["fragment_setting"],
+ "random_user_agent": host["random_user_agent"],
}
)
- if mode == "v2ray":
- results.append(
- get_v2ray_link(
- remark=host["remark"].format_map(format_variables),
- address=address.format_map(
- format_variables),
- inbound=host_inbound,
- settings=settings.dict(no_obj=True),
- )
- )
- elif mode in ["clash", "sing-box", "outline", "v2ray-json"]:
- conf.add(
- remark=host["remark"].format_map(format_variables),
- address=address.format_map(format_variables),
- inbound=host_inbound,
- settings=settings.dict(no_obj=True),
- )
-
- if mode in ["clash", "sing-box", "outline", "v2ray-json"]:
- return conf.render()
-
- return results
+ conf.add(
+ remark=host["remark"].format_map(format_variables),
+ address=address.format_map(format_variables),
+ inbound=host_inbound,
+ settings=settings.dict(no_obj=True),
+ )
+
+ return conf.render()
def encode_title(text: str) -> str:
diff --git a/app/subscription/singbox.py b/app/subscription/singbox.py
index 01e14cabd..efe72f9ca 100644
--- a/app/subscription/singbox.py
+++ b/app/subscription/singbox.py
@@ -1,7 +1,14 @@
import json
+from random import choice
from app.templates import render_template
+from app.subscription.funcs import get_grpc_gun
-from config import SINGBOX_SUBSCRIPTION_TEMPLATE, MUX_TEMPLATE
+from config import (
+ SINGBOX_SUBSCRIPTION_TEMPLATE,
+ MUX_TEMPLATE,
+ USER_AGENT_TEMPLATE,
+ GRPC_USER_AGENT_TEMPLATE,
+)
class SingBoxConfiguration(str):
@@ -10,6 +17,21 @@ def __init__(self):
template = render_template(SINGBOX_SUBSCRIPTION_TEMPLATE)
self.config = json.loads(template)
self.mux_template = render_template(MUX_TEMPLATE)
+ temp_user_agent_data = render_template(USER_AGENT_TEMPLATE)
+ user_agent_data = json.loads(temp_user_agent_data)
+
+ if 'list' in user_agent_data and isinstance(user_agent_data['list'], list):
+ self.user_agent_list = user_agent_data['list']
+ else:
+ self.user_agent_list = []
+
+ temp_grpc_user_agent_data = render_template(GRPC_USER_AGENT_TEMPLATE)
+ grpc_user_agent_data = json.loads(temp_grpc_user_agent_data)
+
+ if 'list' in grpc_user_agent_data and isinstance(grpc_user_agent_data['list'], list):
+ self.grpc_user_agent_data = grpc_user_agent_data['list']
+ else:
+ self.grpc_user_agent_data = []
def add_outbound(self, outbound_data):
self.config["outbounds"].append(outbound_data)
@@ -64,8 +86,8 @@ def tls_config(sni=None, fp=None, tls=None, pbk=None,
return config
- @staticmethod
- def transport_config(transport_type='',
+ def transport_config(self,
+ transport_type='',
host='',
path='',
method='',
@@ -73,7 +95,8 @@ def transport_config(transport_type='',
ping_timeout="15s",
max_early_data=None,
early_data_header_name=None,
- permit_without_stream=False):
+ permit_without_stream=False,
+ random_user_agent: bool = False):
transport_config = {}
@@ -86,8 +109,12 @@ def transport_config(transport_type='',
transport_config['path'] = path
if method:
transport_config['method'] = method
+ if host or random_user_agent:
+ transport_config['headers'] = {}
if host:
transport_config["host"] = [host]
+ if random_user_agent:
+ transport_config['headers']['User-Agent'] = choice(self.user_agent_list)
if idle_timeout:
transport_config['idle_timeout'] = idle_timeout
if ping_timeout:
@@ -96,8 +123,12 @@ def transport_config(transport_type='',
elif transport_type == "ws":
if path:
transport_config['path'] = path
+ if host or random_user_agent:
+ transport_config['headers'] = {}
if host:
transport_config['headers'] = {'Host': host}
+ if random_user_agent:
+ transport_config['headers']['User-Agent'] = choice(self.user_agent_list)
if max_early_data is not None:
transport_config['max_early_data'] = max_early_data
if early_data_header_name:
@@ -112,13 +143,17 @@ def transport_config(transport_type='',
transport_config['ping_timeout'] = ping_timeout
if permit_without_stream:
transport_config['permit_without_stream'] = permit_without_stream
+ if random_user_agent:
+ transport_config['headers'] = {}
+ transport_config['headers']['User-Agent'] = choice(self.grpc_user_agent_data)
elif transport_type == "httpupgrade":
- transport_config['host'] = ""
+ transport_config['host'] = host
if path:
transport_config['path'] = path
- if host:
- transport_config['headers'] = {'Host': host}
+ if random_user_agent:
+ transport_config['headers'] = {}
+ transport_config['headers']['User-Agent'] = choice(self.user_agent_list)
return transport_config
@@ -140,8 +175,13 @@ def make_outbound(self,
headers='',
ais='',
mux_enable: bool = False,
+ random_user_agent: bool = False,
):
+ if isinstance(port, str):
+ ports = port.split(',')
+ port = int(choice(ports))
+
config = {
"type": type,
"tag": remark,
@@ -174,7 +214,8 @@ def make_outbound(self,
host=host,
path=path,
max_early_data=max_early_data,
- early_data_header_name=early_data_header_name
+ early_data_header_name=early_data_header_name,
+ random_user_agent=random_user_agent,
)
else:
config["network"] = net
@@ -183,33 +224,43 @@ def make_outbound(self,
config['tls'] = self.tls_config(sni=sni, fp=fp, tls=tls,
pbk=pbk, sid=sid, alpn=alpn,
ais=ais)
- if mux_enable:
- mux_json = json.loads(self.mux_template)
- mux_config = mux_json["sing-box"]
- config['multiplex'] = mux_config
+
+ mux_json = json.loads(self.mux_template)
+ mux_config = mux_json["sing-box"]
+
+ config['multiplex'] = mux_config
+ if config['multiplex']["enabled"]:
config['multiplex']["enabled"] = mux_enable
return config
def add(self, remark: str, address: str, inbound: dict, settings: dict):
+
+ net = inbound["network"]
+ path = inbound["path"]
+
+ if net in ["grpc", "gun"]:
+ path = get_grpc_gun(path)
+
outbound = self.make_outbound(
remark=remark,
type=inbound['protocol'],
address=address,
port=inbound['port'],
- net=inbound['network'],
+ net=net,
tls=(inbound['tls']),
flow=settings.get('flow', ''),
sni=inbound['sni'],
host=inbound['host'],
- path=inbound['path'],
- alpn=inbound.get('alpn', ''),
+ path=path,
+ alpn=inbound.get('alpn', '').rsplit(sep=","),
fp=inbound.get('fp', ''),
pbk=inbound.get('pbk', ''),
sid=inbound.get('sid', ''),
headers=inbound['header_type'],
ais=inbound.get('ais', ''),
- mux_enable=inbound.get('mux_enable', False))
+ mux_enable=inbound.get('mux_enable', False),
+ random_user_agent=inbound.get('random_user_agent', False),)
if inbound['protocol'] == 'vmess':
outbound['uuid'] = settings['id']
diff --git a/app/subscription/v2ray.py b/app/subscription/v2ray.py
index 3fb3a4943..60d97213a 100644
--- a/app/subscription/v2ray.py
+++ b/app/subscription/v2ray.py
@@ -1,15 +1,132 @@
import base64
import json
import urllib.parse as urlparse
+from random import choice
from typing import Union
+from urllib.parse import quote
from uuid import UUID
+from app.subscription.funcs import get_grpc_gun, get_grpc_multi
from app.templates import render_template
-
-from config import (MUX_TEMPLATE, V2RAY_SUBSCRIPTION_TEMPLATE)
+from config import (
+ MUX_TEMPLATE,
+ USER_AGENT_TEMPLATE,
+ V2RAY_SUBSCRIPTION_TEMPLATE,
+ GRPC_USER_AGENT_TEMPLATE,
+)
class V2rayShareLink(str):
+ def __init__(self):
+ self.links = []
+
+ def add_link(self, link):
+ self.links.append(link)
+
+ def render(self):
+ return self.links
+
+ def add(self, remark: str, address: str, inbound: dict, settings: dict):
+ net = inbound["network"]
+ multi_mode = inbound.get("multiMode", False)
+ old_path: str = inbound["path"]
+
+ if net in ["grpc", "gun"]:
+ if multi_mode:
+ path = get_grpc_multi(old_path)
+ else:
+ path = get_grpc_gun(old_path)
+ if old_path.startswith("/"):
+ path = quote(path, safe="-_.!~*'()")
+
+ else:
+ path = old_path
+
+ if inbound["protocol"] == "vmess":
+ link = self.vmess(
+ remark=remark,
+ address=address,
+ port=inbound["port"],
+ id=settings["id"],
+ net=net,
+ tls=inbound["tls"],
+ sni=inbound.get("sni", ""),
+ fp=inbound.get("fp", ""),
+ alpn=inbound.get("alpn", ""),
+ pbk=inbound.get("pbk", ""),
+ sid=inbound.get("sid", ""),
+ spx=inbound.get("spx", ""),
+ host=inbound["host"],
+ path=path,
+ type=inbound["header_type"],
+ ais=inbound.get("ais", ""),
+ fs=inbound.get("fragment_setting", ""),
+ multiMode=multi_mode,
+ max_upload_size=inbound.get('max_upload_size', 1000000),
+ max_concurrent_uploads=inbound.get('max_concurrent_uploads', 10),
+ )
+
+ elif inbound["protocol"] == "vless":
+ link = self.vless(
+ remark=remark,
+ address=address,
+ port=inbound["port"],
+ id=settings["id"],
+ flow=settings.get("flow", ""),
+ net=net,
+ tls=inbound["tls"],
+ sni=inbound.get("sni", ""),
+ fp=inbound.get("fp", ""),
+ alpn=inbound.get("alpn", ""),
+ pbk=inbound.get("pbk", ""),
+ sid=inbound.get("sid", ""),
+ spx=inbound.get("spx", ""),
+ host=inbound["host"],
+ path=path,
+ type=inbound["header_type"],
+ ais=inbound.get("ais", ""),
+ fs=inbound.get("fragment_setting", ""),
+ multiMode=multi_mode,
+ max_upload_size=inbound.get('max_upload_size', 1000000),
+ max_concurrent_uploads=inbound.get('max_concurrent_uploads', 10),
+ )
+
+ elif inbound["protocol"] == "trojan":
+ link = self.trojan(
+ remark=remark,
+ address=address,
+ port=inbound["port"],
+ password=settings["password"],
+ flow=settings.get("flow", ""),
+ net=net,
+ tls=inbound["tls"],
+ sni=inbound.get("sni", ""),
+ fp=inbound.get("fp", ""),
+ alpn=inbound.get("alpn", ""),
+ pbk=inbound.get("pbk", ""),
+ sid=inbound.get("sid", ""),
+ spx=inbound.get("spx", ""),
+ host=inbound["host"],
+ path=path,
+ type=inbound["header_type"],
+ ais=inbound.get("ais", ""),
+ fs=inbound.get("fragment_setting", ""),
+ multiMode=multi_mode,
+ max_upload_size=inbound.get('max_upload_size', 1000000),
+ max_concurrent_uploads=inbound.get('max_concurrent_uploads', 10),
+ )
+
+ elif inbound["protocol"] == "shadowsocks":
+ link = self.shadowsocks(
+ remark=remark,
+ address=address,
+ port=inbound["port"],
+ password=settings["password"],
+ method=settings["method"],
+ )
+
+ self.add_link(link=link)
+
@classmethod
def vmess(
cls,
@@ -30,6 +147,9 @@ def vmess(
spx="",
ais="",
fs="",
+ multiMode: bool = False,
+ max_upload_size: int = 1000000,
+ max_concurrent_uploads: int = 10,
):
payload = {
"add": address,
@@ -53,6 +173,8 @@ def vmess(
payload["sni"] = sni
payload["fp"] = fp
payload["alpn"] = alpn
+ if fs:
+ payload["fragment"] = fs
if ais:
payload["allowInsecure"] = 1
elif tls == "reality":
@@ -60,7 +182,18 @@ def vmess(
payload["fp"] = fp
payload["pbk"] = pbk
payload["sid"] = sid
- payload["spx"] = spx
+ if spx:
+ payload["spx"] = spx
+
+ if net == "grpc":
+ if multiMode:
+ payload["mode"] = "multi"
+ else:
+ payload["mode"] = "gun"
+
+ elif net == "splithttp":
+ payload["maxUploadSize"] = max_upload_size
+ payload["maxConcurrentUploads"] = max_concurrent_uploads
return (
"vmess://"
@@ -89,7 +222,10 @@ def vless(cls,
spx='',
ais='',
fs="",
- ):
+ multiMode: bool = False,
+ max_upload_size: int = 1000000,
+ max_concurrent_uploads: int = 10,
+ ):
payload = {
"security": tls,
@@ -101,10 +237,22 @@ def vless(cls,
if net == 'grpc':
payload['serviceName'] = path
- payload["host"] = host
+ payload["authority"] = host
+ if multiMode:
+ payload["mode"] = "multi"
+ else:
+ payload["mode"] = "gun"
+
elif net == 'quic':
payload['key'] = path
payload["quicSecurity"] = host
+
+ elif net == "splithttp":
+ payload["path"] = path
+ payload["host"] = host
+ payload["maxUploadSize"] = max_upload_size
+ payload["maxConcurrentUploads"] = max_concurrent_uploads
+
else:
payload["path"] = path
payload["host"] = host
@@ -122,7 +270,8 @@ def vless(cls,
payload["fp"] = fp
payload["pbk"] = pbk
payload["sid"] = sid
- payload["spx"] = spx
+ if spx:
+ payload["spx"] = spx
return (
"vless://"
@@ -151,6 +300,9 @@ def trojan(cls,
spx='',
ais='',
fs="",
+ multiMode: bool = False,
+ max_upload_size: int = 1000000,
+ max_concurrent_uploads: int = 10,
):
payload = {
@@ -163,7 +315,18 @@ def trojan(cls,
if net == 'grpc':
payload['serviceName'] = path
+ payload["authority"] = host
+ if multiMode:
+ payload["mode"] = "multi"
+ else:
+ payload["mode"] = "gun"
+
+ elif net == "splithttp":
+ payload["path"] = path
payload["host"] = host
+ payload["maxUploadSize"] = max_upload_size
+ payload["maxConcurrentUploads"] = max_concurrent_uploads
+
elif net == 'quic':
payload['key'] = path
payload["quicSecurity"] = host
@@ -184,7 +347,8 @@ def trojan(cls,
payload["fp"] = fp
payload["pbk"] = pbk
payload["sid"] = sid
- payload["spx"] = spx
+ if spx:
+ payload["spx"] = spx
return (
"trojan://"
@@ -210,6 +374,21 @@ def __init__(self):
self.config = []
self.template = render_template(V2RAY_SUBSCRIPTION_TEMPLATE)
self.mux_template = render_template(MUX_TEMPLATE)
+ temp_user_agent_data = render_template(USER_AGENT_TEMPLATE)
+ user_agent_data = json.loads(temp_user_agent_data)
+
+ if 'list' in user_agent_data and isinstance(user_agent_data['list'], list):
+ self.user_agent_list = user_agent_data['list']
+ else:
+ self.user_agent_list = []
+
+ temp_grpc_user_agent_data = render_template(GRPC_USER_AGENT_TEMPLATE)
+ grpc_user_agent_data = json.loads(temp_grpc_user_agent_data)
+
+ if 'list' in grpc_user_agent_data and isinstance(grpc_user_agent_data['list'], list):
+ self.grpc_user_agent_data = grpc_user_agent_data['list']
+ else:
+ self.grpc_user_agent_data = []
def add_config(self, remarks, outbounds):
json_template = json.loads(self.template)
@@ -240,7 +419,7 @@ def tls_config(sni=None, fp=None, alpn=None, ais=None):
return tlsSettings
@staticmethod
- def reality_config(sni=None, fp=None, pbk=None, sid=None):
+ def reality_config(sni=None, fp=None, pbk=None, sid=None, spx=None):
realitySettings = {}
if sni is not None:
@@ -254,13 +433,12 @@ def reality_config(sni=None, fp=None, pbk=None, sid=None):
realitySettings["publicKey"] = pbk
if sid:
realitySettings["shortId"] = sid
-
- realitySettings["spiderX"] = ""
+ if spx:
+ realitySettings["spiderX"] = spx
return realitySettings
- @staticmethod
- def ws_config(path=None, host=None):
+ def ws_config(self, path=None, host=None, random_user_agent=None):
wsSettings = {}
wsSettings["headers"] = {}
@@ -268,36 +446,62 @@ def ws_config(path=None, host=None):
wsSettings["path"] = path
if host:
wsSettings["headers"]["Host"] = host
+ if random_user_agent:
+ wsSettings["headers"]["User-Agent"] = choice(self.user_agent_list)
return wsSettings
- @staticmethod
- def httpupgrade_config(path=None, host=None):
+ def httpupgrade_config(self, path=None, host=None, random_user_agent=None):
httpupgradeSettings = {}
+ httpupgradeSettings["headers"] = {}
if path:
httpupgradeSettings["path"] = path
if host:
httpupgradeSettings["host"] = host
+ if random_user_agent:
+ httpupgradeSettings["headers"]["User-Agent"] = choice(self.user_agent_list)
return httpupgradeSettings
- @staticmethod
- def grpc_config(path=None, multiMode=False):
+ def splithttp_config(self, path=None, host=None, random_user_agent=None,
+ max_upload_size: int = 1000000,
+ max_concurrent_uploads: int = 10,
+ ):
+
+ splithttpSettings = {}
+ splithttpSettings["headers"] = {}
+ if path:
+ splithttpSettings["path"] = path
+ if host:
+ splithttpSettings["host"] = host
+ if random_user_agent:
+ splithttpSettings["headers"]["User-Agent"] = choice(
+ self.user_agent_list)
+ splithttpSettings["maxUploadSize"] = max_upload_size
+ splithttpSettings["maxConcurrentUploads"] = max_concurrent_uploads
+
+ return splithttpSettings
+
+ def grpc_config(self, path=None, host=None, multiMode=False, random_user_agent=None):
grpcSettings = {}
if path:
grpcSettings["serviceName"] = path
+ if host:
+ grpcSettings["authority"] = host
grpcSettings["multiMode"] = multiMode
grpcSettings["idle_timeout"] = 60
grpcSettings["health_check_timeout"] = 20
grpcSettings["permit_without_stream"] = False
- grpcSettings["initial_windows_size"] = 0
+ grpcSettings["initial_windows_size"] = 35536
+
+ if random_user_agent:
+ grpcSettings["user_agent"] = choice(self.grpc_user_agent_data)
return grpcSettings
- @staticmethod
- def tcp_http_config(path=None, host=None):
+ def tcp_http_config(self, path=None, host=None, random_user_agent=None):
tcpSettings = {}
if any((path, host)):
@@ -309,7 +513,6 @@ def tcp_http_config(path=None, host=None):
tcpSettings["header"]["request"]["headers"] = {}
tcpSettings["header"]["request"]["method"] = "GET"
- tcpSettings["header"]["request"]["headers"]["User-Agent"] = []
tcpSettings["header"]["request"]["headers"]["Accept-Encoding"] = ["gzip, deflate"]
tcpSettings["header"]["request"]["headers"]["Connection"] = ["keep-alive"]
tcpSettings["header"]["request"]["headers"]["Pragma"] = "no-cache"
@@ -320,12 +523,17 @@ def tcp_http_config(path=None, host=None):
if host:
tcpSettings["header"]["request"]["headers"]["Host"] = [host]
+ if random_user_agent:
+ tcpSettings["header"]["request"]["headers"]["User-Agent"] = [choice(self.user_agent_list)]
+ else:
+ tcpSettings["header"]["request"]["headers"]["User-Agent"] = []
+
return tcpSettings
- @staticmethod
- def h2_config(path=None, host=None):
+ def h2_config(self, path=None, host=None, random_user_agent=None):
httpSettings = {}
+ httpSettings["headers"] = {}
if path:
httpSettings["path"] = path
else:
@@ -333,12 +541,14 @@ def h2_config(path=None, host=None):
if host:
httpSettings["host"] = [host]
else:
- httpSettings["host"] = {}
+ httpSettings["host"] = []
+ if random_user_agent:
+ httpSettings["headers"]["User-Agent"] = [choice(self.user_agent_list)]
return httpSettings
@staticmethod
- def quic_config(path=None, host=None, header=None):
+ def quic_config(path=None, host=None, header=None,):
quicSettings = {}
quicSettings["header"] = {"none"}
@@ -347,16 +557,18 @@ def quic_config(path=None, host=None, header=None):
else:
quicSettings["key"] = ""
if host:
- quicSettings["security"] = [host]
+ quicSettings["security"] = host
else:
- quicSettings["security"] = ""
+ quicSettings["security"] = "none"
if header:
quicSettings["header"]["type"] = header
+ else:
+ quicSettings["header"]["type"] = "none"
return quicSettings
@staticmethod
- def kpc_config(path=None, host=None, header=None):
+ def kcp_config(path=None, host=None, header=None):
kcpSettings = {}
kcpSettings["header"] = {}
@@ -400,7 +612,7 @@ def stream_setting_config(network=None, security=None,
streamSettings["grpcSettings"] = network_setting
elif network == "h2":
streamSettings["httpSettings"] = network_setting
- elif network == "kpc":
+ elif network == "kcp":
streamSettings["kcpSettings"] = network_setting
elif network == "tcp" and network_setting:
streamSettings["tcpSettings"] = network_setting
@@ -408,6 +620,8 @@ def stream_setting_config(network=None, security=None,
streamSettings["quicSettings"] = network_setting
elif network == "httpupgrade":
streamSettings["httpupgradeSettings"] = network_setting
+ elif network == "splithttp":
+ streamSettings["splithttpSettings"] = network_setting
if sockopt:
streamSettings['sockopt'] = sockopt
@@ -461,7 +675,6 @@ def trojan_config(address=None, port=None, password=None, method="chacha20"):
servers["email"] = "https://gozargah.github.io/marzban/"
servers["method"] = method
servers["ota"] = False
- servers["level"] = 1
settings["servers"] = [servers]
@@ -479,7 +692,6 @@ def shadowsocks_config(address=None, port=None, password=None, method=None):
servers["email"] = "https://gozargah.github.io/marzban/"
servers["method"] = method
servers["uot"] = False
- servers["level"] = 1
settings["servers"] = [servers]
@@ -511,33 +723,41 @@ def make_stream_setting(self,
alpn='',
pbk='',
sid='',
+ spx='',
headers='',
ais='',
- dialer_proxy=''
+ dialer_proxy='',
+ multiMode: bool = False,
+ random_user_agent: bool = False,
+ max_upload_size: int = 1,
+ max_concurrent_uploads: int = 10,
):
if net == "ws":
- network_setting = self.ws_config(path=path, host=host)
+ network_setting = self.ws_config(path=path, host=host, random_user_agent=random_user_agent)
elif net == "grpc":
- network_setting = self.grpc_config(path=path)
+ network_setting = self.grpc_config(path=path, host=host, multiMode=multiMode, random_user_agent=random_user_agent)
elif net == "h2":
- network_setting = self.h2_config(path=path, host=host)
- elif net == "kpc":
- network_setting = self.kpc_config(
+ network_setting = self.h2_config(path=path, host=host, random_user_agent=random_user_agent)
+ elif net == "kcp":
+ network_setting = self.kcp_config(
path=path, host=host, header=headers)
elif net == "tcp":
- network_setting = self.tcp_http_config(path=path, host=host)
+ network_setting = self.tcp_http_config(path=path, host=host, random_user_agent=random_user_agent)
elif net == "quic":
- network_setting = self.quic_config(
- path=path, host=host, header=headers)
+ network_setting = self.quic_config(path=path, host=host, header=headers)
elif net == "httpupgrade":
- network_setting = self.httpupgrade_config(path=path, host=host)
+ network_setting = self.httpupgrade_config(path=path, host=host, random_user_agent=random_user_agent)
+ elif net == "splithttp":
+ network_setting = self.splithttp_config(path=path, host=host, random_user_agent=random_user_agent,
+ max_upload_size=max_upload_size,
+ max_concurrent_uploads=max_concurrent_uploads)
if tls == "tls":
tls_settings = self.tls_config(sni=sni, fp=fp, alpn=alpn, ais=ais)
elif tls == "reality":
tls_settings = self.reality_config(
- sni=sni, fp=fp, pbk=pbk, sid=sid)
+ sni=sni, fp=fp, pbk=pbk, sid=sid, spx=spx)
else:
tls_settings = None
@@ -563,6 +783,14 @@ def add(self, remark: str, address: str, inbound: dict, settings: dict):
tls = (inbound['tls'])
headers = inbound['header_type']
fragment = inbound['fragment_setting']
+ path = inbound["path"]
+ multi_mode = inbound.get("multiMode", False)
+
+ if net in ["grpc", "gun"]:
+ if multi_mode:
+ path = get_grpc_multi(path)
+ else:
+ path = get_grpc_gun(path)
outbound = {
"tag": remark,
@@ -618,14 +846,19 @@ def add(self, remark: str, address: str, inbound: dict, settings: dict):
tls=tls,
sni=inbound['sni'],
host=inbound['host'],
- path=inbound['path'],
- alpn=inbound.get('alpn', ''),
+ path=path,
+ alpn=inbound.get('alpn', '').rsplit(sep=","),
fp=inbound.get('fp', ''),
pbk=inbound.get('pbk', ''),
sid=inbound.get('sid', ''),
+ spx=inbound.get('spx', ''),
headers=headers,
ais=inbound.get('ais', ''),
- dialer_proxy=dialer_proxy
+ dialer_proxy=dialer_proxy,
+ multiMode=multi_mode,
+ random_user_agent=inbound.get('random_user_agent', False),
+ max_upload_size=inbound.get('max_upload_size', 1000000),
+ max_concurrent_uploads=inbound.get('max_concurrent_uploads', 10),
)
mux_json = json.loads(self.mux_template)
diff --git a/app/templates/user_agent/default.json b/app/templates/user_agent/default.json
new file mode 100644
index 000000000..abb85644c
--- /dev/null
+++ b/app/templates/user_agent/default.json
@@ -0,0 +1,104 @@
+{
+ "list":[
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36",
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36 Edg/125.0.0.0",
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36",
+ "Mozilla/5.0 (iPhone; CPU iPhone OS 17_4_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4.1 Mobile/15E148 Safari/604.1",
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.0.0 Safari/537.36 PageSpeedPlus/1.0.0",
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36 Edg/124.0.0.0",
+ "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Mobile Safari/537.36",
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:126.0) Gecko/20100101 Firefox/126.0",
+ "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Mobile Safari/537.36",
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36",
+ "Mozilla/5.0 (iPhone; CPU iPhone OS 17_4_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148",
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4.1 Safari/605.1.15",
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 11.6; rv:92.0) Gecko/20100101 Firefox/92.0",
+ "Mozilla/5.0 (iPhone; CPU iPhone OS 16_6_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1",
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
+ "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36",
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) obsidian/1.4.14 Chrome/114.0.5735.289 Electron/25.8.1 Safari/537.36",
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36",
+ "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:126.0) Gecko/20100101 Firefox/126.0",
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15",
+ "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Mobile Safari/537.36",
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36",
+ "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36",
+ "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36",
+ "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/25.0 Chrome/121.0.0.0 Mobile Safari/537.36",
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:125.0) Gecko/20100101 Firefox/125.0",
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
+ "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Mobile Safari/537.36",
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:125.0) Gecko/20100101 Firefox/125.0",
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) obsidian/1.4.16 Chrome/114.0.5735.289 Electron/25.8.1 Safari/537.36",
+ "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Mobile Safari/537.36",
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36",
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.114 Safari/537.36",
+ "Mozilla/5.0 (iPhone; CPU iPhone OS 17_2_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Mobile/15E148 Safari/604.1",
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36 Edg/123.0.0.0",
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Safari/605.1.15",
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
+ "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36",
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) obsidian/1.5.3 Chrome/114.0.5735.289 Electron/25.8.1 Safari/537.36",
+ "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36",
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Safari/605.1.15",
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36",
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36",
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36",
+ "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1",
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Edg/122.0.0.0",
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2.1 Safari/605.1.15",
+ "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36",
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) obsidian/1.5.12 Chrome/120.0.6099.283 Electron/28.2.3 Safari/537.36",
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) obsidian/1.4.16 Chrome/114.0.5735.289 Electron/25.8.1 Safari/537.36",
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36",
+ "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Mobile Safari/537.36",
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36 OPR/109.0.0.0",
+ "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.129 Safari/537.36",
+ "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
+ "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Mobile Safari/537.36",
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.1 Safari/605.1.15",
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.3.1 Safari/605.1.15",
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) obsidian/1.4.13 Chrome/114.0.5735.289 Electron/25.8.1 Safari/537.36",
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15",
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36",
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Safari/605.1.15",
+ "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:109.0) Gecko/20100101 Firefox/115.0",
+ "Mozilla/5.0 (iPhone; CPU iPhone OS 17_5_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Mobile/15E148 Safari/604.1",
+ "Mozilla/5.0 (iPhone; CPU iPhone OS 17_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.3 Mobile/15E148 Safari/604.1",
+ "Mozilla/5.0 (iPhone; CPU iPhone OS 16_1_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.1 Mobile/15E148 Safari/604.1",
+ "Mozilla/5.0 (iPhone; CPU iPhone OS 17_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.3.1 Mobile/15E148 Safari/604.1",
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:124.0) Gecko/20100101 Firefox/124.0",
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.3 Safari/605.1.15",
+ "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36",
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) obsidian/1.4.13 Chrome/114.0.5735.289 Electron/25.8.1 Safari/537.36",
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Safari/605.1.15",
+ "Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1",
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.6.1 Safari/605.1.15",
+ "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36",
+ "Mozilla/5.0 (iPhone; CPU iPhone OS 17_1_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1.1 Mobile/15E148 Safari/604.1",
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0",
+ "Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Mobile/15E148 Safari/604.1",
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) obsidian/1.5.12 Chrome/120.0.6099.283 Electron/28.2.3 Safari/537.36",
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36",
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1.2 Safari/605.1.15",
+ "Mozilla/5.0 (iPhone; CPU iPhone OS 16_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.2 Mobile/15E148 Safari/604.1",
+ "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36",
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5 Safari/605.1.15",
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36 Edg/121.0.0.0",
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:124.0) Gecko/20100101 Firefox/124.0",
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36",
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) obsidian/1.5.3 Chrome/114.0.5735.289 Electron/25.8.1 Safari/537.36",
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.67 Safari/537.36",
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.3 Safari/605.1.15",
+ "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36",
+ "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36",
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36 Edg/124.0.0.0",
+ "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Mobile Safari/537.36",
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36",
+ "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/24.0 Chrome/117.0.0.0 Mobile Safari/537.36"
+ ]
+}
\ No newline at end of file
diff --git a/app/templates/user_agent/grpc.json b/app/templates/user_agent/grpc.json
new file mode 100644
index 000000000..821671ea4
--- /dev/null
+++ b/app/templates/user_agent/grpc.json
@@ -0,0 +1,20 @@
+{
+ "list": [
+ "grpc-dotnet/2.41.0 (.NET 6.0.1; CLR 6.0.1; net6.0; windows; x64)",
+ "grpc-dotnet/2.41.0 (.NET 6.0.0-preview.7.21377.19; CLR 6.0.0; net6.0; osx; x64)",
+ "grpc-dotnet/2.41.0 (Mono 6.12.0.140; CLR 4.0.30319; netstandard2.0; osx; x64)",
+ "grpc-dotnet/2.41.0 (.NET 6.0.0-rc.1.21380.1; CLR 6.0.0; net6.0; linux; arm64)",
+ "grpc-dotnet/2.41.0 (.NET 5.0.8; CLR 5.0.8; net5.0; linux; arm64)",
+ "grpc-dotnet/2.41.0 (.NET Core; CLR 3.1.4; netstandard2.1; linux; arm64)",
+ "grpc-dotnet/2.41.0 (.NET Framework; CLR 4.0.30319.42000; netstandard2.0; windows; x86)",
+ "grpc-dotnet/2.41.0 (.NET 6.0.0-rc.1.21380.1; CLR 6.0.0; net6.0; windows; x64)",
+ "grpc-python-asyncio/1.62.1 grpc-c/39.0.0 (linux; chttp2)",
+ "grpc-go/1.58.1",
+ "grpc-java-okhttp/1.55.1",
+ "grpc-node/1.7.1 grpc-c/1.7.1 (osx; chttp2)",
+ "grpc-node/1.24.2 grpc-c/8.0.0 (linux; chttp2; ganges)",
+ "grpc-c++/1.16.0 grpc-c/6.0.0 (linux; nghttp2; hw)",
+ "grpc-node/1.19.0 grpc-c/7.0.0 (linux; chttp2; gold)",
+ "grpc-ruby/1.62.0 grpc-c/39.0.0 (osx; chttp2)]"
+ ]
+}
\ No newline at end of file
diff --git a/app/views/subscription.py b/app/views/subscription.py
index 31e021fdc..f176a6329 100644
--- a/app/views/subscription.py
+++ b/app/views/subscription.py
@@ -8,18 +8,19 @@
from app import app
from app.db import Session, crud, get_db
from app.models.user import UserResponse
+from app.subscription.share import encode_title, generate_subscription
from app.templates import render_template
from app.utils.jwt import get_subscription_payload
-from app.subscription.share import encode_title, generate_subscription
from config import (
SUB_PROFILE_TITLE,
SUB_SUPPORT_URL,
SUB_UPDATE_INTERVAL,
SUBSCRIPTION_PAGE_TEMPLATE,
- XRAY_SUBSCRIPTION_PATH,
USE_CUSTOM_JSON_DEFAULT,
+ USE_CUSTOM_JSON_FOR_STREISAND,
USE_CUSTOM_JSON_FOR_V2RAYN,
- USE_CUSTOM_JSON_FOR_V2RAYNG
+ USE_CUSTOM_JSON_FOR_V2RAYNG,
+ XRAY_SUBSCRIPTION_PATH
)
@@ -78,7 +79,7 @@ def get_subscription_user_info(user: UserResponse) -> dict:
crud.update_user_sub(db, dbuser, user_agent)
- if re.match('^([Cc]lash-verge|[Cc]lash-?[Mm]eta)', user_agent):
+ if re.match('^([Cc]lash-verge|[Cc]lash[-\.]?[Mm]eta|[Ff][Ll][Cc]lash|[Mm]ihomo)', user_agent):
conf = generate_subscription(user=user, config_format="clash-meta", as_base64=False)
return Response(content=conf, media_type="text/yaml", headers=response_headers)
@@ -106,7 +107,7 @@ def get_subscription_user_info(user: UserResponse) -> dict:
elif re.match('^v2rayNG/(\d+\.\d+\.\d+)', user_agent):
version_str = re.match('^v2rayNG/(\d+\.\d+\.\d+)', user_agent).group(1)
- if LooseVersion(version_str) >= LooseVersion("1.8.16") and \
+ if LooseVersion(version_str) >= LooseVersion("1.8.18") and \
(USE_CUSTOM_JSON_DEFAULT or USE_CUSTOM_JSON_FOR_V2RAYNG):
conf = generate_subscription(user=user, config_format="v2ray-json", as_base64=False)
return Response(content=conf, media_type="application/json", headers=response_headers)
@@ -114,6 +115,14 @@ def get_subscription_user_info(user: UserResponse) -> dict:
conf = generate_subscription(user=user, config_format="v2ray", as_base64=True)
return Response(content=conf, media_type="text/plain", headers=response_headers)
+ elif re.match('^[Ss]treisand', user_agent):
+ if USE_CUSTOM_JSON_DEFAULT or USE_CUSTOM_JSON_FOR_STREISAND:
+ conf = generate_subscription(user=user, config_format="v2ray-json", as_base64=False)
+ return Response(content=conf, media_type="application/json", headers=response_headers)
+ else:
+ conf = generate_subscription(user=user, config_format="v2ray", as_base64=True)
+ return Response(content=conf, media_type="text/plain", headers=response_headers)
+
else:
conf = generate_subscription(user=user, config_format="v2ray", as_base64=True)
return Response(content=conf, media_type="text/plain", headers=response_headers)
@@ -174,6 +183,7 @@ def user_subscription_with_client_type(
request: Request,
client_type: str = Path(..., regex="sing-box|clash-meta|clash|outline|v2ray|v2ray-json"),
db: Session = Depends(get_db),
+ user_agent: str = Header(default="")
):
"""
Subscription link, v2ray, clash, sing-box, outline and clash-meta supported
@@ -183,8 +193,8 @@ def get_subscription_user_info(user: UserResponse) -> dict:
return {
"upload": 0,
"download": user.used_traffic,
- "total": user.data_limit,
- "expire": user.expire,
+ "total": user.data_limit if user.data_limit is not None else 0,
+ "expire": user.expire if user.expire is not None else 0,
}
sub = get_subscription_payload(token)
@@ -209,10 +219,11 @@ def get_subscription_user_info(user: UserResponse) -> dict:
"subscription-userinfo": "; ".join(
f"{key}={val}"
for key, val in get_subscription_user_info(user).items()
- if val is not None
)
}
+ crud.update_user_sub(db, dbuser, user_agent)
+
if client_type == "clash-meta":
conf = generate_subscription(user=user, config_format="clash-meta", as_base64=False)
return Response(content=conf, media_type="text/yaml", headers=response_headers)
diff --git a/app/xray/__init__.py b/app/xray/__init__.py
index 8f13d9424..1bc23fe60 100644
--- a/app/xray/__init__.py
+++ b/app/xray/__init__.py
@@ -61,7 +61,8 @@ def hosts(storage: dict):
else host.security.value,
"allowinsecure": host.allowinsecure,
"mux_enable": host.mux_enable,
- "fragment_setting": host.fragment_setting
+ "fragment_setting": host.fragment_setting,
+ "random_user_agent": host.random_user_agent,
} for host in inbound_hosts if not host.is_disabled
]
diff --git a/app/xray/config.py b/app/xray/config.py
index 9759c44e6..fe1af2a78 100644
--- a/app/xray/config.py
+++ b/app/xray/config.py
@@ -1,14 +1,17 @@
from __future__ import annotations
import json
+from collections import defaultdict
from copy import deepcopy
from pathlib import PosixPath
from typing import Union
import commentjson
+from sqlalchemy import func
-from app.db import GetDB, crud
-from app.models.proxy import ProxySettings, ProxyTypes
+from app.db import GetDB
+from app.db import models as db_models
+from app.models.proxy import ProxyTypes
from app.models.user import UserStatus
from app.utils.crypto import get_cert_SANs
from config import DEBUG, XRAY_EXCLUDE_INBOUND_TAGS, XRAY_FALLBACKS_INBOUND_TAG
@@ -45,7 +48,6 @@ def __init__(self,
self.inbounds_by_protocol = {}
self.inbounds_by_tag = {}
self._fallbacks_inbound = self.get_inbound(XRAY_FALLBACKS_INBOUND_TAG)
- self._addr_clients_by_tag = {}
self._resolve_inbounds()
self._apply_api()
@@ -119,6 +121,8 @@ def _validate(self):
for inbound in self['inbounds']:
if not inbound.get("tag"):
raise ValueError("all inbounds must have a unique tag")
+ if ',' in inbound.get("tag"):
+ raise ValueError("character «,» is not allowed in inbound tag")
for outbound in self['outbounds']:
if not outbound.get("tag"):
raise ValueError("all outbounds must have a unique tag")
@@ -135,8 +139,6 @@ def _resolve_inbounds(self):
inbound['settings'] = {}
if not inbound['settings'].get('clients'):
inbound['settings']['clients'] = []
- self._addr_clients_by_tag[inbound['tag']
- ] = inbound['settings']['clients']
settings = {
"tag": inbound["tag"],
@@ -155,16 +157,12 @@ def _resolve_inbounds(self):
try:
settings['port'] = inbound['port']
except KeyError:
- if not self._fallbacks_inbound:
- raise ValueError(
- f"port missing on {inbound['tag']}"
- "\nset XRAY_FALLBACKS_INBOUND_TAG if you're using an inbound containing fallbacks"
- )
- try:
- settings['port'] = self._fallbacks_inbound['port']
- settings['is_fallback'] = True
- except KeyError:
- raise ValueError("fallbacks inbound doesn't have port")
+ if self._fallbacks_inbound:
+ try:
+ settings['port'] = self._fallbacks_inbound['port']
+ settings['is_fallback'] = True
+ except KeyError:
+ raise ValueError("fallbacks inbound doesn't have port")
# stream settings
if stream := inbound.get('streamSettings'):
@@ -230,6 +228,10 @@ def _resolve_inbounds(self):
except (IndexError, TypeError):
raise ValueError(
f"You need to define at least one shortID in realitySettings of {inbound['tag']}")
+ try:
+ settings['spx'] = tls_settings.get('SpiderX')
+ except:
+ settings['spx'] = ""
if net == 'tcp':
header = net_settings.get('header', {})
@@ -265,10 +267,12 @@ def _resolve_inbounds(self):
if isinstance(host, str):
settings['host'] = [host]
- elif net == 'grpc':
+ elif net == 'grpc' or net == 'gun':
settings['header_type'] = ''
settings['path'] = net_settings.get('serviceName', '')
- settings['host'] = []
+ host = net_settings.get('authority', '')
+ settings['host'] = [host]
+ settings['multiMode'] = net_settings.get('multiMode', False)
elif net == 'quic':
settings['header_type'] = net_settings.get('header', {}).get('type', '')
@@ -280,6 +284,13 @@ def _resolve_inbounds(self):
host = net_settings.get('host', '')
settings['host'] = [host]
+ elif net == 'splithttp':
+ settings['path'] = net_settings.get('path', '')
+ host = net_settings.get('host', '')
+ settings['host'] = [host]
+ settings['maxUploadSize'] = net_settings.get('maxUploadSize', 1000000)
+ settings['maxConcurrentUploads'] = net_settings.get('maxConcurrentUploads', 10)
+
else:
settings['path'] = net_settings.get('path', '')
host = net_settings.get(
@@ -297,30 +308,6 @@ def _resolve_inbounds(self):
except KeyError:
self.inbounds_by_protocol[inbound['protocol']] = [settings]
- def add_inbound_client(self, inbound_tag: str, email: str, settings: dict):
- inbound = self.inbounds_by_tag.get(inbound_tag, {})
- client = {"email": email, **settings}
-
- # XTLS currently only supports transmission methods of TCP and mKCP
- if client.get('flow') and (
- inbound.get('network', 'tcp') not in ('tcp', 'kcp')
- or
- (
- inbound.get('network', 'tcp') in ('tcp', 'kcp')
- and
- inbound.get('tls') not in ('tls', 'reality')
- )
- or
- inbound.get('header_type') == 'http'
- ):
- del client['flow']
-
- try:
- self._addr_clients_by_tag[inbound_tag].append(client)
- except KeyError:
- return
- return client
-
def get_inbound(self, tag) -> dict:
for inbound in self['inbounds']:
if inbound['tag'] == tag:
@@ -341,18 +328,71 @@ def include_db_users(self) -> XRayConfig:
config = self.copy()
with GetDB() as db:
- for user in crud.get_users(db, status=[UserStatus.active,
- UserStatus.on_hold]):
- proxies_settings = {
- p.type: ProxySettings.from_dict(
- p.type, p.settings).dict(no_obj=True)
- for p in user.proxies
- }
- for proxy_type, inbound_tags in user.inbounds.items():
- for inbound_tag in inbound_tags:
- config.add_inbound_client(inbound_tag,
- f"{user.id}.{user.username}",
- proxies_settings[proxy_type])
+ query = db.query(
+ db_models.User.id,
+ db_models.User.username,
+ func.lower(db_models.Proxy.type).label('type'),
+ db_models.Proxy.settings,
+ func.group_concat(db_models.excluded_inbounds_association.c.inbound_tag).label('excluded_inbound_tags')
+ ).join(
+ db_models.Proxy, db_models.User.id == db_models.Proxy.user_id
+ ).outerjoin(
+ db_models.excluded_inbounds_association, db_models.Proxy.id == db_models.excluded_inbounds_association.c.proxy_id
+ ).filter(
+ db_models.User.status.in_([UserStatus.active, UserStatus.on_hold])
+ ).group_by(
+ func.lower(db_models.Proxy.type),
+ db_models.User.id,
+ db_models.User.username,
+ db_models.Proxy.settings,
+ )
+ result = query.all()
+
+ grouped_data = defaultdict(list)
+
+ for row in result:
+ grouped_data[row["type"]].append((
+ row["id"],
+ row["username"],
+ row["settings"],
+ [i for i in row['excluded_inbound_tags'].split(',') if i] if row['excluded_inbound_tags'] else None
+ ))
+
+ for proxy_type, rows in grouped_data.items():
+
+ inbounds = self.inbounds_by_protocol.get(proxy_type)
+ if not inbounds:
+ continue
+
+ for inbound in inbounds:
+ clients = config.get_inbound(inbound['tag'])['settings']['clients']
+
+ for row in rows:
+ user_id, username, settings, excluded_inbound_tags = row
+
+ if excluded_inbound_tags and inbound['tag'] in excluded_inbound_tags:
+ continue
+
+ client = {
+ "email": f"{user_id}.{username}",
+ **settings
+ }
+
+ # XTLS currently only supports transmission methods of TCP and mKCP
+ if client.get('flow') and (
+ inbound.get('network', 'tcp') not in ('tcp', 'kcp')
+ or
+ (
+ inbound.get('network', 'tcp') in ('tcp', 'kcp')
+ and
+ inbound.get('tls') not in ('tls', 'reality')
+ )
+ or
+ inbound.get('header_type') == 'http'
+ ):
+ del client['flow']
+
+ clients.append(client)
if DEBUG:
with open('generated_config-debug.json', 'w') as f:
diff --git a/cli/admin.py b/cli/admin.py
index 5e2282379..ddbf7c6cf 100644
--- a/cli/admin.py
+++ b/cli/admin.py
@@ -1,16 +1,18 @@
from typing import Optional, Union
import typer
-from rich.table import Table
+from decouple import UndefinedValueError, config
from rich.console import Console
from rich.panel import Panel
+from rich.table import Table
+from sqlalchemy import func
from sqlalchemy.exc import IntegrityError
-from decouple import config, UndefinedValueError
-from app.db import GetDB
-from app.db import crud
+from app.db import GetDB, crud
from app.db.models import Admin, User
from app.models.admin import AdminCreate, AdminPartialModify
+from app.utils.system import readable_size
+
from . import utils
app = typer.Typer(no_args_is_help=True)
@@ -34,6 +36,18 @@ def validate_discord_webhook(value: str) -> Union[str, None]:
return value
+def calculate_admin_usage(admin_id: int) -> str:
+ with GetDB() as db:
+ usage = db.query(func.sum(User.used_traffic)).filter_by(admin_id=admin_id).first()[0]
+ return readable_size(int(usage or 0))
+
+
+def calculate_admin_reseted_usage(admin_id: int) -> str:
+ with GetDB() as db:
+ usage = db.query(func.sum(User.reseted_usage)).filter_by(admin_id=admin_id).first()[0]
+ return readable_size(int(usage or 0))
+
+
@app.command(name="list")
def list_admins(
offset: Optional[int] = typer.Option(None, *utils.FLAGS["offset"]),
@@ -44,9 +58,11 @@ def list_admins(
with GetDB() as db:
admins: list[Admin] = crud.get_admins(db, offset=offset, limit=limit, username=username)
utils.print_table(
- table=Table("Username", "Is sudo", "Created at", "Telegram ID", "Discord Webhook"),
+ table=Table("Username", 'Usage', 'Reseted usage', "Is sudo", "Created at", "Telegram ID", "Discord Webhook"),
rows=[
(str(admin.username),
+ calculate_admin_usage(admin.id),
+ calculate_admin_reseted_usage(admin.id),
"✔️" if admin.is_sudo else "✖️",
utils.readable_datetime(admin.created_at),
str(admin.telegram_id or "✖️"),
diff --git a/config.py b/config.py
index a75cb2301..f8afc673a 100755
--- a/config.py
+++ b/config.py
@@ -50,10 +50,14 @@
SINGBOX_SUBSCRIPTION_TEMPLATE = config("SINGBOX_SUBSCRIPTION_TEMPLATE", default="singbox/default.json")
MUX_TEMPLATE = config("MUX_TEMPLATE", default="mux/default.json")
V2RAY_SUBSCRIPTION_TEMPLATE = config("V2RAY_SUBSCRIPTION_TEMPLATE", default="v2ray/default.json")
+USER_AGENT_TEMPLATE = config("USER_AGENT_TEMPLATE", default="user_agent/default.json")
+GRPC_USER_AGENT_TEMPLATE = config("GRPC_USER_AGENT_TEMPLATE", default="user_agent/grpc.json")
+
USE_CUSTOM_JSON_DEFAULT = config("USE_CUSTOM_JSON_DEFAULT", default=False, cast=bool)
USE_CUSTOM_JSON_FOR_V2RAYN = config("USE_CUSTOM_JSON_FOR_V2RAYN", default=False, cast=bool)
USE_CUSTOM_JSON_FOR_V2RAYNG = config("USE_CUSTOM_JSON_FOR_V2RAYNG", default=False, cast=bool)
+USE_CUSTOM_JSON_FOR_STREISAND = config("USE_CUSTOM_JSON_FOR_STREISAND", default=False, cast=bool)
ACTIVE_STATUS_TEXT = config("ACTIVE_STATUS_TEXT", default="Active")
EXPIRED_STATUS_TEXT = config("EXPIRED_STATUS_TEXT", default="Expired")
@@ -94,6 +98,7 @@
SUB_UPDATE_INTERVAL = config("SUB_UPDATE_INTERVAL", default="12")
SUB_SUPPORT_URL = config("SUB_SUPPORT_URL", default="https://t.me/")
SUB_PROFILE_TITLE = config("SUB_PROFILE_TITLE", default="Subscription")
+RANDOMIZE_SUBSCRIPTION_CONFIGS = config("RANDOMIZE_SUBSCRIPTION_CONFIGS", default=False, cast=bool)
# discord webhook log
DISCORD_WEBHOOK_URL = config("DISCORD_WEBHOOK_URL", default="")
diff --git a/xray_api/proto/app/commander/config_pb2.py b/xray_api/proto/app/commander/config_pb2.py
index bd9bffa75..8310a393e 100644
--- a/xray_api/proto/app/commander/config_pb2.py
+++ b/xray_api/proto/app/commander/config_pb2.py
@@ -15,7 +15,7 @@
from xray_api.proto.common.serial import typed_message_pb2 as common_dot_serial_dot_typed__message__pb2
-DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1a\x61pp/commander/config.proto\x12\x12xray.app.commander\x1a!common/serial/typed_message.proto\"H\n\x06\x43onfig\x12\x0b\n\x03tag\x18\x01 \x01(\t\x12\x31\n\x07service\x18\x02 \x03(\x0b\x32 .xray.common.serial.TypedMessage\"\x12\n\x10ReflectionConfigBX\n\x16\x63om.xray.app.commanderP\x01Z\'github.com/xtls/xray-core/app/commander\xaa\x02\x12Xray.App.Commanderb\x06proto3')
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1a\x61pp/commander/config.proto\x12\x12xray.app.commander\x1a!common/serial/typed_message.proto\"X\n\x06\x43onfig\x12\x0b\n\x03tag\x18\x01 \x01(\t\x12\x0e\n\x06listen\x18\x03 \x01(\t\x12\x31\n\x07service\x18\x02 \x03(\x0b\x32 .xray.common.serial.TypedMessage\"\x12\n\x10ReflectionConfigBX\n\x16\x63om.xray.app.commanderP\x01Z\'github.com/xtls/xray-core/app/commander\xaa\x02\x12Xray.App.Commanderb\x06proto3')
@@ -40,7 +40,7 @@
DESCRIPTOR._options = None
DESCRIPTOR._serialized_options = b'\n\026com.xray.app.commanderP\001Z\'github.com/xtls/xray-core/app/commander\252\002\022Xray.App.Commander'
_CONFIG._serialized_start=85
- _CONFIG._serialized_end=157
- _REFLECTIONCONFIG._serialized_start=159
- _REFLECTIONCONFIG._serialized_end=177
+ _CONFIG._serialized_end=173
+ _REFLECTIONCONFIG._serialized_start=175
+ _REFLECTIONCONFIG._serialized_end=193
# @@protoc_insertion_point(module_scope)
diff --git a/xray_api/proto/transport/internet/config_pb2.py b/xray_api/proto/transport/internet/config_pb2.py
index 2983bdf1d..cb75d9dfb 100644
--- a/xray_api/proto/transport/internet/config_pb2.py
+++ b/xray_api/proto/transport/internet/config_pb2.py
@@ -16,7 +16,7 @@
from xray_api.proto.common.serial import typed_message_pb2 as common_dot_serial_dot_typed__message__pb2
-DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1ftransport/internet/config.proto\x12\x17xray.transport.internet\x1a!common/serial/typed_message.proto\"\x9e\x01\n\x0fTransportConfig\x12@\n\x08protocol\x18\x01 \x01(\x0e\x32*.xray.transport.internet.TransportProtocolB\x02\x18\x01\x12\x15\n\rprotocol_name\x18\x03 \x01(\t\x12\x32\n\x08settings\x18\x02 \x01(\x0b\x32 .xray.common.serial.TypedMessage\"\xc1\x02\n\x0cStreamConfig\x12@\n\x08protocol\x18\x01 \x01(\x0e\x32*.xray.transport.internet.TransportProtocolB\x02\x18\x01\x12\x15\n\rprotocol_name\x18\x05 \x01(\t\x12\x44\n\x12transport_settings\x18\x02 \x03(\x0b\x32(.xray.transport.internet.TransportConfig\x12\x15\n\rsecurity_type\x18\x03 \x01(\t\x12;\n\x11security_settings\x18\x04 \x03(\x0b\x32 .xray.common.serial.TypedMessage\x12>\n\x0fsocket_settings\x18\x06 \x01(\x0b\x32%.xray.transport.internet.SocketConfig\"7\n\x0bProxyConfig\x12\x0b\n\x03tag\x18\x01 \x01(\t\x12\x1b\n\x13transportLayerProxy\x18\x02 \x01(\x08\"\xce\x04\n\x0cSocketConfig\x12\x0c\n\x04mark\x18\x01 \x01(\x05\x12\x0b\n\x03tfo\x18\x02 \x01(\x05\x12@\n\x06tproxy\x18\x03 \x01(\x0e\x32\x30.xray.transport.internet.SocketConfig.TProxyMode\x12%\n\x1dreceive_original_dest_address\x18\x04 \x01(\x08\x12\x14\n\x0c\x62ind_address\x18\x05 \x01(\x0c\x12\x11\n\tbind_port\x18\x06 \x01(\r\x12\x1d\n\x15\x61\x63\x63\x65pt_proxy_protocol\x18\x07 \x01(\x08\x12@\n\x0f\x64omain_strategy\x18\x08 \x01(\x0e\x32\'.xray.transport.internet.DomainStrategy\x12\x14\n\x0c\x64ialer_proxy\x18\t \x01(\t\x12\x1f\n\x17tcp_keep_alive_interval\x18\n \x01(\x05\x12\x1b\n\x13tcp_keep_alive_idle\x18\x0b \x01(\x05\x12\x16\n\x0etcp_congestion\x18\x0c \x01(\t\x12\x11\n\tinterface\x18\r \x01(\t\x12\x0e\n\x06v6only\x18\x0e \x01(\x08\x12\x18\n\x10tcp_window_clamp\x18\x0f \x01(\x05\x12\x18\n\x10tcp_user_timeout\x18\x10 \x01(\x05\x12\x13\n\x0btcp_max_seg\x18\x11 \x01(\x05\x12\x14\n\x0ctcp_no_delay\x18\x12 \x01(\x08\x12\x11\n\ttcp_mptcp\x18\x13 \x01(\x08\"/\n\nTProxyMode\x12\x07\n\x03Off\x10\x00\x12\n\n\x06TProxy\x10\x01\x12\x0c\n\x08Redirect\x10\x02*k\n\x11TransportProtocol\x12\x07\n\x03TCP\x10\x00\x12\x07\n\x03UDP\x10\x01\x12\x08\n\x04MKCP\x10\x02\x12\r\n\tWebSocket\x10\x03\x12\x08\n\x04HTTP\x10\x04\x12\x10\n\x0c\x44omainSocket\x10\x05\x12\x0f\n\x0bHTTPUpgrade\x10\x06*\xa9\x01\n\x0e\x44omainStrategy\x12\t\n\x05\x41S_IS\x10\x00\x12\n\n\x06USE_IP\x10\x01\x12\x0b\n\x07USE_IP4\x10\x02\x12\x0b\n\x07USE_IP6\x10\x03\x12\x0c\n\x08USE_IP46\x10\x04\x12\x0c\n\x08USE_IP64\x10\x05\x12\x0c\n\x08\x46ORCE_IP\x10\x06\x12\r\n\tFORCE_IP4\x10\x07\x12\r\n\tFORCE_IP6\x10\x08\x12\x0e\n\nFORCE_IP46\x10\t\x12\x0e\n\nFORCE_IP64\x10\nBg\n\x1b\x63om.xray.transport.internetP\x01Z,github.com/xtls/xray-core/transport/internet\xaa\x02\x17Xray.Transport.Internetb\x06proto3')
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1ftransport/internet/config.proto\x12\x17xray.transport.internet\x1a!common/serial/typed_message.proto\"\x9e\x01\n\x0fTransportConfig\x12@\n\x08protocol\x18\x01 \x01(\x0e\x32*.xray.transport.internet.TransportProtocolB\x02\x18\x01\x12\x15\n\rprotocol_name\x18\x03 \x01(\t\x12\x32\n\x08settings\x18\x02 \x01(\x0b\x32 .xray.common.serial.TypedMessage\"\xc1\x02\n\x0cStreamConfig\x12@\n\x08protocol\x18\x01 \x01(\x0e\x32*.xray.transport.internet.TransportProtocolB\x02\x18\x01\x12\x15\n\rprotocol_name\x18\x05 \x01(\t\x12\x44\n\x12transport_settings\x18\x02 \x03(\x0b\x32(.xray.transport.internet.TransportConfig\x12\x15\n\rsecurity_type\x18\x03 \x01(\t\x12;\n\x11security_settings\x18\x04 \x03(\x0b\x32 .xray.common.serial.TypedMessage\x12>\n\x0fsocket_settings\x18\x06 \x01(\x0b\x32%.xray.transport.internet.SocketConfig\"7\n\x0bProxyConfig\x12\x0b\n\x03tag\x18\x01 \x01(\t\x12\x1b\n\x13transportLayerProxy\x18\x02 \x01(\x08\"\xce\x04\n\x0cSocketConfig\x12\x0c\n\x04mark\x18\x01 \x01(\x05\x12\x0b\n\x03tfo\x18\x02 \x01(\x05\x12@\n\x06tproxy\x18\x03 \x01(\x0e\x32\x30.xray.transport.internet.SocketConfig.TProxyMode\x12%\n\x1dreceive_original_dest_address\x18\x04 \x01(\x08\x12\x14\n\x0c\x62ind_address\x18\x05 \x01(\x0c\x12\x11\n\tbind_port\x18\x06 \x01(\r\x12\x1d\n\x15\x61\x63\x63\x65pt_proxy_protocol\x18\x07 \x01(\x08\x12@\n\x0f\x64omain_strategy\x18\x08 \x01(\x0e\x32\'.xray.transport.internet.DomainStrategy\x12\x14\n\x0c\x64ialer_proxy\x18\t \x01(\t\x12\x1f\n\x17tcp_keep_alive_interval\x18\n \x01(\x05\x12\x1b\n\x13tcp_keep_alive_idle\x18\x0b \x01(\x05\x12\x16\n\x0etcp_congestion\x18\x0c \x01(\t\x12\x11\n\tinterface\x18\r \x01(\t\x12\x0e\n\x06v6only\x18\x0e \x01(\x08\x12\x18\n\x10tcp_window_clamp\x18\x0f \x01(\x05\x12\x18\n\x10tcp_user_timeout\x18\x10 \x01(\x05\x12\x13\n\x0btcp_max_seg\x18\x11 \x01(\x05\x12\x14\n\x0ctcp_no_delay\x18\x12 \x01(\x08\x12\x11\n\ttcp_mptcp\x18\x13 \x01(\x08\"/\n\nTProxyMode\x12\x07\n\x03Off\x10\x00\x12\n\n\x06TProxy\x10\x01\x12\x0c\n\x08Redirect\x10\x02*z\n\x11TransportProtocol\x12\x07\n\x03TCP\x10\x00\x12\x07\n\x03UDP\x10\x01\x12\x08\n\x04MKCP\x10\x02\x12\r\n\tWebSocket\x10\x03\x12\x08\n\x04HTTP\x10\x04\x12\x10\n\x0c\x44omainSocket\x10\x05\x12\x0f\n\x0bHTTPUpgrade\x10\x06\x12\r\n\tSplitHTTP\x10\x07*\xa9\x01\n\x0e\x44omainStrategy\x12\t\n\x05\x41S_IS\x10\x00\x12\n\n\x06USE_IP\x10\x01\x12\x0b\n\x07USE_IP4\x10\x02\x12\x0b\n\x07USE_IP6\x10\x03\x12\x0c\n\x08USE_IP46\x10\x04\x12\x0c\n\x08USE_IP64\x10\x05\x12\x0c\n\x08\x46ORCE_IP\x10\x06\x12\r\n\tFORCE_IP4\x10\x07\x12\r\n\tFORCE_IP6\x10\x08\x12\x0e\n\nFORCE_IP46\x10\t\x12\x0e\n\nFORCE_IP64\x10\nBg\n\x1b\x63om.xray.transport.internetP\x01Z,github.com/xtls/xray-core/transport/internet\xaa\x02\x17Xray.Transport.Internetb\x06proto3')
_TRANSPORTPROTOCOL = DESCRIPTOR.enum_types_by_name['TransportProtocol']
TransportProtocol = enum_type_wrapper.EnumTypeWrapper(_TRANSPORTPROTOCOL)
@@ -29,6 +29,7 @@
HTTP = 4
DomainSocket = 5
HTTPUpgrade = 6
+SplitHTTP = 7
AS_IS = 0
USE_IP = 1
USE_IP4 = 2
@@ -84,9 +85,9 @@
_STREAMCONFIG.fields_by_name['protocol']._options = None
_STREAMCONFIG.fields_by_name['protocol']._serialized_options = b'\030\001'
_TRANSPORTPROTOCOL._serialized_start=1230
- _TRANSPORTPROTOCOL._serialized_end=1337
- _DOMAINSTRATEGY._serialized_start=1340
- _DOMAINSTRATEGY._serialized_end=1509
+ _TRANSPORTPROTOCOL._serialized_end=1352
+ _DOMAINSTRATEGY._serialized_start=1355
+ _DOMAINSTRATEGY._serialized_end=1524
_TRANSPORTCONFIG._serialized_start=96
_TRANSPORTCONFIG._serialized_end=254
_STREAMCONFIG._serialized_start=257
diff --git a/xray_api/proto/transport/internet/splithttp/config_pb2.py b/xray_api/proto/transport/internet/splithttp/config_pb2.py
new file mode 100644
index 000000000..56e9ae4cf
--- /dev/null
+++ b/xray_api/proto/transport/internet/splithttp/config_pb2.py
@@ -0,0 +1,48 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: transport/internet/splithttp/config.proto
+"""Generated protocol buffer code."""
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n)transport/internet/splithttp/config.proto\x12!xray.transport.internet.splithttp\"\xcf\x01\n\x06\x43onfig\x12\x0c\n\x04host\x18\x01 \x01(\t\x12\x0c\n\x04path\x18\x02 \x01(\t\x12\x45\n\x06header\x18\x03 \x03(\x0b\x32\x35.xray.transport.internet.splithttp.Config.HeaderEntry\x12\x1c\n\x14maxConcurrentUploads\x18\x04 \x01(\x05\x12\x15\n\rmaxUploadSize\x18\x05 \x01(\x05\x1a-\n\x0bHeaderEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x42\x85\x01\n%com.xray.transport.internet.splithttpP\x01Z6github.com/xtls/xray-core/transport/internet/splithttp\xaa\x02!Xray.Transport.Internet.SplitHttpb\x06proto3')
+
+
+
+_CONFIG = DESCRIPTOR.message_types_by_name['Config']
+_CONFIG_HEADERENTRY = _CONFIG.nested_types_by_name['HeaderEntry']
+Config = _reflection.GeneratedProtocolMessageType('Config', (_message.Message,), {
+
+ 'HeaderEntry' : _reflection.GeneratedProtocolMessageType('HeaderEntry', (_message.Message,), {
+ 'DESCRIPTOR' : _CONFIG_HEADERENTRY,
+ '__module__' : 'transport.internet.splithttp.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.transport.internet.splithttp.Config.HeaderEntry)
+ })
+ ,
+ 'DESCRIPTOR' : _CONFIG,
+ '__module__' : 'transport.internet.splithttp.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.transport.internet.splithttp.Config)
+ })
+_sym_db.RegisterMessage(Config)
+_sym_db.RegisterMessage(Config.HeaderEntry)
+
+if _descriptor._USE_C_DESCRIPTORS == False:
+
+ DESCRIPTOR._options = None
+ DESCRIPTOR._serialized_options = b'\n%com.xray.transport.internet.splithttpP\001Z6github.com/xtls/xray-core/transport/internet/splithttp\252\002!Xray.Transport.Internet.SplitHttp'
+ _CONFIG_HEADERENTRY._options = None
+ _CONFIG_HEADERENTRY._serialized_options = b'8\001'
+ _CONFIG._serialized_start=81
+ _CONFIG._serialized_end=288
+ _CONFIG_HEADERENTRY._serialized_start=243
+ _CONFIG_HEADERENTRY._serialized_end=288
+# @@protoc_insertion_point(module_scope)
diff --git a/xray_api/proto/transport/internet/splithttp/config_pb2_grpc.py b/xray_api/proto/transport/internet/splithttp/config_pb2_grpc.py
new file mode 100644
index 000000000..2daafffeb
--- /dev/null
+++ b/xray_api/proto/transport/internet/splithttp/config_pb2_grpc.py
@@ -0,0 +1,4 @@
+# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
+"""Client and server classes corresponding to protobuf-defined services."""
+import grpc
+