[
  {
    "path": ".github/ISSUE_TEMPLATE.md",
    "content": "<!--\n请根据以下模版提issue，不提供必要信息的issue将直接关闭\n日志文件在proxyee-down安装目录/main/log文件夹里\n-->\n### 问题描述(必要)\n### 版本号(必要)\n### 操作系统(必要)\n### 相关截图\n### 相关日志\n"
  },
  {
    "path": ".gitignore",
    "content": ".DS_Store\n\ntarget/\nlog/\n!.mvn/wrapper/maven-wrapper.jar\n\n### STS ###\n.apt_generated\n.classpath\n.factorypath\n.project\n.settings\n.springBeans\n\n### IntelliJ IDEA ###\n.idea\n*.iws\n*.iml\n*.ipr"
  },
  {
    "path": "LICENSE",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "README.md",
    "content": "## 续作出炉！！！\n\n新项目使用`golang`+`flutter`开发，支持所有平台的下载器，地址：https://github.com/GopeedLab/gopeed\n\n## 暂停维护此项目\n首先感谢大家支持和反馈才使得proxyee-down能一直迭代到现在的版本，但由于本人精力有限，宣布暂时停止此项目的维护，并且关闭**issue**模块。\n\n其次因为`JAVA`不太适合做客户端开发，打包后体积太大且内存占用太高，本人计划在空余时间用`GO`来重写一遍，目标是打造一个`体积小`、`跨平台`、`内存低`、`可扩展`、`免费`的下载器。\n\n![](https://i.imgur.com/dUvNgmd.jpg)  \n\n# [Proxyee Down](https://pdown.org)\n[![Author](https://img.shields.io/badge/author-monkeyWie-red.svg?style=flat-square)](https://github.com/monkeyWie)\n[![Contributors](https://img.shields.io/github/contributors/proxyee-down-org/proxyee-down.svg?style=flat-square)](https://github.com/proxyee-down-org/proxyee-down/graphs/contributors)\n[![Stargazers](https://img.shields.io/github/stars/proxyee-down-org/proxyee-down.svg?style=flat-square)](https://github.com/proxyee-down-org/proxyee-down/stargazers)\n[![Fork](https://img.shields.io/github/forks/proxyee-down-org/proxyee-down.svg?style=flat-square)](https://github.com/proxyee-down-org/proxyee-down/fork)\n[![License](https://img.shields.io/github/license/proxyee-down-org/proxyee-down.svg?style=flat-square)](https://github.com/proxyee-down-org/proxyee-down/blob/master/LICENSE)\n\n> Proxyee Down 是一款开源的免费 HTTP 高速下载器，底层使用`netty`开发，支持自定义 HTTP 请求下载且支持扩展功能，可以通过安装扩展实现特殊的下载需求。\n\n## 使用教程\n\n[点击查看教程](https://github.com/proxyee-down-org/proxyee-down/wiki/%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B)\n\n## 交流群\n\n1 群**11352304**、2 群**20236964**、3 群**20233754**、4 群**737991056**\n\n## 开发\n\n本项目后端主要使用 `java` + `spring` + `boot` + `netty`，前端使用 `vue.js` + `iview`\n\n### 环境\n![](https://img.shields.io/badge/JAVA-1.8%2B-brightgreen.svg) ![](https://img.shields.io/badge/maven-3.0%2B-brightgreen.svg) ![](https://img.shields.io/badge/node.js-8.0%2B-brightgreen.svg)\n\n\toracle jdk 1.8+或 openjfx(openjdk默认不包含javafx包)\n\n### 编译\n\n```\ngit clone https://github.com/proxyee-down-org/proxyee-down.git\ncd proxyee-down/front\n#build html\nnpm install\nnpm run build\ncd ../main\nmvn clean package -Pprd\n```\n\n### 运行\n```\njava -jar proxyee-down-main.jar\n```\n"
  },
  {
    "path": "front/.eslintrc.js",
    "content": "module.exports = {\n  root: true,\n  env: {\n    node: true\n  },\n  extends: ['plugin:vue/essential', 'eslint:recommended'],\n  rules: {\n    'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',\n    'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',\n    'no-alert': process.env.NODE_ENV === 'production' ? 'error' : 'off',\n    //强制使用单引号\n    quotes: ['error', 'single'],\n    //强制不使用分号结尾\n    semi: ['error', 'never']\n  },\n  parserOptions: {\n    parser: 'babel-eslint'\n  }\n}\n"
  },
  {
    "path": "front/.gitignore",
    "content": ".DS_Store\nnode_modules\n/dist\n\n# local env files\n.env.local\n.env.*.local\n\n# Log files\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n# Editor directories and files\n.idea\n\n*.suo\n*.ntvs*\n*.njsproj4\n*.sln\n*.sw*\n\npackage-lock.json"
  },
  {
    "path": "front/.postcssrc.js",
    "content": "module.exports = {\n  plugins: {\n    autoprefixer: {}\n  }\n}"
  },
  {
    "path": "front/.prettierrc",
    "content": "{\n  \"eslintIntegration\": true,\n  \"singleQuote\": true,\n  \"semi\": false,\n  \"printWidth\": 120\n}\n"
  },
  {
    "path": "front/.vscode/settings.json",
    "content": "{\n  \"vetur.format.defaultFormatter.html\": \"js-beautify-html\",\n  \"vetur.format.defaultFormatterOptions\": {\n    \"js-beautify-html\": {\n      \"wrap_attributes\": \"force\"\n    }\n  },\n  \"files.associations\": {\n    \"*.vue\": \"vue\"\n  },\n  \"eslint.validate\": [\n    \"javascript\",\n    \"javascriptreact\",\n    {\n      \"language\": \"vue\",\n      \"autoFix\": true\n    }\n  ],\n  \"eslint.autoFixOnSave\": true\n}\n"
  },
  {
    "path": "front/babel.config.js",
    "content": "module.exports = {\r\n  presets: ['@vue/app'],\r\n  plugins: ['jsx-v-model']\r\n}\r\n"
  },
  {
    "path": "front/package.json",
    "content": "{\n  \"name\": \"proxyee-down\",\n  \"version\": \"3.0.0\",\n  \"private\": true,\n  \"scripts\": {\n    \"start\": \"npm run serve\",\n    \"serve\": \"vue-cli-service serve\",\n    \"build\": \"vue-cli-service build\",\n    \"lint\": \"vue-cli-service lint\"\n  },\n  \"dependencies\": {\n    \"axios\": \"^0.18.0\",\n    \"iview\": \"^2.14.3\",\n    \"numeral\": \"^2.0.6\",\n    \"reconnecting-websocket\": \"^4.1.0\",\n    \"vue\": \"^2.5.17\",\n    \"vue-i18n\": \"^8.0.0\",\n    \"vue-router\": \"^3.0.1\",\n    \"vuex\": \"^3.0.1\"\n  },\n  \"devDependencies\": {\n    \"@vue/cli-plugin-babel\": \"^3.0.1\",\n    \"@vue/cli-plugin-eslint\": \"^3.0.1\",\n    \"@vue/cli-service\": \"^3.0.1\",\n    \"@vue/eslint-config-prettier\": \"^3.0.1\",\n    \"babel-plugin-jsx-v-model\": \"^2.0.3\",\n    \"iview-loader\": \"^1.2.1\",\n    \"less\": \"^3.8.1\",\n    \"less-loader\": \"^4.1.0\",\n    \"lint-staged\": \"^6.0.0\",\n    \"vue-template-compiler\": \"^2.5.17\"\n  },\n  \"browserslist\": [\n    \"> 1%\",\n    \"last 2 versions\",\n    \"not ie <= 8\"\n  ],\n  \"gitHooks\": {\n    \"pre-commit\": \"lint-staged\"\n  },\n  \"lint-staged\": {\n    \"*.js\": [\n      \"vue-cli-service lint\",\n      \"git add\"\n    ],\n    \"*.vue\": [\n      \"vue-cli-service lint\",\n      \"git add\"\n    ]\n  }\n}\n"
  },
  {
    "path": "front/public/index.html",
    "content": "<!DOCTYPE html>\r\n<html>\r\n\r\n<head>\r\n  <meta charset=\"utf-8\">\r\n  <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\r\n  <meta name=\"viewport\" content=\"width=device-width,initial-scale=1.0\">\r\n  <link rel=\"icon\" href=\"<%= BASE_URL %>favicon.ico\">\r\n  <title>Proxyee Down</title>\r\n  <style>\r\n    body {\r\n      background-color: #f6f7f9 !important;\r\n    }\r\n  </style>\r\n</head>\r\n\r\n<body>\r\n  <noscript>\r\n    <strong>We're sorry but front doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>\r\n  </noscript>\r\n  <div id=\"app\"></div>\r\n  <!-- built files will be auto injected -->\r\n</body>\r\n\r\n</html>"
  },
  {
    "path": "front/src/App.vue",
    "content": "<template>\r\n  <div id=\"app\">\r\n    <i-menu mode=\"horizontal\"\r\n      theme=\"dark\"\r\n      :active-name=\"$route.path.substring(1)\"\r\n      @on-select=\"forward\">\r\n      <Badge :count=\"$root.badges.tasks\">\r\n        <i-menu-item name=\"tasks\">\r\n          <Icon type=\"ios-download-outline\"></Icon>\r\n          {{ $t(\"nav.tasks\") }}\r\n        </i-menu-item>\r\n      </Badge>\r\n      <Badge :count=\"$root.badges.extension\">\r\n        <i-menu-item name=\"extension\">\r\n          <Icon type=\"social-windows\"></Icon>\r\n          {{ $t(\"nav.extension\") }}\r\n        </i-menu-item>\r\n      </Badge>\r\n      <Badge :count=\"$root.badges.setting\">\r\n        <i-menu-item name=\"setting\">\r\n          <Icon type=\"settings\"></Icon>\r\n          {{ $t(\"nav.setting\") }}\r\n        </i-menu-item>\r\n      </Badge>\r\n      <Badge :count=\"$root.badges.about\">\r\n        <i-menu-item name=\"about\">\r\n          <Icon type=\"information-circled\"></Icon>\r\n          {{ $t(\"nav.about\") }}\r\n        </i-menu-item>\r\n      </Badge>\r\n      <Badge :count=\"$root.badges.support\">\r\n        <i-menu-item name=\"support\">\r\n          <Icon type=\"social-usd\"></Icon>\r\n          {{ $t(\"nav.support\") }}\r\n        </i-menu-item>\r\n      </Badge>\r\n    </i-menu>\r\n\r\n    <div style=\"padding: 1.25rem 1.25rem\">\r\n      <keep-alive>\r\n        <router-view />\r\n      </keep-alive>\r\n    </div>\r\n  </div>\r\n</template>\r\n\r\n<script>\r\nimport { checkCert, getExtensions } from './common/native'\r\n\r\nexport default {\r\n  methods: {\r\n    forward(route) {\r\n      this.$router.push(route)\r\n    }\r\n  },\r\n\r\n  data() {\r\n    return {\r\n      badges: { tasks: 0, extension: 2, setting: 0, about: 0, support: 0 }\r\n    }\r\n  },\r\n\r\n  async created() {\r\n    // Check update\r\n    if (this.$config.needCheckUpdate) {\r\n      try {\r\n        const { data: versionInfo } = await this.$noSpinHttp.get(this.$config.adminServer + 'version/checkUpdate')\r\n        if (versionInfo && versionInfo.version > this.$config.version) {\r\n          this.$router.push({ path: '/about', query: { checkUpdate: true, versionInfo: JSON.stringify(versionInfo) } })\r\n        }\r\n      } catch (e) {\r\n        console.error(e)\r\n      }\r\n    }\r\n    // Check extension update\r\n    try {\r\n      const status = await checkCert()\r\n      if (status) {\r\n        const extensions = await getExtensions()\r\n        if (extensions.length > 0) {\r\n          const { data: serverExtensions } = await this.$noSpinHttp.post(\r\n            this.$config.adminServer + 'extension/checkExtensionUpdate',\r\n            extensions.map(e => {\r\n              return { path: e.meta.path, version: e.version }\r\n            })\r\n          )\r\n          if (serverExtensions && serverExtensions.length > 0) {\r\n            this.$root.badges.extension = serverExtensions.length\r\n          }\r\n        }\r\n      }\r\n    } catch (e) {\r\n      console.error(e)\r\n    }\r\n  }\r\n}\r\n</script>\r\n\r\n<style>\r\ni.action-icon {\r\n  cursor: pointer;\r\n  font-size: 1.25rem;\r\n}\r\ni.tip-icon {\r\n  position: relative;\r\n  top: 5px;\r\n  padding-left: 5px;\r\n}\r\ni.action-icon + i.action-icon {\r\n  padding-left: 0.625rem;\r\n}\r\n</style>\r\n\r\n"
  },
  {
    "path": "front/src/common/http.js",
    "content": "import Vue from 'vue'\nimport axios from 'axios'\n\nexport default {\n  build() {\n    const client = axios.create()\n    client.interceptors.request.use(\n      config => {\n        Vue.prototype.$Spin.show()\n        return config\n      },\n      error => Promise.reject(error)\n    )\n    client.interceptors.response.use(\n      response => {\n        Vue.prototype.$Spin.hide()\n        return response\n      },\n      error => {\n        Vue.prototype.$Spin.hide()\n        return Promise.reject(error)\n      }\n    )\n    return client\n  }\n}\n"
  },
  {
    "path": "front/src/common/native.js",
    "content": "import http from './http'\nimport axios from 'axios'\n\nconst client = http.build()\nconst clientNoSpin = axios.create()\n\n/**\n * 弹出原生文件选择框\n */\nexport const showFileChooser = () => {\n  return new Promise((resolve, reject) => {\n    client\n      .get('/native/fileChooser')\n      .then(response => resolve(response.data))\n      .catch(error => reject(error))\n  })\n}\n\n/**\n * 弹出原生文件夹选择框\n */\nexport const showDirChooser = () => {\n  return new Promise((resolve, reject) => {\n    client\n      .get('/native/dirChooser')\n      .then(response => resolve(response.data))\n      .catch(error => reject(error))\n  })\n}\n\n/**\n * 取应用初始化配置信息\n */\nexport const getInitConfig = () => {\n  return new Promise((resolve, reject) => {\n    client\n      .get('/native/getInitConfig')\n      .then(response => resolve(response.data))\n      .catch(error => reject(error))\n  })\n}\n\n/**\n * 弹出系统资源管理器并选中指定文件\n * @param {string} path 文件路径\n */\nexport const showFile = path => {\n  return new Promise((resolve, reject) => {\n    client\n      .post('/native/showFile', { path: path })\n      .then(response => resolve(response.data))\n      .catch(error => reject(error))\n  })\n}\n\n/**\n * 检查证书是否安装\n */\nexport const checkCert = () => {\n  return new Promise((resolve, reject) => {\n    clientNoSpin\n      .get('/native/checkCert')\n      .then(response => resolve(response.data.status))\n      .catch(error => reject(error))\n  })\n}\n\n/**\n * 安装证书\n */\nexport const installCert = () => {\n  return new Promise((resolve, reject) => {\n    client\n      .get('/native/installCert')\n      .then(response => resolve(response.data.status))\n      .catch(error => reject(error))\n  })\n}\n\n/**\n * 取设置的代理模式\n */\nexport const getProxyMode = () => {\n  return new Promise((resolve, reject) => {\n    clientNoSpin\n      .get('/native/getProxyMode')\n      .then(response => resolve(response.data.mode))\n      .catch(error => reject(error))\n  })\n}\n\n/**\n * 修改代理模式\n * @param {number} mode 0.不接管系统代理 1.接管系统代理\n */\nexport const changeProxyMode = mode => {\n  return new Promise((resolve, reject) => {\n    client\n      .post('/native/changeProxyMode', { mode: mode })\n      .then(response => resolve(response.data))\n      .catch(error => reject(error))\n  })\n}\n\n/**\n * 取本地已安装的扩展列表\n */\nexport const getExtensions = () => {\n  return new Promise((resolve, reject) => {\n    clientNoSpin\n      .get('/native/getExtensions')\n      .then(response => resolve(response.data))\n      .catch(error => reject(error))\n  })\n}\n\n/**\n * 安装指定扩展\n * @param {object} data 扩展相关信息\n */\nexport const installExtension = data => {\n  return new Promise((resolve, reject) => {\n    clientNoSpin\n      .post('/native/installExtension', data)\n      .then(response => resolve(response.data))\n      .catch(error => reject(error))\n  })\n}\n\n/**\n * 更新指定扩展\n * @param {object} data 扩展相关信息\n */\nexport const updateExtension = data => {\n  return new Promise((resolve, reject) => {\n    clientNoSpin\n      .post('/native/updateExtension', data)\n      .then(response => resolve(response.data))\n      .catch(error => reject(error))\n  })\n}\n\n/**\n * 安装本地扩展\n * @param {string} path 扩展所在目录\n */\nexport const installLocalExtension = path => {\n  return new Promise((resolve, reject) => {\n    clientNoSpin\n      .post('/native/installLocalExtension', { path: path })\n      .then(response => resolve(response.data.data))\n      .catch(error => reject(error))\n  })\n}\n\n/**\n * 卸载扩展\n * @param {string} path 扩展所在目录\n * @param {boolean} isLocal 是否本地加载的扩展\n */\nexport const uninstallExtension = (path, local) => {\n  return new Promise((resolve, reject) => {\n    client\n      .post('/native/uninstallExtension', { path, local })\n      .then(response => resolve(response.data))\n      .catch(error => reject(error))\n  })\n}\n\n/**\n * 启用或禁用扩展\n * @param {object} data\n */\nexport const toggleExtension = data => {\n  return new Promise((resolve, reject) => {\n    client\n      .post('/native/toggleExtension', data)\n      .then(response => resolve(response.data))\n      .catch(error => reject(error))\n  })\n}\n\n/**\n * 保存指定扩展的设置\n * @param {String} path 扩展路径\n * @param {object} setting 扩展设置信息\n */\nexport const updateExtensionSetting = (path, setting) => {\n  return new Promise((resolve, reject) => {\n    client\n      .post('/native/updateExtensionSetting', { path, setting })\n      .then(response => resolve(response.data))\n      .catch(error => reject(error))\n  })\n}\n\n/**\n * 打开浏览器并访问指定url\n * @param {object} data\n */\nexport const openUrl = url => {\n  if (window.navigator.userAgent.indexOf('JavaFX') !== -1) {\n    clientNoSpin.post('/native/openUrl', { url: encodeURIComponent(url) })\n  } else {\n    window.open(url)\n  }\n}\n\n/**\n * 在解析任务时触发\n * @param {object} request\n */\nexport const onResolve = request => {\n  return new Promise((resolve, reject) => {\n    clientNoSpin\n      .post('/native/onResolve', request)\n      .then(response => resolve(response.data))\n      .catch(error => reject(error))\n  })\n}\n\n/**\n * 更新软件\n * @param {string} path 更新包下载地址\n */\nexport const doUpdate = path => {\n  return new Promise((resolve, reject) => {\n    clientNoSpin\n      .post('/native/doUpdate', { path: path })\n      .then(response => resolve(response.data))\n      .catch(error => reject(error))\n  })\n}\n\n/**\n * 更新软件进度获取\n */\nexport const getUpdateProgress = () => {\n  return new Promise((resolve, reject) => {\n    clientNoSpin\n      .get('/native/getUpdateProgress')\n      .then(response => resolve(response.data))\n      .catch(error => reject(error))\n  })\n}\n\n/**\n * 重启软件\n */\nexport const doRestart = () => {\n  return new Promise((resolve, reject) => {\n    client\n      .get('/native/doRestart')\n      .then(response => resolve(response.data))\n      .catch(error => reject(error))\n  })\n}\n\n/**\n * 取软件设置信息\n */\nexport const getConfig = () => {\n  return new Promise((resolve, reject) => {\n    clientNoSpin\n      .get('/native/getConfig')\n      .then(response => resolve(response.data))\n      .catch(error => reject(error))\n  })\n}\n\n/**\n * 保存软件设置\n * @param {object} config\n */\nexport const setConfig = config => {\n  return new Promise((resolve, reject) => {\n    clientNoSpin\n      .put('/native/setConfig', config)\n      .then(response => resolve(response))\n      .catch(error => reject(error))\n  })\n}\n\n/**\n * 复制数据到系统剪贴板\n * @param {object} data\n */\nexport const copy = data => {\n  return new Promise((resolve, reject) => {\n    clientNoSpin\n      .put('/native/copy', data)\n      .then(response => resolve(response))\n      .catch(error => reject(error))\n  })\n}\n"
  },
  {
    "path": "front/src/components/ExtensionSetting.vue",
    "content": "<template>\n  <Form :label-width=\"70\">\n    <FormItem v-for=\"(setting,index) in settings\"\n      :key=\"index\"\n      :label=\"setting.title\"\n      :prop=\"setting.name\">\n      <Switch v-if=\"setting.type==='Boolean'\"\n        v-model=\"setting.value\" />\n      <Select v-else-if=\"setting.options\"\n        style=\"width:90%\"\n        v-model=\"setting.value\">\n        <Option v-for=\"(key,label) in setting.options\"\n          :key=\"key\"\n          :value=\"key\">{{label}}</Option>\n      </Select>\n      <Input v-else\n        style=\"width:90%\"\n        v-model=\"setting.value\" />\n      <Tooltip class=\"item\"\n        placement=\"right\">\n        <Icon type=\"help-circled\"\n          class=\"action-icon tip-icon\" />\n        <div slot=\"content\"\n          style=\"white-space: normal;width:200px;\">\n          <p>{{ setting.description }}</p>\n        </div>\n      </Tooltip>\n    </FormItem>\n  </Form>\n</template>\n\n<script>\nexport default {\n  props: {\n    settings: {\n      type: Array\n    }\n  }\n}\n</script>\n\n"
  },
  {
    "path": "front/src/components/FileChoose/index.vue",
    "content": "<template>\n  <div class=\"file-choose\">\n    <Input class=\"file-choose-input\"\n      :value=\"value\"\n      readonly\n      disabled />\n    <Button type=\"primary\"\n      class=\"file-choose-button\"\n      :disabled=\"disabled||chooserWait\"\n      @click=\"showChooser\">{{ $t('tip.choose') }}</Button>\n  </div>\n</template>\n\n<script>\nimport { showDirChooser, showFileChooser } from '../../common/native'\n\nexport default {\n  props: {\n    value: {\n      type: String\n    },\n    mode: {\n      type: String,\n      default: 'dir'\n    },\n    disabled: {\n      type: Boolean\n    }\n  },\n  data() {\n    return {\n      chooserWait: false\n    }\n  },\n  methods: {\n    showChooser() {\n      this.chooserWait = true\n      let chooserPromise =\n        this.mode === 'dir' ? showDirChooser() : showFileChooser()\n      chooserPromise\n        .then(result => {\n          if (result) {\n            this.$emit('input', result.path)\n          }\n        })\n        .finally(() => {\n          this.chooserWait = false\n        })\n    }\n  }\n}\n</script>\n\n<style scoped>\n.file-choose {\n  display: inline-block;\n  width: 100%;\n}\n.file-choose-input {\n  width: 85%;\n  padding-right: 3px;\n}\n.file-choose-button {\n  width: 15%;\n}\n</style>\n"
  },
  {
    "path": "front/src/components/Table/index.vue",
    "content": "<template>\n  <section class=\"prye-tb\">\n    <div class=\"tb-wrapper\">\n      <div class=\"bg\"></div>\n      <div class=\"tb-head\">\n        <div class=\"tb-tr\">\n          <div class=\"ths\">\n            <div class=\"th\">\n              <Checkbox v-model=\"all\"\n                @on-change=\"setAll\"></Checkbox>\n            </div>\n            <div class=\"th\">{{ $t(\"tasks.fileName\") }}</div>\n            <div class=\"th\">{{ $t(\"tasks.fileSize\") }}</div>\n            <div class=\"th\">{{ $t(\"tasks.taskProgress\") }}</div>\n            <div class=\"th\">{{ $t(\"tasks.downloadSpeed\") }}</div>\n            <div class=\"th\">{{ $t(\"tasks.status\") }}</div>\n            <div class=\"th\">{{ $t(\"tasks.operate\") }}</div>\n          </div>\n        </div>\n      </div>\n\n      <div class=\"tb-body\"\n        :style=\"{'max-height':maxHeight+'px'}\">\n        <div class=\"tb-tr\"\n          v-for=\"task in taskList\"\n          :key=\"task.id\">\n          <div class=\"progress\"\n            :style=\"`width: ${ calcProgress(task) };`\"></div>\n          <div class=\"tds\">\n            <div class=\"td\">\n              <Checkbox v-model=\"checkedMap[task.id]\"\n                @on-change=\"toggleAll\"></Checkbox>\n            </div>\n            <div class=\"td\">{{ task.response.fileName }}</div>\n            <div class=\"td\">{{ task.response.totalSize?$numeral(task.response.totalSize).format('0.00 ib'):$t('tasks.unknowLeft') }}</div>\n            <div class=\"td\">{{ calcProgress(task) }}</div>\n            <div class=\"td\">{{ $numeral(task.info.speed).format('0.00 ib') }}/S</div>\n            <div class=\"td\">{{ calcStatus(task) }}</div>\n            <div class=\"td\">\n              <Icon v-if=\"task.info.status === 1\"\n                class=\"action-icon\"\n                type=\"ios-pause\"\n                :title=\"$t('tasks.pauseDownloads')\"\n                @click=\"$emit('on-pause', task)\"></Icon>\n              <Icon v-else-if=\"task.info.status !== 4\"\n                class=\"action-icon\"\n                type=\"ios-play\"\n                :title=\"$t('tasks.continueDownloading')\"\n                @click=\"$emit('on-resume', task)\"></Icon>\n              <Icon type=\"ios-trash\"\n                class=\"action-icon\"\n                :title=\"$t('tasks.deleteTask')\"\n                @click=\"$emit('on-delete', task)\"></Icon>\n              <Icon class=\"action-icon\"\n                type=\"ios-folder\"\n                :title=\"$t('tasks.revealInFolder')\"\n                @click=\"$emit('on-open', task)\"></Icon>\n              <Poptip placement=\"right-end\"\n                :title=\"$t('tasks.detail')\"\n                transfer\n                width=\"400\"\n                trigger=\"click\">\n                <Icon class=\"action-icon\"\n                  :title=\"$t('tasks.detail')\"\n                  type=\"ios-eye-outline\"></Icon>\n                <div class=\"file-detail\"\n                  slot=\"content\">\n                  <p>\n                    <b>{{ $t('tasks.url') }}：</b>\n                    <span>{{ task.request.url }}</span>\n                  </p>\n                  <p>\n                    <b>{{ $t('tasks.fileName') }}：</b>\n                    <span>{{ task.response.fileName }}</span>\n                  </p>\n                  <p>\n                    <b>{{ $t('tasks.filePath') }}：</b>\n                    <span>{{ task.config.filePath }}</span>\n                  </p>\n                  <p>\n                    <b>{{ $t('tasks.fileSize') }}：</b>\n                    <span>{{ $numeral(task.response.totalSize).format('0.00 ib') }}</span>\n                  </p>\n                  <p>\n                    <b>{{ $t('tasks.connections') }}：</b>\n                    <span>{{ task.config.connections }}</span>\n                  </p>\n                  <p>\n                    <b>{{ $t('tasks.downloadSpeed') }}：</b>\n                    <span>{{ $numeral(task.info.speed).format('0.00 ib') }}/S</span>\n                  </p>\n                  <p>\n                    <b>{{ $t('tasks.status') }}：</b>\n                    <span>{{ calcStatus(task) }}</span>\n                  </p>\n                  <p>\n                    <b>{{ $t(\"tasks.createTime\") }}：</b>\n                    <span>{{ new Date(task.info.startTime).format('yyyy-MM-dd hh:mm:ss') }}</span>\n                  </p>\n                </div>\n              </Poptip>\n            </div>\n          </div>\n        </div>\n      </div>\n    </div>\n  </section>\n</template>\n\n<script>\nexport default {\n  // data\n  data() {\n    return {\n      all: false,\n      checkedMap: {}\n    }\n  },\n\n  // props\n  props: {\n    taskList: {\n      type: Array,\n      required: true\n    },\n    maxHeight: {\n      type: Number\n    }\n  },\n\n  watch: {\n    taskList() {\n      if (this.taskList.length === 0) {\n        this.checkedMap = {}\n        this.all = false\n      }\n    }\n  },\n\n  // methods\n  methods: {\n    setAll(checked) {\n      if (checked) {\n        this.taskList.forEach(task => (this.checkedMap[task.id] = true))\n      } else {\n        for (let key in this.checkedMap) {\n          this.checkedMap[key] = false\n        }\n      }\n    },\n\n    toggleAll(checked) {\n      if (checked) {\n        for (let key in this.checkedMap) {\n          if (this.checkedMap[key] !== true) {\n            return\n          }\n        }\n        this.all = true\n      } else {\n        this.all = false\n      }\n    },\n\n    calcProgress(task) {\n      let progress = task.info.downSize / task.response.totalSize\n      return progress ? this.$numeral(task.info.downSize / task.response.totalSize).format('0.00%') : '0%'\n    },\n\n    calcStatus(task) {\n      switch (task.info.status) {\n        case 0:\n          return this.$t('tasks.wait')\n        case 1:\n          if (task.info.speed > 0 && task.response.totalSize > 0) {\n            return this.$numeral((task.response.totalSize - task.info.downSize) / task.info.speed).format('00:00:00')\n          } else {\n            return this.$t('tasks.unknowLeft')\n          }\n        case 2:\n          return this.$t('tasks.statusPause')\n        case 3:\n          return this.$t('tasks.statusFail')\n        case 4:\n          return this.$t('tasks.statusDone')\n      }\n    },\n\n    getCheckedTasks() {\n      return this.taskList.filter(task => {\n        for (let key in this.checkedMap) {\n          if (this.checkedMap.hasOwnProperty(key) && this.checkedMap[key] && key === task.id) {\n            return true\n          }\n        }\n        return false\n      })\n    }\n  }\n}\n</script>\n\n<style lang=\"less\" scoped>\n.prye-tb {\n  .tb-body {\n    overflow-y: auto;\n  }\n  .tb-wrapper {\n    position: relative;\n    width: 100%;\n    border: 1px solid #e9eaec;\n    border-bottom: 0 none;\n\n    .bg {\n      position: absolute;\n      background: white;\n      left: 0;\n      right: 0;\n      bottom: 0;\n      top: 0;\n      z-index: -2;\n    }\n\n    .tb-tr {\n      position: relative;\n\n      .tds,\n      .ths {\n        width: 100%;\n        display: flex;\n\n        .th,\n        .td {\n          padding: 12px 5px;\n          border-bottom: 1px solid #e9eaec;\n          box-sizing: border-box;\n          word-break: break-all;\n          display: flex;\n          align-items: center;\n\n          &:nth-child(1) {\n            width: 5%;\n\n            > label {\n              margin: 0 auto;\n            }\n          }\n          &:nth-child(2) {\n            width: 30%;\n          }\n          &:nth-child(3) {\n            width: 15%;\n          }\n          &:nth-child(4) {\n            width: 15%;\n          }\n          &:nth-child(5) {\n            width: 15%;\n          }\n          &:nth-child(6) {\n            width: 10%;\n          }\n          &:nth-child(7) {\n            width: 10%;\n          }\n        }\n\n        .th {\n          background-color: #f8f8f9;\n          text-align: left;\n        }\n      }\n\n      .tds {\n        > .td:last-child {\n          .action-icon {\n            padding: 3px 5px;\n            margin-right: 5px;\n\n            &:last-child {\n              margin-right: 0;\n            }\n          }\n        }\n      }\n\n      .progress {\n        position: absolute;\n        z-index: -1;\n        height: 100%;\n        background: rgba(87, 197, 247, 0.2);\n        transition: all 0.6s;\n      }\n    }\n  }\n}\n</style>\n\n<style>\n.file-detail p {\n  padding: 2px;\n}\n.file-detail b {\n  display: inline-block;\n  width: 60px;\n}\n</style>\n\n"
  },
  {
    "path": "front/src/components/Task/Create.vue",
    "content": "<template>\n  <Modal :title=\"$t('tasks.createTask')\"\n    :value=\"visible\"\n    @input=\"closeModal\"\n    @on-visible-change=\"init\"\n    :closable=\"false\"\n    :mask-closable=\"false\">\n    <Form v-if=\"visible\"\n      ref=\"form\"\n      :model=\"form\"\n      :rules=\"rules\"\n      :label-width=\"70\">\n      <FormItem v-if=\"sameTasks.length>0\"\n        prop=\"taskId\"\n        :label=\"$t('tasks.sameTaskList')\">\n        <Select v-model=\"form.taskId\"\n          clearable\n          @on-change=\"sameTaskChange\"\n          :placeholder=\"$t('tasks.sameTaskPlaceholder')\">\n          <Option v-for=\"task in sameTasks\"\n            :key=\"task.id\"\n            :label=\"task.config.filePath+getFileSeparator()+task.response.fileName\"\n            :value=\"task.id\">\n          </Option>\n        </Select>\n      </FormItem>\n      <template v-if=\"!selectOldTask\">\n        <FormItem :label=\"$t('tasks.fileName')\"\n          prop=\"response.fileName\">\n          <Input :disabled=\"disabledForm\"\n            v-model=\"form.response.fileName\" />\n        </FormItem>\n        <FormItem :label=\"$t('tasks.fileSize')\">{{ form.response.totalSize?$numeral(form.response.totalSize).format('0.00 ib'):$t('tasks.unknowLeft') }}</FormItem>\n        <FormItem :label=\"$t('tasks.connections')\"\n          prop=\"config.connections\">\n          <Slider v-if=\"response.supportRange\"\n            v-model=\"form.config.connections\"\n            :disabled=\"disabledForm\"\n            :min=\"2\"\n            :max=\"256\"\n            :step=\"2\"\n            show-input />\n          <Slider v-else\n            disabled\n            v-model=\"form.config.connections\"\n            :min=\"1\"\n            :max=\"1\"\n            show-input />\n        </FormItem>\n        <FormItem :label=\"$t('tasks.filePath')\"\n          prop=\"config.filePath\">\n          <FileChoose :disabled=\"disabledForm\"\n            v-model=\"form.config.filePath\" />\n        </FormItem>\n      </template>\n    </Form>\n    <div slot=\"footer\">\n      <Button type=\"primary\"\n        @click=\"onSubmit\">{{ $t('tip.ok') }}</Button>\n      <Button @click=\"closeModal\">{{ $t('tip.cancel') }}</Button>\n    </div>\n    <Spin size=\"large\"\n      fix\n      v-if=\"load\" />\n  </Modal>\n</template>\n\n<script>\nimport FileChoose from '../FileChoose'\n\nexport default {\n  props: {\n    request: {\n      type: Object\n    },\n    response: {\n      type: Object\n    },\n    config: {\n      type: Object\n    },\n    data: {\n      type: Object\n    }\n  },\n  data() {\n    return {\n      load: false,\n      selectOldTask: false,\n      disabledForm: false,\n      form: {\n        taskId: undefined,\n        request: this.request,\n        response: this.response,\n        config: this.config,\n        data: this.data\n      },\n      rules: {\n        taskId: [{ required: true, message: this.$t('tip.notNull') }],\n        'response.fileName': [{ required: true, message: this.$t('tip.notNull') }],\n        'config.filePath': [{ required: true, message: this.$t('tip.notNull') }]\n      },\n      sameTasks: []\n    }\n  },\n  watch: {\n    request() {\n      this.form.request = this.request\n      this.form.response = this.response\n      this.form.data = this.data\n      this.setDefaultConfig()\n    }\n  },\n  computed: {\n    visible() {\n      if (this.request && this.response) {\n        return true\n      } else {\n        return false\n      }\n    }\n  },\n  components: {\n    FileChoose\n  },\n  methods: {\n    closeModal() {\n      this.$emit('close')\n    },\n    onSubmit() {\n      this.$refs['form'].validate(valid => {\n        if (valid) {\n          this.load = true\n          if (this.form.taskId) {\n            //refresh download request\n            this.$http\n              .put('http://127.0.0.1:26339/tasks/' + this.form.taskId, this.form.request)\n              .then(() => {\n                this.$router.push('/')\n              })\n              .finally(() => {\n                this.load = false\n              })\n          } else {\n            //create download task\n            this.$http\n              .post('http://127.0.0.1:26339/tasks', this.form)\n              .then(() => {\n                this.$router.push('/')\n              })\n              .finally(() => {\n                this.load = false\n              })\n          }\n        }\n      })\n    },\n    async init(visible) {\n      //reset params\n      this.sameTasks = []\n      this.form.taskId = undefined\n      this.disabledForm = false\n      if (visible) {\n        //check same task\n        const { data: downTasks } = await this.$http.get('http://127.0.0.1:26339/tasks?status=1,2,3')\n        this.sameTasks = downTasks\n          ? downTasks.filter(task => task.response.supportRange && task.response.totalSize === this.response.totalSize)\n          : []\n        if (this.sameTasks.length > 0) {\n          const _this = this\n          this.$Modal.confirm({\n            title: _this.$t('tip.tip'),\n            content: _this.$t('tasks.checkSameTask'),\n            okText: _this.$t('tip.ok'),\n            cancelText: _this.$t('tip.cancel'),\n            onOk() {\n              _this.selectOldTask = true\n            },\n            onCancel() {\n              _this.sameTasks = []\n            }\n          })\n        }\n      }\n    },\n    getFileSeparator() {\n      if (window.navigator.platform.indexOf('Win') != -1) {\n        return '\\\\'\n      } else {\n        return '/'\n      }\n    },\n    sameTaskChange(taskId) {\n      const oldTask = this.sameTasks.find(task => task.id == taskId)\n      if (oldTask) {\n        this.form.config = { ...oldTask.config }\n        this.selectOldTask = false\n        this.disabledForm = true\n      } else {\n        this.selectOldTask = true\n      }\n    },\n    setDefaultConfig() {\n      this.form.config = {}\n      this.$noSpinHttp.get('http://127.0.0.1:26339/config').then(result => {\n        const serverConfig = result.data\n        this.form.config = {\n          ...{\n            filePath: serverConfig.filePath,\n            connections: serverConfig.connections,\n            timeout: serverConfig.timeout,\n            retryCount: serverConfig.retryCount,\n            autoRename: serverConfig.autoRename,\n            speedLimit: serverConfig.speedLimit\n          },\n          ...this.config\n        }\n      })\n    }\n  },\n  created() {\n    this.setDefaultConfig()\n    this.init(this.visible)\n  }\n}\n</script>"
  },
  {
    "path": "front/src/components/Task/Resolve.vue",
    "content": "<template>\n  <Modal :title=\"$t('tasks.createTask')\"\n    :value=\"value\"\n    @input=\"$emit('input', arguments[0])\"\n    :closable=\"false\"\n    :mask-closable=\"false\"\n    @on-visible-change=\"onReset\">\n    <Form ref=\"form\"\n      :rules=\"rules\"\n      :model=\"form\"\n      :label-width=\"60\">\n      <FormItem :label=\"$t('tasks.method')\"\n        prop=\"method\">\n        <Select v-model=\"form.method\"\n          style=\"width:70px;\">\n          <Option value=\"GET\">GET</Option>\n          <Option value=\"POST\">POST</Option>\n        </Select>\n      </FormItem>\n      <FormItem :label=\"$t('tasks.url')\"\n        prop=\"url\">\n        <Input v-model=\"form.url\" />\n      </FormItem>\n      <FormItem :label=\"$t('tasks.option')\">\n        <Checkbox v-model=\"hasHead\">{{ $t('tasks.head') }}</Checkbox>\n        <Checkbox v-model=\"hasBody\">{{ $t('tasks.body') }}</Checkbox>\n      </FormItem>\n      <FormItem v-show=\"hasHead\"\n        :label=\"$t('tasks.head')\"\n        prop=\"heads\">\n        <div v-for=\"(head, index) in form.heads\"\n          :key=\"index\"\n          :class=\"index === 0 ? null : 'head-margin' \">\n          <Input class=\"head-input\"\n            v-model=\"head.key\"\n            placeholder=\"key\" />\n          <Input class=\"head-input\"\n            v-model=\"head.value\"\n            placeholder=\"value\" />\n          <Icon v-if=\"index !== 0\"\n            type=\"minus-circled\"\n            @click=\"delHead(index)\"></Icon>\n          <Icon v-if=\"index === form.heads.length - 1\"\n            type=\"plus-circled\"\n            @click=\"addHead\"></Icon>\n        </div>\n      </FormItem>\n      <FormItem v-show=\"hasBody\"\n        :label=\"$t('tasks.body')\"\n        prop=\"body\">\n        <Input type=\"textarea\"\n          :autosize=\"{ minRows: 2, maxRows: 4}\"\n          v-model=\"form.body\" />\n      </FormItem>\n    </Form>\n    <div slot=\"footer\">\n      <Button type=\"primary\"\n        @click=\"onSubmit\">{{ $t('tip.ok') }}</Button>\n      <Button @click=\"$emit('input', false)\">{{ $t('tip.cancel') }}</Button>\n    </div>\n  </Modal>\n</template>\n\n<script>\nimport { onResolve } from '../../common/native.js'\n\nexport default {\n  props: {\n    value: {\n      type: Boolean\n    }\n  },\n  data() {\n    return {\n      hasHead: false,\n      hasBody: false,\n      form: {\n        method: 'GET',\n        url: '',\n        heads: [],\n        body: '',\n        dir: ''\n      },\n      rules: {\n        url: [\n          { required: true, message: this.$t('tip.notNull') },\n          { pattern: /^https?:\\/\\/.*$/i, message: this.$t('tip.fmtErr') }\n        ]\n      }\n    }\n  },\n  watch: {\n    hasHead(val) {\n      if (val && this.form.heads.length === 0) {\n        this.addHead()\n      }\n    }\n  },\n  methods: {\n    addHead() {\n      this.form.heads.push({ key: '', value: '' })\n    },\n    delHead(index) {\n      this.form.heads.splice(index, 1)\n    },\n    onSubmit() {\n      this.$refs['form'].validate(async valid => {\n        if (valid) {\n          const requestData = {\n            method: this.form.method,\n            url: this.form.url,\n            heads: {},\n            body: ''\n          }\n          if (this.hasHead) {\n            for (let head of this.form.heads) {\n              if (head.key && head.value) {\n                requestData.heads[head.key] = head.value\n              }\n            }\n          }\n          if (this.hasBody) {\n            requestData.body = this.form.body\n          }\n          this.$Spin.show()\n          try {\n            let resolveData = await onResolve(requestData)\n            if (!resolveData) {\n              const result = await this.$http.put('http://127.0.0.1:26339/util/resolve', requestData)\n              resolveData = result.data\n            }\n            this.$emit('input', false)\n            const request = JSON.stringify(resolveData.request)\n            const response = JSON.stringify(resolveData.response)\n            const config = JSON.stringify(resolveData.config)\n            const data = JSON.stringify(resolveData.data)\n            this.$router.push({\n              path: '/',\n              query: { request: request, response: response, config: config, data: data }\n            })\n          } finally {\n            this.$Spin.hide()\n          }\n        }\n      })\n    },\n    onReset(visible) {\n      if (visible) {\n        this.$refs['form'].resetFields()\n        this.hasHead = false\n        this.hasBody = false\n      }\n    }\n  }\n}\n</script>\n\n<style scoped lang=\"less\">\n.head-input {\n  width: 40%;\n  & + .head-input {\n    margin-left: 10px;\n  }\n}\n\n.ivu-icon {\n  margin-left: 10px;\n  font-size: 22px;\n  cursor: pointer;\n  position: relative;\n  top: 5px;\n}\n\n.head-margin {\n  margin-top: 10px;\n}\n</style>\n"
  },
  {
    "path": "front/src/i18n/en-US.js",
    "content": "export default {\n  nav: {\n    tasks: 'Tasks',\n    extension: 'Extensions',\n    setting: 'Settings',\n    about: 'About',\n    support: 'Support Us'\n  },\n  tip: {\n    tip: 'Hint',\n    ok: 'OK',\n    cancel: 'Cancel',\n    notNull: 'Cannot be empty',\n    fmtErr: 'Incorrect format',\n    choose: 'Choose',\n    save: 'Save',\n    refresh: 'Refresh',\n    copySucc: 'Copied successfully',\n    copyFail: 'Copy failed',\n    saveSucc: 'Save successfully',\n    saveFail: 'Save failed'\n  },\n  tasks: {\n    createTask: 'New Task',\n    continueDownloading: 'Resume',\n    pauseDownloads: 'Pause',\n    deleteTask: 'Delete Task',\n    deleteTaskTip: 'Delete both task and file',\n    revealInFolder: 'Reveal in download folder',\n    method: 'Method',\n    url: 'URL',\n    fileName: 'Name',\n    fileSize: 'Size',\n    connections: 'Connections',\n    filePath: 'Path',\n    status: 'Status',\n    operate: 'Actions',\n    downloadAddress: 'Address',\n    wait: 'Waiting',\n    unknowLeft: 'Unknown',\n    downloadSpeed: 'Speed',\n    createTime: 'Created',\n    taskProgress: 'Progress',\n    statusPause: 'Pause',\n    statusFail: 'Failed',\n    statusDone: 'Done',\n    option: 'Options',\n    head: 'Header',\n    body: 'Body',\n    detail: 'Details',\n    checkSameTask: 'The same task already exists. Refresh the task?',\n    sameTaskList: 'Task List',\n    sameTaskPlaceholder: 'Please select the task to refresh',\n    running: 'Downloading',\n    waiting: 'Waiting',\n    done: 'Done'\n  },\n  extension: {\n    conditions: 'Notes',\n    conditionsContent:\n      'When using the extension for the first time, you must install a CA certificate randomly generated by Proxyee Down. Click Install below and follow the instructions. If a Proxyee Down CA certificate has been installed, you will be prompted to delete the old CA certificate.',\n    install: 'Install',\n    globalProxy: 'Global Proxy',\n    proxyTip: 'View instructions',\n    copyPac: 'Copy PAC URL',\n    title: 'Title',\n    description: 'Description',\n    currVersion: 'Current Version',\n    newVersion: 'Latest Version',\n    installStatus: 'Status',\n    installStatusTrue: 'ON',\n    installStatusFalse: 'OFF',\n    action: 'Actions',\n    actionUpdate: 'Update',\n    actionInstall: 'Install',\n    uninstall: 'Uninstall',\n    uninstallTip: 'Do you want to uninstall this extension?',\n    actionDetail: 'Details',\n    switch: 'ON/OFF',\n    downloadingTip: 'Downloading...[servers(',\n    downloadOk: 'Downloaded successfully',\n    downloadErr: 'Download failed',\n    downloadErrTip: 'Automatically switch servers',\n    extCenter: 'Extension center',\n    installLocalExt: 'Install local extension',\n    installOk: 'Installed successfully',\n    installErr: 'Installation failed, please check the manifest.json file',\n    setting: 'Setting'\n  },\n  setting: {\n    downSetting: 'Download settings',\n    path: 'Path',\n    pathTip: 'Default download path',\n    connections: 'Connections',\n    connectionsTip: 'Default download connections',\n    taskLimit: 'Simultaneous download tasks',\n    taskSpeedLimit: 'Single task speed limit',\n    globalSpeedLimit: 'Global speed limit',\n    speedLimitTip: '0 for unlimited',\n    appSetting: 'System settings',\n    language: 'Language',\n    uiMode: 'UI mode',\n    uiModeWindows: 'Windows',\n    uiModeBrowser: 'Browser',\n    autoOpen: 'Popup at startup',\n    checkUpdate: 'Check for update',\n    checkUpdateWeek: 'Every week',\n    checkUpdateStartup: 'Every startup',\n    checkUpdateNever: 'Never',\n    secondProxy: {\n      secondProxy: 'Second proxy',\n      tip: 'Configure the second (pre-proxy) proxy server for the downloader',\n      type: 'Type',\n      host: 'Host',\n      port: 'Port',\n      user: 'Username',\n      pwd: 'password'\n    }\n  },\n  about: {\n    project: {\n      title: 'Project',\n      content:\n        'Proxyee-Down is an open source, free software based on the software\\'s high-speed download kernel and extensions to easily and quickly download the required resources.',\n      githubAddress: 'Project homepage: ',\n      official: 'Official website: ',\n      community: 'Official community: ',\n      tutorial: 'Tutorials: ',\n      feedback: 'Feedback: ',\n      currentVersion: 'Current Version: ',\n      checkUpdate: 'Check update：',\n      noNewVersion: 'Already the latest version'\n    },\n    team: {\n      title: 'Team'\n    }\n  },\n  update: {\n    checkNew: 'New version available',\n    version: 'Version',\n    changeLog: 'Changelog',\n    update: 'Update',\n    done: 'Update completed',\n    restart: 'Restart Proxyee Down?',\n    error: 'Update failed, please check the network or manually download the update package'\n  },\n  alert: {\n    refused: 'Program exception: Connection refused',\n    timeout: 'Program exception: Connection timeout',\n    error: 'Program error',\n    notFound: 'Task not found',\n    '/tasks': {\n      post: {\n        4000: 'Params parse error',\n        4001: 'Request is empty',\n        4002: 'Request URL is empty',\n        4003: 'File save path is empty',\n        4004: 'Failed to create folder',\n        4005: 'No write permission',\n        4006: 'Not enough disk space',\n        4007: 'File already exists'\n      }\n    }\n  },\n  '/util/resolve': {\n    put: {\n      4000: 'Params parse error',\n      4001: 'Request URL is empty',\n      4002: 'Response status code exception',\n      4003: 'Request timeout'\n    }\n  },\n  '/config': {\n    put: {\n      4000: 'Params parse error'\n    }\n  }\n}\n"
  },
  {
    "path": "front/src/i18n/zh-CN.js",
    "content": "export default {\n  nav: {\n    tasks: '任务管理',\n    extension: '扩展管理',\n    setting: '软件设置',\n    about: '关于项目',\n    support: '支持我们'\n  },\n  tip: {\n    tip: '提示',\n    ok: '确定',\n    cancel: '取消',\n    notNull: '不能为空',\n    fmtErr: '格式不正确',\n    choose: '选择',\n    save: '保存',\n    refresh: '刷新',\n    copySucc: '复制成功',\n    copyFail: '复制失败',\n    saveSucc: '保存成功',\n    saveFail: '保存失败'\n  },\n  tasks: {\n    createTask: '创建任务',\n    continueDownloading: '继续下载',\n    pauseDownloads: '暂停下载',\n    deleteTask: '删除任务',\n    deleteTaskTip: '是否删除任务和文件？',\n    revealInFolder: '打开下载目录',\n    method: '方法',\n    url: '链接',\n    fileName: '文件名',\n    fileSize: '大小',\n    connections: '连接数',\n    filePath: '路径',\n    status: '状态',\n    operate: '操作',\n    downloadAddress: '下载地址',\n    downloadSpeed: '下载速度',\n    createTime: '开始时间',\n    taskProgress: '任务进度',\n    wait: '待下载',\n    unknowLeft: '未知',\n    statusPause: '暂停',\n    statusFail: '失败',\n    statusDone: '完成',\n    option: '附加',\n    head: '请求头',\n    body: '请求体',\n    detail: '下载详情',\n    checkSameTask: '检测到可能相同的下载任务，是否选择任务进行刷新？',\n    sameTaskList: '任务列表',\n    sameTaskPlaceholder: '请选择要刷新的任务',\n    running: '进行中',\n    waiting: '等待中',\n    done: '已完成'\n  },\n  extension: {\n    conditions: '使用须知',\n    conditionsContent:\n      '首次使用扩展模块时，必须安装由Proxyee Down随机生成的一个CA证书，点击下面的安装按钮并按系统的引导进行确认安装。(注意：程序会在安装前检测操作系统中是否有安装过证书，当检测到有安装的情况会提示删除对应的旧CA证书)',\n    install: '安装',\n    globalProxy: '全局代理',\n    proxyTip: '点击查看说明',\n    copyPac: '复制PAC链接',\n    title: '名称',\n    description: '描述',\n    currVersion: '当前版本',\n    newVersion: '最新版本',\n    installStatus: '状态',\n    installStatusTrue: '已安装',\n    installStatusFalse: '未安装',\n    action: '操作',\n    actionUpdate: '更新',\n    actionInstall: '安装',\n    uninstall: '卸载',\n    uninstallTip: '确定卸载此扩展吗？',\n    actionDetail: '详情',\n    switch: '开关',\n    downloadingTip: '下载中...[服务器(',\n    downloadOk: '下载成功',\n    downloadErr: '下载失败',\n    downloadErrTip: '自动切换服务器',\n    extCenter: '扩展中心',\n    installLocalExt: '加载本地扩展',\n    installOk: '加载成功',\n    installErr: '加载失败，请检查manifest.json文件',\n    setting: '设置'\n  },\n  setting: {\n    downSetting: '下载设置',\n    path: '路径',\n    pathTip: '默认下载路径',\n    connections: '连接数',\n    connectionsTip: '默认连接数',\n    taskLimit: '同时下载任务数',\n    taskSpeedLimit: '单任务限速',\n    globalSpeedLimit: '全局限速',\n    speedLimitTip: '0为不限速',\n    appSetting: '系统设置',\n    language: '语言',\n    uiMode: 'UI模式',\n    uiModeWindows: '窗口',\n    uiModeBrowser: '浏览器',\n    autoOpen: '启动弹窗',\n    checkUpdate: '检查更新',\n    checkUpdateWeek: '每周',\n    checkUpdateStartup: '每次启动',\n    checkUpdateNever: '从不',\n    secondProxy: {\n      secondProxy: '二级代理',\n      tip: '配置下载器的二级(前置)代理服务器',\n      type: '类型',\n      host: '服务器',\n      port: '端口',\n      user: '用户名',\n      pwd: '密码'\n    }\n  },\n  about: {\n    project: {\n      title: '项目',\n      content: 'Proxyee Down是一款开源的免费软件，基于本软件的高速下载内核和扩展，可以方便并快速的下载所需资源。',\n      githubAddress: '项目主页：',\n      official: '官方网站：',\n      community: '官方社区：',\n      tutorial: '使用教程：',\n      feedback: '问题反馈：',\n      currentVersion: '当前版本：',\n      checkUpdate: '检查更新：',\n      noNewVersion: '已经是最新版本'\n    },\n    team: {\n      title: '团队'\n    }\n  },\n  update: {\n    checkNew: '检测到新版本',\n    version: '版本号',\n    changeLog: '更新内容',\n    update: '更新',\n    done: '更新完毕',\n    restart: '是否重新启动？',\n    error: '更新失败，请检查网络或手动下载更新包'\n  },\n  alert: {\n    refused: '程序异常，拒绝访问',\n    timeout: '程序异常，连接超时',\n    error: '程序出错',\n    notFound: '任务不存在',\n    '/tasks': {\n      post: {\n        4000: '参数解析错误',\n        4001: '请求对象不能为空',\n        4002: '请求地址不能为空',\n        4003: '文件保存路径不能为空',\n        4004: '创建文件夹失败',\n        4005: '无写入权限',\n        4006: '磁盘空间不足',\n        4007: '文件已存在'\n      }\n    },\n    '/util/resolve': {\n      put: {\n        4000: '参数解析错误',\n        4001: '请求地址不能为空',\n        4002: '响应状态码异常',\n        4003: '请求超时'\n      }\n    },\n    '/config': {\n      put: {\n        4000: '参数解析错误'\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "front/src/i18n/zh-TW.js",
    "content": "export default {\n  nav: {\n    tasks: '任務管理',\n    extension: '擴充管理',\n    setting: '軟體設定',\n    about: '關於專案',\n    support: '支持我們'\n  },\n  tip: {\n    tip: '提示',\n    ok: '確定',\n    cancel: '取消',\n    notNull: '不能為空',\n    fmtErr: '格式不正確',\n    choose: '選擇',\n    save: '儲存',\n    refresh: '刷新',\n    copySucc: '複製成功',\n    copyFail: '複製失敗',\n    saveSucc: '保存成功',\n    saveFail: '保存失敗'\n  },\n  tasks: {\n    createTask: '建立任務',\n    continueDownloading: '繼續下載',\n    pauseDownloads: '暫停下載',\n    deleteTask: '刪除任務',\n    deleteTaskTip: '是否刪除任務和檔案？',\n    revealInFolder: '打開下載目錄',\n    method: '方法',\n    url: '連結',\n    fileName: '名稱',\n    fileSize: '大小',\n    connections: '連線數',\n    filePath: '路徑',\n    status: '狀態',\n    operate: '操作',\n    downloadAddress: '下載位址',\n    downloadSpeed: '下載速度',\n    createTime: '開始時間',\n    taskProgress: '任務進度',\n    wait: '待下載',\n    unknowLeft: '不詳',\n    statusPause: '暫停',\n    statusFail: '失敗',\n    statusDone: '完成',\n    option: '附加',\n    head: '要求標頭',\n    body: '要求主體',\n    detail: '下載細節',\n    checkSameTask: '偵測到可能相同的下載任務，是否選擇任務進行更新？',\n    sameTaskList: '任務清單',\n    sameTaskPlaceholder: '請選擇要更新的任務',\n    running: '進行中',\n    waiting: '等待中',\n    done: '已完成'\n  },\n  extension: {\n    conditions: '使用須知',\n    conditionsContent:\n      '首次使用擴充模組時，必須安裝由 Proxyee Down 隨機產生的一個 CA 憑證，點選下方的安裝按鈕並依系統的引導進行確認安裝。(注意：程式會在安裝前偵測作業系統中是否有安裝過憑證，當偵測到有安裝的情況會提示刪除對應的舊 CA 憑證)',\n    install: '安裝',\n    globalProxy: '全域代理',\n    proxyTip: '點選檢視說明',\n    copyPac: '複製 PAC 連結',\n    title: '名稱',\n    description: '描述',\n    currVersion: '目前版本',\n    newVersion: '最新版本',\n    installStatus: '狀態',\n    installStatusTrue: '已安装',\n    installStatusFalse: '未安装',\n    action: '操作',\n    actionUpdate: '更新',\n    actionInstall: '安裝',\n    actionDetail: '細節',\n    uninstall: '卸載',\n    uninstallTip: '確定卸載此擴展嗎？',\n    switch: '開關',\n    downloadingTip: '下載中...[伺服器(',\n    downloadOk: '下載成功',\n    downloadErr: '下載失敗',\n    downloadErrTip: '自動切換伺服器',\n    extCenter: '擴充中心',\n    installLocalExt: '加載本地擴充',\n    installOk: '加載成功',\n    installErr: '加載失敗，請檢查manifest.json文件',\n    setting: '預設'\n  },\n  setting: {\n    downSetting: '下載設定',\n    path: '路徑',\n    pathTip: '預設下載路徑',\n    connections: '連線數',\n    connectionsTip: '預設連線數',\n    taskLimit: '同時下載任務數',\n    taskSpeedLimit: '單任務限速',\n    globalSpeedLimit: '全域限速',\n    speedLimitTip: '0為不限速',\n    appSetting: '系統設定',\n    language: '語言',\n    uiMode: 'UI 模式',\n    uiModeWindows: '視窗',\n    uiModeBrowser: '瀏覽器',\n    autoOpen: '啟動彈窗',\n    checkUpdate: '檢查更新',\n    checkUpdateWeek: '每週',\n    checkUpdateStartup: '每次啟動',\n    checkUpdateNever: '從不',\n    secondProxy: {\n      secondProxy: '二級代理',\n      tip: '配置下載器的二級（前置）代理服務器',\n      type: '類型',\n      host: '服務器',\n      port: '端口',\n      user: '用戶名',\n      pwd: '密碼'\n    }\n  },\n  about: {\n    project: {\n      title: '項目',\n      content: 'Proxyee Down 是一款開源的免費軟體，基於本軟體的高速下載核心和擴充套件，可以方便並快速的下載所需資源。',\n      githubAddress: '項目首頁：',\n      official: '官方網站：',\n      community: '官方社區：',\n      tutorial: '使用教學：',\n      feedback: '問題回報：',\n      currentVersion: '目前版本：',\n      checkUpdate: '檢查更新：',\n      noNewVersion: '已經是最新版本'\n    },\n    team: {\n      title: '團隊'\n    }\n  },\n  update: {\n    checkNew: '偵測到新版本',\n    version: '版本號',\n    changeLog: '更新內容',\n    update: '更新',\n    done: '更新完畢',\n    restart: '是否重新啟動？',\n    error: '更新失敗，請檢查網絡或手動下載更新包'\n  },\n  alert: {\n    refused: '程式異常，拒絕存取',\n    timeout: '程式異常，連線逾時',\n    error: '程式出錯',\n    notFound: '任務不存在',\n    '/tasks': {\n      post: {\n        4000: '參數解析錯誤',\n        4001: '要求對象不能為空',\n        4002: '要求位址不能為空',\n        4003: '檔案儲存路徑不能為空',\n        4004: '建立資料夾失敗',\n        4005: '無寫入權限',\n        4006: '磁碟空間不足',\n        4007: '檔案已存在'\n      }\n    },\n    '/util/resolve': {\n      put: {\n        4000: '參數解析錯誤',\n        4001: '要求位址不能為空',\n        4002: '回應狀態碼異常',\n        4003: '請求超時'\n      }\n    },\n    '/config': {\n      put: {\n        4000: '參數解析錯誤'\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "front/src/main.js",
    "content": "import Vue from 'vue'\r\nimport App from './App.vue'\r\nimport router from './router'\r\nimport store from './store'\r\nimport iView from 'iview'\r\nimport axios from 'axios'\r\nimport VueI18n from 'vue-i18n'\r\nimport numeral from 'numeral'\r\n\r\nimport 'iview/dist/styles/iview.css'\r\nimport en_US from 'iview/dist/locale/en-US'\r\nimport zh_CN from 'iview/dist/locale/zh-CN'\r\nimport zh_TW from 'iview/dist/locale/zh-TW'\r\nimport http from './common/http'\r\nimport { getInitConfig } from './common/native'\r\n\r\nVue.use(VueI18n)\r\nVue.use(iView)\r\n\r\nVue.config.productionTip = false\r\n\r\n// Setting i18n\r\nconst i18n = new VueI18n({\r\n  locale: 'zh-CN',\r\n  messages: {\r\n    'en-US': Object.assign(require('./i18n/en-US').default, en_US),\r\n    'zh-CN': Object.assign(require('./i18n/zh-CN').default, zh_CN),\r\n    'zh-TW': Object.assign(require('./i18n/zh-TW').default, zh_TW)\r\n  }\r\n})\r\n\r\nVue.prototype.$noSpinHttp = axios.create()\r\nVue.prototype.$http = http.build()\r\nVue.prototype.$http.interceptors.response.use(\r\n  response => {\r\n    return response\r\n  },\r\n  error => {\r\n    if (!error.response) {\r\n      Vue.prototype.$Message.error(i18n.t('alert.refused'))\r\n    } else if (error.response.status == 400) {\r\n      let i18nKey =\r\n        'alert[\"' +\r\n        new URL(error.config.url).pathname +\r\n        '\"]' +\r\n        '.' +\r\n        error.config.method +\r\n        '.' +\r\n        error.response.data.code\r\n      Vue.prototype.$Message.error(i18n.t(i18nKey))\r\n    } else if (error.response.status == 404) {\r\n      Vue.prototype.$Message.error(i18n.t('alert.notFound'))\r\n    } else if (error.response.status == 504) {\r\n      Vue.prototype.$Message.error(i18n.t('alert.timeout'))\r\n    } else {\r\n      Vue.prototype.$Message.error(i18n.t('alert.error'))\r\n    }\r\n    return Promise.reject(error)\r\n  }\r\n)\r\n\r\n//去除字节大小格式化后的i字符\r\nconst format = numeral.prototype.constructor.fn.format\r\nnumeral.prototype.constructor.fn.format = function(fmt) {\r\n  let result = format.call(this, fmt)\r\n  if (/^.*ib$/.test(fmt)) {\r\n    result = result.replace('i', '')\r\n  }\r\n  return result\r\n}\r\nVue.prototype.$numeral = numeral\r\n\r\nDate.prototype.format = function(fmt) {\r\n  var o = {\r\n    'M+': this.getMonth() + 1, // Month\r\n    'd+': this.getDate(), // Day\r\n    'h+': this.getHours(), // Hour\r\n    'm+': this.getMinutes(), // Minute\r\n    's+': this.getSeconds(), // Second\r\n    'q+': Math.floor((this.getMonth() + 3) / 3), // Quarter\r\n    S: this.getMilliseconds() // Millisecond\r\n  }\r\n  if (/(y+)/.test(fmt)) {\r\n    fmt = fmt.replace(RegExp.$1, (this.getFullYear() + '').substr(4 - RegExp.$1.length))\r\n  }\r\n  for (var k in o) {\r\n    if (new RegExp('(' + k + ')').test(fmt)) {\r\n      fmt = fmt.replace(RegExp.$1, RegExp.$1.length === 1 ? o[k] : ('00' + o[k]).substr(('' + o[k]).length))\r\n    }\r\n  }\r\n  return fmt\r\n}\r\n\r\nPromise.prototype.finally = function(callback) {\r\n  let P = this.constructor\r\n  return this.then(\r\n    value => P.resolve(callback()).then(() => value),\r\n    reason =>\r\n      P.resolve(callback()).then(() => {\r\n        throw reason\r\n      })\r\n  )\r\n}\r\n\r\n// Change the page according to the routing changes title\r\nrouter.beforeEach((to, from, next) => {\r\n  if (to.meta.title) {\r\n    document.title = `Proxyee Down-${to.meta.title}`\r\n  }\r\n  next()\r\n})\r\n\r\n// Get client configuration information\r\ngetInitConfig()\r\n  .then(result => {\r\n    Vue.prototype.$config = result\r\n    // Set default language\r\n    i18n.locale = result.locale\r\n  })\r\n  .catch(() => {\r\n    Vue.prototype.$config = {}\r\n  })\r\n  .finally(() => {\r\n    new Vue({\r\n      router,\r\n      store,\r\n      i18n,\r\n      data() {\r\n        return {\r\n          badges: { tasks: 0, extension: 0, setting: 0, about: 0, support: 0 }\r\n        }\r\n      },\r\n      render: h => h(App)\r\n    }).$mount('#app')\r\n  })\r\n"
  },
  {
    "path": "front/src/router.js",
    "content": "import Vue from 'vue'\r\nimport Router from 'vue-router'\r\nimport Tasks from './views/Tasks.vue'\r\nimport Extension from './views/Extension.vue'\r\nimport Setting from './views/Setting.vue'\r\nimport About from './views/About.vue'\r\nimport Support from './views/Support.vue'\r\n\r\nVue.use(Router)\r\n\r\nexport default new Router({\r\n  routes: [\r\n    {\r\n      path: '/',\r\n      redirect: '/tasks'\r\n    },\r\n    {\r\n      path: '/tasks',\r\n      name: 'tasks',\r\n      component: Tasks\r\n    },\r\n    {\r\n      path: '/extension',\r\n      name: 'extension',\r\n      component: Extension\r\n    },\r\n    {\r\n      path: '/setting',\r\n      name: 'setting',\r\n      component: Setting\r\n    },\r\n    {\r\n      path: '/about',\r\n      name: 'About',\r\n      component: About\r\n    },\r\n    {\r\n      path: '/support',\r\n      name: 'Support',\r\n      component: Support\r\n    }\r\n  ]\r\n})\r\n"
  },
  {
    "path": "front/src/store.js",
    "content": "import Vue from 'vue'\r\nimport Vuex from 'vuex'\r\n\r\nVue.use(Vuex)\r\n\r\nexport default new Vuex.Store({\r\n  state: {},\r\n  mutations: {},\r\n  actions: {}\r\n})\r\n"
  },
  {
    "path": "front/src/views/About.vue",
    "content": "<template>\n  <div class=\"v-about\">\n    <Card>\n      <p slot=\"title\">{{ $t(\"about.project.title\") }}</p>\n      <ul class=\"project-ul\">\n        <li>\n          <p>{{ $t(\"about.project.content\") }}</p>\n        </li>\n        <li>\n          <b>{{ $t(\"about.project.githubAddress\") }}</b>\n          <a href=\"javascript:void(0)\"\n            @click=\"openUrl('https://github.com/proxyee-down-org/proxyee-down')\">\n            GitHub@proxyee-down\n          </a>\n        </li>\n        <li>\n          <b>{{ $t(\"about.project.official\") }}</b>\n          <a href=\"javascript:void(0)\"\n            @click=\"openUrl('https://pdown.org')\">\n            pdown.org\n          </a>\n        </li>\n        <li>\n          <b>{{ $t(\"about.project.community\") }}</b>\n          <a href=\"javascript:void(0)\"\n            @click=\"openUrl('https://community.pdown.org')\">\n            community.pdown.org\n          </a>\n        </li>\n        <li>\n          <b>{{ $t(\"about.project.tutorial\") }}</b>\n          <a href=\"javascript:void(0)\"\n            @click=\"openUrl('https://github.com/proxyee-down-org/proxyee-down/wiki/%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B')\">\n            GitHub@proxyee-down/wiki\n          </a>\n        </li>\n        <li>\n          <b>{{ $t(\"about.project.feedback\") }}</b>\n          <a href=\"javascript:void(0)\"\n            @click=\"openUrl('https://github.com/proxyee-down-org/proxyee-down/issues')\">\n            GitHub@proxyee-down/issues\n          </a>\n        </li>\n        <li>\n          <b>{{ $t(\"about.project.currentVersion\") }}</b>\n          <span> {{ $config.version }}</span>\n        </li>\n        <li>\n          <b>{{ $t(\"about.project.checkUpdate\") }}</b>\n          <Icon type=\"ios-refresh-empty\"\n            @click=\"checkUpdate()\"></Icon>\n        </li>\n      </ul>\n    </Card>\n\n    <br>\n\n    <Card class=\"team\">\n      <p slot=\"title\">{{ $t(\"about.team.title\") }}</p>\n      <Card class=\"item\">\n        <div>\n          <img src=\"team_header/monkeyWie.png\"\n            @click=\"openUrl('https://github.com/monkeyWie')\">\n          <b>monkeyWie</b>\n        </div>\n      </Card>\n      <Card class=\"item\">\n        <div>\n          <img src=\"team_header/Black-Hole.png\"\n            @click=\"openUrl('https://github.com/BlackHole1')\">\n          <b>Black-Hole</b>\n        </div>\n      </Card>\n      <Card class=\"item\">\n        <div>\n          <img src=\"team_header/NISAL.png\"\n            @click=\"openUrl('https://github.com/hiNISAL')\">\n          <b>NISAL</b>\n        </div>\n      </Card>\n      <br>\n    </Card>\n\n    <Modal v-model=\"hasUpdate\"\n      :title=\"$t('update.checkNew')\">\n      <b>{{ $t('update.version') }}：</b>\n      <span>{{ versionInfo.version }}</span>\n      <br>\n      <br>\n      <b>{{ $t('update.changeLog') }}：</b>\n      <div style=\"padding-top:10px;\"\n        v-html=\"versionInfo.description\"></div>\n      <span slot=\"footer\">\n        <Button @click=\"hasUpdate = false\">{{ $t('tip.cancel') }}</Button>\n        <Button type=\"primary\"\n          @click=\"doUpdate()\">{{ $t('update.update') }}</Button>\n      </span>\n    </Modal>\n\n    <Modal v-model=\"restatModel\"\n      :title=\"$t('update.done')\">\n      <h3>{{ $t('update.restart') }}</h3>\n      <span slot=\"footer\">\n        <Button @click=\"restatModel = false\">{{ $t('tip.cancel') }}</Button>\n        <Button type=\"primary\"\n          @click=\"doRestart()\">{{ $t('tip.ok') }}</Button>\n      </span>\n    </Modal>\n\n    <Spin v-if=\"showUpdateProgress\"\n      size=\"large\"\n      class=\"update-progress\"\n      fix>\n      <Circle :percent=\"updateInfo.progress\"\n        :size=\"150\">\n        <h1>{{ updateInfo.progress.toFixed(2) }}%</h1>\n        <p>{{ $numeral(updateInfo.speed).format('0.00 ib') }}/S</p>\n      </Circle>\n    </Spin>\n\n  </div>\n</template>\n<script>\nimport { openUrl, doUpdate, getUpdateProgress, doRestart } from '../common/native.js'\n\nexport default {\n  name: 'about',\n  data() {\n    return {\n      hasUpdate: false,\n      showUpdateProgress: false,\n      versionInfo: {},\n      restatModel: false,\n      updateInfo: {\n        progress: 0,\n        speed: 0\n      }\n    }\n  },\n  watch: {\n    $route() {\n      this.onRouteChange()\n    }\n  },\n  methods: {\n    openUrl(url) {\n      openUrl(url)\n    },\n    onRouteChange() {\n      if (this.$route.query.checkUpdate) {\n        this.checkUpdate(this.$route.query.versionInfo)\n      }\n    },\n    checkUpdate(versionInfoQuery) {\n      if (versionInfoQuery) {\n        const versionInfo = JSON.parse(versionInfoQuery)\n        if (versionInfo.version > this.$config.version) {\n          this.hasUpdate = true\n          this.versionInfo = versionInfo\n        }\n      } else {\n        this.$http.get(this.$config.adminServer + 'version/checkUpdate').then(result => {\n          const versionInfo = result.data\n          if (versionInfo && versionInfo.version > this.$config.version) {\n            this.hasUpdate = true\n            this.versionInfo = versionInfo\n          } else {\n            this.$Message.warning(this.$t('about.project.noNewVersion'))\n          }\n        })\n      }\n    },\n    doUpdate() {\n      this.showUpdateProgress = true\n      this.hasUpdate = false\n      //开始下载更新包\n      doUpdate(this.versionInfo.path)\n        .then(() => {\n          //获取更新进度\n          const updateProgressInterval = setInterval(() => {\n            getUpdateProgress().then(result => {\n              this.updateInfo.progress = (result.downSize / result.totalSize) * 100\n              this.updateInfo.speed = result.speed\n              if (result.status == 3) {\n                //下载失败\n                this.$Message.error({\n                  content: this.$t('update.error'),\n                  duration: 0\n                })\n              } else if (result.status == 4) {\n                //下载完成\n                this.restatModel = true\n              }\n              if (result.status == 3 || result.status == 4) {\n                this.showUpdateProgress = false\n                clearInterval(updateProgressInterval)\n              }\n            })\n          }, 1000)\n        })\n        .catch(() => {\n          this.showUpdateProgress = false\n          this.$Message.error({\n            content: this.$t('update.error'),\n            duration: 0\n          })\n        })\n    },\n\n    doRestart() {\n      doRestart().then((this.restatModel = false))\n    }\n  },\n  created() {\n    this.onRouteChange()\n  }\n}\n</script>\n<style lang=\"less\" scoped>\n.v-about {\n  .team {\n    .item {\n      display: inline-table;\n      margin-right: 8px;\n      width: 10rem;\n      height: 10rem;\n      div {\n        text-align: center;\n        img {\n          width: 6.25rem;\n          border-radius: 50px;\n          cursor: pointer;\n        }\n        b,\n        span {\n          display: block;\n        }\n        b {\n          margin-bottom: 5px;\n        }\n      }\n    }\n  }\n  .project-ul {\n    list-style: none;\n    li:not(:first-child) {\n      padding-top: 5px;\n    }\n    .ivu-icon {\n      cursor: pointer;\n      font-size: 1.5em;\n      position: relative;\n      top: 4px;\n      left: 4px;\n    }\n  }\n}\n.update-progress {\n  z-index: 1001;\n  h1 {\n    color: #3f414d;\n    font-size: 28px;\n    font-weight: normal;\n  }\n  p {\n    color: #657180;\n    font-size: 14px;\n    padding-top: 10px;\n  }\n}\n</style>\n\n"
  },
  {
    "path": "front/src/views/Extension.vue",
    "content": "<template>\n  <div v-if=\"!certStatus\">\n    <Card shadow>\n      <p slot=\"title\">{{ $t('extension.conditions') }}</p>\n      <p>{{ $t('extension.conditionsContent') }}</p>\n    </Card>\n    <Button type=\"primary\"\n      class=\"install-button\"\n      @click=\"installCert\">{{ $t('extension.install') }}</Button>\n  </div>\n  <div v-else>\n    <div class=\"proxy-switch-div\">\n      <b>{{ $t('extension.globalProxy') }}</b>\n      <Switch v-model=\"proxySwitch\"\n        @on-change=\"changeProxyMode\">\n      </Switch>\n\n      <Tooltip :content=\"$t('extension.proxyTip')\"\n        theme=\"light\">\n        <Icon type=\"help-circled\"\n          @click=\"openUrl('https://github.com/proxyee-down-org/proxyee-down/wiki/%E5%AE%89%E8%A3%85%E6%89%A9%E5%B1%95')\"\n          class=\"action-icon tip-icon\" />\n      </Tooltip>\n\n      <Tooltip class=\"icon-button\"\n        :content=\"$t('tip.refresh')\"\n        theme=\"light\">\n        <Button type=\"info\"\n          shape=\"circle\"\n          icon=\"loop\"\n          @click=\"loadExtensions\">\n        </Button>\n      </Tooltip>\n\n      <Tooltip class=\"icon-button\"\n        :content=\"$t('extension.copyPac')\"\n        theme=\"light\">\n        <Button type=\"info\"\n          shape=\"circle\"\n          icon=\"ios-copy\"\n          @click=\"copyPac\">\n        </Button>\n      </Tooltip>\n\n      <Tooltip class=\"icon-button\"\n        :content=\"$t('extension.installLocalExt')\"\n        theme=\"light\">\n        <Button type=\"info\"\n          shape=\"circle\"\n          icon=\"android-folder-open\"\n          @click=\"installLocalExt\">\n        </Button>\n      </Tooltip>\n\n    </div>\n    <Tabs type=\"card\"\n      :animated=\"false\"\n      v-model=\"activeTab\">\n      <TabPane :label=\"$t('extension.extCenter')+'('+onlinePage.totalCount+')'\"\n        name=\"online\"\n        icon=\"android-playstore\">\n        <Table :columns=\"onlineColumns\"\n          :data=\"onlinePage.data\"\n          :loading=\"onlineLoading\"></Table>\n        <div style=\"margin: 10px;overflow: hidden\">\n          <div style=\"float: right;\">\n            <Page :total=\"onlinePage.totalCount\"\n              :current=\"onlinePage.pageNum\"\n              :page-size=\"onlinePage.pageSize\"\n              @on-change=\"searchExtensions(arguments[0])\"></Page>\n          </div>\n        </div>\n      </TabPane>\n      <TabPane :label=\"$t('extension.installStatusTrue')+'('+localAllList.length+')'\"\n        name=\"local\"\n        icon=\"social-buffer\">\n        <Table :columns=\"localColumns\"\n          :data=\"localAllList\"></Table>\n        <Modal v-model=\"settingModal\"\n          :title=\"$t('extension.setting')\">\n          <ExtensionSetting :settings=\"settings\" />\n          <span slot=\"footer\">\n            <Button @click=\"settingModal = false\">{{ $t('tip.cancel') }}</Button>\n            <Button type=\"primary\"\n              @click=\"saveSetting()\">{{ $t('tip.ok') }}</Button>\n          </span>\n        </Modal>\n      </TabPane>\n    </Tabs>\n    <Spin fix\n      v-if=\"spinShow\">\n      <Icon type=\"load-c\"\n        class=\"spin-icon-load\"></Icon>\n      <div>{{ spinTip }}</div>\n    </Spin>\n  </div>\n</template>\n<script>\nimport { Icon, Tag } from 'iview'\nimport ExtensionSetting from '../components/ExtensionSetting.vue'\nimport {\n  checkCert,\n  installCert,\n  getProxyMode,\n  changeProxyMode,\n  getExtensions,\n  installExtension,\n  updateExtension,\n  installLocalExtension,\n  uninstallExtension,\n  toggleExtension,\n  openUrl,\n  copy,\n  showDirChooser,\n  updateExtensionSetting\n} from '../common/native.js'\n\nexport default {\n  name: 'extension',\n  components: {\n    ExtensionSetting\n  },\n  data() {\n    return {\n      certStatus: false,\n      proxySwitch: false,\n      activeTab: 'online',\n      onlineLoading: false,\n      onlinePage: {\n        pageNum: 1,\n        pageSize: 10,\n        totalPage: 0,\n        totalCount: 0,\n        data: []\n      },\n      localAllList: [],\n      spinShow: false,\n      spinTip: '',\n      onlineColumns: this.buildCommonColumns(),\n      localColumns: this.buildCommonColumns(true),\n      settingModal: false,\n      settingExt: null,\n      settings: []\n    }\n  },\n  methods: {\n    installCert() {\n      installCert().then(status => {\n        this.certStatus = status\n        if (status) {\n          // Install Success\n          changeProxyMode(1).then(() => (this.proxySwitch = true))\n          this.loadExtensions()\n        }\n      })\n    },\n\n    changeProxyMode(val) {\n      changeProxyMode(val ? 1 : 0)\n    },\n\n    buildCommonColumns(local) {\n      const _this = this\n      return [\n        {\n          title: this.$t('extension.title'),\n          key: 'title'\n        },\n        {\n          title: this.$t('extension.description'),\n          key: 'description'\n        },\n        {\n          title: this.$t('extension.currVersion'),\n          key: 'currVersion',\n          width: 100\n        },\n        ...(local\n          ? []\n          : [\n              {\n                title: this.$t('extension.newVersion'),\n                key: 'version',\n                width: 100\n              }\n            ]),\n        {\n          title: this.$t('extension.installStatus'),\n          key: 'meta.disabled',\n          align: 'center',\n          width: 100,\n          render(h, params) {\n            return (\n              <div>\n                {params.row.installed ? (\n                  <Tag color=\"green\">{_this.$t('extension.installStatusTrue')}</Tag>\n                ) : (\n                  <Tag>{_this.$t('extension.installStatusFalse')}</Tag>\n                )}\n              </div>\n            )\n          }\n        },\n        {\n          title: this.$t('extension.action'),\n          key: 'action',\n          align: 'center',\n          width: 150,\n          render(h, params) {\n            return [\n              ...(params.row.installed\n                ? [\n                    ...(params.row.currVersion < params.row.version\n                      ? [\n                          <Icon\n                            type=\"ios-cloud-upload-outline\"\n                            class=\"action-icon\"\n                            title={_this.$t('extension.actionUpdate')}\n                            nativeOnClick={() => _this.downExtension(true, params.row, 0)}\n                          />\n                        ]\n                      : []),\n                    <Icon\n                      type=\"trash-a\"\n                      class=\"action-icon\"\n                      title={_this.$t('extension.uninstall')}\n                      nativeOnClick={() => _this.uninstallExtension(params.row)}\n                    />,\n                    ...(params.row.settings && params.row.settings.length\n                      ? [\n                          <Icon\n                            type=\"android-settings\"\n                            class=\"action-icon\"\n                            title={_this.$t('extension.setting')}\n                            nativeOnClick={() => {\n                              _this.settingModal = true\n                              _this.settingExt = params.row\n                              _this.settings = params.row.settings\n                              const settingValues = params.row.meta.settings\n                              if (settingValues) {\n                                _this.settings.forEach(s => (s.value = settingValues[s.name] || s.value))\n                              }\n                            }}\n                          />\n                        ]\n                      : [])\n                  ]\n                : [\n                    <Icon\n                      type=\"ios-cloud-download-outline\"\n                      class=\"action-icon\"\n                      title={_this.$t('extension.actionInstall')}\n                      nativeOnClick={() => _this.downExtension(false, params.row, 0)}\n                    />\n                  ]),\n              <Icon\n                type=\"ios-home\"\n                class=\"action-icon\"\n                title={_this.$t('extension.actionDetail')}\n                nativeOnClick={() => {\n                  _this.openHomepage(params.row)\n                }}\n              />\n            ]\n          }\n        },\n        {\n          title: this.$t('extension.switch'),\n          key: 'switch',\n          align: 'center',\n          width: 100,\n          render(h, params) {\n            return (\n              <Switch\n                disabled={!params.row.installed}\n                v-model={params.row.meta.enabled}\n                onOn-change={enabled => _this.changeEnabled(enabled, params.row)}\n              />\n            )\n          }\n        }\n      ]\n    },\n\n    changeEnabled(enabled, row) {\n      toggleExtension({ path: row.meta.path, enabled: enabled, local: row.meta.local }).then(() => {\n        const localExt = this.localAllList.find(localExt => localExt.meta.path == row.meta.path)\n        localExt.meta.enabled = enabled\n        this.refreshExtensions()\n      })\n    },\n\n    downExtension(isUpdate, row, index) {\n      let _this = this\n      let extFileServers = this.$config.extFileServers\n      if (index < extFileServers.length) {\n        this.spinShow = true\n        let extFileServer = extFileServers[index]\n        let url = new URL(extFileServer)\n        this.spinTip =\n          this.$t('extension.downloadingTip') + (index + 1) + '/' + extFileServers.length + ')：' + url.host + ']'\n        const params = {\n          server: extFileServer,\n          path: row.meta.path,\n          files: row.files\n        }\n        let downPromise = isUpdate ? updateExtension(params) : installExtension(params)\n        downPromise\n          .then(() => {\n            const localExt = this.localAllList.find(localExt => localExt.meta.path == row.meta.path)\n            if (localExt) {\n              localExt.version = row.version\n              localExt.currVersion = row.version\n              localExt.title = row.title\n              localExt.description = row.description\n            } else {\n              row.installed = true\n              row.currVersion = row.version\n              row.meta = { path: row.meta.path, enabled: true }\n              this.localAllList.push(row)\n            }\n            this.getLocalExtensions(() => {\n              this.refreshExtensions()\n            })\n            this.spinShow = false\n            this.$Message.success({\n              content: this.$t('extension.downloadOk'),\n              closable: true\n            })\n            const afterBadges = this.$root.badges.extension - 1\n            this.$root.badges.extension = afterBadges < 0 ? 0 : afterBadges\n            // Update info to server\n            this.$noSpinHttp.get(\n              this.$config.adminServer +\n                'extension/down?ext_id=' +\n                row.id +\n                '&version=' +\n                row.version +\n                '&pd_version=' +\n                this.$config.version\n            )\n          })\n          .catch(error => {\n            if (index + 1 < extFileServers.length) {\n              this.$Notice.error({\n                title: this.$t('extension.downloadErr'),\n                desc: this.$t('extension.downloadErrTip'),\n                onClose() {\n                  _this.downExtension(isUpdate, row, index + 1)\n                }\n              })\n            } else {\n              this.$Notice.error({\n                title: this.$t('extension.downloadErr'),\n                desc: error.response.data.error,\n                duration: 0,\n                closable: true\n              })\n              this.spinShow = false\n            }\n          })\n      }\n    },\n    installLocalExt() {\n      showDirChooser().then(result => {\n        if (!result) {\n          return\n        }\n        installLocalExtension(result.path)\n          .then(localExt => {\n            localExt.installed = true\n            localExt.currVersion = localExt.version\n            this.localAllList.push(localExt)\n            this.refreshExtensions()\n            this.$Message.success(this.$t('extension.installOk'))\n            this.activeTab = 'local'\n          })\n          .catch(error => {\n            if (error.response.status == 400) {\n              this.$Message.error(this.$t('extension.installErr'))\n            } else {\n              this.$Message.error(this.$t('alert.error'))\n            }\n          })\n      })\n    },\n    uninstallExtension(row) {\n      const _this = this\n      _this.$Modal.confirm({\n        title: _this.$t('extension.uninstall'),\n        content: _this.$t('extension.uninstallTip'),\n        okText: _this.$t('tip.ok'),\n        cancelText: _this.$t('tip.cancel'),\n        onOk() {\n          uninstallExtension(row.meta.path, row.meta.local)\n            .then(() => {\n              const index = _this.localAllList.findIndex(localExt => localExt.meta.path == row.meta.path)\n              if (index != -1) {\n                _this.localAllList.splice(index, 1)\n                _this.refreshExtensions()\n              }\n            })\n            .catch(() => _this.$Message.error(_this.$t('alert.error')))\n        }\n      })\n    },\n    getLocalExtensions(callback) {\n      getExtensions().then(localAllList => {\n        this.localAllList = localAllList\n        this.localAllList.forEach(localExt => {\n          localExt.installed = true\n          localExt.currVersion = localExt.version\n        })\n        if (callback) {\n          callback()\n        }\n      })\n    },\n    loadExtensions() {\n      // Loading proxy mode\n      getProxyMode().then(mode => (this.proxySwitch = mode === 1))\n      // Get local installed extension\n      this.getLocalExtensions(() => {\n        this.searchExtensions()\n      })\n    },\n    searchExtensions(pageSize) {\n      pageSize = pageSize ? pageSize : 1\n      this.onlineLoading = true\n      this.$noSpinHttp\n        .get(`${this.$config.adminServer}extension/search?pageSize=${pageSize}&version=${this.$config.version}`)\n        .then(result => {\n          this.onlinePage = result.data\n          this.refreshExtensions()\n        })\n        .finally(() => (this.onlineLoading = false))\n    },\n    refreshExtensions() {\n      this.onlinePage.data.forEach(onlineExt => {\n        const localExt = this.localAllList.find(localExt => localExt.meta.path == onlineExt.path)\n        if (localExt) {\n          this.$set(onlineExt, 'installed', true)\n          this.$set(onlineExt, 'currVersion', localExt.version)\n          this.$set(onlineExt, 'meta', localExt.meta)\n          this.$set(onlineExt, 'settings', localExt.settings)\n        } else {\n          onlineExt.installed = false\n          onlineExt.meta = { path: onlineExt.path, enabled: false }\n        }\n      })\n    },\n    openHomepage(row) {\n      const url =\n        row.homepage ||\n        'https://github.com/proxyee-down-org/proxyee-down-extension/blob/master' + row.meta.path + '/README.md'\n      openUrl(url)\n    },\n    openUrl(url) {\n      openUrl(url)\n    },\n    copyPac() {\n      const { protocol, host } = window.location\n      copy({\n        type: 'text',\n        data: `${protocol}//${host}/pac/pdown.pac?t=` + new Date().getTime()\n      })\n        .then(() => this.$Message.success(this.$t('tip.copySucc')))\n        .catch(() => this.$Message.error(this.$t('tip.copyFail')))\n    },\n    saveSetting() {\n      const setting = {}\n      this.settingExt.settings.forEach(s => {\n        setting[s.name] = s.value\n      })\n      updateExtensionSetting(this.settingExt.meta.path, setting)\n        .then(() => this.$Message.success(this.$t('tip.saveSucc')))\n        .catch(() => this.$Message.error(this.$t('tip.saveFail')))\n    }\n  },\n  created() {\n    // Check whether the certificate has been installed\n    checkCert().then(status => {\n      this.certStatus = status\n      // Already installed\n      if (status) {\n        this.loadExtensions()\n      }\n    })\n  }\n}\n</script>\n\n<style scoped>\n.install-button {\n  margin-top: 1.25rem;\n}\n.proxy-switch-div {\n  margin-bottom: 1.25rem;\n}\n.proxy-switch-div b {\n  padding-right: 10px;\n}\n.proxy-switch-div .icon-button {\n  margin-left: 25px;\n}\n.spin-icon-load {\n  animation: ani-demo-spin 1s linear infinite;\n}\n</style>\n"
  },
  {
    "path": "front/src/views/Setting.vue",
    "content": "<template>\n  <Form ref=\"form\"\n    :label-width=\"100\"\n    :model=\"form\"\n    :rules=\"rules\"\n    class=\"setting-form\">\n    <Collapse :value=\"['down','app']\">\n      <Panel name=\"down\">\n        {{ $t('setting.downSetting') }}\n        <div slot=\"content\">\n          <FormItem :label=\"$t('setting.path')\"\n            prop=\"downConfig.filePath\">\n            <FileChoose v-model=\"form.downConfig.filePath\"\n              style=\"width: 30rem\" />\n            <Tooltip class=\"item\"\n              placement=\"right\">\n              <Icon type=\"help-circled\"\n                class=\"action-icon tip-icon\" />\n              <div slot=\"content\">\n                <p>{{ $t('setting.pathTip') }}</p>\n              </div>\n            </Tooltip>\n          </FormItem>\n          <FormItem :label=\"$t('setting.connections')\"\n            prop=\"downConfig.connections\">\n            <Slider v-model=\"form.downConfig.connections\"\n              :min=\"2\"\n              :max=\"256\"\n              :step=\"2\"\n              show-input\n              style=\"width: 30rem\" />\n            <Tooltip class=\"item\"\n              style=\"position:absolute;left:30rem;top:-15px;\"\n              placement=\"right\">\n              <Icon type=\"help-circled\"\n                class=\"action-icon tip-icon\" />\n              <div slot=\"content\">\n                <p>{{ $t('setting.connectionsTip') }}</p>\n              </div>\n            </Tooltip>\n          </FormItem>\n          <FormItem :label=\"$t('setting.taskLimit')\"\n            prop=\"downConfig.taskLimit\">\n            <InputNumber v-model=\"form.downConfig.taskLimit\"\n              :min=\"1\"\n              :max=\"10\"></InputNumber>\n          </FormItem>\n          <FormItem :label=\"$t('setting.taskSpeedLimit')\"\n            prop=\"downConfig.speedLimit\">\n            <Input v-model=\"form.downConfig.speedLimit\" />\n            <span style=\"padding-left:5px\">KB/S({{ $t('setting.speedLimitTip') }})</span>\n          </FormItem>\n          <FormItem :label=\"$t('setting.globalSpeedLimit')\"\n            prop=\"downConfig.totalSpeedLimit\">\n            <Input v-model=\"form.downConfig.totalSpeedLimit\" />\n            <span style=\"padding-left:5px\">KB/S({{ $t('setting.speedLimitTip') }})</span>\n          </FormItem>\n        </div>\n      </Panel>\n      <Panel name=\"app\">\n        {{ $t('setting.appSetting') }}\n        <div slot=\"content\">\n          <FormItem :label=\"$t('setting.language')\"\n            prop=\"appConfig.locale\">\n            <Select v-model=\"form.appConfig.locale\">\n              <Option value=\"zh-CN\">中文(简体)</Option>\n              <Option value=\"zh-TW\">中文(繁體)</Option>\n              <Option value=\"en-US\">English(USA)</Option>\n            </Select>\n          </FormItem>\n          <FormItem :label=\"$t('setting.uiMode')\"\n            prop=\"appConfig.uiMode\">\n            <Select v-model=\"form.appConfig.uiMode\">\n              <Option v-for=\"option in setting.uiModes\"\n                :key=\"option.value\"\n                :value=\"option.value\">{{ option.text }}</Option>\n            </Select>\n          </FormItem>\n          <FormItem :label=\"$t('setting.checkUpdate')\"\n            prop=\"appConfig.updateCheckRate\">\n            <Select v-model=\"form.appConfig.updateCheckRate\">\n              <Option v-for=\"option in setting.updateChecks\"\n                :key=\"option.value\"\n                :value=\"option.value\">{{ option.text }}</Option>\n            </Select>\n          </FormItem>\n          <FormItem :label=\"$t('setting.autoOpen')\"\n            prop=\"appConfig.autoOpen\">\n            <Switch v-model=\"form.appConfig.autoOpen\"></Switch>\n          </FormItem>\n          <FormItem :label=\"$t('setting.secondProxy.secondProxy')\">\n            <Switch v-model=\"secondProxyEnable\"\n              @on-change=\"switchSecondProxy\"></Switch>\n            <Tooltip class=\"item\"\n              placement=\"right\">\n              <Icon type=\"help-circled\"\n                class=\"action-icon tip-icon\" />\n              <div slot=\"content\">\n                <p>{{$t('setting.secondProxy.tip')}}</p>\n              </div>\n            </Tooltip>\n          </FormItem>\n          <div v-if=\"secondProxyEnable\">\n            <FormItem :label=\"$t('setting.secondProxy.type')\"\n              prop=\"appConfig.proxyConfig.proxyType\">\n              <Select v-model=\"form.appConfig.proxyConfig.proxyType\"\n                style=\"width:6rem;\">\n                <Option value=\"HTTP\">HTTP</Option>\n                <Option value=\"SOCKS4\">SOCKS4</Option>\n                <Option value=\"SOCKS5\">SOCKS5</Option>\n              </Select>\n            </FormItem>\n            <FormItem :label=\"$t('setting.secondProxy.host')\"\n              prop=\"appConfig.proxyConfig.host\">\n              <Input v-model=\"form.appConfig.proxyConfig.host\"\n                class=\"string-input\" />\n            </FormItem>\n            <FormItem :label=\"$t('setting.secondProxy.port')\"\n              prop=\"appConfig.proxyConfig.port\">\n              <InputNumber v-model=\"form.appConfig.proxyConfig.port\"\n                :min=\"1\"\n                :max=\"65535\" />\n            </FormItem>\n            <FormItem :label=\"$t('setting.secondProxy.user')\">\n              <Input v-model=\"form.appConfig.proxyConfig.user\"\n                class=\"string-input\" />\n            </FormItem>\n            <FormItem :label=\"$t('setting.secondProxy.pwd')\">\n              <Input type=\"password\"\n                v-model=\"form.appConfig.proxyConfig.pwd\"\n                class=\"string-input\" />\n            </FormItem>\n          </div>\n        </div>\n      </Panel>\n    </Collapse>\n  </Form>\n</template>\n<script>\nimport FileChoose from '../components/FileChoose'\nimport { getConfig, setConfig } from '../common/native.js'\n\nlet debounceTimer\n\nexport default {\n  name: 'setting',\n  components: {\n    FileChoose\n  },\n  data() {\n    return {\n      form: {\n        downConfig: {},\n        appConfig: {}\n      },\n      secondProxyEnable: false,\n      rules: {\n        'downConfig.speedLimit': [\n          { required: true, message: this.$t('tip.notNull') },\n          { pattern: /^\\d+$/, message: this.$t('tip.fmtErr') }\n        ],\n        'downConfig.totalSpeedLimit': [\n          { required: true, message: this.$t('tip.notNull') },\n          { pattern: /^\\d+$/, message: this.$t('tip.fmtErr') }\n        ]\n      }\n    }\n  },\n  computed: {\n    setting() {\n      return {\n        uiModes: [\n          { value: 0, text: this.$t('setting.uiModeBrowser') },\n          { value: 1, text: this.$t('setting.uiModeWindows') }\n        ],\n        updateChecks: [\n          { value: 0, text: this.$t('setting.checkUpdateNever') },\n          { value: 1, text: this.$t('setting.checkUpdateWeek') },\n          { value: 2, text: this.$t('setting.checkUpdateStartup') }\n        ]\n      }\n    }\n  },\n  watch: {\n    form: {\n      handler(nowVal, oldVal) {\n        //不是首次加载触发\n        if (Object.keys(oldVal.downConfig).length !== 0) {\n          if (debounceTimer) {\n            clearTimeout(debounceTimer)\n          }\n          debounceTimer = setTimeout(() => {\n            debounceTimer = null\n            this.setConfig()\n          }, 300)\n        }\n      },\n      deep: true\n    }\n  },\n  methods: {\n    switchSecondProxy(val) {\n      if (val) {\n        this.rules['appConfig.proxyConfig.proxyType'] = [{ required: true, message: this.$t('tip.notNull') }]\n        this.rules['appConfig.proxyConfig.host'] = [{ required: true, message: this.$t('tip.notNull') }]\n        this.rules['appConfig.proxyConfig.port'] = [{ required: true, message: this.$t('tip.notNull') }]\n      } else if (this.form.appConfig.proxyConfig) {\n        this.rules['appConfig.proxyConfig.proxyType'] = null\n        this.rules['appConfig.proxyConfig.host'] = null\n        this.rules['appConfig.proxyConfig.port'] = null\n        this.setConfig()\n      }\n    },\n    async getConfig() {\n      let downConfig = await this.$noSpinHttp.get('http://127.0.0.1:26339/config')\n      let appConfig = await getConfig()\n      this.form = {\n        downConfig: { ...downConfig.data },\n        appConfig: { ...appConfig }\n      }\n      this.secondProxyEnable = !!this.form.appConfig.proxyConfig\n      //设置默认值\n      if (!this.form.appConfig.proxyConfig) {\n        this.$set(this.form.appConfig, 'proxyConfig', {\n          proxyType: 'HTTP',\n          host: null,\n          port: null,\n          user: null,\n          pwd: null\n        })\n      }\n      if (this.form.downConfig.speedLimit > 0) {\n        this.form.downConfig.speedLimit /= 1024\n      }\n      if (this.form.downConfig.totalSpeedLimit > 0) {\n        this.form.downConfig.totalSpeedLimit /= 1024\n      }\n    },\n    async setConfig() {\n      this.$refs['form'].validate(async valid => {\n        if (valid) {\n          let downConfig = { ...{}, ...this.form.downConfig }\n          let appConfig = { ...{}, ...this.form.appConfig }\n          if (downConfig.speedLimit > 0) {\n            downConfig.speedLimit *= 1024\n          }\n          if (downConfig.totalSpeedLimit > 0) {\n            downConfig.totalSpeedLimit *= 1024\n          }\n          if (!this.secondProxyEnable) {\n            this.$delete(appConfig, 'proxyConfig')\n            this.$delete(downConfig, 'proxyConfig')\n          } else {\n            downConfig.proxyConfig = { ...this.form.appConfig.proxyConfig }\n          }\n          await this.$noSpinHttp.put('http://127.0.0.1:26339/config', downConfig)\n          await setConfig(appConfig)\n          this.$Message.success(this.$t('tip.saveSucc'))\n          if (this.$i18n.locale != this.form.appConfig.locale) {\n            this.$i18n.locale = this.form.appConfig.locale\n            //强制渲染一遍，避免切换语言后下拉框不渲染的问题\n            /* const uiMode = this.form.appConfig.uiMode\n            const updateCheckRate = this.form.appConfig.updateCheckRate\n            this.form.appConfig.uiMode = null\n            this.form.appConfig.updateCheckRate = null\n            setTimeout(() => {\n              this.form.appConfig.uiMode = uiMode\n              this.form.appConfig.updateCheckRate = updateCheckRate\n            }, 0) */\n          }\n        }\n      })\n    }\n  },\n  created() {\n    this.getConfig()\n  }\n}\n</script>\n\n<style scoped>\n.setting-form .ivu-input-wrapper {\n  width: 5rem;\n}\n.setting-form .ivu-select {\n  width: 10rem;\n}\n.setting-form .string-input {\n  width: 10rem;\n}\n</style>\n"
  },
  {
    "path": "front/src/views/Support.vue",
    "content": "<template>\n  <div>\n    <Card>\n      <p slot=\"title\">如果觉得本软件不错的话，可以通过下面的二维码打赏作者，让作者有动力持续更新版本和修复BUG。</p>\n      <div class=\"card-container qr-container\">\n        <img class=\"qr-img\" src=\"pay/alipay.png\" />\n        <b>支付宝</b>\n      </div>\n      <div class=\"card-container qr-container\">\n        <img class=\"qr-img\" src=\"pay/weipay.png\" />\n        <b>微信</b>\n      </div>\n    </Card>\n    <Card v-if=\"softList.length>0\"\n      style=\"margin-top:20px;\">\n      <p slot=\"title\">另外作者在这里推荐些正版软件，有购买意向的话可以点进去看一看，你们每一次点击也是对作者的支持与鼓励。</p>\n      <Card v-for=\"(soft,index) in softList\"\n        :key=\"index\"\n        class=\"card-container recommend-container\">\n        <a href=\"javascript:;\"\n          @click=\"openUrl(soft.url)\">\n          <img class=\"ad-img\" :src=\"soft.preview\" />\n          <b>{{soft.title}}</b></a>\n      </Card>\n    </Card>\n  </div>\n</template>\n\n<script>\nimport { openUrl } from '../common/native.js'\nexport default {\n  data() {\n    return {\n      softList: []\n    }\n  },\n  methods: {\n    openUrl(url) {\n      openUrl(url)\n    }\n  },\n  created() {\n    this.$noSpinHttp.get(this.$config.adminServer + 'recommend/soft').then(result => {\n      this.softList = result.data\n    })\n  }\n}\n</script>\n\n<style scoped>\n.card-container {\n  width: 220px;\n  text-align: center;\n  margin: 5px;\n}\n.qr-container {\n  display: inline-block;\n}\n.recommend-container {\n  display: inline-flex;\n  height: 300px;\n}\n.card-container b {\n  display: inline-block;\n  padding-top: 5px;\n}\n.card-container img {\n  width: 200px;\n  height: 200px;\n}\n</style>\n"
  },
  {
    "path": "front/src/views/Tasks.vue",
    "content": "<template>\r\n  <div class=\"tasks\">\r\n    <div class=\"tasks-entry\">\r\n      <Button type=\"info\"\r\n        icon=\"plus\"\r\n        class=\"tasks-button\"\r\n        @click=\"resolveVisible=true\">{{ $t(\"tasks.createTask\") }}</Button>\r\n      <Button type=\"warning\"\r\n        icon=\"ios-pause\"\r\n        class=\"tasks-button\"\r\n        @click=\"onPauseBatch\">{{ $t(\"tasks.pauseDownloads\") }}</Button>\r\n      <Button type=\"warning\"\r\n        icon=\"ios-play\"\r\n        class=\"tasks-button\"\r\n        @click=\"onResumeBatch\">{{ $t(\"tasks.continueDownloading\") }}</Button>\r\n      <Button type=\"error\"\r\n        icon=\"ios-trash\"\r\n        class=\"tasks-button\"\r\n        @click=\"onDeleteBatch\">{{ $t(\"tasks.deleteTask\") }}</Button>\r\n    </div>\r\n    <Tabs type=\"card\"\r\n      :animated=\"false\"\r\n      v-model=\"activeTab\"\r\n      style=\"overflow:visible;\">\r\n      <TabPane :label=\"$t('tasks.running')+'('+runList.length+')'\"\r\n        name=\"run\"\r\n        icon=\"play\">\r\n        <Table :taskList=\"runList\"\r\n          ref=\"runTable\"\r\n          :maxHeight=\"taskListMaxHeight\"\r\n          @on-delete=\"onDelete\"\r\n          @on-pause=\"onPause\"\r\n          @on-resume=\"onResume\"\r\n          @on-open=\"onOpen\" />\r\n      </TabPane>\r\n      <TabPane :label=\"$t('tasks.waiting')+'('+waitList.length+')'\"\r\n        name=\"wait\"\r\n        icon=\"pause\">\r\n        <Table :taskList=\"waitList\"\r\n          ref=\"waitTable\"\r\n          :maxHeight=\"taskListMaxHeight\"\r\n          @on-delete=\"onDelete\"\r\n          @on-pause=\"onPause\"\r\n          @on-resume=\"onResume\"\r\n          @on-open=\"onOpen\" />\r\n      </TabPane>\r\n      <TabPane :label=\"$t('tasks.done')+'('+doneList.length+')'\"\r\n        name=\"done\"\r\n        icon=\"checkmark\">\r\n        <Table :taskList=\"doneList\"\r\n          ref=\"doneTable\"\r\n          :maxHeight=\"taskListMaxHeight\"\r\n          @on-delete=\"onDelete\"\r\n          @on-pause=\"onPause\"\r\n          @on-resume=\"onResume\"\r\n          @on-open=\"onOpen\" />\r\n      </TabPane>\r\n    </Tabs>\r\n\r\n    <Modal v-model=\"deleteModal\"\r\n      :title=\"$t('tasks.deleteTask')\">\r\n      <Checkbox v-model=\"delFile\"></Checkbox>\r\n      <span @click=\"delFile=!delFile\">{{ $t('tasks.deleteTaskTip') }}</span>\r\n      <div slot=\"footer\">\r\n        <Button type=\"primary\"\r\n          @click=\"doDelete(delTaskIds)\">{{ $t('tip.ok') }}</Button>\r\n        <Button @click=\"deleteModal=false\">{{ $t('tip.cancel') }}</Button>\r\n      </div>\r\n    </Modal>\r\n\r\n    <Resolve v-model=\"resolveVisible\" />\r\n    <Create :request=\"createForm.request\"\r\n      :response=\"createForm.response\"\r\n      :config=\"createForm.config\"\r\n      :data=\"createForm.data\"\r\n      @close=\"$router.push('/');\" />\r\n  </div>\r\n</template>\r\n\r\n<script>\r\nimport Table from '../components/Table'\r\nimport Resolve from '../components/Task/Resolve'\r\nimport Create from '../components/Task/Create'\r\nimport { showFile } from '../common/native'\r\nimport ReconnectingWebSocket from 'reconnecting-websocket'\r\n\r\nlet ws\r\n\r\nexport default {\r\n  name: 'tasks',\r\n  components: {\r\n    Table,\r\n    Resolve,\r\n    Create\r\n  },\r\n\r\n  mounted() {\r\n    ws = new ReconnectingWebSocket('ws://' + window.location.hostname + ':26339/ws')\r\n    ws.onmessage = evt => {\r\n      const msg = eval('(' + evt.data + ')')\r\n      const data = msg.data\r\n      const updateTask = (taskIds, fromListArray, toList, handle) => {\r\n        if (taskIds && taskIds.length) {\r\n          taskIds.forEach(taskId => {\r\n            fromListArray.forEach(fromList => {\r\n              const index = fromList.findIndex(task => task.id == taskId)\r\n              if (index >= 0) {\r\n                const task = fromList[index]\r\n                let moveFlag = false\r\n                if (handle) {\r\n                  moveFlag = handle(task)\r\n                }\r\n                //Move to the other task list\r\n                if (moveFlag && fromList != toList) {\r\n                  fromList.splice(index, 1)\r\n                  toList.splice(0, 0, task)\r\n                }\r\n                this.refreshMaxHeight()\r\n              }\r\n            })\r\n          })\r\n        }\r\n      }\r\n      switch (msg.type) {\r\n        case 'CREATE':\r\n          if (data.info.status == 1) {\r\n            this.runList.push(data)\r\n            this.activeTab = 'run'\r\n          } else {\r\n            this.waitList.push(data)\r\n            this.activeTab = 'wait'\r\n          }\r\n          this.refreshMaxHeight()\r\n          break\r\n        case 'PROGRESS':\r\n          updateTask([data.id], [this.runList], this.doneList, task => {\r\n            //Update the task progress info\r\n            task.info = data.info\r\n            return data.info.status == 4\r\n          })\r\n          break\r\n        case 'PAUSE':\r\n          updateTask(data, [this.runList, this.waitList], this.waitList, task => {\r\n            //Update the task status to pause\r\n            task.info.status = 2\r\n            return true\r\n          })\r\n          this.activeTab = 'wait'\r\n          break\r\n        case 'ERROR':\r\n          updateTask([data.id], [this.runList], this.waitList, task => {\r\n            //Update the task status to error\r\n            task.info.status = 3\r\n            return true\r\n          })\r\n          this.activeTab = 'wait'\r\n          break\r\n        case 'RESUME':\r\n          updateTask(data.pauseIds, [this.runList, this.waitList], this.waitList, task => {\r\n            //Update the task status to pause\r\n            task.info.status = 2\r\n            return true\r\n          })\r\n          updateTask(data.waitIds, [this.runList, this.waitList], this.waitList, task => {\r\n            //Update the task status to wait\r\n            task.info.status = 0\r\n            return true\r\n          })\r\n          updateTask(data.resumeIds, [this.waitList], this.runList, task => {\r\n            //Update the task status to downloading\r\n            task.info.status = 1\r\n            return true\r\n          })\r\n          this.activeTab = 'run'\r\n          break\r\n        case 'DELETE': {\r\n          let list = this[this.activeTab + 'List']\r\n          if (list && data) {\r\n            data.forEach(taskId => {\r\n              const index = list.findIndex(task => task.id == taskId)\r\n              if (index != -1) {\r\n                list.splice(index, 1)\r\n              }\r\n            })\r\n          }\r\n          break\r\n        }\r\n      }\r\n    }\r\n    this.retryLoadTaskList()\r\n  },\r\n\r\n  destroyed() {\r\n    ws.close()\r\n  },\r\n\r\n  data() {\r\n    return {\r\n      activeTab: 'run',\r\n      taskListMaxHeight: undefined,\r\n      runList: [],\r\n      waitList: [],\r\n      doneList: [],\r\n      deleteModal: false,\r\n      delTaskIds: [],\r\n      delFile: false,\r\n      resolveVisible: false,\r\n      createForm: {\r\n        request: null,\r\n        response: null,\r\n        config: null,\r\n        data: null\r\n      }\r\n    }\r\n  },\r\n\r\n  watch: {\r\n    $route() {\r\n      this.onRouteChange(this.$route.query)\r\n    }\r\n  },\r\n\r\n  methods: {\r\n    showResolve() {\r\n      this.resolveVisible = true\r\n    },\r\n\r\n    onRouteChange(query) {\r\n      const flag = !!(query.request && query.response)\r\n      this.createForm = {\r\n        request: flag ? JSON.parse(query.request) : null,\r\n        response: flag ? JSON.parse(query.response) : null,\r\n        config: flag && query.config ? JSON.parse(query.config) : null,\r\n        data: flag && query.data ? JSON.parse(query.data) : null\r\n      }\r\n    },\r\n\r\n    onPause(task) {\r\n      this.doPause([task.id])\r\n    },\r\n\r\n    onResume(task) {\r\n      this.doResume([task.id])\r\n    },\r\n\r\n    onDelete(task) {\r\n      this.delTaskIds = [task.id]\r\n      this.delFile = false\r\n      this.deleteModal = true\r\n    },\r\n\r\n    onOpen(task) {\r\n      showFile(`${task.config.filePath}/${task.response.fileName}`)\r\n    },\r\n\r\n    onPauseBatch() {\r\n      const ids = this.getCheckedIds()\r\n      if (ids.length) {\r\n        this.doPause(ids)\r\n      }\r\n    },\r\n\r\n    onResumeBatch() {\r\n      const ids = this.getCheckedIds()\r\n      if (ids.length) {\r\n        this.doResume(ids)\r\n      }\r\n    },\r\n\r\n    onDeleteBatch() {\r\n      const ids = this.getCheckedIds()\r\n      if (ids.length) {\r\n        this.delTaskIds = ids\r\n        this.delFile = false\r\n        this.deleteModal = true\r\n      }\r\n    },\r\n\r\n    doPause(ids) {\r\n      this.$http.put('http://127.0.0.1:26339/tasks/pause', ids)\r\n    },\r\n\r\n    doResume(ids) {\r\n      this.$http.put('http://127.0.0.1:26339/tasks/resume', ids)\r\n    },\r\n\r\n    doDelete(ids) {\r\n      this.$http\r\n        .post(`http://127.0.0.1:26339/tasks/delete?delFile=${this.delFile}`, ids)\r\n        .finally(() => (this.deleteModal = false))\r\n    },\r\n\r\n    getCheckedIds() {\r\n      return this.$refs[this.activeTab + 'Table'].getCheckedTasks().map(task => task.id)\r\n    },\r\n\r\n    getIndexByTaskId(taskId) {\r\n      return this.taskList.findIndex(t => t.id === taskId)\r\n    },\r\n\r\n    getTop(e) {\r\n      let offset = e.offsetTop\r\n      if (e.offsetParent) {\r\n        offset += this.getTop(e.offsetParent)\r\n      }\r\n      return offset\r\n    },\r\n\r\n    retryLoadTaskList() {\r\n      this.$noSpinHttp\r\n        .get('http://127.0.0.1:26339/tasks')\r\n        .then(result => {\r\n          result.data.forEach(task => {\r\n            if (task.info.status == 1) {\r\n              //Downloading tasks\r\n              this.runList.push(task)\r\n            } else if (task.info.status == 4) {\r\n              //Completed task\r\n              this.doneList.push(task)\r\n            } else {\r\n              this.waitList.push(task)\r\n            }\r\n          })\r\n          this.refreshMaxHeight()\r\n        })\r\n        .catch(error => {\r\n          if (!error.response) {\r\n            setTimeout(this.retryLoadTaskList, 3000)\r\n          }\r\n        })\r\n    },\r\n\r\n    refreshMaxHeight() {\r\n      const taskListTop = this.getTop(this.$refs[this.activeTab + 'Table'].$el.querySelector('div.tb-body'))\r\n      const windowHeight = document.documentElement.clientHeight || document.body.clientHeight\r\n      this.taskListMaxHeight = windowHeight - taskListTop - 25\r\n    }\r\n  },\r\n\r\n  created() {\r\n    window.onresize = () => {\r\n      this.refreshMaxHeight()\r\n    }\r\n    this.onRouteChange(this.$route.query)\r\n  }\r\n}\r\n</script>\r\n\r\n<style lang=\"less\" scoped>\r\n.tasks-entry {\r\n  margin-bottom: 20px;\r\n  .tasks-button {\r\n    margin-right: 10px;\r\n    &:last-of-type {\r\n      margin-right: 0;\r\n    }\r\n  }\r\n}\r\n\r\n.ivu-table {\r\n  td {\r\n    background-color: inherit;\r\n  }\r\n}\r\n</style>\r\n\r\n<style lang=\"less\">\r\n.tasks {\r\n  .ivu-table {\r\n    .taskList {\r\n      // background-color: yellow;\r\n    }\r\n    td {\r\n      background-color: inherit;\r\n    }\r\n  }\r\n}\r\n</style>\r\n"
  },
  {
    "path": "front/vue.config.js",
    "content": "module.exports = {\r\n  productionSourceMap: true, // Production environment does not generate source-map\r\n  css: {\r\n    sourceMap: false // CSS does not generate source-map\r\n  },\r\n  outputDir: '../main/src/main/resources/http',\r\n  devServer: {\r\n    proxy: {\r\n      '/native': {\r\n        target: 'http://127.0.0.1:7478',\r\n        changeOrigin: true\r\n      },\r\n      '/pac': {\r\n        target: 'http://127.0.0.1:7478',\r\n        changeOrigin: true\r\n      },\r\n      '/ws': {\r\n        target: 'http://127.0.0.1:7478',\r\n        ws: true,\r\n      }\r\n    }\r\n  },\r\n  chainWebpack: config => {\r\n    config.module\r\n      .rule('vue')\r\n      .test(/\\.vue$/)\r\n      .use('iview-loader')\r\n      .loader('iview-loader')\r\n      .options({\r\n        prefix: true\r\n      })\r\n  }\r\n}"
  },
  {
    "path": "main/.gitignore",
    "content": "﻿target/\n!.mvn/wrapper/maven-wrapper.jar\nsrc/main/resources/http\n\n### STS ###\n.apt_generated\n.classpath\n.factorypath\n.project\n.settings\n.springBeans\n\n### IntelliJ IDEA ###\n.idea\n*.iws\n*.iml\n*.ipr"
  },
  {
    "path": "main/pom.xml",
    "content": "<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n  xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n  xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n  <modelVersion>4.0.0</modelVersion>\n  <parent>\n    <groupId>org.pdown.gui</groupId>\n    <artifactId>proxyee-down</artifactId>\n    <version>3.0</version>\n  </parent>\n\n  <groupId>org.pdown.gui</groupId>\n  <artifactId>main</artifactId>\n  <packaging>jar</packaging>\n\n  <repositories>\n    <repository>\n      <id>snapshots-repo</id>\n      <url>https://oss.sonatype.org/content/repositories/snapshots</url>\n      <releases>\n        <enabled>false</enabled>\n      </releases>\n      <snapshots>\n        <enabled>true</enabled>\n      </snapshots>\n    </repository>\n  </repositories>\n\n  <profiles>\n    <profile>\n      <id>dev</id>\n      <properties>\n        <environment>dev</environment>\n      </properties>\n      <activation>\n        <activeByDefault>true</activeByDefault>\n      </activation>\n    </profile>\n    <profile>\n      <id>prd</id>\n      <properties>\n        <environment>prd</environment>\n      </properties>\n    </profile>\n  </profiles>\n\n  <dependencies>\n    <dependency>\n      <groupId>org.pdown</groupId>\n      <artifactId>rest</artifactId>\n      <version>1.0.2-SNAPSHOT</version>\n    </dependency>\n    <dependency>\n      <groupId>com.github.monkeywie</groupId>\n      <artifactId>proxyee</artifactId>\n      <version>1.0.4</version>\n    </dependency>\n    <dependency>\n      <groupId>net.java.dev.jna</groupId>\n      <artifactId>jna</artifactId>\n      <version>4.5.1</version>\n    </dependency>\n    <dependency>\n      <groupId>org.yaml</groupId>\n      <artifactId>snakeyaml</artifactId>\n      <version>1.19</version>\n    </dependency>\n  </dependencies>\n\n  <build>\n    <resources>\n      <resource>\n        <filtering>true</filtering>\n        <directory>src/main/resources</directory>\n        <excludes>\n          <exclude>application.yml</exclude>\n          <exclude>application-dev.yml</exclude>\n          <exclude>application-prd.yml</exclude>\n          <exclude>logback-dev.xml</exclude>\n          <exclude>logback-prd.xml</exclude>\n        </excludes>\n      </resource>\n      <resource>\n        <filtering>true</filtering>\n        <directory>src/main/resources</directory>\n        <includes>\n          <include>logback-${environment}.xml</include>\n          <include>application-${environment}.yml</include>\n          <include>application.yml</include>\n        </includes>\n      </resource>\n    </resources>\n    <finalName>proxyee-down-main</finalName>\n    <plugins>\n      <plugin>\n        <groupId>org.apache.maven.plugins</groupId>\n        <artifactId>maven-resources-plugin</artifactId>\n        <configuration>\n          <nonFilteredFileExtensions>\n            <nonFilteredFileExtension>bin</nonFilteredFileExtension>\n            <nonFilteredFileExtension>css</nonFilteredFileExtension>\n            <nonFilteredFileExtension>js</nonFilteredFileExtension>\n            <nonFilteredFileExtension>eot</nonFilteredFileExtension>\n            <nonFilteredFileExtension>woff</nonFilteredFileExtension>\n            <nonFilteredFileExtension>ttf</nonFilteredFileExtension>\n            <nonFilteredFileExtension>ico</nonFilteredFileExtension>\n            <nonFilteredFileExtension>html</nonFilteredFileExtension>\n            <nonFilteredFileExtension>svg</nonFilteredFileExtension>\n          </nonFilteredFileExtensions>\n        </configuration>\n      </plugin>\n      <plugin>\n        <groupId>org.springframework.boot</groupId>\n        <artifactId>spring-boot-maven-plugin</artifactId>\n        <version>2.0.2.RELEASE</version>\n        <configuration>\n          <mainClass>org.pdown.gui.DownApplication</mainClass>\n        </configuration>\n        <executions>\n          <execution>\n            <goals>\n              <goal>repackage</goal>\n            </goals>\n          </execution>\n        </executions>\n      </plugin>\n    </plugins>\n  </build>\n</project>\n"
  },
  {
    "path": "main/src/main/java/org/pdown/gui/DownApplication.java",
    "content": "package org.pdown.gui;\n\nimport java.awt.AWTException;\nimport java.awt.Desktop;\nimport java.awt.Dimension;\nimport java.awt.Image;\nimport java.awt.MenuItem;\nimport java.awt.PopupMenu;\nimport java.awt.SystemTray;\nimport java.awt.Toolkit;\nimport java.awt.TrayIcon;\nimport java.io.File;\nimport java.io.IOException;\nimport java.lang.reflect.Method;\nimport java.net.JarURLConnection;\nimport java.net.URI;\nimport java.net.URL;\nimport java.net.URLConnection;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.nio.file.attribute.PosixFilePermission;\nimport java.nio.file.attribute.PosixFilePermissions;\nimport java.util.Set;\nimport java.util.concurrent.CountDownLatch;\nimport javafx.application.Application;\nimport javafx.application.Platform;\nimport javafx.geometry.Rectangle2D;\nimport javafx.scene.Scene;\nimport javafx.stage.Screen;\nimport javafx.stage.Stage;\nimport javax.swing.JOptionPane;\nimport org.pdown.core.util.OsUtil;\nimport org.pdown.gui.com.Browser;\nimport org.pdown.gui.com.Components;\nimport org.pdown.gui.content.PDownConfigContent;\nimport org.pdown.gui.extension.ExtensionContent;\nimport org.pdown.gui.extension.mitm.util.ExtensionProxyUtil;\nimport org.pdown.gui.http.EmbedHttpServer;\nimport org.pdown.gui.http.controller.ApiController;\nimport org.pdown.gui.http.controller.NativeController;\nimport org.pdown.gui.http.controller.PacController;\nimport org.pdown.gui.rest.HttpDownAppCallback;\nimport org.pdown.gui.util.AppUtil;\nimport org.pdown.gui.util.ConfigUtil;\nimport org.pdown.gui.util.ExecUtil;\nimport org.pdown.gui.util.I18nUtil;\nimport org.pdown.rest.DownRestServer;\nimport org.pdown.rest.content.ConfigContent;\nimport org.pdown.rest.content.RestWebServerFactoryCustomizer;\nimport org.pdown.rest.controller.HttpDownRestCallback;\nimport org.pdown.rest.entity.ServerConfigInfo;\nimport org.pdown.rest.util.PathUtil;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.boot.builder.SpringApplicationBuilder;\nimport org.springframework.util.StringUtils;\n\npublic class DownApplication extends Application {\n\n  private static final Logger LOGGER = LoggerFactory.getLogger(DownApplication.class);\n\n  private static final String OS = OsUtil.isWindows() ? \"windows\"\n      : (OsUtil.isMac() ? \"mac\" : \"linux\");\n  private static final String ICON_PATH = OS + (OsUtil.isWindowsXP() ? \"/logo_xp.png\" : \"/logo.png\");\n\n  public static DownApplication INSTANCE;\n\n  private Stage stage;\n  private Browser browser;\n  private TrayIcon trayIcon;\n\n  private CountDownLatch countDownLatch;\n  //前端页面http服务器端口\n  public int FRONT_PORT;\n  //native api服务器端口\n  public int API_PORT;\n  //代理服务器端口\n  public int PROXY_PORT;\n\n  @Override\n  public void start(Stage primaryStage) throws Exception {\n    INSTANCE = this;\n    stage = primaryStage;\n    Platform.setImplicitExit(false);\n    //load config\n    initConfig();\n    //load pdown-rest\n    initRest();\n    initMacMITMTool();\n    initEmbedHttpServer();\n    initExtension();\n    initTray();\n    //xp不支持webview\n    if (!OsUtil.isWindowsXP()) {\n      initWindow();\n      initBrowser();\n    }\n    loadUri(null, true, true);\n  }\n\n\n  private void initConfig() throws IOException {\n    PDownConfigContent.getInstance().load();\n    //取前端http server端口\n    FRONT_PORT = ConfigUtil.getInt(\"front.port\");\n    //取native api http server端口\n    API_PORT = ConfigUtil.getInt(\"api.port\");\n    if (\"prd\".equals(ConfigUtil.getString(\"spring.profiles.active\"))) {\n      try {\n        //端口被时占用随机分配一个端口\n        API_PORT = OsUtil.getFreePort(API_PORT);\n        if (FRONT_PORT == -1) {\n          FRONT_PORT = API_PORT;\n        }\n      } catch (IOException e) {\n        LOGGER.error(\"initConfig error\", e);\n        alertAndExit(I18nUtil.getMessage(\"gui.alert.startError\", e.getMessage()));\n      }\n    }\n  }\n\n  private void initRest() {\n    //init rest server config\n    HttpDownRestCallback.setCallback(new HttpDownAppCallback());\n    RestWebServerFactoryCustomizer.init(null);\n    ServerConfigInfo serverConfigInfo = ConfigContent.getInstance().get();\n    serverConfigInfo.setPort(REST_PORT);\n    if (StringUtils.isEmpty(serverConfigInfo.getFilePath())) {\n      serverConfigInfo.setFilePath(System.getProperty(\"user.home\") + File.separator + \"Downloads\");\n    }\n    new SpringApplicationBuilder(DownRestServer.class).headless(false).build().run();\n  }\n\n  //读取扩展信息和启动代理服务器\n  private void initExtension() {\n    Runtime.getRuntime().addShutdownHook(new Thread(() -> {\n      //退出时把系统代理还原\n      if (PDownConfigContent.getInstance().get().getProxyMode() == 1) {\n        try {\n          ExtensionProxyUtil.disabledProxy();\n        } catch (IOException e) {\n        }\n      }\n    }));\n    new Thread(() -> {\n      //检查是否安装了证书\n      try {\n        if (AppUtil.checkIsInstalledCert()) {\n          AppUtil.startProxyServer();\n        }\n      } catch (Exception e) {\n        LOGGER.error(\"Init extension error\", e);\n      }\n    }).start();\n    //根据扩展生成pac文件并切换系统代理\n    try {\n      ExtensionContent.load();\n      AppUtil.refreshPAC();\n    } catch (IOException e) {\n      LOGGER.error(\"Extension content load error\", e);\n    }\n  }\n\n  public static int macToolPort;\n\n  //加载mac tool\n  private void initMacMITMTool() {\n    if (OsUtil.isMac()) {\n      new Thread(() -> {\n        String toolUri = \"mac/mitm-tool.bin\";\n        Path toolPath = Paths.get(PathUtil.ROOT_PATH + File.separator + toolUri);\n        try {\n          if (!toolPath.toFile().exists()) {\n            ClassLoader classLoader = Thread.currentThread().getContextClassLoader();\n            URL url = classLoader.getResource(toolUri);\n            URLConnection connection = url.openConnection();\n            if (connection instanceof JarURLConnection) {\n              if (!toolPath.getParent().toFile().exists()) {\n                Files.createDirectories(toolPath.getParent());\n              }\n              Files.copy(classLoader.getResourceAsStream(toolUri), toolPath);\n              Set<PosixFilePermission> perms = PosixFilePermissions.fromString(\"rwxrw-rw-\");\n              Files.setPosixFilePermissions(toolPath, perms);\n            }\n          }\n          //取一个空闲端口来运行mac tool\n          macToolPort = OsUtil.getFreePort();\n          //程序退出监听\n          Runtime.getRuntime().addShutdownHook(new Thread(() -> {\n            try {\n              ExecUtil.httpGet(\"http://127.0.0.1:\" + macToolPort + \"/quit\");\n            } catch (IOException e) {\n            }\n          }));\n          ExecUtil.execBlockWithAdmin(\"'\" + toolPath.toFile().getPath() + \"' \" + macToolPort);\n        } catch (Exception e) {\n          LOGGER.error(\"initMacMITMTool error\", e);\n          alertAndExit(\"Init mitm-tool error：\" + e.getMessage());\n        }\n        System.exit(0);\n      }).start();\n    }\n  }\n\n  private void initEmbedHttpServer() {\n    countDownLatch = new CountDownLatch(1);\n    new Thread(() -> {\n      EmbedHttpServer embedHttpServer = new EmbedHttpServer(API_PORT);\n      embedHttpServer.addController(new NativeController());\n      embedHttpServer.addController(new ApiController());\n      embedHttpServer.addController(new PacController());\n      embedHttpServer.start(future -> countDownLatch.countDown());\n    }).start();\n  }\n\n  //加载托盘\n  private void initTray() throws AWTException {\n    if (SystemTray.isSupported()) {\n      // 获得系统托盘对象\n      SystemTray systemTray = SystemTray.getSystemTray();\n      // 获取图片所在的URL\n      URL url = Thread.currentThread().getContextClassLoader().getResource(ICON_PATH);\n      // 为系统托盘加托盘图标\n      Image trayImage = Toolkit.getDefaultToolkit().getImage(url);\n      Dimension trayIconSize = systemTray.getTrayIconSize();\n      trayImage = trayImage.getScaledInstance(trayIconSize.width, trayIconSize.height, Image.SCALE_SMOOTH);\n      trayIcon = new TrayIcon(trayImage, \"Proxyee Down\");\n      systemTray.add(trayIcon);\n      loadPopupMenu();\n      //双击事件监听\n      trayIcon.addActionListener(event -> Platform.runLater(() -> loadUri(null, true)));\n    }\n  }\n\n  public void loadPopupMenu() {\n    //添加右键菜单\n    PopupMenu popupMenu = new PopupMenu();\n    MenuItem showItem = new MenuItem(I18nUtil.getMessage(\"gui.tray.show\"));\n    showItem.addActionListener(event -> Platform.runLater(() -> loadUri(\"\", true)));\n    MenuItem setItem = new MenuItem(I18nUtil.getMessage(\"gui.tray.set\"));\n    setItem.addActionListener(event -> loadUri(\"/#/setting\", true));\n    MenuItem aboutItem = new MenuItem(I18nUtil.getMessage(\"gui.tray.about\"));\n    aboutItem.addActionListener(event -> loadUri(\"/#/about\", true));\n    MenuItem supportItem = new MenuItem(I18nUtil.getMessage(\"gui.tray.support\"));\n    supportItem.addActionListener(event -> loadUri(\"/#/support\", true));\n    MenuItem closeItem = new MenuItem(I18nUtil.getMessage(\"gui.tray.exit\"));\n    closeItem.addActionListener(event -> {\n      Platform.exit();\n      System.exit(0);\n    });\n    popupMenu.add(showItem);\n    popupMenu.addSeparator();\n    popupMenu.add(setItem);\n    popupMenu.add(aboutItem);\n    popupMenu.add(supportItem);\n    popupMenu.addSeparator();\n    popupMenu.add(closeItem);\n    trayIcon.setPopupMenu(popupMenu);\n  }\n\n  public void refreshBrowserMenu() {\n    browser.refreshText();\n  }\n\n  //加载webView\n  private void initBrowser() throws AWTException {\n    browser = new Browser();\n    stage.setScene(new Scene(browser));\n    try {\n      countDownLatch.await();\n    } catch (InterruptedException e) {\n    }\n  }\n\n  //加载gui窗口\n  private void initWindow() {\n    stage.setTitle(\"Proxyee Down\");\n    Rectangle2D bounds = Screen.getPrimary().getVisualBounds();\n    int width = 1024;\n    int height = 576;\n    stage.setX((bounds.getWidth() - width) / 2);\n    stage.setY((bounds.getHeight() - height) / 2);\n    stage.setMinWidth(width);\n    stage.setMinHeight(height);\n    stage.getIcons().add(new javafx.scene.image.Image(Thread.currentThread().getContextClassLoader().getResourceAsStream(ICON_PATH)));\n    stage.setResizable(true);\n    //关闭窗口监听\n    stage.setOnCloseRequest(event -> {\n      event.consume();\n      stage.hide();\n    });\n  }\n\n  /**\n   * 显示gui窗口\n   *\n   * @param isTray 是否从托盘按钮打开的(windows下如果非托盘按钮调用窗口可能不会置顶)\n   */\n  public void show(boolean isTray) {\n    //是否需要调用窗口置顶\n    boolean isFront = false;\n    if (stage.isShowing()) {\n      if (stage.isIconified()) {\n        stage.setIconified(false);\n      } else {\n        isFront = true;\n        stage.toFront();\n      }\n    } else {\n      isFront = true;\n      stage.show();\n      stage.toFront();\n    }\n    //避免有时候窗口不弹出\n    if (isFront && !isTray && OsUtil.isWindows()) {\n      stage.setIconified(true);\n      stage.setIconified(false);\n    }\n  }\n\n  public void loadUri(String uri, boolean isTray, boolean isStartup) {\n    String url = \"http://127.0.0.1:\" + FRONT_PORT + (uri == null ? \"\" : uri);\n    boolean autoOpen = PDownConfigContent.getInstance().get().isAutoOpen();\n    if (OsUtil.isWindowsXP() || PDownConfigContent.getInstance().get().getUiMode() == 0) {\n      if (!isStartup || autoOpen) {\n        try {\n          Desktop.getDesktop().browse(URI.create(url));\n        } catch (IOException e) {\n          LOGGER.error(\"Open browse error\", e);\n        }\n      }\n\n    } else {\n      Platform.runLater(() -> {\n        if (uri != null || !browser.isLoad()) {\n          browser.load(url);\n        }\n        if (!isStartup || autoOpen) {\n          show(isTray);\n        }\n      });\n    }\n  }\n\n  public void loadUri(String uri, boolean isTray) {\n    loadUri(uri, isTray, false);\n  }\n\n  //提示并退出程序\n  private void alertAndExit(String msg) {\n    Platform.runLater(() -> {\n      Components.alert(msg);\n      System.exit(0);\n    });\n  }\n\n  static {\n    //设置日志存放路径\n    System.setProperty(\"ROOT_PATH\", PathUtil.ROOT_PATH);\n    //webView允许跨域访问\n    System.setProperty(\"sun.net.http.allowRestrictedHeaders\", \"true\");\n\n    //处理MAC dock图标\n    if (OsUtil.isMac()) {\n      try {\n        Class<?> appClass = Class.forName(\"com.apple.eawt.Application\");\n        Method getApplication = appClass.getMethod(\"getApplication\");\n        Object application = getApplication.invoke(appClass);\n        Method setDockIconImage = appClass.getMethod(\"setDockIconImage\", Image.class);\n        URL url = Thread.currentThread().getContextClassLoader().getResource(\"mac/dock_logo.png\");\n        Image image = Toolkit.getDefaultToolkit().getImage(url);\n        setDockIconImage.invoke(application, image);\n      } catch (Exception e) {\n        LOGGER.error(\"handle mac dock icon error\", e);\n      }\n    }\n  }\n\n  private static final int REST_PORT = 26339;\n\n  private static void doCheck() {\n    if (OsUtil.isBusyPort(REST_PORT)) {\n      JOptionPane.showMessageDialog(\n          null,\n          I18nUtil.getMessage(\"gui.alert.startError\", I18nUtil.getMessage(\"gui.alert.restPortBusy\")),\n          I18nUtil.getMessage(\"gui.warning\"),\n          JOptionPane.WARNING_MESSAGE);\n      System.exit(0);\n    }\n  }\n\n  //-Dio.netty.leakDetection.level=PARANOID\n  //https://stackoverflow.com/questions/39192528/how-can-you-send-information-to-the-windows-task-bar-from-java-o-javafx\n  public static void main(String[] args) {\n    //get free port\n    doCheck();\n    launch(args);\n  }\n}\n"
  },
  {
    "path": "main/src/main/java/org/pdown/gui/com/Browser.java",
    "content": "package org.pdown.gui.com;\n\nimport javafx.geometry.HPos;\nimport javafx.geometry.VPos;\nimport javafx.scene.control.ContextMenu;\nimport javafx.scene.input.Clipboard;\nimport javafx.scene.input.ClipboardContent;\nimport javafx.scene.input.DataFormat;\nimport javafx.scene.input.MouseButton;\nimport javafx.scene.layout.Region;\nimport javafx.scene.web.WebEngine;\nimport javafx.scene.web.WebView;\nimport org.pdown.gui.util.I18nUtil;\n\npublic class Browser extends Region {\n\n  final WebView webView = new WebView();\n  final WebEngine webEngine = webView.getEngine();\n  private javafx.scene.control.MenuItem copy;\n  private javafx.scene.control.MenuItem paste;\n\n  public Browser() {\n    getChildren().add(webView);\n    webView.setContextMenuEnabled(false);\n    //自定义webview右键菜单\n    final Clipboard clipboard = Clipboard.getSystemClipboard();\n    ContextMenu contextMenu = new ContextMenu();\n    copy = new javafx.scene.control.MenuItem();\n    copy.setOnAction(e -> {\n      ClipboardContent content = new ClipboardContent();\n      Object selection = webView.getEngine().executeScript(\"window.getSelection().toString()\");\n      if (selection != null) {\n        content.putString(selection.toString());\n        clipboard.setContent(content);\n      }\n    });\n    paste = new javafx.scene.control.MenuItem();\n    paste.setOnAction(e -> {\n      Object content = clipboard.getContent(DataFormat.PLAIN_TEXT);\n      if (content != null) {\n        webView.getEngine().executeScript(\"if(document.activeElement.selectionStart>=0){\"\n            + \"var value = document.activeElement.value;\"\n            + \"if(!value){\"\n            + \" document.activeElement.value='\" + content + \"';\"\n            + \"}else{\"\n            + \" document.activeElement.value=value.substring(0,document.activeElement.selectionStart)+'\" + content + \"'+value.substring(document.activeElement.selectionEnd);\"\n            + \"}\"\n            + \"var event = document.createEvent('Event');\"\n            + \"event.initEvent('input', true, true);\"\n            + \"document.activeElement.dispatchEvent(event);\"\n            + \"}\");\n      }\n    });\n    refreshText();\n    contextMenu.getItems().addAll(copy, paste);\n    webView.setOnMousePressed(e -> {\n      if (e.getButton() == MouseButton.SECONDARY) {\n        contextMenu.show(webView, e.getScreenX(), e.getScreenY());\n      } else {\n        contextMenu.hide();\n      }\n    });\n  }\n\n  @Override\n  protected void layoutChildren() {\n    double w = getWidth();\n    double h = getHeight();\n    layoutInArea(webView, 0, 0, w, h, 0, HPos.CENTER, VPos.CENTER);\n  }\n\n  public void load(String url) {\n    webEngine.load(url);\n  }\n\n  public boolean isLoad() {\n    return webEngine.getLocation() != null;\n  }\n\n  public void refreshText() {\n    copy.setText(I18nUtil.getMessage(\"gui.menu.copy\"));\n    paste.setText(I18nUtil.getMessage(\"gui.menu.paste\"));\n  }\n}\n"
  },
  {
    "path": "main/src/main/java/org/pdown/gui/com/CheckboxMenuItemGroup.java",
    "content": "package org.pdown.gui.com;\n\nimport java.awt.CheckboxMenuItem;\nimport java.awt.event.ItemEvent;\nimport java.awt.event.ItemListener;\nimport java.util.HashSet;\nimport java.util.Set;\n\npublic class CheckboxMenuItemGroup implements ItemListener {\n\n  private Set<CheckboxMenuItem> items = new HashSet<>();\n  private ItemListener itemListener;\n\n  public void add(CheckboxMenuItem cbmi) {\n    cbmi.addItemListener(this);\n    cbmi.setState(false);\n    items.add(cbmi);\n  }\n\n  public void addActionListener(ItemListener itemListener) {\n    this.itemListener = itemListener;\n  }\n\n  @Override\n  public void itemStateChanged(ItemEvent e) {\n    CheckboxMenuItem checkedItem = ((CheckboxMenuItem) e.getSource());\n    if (e.getStateChange() == ItemEvent.SELECTED) {\n      String selectedItemName = checkedItem.getName();\n      for (CheckboxMenuItem item : items) {\n        if (!item.getName().equals(selectedItemName)) {\n          item.setState(false);\n        }\n      }\n      if (itemListener != null) {\n        itemListener.itemStateChanged(e);\n      }\n    } else {\n      checkedItem.setState(true);\n    }\n  }\n\n  public void selectItem(CheckboxMenuItem itemToSelect) {\n    for (CheckboxMenuItem item : items) {\n      item.setState(item == itemToSelect);\n    }\n  }\n\n  public CheckboxMenuItem getSelectedItem() {\n    for (CheckboxMenuItem item : items) {\n      if (item.getState()) {\n        return item;\n      }\n    }\n    return null;\n  }\n}\n"
  },
  {
    "path": "main/src/main/java/org/pdown/gui/com/Components.java",
    "content": "package org.pdown.gui.com;\n\nimport java.io.File;\nimport javafx.geometry.Insets;\nimport javafx.scene.Group;\nimport javafx.scene.Scene;\nimport javafx.scene.control.Alert;\nimport javafx.scene.control.Alert.AlertType;\nimport javafx.scene.control.ButtonBase;\nimport javafx.scene.control.ButtonType;\nimport javafx.scene.control.DialogPane;\nimport javafx.stage.DirectoryChooser;\nimport javafx.stage.FileChooser;\nimport javafx.stage.Modality;\nimport javafx.stage.Stage;\nimport javafx.stage.StageStyle;\n\npublic class Components {\n\n  /**\n   * 弹出提示窗，窗口置顶\n   */\n  public static void alert(String msg) {\n    Alert alert = new Alert(AlertType.INFORMATION);\n//    alert.setTitle(\"提示\");\n    alert.setHeaderText(null);\n    alert.setContentText(msg);\n\n    DialogPane root = alert.getDialogPane();\n    Stage dialogStage = new Stage();\n\n    for (ButtonType buttonType : root.getButtonTypes()) {\n      ButtonBase button = (ButtonBase) root.lookupButton(buttonType);\n      button.setOnAction(evt -> dialogStage.close());\n    }\n\n    root.getScene().setRoot(new Group());\n    root.setPadding(new Insets(10, 0, 10, 0));\n\n    Scene scene = new Scene(root);\n    dialogStage.setScene(scene);\n    dialogStage.initModality(Modality.APPLICATION_MODAL);\n    dialogStage.setAlwaysOnTop(true);\n    dialogStage.setResizable(false);\n    dialogStage.showAndWait();\n  }\n\n  /**\n   * 弹出文件选择框\n   */\n  public static File fileChooser() {\n    Stage stage = buildBackgroundTopStage();\n    FileChooser chooser = new FileChooser();\n    chooser.setTitle(\"选择文件\");\n    File file = chooser.showOpenDialog(stage);\n    stage.close();\n    return file;\n  }\n\n  /**\n   * 弹出文件夹选择框\n   */\n  public static File dirChooser() {\n    Stage stage = buildBackgroundTopStage();\n    DirectoryChooser chooser = new DirectoryChooser();\n    chooser.setTitle(\"选择文件夹\");\n    File file = chooser.showDialog(stage);\n    stage.close();\n    return file;\n  }\n\n  private static Stage buildBackgroundTopStage() {\n    Stage stage = new Stage();\n    stage.setAlwaysOnTop(true);\n    stage.setWidth(1);\n    stage.setHeight(1);\n    stage.initStyle(StageStyle.UNDECORATED);\n    stage.show();\n    return stage;\n  }\n}\n"
  },
  {
    "path": "main/src/main/java/org/pdown/gui/content/PDownConfigContent.java",
    "content": "package org.pdown.gui.content;\n\nimport com.fasterxml.jackson.core.type.TypeReference;\nimport java.io.File;\nimport java.nio.file.FileSystem;\nimport java.nio.file.FileSystems;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Locale;\nimport org.pdown.gui.entity.PDownConfigInfo;\nimport org.pdown.rest.base.content.PersistenceContent;\nimport org.pdown.rest.util.PathUtil;\n\npublic class PDownConfigContent extends PersistenceContent<PDownConfigInfo, PDownConfigContent> {\n\n  private static final PDownConfigContent INSTANCE = new PDownConfigContent();\n\n  public static PDownConfigContent getInstance() {\n    return INSTANCE;\n  }\n\n  @Override\n  protected TypeReference type() {\n    return new TypeReference<PDownConfigInfo>() {\n    };\n  }\n\n  @Override\n  protected String savePath() {\n    return PathUtil.ROOT_PATH + File.separator + \"pdown.cfg\";\n  }\n\n  @Override\n  protected PDownConfigInfo defaultValue() {\n    PDownConfigInfo pDownConfigInfo = new PDownConfigInfo();\n    //取系统默认语言\n    Locale defaultLocale = Locale.getDefault();\n    pDownConfigInfo.setLocale(defaultLocale.getLanguage() + \"-\" + defaultLocale.getCountry());\n    //插件文件服务器\n    List<String> extFileServers = new ArrayList<>();\n    extFileServers.add(\"https://github.com/proxyee-down-org/proxyee-down-extension/raw/master\");\n    extFileServers.add(\"http://static.pdown.org/extensions\");\n    pDownConfigInfo.setExtFileServers(extFileServers);\n    return pDownConfigInfo;\n  }\n}\n"
  },
  {
    "path": "main/src/main/java/org/pdown/gui/entity/PDownConfigInfo.java",
    "content": "package org.pdown.gui.entity;\n\nimport java.io.Serializable;\nimport java.util.List;\nimport org.pdown.core.proxy.ProxyConfig;\n\npublic class PDownConfigInfo implements Serializable {\n\n  private static final long serialVersionUID = 250452934883002540L;\n  //客户端语言\n  private String locale;\n  //UI模式 0.浏览器模式 1.GUI模式\n  private int uiMode = 1;\n  //代理模式 0.不接管系统代理 1.由pdown接管系统代理\n  private int proxyMode;\n  //插件文件服务器(用于下载插件相关文件)\n  private List<String> extFileServers;\n  //检测更新频率 0.从不 1.一周检查一次 2.每次打开检查\n  private int updateCheckRate = 2;\n  //启动时是否自动打开窗口\n  private boolean autoOpen = true;\n  //最后一次检查更新时间\n  private long lastUpdateCheck;\n  //前置代理\n  private ProxyConfig proxyConfig;\n\n  public String getLocale() {\n    return locale;\n  }\n\n  public PDownConfigInfo setLocale(String locale) {\n    this.locale = locale;\n    return this;\n  }\n\n  public int getUiMode() {\n    return uiMode;\n  }\n\n  public PDownConfigInfo setUiMode(int uiMode) {\n    this.uiMode = uiMode;\n    return this;\n  }\n\n  public int getProxyMode() {\n    return proxyMode;\n  }\n\n  public PDownConfigInfo setProxyMode(int proxyMode) {\n    this.proxyMode = proxyMode;\n    return this;\n  }\n\n  public List<String> getExtFileServers() {\n    return extFileServers;\n  }\n\n  public PDownConfigInfo setExtFileServers(List<String> extFileServers) {\n    this.extFileServers = extFileServers;\n    return this;\n  }\n\n  public int getUpdateCheckRate() {\n    return updateCheckRate;\n  }\n\n  public PDownConfigInfo setUpdateCheckRate(int updateCheckRate) {\n    this.updateCheckRate = updateCheckRate;\n    return this;\n  }\n\n  public long getLastUpdateCheck() {\n    return lastUpdateCheck;\n  }\n\n  public PDownConfigInfo setLastUpdateCheck(long lastUpdateCheck) {\n    this.lastUpdateCheck = lastUpdateCheck;\n    return this;\n  }\n\n  public ProxyConfig getProxyConfig() {\n    return proxyConfig;\n  }\n\n  public PDownConfigInfo setProxyConfig(ProxyConfig proxyConfig) {\n    this.proxyConfig = proxyConfig;\n    return this;\n  }\n\n  public boolean isAutoOpen() {\n    return autoOpen;\n  }\n\n  public PDownConfigInfo setAutoOpen(boolean autoOpen) {\n    this.autoOpen = autoOpen;\n    return this;\n  }\n\n  public static com.github.monkeywie.proxyee.proxy.ProxyConfig convert(ProxyConfig proxyConfig) {\n    if (proxyConfig == null) {\n      return null;\n    }\n    return new com.github.monkeywie.proxyee.proxy.ProxyConfig(\n        com.github.monkeywie.proxyee.proxy.ProxyType.valueOf(proxyConfig.getProxyType().name()),\n        proxyConfig.getHost(),\n        proxyConfig.getPort(),\n        proxyConfig.getUser(),\n        proxyConfig.getPwd());\n  }\n}\n"
  },
  {
    "path": "main/src/main/java/org/pdown/gui/extension/ContentScript.java",
    "content": "package org.pdown.gui.extension;\n\nimport java.util.Arrays;\n\npublic class ContentScript {\n\n  private String[] matches;\n  private String[] scripts;\n\n  public String[] getMatches() {\n    return matches;\n  }\n\n  public ContentScript setMatches(String[] matches) {\n    this.matches = matches;\n    return this;\n  }\n\n  public String[] getScripts() {\n    return scripts;\n  }\n\n  public ContentScript setScripts(String[] scripts) {\n    this.scripts = scripts;\n    return this;\n  }\n\n  public boolean isMatch(String url) {\n    if (matches != null\n        && Arrays.stream(matches).anyMatch(m -> url.matches(m))) {\n      return true;\n    }\n    return false;\n  }\n}\n"
  },
  {
    "path": "main/src/main/java/org/pdown/gui/extension/ExtensionConfig.java",
    "content": "package org.pdown.gui.extension;\n\nimport java.util.List;\n\npublic class ExtensionConfig {\n\n  //本地加载的扩展\n  private List<String> localExtensions;\n\n  public List<String> getLocalExtensions() {\n    return localExtensions;\n  }\n\n  public void setLocalExtensions(List<String> localExtensions) {\n    this.localExtensions = localExtensions;\n  }\n}\n"
  },
  {
    "path": "main/src/main/java/org/pdown/gui/extension/ExtensionContent.java",
    "content": "package org.pdown.gui.extension;\n\nimport com.fasterxml.jackson.databind.DeserializationFeature;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.IOException;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Set;\nimport org.pdown.core.util.FileUtil;\nimport org.pdown.rest.util.ContentUtil;\nimport org.pdown.rest.util.PathUtil;\n\npublic class ExtensionContent {\n\n  public static final String EXT_DIR = PathUtil.ROOT_PATH + File.separator + \"extensions\";\n  public static final String EXT_DIR_CONFIG = EXT_DIR + File.separator + \"ext.cfg\";\n  private static final String EXT_MANIFEST = \"manifest.json\";\n\n  private static List<ExtensionInfo> EXTENSION_INFO_LIST;\n  //代理服务器域名通配符列表\n  private static Set<String> PROXY_WILDCARDS;\n  //需要嗅探下载的url正则表达式列表\n  private static Set<String> SNIFF_REGEXS;\n  //配置\n  private static ExtensionConfig CONFIG;\n\n  public static void load() throws IOException {\n    File file = new File(EXT_DIR);\n    if (EXTENSION_INFO_LIST == null) {\n      EXTENSION_INFO_LIST = new ArrayList<>();\n    } else {\n      EXTENSION_INFO_LIST.clear();\n    }\n    if (file.exists() && file.isDirectory()) {\n      //加载所有已安装的扩展\n      for (File extendDir : file.listFiles()) {\n        if (extendDir.isDirectory()) {\n          //读取manifest.json\n          ExtensionInfo extensionInfo = parseExtensionDir(extendDir);\n          if (extensionInfo != null) {\n            EXTENSION_INFO_LIST.add(extensionInfo);\n          }\n        }\n      }\n    }\n    //加载本地安装的扩展\n    try {\n      CONFIG = ContentUtil.get(EXT_DIR_CONFIG, ExtensionConfig.class);\n    } catch (Exception e) {\n    }\n    if (CONFIG == null) {\n      CONFIG = new ExtensionConfig();\n    } else if (CONFIG.getLocalExtensions() != null) {\n      for (String localExtendDir : CONFIG.getLocalExtensions()) {\n        File extendDir = new File(localExtendDir);\n        if (extendDir.isDirectory()) {\n          //读取manifest.json\n          ExtensionInfo extensionInfo = parseExtensionDir(extendDir, true);\n          if (extensionInfo != null) {\n            EXTENSION_INFO_LIST.add(extensionInfo);\n          }\n        }\n      }\n    }\n    refresh();\n  }\n\n  public static ExtensionConfig getConfig() {\n    return CONFIG;\n  }\n\n  public synchronized static void saveConfig() throws IOException {\n    ContentUtil.save(CONFIG, EXT_DIR_CONFIG);\n  }\n\n  public synchronized static ExtensionInfo refresh(String path, boolean isLocal) throws IOException {\n    ExtensionInfo loadExt = parseExtensionDir(new File((isLocal ? \"\" : EXT_DIR) + path), isLocal);\n    if (loadExt != null && EXTENSION_INFO_LIST != null && path != null) {\n      boolean match = false;\n      for (int i = 0; i < EXTENSION_INFO_LIST.size(); i++) {\n        ExtensionInfo extensionInfo = EXTENSION_INFO_LIST.get(i);\n        if (loadExt.getMeta().getPath().equals(extensionInfo.getMeta().getPath())\n            && loadExt.getMeta().isLocal() == extensionInfo.getMeta().isLocal()) {\n          match = true;\n          EXTENSION_INFO_LIST.set(i, loadExt);\n          break;\n        }\n      }\n      if (!match) {\n        EXTENSION_INFO_LIST.add(loadExt);\n        if (isLocal) {\n          //保存文件\n          if (CONFIG.getLocalExtensions() == null) {\n            CONFIG.setLocalExtensions(new ArrayList<>());\n          }\n          CONFIG.getLocalExtensions().add(path);\n          ExtensionContent.saveConfig();\n        }\n      }\n      refresh();\n    }\n    return loadExt;\n  }\n\n  public synchronized static ExtensionInfo refresh(String path) throws IOException {\n    return refresh(path, false);\n  }\n\n  public synchronized static void remove(String path, boolean isLocal) throws IOException {\n    if (EXTENSION_INFO_LIST != null && path != null) {\n      for (int i = 0; i < EXTENSION_INFO_LIST.size(); i++) {\n        ExtensionInfo extensionInfo = EXTENSION_INFO_LIST.get(i);\n        if (path.equals(extensionInfo.getMeta().getPath())\n            && extensionInfo.getMeta().isLocal() == isLocal) {\n          EXTENSION_INFO_LIST.remove(i);\n          if (!extensionInfo.getMeta().isLocal()) {\n            FileUtil.deleteIfExists(extensionInfo.getMeta().getFullPath());\n          } else {\n            //保存文件\n            if (CONFIG.getLocalExtensions() != null) {\n              CONFIG.getLocalExtensions().remove(extensionInfo.getMeta().getFullPath());\n              ExtensionContent.saveConfig();\n            }\n          }\n          break;\n        }\n      }\n      refresh();\n    }\n  }\n\n  public synchronized static void refresh() {\n    if (PROXY_WILDCARDS == null) {\n      PROXY_WILDCARDS = new HashSet<>();\n    } else {\n      PROXY_WILDCARDS.clear();\n    }\n    if (SNIFF_REGEXS == null) {\n      SNIFF_REGEXS = new HashSet<>();\n    } else {\n      SNIFF_REGEXS.clear();\n    }\n    if (EXTENSION_INFO_LIST != null) {\n      for (ExtensionInfo extensionInfo : EXTENSION_INFO_LIST) {\n        if (extensionInfo.getMeta().isEnabled()) {\n          //读取需要代理的域名匹配符\n          if (extensionInfo.getProxyWildcards() != null) {\n            for (String wildcard : extensionInfo.getProxyWildcards()) {\n              PROXY_WILDCARDS.add(wildcard.trim());\n            }\n          }\n          //读取需要嗅探下载的url正则表达式\n          if (extensionInfo.getSniffRegexs() != null) {\n            for (String regex : extensionInfo.getSniffRegexs()) {\n              SNIFF_REGEXS.add(regex.trim());\n            }\n          }\n        }\n      }\n    }\n  }\n\n  private static ExtensionInfo parseExtensionDir(File extendDir, boolean isLocal) {\n    ExtensionInfo extensionInfo = null;\n    ObjectMapper objectMapper = new ObjectMapper();\n    objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);\n    try {\n      extensionInfo = objectMapper.readValue(new FileInputStream(extendDir + File.separator + EXT_MANIFEST), ExtensionInfo.class);\n    } catch (IOException e) {\n    }\n    if (extensionInfo != null) {\n      Meta meta = Meta.load(extendDir.getPath());\n      meta.setLocal(isLocal);\n      //如果没有设置则生成默认设置信息\n      if (extensionInfo.getSettings() != null\n          && extensionInfo.getSettings().size() > 0) {\n        if (meta.getSettings() == null) {\n          meta.setSettings(new HashMap<>());\n        }\n        if (meta.getSettings().size() == 0) {\n          for (Setting setting : extensionInfo.getSettings()) {\n            meta.getSettings().put(setting.getName(), setting.getValue());\n          }\n        }\n      }\n      extensionInfo.setMeta(meta);\n    }\n    return extensionInfo;\n  }\n\n  private static ExtensionInfo parseExtensionDir(File extendDir) {\n    return parseExtensionDir(extendDir, false);\n  }\n\n  public static List<ExtensionInfo> get() {\n    return EXTENSION_INFO_LIST;\n  }\n\n  public static Set<String> getProxyWildCards() {\n    return PROXY_WILDCARDS;\n  }\n\n  public static Set<String> getSniffRegexs() {\n    return SNIFF_REGEXS;\n  }\n}\n"
  },
  {
    "path": "main/src/main/java/org/pdown/gui/extension/ExtensionInfo.java",
    "content": "package org.pdown.gui.extension;\n\nimport java.util.List;\n\npublic class ExtensionInfo {\n\n  private String title; //扩展名称\n  private double version; //扩展版本号\n  private String homepage; //扩展主页\n  private String description; //扩展描述\n  private List<String> proxyWildcards;  //扩展生效配置的域名通配符列表\n  private List<String> sniffRegexs;  //扩展嗅探下载的url正则表达式列表\n  private List<ContentScript> contentScripts;\n  private HookScript hookScript;  //下载状态变更时触发的钩子函数脚本\n  private List<Setting> settings; //扩展设置选项\n  private Meta meta;\n\n  public String getTitle() {\n    return title;\n  }\n\n  public ExtensionInfo setTitle(String title) {\n    this.title = title;\n    return this;\n  }\n\n  public double getVersion() {\n    return version;\n  }\n\n  public ExtensionInfo setVersion(double version) {\n    this.version = version;\n    return this;\n  }\n\n  public String getHomepage() {\n    return homepage;\n  }\n\n  public ExtensionInfo setHomepage(String homepage) {\n    this.homepage = homepage;\n    return this;\n  }\n\n  public String getDescription() {\n    return description;\n  }\n\n  public ExtensionInfo setDescription(String description) {\n    this.description = description;\n    return this;\n  }\n\n  public List<String> getProxyWildcards() {\n    return proxyWildcards;\n  }\n\n  public ExtensionInfo setProxyWildcards(List<String> proxyWildcards) {\n    this.proxyWildcards = proxyWildcards;\n    return this;\n  }\n\n  public List<String> getSniffRegexs() {\n    return sniffRegexs;\n  }\n\n  public ExtensionInfo setSniffRegexs(List<String> sniffRegexs) {\n    this.sniffRegexs = sniffRegexs;\n    return this;\n  }\n\n  public List<ContentScript> getContentScripts() {\n    return contentScripts;\n  }\n\n  public ExtensionInfo setContentScripts(List<ContentScript> contentScripts) {\n    this.contentScripts = contentScripts;\n    return this;\n  }\n\n  public HookScript getHookScript() {\n    return hookScript;\n  }\n\n  public ExtensionInfo setHookScript(HookScript hookScript) {\n    this.hookScript = hookScript;\n    return this;\n  }\n\n  public List<Setting> getSettings() {\n    return settings;\n  }\n\n  public ExtensionInfo setSettings(List<Setting> settings) {\n    this.settings = settings;\n    return this;\n  }\n\n  public Meta getMeta() {\n    return meta;\n  }\n\n  public ExtensionInfo setMeta(Meta meta) {\n    this.meta = meta;\n    return this;\n  }\n}\n"
  },
  {
    "path": "main/src/main/java/org/pdown/gui/extension/HookScript.java",
    "content": "package org.pdown.gui.extension;\n\nimport java.util.Arrays;\n\npublic class HookScript {\n\n  public static final String EVENT_RESOLVE = \"resolve\";\n  public static final String EVENT_START = \"start\";\n  public static final String EVENT_RESUME = \"resume\";\n  public static final String EVENT_PAUSE = \"pause\";\n  public static final String EVENT_ERROR = \"error\";\n  public static final String EVENT_DONE = \"done\";\n  public static final String EVENT_DELETE = \"delete\";\n\n  private Event[] events;\n  private String script;\n\n  public Event[] getEvents() {\n    return events;\n  }\n\n  public HookScript setEvents(Event[] events) {\n    this.events = events;\n    return this;\n  }\n\n  public String getScript() {\n    return script;\n  }\n\n  public HookScript setScript(String script) {\n    this.script = script;\n    return this;\n  }\n\n  /**\n   * 判断扩展是否有注册钩子函数\n   */\n  public Event hasEvent(String event, String url) {\n    String matchUrl = url != null ? url.replaceAll(\"^(?i)(https?://)\", \"\") : \"\";\n    if (events != null) {\n      return Arrays.stream(events)\n          .filter(e -> event.equalsIgnoreCase(e.getOn()) && (e.getMatches() == null || (Arrays.stream(e.getMatches()).anyMatch(m -> matchUrl.matches(m)))))\n          .findFirst()\n          .orElse(null);\n    }\n    return null;\n  }\n\n  public static class Event {\n\n    private String on;\n    private String[] matches;\n    private String method;\n\n    public String getOn() {\n      return on;\n    }\n\n    public Event setOn(String on) {\n      this.on = on;\n      return this;\n    }\n\n    public String[] getMatches() {\n      return matches;\n    }\n\n    public Event setMatches(String[] matches) {\n      this.matches = matches;\n      return this;\n    }\n\n    public String getMethod() {\n      return method;\n    }\n\n    public Event setMethod(String method) {\n      this.method = method;\n      return this;\n    }\n  }\n}\n"
  },
  {
    "path": "main/src/main/java/org/pdown/gui/extension/Meta.java",
    "content": "package org.pdown.gui.extension;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.util.Map;\nimport org.pdown.rest.util.ContentUtil;\n\npublic class Meta {\n\n  public transient static final String CONFIG_FILE = \".ext_data/.config.dat\";\n\n  private transient String path;\n  private transient String fullPath;\n  private boolean enabled = true;\n  private boolean local = true;\n  private Map<String, Object> settings;\n  private Map<String, Object> data;\n\n  public String getPath() {\n    return path;\n  }\n\n  public Meta setPath(String path) {\n    this.path = path;\n    return this;\n  }\n\n  public String getFullPath() {\n    return fullPath;\n  }\n\n  public Meta setFullPath(String fullPath) {\n    this.fullPath = fullPath;\n    return this;\n  }\n\n  public boolean isEnabled() {\n    return enabled;\n  }\n\n  public Meta setEnabled(boolean enabled) {\n    this.enabled = enabled;\n    return this;\n  }\n\n  public Map<String, Object> getSettings() {\n    return settings;\n  }\n\n  public Meta setSettings(Map<String, Object> settings) {\n    this.settings = settings;\n    return this;\n  }\n\n  public Map<String, Object> getData() {\n    return data;\n  }\n\n  public Meta setData(Map<String, Object> data) {\n    this.data = data;\n    return this;\n  }\n\n  public boolean isLocal() {\n    return local;\n  }\n\n  public void setLocal(boolean local) {\n    this.local = local;\n  }\n\n  public void save() {\n    try {\n      ContentUtil.save(this, getFullPath() + File.separator + CONFIG_FILE, true);\n    } catch (IOException e) {\n    }\n  }\n\n  public static Meta load(String path) {\n    Meta meta = null;\n    try {\n      meta = ContentUtil.get(path + File.separator + CONFIG_FILE, Meta.class);\n    } catch (IOException e) {\n    }\n    if (meta == null) {\n      meta = new Meta();\n    }\n    meta.setPath(\"/\" + new File(path).getName());\n    meta.setFullPath(path);\n    return meta;\n  }\n}\n"
  },
  {
    "path": "main/src/main/java/org/pdown/gui/extension/Setting.java",
    "content": "package org.pdown.gui.extension;\n\nimport java.util.Map;\n\npublic class Setting {\n\n  private String name;\n  private String title;\n  private String type;\n  private Object value;\n  private String description;\n  private boolean isMultiple;\n  private Map<String, Object> options;\n\n  public String getName() {\n    return name;\n  }\n\n  public Setting setName(String name) {\n    this.name = name;\n    return this;\n  }\n\n  public String getTitle() {\n    return title;\n  }\n\n  public Setting setTitle(String title) {\n    this.title = title;\n    return this;\n  }\n\n  public String getType() {\n    return type;\n  }\n\n  public Setting setType(String type) {\n    this.type = type;\n    return this;\n  }\n\n  public Object getValue() {\n    return value;\n  }\n\n  public Setting setValue(Object value) {\n    this.value = value;\n    return this;\n  }\n\n  public String getDescription() {\n    return description;\n  }\n\n  public Setting setDescription(String description) {\n    this.description = description;\n    return this;\n  }\n\n  public boolean isMultiple() {\n    return isMultiple;\n  }\n\n  public Setting setMultiple(boolean multiple) {\n    isMultiple = multiple;\n    return this;\n  }\n\n  public Map<String, Object> getOptions() {\n    return options;\n  }\n\n  public Setting setOptions(Map<String, Object> options) {\n    this.options = options;\n    return this;\n  }\n}\n"
  },
  {
    "path": "main/src/main/java/org/pdown/gui/extension/jsruntime/JavascriptEngine.java",
    "content": "package org.pdown.gui.extension.jsruntime;\n\nimport com.fasterxml.jackson.core.JsonProcessingException;\nimport javax.script.Invocable;\nimport javax.script.ScriptContext;\nimport javax.script.ScriptEngine;\nimport javax.script.ScriptEngineManager;\nimport javax.script.ScriptException;\nimport javax.script.SimpleScriptContext;\nimport jdk.nashorn.api.scripting.ClassFilter;\nimport jdk.nashorn.api.scripting.NashornScriptEngineFactory;\nimport org.pdown.gui.extension.jsruntime.polyfill.Window;\nimport org.pdown.rest.form.HttpRequestForm;\n\npublic class JavascriptEngine {\n\n  public static ScriptEngine buildEngine() throws ScriptException, NoSuchMethodException {\n    NashornScriptEngineFactory factory = new NashornScriptEngineFactory();\n    ScriptEngine engine = factory.getScriptEngine(new SafeClassFilter());\n    Window window = new Window();\n    Object global = engine.eval(\"this\");\n    Object jsObject = engine.eval(\"Object\");\n    Invocable invocable = (Invocable) engine;\n    invocable.invokeMethod(jsObject, \"bindProperties\", global, window);\n    engine.eval(\"var window = this\");\n    return engine;\n  }\n\n  /**\n   * 禁止任何显式调用java代码\n   */\n  private static class SafeClassFilter implements ClassFilter {\n\n    @Override\n    public boolean exposeToScripts(String s) {\n      return false;\n    }\n  }\n\n  public static void main(String[] args) throws ScriptException, NoSuchMethodException, JsonProcessingException, InterruptedException {\n    ScriptEngine engine = buildEngine();\n    Invocable invocable = (Invocable) engine;\n    engine.eval(\"load('E:/study/extensions/bilibili-helper/dist/hook.js')\");\n    HttpRequestForm requestForm = new HttpRequestForm();\n    requestForm.setUrl(\"https://www.bilibili.com/video/av34765642\");\n    Object result = invocable.invokeFunction(\"error\");\n    ScriptContext ctx = new SimpleScriptContext();\n    ctx.setAttribute(\"result\", result, ScriptContext.ENGINE_SCOPE);\n    System.out.println(engine.eval(\"!!result&&typeof result=='object'&&typeof result.then=='function'\", ctx));\n  }\n\n}\n"
  },
  {
    "path": "main/src/main/java/org/pdown/gui/extension/jsruntime/polyfill/Window.java",
    "content": "package org.pdown.gui.extension.jsruntime.polyfill;\n\nimport java.util.concurrent.TimeUnit;\nimport java.util.concurrent.atomic.AtomicLong;\nimport java.util.function.Function;\nimport jdk.internal.dynalink.beans.StaticClass;\nimport org.pdown.gui.extension.jsruntime.polyfill.property.Console;\nimport org.pdown.gui.extension.jsruntime.polyfill.property.Document;\n\npublic class Window {\n\n  private static final String TIMEOUT_THREAD_NAME = \"babel4j-timeout-\";\n  private static final String INTERVAL_THREAD_NAME = \"babel4j-interval-\";\n  private static final AtomicLong THREAD_ID = new AtomicLong(0);\n\n  public Console console = new Console();\n  public Document document = new Document();\n  public StaticClass XMLHttpRequest = StaticClass.forClass(org.pdown.gui.extension.jsruntime.polyfill.property.XMLHttpRequest.class);\n\n  public long setTimeout(Function function, long timeout) {\n    Long id = THREAD_ID.addAndGet(1);\n    new Thread(() -> {\n      try {\n        TimeUnit.MILLISECONDS.sleep(timeout);\n        function.apply(null);\n      } catch (InterruptedException e) {\n      }\n    }, TIMEOUT_THREAD_NAME + id).start();\n    return id;\n  }\n\n  public void clearTimeout(Long id) {\n    Thread temp = Thread.getAllStackTraces().keySet().stream()\n        .filter(thread -> (TIMEOUT_THREAD_NAME + id).equals(thread.getName()))\n        .findFirst()\n        .orElse(null);\n    if (temp != null) {\n      temp.interrupt();\n    }\n  }\n\n  public long setInterval(Function function, long timeout) {\n    Long id = THREAD_ID.addAndGet(1);\n    new Thread(() -> {\n      try {\n        while (true) {\n          TimeUnit.MILLISECONDS.sleep(timeout);\n          function.apply(null);\n        }\n      } catch (InterruptedException e) {\n      }\n    }, INTERVAL_THREAD_NAME + id).start();\n    return id;\n  }\n\n  public void clearInterval(Long id) {\n    Thread temp = Thread.getAllStackTraces().keySet().stream()\n        .filter(thread -> (INTERVAL_THREAD_NAME + id).equals(thread.getName()))\n        .findFirst()\n        .orElse(null);\n    if (temp != null) {\n      temp.interrupt();\n    }\n  }\n}\n"
  },
  {
    "path": "main/src/main/java/org/pdown/gui/extension/jsruntime/polyfill/property/Console.java",
    "content": "package org.pdown.gui.extension.jsruntime.polyfill.property;\n\npublic class Console {\n\n  public void log(Object object) {\n    System.out.println(object);\n  }\n\n  public void debug(Object object) {\n    System.out.println(object);\n  }\n\n  public void error(Object object) {\n    if (object instanceof Throwable) {\n      Throwable throwable = (Throwable) object;\n      throwable.printStackTrace();\n    } else {\n      System.out.println(object);\n    }\n  }\n\n  public void error(Object msg, Object throwable) {\n    if (throwable instanceof Throwable) {\n      ((Throwable) throwable).printStackTrace();\n    } else {\n      System.out.println(msg);\n    }\n  }\n}\n"
  },
  {
    "path": "main/src/main/java/org/pdown/gui/extension/jsruntime/polyfill/property/Document.java",
    "content": "package org.pdown.gui.extension.jsruntime.polyfill.property;\n\npublic class Document {\n\n  private String cookie;\n\n  public String getCookie() {\n    return cookie;\n  }\n\n  public void setCookie(String cookie) {\n    this.cookie = cookie;\n  }\n}\n"
  },
  {
    "path": "main/src/main/java/org/pdown/gui/extension/jsruntime/polyfill/property/XMLHttpRequest.java",
    "content": "package org.pdown.gui.extension.jsruntime.polyfill.property;\n\nimport java.io.BufferedReader;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.io.OutputStream;\nimport java.net.HttpURLConnection;\nimport java.net.InetSocketAddress;\nimport java.net.Proxy;\nimport java.net.Proxy.Type;\nimport java.net.URL;\nimport java.nio.charset.Charset;\nimport java.util.LinkedHashMap;\nimport java.util.Map;\nimport java.util.function.Function;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\nimport java.util.stream.Collectors;\nimport java.util.zip.GZIPInputStream;\nimport org.springframework.util.StringUtils;\n\npublic class XMLHttpRequest {\n\n  private String method;\n  private String url;\n\n  public Function onreadystatechange;\n  public int readyState = 0;\n  public int status = 0;\n  public String responseText;\n\n  private Map<String, String> customRequestHeads = new LinkedHashMap<>();\n  private Map<String, String> responseHeads = new LinkedHashMap<>();\n\n  public void setRequestHeader(String header, String value) {\n    customRequestHeads.put(header, value);\n  }\n\n  public String getResponseHeader(String header) {\n    return responseHeads.get(header.toLowerCase());\n  }\n\n  public void open(String method, String url) {\n    this.method = method;\n    this.url = url;\n  }\n\n  public void open(String method, String url, boolean async) {\n    this.open(method, url);\n  }\n\n  private static String DEFAULT_UA = \"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36\";\n\n  public void send(String data) throws IOException {\n    URL u = new URL(url);\n    HttpURLConnection connection = (HttpURLConnection) u.openConnection();\n    readystatechange(1);\n    connection.setRequestMethod(method.toUpperCase());\n    connection.setRequestProperty(\"User-Agent\", DEFAULT_UA);\n    customRequestHeads.entrySet().stream().forEach(entry -> connection.setRequestProperty(entry.getKey(), entry.getValue()));\n    connection.setDoOutput(true);\n    if (data != null && data.trim().length() > 0) {\n      try (\n          OutputStream outputStream = connection.getOutputStream()\n      ) {\n        outputStream.write(data.getBytes(Charset.forName(\"UTF-8\")));\n      }\n    }\n    int code = connection.getResponseCode();\n    connection.getHeaderFields().entrySet().forEach(entry -> {\n          if (entry.getKey() != null) {\n            responseHeads.put(entry.getKey().toLowerCase(), entry.getValue().stream().collect(Collectors.joining(\"; \")));\n          }\n        }\n    );\n    readystatechange(2, code);\n    String charset = \"UTF-8\";\n    String contentType = connection.getContentType();\n    if (!StringUtils.isEmpty(contentType)) {\n      Pattern pattern = Pattern.compile(\"charset=(.*)$\", Pattern.CASE_INSENSITIVE);\n      Matcher matcher = pattern.matcher(contentType);\n      if (matcher.find()) {\n        charset = matcher.group(1);\n      }\n    }\n    InputStream inputStream = code != 200 ? connection.getErrorStream() : connection.getInputStream();\n    if (responseHeads.entrySet().stream().anyMatch(entry -> \"Content-Encoding\".equalsIgnoreCase(entry.getKey()) && entry.getValue().matches(\"^.*(?i)(gzip).*$\"))) {\n      inputStream = new GZIPInputStream(inputStream);\n    }\n    try (\n        BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, charset))\n    ) {\n      readystatechange(3, code);\n      responseText = reader.lines().collect(Collectors.joining(\"\\n\"));\n      readystatechange(4, code);\n    }\n  }\n\n  public static void main(String[] args) throws IOException {\n    URL u = new URL(\"http://www.baidu.com\");\n    Proxy proxy = new Proxy(Type.SOCKS, new InetSocketAddress(\"127.0.0.1\", 1088));\n    HttpURLConnection connection = (HttpURLConnection) u.openConnection(proxy);\n    System.out.println(connection.getResponseCode());\n  }\n\n  public void send() throws IOException {\n    send(null);\n  }\n\n  private void readystatechange(int readyState, int status) {\n    this.readyState = readyState;\n    this.status = status;\n    if (onreadystatechange != null) {\n      onreadystatechange.apply(null);\n    }\n  }\n\n  private void readystatechange(int readyState) {\n    readystatechange(readyState, status);\n  }\n}\n"
  },
  {
    "path": "main/src/main/java/org/pdown/gui/extension/mitm/intercept/AjaxIntercept.java",
    "content": "package org.pdown.gui.extension.mitm.intercept;\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.github.monkeywie.proxyee.intercept.HttpProxyIntercept;\nimport com.github.monkeywie.proxyee.intercept.HttpProxyInterceptPipeline;\nimport io.netty.channel.Channel;\nimport io.netty.handler.codec.http.*;\nimport io.netty.util.AsciiString;\nimport org.springframework.util.StringUtils;\n\nimport java.io.ByteArrayOutputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.OutputStream;\nimport java.net.HttpURLConnection;\nimport java.net.URL;\nimport java.net.URLDecoder;\nimport java.util.Map;\n\n/**\n * 通过代理服务器代理ajax请求，避免浏览器CORS问题\n */\npublic class AjaxIntercept extends HttpProxyIntercept {\n\n    private static final String PROXY_SEND_KEY = \"X-Proxy-Send\";\n\n    private boolean proxyFlag;\n\n    @Override\n    public void beforeRequest(Channel clientChannel, HttpRequest httpRequest, HttpProxyInterceptPipeline pipeline) throws Exception {\n        proxyFlag = httpRequest.headers().contains(PROXY_SEND_KEY);\n        super.beforeRequest(clientChannel, httpRequest, pipeline);\n    }\n\n    @Override\n    public void afterResponse(Channel clientChannel, Channel proxyChannel, HttpResponse httpResponse, HttpProxyInterceptPipeline pipeline) throws Exception {\n        if (proxyFlag) {\n            httpResponse.setStatus(HttpResponseStatus.OK);\n            proxyChannel.close();\n            ObjectMapper objectMapper = new ObjectMapper();\n            LastHttpContent content = new DefaultLastHttpContent();\n            String proxyRequestRaw = URLDecoder.decode(pipeline.getHttpRequest().headers().get(PROXY_SEND_KEY), \"utf-8\");\n            try {\n                ProxyRequest proxyRequest = objectMapper.readValue(proxyRequestRaw, ProxyRequest.class);\n                ProxyResponse proxyResponse = doRequest(proxyRequest);\n                httpResponse.setStatus(HttpResponseStatus.valueOf(proxyResponse.getStatus()));\n                content.content().writeBytes(proxyResponse.getData());\n            } catch (IOException e) {\n                e.printStackTrace();\n                httpResponse.setStatus(HttpResponseStatus.SERVICE_UNAVAILABLE);\n            }\n            httpResponse.headers().remove(HttpHeaderNames.CONTENT_ENCODING);\n            httpResponse.headers().remove(HttpHeaderNames.TRANSFER_ENCODING);\n            httpResponse.headers().set(HttpHeaderNames.CONTENT_LENGTH, content.content().readableBytes());\n            httpResponse.headers().set(HttpHeaderNames.CONTENT_TYPE, AsciiString.cached(\"application/json; charset=utf-8\"));\n            super.afterResponse(clientChannel, proxyChannel, httpResponse, pipeline);\n            clientChannel.writeAndFlush(content);\n        } else {\n            super.afterResponse(clientChannel, proxyChannel, httpResponse, pipeline);\n        }\n    }\n\n    @Override\n    public void afterResponse(Channel clientChannel, Channel proxyChannel, HttpContent httpContent, HttpProxyInterceptPipeline pipeline) throws Exception {\n        if (!proxyFlag) {\n            super.afterResponse(clientChannel, proxyChannel, httpContent, pipeline);\n        }\n    }\n\n    private ProxyResponse doRequest(ProxyRequest proxyRequest) throws IOException {\n        URL url = new URL(proxyRequest.getUrl());\n        HttpURLConnection connection = (HttpURLConnection) url.openConnection();\n        connection.setRequestMethod(proxyRequest.getMethod().toUpperCase());\n        connection.setRequestProperty(\"Content-Type\", \"application/json; charset=utf-8\");\n        if (proxyRequest.getHeads() != null) {\n            for (Map.Entry<String, String> entry : proxyRequest.getHeads().entrySet()) {\n                connection.setRequestProperty(entry.getKey(), entry.getValue());\n            }\n        }\n        connection.setDoInput(true);\n        if (!StringUtils.isEmpty(proxyRequest.getRawData())) {\n            connection.setDoOutput(true);\n            try (\n                    OutputStream output = connection.getOutputStream()\n            ) {\n                output.write(proxyRequest.getRawData().getBytes());\n                output.flush();\n            }\n        } else if (proxyRequest.getData() != null) {\n            connection.setDoOutput(true);\n            try (\n                    OutputStream output = connection.getOutputStream()\n            ) {\n                ObjectMapper objectMapper = new ObjectMapper();\n                output.write(objectMapper.writeValueAsBytes(proxyRequest.getData()));\n                output.flush();\n            }\n        }\n        ProxyResponse proxyResponse = new ProxyResponse();\n        proxyResponse.setStatus(connection.getResponseCode());\n        try (\n                ByteArrayOutputStream output = new ByteArrayOutputStream();\n                InputStream input = connection.getResponseCode() == 200 ? connection.getInputStream() : connection.getErrorStream()\n        ) {\n            byte[] bts = new byte[8192];\n            int len;\n            while ((len = input.read(bts)) != -1) {\n                output.write(bts, 0, len);\n            }\n            proxyResponse.setData(output.toByteArray());\n            return proxyResponse;\n        }\n    }\n\n    static class ProxyRequest {\n\n        private String method;\n        private String url;\n        private Map<String, String> heads;\n        private Map<String, Object> data;\n        private String rawData;\n\n\n        public String getMethod() {\n            return method;\n        }\n\n        public void setMethod(String method) {\n            this.method = method;\n        }\n\n        public String getUrl() {\n            return url;\n        }\n\n        public void setUrl(String url) {\n            this.url = url;\n        }\n\n        public Map<String, String> getHeads() {\n            return heads;\n        }\n\n        public void setHeads(Map<String, String> heads) {\n            this.heads = heads;\n        }\n\n        public Map<String, Object> getData() {\n            return data;\n        }\n\n        public void setData(Map<String, Object> data) {\n            this.data = data;\n        }\n\n        public String getRawData() {\n            return rawData;\n        }\n\n        public void setRawData(String rawData) {\n            this.rawData = rawData;\n        }\n    }\n\n    static class ProxyResponse {\n\n        private int status;\n        private byte[] data;\n\n        public int getStatus() {\n            return status;\n        }\n\n        public void setStatus(int status) {\n            this.status = status;\n        }\n\n        public byte[] getData() {\n            return data;\n        }\n\n        public void setData(byte[] data) {\n            this.data = data;\n        }\n    }\n}\n"
  },
  {
    "path": "main/src/main/java/org/pdown/gui/extension/mitm/intercept/CookieIntercept.java",
    "content": "package org.pdown.gui.extension.mitm.intercept;\n\nimport com.github.monkeywie.proxyee.intercept.HttpProxyIntercept;\nimport com.github.monkeywie.proxyee.intercept.HttpProxyInterceptPipeline;\nimport io.netty.channel.Channel;\nimport io.netty.handler.codec.http.DefaultHttpHeaders;\nimport io.netty.handler.codec.http.DefaultHttpResponse;\nimport io.netty.handler.codec.http.DefaultLastHttpContent;\nimport io.netty.handler.codec.http.HttpHeaderNames;\nimport io.netty.handler.codec.http.HttpRequest;\nimport io.netty.handler.codec.http.HttpResponse;\nimport io.netty.handler.codec.http.HttpResponseStatus;\nimport io.netty.handler.codec.http.HttpVersion;\nimport io.netty.util.AsciiString;\nimport io.netty.util.internal.StringUtil;\nimport java.net.URL;\n\n/**\n * 嗅探目标网站cookie，支持HTTP only\n */\npublic class CookieIntercept extends HttpProxyIntercept {\n\n  @Override\n  public void beforeRequest(Channel clientChannel, HttpRequest httpRequest, HttpProxyInterceptPipeline pipeline) throws Exception {\n    String acceptValue = httpRequest.headers().get(HttpHeaderNames.ACCEPT);\n    if (acceptValue != null && acceptValue.contains(\"application/x-sniff-cookie\")) {\n      HttpResponse httpResponse = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, new DefaultHttpHeaders());\n      httpResponse.headers().set(HttpHeaderNames.CONTENT_LENGTH, 0);\n      //https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Access-Control-Expose-Headers\n      AsciiString customHeadKey = AsciiString.cached(\"X-Sniff-Cookie\");\n      String cookie = pipeline.getHttpRequest().headers().get(HttpHeaderNames.COOKIE);\n      httpResponse.headers().set(customHeadKey, cookie == null ? \"\" : cookie);\n      httpResponse.headers().set(HttpHeaderNames.ACCESS_CONTROL_EXPOSE_HEADERS, customHeadKey);\n      String origin = httpRequest.headers().get(HttpHeaderNames.ORIGIN);\n      if (StringUtil.isNullOrEmpty(origin)) {\n        String referer = httpRequest.headers().get(HttpHeaderNames.REFERER);\n        URL url = new URL(referer);\n        origin = url.getHost();\n      }\n      httpResponse.headers().set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_ORIGIN, origin);\n      httpResponse.headers().set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_CREDENTIALS, true);\n      clientChannel.writeAndFlush(httpResponse);\n      clientChannel.writeAndFlush(new DefaultLastHttpContent());\n      clientChannel.close();\n    } else {\n      super.beforeRequest(clientChannel, httpRequest, pipeline);\n    }\n  }\n}\n"
  },
  {
    "path": "main/src/main/java/org/pdown/gui/extension/mitm/intercept/ScriptIntercept.java",
    "content": "package org.pdown.gui.extension.mitm.intercept;\n\nimport com.github.monkeywie.proxyee.intercept.HttpProxyInterceptPipeline;\nimport com.github.monkeywie.proxyee.intercept.common.FullResponseIntercept;\nimport com.github.monkeywie.proxyee.util.ByteUtil;\nimport io.netty.handler.codec.http.FullHttpResponse;\nimport io.netty.handler.codec.http.HttpRequest;\nimport io.netty.handler.codec.http.HttpResponse;\nimport java.io.File;\nimport java.io.IOException;\nimport java.nio.charset.Charset;\nimport java.nio.file.Files;\nimport java.util.List;\nimport org.pdown.core.util.HttpDownUtil;\nimport org.pdown.gui.extension.ContentScript;\nimport org.pdown.gui.extension.ExtensionContent;\nimport org.pdown.gui.extension.ExtensionInfo;\nimport org.pdown.gui.extension.util.ExtensionUtil;\n\npublic class ScriptIntercept extends FullResponseIntercept {\n\n  @Override\n  public boolean match(HttpRequest httpRequest, HttpResponse httpResponse, HttpProxyInterceptPipeline pipeline) {\n    return isHtml(httpRequest, httpResponse);\n  }\n\n  private static final String INSERT_TOKEN = \"</head>\";\n  private static final String INIT_TEMPLATE = \";(function (pdown) {\\n\"\n      + \"  ${content}\\n\"\n      + \"})(${runtime})\";\n\n  private String readInsertTemplate(ExtensionInfo extensionInfo) {\n    String js = ExtensionUtil.readRuntimeTemplate(extensionInfo);\n    js = INIT_TEMPLATE.replace(\"${runtime}\", js);\n    js = \"<script type=\\\"text/javascript\\\">\\n\" + js + \"\\n</script>\";\n    return js;\n  }\n\n  @Override\n  public void handelResponse(HttpRequest httpRequest, FullHttpResponse httpResponse, HttpProxyInterceptPipeline pipeline) {\n    List<ExtensionInfo> extensionInfoList = ExtensionContent.get();\n    if (isEmpty(extensionInfoList)) {\n      return;\n    }\n    for (ExtensionInfo extensionInfo : extensionInfoList) {\n      if (isEmpty(extensionInfo.getContentScripts())) {\n        continue;\n      }\n      for (ContentScript contentScript : extensionInfo.getContentScripts()) {\n        //扩展注入正则表达式与当前访问的url匹配则注入脚本\n        String url = HttpDownUtil.getUrl(httpRequest);\n        if (contentScript.isMatch(url)) {\n          String apiTemplate = readInsertTemplate(extensionInfo);\n          StringBuilder scriptsBuilder = new StringBuilder();\n          for (String script : contentScript.getScripts()) {\n            File scriptFile = new File(extensionInfo.getMeta().getFullPath() + File.separator + script);\n            if (scriptFile.exists() && scriptFile.isFile()) {\n              try {\n                scriptsBuilder.append(new String(Files.readAllBytes(scriptFile.toPath()), \"UTF-8\"));\n              } catch (IOException e) {\n              }\n            }\n          }\n          apiTemplate = apiTemplate.replace(\"${content}\", scriptsBuilder.toString());\n          int index = ByteUtil.findText(httpResponse.content(), INSERT_TOKEN);\n          ByteUtil.insertText(httpResponse.content(), index == -1 ? 0 : index - INSERT_TOKEN.length(), apiTemplate, Charset.forName(\"UTF-8\"));\n        }\n      }\n    }\n  }\n\n  private boolean isEmpty(List list) {\n    return list == null || list.size() == 0;\n  }\n}\n"
  },
  {
    "path": "main/src/main/java/org/pdown/gui/extension/mitm/intercept/SniffIntercept.java",
    "content": "package org.pdown.gui.extension.mitm.intercept;\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.github.monkeywie.proxyee.intercept.HttpProxyIntercept;\nimport com.github.monkeywie.proxyee.intercept.HttpProxyInterceptPipeline;\nimport com.github.monkeywie.proxyee.util.HttpUtil;\nimport io.netty.buffer.ByteBuf;\nimport io.netty.buffer.PooledByteBufAllocator;\nimport io.netty.channel.Channel;\nimport io.netty.channel.nio.NioEventLoopGroup;\nimport io.netty.handler.codec.http.DefaultLastHttpContent;\nimport io.netty.handler.codec.http.HttpContent;\nimport io.netty.handler.codec.http.HttpHeaderNames;\nimport io.netty.handler.codec.http.HttpHeaderValues;\nimport io.netty.handler.codec.http.HttpHeaders;\nimport io.netty.handler.codec.http.HttpRequest;\nimport io.netty.handler.codec.http.HttpResponse;\nimport io.netty.handler.codec.http.LastHttpContent;\nimport io.netty.util.ReferenceCountUtil;\nimport java.net.URLEncoder;\nimport java.util.Arrays;\nimport java.util.Set;\nimport org.pdown.core.entity.HttpRequestInfo;\nimport org.pdown.core.entity.HttpResponseInfo;\nimport org.pdown.core.util.HttpDownUtil;\nimport org.pdown.core.util.ProtoUtil.RequestProto;\nimport org.pdown.gui.DownApplication;\nimport org.pdown.gui.extension.ExtensionContent;\nimport org.pdown.rest.form.HttpRequestForm;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\npublic class SniffIntercept extends HttpProxyIntercept {\n\n  private final static Logger LOGGER = LoggerFactory.getLogger(SniffIntercept.class);\n\n  private boolean matchFlag = false;\n\n  private ByteBuf content;\n  private boolean downFlag = false;\n\n  @Override\n  public void beforeRequest(Channel clientChannel, HttpRequest httpRequest,\n      HttpProxyInterceptPipeline pipeline) throws Exception {\n    Set<String> sniffRegexs = ExtensionContent.getSniffRegexs();\n    if (sniffRegexs == null) {\n      matchFlag = false;\n    } else {\n      matchFlag = sniffRegexs.stream().anyMatch(regex -> HttpUtil.checkUrl(httpRequest, regex));\n    }\n    if (!matchFlag) {\n      super.beforeRequest(clientChannel, httpRequest, pipeline);\n      return;\n    }\n    String contentLength = httpRequest.headers().get(HttpHeaderNames.CONTENT_LENGTH);\n    //缓存request content\n    if (contentLength != null) {\n      content = PooledByteBufAllocator.DEFAULT.buffer();\n    }\n    pipeline.beforeRequest(clientChannel, HttpRequestInfo.adapter(httpRequest));\n  }\n\n  @Override\n  public void beforeRequest(Channel clientChannel, HttpContent httpContent,\n      HttpProxyInterceptPipeline pipeline) throws Exception {\n    if (!matchFlag) {\n      super.beforeRequest(clientChannel, httpContent, pipeline);\n      return;\n    }\n    if (content != null) {\n      ByteBuf temp = httpContent.content().slice();\n      content.writeBytes(temp);\n      if (httpContent instanceof LastHttpContent) {\n        try {\n          byte[] contentBts = new byte[content.readableBytes()];\n          content.readBytes(contentBts);\n          ((HttpRequestInfo) pipeline.getHttpRequest()).setContent(contentBts);\n        } finally {\n          ReferenceCountUtil.release(content);\n        }\n      }\n    }\n    pipeline.beforeRequest(clientChannel, httpContent);\n  }\n\n  @Override\n  public void afterResponse(Channel clientChannel, Channel proxyChannel, HttpResponse httpResponse,\n      HttpProxyInterceptPipeline pipeline) throws Exception {\n    if (!matchFlag) {\n      super.afterResponse(clientChannel, proxyChannel, httpResponse, pipeline);\n      return;\n    }\n    if ((httpResponse.status().code() + \"\").indexOf(\"20\") == 0) { //响应码为20x\n      HttpHeaders httpResHeaders = httpResponse.headers();\n      String accept = pipeline.getHttpRequest().headers().get(HttpHeaderNames.ACCEPT);\n      String contentType = httpResHeaders.get(HttpHeaderNames.CONTENT_TYPE);\n      //有两种情况进行下载 1.url后缀为.xxx  2.带有CONTENT_DISPOSITION:ATTACHMENT响应头\n      String disposition = httpResHeaders.get(HttpHeaderNames.CONTENT_DISPOSITION);\n      if (accept != null\n          && accept.matches(\"^.*text/html.*$\")\n          && ((disposition != null\n          && disposition.contains(HttpHeaderValues.ATTACHMENT)\n          && disposition.contains(HttpHeaderValues.FILENAME))\n          || isDownContentType(contentType))) {\n        downFlag = true;\n      }\n\n      HttpRequestInfo httpRequestInfo = (HttpRequestInfo) pipeline.getHttpRequest();\n      if (downFlag) {   //如果是下载\n        LOGGER.debug(\"=====================下载===========================\\n\" +\n            pipeline.getHttpRequest().toString() + \"\\n\" +\n            \"------------------------------------------------\" +\n            httpResponse.toString() + \"\\n\" +\n            \"================================================\");\n        proxyChannel.close();//关闭嗅探下载连接\n        httpRequestInfo.setRequestProto(new RequestProto(pipeline.getRequestProto().getHost(), pipeline.getRequestProto().getPort(), pipeline.getRequestProto().getSsl()));\n        HttpRequestForm requestForm = HttpRequestForm.parse(httpRequestInfo);\n        HttpResponseInfo responseInfo = HttpDownUtil.getHttpResponseInfo(httpRequestInfo, null, null, (NioEventLoopGroup) clientChannel.eventLoop().parent());\n        httpResponse.headers().clear();\n        httpResponse.headers().set(HttpHeaderNames.CONTENT_TYPE, \"text/html\");\n        httpResponse.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);\n        String js = \"<script type=\\\"text/javascript\\\">window.history.go(-1)</script>\";\n        HttpContent httpContent = new DefaultLastHttpContent();\n        httpContent.content().writeBytes(js.getBytes());\n        httpResponse.headers().set(HttpHeaderNames.CONTENT_LENGTH, httpContent.content().readableBytes());\n        clientChannel.writeAndFlush(httpResponse);\n        clientChannel.writeAndFlush(httpContent);\n        clientChannel.close();\n        ObjectMapper objectMapper = new ObjectMapper();\n        String requestParam = URLEncoder.encode(objectMapper.writeValueAsString(requestForm), \"utf-8\");\n        String responseParam = URLEncoder.encode(objectMapper.writeValueAsString(responseInfo), \"utf-8\");\n        String uri = \"/#/tasks?request=\" + requestParam + \"&response=\" + responseParam;\n        DownApplication.INSTANCE.loadUri(uri, false);\n        return;\n      } else {\n        if (httpRequestInfo.content() != null) {\n          httpRequestInfo.setContent(null);\n        }\n      }\n    }\n    super.afterResponse(clientChannel, proxyChannel, httpResponse, pipeline);\n  }\n\n  @Override\n  public void afterResponse(Channel clientChannel, Channel proxyChannel, HttpContent httpContent,\n      HttpProxyInterceptPipeline pipeline) throws Exception {\n    if (!matchFlag) {\n      super.afterResponse(clientChannel, proxyChannel, httpContent, pipeline);\n      return;\n    }\n    if (downFlag) {\n      httpContent.release();\n    } else {\n      pipeline.afterResponse(clientChannel, proxyChannel, httpContent);\n    }\n  }\n\n  //https://chromium.googlesource.com/chromium/src/+/master/net/base/mime_util.cc\n  private static final String[] CONTENT_TYPES = {\n      \"application/javascript\",\n      \"application/x-javascript\",\n      \"application/wasm\",\n      \"application/x-chrome-extension\",\n      \"application/xhtml+xml\",\n      \"application/font-woff\",\n      \"application/json\",\n      \"application/x-shockwave-flash\",\n      \"audio/mpeg\",\n      \"audio/flac\",\n      \"audio/mp3\",\n      \"audio/ogg\",\n      \"audio/wav\",\n      \"audio/webm\",\n      \"audio/x-m4a\",\n      \"image/gif\",\n      \"image/jpeg\",\n      \"image/png\",\n      \"image/apng\",\n      \"image/webp\",\n      \"image/x-icon\",\n      \"image/bmp\",\n      \"image/jpeg\",\n      \"image/svg+xml\",\n      \"image/tiff\",\n      \"image/vnd.microsoft.icon\",\n      \"image/x-png\",\n      \"image/x-xbitmap\",\n      \"video/webm\",\n      \"video/ogg\",\n      \"video/mp4\",\n      \"video/mpeg\",\n      \"text/css\",\n      \"text/html\",\n      \"text/xml\",\n      \"text/calendar\",\n      \"text/html\",\n      \"text/plain\",\n      \"text/x-sh\",\n      \"text/xml\",\n      \"multipart/related\",\n      \"message/rfc822\",\n  };\n\n  private boolean isDownContentType(String contentType) {\n    if (contentType != null) {\n      String contentTypeFinal = contentType.split(\";\")[0].trim().toLowerCase();\n      return Arrays.stream(CONTENT_TYPES).noneMatch(type -> contentTypeFinal.equals(type));\n    }\n    return true;\n  }\n}\n"
  },
  {
    "path": "main/src/main/java/org/pdown/gui/extension/mitm/server/PDownProxyServer.java",
    "content": "package org.pdown.gui.extension.mitm.server;\n\nimport com.github.monkeywie.proxyee.exception.HttpProxyExceptionHandle;\nimport com.github.monkeywie.proxyee.intercept.HttpProxyInterceptInitializer;\nimport com.github.monkeywie.proxyee.intercept.HttpProxyInterceptPipeline;\nimport com.github.monkeywie.proxyee.server.HttpProxyServer;\nimport com.github.monkeywie.proxyee.server.HttpProxyServerConfig;\nimport io.netty.channel.Channel;\nimport org.pdown.gui.content.PDownConfigContent;\nimport org.pdown.gui.entity.PDownConfigInfo;\nimport org.pdown.gui.extension.mitm.intercept.AjaxIntercept;\nimport org.pdown.gui.extension.mitm.intercept.CookieIntercept;\nimport org.pdown.gui.extension.mitm.intercept.ScriptIntercept;\nimport org.pdown.gui.extension.mitm.intercept.SniffIntercept;\nimport org.pdown.gui.extension.mitm.ssl.PDownCACertFactory;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\npublic class PDownProxyServer {\n\n  private static final Logger LOGGER = LoggerFactory.getLogger(PDownProxyServer.class);\n\n  private static volatile HttpProxyServer httpProxyServer;\n  public static volatile boolean isStart = false;\n\n  public static void start(int port) {\n    HttpProxyServerConfig config = new HttpProxyServerConfig();\n    //处理ssl\n    config.setHandleSsl(true);\n    //线程池数量都设置为1\n    config.setBossGroupThreads(1);\n    config.setWorkerGroupThreads(1);\n    config.setProxyGroupThreads(1);\n    httpProxyServer = new HttpProxyServer()\n        .serverConfig(config)\n        .proxyConfig(PDownConfigInfo.convert(PDownConfigContent.getInstance().get().getProxyConfig()))\n        .caCertFactory(new PDownCACertFactory())\n        .proxyInterceptInitializer(new HttpProxyInterceptInitializer() {\n          @Override\n          public void init(HttpProxyInterceptPipeline pipeline) {\n            pipeline.addLast(new CookieIntercept());\n            pipeline.addLast(new AjaxIntercept());\n            pipeline.addLast(new ScriptIntercept());\n            pipeline.addLast(new SniffIntercept());\n          }\n        })\n        .httpProxyExceptionHandle(new HttpProxyExceptionHandle() {\n          @Override\n          public void beforeCatch(Channel clientChannel, Throwable cause) throws Exception {\n            LOGGER.warn(\"beforeCatch\", cause);\n          }\n\n          @Override\n          public void afterCatch(Channel clientChannel, Channel proxyChannel, Throwable cause) throws Exception {\n            LOGGER.warn(\"afterCatch\", cause);\n          }\n        });\n    isStart = true;\n    httpProxyServer.start(port);\n  }\n\n  public static void close() {\n    if (httpProxyServer != null) {\n      httpProxyServer.close();\n    }\n    isStart = false;\n  }\n\n}\n"
  },
  {
    "path": "main/src/main/java/org/pdown/gui/extension/mitm/ssl/PDownCACertFactory.java",
    "content": "package org.pdown.gui.extension.mitm.ssl;\n\nimport com.github.monkeywie.proxyee.crt.CertUtil;\nimport com.github.monkeywie.proxyee.server.HttpProxyCACertFactory;\nimport java.io.File;\nimport java.security.PrivateKey;\nimport java.security.cert.X509Certificate;\nimport org.pdown.rest.util.PathUtil;\n\npublic class PDownCACertFactory implements HttpProxyCACertFactory {\n\n  private static final String SSL_PATH = PathUtil.ROOT_PATH + File.separator + \"ssl\" + File.separator;\n\n  @Override\n  public X509Certificate getCACert() throws Exception {\n    return CertUtil.loadCert(SSL_PATH + \"ca.crt\");\n  }\n\n  @Override\n  public PrivateKey getCAPriKey() throws Exception {\n    return CertUtil.loadPriKey(SSL_PATH + \".ca_pri.der\");\n  }\n}\n"
  },
  {
    "path": "main/src/main/java/org/pdown/gui/extension/mitm/util/ExtensionCertUtil.java",
    "content": "package org.pdown.gui.extension.mitm.util;\n\nimport com.github.monkeywie.proxyee.crt.CertUtil;\nimport java.io.File;\nimport java.io.IOException;\nimport java.net.URLEncoder;\nimport java.nio.file.Files;\nimport java.nio.file.Paths;\nimport java.security.KeyPair;\nimport java.security.MessageDigest;\nimport java.security.cert.X509Certificate;\nimport java.util.Date;\nimport java.util.concurrent.TimeUnit;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\nimport org.pdown.core.util.FileUtil;\nimport org.pdown.core.util.OsUtil;\nimport org.pdown.gui.DownApplication;\nimport org.pdown.gui.util.ExecUtil;\nimport sun.security.x509.X500Name;\n\n/**\n * 用于处理系统的证书安装、查询和卸载\n */\npublic class ExtensionCertUtil {\n\n\n  /**\n   * 在指定目录生成一个ca证书和私钥\n   */\n  public static void buildCert(String path, String subjectName) throws Exception {\n    //生成ca证书和私钥\n    KeyPair keyPair = CertUtil.genKeyPair();\n    File priKeyFile = FileUtil.createFile(path + File.separator + \".ca_pri.der\", true);\n    File caCertFile = FileUtil.createFile(path + File.separator + \"ca.crt\", false);\n    Files.write(Paths.get(priKeyFile.toURI()), keyPair.getPrivate().getEncoded());\n    Files.write(Paths.get(caCertFile.toURI()),\n        CertUtil.genCACert(\n            \"C=CN, ST=GD, L=SZ, O=lee, OU=study, CN=\" + subjectName,\n            new Date(),\n            new Date(System.currentTimeMillis() + TimeUnit.DAYS.toMillis(3650)),\n            keyPair)\n            .getEncoded());\n  }\n\n  /**\n   * 安装证书\n   */\n  public static void installCert(File file) throws IOException {\n    String path = file.getPath();\n    if (OsUtil.isWindows()) {\n      ExecUtil.execBlock(\"certutil\",\n          \"-addstore\",\n          \"-user\",\n          \"root\",\n          path);\n    } else if (OsUtil.isMac()) {\n      ExecUtil.httpGet(\"http://127.0.0.1:\" + DownApplication.macToolPort + \"/cert/install?path=\" + URLEncoder.encode(path, \"utf-8\"));\n    }\n  }\n\n  /**\n   * 通过证书subjectName和sha1，判断系统是否已安装该证书\n   */\n  public static boolean isInstalledCert(File file) throws Exception {\n    if (!file.exists()) {\n      return false;\n    }\n    if (OsUtil.isUnix()) {\n      return true;\n    }\n    X509Certificate cert = CertUtil.loadCert(file.toURI());\n    String subjectName = ((X500Name) cert.getSubjectDN()).getCommonName();\n    String sha1 = getCertSHA1(cert);\n    return findCertList(subjectName).toUpperCase().replaceAll(\"\\\\s\", \"\").indexOf(\":\" + sha1.toUpperCase()) != -1;\n  }\n\n  /**\n   * 通过证书subjectName，判断系统是否已安装此subjectName的证书\n   */\n  public static boolean existsCert(String subjectName) throws IOException {\n    if (OsUtil.isWindows() && findCertList(subjectName).toUpperCase().indexOf(\"=====\") != -1) {\n      return true;\n    } else if (OsUtil.isMac() && findCertList(subjectName).toUpperCase().indexOf(\"BEGIN CERTIFICATE\") != -1) {\n      return true;\n    }\n    return false;\n  }\n\n  /**\n   * 通过证书name，卸载证书\n   */\n  public static void uninstallCert(String subjectName) throws IOException {\n    if (OsUtil.isWindows()) {\n      Pattern pattern = Pattern.compile(\"(?i)\\\\(sha1\\\\):\\\\s(.*)\\r?\\n\");\n      String certList = findCertList(subjectName);\n      Matcher matcher = pattern.matcher(certList);\n      while (matcher.find()) {\n        String hash = matcher.group(1).replaceAll(\"\\\\s\", \"\");\n        ExecUtil.execBlock(\"certutil\",\n            \"-delstore\",\n            \"-user\",\n            \"root\",\n            hash);\n      }\n    } else if (OsUtil.isMac()) {\n      String certList = findCertList(subjectName);\n      Pattern pattern = Pattern.compile(\"(?i)SHA-1 hash:\\\\s(.*)\\r?\\n\");\n      Matcher matcher = pattern.matcher(certList);\n      while (matcher.find()) {\n        String hash = matcher.group(1);\n        ExecUtil.httpGet(\"http://127.0.0.1:\" + DownApplication.macToolPort + \"/cert/uninstall\"\n            + \"?hash=\" + hash);\n      }\n    }\n  }\n\n  //查询证书列表\n  private static String findCertList(String subjectName) throws IOException {\n    if (OsUtil.isWindows()) {\n      return ExecUtil.exec(\"certutil \",\n          \"-store\",\n          \"-user\",\n          \"root\",\n          subjectName);\n    } else if (OsUtil.isMac()) {\n      return ExecUtil.exec(\"security\",\n          \"find-certificate\",\n          \"-a\",\n          \"-c\",\n          subjectName,\n          \"-p\",\n          \"-Z\",\n          \"/Library/Keychains/System.keychain\");\n    }\n    return null;\n  }\n\n  private static String getCertSHA1(X509Certificate certificate) throws Exception {\n    MessageDigest md = MessageDigest.getInstance(\"SHA-1\");\n    byte[] der = certificate.getEncoded();\n    md.update(der);\n    return btsToHex(md.digest());\n  }\n\n  private static String btsToHex(byte[] bts) {\n    StringBuilder str = new StringBuilder();\n    for (byte b : bts) {\n      str.append(String.format(\"%2s\", Integer.toHexString(b & 0xFF)).replace(\" \", \"0\"));\n    }\n    return str.toString();\n  }\n\n  public static void main(String[] args) throws Exception {\n    String subjectName = \"ProxyeeDown CA\";\n    String path = \"f:/test/\";\n    File certFile = new File(path + \"ca.crt\");\n    //证书还未安装\n    if (!isInstalledCert(certFile)) {\n      if (existsCert(subjectName)) {\n        //存在无用证书需要卸载\n        uninstallCert(subjectName);\n      }\n      //生成新的证书\n      buildCert(path, subjectName);\n      //安装\n      installCert(certFile);\n    }\n  }\n}\n"
  },
  {
    "path": "main/src/main/java/org/pdown/gui/extension/mitm/util/ExtensionProxyUtil.java",
    "content": "package org.pdown.gui.extension.mitm.util;\n\nimport com.sun.jna.Pointer;\nimport java.io.IOException;\nimport java.net.InetAddress;\nimport java.net.NetworkInterface;\nimport java.net.Socket;\nimport java.net.SocketException;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Enumeration;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Map.Entry;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\nimport org.pdown.core.util.OsUtil;\nimport org.pdown.gui.DownApplication;\nimport org.pdown.gui.extension.mitm.util.WinInet.INTERNET_PER_CONN_OPTION;\nimport org.pdown.gui.extension.mitm.util.WinInet.INTERNET_PER_CONN_OPTION.ByReference;\nimport org.pdown.gui.extension.mitm.util.WinInet.INTERNET_PER_CONN_OPTION_LIST;\nimport org.pdown.gui.util.ExecUtil;\n\n/**\n * 系统的代理切换工具类\n */\npublic class ExtensionProxyUtil {\n\n  /**\n   * 设置PAC代理\n   */\n  public static void enabledPACProxy(String url) throws IOException {\n    if (OsUtil.isWindows()) {\n      String interName = getRemoteInterface();\n      INTERNET_PER_CONN_OPTION_LIST list = buildOptionList(interName, 2);\n      INTERNET_PER_CONN_OPTION[] pOptions = (INTERNET_PER_CONN_OPTION[]) list.pOptions\n          .toArray(list.dwOptionCount);\n      // Set flags.\n      pOptions[0].dwOption = WinInet.INTERNET_PER_CONN_FLAGS;\n      pOptions[0].Value.dwValue = WinInet.PROXY_TYPE_AUTO_PROXY_URL;\n      pOptions[0].Value.setType(int.class);\n\n      // Set flags.\n      pOptions[1].dwOption = WinInet.INTERNET_PER_CONN_AUTOCONFIG_URL;\n      pOptions[1].Value.pszValue = url;\n      pOptions[1].Value.setType(String.class);\n\n      refreshOptions(list);\n    } else if (OsUtil.isMac()) {\n      String networkService = disabledProxy();\n      ExecUtil.httpGet(\"http://127.0.0.1:\" + DownApplication.macToolPort + \"/proxy/enabledPAC\"\n          + \"?ns=\" + networkService\n          + \"&url=\" + url);\n    }\n  }\n\n  /**\n   * 启用http代理\n   */\n  public static void enabledHTTPProxy(String host, int port) throws IOException {\n    if (OsUtil.isWindows()) {\n      String interName = getRemoteInterface();\n      INTERNET_PER_CONN_OPTION_LIST list = buildOptionList(interName, 2);\n      INTERNET_PER_CONN_OPTION[] pOptions = (INTERNET_PER_CONN_OPTION[]) list.pOptions\n          .toArray(list.dwOptionCount);\n\n      // Set flags.\n      pOptions[0].dwOption = WinInet.INTERNET_PER_CONN_FLAGS;\n      pOptions[0].Value.dwValue = WinInet.PROXY_TYPE_PROXY;\n      pOptions[0].Value.setType(int.class);\n\n      // Set proxy name.\n      pOptions[1].dwOption = WinInet.INTERNET_PER_CONN_PROXY_SERVER;\n      pOptions[1].Value.pszValue = host + \":\" + port;\n      pOptions[1].Value.setType(String.class);\n\n      refreshOptions(list);\n    } else if (OsUtil.isMac()) {\n      String networkService = disabledProxy();\n      ExecUtil.httpGet(\"http://127.0.0.1:\" + DownApplication.macToolPort + \"/proxy/enabledHTTP\"\n          + \"?ns=\" + networkService\n          + \"&host=\" + host\n          + \"&port=\" + port);\n    }\n  }\n\n  /**\n   * 禁用代理\n   */\n  public static String disabledProxy() throws IOException {\n    if (OsUtil.isWindows()) {\n      String interName = getRemoteInterface();\n      INTERNET_PER_CONN_OPTION_LIST list = buildOptionList(interName, 1);\n      INTERNET_PER_CONN_OPTION[] pOptions = (INTERNET_PER_CONN_OPTION[]) list.pOptions\n          .toArray(list.dwOptionCount);\n      // Set flags.\n      pOptions[0].dwOption = WinInet.INTERNET_PER_CONN_FLAGS;\n      pOptions[0].Value.dwValue = WinInet.PROXY_TYPE_DIRECT;\n      pOptions[0].Value.setType(int.class);\n\n      refreshOptions(list);\n    } else if (OsUtil.isMac()) {\n      String networkService = getRemoteInterface();\n      ExecUtil.httpGet(\"http://127.0.0.1:\" + DownApplication.macToolPort + \"/proxy/disabled\"\n          + \"?ns=\" + networkService);\n      return networkService;\n    }\n    return null;\n  }\n\n  /**\n   * 获取访问外网使用的网卡\n   */\n  private static String getRemoteInterface() throws IOException {\n    Map<String, List<String>> interfacesInfo = getInterfacesInfo();\n    Socket socket = new Socket(\"www.baidu.com\", 80);\n    for (Entry<String, List<String>> entry : interfacesInfo.entrySet()) {\n      if (entry.getValue().contains(socket.getLocalAddress().getHostAddress())) {\n        String remoteInterface = entry.getKey();\n        if (OsUtil.isWindows()) {\n          try {\n            String result = ExecUtil.exec(\"rasdial\");\n            if (result != null && Arrays.stream(result.split(\"\\r\\n\")).anyMatch(line -> line.equals(remoteInterface))) {\n              return remoteInterface;\n            }\n          } catch (IOException e) {\n            return null;\n          }\n        } else if (OsUtil.isMac()) {\n          String result = ExecUtil.exec(\"networksetup\", \"-listnetworkserviceorder\");\n          Pattern pattern = Pattern.compile(\"\\\\(Hardware\\\\sPort:\\\\s(.*),\\\\sDevice:\\\\s(.*)\\\\)\");\n          Matcher matcher = pattern.matcher(result);\n          while (matcher.find()) {\n            if (matcher.group(2).equalsIgnoreCase(remoteInterface)) {\n              return matcher.group(1);\n            }\n          }\n        }\n      }\n    }\n    return null;\n  }\n\n  /**\n   * 获取本机所有网卡\n   */\n  public static Map<String, List<String>> getInterfacesInfo() throws SocketException {\n    Map<String, List<String>> interfacesInfo = new HashMap<>();\n    Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();\n    while (interfaces.hasMoreElements()) {\n      NetworkInterface networkInterface = interfaces.nextElement();\n      Enumeration<InetAddress> addresses = networkInterface.getInetAddresses();\n      while (addresses.hasMoreElements()) {\n        InetAddress nextElement = addresses.nextElement();\n        String name = networkInterface.getDisplayName();\n        List<String> ipList = interfacesInfo.get(name);\n        if (ipList == null) {\n          ipList = new ArrayList<>();\n          interfacesInfo.put(name, ipList);\n        }\n        ipList.add(nextElement.getHostAddress());\n      }\n    }\n    return interfacesInfo;\n  }\n\n  public static void main(String[] args) throws Exception {\n    System.out.println(getRemoteInterface());\n  }\n\n  private static INTERNET_PER_CONN_OPTION_LIST buildOptionList(String connectionName, int size) {\n    INTERNET_PER_CONN_OPTION_LIST list = new INTERNET_PER_CONN_OPTION_LIST();\n    // Fill the list structure.\n    list.dwSize = list.size();\n\n    // NULL == LAN, otherwise connectoid name.\n    list.pszConnection = connectionName;\n\n    // Set three options.\n    list.dwOptionCount = size;\n    list.pOptions = new ByReference();\n\n    // Ensure that the memory was allocated.\n    if (null == list.pOptions) {\n      // Return FALSE if the memory wasn't allocated.\n      return null;\n    }\n    return list;\n  }\n\n  private static boolean refreshOptions(INTERNET_PER_CONN_OPTION_LIST list) {\n    if (!WinInet.INSTANCE\n        .InternetSetOption(Pointer.NULL, WinInet.INTERNET_OPTION_PER_CONNECTION_OPTION, list,\n            list.size())) {\n      return false;\n    }\n\n    if (!WinInet.INSTANCE\n        .InternetSetOption(Pointer.NULL, WinInet.INTERNET_OPTION_PROXY_SETTINGS_CHANGED,\n            Pointer.NULL, 0)) {\n      return false;\n    }\n\n    // Refresh Internet Options\n    if (!WinInet.INSTANCE\n        .InternetSetOption(Pointer.NULL, WinInet.INTERNET_OPTION_REFRESH, Pointer.NULL, 0)) {\n      return false;\n    }\n    return true;\n  }\n}\n\n"
  },
  {
    "path": "main/src/main/java/org/pdown/gui/extension/mitm/util/WinInet.java",
    "content": "package org.pdown.gui.extension.mitm.util;\n\nimport com.sun.jna.Native;\nimport com.sun.jna.Pointer;\nimport com.sun.jna.Structure;\nimport com.sun.jna.Union;\nimport com.sun.jna.win32.StdCallLibrary;\nimport com.sun.jna.win32.W32APIOptions;\nimport java.util.Arrays;\nimport java.util.List;\n\npublic interface WinInet extends StdCallLibrary {\n  WinInet INSTANCE = (WinInet) Native.loadLibrary(\"wininet\", WinInet.class, W32APIOptions.UNICODE_OPTIONS);\n\n  int INTERNET_PER_CONN_FLAGS                         = 1;\n  int INTERNET_PER_CONN_PROXY_SERVER                  = 2;\n  int INTERNET_PER_CONN_PROXY_BYPASS                  = 3;\n  int INTERNET_PER_CONN_AUTOCONFIG_URL                = 4;\n  int INTERNET_PER_CONN_AUTODISCOVERY_FLAGS           = 5;\n  int INTERNET_PER_CONN_AUTOCONFIG_SECONDARY_URL      = 6;\n  int INTERNET_PER_CONN_AUTOCONFIG_RELOAD_DELAY_MINS  = 7;\n  int INTERNET_PER_CONN_AUTOCONFIG_LAST_DETECT_TIME   = 8;\n  int INTERNET_PER_CONN_AUTOCONFIG_LAST_DETECT_URL    = 9;\n\n  int PROXY_TYPE_DIRECT                               = 0x00000001;   // direct to net\n  int PROXY_TYPE_PROXY                                = 0x00000002;   // via named proxy\n  int PROXY_TYPE_AUTO_PROXY_URL                       = 0x00000004;   // autoproxy URL\n  int PROXY_TYPE_AUTO_DETECT                          = 0x00000008;   // use autoproxy detection\n\n  //\n  // options manifests for Internet{Query|Set}Option\n  //\n\n  int INTERNET_OPTION_CALLBACK                = 1;\n  int INTERNET_OPTION_CONNECT_TIMEOUT         = 2;\n  int INTERNET_OPTION_CONNECT_RETRIES         = 3;\n  int INTERNET_OPTION_CONNECT_BACKOFF         = 4;\n  int INTERNET_OPTION_SEND_TIMEOUT            = 5;\n  int INTERNET_OPTION_CONTROL_SEND_TIMEOUT    = INTERNET_OPTION_SEND_TIMEOUT;\n  int INTERNET_OPTION_RECEIVE_TIMEOUT         = 6;\n  int INTERNET_OPTION_CONTROL_RECEIVE_TIMEOUT = INTERNET_OPTION_RECEIVE_TIMEOUT;\n  int INTERNET_OPTION_DATA_SEND_TIMEOUT       = 7;\n  int INTERNET_OPTION_DATA_RECEIVE_TIMEOUT    = 8;\n  int INTERNET_OPTION_HANDLE_TYPE             = 9;\n  int INTERNET_OPTION_LISTEN_TIMEOUT          = 11;\n  int INTERNET_OPTION_READ_BUFFER_SIZE        = 12;\n  int INTERNET_OPTION_WRITE_BUFFER_SIZE       = 13;\n\n  int INTERNET_OPTION_ASYNC_ID                = 15;\n  int INTERNET_OPTION_ASYNC_PRIORITY          = 16;\n\n  int INTERNET_OPTION_PARENT_HANDLE           = 21;\n  int INTERNET_OPTION_KEEP_CONNECTION         = 22;\n  int INTERNET_OPTION_REQUEST_FLAGS           = 23;\n  int INTERNET_OPTION_EXTENDED_ERROR          = 24;\n\n  int INTERNET_OPTION_OFFLINE_MODE            = 26;\n  int INTERNET_OPTION_CACHE_STREAM_HANDLE     = 27;\n  int INTERNET_OPTION_USERNAME                = 28;\n  int INTERNET_OPTION_PASSWORD                = 29;\n  int INTERNET_OPTION_ASYNC                   = 30;\n  int INTERNET_OPTION_SECURITY_FLAGS          = 31;\n  int INTERNET_OPTION_SECURITY_CERTIFICATE_STRUCT = 32;\n  int INTERNET_OPTION_DATAFILE_NAME           = 33;\n  int INTERNET_OPTION_URL                     = 34;\n  int INTERNET_OPTION_SECURITY_CERTIFICATE    = 35;\n  int INTERNET_OPTION_SECURITY_KEY_BITNESS    = 36;\n  int INTERNET_OPTION_REFRESH                 = 37;\n  int INTERNET_OPTION_PROXY                   = 38;\n  int INTERNET_OPTION_SETTINGS_CHANGED        = 39;\n  int INTERNET_OPTION_VERSION                 = 40;\n  int INTERNET_OPTION_USER_AGENT              = 41;\n  int INTERNET_OPTION_END_BROWSER_SESSION     = 42;\n  int INTERNET_OPTION_PROXY_USERNAME          = 43;\n  int INTERNET_OPTION_PROXY_PASSWORD          = 44;\n  int INTERNET_OPTION_CONTEXT_VALUE           = 45;\n  int INTERNET_OPTION_CONNECT_LIMIT           = 46;\n  int INTERNET_OPTION_SECURITY_SELECT_CLIENT_CERT = 47;\n  int INTERNET_OPTION_POLICY                  = 48;\n  int INTERNET_OPTION_DISCONNECTED_TIMEOUT    = 49;\n  int INTERNET_OPTION_CONNECTED_STATE         = 50;\n  int INTERNET_OPTION_IDLE_STATE              = 51;\n  int INTERNET_OPTION_OFFLINE_SEMANTICS       = 52;\n  int INTERNET_OPTION_SECONDARY_CACHE_KEY     = 53;\n  int INTERNET_OPTION_CALLBACK_FILTER         = 54;\n  int INTERNET_OPTION_CONNECT_TIME            = 55;\n  int INTERNET_OPTION_SEND_THROUGHPUT         = 56;\n  int INTERNET_OPTION_RECEIVE_THROUGHPUT      = 57;\n  int INTERNET_OPTION_REQUEST_PRIORITY        = 58;\n  int INTERNET_OPTION_HTTP_VERSION            = 59;\n  int INTERNET_OPTION_RESET_URLCACHE_SESSION  = 60;\n  int INTERNET_OPTION_ERROR_MASK              = 62;\n  int INTERNET_OPTION_FROM_CACHE_TIMEOUT      = 63;\n  int INTERNET_OPTION_BYPASS_EDITED_ENTRY     = 64;\n  int INTERNET_OPTION_DIAGNOSTIC_SOCKET_INFO  = 67;\n  int INTERNET_OPTION_CODEPAGE                = 68;\n  int INTERNET_OPTION_CACHE_TIMESTAMPS        = 69;\n  int INTERNET_OPTION_DISABLE_AUTODIAL        = 70;\n  int INTERNET_OPTION_MAX_CONNS_PER_SERVER     = 73;\n  int INTERNET_OPTION_MAX_CONNS_PER_1_0_SERVER = 74;\n  int INTERNET_OPTION_PER_CONNECTION_OPTION   = 75;\n  int INTERNET_OPTION_DIGEST_AUTH_UNLOAD             = 76;\n  int INTERNET_OPTION_IGNORE_OFFLINE           = 77;\n  int INTERNET_OPTION_IDENTITY                 = 78;\n  int INTERNET_OPTION_REMOVE_IDENTITY          = 79;\n  int INTERNET_OPTION_ALTER_IDENTITY           = 80;\n  int INTERNET_OPTION_SUPPRESS_BEHAVIOR        = 81;\n  int INTERNET_OPTION_AUTODIAL_MODE            = 82;\n  int INTERNET_OPTION_AUTODIAL_CONNECTION      = 83;\n  int INTERNET_OPTION_CLIENT_CERT_CONTEXT      = 84;\n  int INTERNET_OPTION_AUTH_FLAGS               = 85;\n  int INTERNET_OPTION_COOKIES_3RD_PARTY        = 86;\n  int INTERNET_OPTION_DISABLE_PASSPORT_AUTH    = 87;\n  int INTERNET_OPTION_SEND_UTF8_SERVERNAME_TO_PROXY         = 88;\n  int INTERNET_OPTION_EXEMPT_CONNECTION_LIMIT  = 89;\n  int INTERNET_OPTION_ENABLE_PASSPORT_AUTH     = 90;\n\n  int INTERNET_OPTION_HIBERNATE_INACTIVE_WORKER_THREADS       = 91;\n  int INTERNET_OPTION_ACTIVATE_WORKER_THREADS                 = 92;\n  int INTERNET_OPTION_RESTORE_WORKER_THREAD_DEFAULTS          = 93;\n  int INTERNET_OPTION_SOCKET_SEND_BUFFER_LENGTH               = 94;\n  int INTERNET_OPTION_PROXY_SETTINGS_CHANGED                  = 95;\n\n  int INTERNET_OPTION_DATAFILE_EXT                                              = 96;\n\n\n  // BOOL InternetSetOption(\n  //   _In_  HINTERNET hInternet,\n  //   _In_  DWORD dwOption,\n  //   _In_  LPVOID lpBuffer,\n  //   _In_  DWORD dwBufferLength\n  // );\n  boolean InternetSetOption(Pointer hInternet, int dwOption, INTERNET_PER_CONN_OPTION_LIST lpBuffer,\n      int dwBufferLength);\n  boolean InternetSetOption(Pointer hInternet, int dwOption, Pointer lpBuffer, int dwBufferLength);\n\n  // typedef struct _FILETIME {\n  //   DWORD dwLowDateTime;\n  //   DWORD dwHighDateTime;\n  // } FILETIME, *PFILETIME;\n  class FILETIME extends Structure {\n    public static class ByReference extends FILETIME implements Structure.ByReference {\n      public ByReference() {\n      }\n\n      public ByReference(Pointer memory) {\n        super(memory);\n      }\n    }\n\n    public FILETIME() {\n    }\n\n    public FILETIME(Pointer memory) {\n      super(memory);\n      read();\n    }\n\n    public int dwLowDateTime;\n    public int dwHighDateTime;\n\n    @SuppressWarnings(\"rawtypes\")\n    @Override\n    protected List getFieldOrder() {\n      return Arrays.asList(\"dwLowDateTime\", \"dwHighDateTime\");\n    }\n  }\n\n  // typedef struct {\n  //   DWORD dwOption;\n  //   union {\n  //     DWORD    dwValue;\n  //     LPTSTR   pszValue;\n  //     FILETIME ftValue;\n  //   } Value;\n  // } INTERNET_PER_CONN_OPTION, *LPINTERNET_PER_CONN_OPTION;\n  class INTERNET_PER_CONN_OPTION extends Structure {\n    public static class ByReference extends INTERNET_PER_CONN_OPTION implements Structure.ByReference {\n      public ByReference() {\n      }\n\n      public ByReference(Pointer memory) {\n        super(memory);\n      }\n    }\n\n    public INTERNET_PER_CONN_OPTION() {\n    }\n\n    public INTERNET_PER_CONN_OPTION(Pointer memory) {\n      super(memory);\n      read();\n    }\n\n    public static class VALUE_UNION extends Union {\n      public int dwValue;\n      public String pszValue;\n      public FILETIME ftValue;\n    }\n\n    public int dwOption;\n    public VALUE_UNION Value = new VALUE_UNION();\n\n    @SuppressWarnings(\"rawtypes\")\n    @Override\n    protected List getFieldOrder() {\n      return Arrays.asList(\"dwOption\", \"Value\");\n    }\n  }\n\n  // typedef struct {\n  //   DWORD                      dwSize;\n  //   LPTSTR                     pszConnection;\n  //   DWORD                      dwOptionCount;\n  //   DWORD                      dwOptionError;\n  //   LPINTERNET_PER_CONN_OPTION pOptions;\n  // } INTERNET_PER_CONN_OPTION_LIST, *LPINTERNET_PER_CONN_OPTION_LIST;\n  class INTERNET_PER_CONN_OPTION_LIST extends Structure {\n    public static class ByReference extends INTERNET_PER_CONN_OPTION_LIST implements Structure.ByReference {\n      public ByReference() {\n      }\n\n      public ByReference(Pointer memory) {\n        super(memory);\n      }\n    }\n\n    public INTERNET_PER_CONN_OPTION_LIST() {\n    }\n\n    public INTERNET_PER_CONN_OPTION_LIST(Pointer memory) {\n      super(memory);\n      read();\n    }\n\n    public int dwSize;\n    public String pszConnection;\n    public int dwOptionCount;\n    public int dwOptionError;\n    public INTERNET_PER_CONN_OPTION.ByReference pOptions;\n\n    @SuppressWarnings(\"rawtypes\")\n    @Override\n    protected List getFieldOrder() {\n      return Arrays\n          .asList(\"dwSize\", \"pszConnection\", \"dwOptionCount\", \"dwOptionError\", \"pOptions\");\n    }\n  }\n}\n"
  },
  {
    "path": "main/src/main/java/org/pdown/gui/extension/util/ExtensionUtil.java",
    "content": "package org.pdown.gui.extension.util;\n\nimport com.fasterxml.jackson.core.JsonProcessingException;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport java.io.BufferedReader;\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.FileNotFoundException;\nimport java.io.FileOutputStream;\nimport java.io.FileReader;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.io.OutputStream;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.nio.file.StandardCopyOption;\nimport java.util.concurrent.CountDownLatch;\nimport java.util.function.Function;\nimport java.util.stream.Collectors;\nimport javax.script.Invocable;\nimport javax.script.ScriptContext;\nimport javax.script.ScriptEngine;\nimport javax.script.ScriptException;\nimport javax.script.SimpleScriptContext;\nimport org.pdown.core.util.FileUtil;\nimport org.pdown.gui.DownApplication;\nimport org.pdown.gui.content.PDownConfigContent;\nimport org.pdown.gui.extension.ExtensionContent;\nimport org.pdown.gui.extension.ExtensionInfo;\nimport org.pdown.gui.extension.HookScript;\nimport org.pdown.gui.extension.HookScript.Event;\nimport org.pdown.gui.extension.Meta;\nimport org.pdown.gui.extension.jsruntime.JavascriptEngine;\nimport org.pdown.gui.http.controller.NativeController;\nimport org.pdown.gui.http.util.HttpHandlerUtil;\nimport org.pdown.gui.util.AppUtil;\nimport org.pdown.gui.util.ConfigUtil;\nimport org.pdown.rest.form.TaskForm;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.util.StringUtils;\n\npublic class ExtensionUtil {\n\n  private static final Logger LOGGER = LoggerFactory.getLogger(ExtensionUtil.class);\n\n  /**\n   * 安装扩展\n   */\n  public static void install(String server, String path, String files) throws Exception {\n    download(server, path, path, files);\n  }\n\n  /**\n   * 更新扩展,先把扩展文件下载到临时目录中\n   */\n  public static void update(String server, String path, String files) throws Exception {\n    String extDir = ExtensionContent.EXT_DIR + File.separator + path;\n    String tmpPath = path + \"_tmp\";\n    String extTmpPath = ExtensionContent.EXT_DIR + File.separator + tmpPath;\n    String extBakPath = ExtensionContent.EXT_DIR + File.separator + path + \"_bak\";\n    try {\n      download(server, path, tmpPath, files);\n      //备份老版本扩展\n      copy(new File(extDir), new File(extBakPath));\n      //备份扩展配置\n      String configPath = extDir + File.separator + Meta.CONFIG_FILE;\n      if (FileUtil.exists(configPath)) {\n        Path bakConfigPath = Paths.get(extTmpPath + File.separator + Meta.CONFIG_FILE);\n        FileUtil.createFileSmart(bakConfigPath.toFile().getAbsolutePath());\n        Files.copy(Paths.get(configPath), bakConfigPath, StandardCopyOption.REPLACE_EXISTING);\n      }\n      String configBakPath = extDir + File.separator + Meta.CONFIG_FILE + \".bak\";\n      if (FileUtil.exists(configBakPath)) {\n        Files.copy(Paths.get(configBakPath), Paths.get(extTmpPath + File.separator + Meta.CONFIG_FILE + \".bak\"), StandardCopyOption.REPLACE_EXISTING);\n      }\n      try {\n        //删除原始扩展目录并将临时目录重命名\n        FileUtil.deleteIfExists(extDir);\n      } catch (Exception e) {\n        //删除失败还原扩展\n        copy(new File(extBakPath), new File(extDir));\n        throw new IOException(e);\n      } finally {\n        FileUtil.deleteIfExists(extBakPath);\n      }\n      new File(extTmpPath).renameTo(new File(extDir));\n    } finally {\n      //删除临时目录\n      FileUtil.deleteIfExists(extTmpPath);\n    }\n  }\n\n  /**\n   * 根据扩展的路径和文件列表，下载对应的文件\n   */\n  private static void download(String server, String path, String writePath, String files) throws Exception {\n    String extDir = ExtensionContent.EXT_DIR + File.separator + writePath;\n    if (!FileUtil.exists(extDir)) {\n      Files.createDirectories(Paths.get(extDir));\n    }\n    for (String fileName : files.split(\",\")) {\n      AppUtil.download(server + path + fileName, extDir + File.separator + fileName);\n    }\n  }\n\n  public static void copy(File sourceLocation, File targetLocation) throws IOException {\n    if (sourceLocation.isDirectory()) {\n      copyDirectory(sourceLocation, targetLocation);\n    } else {\n      copyFile(sourceLocation, targetLocation);\n    }\n  }\n\n  private static void copyDirectory(File source, File target) throws IOException {\n    if (!target.exists()) {\n      target.mkdir();\n    }\n    for (String f : source.list()) {\n      copy(new File(source, f), new File(target, f));\n    }\n  }\n\n  private static void copyFile(File source, File target) throws IOException {\n    if (target.exists()) {\n      return;\n    }\n    try (\n        InputStream in = new FileInputStream(source);\n        OutputStream out = new FileOutputStream(target)\n    ) {\n      byte[] buf = new byte[1024];\n      int length;\n      while ((length = in.read(buf)) > 0) {\n        out.write(buf, 0, length);\n      }\n    }\n  }\n\n  public static String readRuntimeTemplate(ExtensionInfo extensionInfo) {\n    String template = \"\";\n    try (\n        BufferedReader reader = new BufferedReader(new InputStreamReader(Thread.currentThread().getContextClassLoader().getResourceAsStream(\"extension/runtime.js\")))\n    ) {\n      template = reader.lines().collect(Collectors.joining(\"\\n\"));\n      template = template.replace(\"${version}\", ConfigUtil.getString(\"version\"));\n      template = template.replace(\"${apiPort}\", DownApplication.INSTANCE.API_PORT + \"\");\n      template = template.replace(\"${frontPort}\", DownApplication.INSTANCE.FRONT_PORT + \"\");\n      template = template.replace(\"${uiMode}\", PDownConfigContent.getInstance().get().getUiMode() + \"\");\n      String settingJson = \"{}\";\n      if (extensionInfo.getMeta().getSettings() != null) {\n        ObjectMapper objectMapper = new ObjectMapper();\n        try {\n          settingJson = objectMapper.writeValueAsString(extensionInfo.getMeta().getSettings());\n        } catch (JsonProcessingException e) {\n        }\n      }\n      template = template.replace(\"${settings}\", settingJson);\n    } catch (IOException e) {\n    }\n    return template;\n  }\n\n  /**\n   * 创建扩展环境的js引擎，可以在引擎中访问pdown对象\n   */\n  public static ScriptEngine buildExtensionRuntimeEngine(ExtensionInfo extensionInfo) throws ScriptException, NoSuchMethodException, FileNotFoundException {\n    //初始化js引擎\n    ScriptEngine engine = JavascriptEngine.buildEngine();\n    //加载运行时脚本\n    Object runtime = engine.eval(ExtensionUtil.readRuntimeTemplate(extensionInfo));\n    engine.put(\"pdown\", runtime);\n    //加载扩展脚本\n    engine.eval(new FileReader(Paths.get(extensionInfo.getMeta().getFullPath(), extensionInfo.getHookScript().getScript()).toFile()));\n    return engine;\n  }\n\n  /**\n   * 运行一个js方法\n   */\n  public static Object invoke(ExtensionInfo extensionInfo, Event event, Object param, boolean async) throws NoSuchMethodException, ScriptException, FileNotFoundException, InterruptedException {\n    //初始化js引擎\n    ScriptEngine engine = ExtensionUtil.buildExtensionRuntimeEngine(extensionInfo);\n    Invocable invocable = (Invocable) engine;\n    //执行resolve方法\n    Object result = invocable.invokeFunction(StringUtils.isEmpty(event.getMethod()) ? event.getOn() : event.getMethod(), param);\n    //结果为null或者异步调用直接返回\n    if (result == null || async) {\n      return result;\n    }\n    final Object[] ret = {null};\n    //判断是不是返回Promise对象\n    ScriptContext ctx = new SimpleScriptContext();\n    ctx.setAttribute(\"result\", result, ScriptContext.ENGINE_SCOPE);\n    boolean isPromise = (boolean) engine.eval(\"!!result&&typeof result=='object'&&typeof result.then=='function'\", ctx);\n    if (isPromise) {\n      //如果是返回的Promise则等待执行完成\n      CountDownLatch countDownLatch = new CountDownLatch(1);\n      invocable.invokeMethod(result, \"then\", (Function) o -> {\n        try {\n          ret[0] = o;\n        } catch (Exception e) {\n          LOGGER.error(\"An exception occurred while resolve()\", e);\n        } finally {\n          countDownLatch.countDown();\n        }\n        return null;\n      });\n      invocable.invokeMethod(result, \"catch\", (Function) o -> {\n        countDownLatch.countDown();\n        return null;\n      });\n      //等待解析完成\n      countDownLatch.await();\n    } else {\n      ret[0] = result;\n    }\n    return ret[0];\n  }\n\n  public static void main(String[] args) {\n    String url = \"https://d.pcs.baidu.com/file/ab83d33b3f250a6ff472b8ffa17c3e5f?fid=336129479-250528-831181624029689&dstime=1541581115&rt=sh&sign=FDtAERVY-DCb740ccc5511e5e8fedcff06b081203-aILjqoJW3OEzB4%2Bu7Wnxz63PUho%3D&expires=8h&chkv=1&chkbd=0&chkpc=et&dp-logid=7206180716394015240&dp-callid=0&shareid=3786359813&r=663179228\";\n    String[] urlArray = url.split(\"\\\\?\");\n    StringBuilder params = new StringBuilder(urlArray[1]);\n    String path = urlArray[0].substring(urlArray[0].lastIndexOf(\"/\") + 1);\n    params.append(\"&path=\" + path)\n        .append(\"&check_blue=1\")\n        .append(\"&clienttype=8\")\n        .append(\"&devuid=BDIMXV2-O_9A2DB23216984690875184DCA864434E-C_0-D_Z4Y7YWL9-M_408D5C4224FB-V_D8F90423\")\n        .append(\"&dtype=1\")\n        .append(\"&eck=1\")\n        .append(\"&ehps=1\")\n        .append(\"&err_ver=1\")\n        .append(\"&es=1\")\n        .append(\"&esl=1\")\n        .append(\"&method=locatedownload\")\n        .append(\"&ver=4\")\n        .append(\"&version=2.1.13.11\")\n        .append(\"&version_app=6.4.0.6\");\n    System.out.println(params.toString());\n  }\n}\n"
  },
  {
    "path": "main/src/main/java/org/pdown/gui/http/EmbedHttpServer.java",
    "content": "package org.pdown.gui.http;\n\nimport io.netty.bootstrap.ServerBootstrap;\nimport io.netty.channel.Channel;\nimport io.netty.channel.ChannelFuture;\nimport io.netty.channel.ChannelHandlerContext;\nimport io.netty.channel.ChannelInitializer;\nimport io.netty.channel.SimpleChannelInboundHandler;\nimport io.netty.channel.nio.NioEventLoopGroup;\nimport io.netty.channel.socket.nio.NioServerSocketChannel;\nimport io.netty.handler.codec.http.FullHttpRequest;\nimport io.netty.handler.codec.http.FullHttpResponse;\nimport io.netty.handler.codec.http.HttpHeaderNames;\nimport io.netty.handler.codec.http.HttpHeaderValues;\nimport io.netty.handler.codec.http.HttpObjectAggregator;\nimport io.netty.handler.codec.http.HttpResponseStatus;\nimport io.netty.handler.codec.http.HttpServerCodec;\nimport io.netty.util.concurrent.GenericFutureListener;\nimport java.lang.reflect.Method;\nimport java.net.URI;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport org.pdown.gui.http.controller.DefaultController;\nimport org.pdown.gui.http.controller.NativeController;\nimport org.pdown.gui.http.util.HttpHandlerUtil;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.web.bind.annotation.RequestMapping;\n\npublic class EmbedHttpServer {\n\n  private static final Logger LOGGER = LoggerFactory.getLogger(EmbedHttpServer.class);\n\n  private int port;\n  private DefaultController defaultController;\n  private List<Object> controllerList;\n\n  public EmbedHttpServer(int port) {\n    this.port = port;\n    this.defaultController = new DefaultController();\n    this.controllerList = new ArrayList<>();\n  }\n\n  //根据请求uri找到对应的处理类方法执行\n  public FullHttpResponse invoke(String uri, Channel channel, FullHttpRequest request)\n      throws Exception {\n    if (controllerList != null) {\n      for (Object obj : controllerList) {\n        Class<?> clazz = obj.getClass();\n        RequestMapping mapping = clazz.getAnnotation(RequestMapping.class);\n        if (mapping != null) {\n          String mappingUri = fixUri(mapping.value()[0]);\n          for (Method actionMethod : clazz.getMethods()) {\n            RequestMapping subMapping = actionMethod.getAnnotation(RequestMapping.class);\n            if (subMapping != null) {\n              String subMappingUri = fixUri(subMapping.value()[0]);\n              if (uri.equalsIgnoreCase(mappingUri + subMappingUri)) {\n                return (FullHttpResponse) actionMethod.invoke(obj, channel, request);\n              }\n            }\n          }\n        }\n      }\n    }\n    return defaultController.handle(channel, request);\n  }\n\n  private String fixUri(String uri) {\n    StringBuilder builder = new StringBuilder(uri);\n    if (builder.indexOf(\"/\") != 0) {\n      builder.insert(0, \"/\");\n    }\n    if (builder.lastIndexOf(\"/\") == builder.length() - 1) {\n      builder.delete(builder.length() - 1, builder.length());\n    }\n    return builder.toString();\n  }\n\n  public void start() {\n    start(null);\n  }\n\n  public void start(GenericFutureListener startedListener) {\n    NioEventLoopGroup bossGroup = new NioEventLoopGroup(2);\n    NioEventLoopGroup workGroup = new NioEventLoopGroup(2);\n    try {\n      ServerBootstrap bootstrap = new ServerBootstrap().group(bossGroup, workGroup)\n          .channel(NioServerSocketChannel.class)\n          .childHandler(new ChannelInitializer<Channel>() {\n            @Override\n            protected void initChannel(Channel ch) throws Exception {\n              ch.pipeline().addLast(\"httpCodec\", new HttpServerCodec());\n              ch.pipeline().addLast(new HttpObjectAggregator(4194304));\n              ch.pipeline()\n                  .addLast(\"serverHandle\", new SimpleChannelInboundHandler<FullHttpRequest>() {\n\n                    @Override\n                    protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request)\n                        throws Exception {\n                      URI uri = new URI(request.uri());\n                      FullHttpResponse httpResponse = invoke(uri.getPath(), ctx.channel(), request);\n                      if (httpResponse != null) {\n                        httpResponse.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);\n                        httpResponse.headers().set(HttpHeaderNames.CONTENT_LENGTH, httpResponse.content().readableBytes());\n                        ch.writeAndFlush(httpResponse);\n                      }\n                    }\n\n                    @Override\n                    public void channelUnregistered(ChannelHandlerContext ctx) {\n                      ctx.channel().close();\n                    }\n\n                    @Override\n                    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {\n                      LOGGER.error(\"native request error\", cause.getCause() == null ? cause : cause.getCause());\n                      Map<String, Object> data = new HashMap<>();\n                      data.put(\"error\", cause.getCause().toString());\n                      FullHttpResponse httpResponse = HttpHandlerUtil.buildJson(data);\n                      httpResponse.setStatus(HttpResponseStatus.INTERNAL_SERVER_ERROR);\n                      ctx.channel().writeAndFlush(httpResponse);\n                    }\n                  });\n            }\n          });\n      ChannelFuture f = bootstrap.bind(\"127.0.0.1\", port).sync();\n      if (startedListener != null) {\n        f.addListener(startedListener);\n      }\n      f.channel().closeFuture().sync();\n    } catch (Exception e) {\n      e.printStackTrace();\n    } finally {\n      bossGroup.shutdownGracefully();\n      workGroup.shutdownGracefully();\n    }\n  }\n\n  public EmbedHttpServer addController(Object obj) {\n    this.controllerList.add(obj);\n    return this;\n  }\n\n  public static void main(String[] args) {\n\n    new EmbedHttpServer(8998)\n        .addController(new NativeController())\n        .start();\n  }\n}\n"
  },
  {
    "path": "main/src/main/java/org/pdown/gui/http/controller/ApiController.java",
    "content": "package org.pdown.gui.http.controller;\n\nimport io.netty.channel.Channel;\nimport io.netty.handler.codec.http.DefaultFullHttpResponse;\nimport io.netty.handler.codec.http.FullHttpRequest;\nimport io.netty.handler.codec.http.FullHttpResponse;\nimport io.netty.handler.codec.http.HttpResponseStatus;\nimport io.netty.handler.codec.http.HttpVersion;\nimport java.io.IOException;\nimport java.util.HashMap;\nimport java.util.Map;\nimport javafx.application.Platform;\nimport org.pdown.gui.DownApplication;\nimport org.springframework.web.bind.annotation.RequestMapping;\n\n@RequestMapping(\"api\")\npublic class ApiController {\n\n  @RequestMapping(\"createTask\")\n  public FullHttpResponse createTask(Channel channel, FullHttpRequest request) throws Exception {\n    Map<String, String> map = getQueryParams(request);\n    DownApplication.INSTANCE.loadUri(\"/#/tasks?request=\" + map.get(\"request\") +\n        \"&response=\" + map.get(\"response\") +\n        \"&config=\" + map.get(\"config\") +\n        \"&data=\" + map.get(\"data\"), false);\n    FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);\n    response.headers().set(\"Access-Control-Allow-Origin\", \"*\");\n    return response;\n  }\n\n  private Map<String, String> getQueryParams(FullHttpRequest request) throws IOException {\n    Map<String, String> map = new HashMap<>();\n    String uri = request.uri();\n    int index = uri.lastIndexOf(\"?\");\n    if (index != -1 && index != uri.length() - 1) {\n      String[] params = uri.substring(index + 1).split(\"&\");\n      for (String param : params) {\n        String[] kv = param.split(\"=\");\n        if (kv.length == 2) {\n          map.put(kv[0], kv[1]);\n        }\n      }\n    }\n    return map;\n  }\n}\n"
  },
  {
    "path": "main/src/main/java/org/pdown/gui/http/controller/DefaultController.java",
    "content": "package org.pdown.gui.http.controller;\n\nimport io.netty.channel.Channel;\nimport io.netty.handler.codec.http.DefaultFullHttpResponse;\nimport io.netty.handler.codec.http.FullHttpRequest;\nimport io.netty.handler.codec.http.FullHttpResponse;\nimport io.netty.handler.codec.http.HttpHeaderNames;\nimport io.netty.handler.codec.http.HttpHeaderValues;\nimport io.netty.handler.codec.http.HttpResponseStatus;\nimport io.netty.handler.codec.http.HttpVersion;\nimport io.netty.util.AsciiString;\nimport java.io.InputStream;\nimport java.net.URI;\nimport org.springframework.web.bind.annotation.RequestMapping;\n\n@RequestMapping(\"/\")\npublic class DefaultController {\n\n  public FullHttpResponse handle(Channel channel, FullHttpRequest request) throws Exception {\n    URI uri = new URI(request.uri());\n    String path = uri.getPath();\n    if (\"/\".equals(path)) {\n      path = \"/index.html\";\n    }\n    InputStream inputStream = Thread.currentThread().getContextClassLoader()\n        .getResourceAsStream(\"http\" + path);\n    FullHttpResponse httpResponse;\n    if (inputStream != null) {\n      String mime = path.substring(path.lastIndexOf(\".\") + 1);\n      httpResponse = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);\n      buildHead(httpResponse, mime);\n      try {\n        byte[] bts = new byte[8192];\n        int len;\n        while ((len = inputStream.read(bts)) != -1) {\n          httpResponse.content().writeBytes(bts, 0, len);\n        }\n      } finally {\n        inputStream.close();\n      }\n    } else {\n      httpResponse = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1,\n          HttpResponseStatus.NOT_FOUND);\n      buildHead(httpResponse, null);\n    }\n    return httpResponse;\n  }\n\n  private void buildHead(FullHttpResponse httpResponse, String mime) {\n    if (mime != null) {\n      AsciiString contentType;\n      switch (mime) {\n        case \"txt\":\n        case \"text\":\n          contentType = AsciiString.cached(\"text/plain; charset=utf-8\");\n          break;\n        case \"html\":\n        case \"htm\":\n          contentType = AsciiString.cached(\"text/html; charset=utf-8\");\n          break;\n        case \"css\":\n          contentType = AsciiString.cached(\"text/css; charset=utf-8\");\n          break;\n        case \"js\":\n          contentType = AsciiString.cached(\"application/javascript; charset=utf-8\");\n          break;\n        case \"png\":\n          contentType = AsciiString.cached(\"image/png\");\n          break;\n        case \"jpg\":\n        case \"jpeg\":\n          contentType = AsciiString.cached(\"image/jpeg\");\n          break;\n        case \"bmp\":\n          contentType = AsciiString.cached(\"application/x-bmp\");\n          break;\n        case \"gif\":\n          contentType = AsciiString.cached(\"image/gif\");\n          break;\n        case \"ico\":\n          contentType = AsciiString.cached(\"image/x-icon\");\n          break;\n        case \"ttf\":\n          contentType = AsciiString.cached(\"font/ttf; charset=utf-8\");\n          break;\n        case \"woff\":\n          contentType = AsciiString.cached(\"application/font-woff; charset=utf-8\");\n          break;\n        default:\n          contentType = HttpHeaderValues.APPLICATION_OCTET_STREAM;\n      }\n      httpResponse.headers().set(HttpHeaderNames.CONTENT_TYPE, contentType);\n    }\n  }\n}\n"
  },
  {
    "path": "main/src/main/java/org/pdown/gui/http/controller/NativeController.java",
    "content": "package org.pdown.gui.http.controller;\n\nimport com.fasterxml.jackson.annotation.JsonInclude.Include;\nimport com.fasterxml.jackson.databind.DeserializationFeature;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport io.netty.channel.Channel;\nimport io.netty.handler.codec.http.*;\nimport javafx.application.Platform;\nimport jdk.nashorn.internal.runtime.Undefined;\nimport org.pdown.core.boot.HttpDownBootstrap;\nimport org.pdown.core.dispatch.HttpDownCallback;\nimport org.pdown.core.util.OsUtil;\nimport org.pdown.gui.DownApplication;\nimport org.pdown.gui.com.Components;\nimport org.pdown.gui.content.PDownConfigContent;\nimport org.pdown.gui.entity.PDownConfigInfo;\nimport org.pdown.gui.extension.ExtensionContent;\nimport org.pdown.gui.extension.ExtensionInfo;\nimport org.pdown.gui.extension.HookScript;\nimport org.pdown.gui.extension.HookScript.Event;\nimport org.pdown.gui.extension.mitm.server.PDownProxyServer;\nimport org.pdown.gui.extension.mitm.util.ExtensionCertUtil;\nimport org.pdown.gui.extension.mitm.util.ExtensionProxyUtil;\nimport org.pdown.gui.extension.util.ExtensionUtil;\nimport org.pdown.gui.http.util.HttpHandlerUtil;\nimport org.pdown.gui.util.AppUtil;\nimport org.pdown.gui.util.ConfigUtil;\nimport org.pdown.gui.util.ExecUtil;\nimport org.pdown.rest.form.HttpRequestForm;\nimport org.pdown.rest.form.TaskForm;\nimport org.pdown.rest.util.PathUtil;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.beans.BeanUtils;\nimport org.springframework.util.StringUtils;\nimport org.springframework.web.bind.annotation.RequestMapping;\n\nimport java.awt.*;\nimport java.awt.datatransfer.Clipboard;\nimport java.awt.datatransfer.StringSelection;\nimport java.awt.datatransfer.Transferable;\nimport java.io.File;\nimport java.io.IOException;\nimport java.net.URI;\nimport java.net.URLDecoder;\nimport java.nio.charset.Charset;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\n@RequestMapping(\"native\")\npublic class NativeController {\n\n  private static final Logger LOGGER = LoggerFactory.getLogger(NativeController.class);\n\n  @RequestMapping(\"dirChooser\")\n  public FullHttpResponse dirChooser(Channel channel, FullHttpRequest request) throws Exception {\n    Platform.runLater(() -> {\n      File file = Components.dirChooser();\n      Map<String, Object> data = null;\n      if (file != null) {\n        data = new HashMap<>();\n        data.put(\"path\", file.getPath());\n        data.put(\"canWrite\", file.canWrite());\n        data.put(\"freeSpace\", file.getFreeSpace());\n        data.put(\"totalSpace\", file.getTotalSpace());\n      }\n      HttpHandlerUtil.writeJson(channel, data);\n    });\n    return null;\n  }\n\n  @RequestMapping(\"fileChooser\")\n  public FullHttpResponse handle(Channel channel, FullHttpRequest request) throws Exception {\n    Platform.runLater(() -> {\n      File file = Components.fileChooser();\n      Map<String, Object> data = null;\n      if (file != null) {\n        data = new HashMap<>();\n        data.put(\"name\", file.getName());\n        data.put(\"path\", file.getPath());\n        data.put(\"parent\", file.getParent());\n        data.put(\"size\", file.length());\n      }\n      HttpHandlerUtil.writeJson(channel, data);\n    });\n    return null;\n  }\n\n  //启动的时候检查一次\n  private boolean checkFlag = true;\n  private static final long WEEK = 7 * 24 * 60 * 60 * 1000L;\n\n  @RequestMapping(\"getInitConfig\")\n  public FullHttpResponse getInitConfig(Channel channel, FullHttpRequest request) throws Exception {\n    Map<String, Object> data = new HashMap<>();\n    PDownConfigInfo configInfo = PDownConfigContent.getInstance().get();\n    //语言\n    data.put(\"locale\", configInfo.getLocale());\n    //后台管理API请求地址\n    data.put(\"adminServer\", ConfigUtil.getString(\"adminServer\"));\n    //是否要检查更新\n    boolean needCheckUpdate = false;\n    if (checkFlag) {\n      int rate = configInfo.getUpdateCheckRate();\n      if (rate == 2\n          || (rate == 1 && (System.currentTimeMillis() - configInfo.getLastUpdateCheck()) > WEEK)) {\n        needCheckUpdate = true;\n        checkFlag = false;\n        configInfo.setLastUpdateCheck(System.currentTimeMillis());\n        PDownConfigContent.getInstance().save();\n      }\n    }\n    data.put(\"needCheckUpdate\", needCheckUpdate);\n    //扩展下载服务器列表\n    data.put(\"extFileServers\", configInfo.getExtFileServers());\n    //软件版本\n    data.put(\"version\", ConfigUtil.getString(\"version\"));\n    return HttpHandlerUtil.buildJson(data);\n  }\n\n  @RequestMapping(\"getConfig\")\n  public FullHttpResponse getConfig(Channel channel, FullHttpRequest request) throws Exception {\n    return HttpHandlerUtil.buildJson(PDownConfigContent.getInstance().get());\n  }\n\n  @RequestMapping(\"setConfig\")\n  public FullHttpResponse setConfig(Channel channel, FullHttpRequest request) throws Exception {\n    ObjectMapper objectMapper = new ObjectMapper();\n    objectMapper.enable(DeserializationFeature.READ_ENUMS_USING_TO_STRING);\n    PDownConfigInfo configInfo = objectMapper.readValue(request.content().toString(Charset.forName(\"UTF-8\")), PDownConfigInfo.class);\n    PDownConfigInfo beforeConfigInfo = PDownConfigContent.getInstance().get();\n    boolean proxyChange = (beforeConfigInfo.getProxyConfig() != null && configInfo.getProxyConfig() == null) ||\n        (configInfo.getProxyConfig() != null && beforeConfigInfo.getProxyConfig() == null) ||\n        (beforeConfigInfo.getProxyConfig() != null && !beforeConfigInfo.getProxyConfig().equals(configInfo.getProxyConfig())) ||\n        (configInfo.getProxyConfig() != null && !configInfo.getProxyConfig().equals(beforeConfigInfo.getProxyConfig()));\n    boolean localeChange = !configInfo.getLocale().equals(beforeConfigInfo.getLocale());\n    BeanUtils.copyProperties(configInfo, beforeConfigInfo);\n    if (localeChange) {\n      DownApplication.INSTANCE.loadPopupMenu();\n      DownApplication.INSTANCE.refreshBrowserMenu();\n    }\n    //检查到前置代理有变动重启MITM代理服务器\n    if (proxyChange && PDownProxyServer.isStart) {\n      new Thread(() -> {\n        PDownProxyServer.close();\n        PDownProxyServer.start(DownApplication.INSTANCE.PROXY_PORT);\n      }).start();\n    }\n    PDownConfigContent.getInstance().save();\n    return new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);\n  }\n\n  @RequestMapping(\"showFile\")\n  public FullHttpResponse showFile(Channel channel, FullHttpRequest request) throws Exception {\n    Map<String, Object> map = getJSONParams(request);\n    String path = (String) map.get(\"path\");\n    if (!StringUtils.isEmpty(path)) {\n      File file = new File(path);\n      if (!file.exists() || OsUtil.isUnix()) {\n        Desktop.getDesktop().open(file.getParentFile());\n      } else if (OsUtil.isWindows()) {\n        ExecUtil.execBlock(\"explorer.exe\", \"/select,\", file.getPath());\n      } else if (OsUtil.isMac()) {\n        ExecUtil.execBlock(\"open\", \"-R\", file.getPath());\n      }\n    }\n    return new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);\n  }\n\n  @RequestMapping(\"openUrl\")\n  public FullHttpResponse openUrl(Channel channel, FullHttpRequest request) throws Exception {\n    Map<String, Object> map = getJSONParams(request);\n    String url = (String) map.get(\"url\");\n    Desktop.getDesktop().browse(URI.create(URLDecoder.decode(url, \"UTF-8\")));\n    return new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);\n  }\n\n  private static volatile HttpDownBootstrap updateBootstrap;\n\n  @RequestMapping(\"doUpdate\")\n  public FullHttpResponse doUpdate(Channel channel, FullHttpRequest request) throws Exception {\n    Map<String, Object> map = getJSONParams(request);\n    String url = (String) map.get(\"path\");\n    String path = PathUtil.ROOT_PATH + File.separator + \"proxyee-down-main.jar.tmp\";\n    try {\n      File updateTmpJar = new File(path);\n      if (updateTmpJar.exists()) {\n        updateTmpJar.delete();\n      }\n      updateBootstrap = AppUtil.fastDownload(url, updateTmpJar, new HttpDownCallback() {\n        @Override\n        public void onDone(HttpDownBootstrap httpDownBootstrap) {\n          File updateBakJar = new File(updateTmpJar.getParent() + File.separator + \"proxyee-down-main.jar.bak\");\n          updateTmpJar.renameTo(updateBakJar);\n        }\n\n        @Override\n        public void onError(HttpDownBootstrap httpDownBootstrap) {\n          File file = new File(path);\n          if (file.exists()) {\n            file.delete();\n          }\n          httpDownBootstrap.close();\n        }\n      });\n    } catch (Exception e) {\n      throw e;\n    }\n    return new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);\n  }\n\n  @RequestMapping(\"getUpdateProgress\")\n  public FullHttpResponse getUpdateProgress(Channel channel, FullHttpRequest request) throws Exception {\n    Map<String, Object> data = new HashMap<>();\n    if (updateBootstrap != null) {\n      data.put(\"status\", updateBootstrap.getTaskInfo().getStatus());\n      data.put(\"totalSize\", updateBootstrap.getResponse().getTotalSize());\n      data.put(\"downSize\", updateBootstrap.getTaskInfo().getDownSize());\n      data.put(\"speed\", updateBootstrap.getTaskInfo().getSpeed());\n    } else {\n      data.put(\"status\", 0);\n    }\n    return HttpHandlerUtil.buildJson(data);\n  }\n\n  @RequestMapping(\"doRestart\")\n  public FullHttpResponse doRestart(Channel channel, FullHttpRequest request) throws Exception {\n    System.out.println(\"proxyee-down-exit\");\n    return new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);\n  }\n\n  /**\n   * 获取已安装的插件列表\n   */\n  @RequestMapping(\"getExtensions\")\n  public FullHttpResponse getExtensions(Channel channel, FullHttpRequest request) throws Exception {\n    //刷新扩展信息\n    ExtensionContent.load();\n    return HttpHandlerUtil.buildJson(ExtensionContent.get());\n  }\n\n  /**\n   * 安装扩展\n   */\n  @RequestMapping(\"installExtension\")\n  public FullHttpResponse installExtension(Channel channel, FullHttpRequest request) throws Exception {\n    return extensionCommon(request, false);\n  }\n\n  /**\n   * 更新扩展\n   */\n  @RequestMapping(\"updateExtension\")\n  public FullHttpResponse updateExtension(Channel channel, FullHttpRequest request) throws Exception {\n    return extensionCommon(request, true);\n  }\n\n  /**\n   * 加载本地扩展\n   */\n  @RequestMapping(\"installLocalExtension\")\n  public FullHttpResponse installLocalExtension(Channel channel, FullHttpRequest request) throws Exception {\n    Map<String, Object> data = new HashMap<>();\n    Map<String, Object> map = getJSONParams(request);\n    String path = (String) map.get(\"path\");\n    //刷新扩展content\n    ExtensionInfo loadExt = ExtensionContent.refresh(path, true);\n    if (loadExt == null) {\n      return new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.BAD_REQUEST);\n    }\n    data.put(\"data\", loadExt);\n    //刷新系统pac代理\n    AppUtil.refreshPAC();\n    return HttpHandlerUtil.buildJson(data);\n  }\n\n  /**\n   * 卸载扩展\n   */\n  @RequestMapping(\"uninstallExtension\")\n  public FullHttpResponse uninstallExtension(Channel channel, FullHttpRequest request) throws Exception {\n    Map<String, Object> data = new HashMap<>();\n    Map<String, Object> map = getJSONParams(request);\n    String path = (String) map.get(\"path\");\n    boolean local = map.get(\"local\") != null ? (boolean) map.get(\"local\") : false;\n    //卸载扩展\n    ExtensionContent.remove(path, local);\n    //刷新系统pac代理\n    AppUtil.refreshPAC();\n    return HttpHandlerUtil.buildJson(data);\n  }\n\n  private FullHttpResponse extensionCommon(FullHttpRequest request, boolean isUpdate) throws Exception {\n    Map<String, Object> map = getJSONParams(request);\n    String server = (String) map.get(\"server\");\n    String path = (String) map.get(\"path\");\n    String files = (String) map.get(\"files\");\n    if (isUpdate) {\n      ExtensionUtil.update(server, path, files);\n    } else {\n      ExtensionUtil.install(server, path, files);\n    }\n    //刷新扩展content\n    ExtensionContent.refresh(path);\n    //刷新系统pac代理\n    AppUtil.refreshPAC();\n    return new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);\n  }\n\n  /**\n   * 启用或禁用插件\n   */\n  @RequestMapping(\"toggleExtension\")\n  public FullHttpResponse toggleExtension(Channel channel, FullHttpRequest request) throws Exception {\n    Map<String, Object> map = getJSONParams(request);\n    String path = (String) map.get(\"path\");\n    boolean enabled = (boolean) map.get(\"enabled\");\n    boolean local = map.get(\"local\") != null ? (boolean) map.get(\"local\") : false;\n    ExtensionInfo extensionInfo = ExtensionContent.get()\n        .stream()\n        .filter(e -> e.getMeta().getPath().equals(path))\n        .findFirst()\n        .get();\n    extensionInfo.getMeta().setEnabled(enabled).save();\n    //刷新pac\n    ExtensionContent.refresh(extensionInfo.getMeta().getFullPath(), local);\n    AppUtil.refreshPAC();\n    return new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);\n  }\n\n  @RequestMapping(\"getProxyMode\")\n  public FullHttpResponse getProxyMode(Channel channel, FullHttpRequest request) throws Exception {\n    Map<String, Object> data = new HashMap<>();\n    data.put(\"mode\", PDownConfigContent.getInstance().get().getProxyMode());\n    return HttpHandlerUtil.buildJson(data);\n  }\n\n  @RequestMapping(\"changeProxyMode\")\n  public FullHttpResponse changeProxyMode(Channel channel, FullHttpRequest request) throws Exception {\n    Map<String, Object> map = getJSONParams(request);\n    int mode = (int) map.get(\"mode\");\n    PDownConfigContent.getInstance().get().setProxyMode(mode);\n    //修改系统代理\n    if (mode == 1) {\n      AppUtil.refreshPAC();\n    } else {\n      ExtensionProxyUtil.disabledProxy();\n    }\n    PDownConfigContent.getInstance().save();\n    return new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);\n  }\n\n  @RequestMapping(\"checkCert\")\n  public FullHttpResponse checkCert(Channel channel, FullHttpRequest request) throws Exception {\n    Map<String, Object> data = new HashMap<>();\n    data.put(\"status\", AppUtil.checkIsInstalledCert());\n    return HttpHandlerUtil.buildJson(data);\n  }\n\n  @RequestMapping(\"installCert\")\n  public FullHttpResponse installCert(Channel channel, FullHttpRequest request) throws Exception {\n    Map<String, Object> data = new HashMap<>();\n    boolean status;\n    if (OsUtil.isUnix() || OsUtil.isWindowsXP()) {\n      if (!AppUtil.checkIsInstalledCert()) {\n        ExtensionCertUtil.buildCert(AppUtil.SSL_PATH, AppUtil.SUBJECT);\n      }\n      Desktop.getDesktop().open(new File(AppUtil.SSL_PATH));\n      status = true;\n    } else {\n      //再检测一次，确保不重复安装\n      if (!AppUtil.checkIsInstalledCert()) {\n        if (ExtensionCertUtil.existsCert(AppUtil.SUBJECT)) {\n          //存在无用证书需要卸载\n          ExtensionCertUtil.uninstallCert(AppUtil.SUBJECT);\n        }\n        //生成新的证书\n        ExtensionCertUtil.buildCert(AppUtil.SSL_PATH, AppUtil.SUBJECT);\n        //安装\n        ExtensionCertUtil.installCert(new File(AppUtil.CERT_PATH));\n        //检测是否安装成功，可能点了取消就没安装成功\n        status = AppUtil.checkIsInstalledCert();\n      } else {\n        status = true;\n      }\n    }\n    data.put(\"status\", status);\n    if (status && !PDownProxyServer.isStart) {\n      new Thread(() -> {\n        try {\n          AppUtil.startProxyServer();\n        } catch (IOException e) {\n          LOGGER.error(\"Start proxy server error\", e);\n        }\n      }).start();\n    }\n    return HttpHandlerUtil.buildJson(data);\n  }\n\n  @RequestMapping(\"copy\")\n  public FullHttpResponse copy(Channel channel, FullHttpRequest request) throws Exception {\n    Map<String, Object> map = getJSONParams(request);\n    Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();\n    Transferable selection = null;\n    if (\"text\".equalsIgnoreCase((String) map.get(\"type\"))) {\n      selection = new StringSelection((String) map.get(\"data\"));\n    }\n    clipboard.setContents(selection, null);\n    return new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);\n  }\n\n  @RequestMapping(\"updateExtensionSetting\")\n  public FullHttpResponse updateExtensionSetting(Channel channel, FullHttpRequest request) throws Exception {\n    Map<String, Object> map = getJSONParams(request);\n    String path = (String) map.get(\"path\");\n    Map<String, Object> setting = (Map<String, Object>) map.get(\"setting\");\n    ExtensionInfo extensionInfo = ExtensionContent.get()\n        .stream()\n        .filter(e -> e.getMeta().getPath().equals(path))\n        .findFirst()\n        .get();\n    extensionInfo.getMeta().setSettings(setting).save();\n    return new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);\n  }\n\n  @RequestMapping(\"onResolve\")\n  public FullHttpResponse onResolve(Channel channel, FullHttpRequest request) throws Exception {\n    HttpRequestForm taskRequest = getJSONParams(request, HttpRequestForm.class);\n    //遍历扩展模块是否有对应的处理\n    List<ExtensionInfo> extensionInfos = ExtensionContent.get();\n    for (ExtensionInfo extensionInfo : extensionInfos) {\n      if (extensionInfo.getMeta().isEnabled()) {\n        if (extensionInfo.getHookScript() != null\n            && !StringUtils.isEmpty(extensionInfo.getHookScript().getScript())) {\n          Event event = extensionInfo.getHookScript().hasEvent(HookScript.EVENT_RESOLVE, taskRequest.getUrl());\n          if (event != null) {\n            try {\n              //执行resolve方法\n              Object result = ExtensionUtil.invoke(extensionInfo, event, taskRequest, false);\n              if (result != null && !(result instanceof Undefined)) {\n                ObjectMapper objectMapper = new ObjectMapper();\n                String temp = objectMapper.writeValueAsString(result);\n                TaskForm taskForm = objectMapper.readValue(temp, TaskForm.class);\n                //有一个扩展解析成功的话直接返回\n                return HttpHandlerUtil.buildJson(taskForm, Include.NON_DEFAULT);\n              }\n            } catch (Exception e) {\n              LOGGER.error(\"An exception occurred while resolve()\", e);\n            }\n          }\n        }\n      }\n    }\n    return new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);\n  }\n\n  private Map<String, Object> getJSONParams(FullHttpRequest request) throws IOException {\n    ObjectMapper objectMapper = new ObjectMapper();\n    return objectMapper.readValue(request.content().toString(Charset.forName(\"UTF-8\")), Map.class);\n  }\n\n  private <T> T getJSONParams(FullHttpRequest request, Class<T> clazz) throws IOException {\n    ObjectMapper objectMapper = new ObjectMapper();\n    return objectMapper.readValue(request.content().toString(Charset.forName(\"UTF-8\")), clazz);\n  }\n\n}\n"
  },
  {
    "path": "main/src/main/java/org/pdown/gui/http/controller/PacController.java",
    "content": "package org.pdown.gui.http.controller;\n\nimport io.netty.channel.Channel;\nimport io.netty.handler.codec.http.FullHttpRequest;\nimport io.netty.handler.codec.http.FullHttpResponse;\nimport io.netty.handler.codec.http.HttpHeaderNames;\nimport io.netty.handler.codec.http.HttpHeaderValues;\nimport java.util.Set;\nimport org.pdown.gui.DownApplication;\nimport org.pdown.gui.extension.ExtensionContent;\nimport org.pdown.gui.http.util.HttpHandlerUtil;\nimport org.springframework.web.bind.annotation.RequestMapping;\n\n@RequestMapping(\"pac\")\npublic class PacController {\n\n  private static final String PAC_TEMPLATE = \"function FindProxyForURL(url, host) {\"\n      + \"  if (isInNet(host, '127.0.0.1', '255.0.0.255')\"\n      + \"      || isInNet(dnsResolve(host), '127.0.0.1', '255.0.0.255')) {\"\n      + \"    return 'DIRECT';\"\n      + \"  }\"\n      + \"  var domains = [{domains}];\"\n      + \"  var match = false;\"\n      + \"  for (var i = 0; i < domains.length; i++) {\"\n      + \"    if (shExpMatch(host, domains[i])) {\"\n      + \"      match = true;\"\n      + \"      break;\"\n      + \"    }\"\n      + \"  }\"\n      + \"  return match ? 'PROXY 127.0.0.1:{port};DIRECT' : 'DIRECT';\"\n      + \"}\";\n\n  @RequestMapping(\"pdown.pac\")\n  public FullHttpResponse build(Channel channel, FullHttpRequest request) throws Exception {\n    Set<String> domains = ExtensionContent.getProxyWildCards();\n    String pacContent = PAC_TEMPLATE.replace(\"{port}\", DownApplication.INSTANCE.PROXY_PORT + \"\");\n    if (domains != null && domains.size() > 0) {\n      StringBuilder domainsBuilder = new StringBuilder();\n      for (String domain : domains) {\n        if (domainsBuilder.length() != 0) {\n          domainsBuilder.append(\",\");\n        }\n        domainsBuilder.append(\"'\" + domain + \"'\");\n      }\n      pacContent = pacContent.replace(\"{domains}\", domainsBuilder.toString());\n    } else {\n      pacContent = pacContent.replace(\"{domains}\", \"\");\n    }\n    FullHttpResponse httpResponse = HttpHandlerUtil.buildContent(pacContent, \"application/x-ns-proxy-autoconfig\");\n    httpResponse.headers().set(HttpHeaderNames.CACHE_CONTROL, HttpHeaderValues.NO_CACHE);\n    httpResponse.headers().set(HttpHeaderNames.PRAGMA, HttpHeaderValues.NO_CACHE);\n    httpResponse.headers().set(HttpHeaderNames.EXPIRES, 0);\n    return httpResponse;\n  }\n\n}\n"
  },
  {
    "path": "main/src/main/java/org/pdown/gui/http/util/HttpHandlerUtil.java",
    "content": "package org.pdown.gui.http.util;\n\nimport com.fasterxml.jackson.annotation.JsonInclude.Include;\nimport com.fasterxml.jackson.core.JsonProcessingException;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport io.netty.channel.Channel;\nimport io.netty.handler.codec.http.DefaultFullHttpResponse;\nimport io.netty.handler.codec.http.FullHttpResponse;\nimport io.netty.handler.codec.http.HttpHeaderNames;\nimport io.netty.handler.codec.http.HttpResponseStatus;\nimport io.netty.handler.codec.http.HttpVersion;\nimport io.netty.util.AsciiString;\nimport java.nio.charset.Charset;\n\npublic class HttpHandlerUtil {\n\n  public static void writeJson(Channel channel, Object obj) {\n    channel.writeAndFlush(buildJson(obj));\n  }\n\n  public static FullHttpResponse buildJson(Object obj) {\n    return buildJson(obj, null);\n  }\n\n  public static FullHttpResponse buildJson(Object obj, Include include) {\n    FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);\n    response.headers().set(HttpHeaderNames.CONTENT_TYPE, AsciiString.cached(\"application/json; charset=utf-8\"));\n    if (obj != null) {\n      try {\n        ObjectMapper objectMapper = new ObjectMapper();\n        if (include != null) {\n          objectMapper.setSerializationInclusion(include);\n        }\n        String content = objectMapper.writeValueAsString(obj);\n        response.content().writeBytes(content.getBytes(Charset.forName(\"utf-8\")));\n      } catch (JsonProcessingException e) {\n        response.setStatus(HttpResponseStatus.SERVICE_UNAVAILABLE);\n      }\n    }\n    response.headers().set(HttpHeaderNames.CONTENT_LENGTH, response.content().readableBytes());\n    return response;\n  }\n\n  public static FullHttpResponse buildContent(String content, String contentType) {\n    FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);\n    response.headers().set(HttpHeaderNames.CONTENT_TYPE, AsciiString.cached(contentType));\n    if (content != null) {\n      response.content().writeBytes(content.getBytes(Charset.forName(\"utf-8\")));\n    }\n    response.headers().set(HttpHeaderNames.CONTENT_LENGTH, response.content().readableBytes());\n    return response;\n  }\n}\n"
  },
  {
    "path": "main/src/main/java/org/pdown/gui/rest/HttpDownAppCallback.java",
    "content": "package org.pdown.gui.rest;\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport org.pdown.core.boot.HttpDownBootstrap;\nimport org.pdown.core.entity.HttpResponseInfo;\nimport org.pdown.core.util.HttpDownUtil;\nimport org.pdown.gui.extension.ExtensionContent;\nimport org.pdown.gui.extension.ExtensionInfo;\nimport org.pdown.gui.extension.HookScript;\nimport org.pdown.gui.extension.HookScript.Event;\nimport org.pdown.gui.extension.util.ExtensionUtil;\nimport org.pdown.rest.content.HttpDownContent;\nimport org.pdown.rest.controller.HttpDownRestCallback;\nimport org.pdown.rest.entity.DownInfo;\nimport org.pdown.rest.form.HttpRequestForm;\nimport org.pdown.rest.form.TaskForm;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.beans.BeanUtils;\nimport org.springframework.util.StringUtils;\n\npublic class HttpDownAppCallback extends HttpDownRestCallback {\n\n  private static final Logger LOGGER = LoggerFactory.getLogger(HttpDownAppCallback.class);\n\n  @Override\n  public void onStart(HttpDownBootstrap httpDownBootstrap) {\n    super.onStart(httpDownBootstrap);\n    commonHook(httpDownBootstrap, HookScript.EVENT_START, false);\n  }\n\n  @Override\n  public void onResume(HttpDownBootstrap httpDownBootstrap) {\n    super.onResume(httpDownBootstrap);\n    commonHook(httpDownBootstrap, HookScript.EVENT_RESUME, false);\n  }\n\n  @Override\n  public void onPause(HttpDownBootstrap httpDownBootstrap) {\n    super.onPause(httpDownBootstrap);\n    commonHook(httpDownBootstrap, HookScript.EVENT_PAUSE, false);\n  }\n\n  @Override\n  public void onError(HttpDownBootstrap httpDownBootstrap) {\n    super.onError(httpDownBootstrap);\n    commonHook(httpDownBootstrap, HookScript.EVENT_ERROR, true);\n  }\n\n  @Override\n  public void onDone(HttpDownBootstrap httpDownBootstrap) {\n    super.onDone(httpDownBootstrap);\n    commonHook(httpDownBootstrap, HookScript.EVENT_DONE, true);\n  }\n\n  private void commonHook(HttpDownBootstrap httpDownBootstrap, String event, boolean async) {\n    DownInfo downInfo = findDownInfo(httpDownBootstrap);\n    Map<String, Object> taskInfo = buildTaskInfo(downInfo);\n    if (taskInfo != null) {\n      //遍历扩展模块是否有对应的处理\n      List<ExtensionInfo> extensionInfos = ExtensionContent.get();\n      for (ExtensionInfo extensionInfo : extensionInfos) {\n        if (extensionInfo.getMeta().isEnabled()) {\n          if (extensionInfo.getHookScript() != null\n              && !StringUtils.isEmpty(extensionInfo.getHookScript().getScript())) {\n            Event e = extensionInfo.getHookScript().hasEvent(event, HttpDownUtil.getUrl(httpDownBootstrap.getRequest()));\n            if (e != null) {\n              try {\n                //执行钩子函数\n                Object result = ExtensionUtil.invoke(extensionInfo, e, taskInfo, async);\n                if (result != null) {\n                  ObjectMapper objectMapper = new ObjectMapper();\n                  String temp = objectMapper.writeValueAsString(result);\n                  TaskForm taskForm = objectMapper.readValue(temp, TaskForm.class);\n                  if (taskForm.getRequest() != null) {\n                    httpDownBootstrap.setRequest(\n                        HttpDownUtil.buildRequest(taskForm.getRequest().getMethod(),\n                            taskForm.getRequest().getUrl(),\n                            taskForm.getRequest().getHeads(),\n                            taskForm.getRequest().getBody())\n                    );\n                  }\n                  if (taskForm.getResponse() != null) {\n                    httpDownBootstrap.setResponse(taskForm.getResponse());\n                  }\n                  if (taskForm.getData() != null) {\n                    downInfo.setData(taskForm.getData());\n                  }\n                  HttpDownContent.getInstance().save();\n                }\n              } catch (Exception ex) {\n                LOGGER.error(\"An hook exception occurred while \" + event + \"()\", ex);\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n\n  private Map<String, Object> buildTaskInfo(DownInfo downInfo) {\n    if (downInfo != null) {\n      Map<String, Object> taskForm = new HashMap<>();\n      taskForm.put(\"id\", downInfo.getId());\n      taskForm.put(\"data\", clone(downInfo.getData(), new HashMap<String, Object>()));\n      taskForm.put(\"request\", HttpRequestForm.parse(downInfo.getBootstrap().getRequest()));\n      taskForm.put(\"response\", clone(downInfo.getBootstrap().getResponse(), new HttpResponseInfo()));\n      return taskForm;\n    }\n    return null;\n  }\n\n  private Object clone(Object source, Object target) {\n    if (source != null && target != null) {\n      BeanUtils.copyProperties(source, target);\n      return target;\n    }\n    return null;\n  }\n\n}\n"
  },
  {
    "path": "main/src/main/java/org/pdown/gui/update/CheckUpdate.java",
    "content": "package org.pdown.gui.update;\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport java.net.HttpURLConnection;\nimport java.net.URL;\nimport org.pdown.gui.util.ConfigUtil;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\npublic class CheckUpdate {\n\n  private static final Logger LOGGER = LoggerFactory.getLogger(CheckUpdate.class);\n\n  public static VersionInfo doCheck() {\n    String adminServer = ConfigUtil.getString(\"adminServer\");\n    double currVersion = Double.parseDouble(ConfigUtil.getString(\"version\"));\n    try {\n      URL url = new URL(adminServer + \"checkUpdate\");\n      HttpURLConnection connection = (HttpURLConnection) url.openConnection();\n      if (connection.getResponseCode() == 200) {\n        ObjectMapper objectMapper = new ObjectMapper();\n        VersionInfo versionInfo = objectMapper.readValue(connection.getInputStream(), VersionInfo.class);\n        if (versionInfo.getVersion() > currVersion) {\n          return versionInfo;\n        }\n      }\n    } catch (Exception e) {\n      LOGGER.warn(\"Check update error\", e);\n    }\n    return null;\n  }\n}\n"
  },
  {
    "path": "main/src/main/java/org/pdown/gui/update/VersionInfo.java",
    "content": "package org.pdown.gui.update;\n\npublic class VersionInfo {\n  private double version;\n  private String path;\n\n  public double getVersion() {\n    return version;\n  }\n\n  public VersionInfo setVersion(double version) {\n    this.version = version;\n    return this;\n  }\n\n  public String getPath() {\n    return path;\n  }\n\n  public VersionInfo setPath(String path) {\n    this.path = path;\n    return this;\n  }\n}\n"
  },
  {
    "path": "main/src/main/java/org/pdown/gui/util/AppUtil.java",
    "content": "package org.pdown.gui.util;\n\nimport java.io.File;\nimport java.io.FileOutputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.net.Authenticator;\nimport java.net.HttpURLConnection;\nimport java.net.InetSocketAddress;\nimport java.net.PasswordAuthentication;\nimport java.net.Proxy;\nimport java.net.Proxy.Type;\nimport java.net.URL;\nimport org.pdown.core.boot.HttpDownBootstrap;\nimport org.pdown.core.boot.URLHttpDownBootstrapBuilder;\nimport org.pdown.core.dispatch.HttpDownCallback;\nimport org.pdown.core.entity.HttpDownConfigInfo;\nimport org.pdown.core.entity.HttpResponseInfo;\nimport org.pdown.core.proxy.ProxyConfig;\nimport org.pdown.core.proxy.ProxyType;\nimport org.pdown.core.util.FileUtil;\nimport org.pdown.core.util.OsUtil;\nimport org.pdown.gui.DownApplication;\nimport org.pdown.gui.content.PDownConfigContent;\nimport org.pdown.gui.extension.mitm.server.PDownProxyServer;\nimport org.pdown.gui.extension.mitm.util.ExtensionCertUtil;\nimport org.pdown.gui.extension.mitm.util.ExtensionProxyUtil;\nimport org.pdown.rest.util.PathUtil;\nimport org.springframework.util.StringUtils;\n\npublic class AppUtil {\n\n  public static final String SUBJECT = \"ProxyeeDown CA\";\n  public static final String SSL_PATH = PathUtil.ROOT_PATH + File.separator + \"ssl\" + File.separator;\n  public static final String CERT_PATH = SSL_PATH + \"ca.crt\";\n  public static final String PRIVATE_PATH = SSL_PATH + \".ca_pri.der\";\n\n  /**\n   * 证书和私钥文件都存在并且检测到系统安装了这个证书\n   */\n  public static boolean checkIsInstalledCert() throws Exception {\n    return FileUtil.exists(CERT_PATH)\n        && FileUtil.exists(PRIVATE_PATH)\n        && ExtensionCertUtil.isInstalledCert(new File(CERT_PATH));\n  }\n\n  /**\n   * 刷新系统PAC代理\n   */\n  public static void refreshPAC() throws IOException {\n    if (PDownConfigContent.getInstance().get().getProxyMode() == 1) {\n      ExtensionProxyUtil.enabledPACProxy(\"http://127.0.0.1:\" + DownApplication.INSTANCE.API_PORT + \"/pac/pdown.pac?t=\" + System.currentTimeMillis());\n    }\n  }\n\n  /**\n   * 运行代理服务器\n   */\n  public static void startProxyServer() throws IOException {\n    DownApplication.INSTANCE.PROXY_PORT = OsUtil.getFreePort(9999);\n    PDownProxyServer.start(DownApplication.INSTANCE.PROXY_PORT);\n  }\n\n  /**\n   * 下载http资源\n   */\n  public static void download(String url, String path) throws IOException {\n    URL u = new URL(url);\n    HttpURLConnection connection;\n    ProxyConfig proxyConfig = PDownConfigContent.getInstance().get().getProxyConfig();\n    if (proxyConfig != null) {\n      Type type;\n      if (proxyConfig.getProxyType() == ProxyType.HTTP) {\n        type = Type.HTTP;\n      } else {\n        type = Type.SOCKS;\n      }\n      if (!StringUtils.isEmpty(proxyConfig.getUser())) {\n        Authenticator authenticator = new Authenticator() {\n          public PasswordAuthentication getPasswordAuthentication() {\n            return new PasswordAuthentication(proxyConfig.getUser(),\n                proxyConfig.getPwd() == null ? null : proxyConfig.getPwd().toCharArray());\n          }\n        };\n        Authenticator.setDefault(authenticator);\n      }\n      Proxy proxy = new Proxy(type, new InetSocketAddress(proxyConfig.getHost(), proxyConfig.getPort()));\n      connection = (HttpURLConnection) u.openConnection(proxy);\n    } else {\n      connection = (HttpURLConnection) u.openConnection();\n    }\n    connection.setConnectTimeout(30000);\n    connection.setReadTimeout(0);\n    File file = new File(path);\n    if (!file.exists() || file.isDirectory()) {\n      FileUtil.createFileSmart(file.getPath());\n    }\n    try (\n        InputStream input = connection.getInputStream();\n        FileOutputStream output = new FileOutputStream(file)\n    ) {\n      byte[] bts = new byte[8192];\n      int len;\n      while ((len = input.read(bts)) != -1) {\n        output.write(bts, 0, len);\n      }\n    }\n  }\n\n  /**\n   * 使用pdown-core多连接下载http资源\n   */\n  public static HttpDownBootstrap fastDownload(String url, File file, HttpDownCallback callback) throws IOException {\n    HttpDownBootstrap httpDownBootstrap = new URLHttpDownBootstrapBuilder(url, null, null)\n        .callback(callback)\n        .downConfig(new HttpDownConfigInfo().setFilePath(file.getParent()).setConnections(64))\n        .response(new HttpResponseInfo().setFileName(file.getName()))\n        .proxyConfig(PDownConfigContent.getInstance().get().getProxyConfig())\n        .build();\n    httpDownBootstrap.start();\n    return httpDownBootstrap;\n  }\n}\n"
  },
  {
    "path": "main/src/main/java/org/pdown/gui/util/ConfigUtil.java",
    "content": "package org.pdown.gui.util;\n\nimport java.util.Map;\nimport java.util.Map.Entry;\nimport org.yaml.snakeyaml.Yaml;\n\npublic class ConfigUtil {\n\n  private static Map<String, Object> map = null;\n\n  static {\n    Yaml yaml = new Yaml();\n    map = yaml.load(Thread.currentThread().getContextClassLoader().getResourceAsStream(\"application.yml\"));\n    String active = getString(\"spring.profiles.active\");\n    merge(map, yaml.load(Thread.currentThread().getContextClassLoader().getResourceAsStream(\"application-\" + active + \".yml\")));\n  }\n\n  public static String getString(String key) {\n    return getString(map, key);\n  }\n\n  public static boolean getBoolean(String key) {\n    return getBoolean(map, key);\n  }\n\n  public static int getInt(String key) {\n    return getInt(map, key);\n  }\n\n  static Object get(Map<String, Object> map, String key) {\n    String[] keyArray = key.split(\"\\\\.\");\n    if (keyArray.length == 1) {\n      return map.get(key);\n    } else {\n      for (int i = 0; i < keyArray.length - 1; i++) {\n        map = (Map<String, Object>) get(map, keyArray[i]);\n      }\n      return map.get(keyArray[keyArray.length - 1]);\n    }\n  }\n\n  private static void merge(Map<String, Object> map1, Map<String, Object> map2) {\n    for (Entry<String, Object> entry : map2.entrySet()) {\n      if (map1.containsKey(entry.getKey())) {\n        if (entry.getValue() instanceof Map && map2.get(entry.getKey()) instanceof Map) {\n          merge(map1, (Map<String, Object>) map2.get(entry.getKey()));\n        } else {\n          map1.put(entry.getKey(), map2.get(entry.getKey()));\n        }\n      } else {\n        map1.put(entry.getKey(), map2.get(entry.getKey()));\n      }\n    }\n  }\n\n  private static String getString(Map<String, Object> map, String key) {\n    return String.valueOf(get(map, key));\n  }\n\n  private static boolean getBoolean(Map<String, Object> map, String key) {\n    return Boolean.valueOf(get(map, key).toString());\n  }\n\n  private static int getInt(Map<String, Object> map, String key) {\n    return Integer.valueOf(get(map, key).toString());\n  }\n\n}\n"
  },
  {
    "path": "main/src/main/java/org/pdown/gui/util/ExecUtil.java",
    "content": "package org.pdown.gui.util;\n\nimport java.io.BufferedReader;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.net.HttpURLConnection;\nimport java.net.MalformedURLException;\nimport java.net.URL;\nimport java.nio.charset.Charset;\nimport org.pdown.core.util.OsUtil;\n\npublic class ExecUtil {\n\n  /**\n   * 执行shell并返回标准输出文本内容\n   */\n  public static String exec(String... shell) throws IOException {\n    Process process = Runtime.getRuntime().exec(shell);\n    StringBuilder sb = new StringBuilder();\n    Charset charset = OsUtil.isWindows() ? Charset.forName(\"GBK\") : Charset.defaultCharset();\n    try (\n        BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream(), charset))\n    ) {\n      String line;\n      while ((line = reader.readLine()) != null) {\n        sb.append(line + System.lineSeparator());\n      }\n    } finally {\n      process.destroy();\n    }\n    return sb.toString();\n  }\n\n  /**\n   * 同步执行shell，阻塞当前线程\n   */\n  public static void execBlock(String... shell) throws IOException {\n    Process process = Runtime.getRuntime().exec(shell);\n    try (\n        InputStream inputStream = process.getInputStream()\n    ) {\n      byte[] bytes = new byte[8192];\n      while ((inputStream.read(bytes)) != -1) {\n        //Do nothing\n      }\n    } finally {\n      process.destroy();\n    }\n  }\n\n  /**\n   * 以管理员权限，同步执行shell，阻塞当前线程\n   */\n  public static void execBlockWithAdmin(String shell) throws IOException {\n    //osascript -e \"do shell script \\\"shell\\\" with administrator privileges\"\n    Process process = Runtime.getRuntime().exec(new String[]{\n        \"osascript\",\n        \"-e\",\n        \"do shell script \\\"\" +\n            shell +\n            \"\\\"\" +\n            \"with administrator privileges\"\n    });\n    try (\n        InputStream inputStream = process.getInputStream()\n    ) {\n      byte[] bytes = new byte[8192];\n      while ((inputStream.read(bytes)) != -1) {\n        //Do nothing\n      }\n    } finally {\n      process.destroy();\n    }\n  }\n\n  public static void httpGet(String url) throws IOException {\n    URL u = new URL(url);\n    HttpURLConnection connection = (HttpURLConnection) u.openConnection();\n    if (connection.getResponseCode() != 200) {\n      throw new RuntimeException(\"http get error:\" + url);\n    }\n  }\n\n\n  public static void main(String[] args) throws Exception {\n    httpGet(\"http://www.baidu.com\");\n  }\n}\n"
  },
  {
    "path": "main/src/main/java/org/pdown/gui/util/I18nUtil.java",
    "content": "package org.pdown.gui.util;\n\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.IOException;\nimport java.net.JarURLConnection;\nimport java.net.URL;\nimport java.net.URLConnection;\nimport java.text.MessageFormat;\nimport java.util.Enumeration;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.jar.JarEntry;\nimport java.util.jar.JarFile;\nimport org.pdown.gui.content.PDownConfigContent;\nimport org.yaml.snakeyaml.Yaml;\n\npublic class I18nUtil {\n\n  private static final String DEFAULT_LOCALE = \"zh-CN\";\n  private static Map<String, Map<String, Object>> map;\n\n  static {\n    Yaml yaml = new Yaml();\n    map = new HashMap<>();\n    try {\n      ClassLoader classLoader = Thread.currentThread().getContextClassLoader();\n      URL url = classLoader.getResource(\"i18n\");\n      URLConnection connection = url.openConnection();\n      if (connection instanceof JarURLConnection) {\n        JarURLConnection jarURLConnection = (JarURLConnection) connection;\n        JarFile jarFile = jarURLConnection.getJarFile();\n        Enumeration<JarEntry> entries = jarFile.entries();\n        while (entries.hasMoreElements()) {\n          JarEntry entry = entries.nextElement();\n          if (entry.getName().matches(\"^i18n/[^/]+\\\\.yml$\")) {\n            map.put(takeLocale(entry.getName()), yaml.load(jarFile.getInputStream(entry)));\n          }\n        }\n        jarFile.close();\n      } else {\n        File dir = new File(url.getPath());\n        for (File message : dir.listFiles()) {\n          map.put(takeLocale(message.getName()), yaml.load(new FileInputStream(message)));\n        }\n      }\n    } catch (IOException e) {\n    }\n  }\n\n  private static String takeLocale(String name) {\n    return name.substring(name.lastIndexOf(\"_\") + 1, name.length() - 4);\n  }\n\n  public static String getMessage(String key, Object... args) {\n    String locale = null;\n    if (PDownConfigContent.getInstance().get() != null) {\n      locale = PDownConfigContent.getInstance().get().getLocale();\n    }\n    if (locale == null || !map.containsKey(locale)) {\n      locale = DEFAULT_LOCALE;\n    }\n    if (map == null || map.size() == 0) {\n      return key;\n    }\n    Map<String, Object> localeMap = map.get(locale);\n    if (localeMap == null) {\n      return key;\n    }\n    return MessageFormat.format(ConfigUtil.get(localeMap, key).toString(), args);\n  }\n\n  public static void main(String[] args) {\n    PDownConfigContent.getInstance().load();\n    System.out.println(getMessage(\"gui.alert.startError\", \"test\"));\n  }\n}\n"
  },
  {
    "path": "main/src/main/resources/application-dev.yml",
    "content": "logging:\n  config: classpath:logback-dev.xml\nfront:\n  port: 8080\napi:\n  port: 7478\n#adminServer: http://127.0.0.1:9494/\nadminServer: http://api.pdown.org/"
  },
  {
    "path": "main/src/main/resources/application-prd.yml",
    "content": "logging:\n  config: classpath:logback-prd.xml\nfront:\n  port: -1\napi:\n  port: 7478\nadminServer: http://api.pdown.org/"
  },
  {
    "path": "main/src/main/resources/application.yml",
    "content": "version: 3.41\nspring:\n  profiles:\n    active: @environment@\n  messages:\n    basename: i18n/messages"
  },
  {
    "path": "main/src/main/resources/banner.txt",
    "content": "                                                          _\n                                                         | |\n _ __   _ __   ___  __  __ _   _   ___   ___  ______   __| |  ___  __      __ _ __\n| '_ \\ | '__| / _ \\ \\ \\/ /| | | | / _ \\ / _ \\|______| / _` | / _ \\ \\ \\ /\\ / /| '_ \\\n| |_) || |   | (_) | >  < | |_| ||  __/|  __/        | (_| || (_) | \\ V  V / | | | |\n| .__/ |_|    \\___/ /_/\\_\\ \\__, | \\___| \\___|         \\__,_| \\___/   \\_/\\_/  |_| |_|\n| |                         __/ |\n|_|                        |___/"
  },
  {
    "path": "main/src/main/resources/extension/runtime.js",
    "content": "(function () {\n    var API_PORT = '${apiPort}'\n    var FRONT_PORT = '${frontPort}'\n    var REST_PORT = '26339'\n    var ajax = {\n        buildXHR: function () {\n            var xhr = null\n            if (window.XMLHttpRequest) {\n                xhr = new XMLHttpRequest()\n            } else {\n                xhr = new ActiveXObject('Microsoft.XMLHTTP')\n            }\n            return xhr\n        },\n        proxySend: function (async, method, url, data, onSuccess, onError) {\n            this.proxySend2(async, method, url, null, data, onSuccess, onError)\n        },\n        proxySend2: function (async, method, url, heads, data, onSuccess, onError) {\n            var xhr = this.buildXHR()\n            xhr.onreadystatechange = function () {\n                if (xhr.readyState === 4) {\n                    if (xhr.status === 200) {\n                        if (onSuccess) {\n                            onSuccess(xhr.responseText ? JSON.parse(xhr.responseText) : {})\n                        }\n                    } else if (onError) {\n                        onError(xhr)\n                    }\n                }\n            }\n            //是浏览器环境，需要通过代理服务器来避免跨域安全问题\n            if (window.navigator) {\n                xhr.open('post', '/', async)\n                var req = {method: method, url: url, heads: heads}\n                if (data) {\n                    if (typeof data == 'string') {\n                        req.rawData = data\n                    } else {\n                        req.data = data\n                    }\n                }\n                xhr.setRequestHeader('X-Proxy-Send', encodeURIComponent(JSON.stringify(req)))\n                xhr.send()\n            } else {\n                xhr.open(method, url, async)\n                xhr.setRequestHeader('Content-Type', 'application/json; charset=utf-8')\n                xhr.send(data ? JSON.stringify(data) : null)\n            }\n        },\n        get: function (url) {\n            return this.send('get', url)\n        },\n        getAsync: function (url, onSuccess, onError) {\n            return this.sendAsync('get', url, null, onSuccess, onError)\n        },\n        post: function (url, data) {\n            return this.send('post', url, data)\n        },\n        postAsync: function (url, data, onSuccess, onError) {\n            this.sendAsync('post', url, data, onSuccess, onError)\n        },\n        put: function (url, data) {\n            return this.send('put', url, data)\n        },\n        putAsync: function (url, data, onSuccess, onError) {\n            this.sendAsync('put', url, data, onSuccess, onError)\n        },\n        delete: function (url, data) {\n            return this.send('delete', url, data)\n        },\n        deleteAsync: function (url, data, onSuccess, onError) {\n            this.sendAsync('delete', url, data, onSuccess, onError)\n        },\n        send: function (method, url, data) {\n            var result = null\n            var error = null\n            this.proxySend(false, method, url, data, function (data) {\n                result = data\n            }, function (xhr) {\n                error = xhr\n            })\n            if (error) {\n                throw error\n            }\n            return result\n        },\n        sendAsync: function (method, url, data, onSuccess, onError) {\n            this.proxySend(true, method, url, data, onSuccess, onError)\n        }\n    }\n    return {\n        version: '${version}',\n        settings: ${settings},\n        fetchAsync: function (request, onSuccess, onError) {\n            return ajax.proxySend2(true, request.method, request.url, request.heads, request.data, onSuccess, onError)\n        },\n        resolve: function (request) {\n            return ajax.put('http://127.0.0.1:' + REST_PORT + '/util/resolve', request)\n        },\n        resolveAsync: function (request, onSuccess, onError) {\n            ajax.putAsync('http://127.0.0.1:' + REST_PORT + '/util/resolve', request, onSuccess, onError)\n        },\n        createTask: function () {\n            var request\n            var response\n            var config\n            var data\n            if (arguments.length == 1) {\n                var taskForm = arguments[0]\n                request = taskForm.request\n                response = taskForm.response\n                config = taskForm.config\n                data = taskForm.data\n            } else if (arguments.length == 2) {\n                request = arguments[0]\n                response = arguments[1]\n            } else {\n                return\n            }\n            var requestStr = encodeURIComponent(JSON.stringify(request))\n            var responseStr = encodeURIComponent(JSON.stringify(response))\n            var configStr = config ? encodeURIComponent(JSON.stringify(config)) : ''\n            var dataStr = data ? encodeURIComponent(JSON.stringify(data)) : ''\n            if ('${uiMode}' == '1') {\n                ajax.get('http://127.0.0.1:' + API_PORT + '/api/createTask?request=' + requestStr + '&response=' + responseStr + '&config=' + configStr + '&data=' + dataStr)\n            } else {\n                window.open('http://127.0.0.1:' + FRONT_PORT + '/#/tasks?request=' + requestStr + '&response=' + responseStr + '&config=' + configStr + '&data=' + dataStr)\n            }\n        },\n        createTaskAsync: function (taskForm, onSuccess, onError) {\n            var requestStr = encodeURIComponent(encodeURIComponent(JSON.stringify(taskForm.request)))\n            var responseStr = encodeURIComponent(encodeURIComponent(JSON.stringify(taskForm.response)))\n            var configStr = taskForm.config ? encodeURIComponent(encodeURIComponent(JSON.stringify(taskForm.config))) : ''\n            var dataStr = taskForm.data ? encodeURIComponent(encodeURIComponent(JSON.stringify(taskForm.data))) : ''\n            if ('${uiMode}' == '1') {\n                ajax.getAsync('http://127.0.0.1:' + API_PORT + '/api/createTask?request=' + requestStr + '&response=' + responseStr + '&config=' + configStr + '&data=' + dataStr, onSuccess, onError)\n            } else {\n                window.open('http://127.0.0.1:' + FRONT_PORT + '/#/tasks?request=' + requestStr + '&response=' + responseStr + '&config=' + configStr + '&data=' + dataStr)\n            }\n        },\n        pushTask: function (taskForm, onSuccess, onError) {\n            ajax.postAsync('http://127.0.0.1:' + REST_PORT + '/tasks?refresh=true', taskForm, onSuccess, onError)\n        },\n        refreshTask: function (id, request) {\n            return ajax.put('http://127.0.0.1:' + REST_PORT + '/tasks/' + id, request)\n        },\n        refreshTaskAsync: function (id, request, onSuccess, onError) {\n            ajax.putAsync('http://127.0.0.1:' + REST_PORT + '/tasks/' + id, request, onSuccess, onError)\n        },\n        pauseTask: function (id) {\n            return ajax.put('http://127.0.0.1:' + REST_PORT + '/tasks/' + id + '/pause')\n        },\n        pauseTaskAsync: function (id, onSuccess, onError) {\n            ajax.putAsync('http://127.0.0.1:' + REST_PORT + '/tasks/' + id + '/pause', null, onSuccess, onError)\n        },\n        resumeTask: function (id) {\n            return ajax.put('http://127.0.0.1:' + REST_PORT + '/tasks/' + id + '/resume')\n        },\n        resumeTaskAsync: function (id, onSuccess, onError) {\n            ajax.putAsync('http://127.0.0.1:' + REST_PORT + '/tasks/' + id + '/resume', null, onSuccess, onError)\n        },\n        deleteTask: function (id, delFile) {\n            return ajax.delete('http://127.0.0.1:' + REST_PORT + '/tasks/' + id + '?delFile=' + !!delFile)\n        },\n        deleteTaskAsync: function (id, delFile, onSuccess, onError) {\n            ajax.deleteAsync('http://127.0.0.1:' + REST_PORT + '/tasks/' + id + '?delFile=' + !!delFile, null, onSuccess, onError)\n        },\n        getDownConfig: function () {\n            var config = ajax.get('http://127.0.0.1:' + REST_PORT + '/config')\n            delete config['port']\n            delete config['proxyConfig']\n            delete config['speedLimit']\n            delete config['taskLimit']\n            delete config['totalSpeedLimit']\n            return config\n        },\n        getDownConfigAsync: function (onSuccess, onError) {\n            ajax.getAsync('http://127.0.0.1:' + REST_PORT + '/config', function (config) {\n                delete config['port']\n                delete config['proxyConfig']\n                delete config['speedLimit']\n                delete config['taskLimit']\n                delete config['totalSpeedLimit']\n                if (onSuccess) {\n                    onSuccess(config)\n                }\n            }, onError)\n        },\n        getCookie: function (url) {\n            var cookie = ''\n            var xhr = ajax.buildXHR()\n            xhr.withCredentials = true\n            xhr.open('get', url, false)\n            //https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Access_control_CORS#%E7%AE%80%E5%8D%95%E8%AF%B7%E6%B1%82\n            xhr.setRequestHeader('Accept', 'application/x-sniff-cookie,*/*;q=0.8')\n            xhr.onreadystatechange = function () {\n                if (xhr.readyState === 4) {\n                    if (xhr.status === 200) {\n                        cookie = xhr.getResponseHeader('X-Sniff-Cookie')\n                    }\n                }\n            }\n            xhr.send()\n            return cookie\n        },\n        getCookieAsync: function (url, onSuccess, onError) {\n            var xhr = ajax.buildXHR()\n            xhr.withCredentials = true\n            xhr.open('get', url, true)\n            xhr.setRequestHeader('Accept', 'application/x-sniff-cookie,*/*;q=0.8')\n            xhr.onreadystatechange = function () {\n                if (xhr.readyState === 4) {\n                    if (xhr.status === 200) {\n                        var cookie = xhr.getResponseHeader('X-Sniff-Cookie')\n                        if (onSuccess) {\n                            onSuccess(cookie)\n                        }\n                    } else if (onError) {\n                        onError(xhr)\n                    }\n                }\n            }\n            xhr.send()\n        }\n    }\n})()"
  },
  {
    "path": "main/src/main/resources/i18n/messages_en-US.yml",
    "content": "gui:\n  warning: Warning\n  alert:\n    startError: Startup error:{0}\n    restPortBusy: Port 26339 is busy, please do not run multiple instances of the app.\n    noFreePort: The system cannot allocate the TCP port\n  menu:\n    copy: Copy\n    paste: Paste\n  tray:\n    show: Show\n    set: Settings\n    about: About\n    support: Donate\n    exit: Exit\n"
  },
  {
    "path": "main/src/main/resources/i18n/messages_zh-CN.yml",
    "content": "gui:\n  warning: 警告\n  alert:\n    startError: 启动失败：{0}\n    restPortBusy: 端口26339被占用，请勿重复启动软件\n    noFreePort: 系统无法分配TCP端口\n  menu:\n    copy: 复制\n    paste: 粘贴\n  tray:\n    show: 显示\n    set: 设置\n    about: 关于\n    support: 打赏\n    exit: 退出"
  },
  {
    "path": "main/src/main/resources/i18n/messages_zh-TW.yml",
    "content": "gui:\n  warning: 警告\n  alert:\n    startError: 啟動失敗：{0}\n    restPortBusy: 連接埠 26339 被占用，請勿重複啟動軟體\n    noFreePort: 系統無法指派 TCP 連接埠\n  menu:\n    copy: 複製\n    paste: 貼上\n  tray:\n    show: 顯示\n    set: 設定\n    about: 關於\n    support: 打賞\n    exit: 結束"
  },
  {
    "path": "main/src/main/resources/logback-dev.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<configuration>\n  <appender name=\"stdout\" class=\"ch.qos.logback.core.ConsoleAppender\">\n    <encoder class=\"ch.qos.logback.classic.encoder.PatternLayoutEncoder\">\n      <!--格式化输出：%d表示日期，%thread表示线程名，%-5level：级别从左显示5个字符宽度%msg：日志消息，%n是换行符-->\n      <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>\n    </encoder>\n  </appender>\n  <root level=\"ERROR\">\n    <appender-ref ref=\"stdout\"/>\n  </root>\n  <logger name=\"org.pdown.gui\" level=\"DEBUG\"/>\n  <logger name=\"org.springframework.web.filter.CommonsRequestLoggingFilter\" level=\"DEBUG\"/>\n  <logger name=\"org.springframework.boot.web.embedded.tomcat.TomcatWebServer\" level=\"DEBUG\"/>\n</configuration>\n"
  },
  {
    "path": "main/src/main/resources/logback-prd.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<configuration>\n  <appender name=\"stdout\" class=\"ch.qos.logback.core.ConsoleAppender\">\n    <encoder class=\"ch.qos.logback.classic.encoder.PatternLayoutEncoder\">\n      <!--格式化输出：%d表示日期，%thread表示线程名，%-5level：级别从左显示5个字符宽度%msg：日志消息，%n是换行符-->\n      <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>\n    </encoder>\n  </appender>\n  <appender name=\"file\" class=\"ch.qos.logback.core.rolling.RollingFileAppender\">\n    <rollingPolicy class=\"ch.qos.logback.core.rolling.TimeBasedRollingPolicy\">\n      <fileNamePattern>${ROOT_PATH}/log/pdown-error.%d{yyyy-MM-dd}.log</fileNamePattern>\n      <maxHistory>7</maxHistory>\n    </rollingPolicy>\n    <encoder class=\"ch.qos.logback.classic.encoder.PatternLayoutEncoder\">\n      <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{80} - %msg%n</pattern>\n    </encoder>\n  </appender>\n  <root level=\"ERROR\">\n    <appender-ref ref=\"file\"/>\n  </root>\n  <logger name=\"org.springframework.boot.web.embedded.tomcat.TomcatWebServer\" level=\"DEBUG\" additivity=\"false\">\n    <appender-ref ref=\"stdout\"/>\n  </logger>\n</configuration>\n"
  },
  {
    "path": "pom.xml",
    "content": "<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n  xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n  xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n  <modelVersion>4.0.0</modelVersion>\n  <groupId>org.pdown.gui</groupId>\n  <artifactId>proxyee-down</artifactId>\n  <version>3.0</version>\n  <packaging>pom</packaging>\n\n  <modules>\n    <module>main</module>\n    <module>runner</module>\n  </modules>\n  <name>proxyee-down</name>\n  <url>https://github.com/proxyee-down-org/proxyee-down</url>\n\n  <properties>\n    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>\n    <maven.compiler.target>1.8</maven.compiler.target>\n    <maven.compiler.source>1.8</maven.compiler.source>\n  </properties>\n</project>\n"
  },
  {
    "path": "runner/.gitignore",
    "content": "﻿target/\n!.mvn/wrapper/maven-wrapper.jar\n\n### STS ###\n.apt_generated\n.classpath\n.factorypath\n.project\n.settings\n.springBeans\n\n### IntelliJ IDEA ###\n.idea\n*.iws\n*.iml\n*.ipr"
  },
  {
    "path": "runner/pom.xml",
    "content": "<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n  xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n  xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n  <modelVersion>4.0.0</modelVersion>\n  <parent>\n    <groupId>org.pdown.gui</groupId>\n    <artifactId>proxyee-down</artifactId>\n    <version>3.0</version>\n  </parent>\n\n  <groupId>org.pdown.gui</groupId>\n  <artifactId>runner</artifactId>\n  <packaging>jar</packaging>\n\n  <repositories>\n    <repository>\n      <id>oss</id>\n      <url>https://oss.sonatype.org/content/repositories/snapshots</url>\n      <snapshots>\n        <enabled>true</enabled>\n      </snapshots>\n    </repository>\n  </repositories>\n\n  <build>\n    <finalName>proxyee-down-runner</finalName>\n    <plugins>\n      <plugin>\n        <artifactId>maven-assembly-plugin</artifactId>\n        <configuration>\n          <appendAssemblyId>false</appendAssemblyId>\n          <descriptorRefs>\n            <descriptorRef>jar-with-dependencies</descriptorRef>\n          </descriptorRefs>\n          <archive>\n            <manifest>\n              <mainClass>org.pdown.gui.Runner</mainClass>\n            </manifest>\n          </archive>\n        </configuration>\n        <executions>\n          <execution>\n            <id>make-assembly</id>\n            <phase>package</phase>\n            <goals>\n              <goal>assembly</goal>\n            </goals>\n          </execution>\n        </executions>\n      </plugin>\n    </plugins>\n  </build>\n</project>\n"
  },
  {
    "path": "runner/src/main/java/org/pdown/gui/Runner.java",
    "content": "package org.pdown.gui;\n\nimport java.io.BufferedReader;\nimport java.io.BufferedWriter;\nimport java.io.File;\nimport java.io.FileOutputStream;\nimport java.io.IOException;\nimport java.io.InputStreamReader;\nimport java.io.OutputStreamWriter;\nimport java.nio.file.Files;\nimport java.nio.file.Paths;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.List;\nimport javax.swing.JOptionPane;\n\npublic class Runner {\n\n  private static final String JAVA_CMD_PATH = System.getProperty(\"java.home\") + File.separator + \"bin\" + File.separator + \"java\";\n  private static final String MAIN_JAR_PATH = \"main/proxyee-down-main.jar\";\n  private static final String MAIN_JAR_BAK_PATH = MAIN_JAR_PATH + \".bak\";\n  private static final String VM_OPTIONS_PATH = \"main/run.cfg\";\n\n  private static List<String> VM_OPTIONS;\n\n  public static void main(String[] args) throws IOException {\n    VM_OPTIONS = parseVmOptions();\n    fork();\n  }\n\n  private static List<String> parseVmOptions() {\n    File file = Paths.get(VM_OPTIONS_PATH).toFile();\n    if (!file.exists()) {\n      try {\n        file.createNewFile();\n        try (\n            BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file)))\n        ) {\n          writer.write(\"-Xms128m\");\n          writer.newLine();\n          writer.write(\"-Xmx384m\");\n        } catch (Exception e) {\n          e.printStackTrace();\n        }\n      } catch (Exception e) {\n        e.printStackTrace();\n      }\n    }\n    try {\n      return Files.readAllLines(Paths.get(VM_OPTIONS_PATH));\n    } catch (IOException e) {\n    }\n    return null;\n  }\n\n  private static void fork() {\n    File bakFile = new File(MAIN_JAR_BAK_PATH);\n    if (bakFile.exists()) {\n      //更新后删除旧版本\n      for (int i = 0; i < 30; i++) {\n        if (new File(MAIN_JAR_PATH).delete()) {\n          bakFile.renameTo(new File(MAIN_JAR_PATH));\n          break;\n        } else {\n          try {\n            Thread.sleep(1000);\n          } catch (InterruptedException e) {\n          }\n        }\n      }\n    }\n    try {\n      List<String> execParams = new ArrayList<>();\n      execParams.add(JAVA_CMD_PATH);\n      execParams.add(\"-jar\");\n      if (VM_OPTIONS == null) {\n        execParams.add(\"-Xms128m\");\n        execParams.add(\"-Xmx384m\");\n      } else {\n        for (String option : VM_OPTIONS) {\n          execParams.add(option);\n        }\n      }\n      execParams.add(MAIN_JAR_PATH);\n      String[] execArray = new String[execParams.size()];\n      execParams.toArray(execArray);\n      Process process = Runtime.getRuntime().exec(execArray);\n      BufferedReader br = new BufferedReader(new InputStreamReader(process.getInputStream()));\n      String line;\n      boolean isClose = false;\n      while ((line = br.readLine()) != null) {\n        System.out.println(line);\n        if (\"proxyee-down-exit\".equals(line)) {\n          isClose = true;\n          break;\n        }\n      }\n      if (isClose) {\n        process.destroy();\n        fork();\n      }\n    } catch (Throwable throwable) {\n      alert(throwable.getMessage());\n      System.exit(1);\n    }\n  }\n\n  private static void alert(String msg) {\n    JOptionPane.showMessageDialog(null, msg, \"title\", JOptionPane.ERROR_MESSAGE);\n  }\n}\n"
  }
]