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 +