1import os
2import secrets
3from datetime import datetime
4
5import sqlalchemy.sql
6from sqlalchemy import (
7 BigInteger,
8 Boolean,
9 Column,
10 DateTime,
11 Enum,
12 Float,
13 ForeignKey,
14 Integer,
15 String,
16 Table,
17 UniqueConstraint,
18 JSON,
19 and_,
20 func,
21 select,
22 Text,
23)
24from sqlalchemy.ext.hybrid import hybrid_property
25from sqlalchemy.ext.orderinglist import ordering_list
26from sqlalchemy.orm import relationship, column_property
27from sqlalchemy.sql.expression import text
28
29from app.config.env import SUBSCRIPTION_URL_PREFIX
30from app.db.base import Base
31from app.models.node import NodeStatus
32from app.models.proxy import (
33 InboundHostFingerprint,
34 InboundHostSecurity,
35 ProxyTypes,
36)
37from app.models.user import (
38 UserDataUsageResetStrategy,
39 UserStatus,
40 UserExpireStrategy,
41)
42
43admins_services = Table(
44 "admins_services",
45 Base.metadata,
46 Column("admin_id", ForeignKey("admins.id"), primary_key=True),
47 Column("service_id", ForeignKey("services.id"), primary_key=True),
48)
49
50inbounds_services = Table(
51 "inbounds_services",
52 Base.metadata,
53 Column("inbound_id", ForeignKey("inbounds.id"), primary_key=True),
54 Column("service_id", ForeignKey("services.id"), primary_key=True),
55)
56
57users_services = Table(
58 "users_services",
59 Base.metadata,
60 Column("user_id", ForeignKey("users.id"), primary_key=True),
61 Column("service_id", ForeignKey("services.id"), primary_key=True),
62)
63
64hosts_services = Table(
65 "hosts_services",
66 Base.metadata,
67 Column("host_id", ForeignKey("hosts.id"), primary_key=True),
68 Column("service_id", ForeignKey("services.id"), primary_key=True),
69)
70
71
72class Admin(Base):
73 __tablename__ = "admins"
74
75 id = Column(Integer, primary_key=True)
76 username = Column(String(32), unique=True, index=True)
77 hashed_password = Column(String(128))
78 users = relationship("User", back_populates="admin")
79 services = relationship(
80 "Service",
81 secondary=admins_services,
82 back_populates="admins",
83 lazy="joined",
84 )
85 enabled = Column(
86 Boolean,
87 nullable=False,
88 default=True,
89 server_default=sqlalchemy.sql.true(),
90 )
91 all_services_access = Column(
92 Boolean,
93 nullable=False,
94 default=False,
95 server_default=sqlalchemy.sql.false(),
96 )
97 modify_users_access = Column(
98 Boolean,
99 nullable=False,
100 default=True,
101 server_default=sqlalchemy.sql.true(),
102 )
103 created_at = Column(DateTime, default=datetime.utcnow)
104 is_sudo = Column(Boolean, default=False)
105 password_reset_at = Column(DateTime)
106 subscription_url_prefix = Column(
107 String(256),
108 nullable=False,
109 default="",
110 server_default=sqlalchemy.sql.text(""),
111 )
112
113 @property
114 def service_ids(self):
115 return [service.id for service in self.services]
116
117 @classmethod
118 def __declare_last__(cls):
119 cls.users_data_usage = column_property(
120 select(func.coalesce(func.sum(User.lifetime_used_traffic), 0))
121 .where(User.admin_id == cls.id)
122 .correlate_except(User)
123 .scalar_subquery()
124 )
125
126
127class Service(Base):
128 __tablename__ = "services"
129 id = Column(Integer, primary_key=True)
130 name = Column(String(64))
131 admins = relationship(
132 "Admin", secondary=admins_services, back_populates="services"
133 )
134 users = relationship(
135 "User", secondary=users_services, back_populates="services"
136 )
137 inbounds = relationship(
138 "Inbound", secondary=inbounds_services, back_populates="services"
139 )
140
141 @property
142 def inbound_ids(self):
143 return [inbound.id for inbound in self.inbounds]
144
145 @property
146 def user_ids(self):
147 return [user.id for user in self.users if not user.removed]
148
149
150class User(Base):
151 __tablename__ = "users"
152
153 id = Column(Integer, primary_key=True)
154 username = Column(String(32), unique=True, index=True)
155 key = Column(String(64), unique=True)
156 activated = Column(Boolean, nullable=False, default=True)
157 enabled = Column(
158 Boolean,
159 nullable=False,
160 default=True,
161 server_default=sqlalchemy.sql.true(),
162 )
163 removed = Column(
164 Boolean,
165 nullable=False,
166 default=False,
167 server_default=sqlalchemy.sql.false(),
168 )
169 services = relationship(
170 "Service",
171 secondary=users_services,
172 back_populates="users",
173 lazy="joined",
174 )
175 inbounds = relationship(
176 "Inbound",
177 secondary="join(users_services, inbounds_services, inbounds_services.c.service_id == users_services.c.service_id)"
178 ".join(Inbound, Inbound.id == inbounds_services.c.inbound_id)",
179 viewonly=True,
180 distinct_target_key=True,
181 )
182 used_traffic = Column(BigInteger, default=0)
183 lifetime_used_traffic = Column(
184 BigInteger, default=0, server_default="0", nullable=False
185 )
186 traffic_reset_at = Column(DateTime)
187 node_usages = relationship(
188 "NodeUserUsage",
189 back_populates="user",
190 cascade="all,delete,delete-orphan",
191 )
192 data_limit = Column(BigInteger)
193 data_limit_reset_strategy = Column(
194 Enum(UserDataUsageResetStrategy),
195 nullable=False,
196 default=UserDataUsageResetStrategy.no_reset,
197 )
198 ip_limit = Column(Integer, nullable=False, default=-1)
199 settings = Column(String(1024))
200 expire_strategy = Column(
201 Enum(UserExpireStrategy),
202 nullable=False,
203 default=UserExpireStrategy.NEVER,
204 )
205 expire_date = Column(DateTime)
206 usage_duration = Column(BigInteger)
207 activation_deadline = Column(DateTime)
208 admin_id = Column(Integer, ForeignKey("admins.id"))
209 admin = relationship("Admin", back_populates="users")
210 sub_updated_at = Column(DateTime)
211 sub_last_user_agent = Column(String(512))
212 sub_revoked_at = Column(DateTime)
213 created_at = Column(DateTime, default=datetime.utcnow)
214 note = Column(String(500))
215 online_at = Column(DateTime)
216 edit_at = Column(DateTime)
217
218 @property
219 def service_ids(self):
220 return [service.id for service in self.services]
221
222 @hybrid_property
223 def expired(self):
224 if self.expire_strategy == "fixed_date":
225 return self.expire_date < datetime.utcnow()
226 return False
227
228 @expired.expression
229 def expired(cls):
230 return and_(
231 cls.expire_strategy == "fixed_date", cls.expire_date < func.now()
232 )
233
234 @hybrid_property
235 def data_limit_reached(self):
236 if self.data_limit is not None:
237 return self.used_traffic >= self.data_limit
238 return False
239
240 @data_limit_reached.expression
241 def data_limit_reached(cls):
242 return and_(
243 cls.data_limit.isnot(None), cls.used_traffic >= cls.data_limit
244 )
245
246 @hybrid_property
247 def is_active(self):
248 return (
249 self.enabled
250 and not self.expired
251 and not self.data_limit_reached
252 and not self.removed
253 )
254
255 @is_active.expression
256 def is_active(cls):
257 return and_(
258 cls.enabled == True,
259 ~cls.expired,
260 ~cls.data_limit_reached,
261 ~cls.removed,
262 )
263
264 @property
265 def status(self):
266 return UserStatus.ACTIVE if self.is_active else UserStatus.INACTIVE
267
268 @property
269 def subscription_url(self):
270 prefix = (
271 self.admin.subscription_url_prefix if self.admin else None
272 ) or SUBSCRIPTION_URL_PREFIX
273 return (
274 prefix.replace("*", secrets.token_hex(8))
275 + f"/sub/{self.username}/{self.key}"
276 )
277
278 @hybrid_property
279 def owner_username(self):
280 return self.admin.username if self.admin else None
281
282
283class Backend(Base):
284 __tablename__ = "backends"
285
286 id = Column(Integer, primary_key=True)
287 name = Column(String(64), nullable=False)
288 node_id = Column(Integer, ForeignKey("nodes.id"), index=True)
289 node = relationship("Node", back_populates="backends")
290 backend_type = Column(String(32), nullable=False)
291 version = Column(String(32))
292 running = Column(Boolean, default=True, nullable=False)
293
294
295class Inbound(Base):
296 __tablename__ = "inbounds"
297 __table_args__ = (UniqueConstraint("node_id", "tag"),)
298
299 id = Column(Integer, primary_key=True)
300 protocol = Column(Enum(ProxyTypes))
301 tag = Column(String(256), nullable=False)
302 config = Column(String(512), nullable=False)
303 node_id = Column(Integer, ForeignKey("nodes.id"), index=True)
304 node = relationship("Node", back_populates="inbounds")
305 services = relationship(
306 "Service", secondary=inbounds_services, back_populates="inbounds"
307 )
308 hosts = relationship(
309 "InboundHost",
310 back_populates="inbound",
311 cascade="all, delete, delete-orphan",
312 )
313
314 @property
315 def service_ids(self):
316 return [service.id for service in self.services]
317
318
319class HostChain(Base):
320 __tablename__ = "host_chains"
321
322 host_id = Column(Integer, ForeignKey("hosts.id"), primary_key=True)
323 chained_host_id = Column(Integer, ForeignKey("hosts.id"))
324 seq = Column(Integer, primary_key=True)
325
326 host = relationship(
327 "InboundHost", foreign_keys=[host_id], back_populates="chain"
328 )
329 chained_host = relationship(
330 "InboundHost",
331 foreign_keys=[chained_host_id],
332 lazy="joined",
333 )
334
335
336class InboundHost(Base):
337 __tablename__ = "hosts"
338
339 id = Column(Integer, primary_key=True)
340 remark = Column(String(256), nullable=False)
341 address = Column(String(256), nullable=False)
342 host_protocol = Column(String(32))
343 host_network = Column(String(32))
344 uuid = Column(String(36))
345 password = Column(String(128))
346 port = Column(Integer)
347 path = Column(String(256))
348 sni = Column(String(1024))
349 host = Column(String(1024))
350 security = Column(
351 Enum(InboundHostSecurity),
352 nullable=False,
353 default=InboundHostSecurity.inbound_default,
354 )
355 alpn = Column(
356 String(32),
357 server_default=sqlalchemy.sql.null(),
358 )
359 fingerprint = Column(
360 Enum(InboundHostFingerprint),
361 nullable=False,
362 default=InboundHostSecurity.none,
363 server_default=InboundHostSecurity.none.name,
364 )
365
366 fragment = Column(JSON())
367 udp_noises = Column(JSON())
368 http_headers = Column(JSON())
369 dns_servers = Column(String(128))
370 mtu = Column(Integer)
371 allowed_ips = Column(Text())
372 header_type = Column(String(32))
373 reality_public_key = Column(String(128))
374 reality_short_ids = Column(JSON())
375 flow = Column(String(32))
376 shadowtls_version = Column(Integer)
377 shadowsocks_method = Column(String(32))
378 splithttp_settings = Column(JSON())
379 mux_settings = Column(JSON())
380 early_data = Column(Integer)
381 inbound_id = Column(Integer, ForeignKey("inbounds.id"), nullable=True)
382 inbound = relationship("Inbound", back_populates="hosts", lazy="joined")
383 allowinsecure = Column(Boolean, default=False)
384 is_disabled = Column(Boolean, default=False)
385 weight = Column(Integer, default=1, nullable=False, server_default="1")
386
387 universal = Column(
388 Boolean,
389 default=False,
390 nullable=False,
391 server_default=sqlalchemy.sql.false(),
392 )
393 services = relationship("Service", secondary=hosts_services)
394
395 @property
396 def service_ids(self):
397 return [service.id for service in self.services]
398
399 chain = relationship(
400 "HostChain",
401 foreign_keys="[HostChain.host_id]",
402 order_by=HostChain.seq,
403 collection_class=ordering_list("seq"),
404 lazy="joined",
405 cascade="all, delete-orphan",
406 )
407
408 @property
409 def chain_ids(self):
410 return [c.chained_host_id for c in self.chain]
411
412 @property
413 def protocol(self):
414 return self.inbound.protocol if self.inbound else self.host_protocol
415
416 @property
417 def network(self):
418 return self.host_network
419
420 @property
421 def noise(self):
422 return self.udp_noises
423
424
425class System(Base):
426 __tablename__ = "system"
427
428 id = Column(Integer, primary_key=True)
429 uplink = Column(BigInteger, default=0)
430 downlink = Column(BigInteger, default=0)
431
432
433class JWT(Base):
434 __tablename__ = "jwt"
435
436 id = Column(Integer, primary_key=True)
437 secret_key = Column(
438 String(64), nullable=False, default=lambda: os.urandom(32).hex()
439 )
440
441
442class TLS(Base):
443 __tablename__ = "tls"
444
445 id = Column(Integer, primary_key=True)
446 key = Column(String(4096), nullable=False)
447 certificate = Column(String(2048), nullable=False)
448
449
450class Node(Base):
451 __tablename__ = "nodes"
452 __table_args__ = (UniqueConstraint("address", "port"),)
453 id = Column(Integer, primary_key=True)
454 name = Column(String(256), unique=True)
455 connection_backend = Column(String(32))
456 address = Column(String(256))
457 port = Column(Integer)
458 xray_version = Column(String(32))
459 inbounds = relationship(
460 "Inbound", back_populates="node", cascade="all, delete"
461 )
462 backends = relationship(
463 "Backend", back_populates="node", cascade="all, delete"
464 )
465 status = Column(
466 Enum(NodeStatus), nullable=False, default=NodeStatus.unhealthy
467 )
468 last_status_change = Column(DateTime, default=datetime.utcnow)
469 message = Column(String(1024))
470 created_at = Column(DateTime, default=datetime.utcnow)
471 uplink = Column(BigInteger, default=0)
472 downlink = Column(BigInteger, default=0)
473 user_usages = relationship(
474 "NodeUserUsage",
475 back_populates="node",
476 cascade="save-update, merge",
477 )
478 usages = relationship(
479 "NodeUsage",
480 back_populates="node",
481 cascade="save-update, merge",
482 )
483 usage_coefficient = Column(
484 Float, nullable=False, server_default=text("1.0"), default=1
485 )
486
487 @property
488 def inbound_ids(self):
489 return [inbound.id for inbound in self.inbounds]
490
491
492class NodeUserUsage(Base):
493 __tablename__ = "node_user_usages"
494 __table_args__ = (UniqueConstraint("created_at", "user_id", "node_id"),)
495
496 id = Column(Integer, primary_key=True)
497 created_at = Column(DateTime, nullable=False) # one hour per record
498 user_id = Column(Integer, ForeignKey("users.id"))
499 user = relationship("User", back_populates="node_usages")
500 node_id = Column(Integer, ForeignKey("nodes.id"))
501 node = relationship("Node", back_populates="user_usages")
502 used_traffic = Column(BigInteger, default=0)
503
504
505class NodeUsage(Base):
506 __tablename__ = "node_usages"
507 __table_args__ = (UniqueConstraint("created_at", "node_id"),)
508
509 id = Column(Integer, primary_key=True)
510 created_at = Column(DateTime, nullable=False) # one hour per record
511 node_id = Column(Integer, ForeignKey("nodes.id"))
512 node = relationship("Node", back_populates="usages")
513 uplink = Column(BigInteger, default=0)
514 downlink = Column(BigInteger, default=0)
515
516
517class Settings(Base):
518 __tablename__ = "settings"
519
520 id = Column(Integer, primary_key=True, server_default=text("0"))
521 subscription = Column(JSON, nullable=False)
522 telegram = Column(JSON)