inbound_modal.html 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368
  1. {{define "modals/inboundModal"}}
  2. <a-modal id="inbound-modal" v-model="inModal.visible" :title="inModal.title" :dialog-style="{ top: '20px' }"
  3. @ok="inModal.ok" :confirm-loading="inModal.confirmLoading" :closable="true" :mask-closable="false"
  4. :class="themeSwitcher.currentTheme" :ok-text="inModal.okText" cancel-text='{{ i18n "close" }}'>
  5. {{template "form/inbound" .}}
  6. </a-modal>
  7. <script>
  8. // Make inModal globally available to ensure it works with any base path
  9. const inModal = (window.inModal = {
  10. title: "",
  11. visible: false,
  12. confirmLoading: false,
  13. okText: '{{ i18n "sure" }}',
  14. isEdit: false,
  15. confirm: null,
  16. inbound: new Inbound(),
  17. dbInbound: new DBInbound(),
  18. ok() {
  19. // Block submit when Vision Seed is XRV-gated and partially/invalidly filled.
  20. const seedErr = inModal.testseedError();
  21. if (seedErr) {
  22. if (typeof Vue !== "undefined" && Vue.prototype && Vue.prototype.$message) {
  23. Vue.prototype.$message.error(seedErr);
  24. } else {
  25. alert(seedErr);
  26. }
  27. return;
  28. }
  29. ObjectUtil.execute(inModal.confirm, inModal.inbound, inModal.dbInbound);
  30. },
  31. show({
  32. title = "",
  33. okText = '{{ i18n "sure" }}',
  34. inbound = null,
  35. dbInbound = null,
  36. confirm = (inbound, dbInbound) => {},
  37. isEdit = false,
  38. }) {
  39. this.title = title;
  40. this.okText = okText;
  41. if (inbound) {
  42. this.inbound = Inbound.fromJson(inbound.toJson());
  43. } else {
  44. this.inbound = new Inbound();
  45. }
  46. // Ensure VLESS settings has a testseed array reference for Vue reactivity,
  47. // but leave it empty so we don't auto-emit defaults — user must explicitly
  48. // fill all four fields, or leave blank to fall back to backend defaults.
  49. if (this.inbound.protocol === Protocols.VLESS && this.inbound.settings) {
  50. if (!Array.isArray(this.inbound.settings.testseed)) {
  51. this.inbound.settings.testseed = [];
  52. }
  53. }
  54. if (dbInbound) {
  55. this.dbInbound = new DBInbound(dbInbound);
  56. } else {
  57. this.dbInbound = new DBInbound();
  58. }
  59. this.confirm = confirm;
  60. this.visible = true;
  61. this.isEdit = isEdit;
  62. },
  63. close() {
  64. inModal.visible = false;
  65. inModal.loading(false);
  66. },
  67. loading(loading = true) {
  68. inModal.confirmLoading = loading;
  69. },
  70. // Returns an error string when the current testseed state would be rejected,
  71. // or "" when it's valid (empty == use defaults; full 4 positive ints == custom).
  72. testseedError() {
  73. if (!inModal.inbound || inModal.inbound.protocol !== Protocols.VLESS) return "";
  74. if (typeof inModal.inbound.canEnableVisionSeed === "function"
  75. && !inModal.inbound.canEnableVisionSeed()) return "";
  76. const seed = inModal.inbound.settings && inModal.inbound.settings.testseed;
  77. if (!Array.isArray(seed) || seed.length === 0) return "";
  78. const filled = seed.filter(v => v !== null && v !== undefined && v !== "");
  79. if (filled.length === 0) return "";
  80. if (seed.length !== 4 || filled.length !== 4 ||
  81. !seed.every(v => Number.isInteger(v) && v > 0)) {
  82. return "Provide exactly 4 positive integers or leave empty to use defaults.";
  83. }
  84. return "";
  85. },
  86. // Vision Seed helpers — always available regardless of Vue context
  87. updateTestseed(index, value) {
  88. if (!inModal.inbound || !inModal.inbound.settings) return;
  89. if (!Array.isArray(inModal.inbound.settings.testseed)) {
  90. inModal.inbound.settings.testseed = [];
  91. }
  92. const seed = inModal.inbound.settings.testseed;
  93. while (seed.length <= index) seed.push(null);
  94. seed[index] = value;
  95. // If user cleared every slot, collapse back to empty so we omit testseed entirely.
  96. if (seed.every(v => v === null || v === undefined || v === "")) {
  97. inModal.inbound.settings.testseed = [];
  98. }
  99. },
  100. setRandomTestseed() {
  101. if (!inModal.inbound || !inModal.inbound.settings) return;
  102. // Positive integers only (>=1) so the array passes validation and gets emitted.
  103. inModal.inbound.settings.testseed = [
  104. Math.floor(Math.random() * 999) + 1,
  105. Math.floor(Math.random() * 999) + 1,
  106. Math.floor(Math.random() * 999) + 1,
  107. Math.floor(Math.random() * 999) + 1,
  108. ];
  109. },
  110. resetTestseed() {
  111. if (!inModal.inbound || !inModal.inbound.settings) return;
  112. // Empty == "use server defaults [900, 500, 900, 256]"; placeholders show in the form.
  113. inModal.inbound.settings.testseed = [];
  114. },
  115. });
  116. // Store Vue instance globally to ensure methods are always accessible
  117. let inboundModalVueInstance = null;
  118. inboundModalVueInstance = new Vue({
  119. delimiters: ["[[", "]]"],
  120. el: "#inbound-modal",
  121. data: {
  122. inModal: inModal,
  123. delayedStart: false,
  124. get inbound() {
  125. return inModal.inbound;
  126. },
  127. get dbInbound() {
  128. return inModal.dbInbound;
  129. },
  130. get isEdit() {
  131. return inModal.isEdit;
  132. },
  133. get client() {
  134. return inModal.inbound &&
  135. inModal.inbound.clients &&
  136. inModal.inbound.clients.length > 0 ?
  137. inModal.inbound.clients[0] :
  138. null;
  139. },
  140. get datepicker() {
  141. return app.datepicker;
  142. },
  143. get delayedExpireDays() {
  144. return this.client && this.client.expiryTime < 0 ?
  145. this.client.expiryTime / -86400000 :
  146. 0;
  147. },
  148. set delayedExpireDays(days) {
  149. this.client.expiryTime = -86400000 * days;
  150. },
  151. get externalProxy() {
  152. return this.inbound.stream.externalProxy.length > 0;
  153. },
  154. set externalProxy(value) {
  155. if (value) {
  156. inModal.inbound.stream.externalProxy = [{
  157. forceTls: "same",
  158. dest: window.location.hostname,
  159. port: inModal.inbound.port,
  160. remark: "",
  161. }, ];
  162. } else {
  163. inModal.inbound.stream.externalProxy = [];
  164. }
  165. },
  166. },
  167. watch: {
  168. "inModal.inbound.stream.security"(newVal, oldVal) {
  169. // Clear flow when security changes from reality/tls to none
  170. if (
  171. inModal.inbound.protocol == Protocols.VLESS &&
  172. !inModal.inbound.canEnableTlsFlow()
  173. ) {
  174. inModal.inbound.settings.vlesses.forEach((client) => {
  175. client.flow = "";
  176. });
  177. }
  178. },
  179. // Keep testseed as a valid array reference for Vue reactivity while the user
  180. // toggles flows — but do NOT auto-fill defaults. Empty means "use server defaults"
  181. // and is the only way the form omits testseed from the outbound JSON.
  182. "inModal.inbound.settings.vlesses": {
  183. handler() {
  184. if (
  185. inModal.inbound.protocol === Protocols.VLESS &&
  186. inModal.inbound.settings &&
  187. !Array.isArray(inModal.inbound.settings.testseed)
  188. ) {
  189. inModal.inbound.settings.testseed = [];
  190. }
  191. },
  192. deep: true,
  193. },
  194. },
  195. methods: {
  196. streamNetworkChange() {
  197. if (!inModal.inbound.canEnableTls()) {
  198. this.inModal.inbound.stream.security = "none";
  199. }
  200. if (!inModal.inbound.canEnableReality()) {
  201. this.inModal.inbound.reality = false;
  202. }
  203. if (
  204. this.inModal.inbound.protocol == Protocols.VLESS &&
  205. !inModal.inbound.canEnableTlsFlow()
  206. ) {
  207. this.inModal.inbound.settings.vlesses.forEach((client) => {
  208. client.flow = "";
  209. });
  210. }
  211. if (inModal.inbound.stream.network != "kcp") {
  212. inModal.inbound.stream.finalmask.udp = [];
  213. }
  214. },
  215. SSMethodChange() {
  216. this.inModal.inbound.settings.password =
  217. RandomUtil.randomShadowsocksPassword(
  218. this.inModal.inbound.settings.method,
  219. );
  220. if (this.inModal.inbound.isSSMultiUser) {
  221. if (this.inModal.inbound.settings.shadowsockses.length == 0) {
  222. this.inModal.inbound.settings.shadowsockses = [
  223. new Inbound.ShadowsocksSettings.Shadowsocks(),
  224. ];
  225. }
  226. if (!this.inModal.inbound.isSS2022) {
  227. this.inModal.inbound.settings.shadowsockses.forEach((client) => {
  228. client.method = this.inModal.inbound.settings.method;
  229. });
  230. } else {
  231. this.inModal.inbound.settings.shadowsockses.forEach((client) => {
  232. client.method = "";
  233. });
  234. }
  235. this.inModal.inbound.settings.shadowsockses.forEach((client) => {
  236. client.password = RandomUtil.randomShadowsocksPassword(
  237. this.inModal.inbound.settings.method,
  238. );
  239. });
  240. } else {
  241. if (this.inModal.inbound.settings.shadowsockses.length > 0) {
  242. this.inModal.inbound.settings.shadowsockses = [];
  243. }
  244. }
  245. },
  246. setDefaultCertData(index) {
  247. inModal.inbound.stream.tls.certs[index].certFile = app.defaultCert;
  248. inModal.inbound.stream.tls.certs[index].keyFile = app.defaultKey;
  249. },
  250. async getNewX25519Cert() {
  251. inModal.loading(true);
  252. const msg = await HttpUtil.get("/panel/api/server/getNewX25519Cert");
  253. inModal.loading(false);
  254. if (!msg.success) {
  255. return;
  256. }
  257. inModal.inbound.stream.reality.privateKey = msg.obj.privateKey;
  258. inModal.inbound.stream.reality.settings.publicKey = msg.obj.publicKey;
  259. },
  260. clearX25519Cert() {
  261. this.inbound.stream.reality.privateKey = "";
  262. this.inbound.stream.reality.settings.publicKey = "";
  263. },
  264. async getNewmldsa65() {
  265. inModal.loading(true);
  266. const msg = await HttpUtil.get("/panel/api/server/getNewmldsa65");
  267. inModal.loading(false);
  268. if (!msg.success) {
  269. return;
  270. }
  271. inModal.inbound.stream.reality.mldsa65Seed = msg.obj.seed;
  272. inModal.inbound.stream.reality.settings.mldsa65Verify = msg.obj.verify;
  273. },
  274. clearMldsa65() {
  275. this.inbound.stream.reality.mldsa65Seed = "";
  276. this.inbound.stream.reality.settings.mldsa65Verify = "";
  277. },
  278. randomizeRealityTarget() {
  279. if (typeof getRandomRealityTarget !== "undefined") {
  280. const randomTarget = getRandomRealityTarget();
  281. this.inbound.stream.reality.target = randomTarget.target;
  282. this.inbound.stream.reality.serverNames = randomTarget.sni;
  283. }
  284. },
  285. async getNewEchCert() {
  286. inModal.loading(true);
  287. const msg = await HttpUtil.post("/panel/api/server/getNewEchCert", {
  288. sni: inModal.inbound.stream.tls.sni,
  289. });
  290. inModal.loading(false);
  291. if (!msg.success) {
  292. return;
  293. }
  294. inModal.inbound.stream.tls.echServerKeys = msg.obj.echServerKeys;
  295. inModal.inbound.stream.tls.settings.echConfigList =
  296. msg.obj.echConfigList;
  297. },
  298. clearEchCert() {
  299. this.inbound.stream.tls.echServerKeys = "";
  300. this.inbound.stream.tls.settings.echConfigList = "";
  301. },
  302. // Pulls the requested auth block from `xray vlessenc` (which always returns
  303. // both X25519 and ML-KEM-768 variants) and applies it to the inbound's
  304. // decryption/encryption strings. The auth mode is implied by the resulting
  305. async getNewVlessEnc(authLabel) {
  306. if (!authLabel) return;
  307. inModal.loading(true);
  308. const msg = await HttpUtil.get("/panel/api/server/getNewVlessEnc");
  309. inModal.loading(false);
  310. if (!msg.success) {
  311. return;
  312. }
  313. const auths = msg.obj.auths || [];
  314. const block = auths.find((a) => a.label === authLabel);
  315. if (!block) {
  316. console.error("No auth block for", authLabel);
  317. return;
  318. }
  319. inModal.inbound.settings.decryption = block.decryption;
  320. inModal.inbound.settings.encryption = block.encryption;
  321. },
  322. clearVlessEnc() {
  323. this.inbound.settings.decryption = "none";
  324. this.inbound.settings.encryption = "none";
  325. },
  326. // Vision Seed methods - must be in Vue methods for proper template binding.
  327. // Mirror the inModal helpers but use Vue.set so the form re-renders.
  328. updateTestseed(index, value) {
  329. if (!Array.isArray(this.inbound.settings.testseed)) {
  330. this.$set(this.inbound.settings, "testseed", []);
  331. }
  332. const seed = this.inbound.settings.testseed;
  333. while (seed.length <= index) seed.push(null);
  334. this.$set(seed, index, value);
  335. // Collapse to empty when every slot is cleared so testseed is omitted from JSON.
  336. if (seed.every(v => v === null || v === undefined || v === "")) {
  337. this.$set(this.inbound.settings, "testseed", []);
  338. }
  339. },
  340. setRandomTestseed() {
  341. // Positive integers only (>=1) so the resulting array passes validation.
  342. const newSeed = [
  343. Math.floor(Math.random() * 999) + 1,
  344. Math.floor(Math.random() * 999) + 1,
  345. Math.floor(Math.random() * 999) + 1,
  346. Math.floor(Math.random() * 999) + 1,
  347. ];
  348. this.$set(this.inbound.settings, "testseed", newSeed);
  349. },
  350. resetTestseed() {
  351. // Empty == "use server defaults [900, 500, 900, 256]". Placeholders will show in the form.
  352. this.$set(this.inbound.settings, "testseed", []);
  353. },
  354. testseedError() {
  355. return inModal.testseedError();
  356. },
  357. },
  358. });
  359. </script>
  360. {{end}}