1
0

inbound_modal.html 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370
  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. async getNewVlessEnc() {
  303. const selected = inModal.inbound.settings.selectedAuth;
  304. if (!selected) {
  305. this.clearVlessEnc();
  306. return;
  307. }
  308. inModal.loading(true);
  309. const msg = await HttpUtil.get("/panel/api/server/getNewVlessEnc");
  310. inModal.loading(false);
  311. if (!msg.success) {
  312. return;
  313. }
  314. const auths = msg.obj.auths || [];
  315. const block = auths.find((a) => a.label === selected);
  316. if (!block) {
  317. console.error("No auth block for", selected);
  318. return;
  319. }
  320. inModal.inbound.settings.decryption = block.decryption;
  321. inModal.inbound.settings.encryption = block.encryption;
  322. },
  323. clearVlessEnc() {
  324. this.inbound.settings.decryption = "none";
  325. this.inbound.settings.encryption = "none";
  326. this.inbound.settings.selectedAuth = undefined;
  327. },
  328. // Vision Seed methods - must be in Vue methods for proper template binding.
  329. // Mirror the inModal helpers but use Vue.set so the form re-renders.
  330. updateTestseed(index, value) {
  331. if (!Array.isArray(this.inbound.settings.testseed)) {
  332. this.$set(this.inbound.settings, "testseed", []);
  333. }
  334. const seed = this.inbound.settings.testseed;
  335. while (seed.length <= index) seed.push(null);
  336. this.$set(seed, index, value);
  337. // Collapse to empty when every slot is cleared so testseed is omitted from JSON.
  338. if (seed.every(v => v === null || v === undefined || v === "")) {
  339. this.$set(this.inbound.settings, "testseed", []);
  340. }
  341. },
  342. setRandomTestseed() {
  343. // Positive integers only (>=1) so the resulting array passes validation.
  344. const newSeed = [
  345. Math.floor(Math.random() * 999) + 1,
  346. Math.floor(Math.random() * 999) + 1,
  347. Math.floor(Math.random() * 999) + 1,
  348. Math.floor(Math.random() * 999) + 1,
  349. ];
  350. this.$set(this.inbound.settings, "testseed", newSeed);
  351. },
  352. resetTestseed() {
  353. // Empty == "use server defaults [900, 500, 900, 256]". Placeholders will show in the form.
  354. this.$set(this.inbound.settings, "testseed", []);
  355. },
  356. testseedError() {
  357. return inModal.testseedError();
  358. },
  359. },
  360. });
  361. </script>
  362. {{end}}