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