├── .github └── ISSUE_TEMPLATE.md ├── .gitignore ├── LICENSE ├── README.md ├── front ├── .eslintrc.js ├── .gitignore ├── .postcssrc.js ├── .prettierrc ├── .vscode │ └── settings.json ├── babel.config.js ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ ├── pay │ │ ├── alipay.png │ │ └── weipay.png │ └── team_header │ │ ├── Black-Hole.png │ │ ├── NISAL.png │ │ └── monkeyWie.png ├── 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 │ ├── linux │ └── logo.png │ ├── logback-dev.xml │ ├── logback-prd.xml │ ├── mac │ ├── dock_logo.png │ ├── logo.png │ └── mitm-tool.bin │ └── windows │ ├── logo.png │ └── logo_xp.png ├── pom.xml └── runner ├── .gitignore ├── pom.xml └── src └── main ├── deploy └── package │ ├── linux │ └── Proxyee Down.png │ ├── macosx │ └── Proxyee Down.icns │ └── windows │ └── Proxyee Down.ico └── java └── org └── pdown └── gui └── Runner.java /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 5 | ### 问题描述(必要) 6 | ### 版本号(必要) 7 | ### 操作系统(必要) 8 | ### 相关截图 9 | ### 相关日志 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | target/ 4 | log/ 5 | !.mvn/wrapper/maven-wrapper.jar 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | 15 | ### IntelliJ IDEA ### 16 | .idea 17 | *.iws 18 | *.iml 19 | *.ipr -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 续作出炉!!! 2 | 3 | 新项目使用`golang`+`flutter`开发,支持所有平台的下载器,地址:https://github.com/GopeedLab/gopeed 4 | 5 | ## 暂停维护此项目 6 | 首先感谢大家支持和反馈才使得proxyee-down能一直迭代到现在的版本,但由于本人精力有限,宣布暂时停止此项目的维护,并且关闭**issue**模块。 7 | 8 | 其次因为`JAVA`不太适合做客户端开发,打包后体积太大且内存占用太高,本人计划在空余时间用`GO`来重写一遍,目标是打造一个`体积小`、`跨平台`、`内存低`、`可扩展`、`免费`的下载器。 9 | 10 | ![](https://i.imgur.com/dUvNgmd.jpg) 11 | 12 | # [Proxyee Down](https://pdown.org) 13 | [![Author](https://img.shields.io/badge/author-monkeyWie-red.svg?style=flat-square)](https://github.com/monkeyWie) 14 | [![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) 15 | [![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) 16 | [![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) 17 | [![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) 18 | 19 | > Proxyee Down 是一款开源的免费 HTTP 高速下载器,底层使用`netty`开发,支持自定义 HTTP 请求下载且支持扩展功能,可以通过安装扩展实现特殊的下载需求。 20 | 21 | ## 使用教程 22 | 23 | [点击查看教程](https://github.com/proxyee-down-org/proxyee-down/wiki/%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B) 24 | 25 | ## 交流群 26 | 27 | 1 群**11352304**、2 群**20236964**、3 群**20233754**、4 群**737991056** 28 | 29 | ## 开发 30 | 31 | 本项目后端主要使用 `java` + `spring` + `boot` + `netty`,前端使用 `vue.js` + `iview` 32 | 33 | ### 环境 34 | ![](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) 35 | 36 | oracle jdk 1.8+或 openjfx(openjdk默认不包含javafx包) 37 | 38 | ### 编译 39 | 40 | ``` 41 | git clone https://github.com/proxyee-down-org/proxyee-down.git 42 | cd proxyee-down/front 43 | #build html 44 | npm install 45 | npm run build 46 | cd ../main 47 | mvn clean package -Pprd 48 | ``` 49 | 50 | ### 运行 51 | ``` 52 | java -jar proxyee-down-main.jar 53 | ``` 54 | -------------------------------------------------------------------------------- /front/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | extends: ['plugin:vue/essential', 'eslint:recommended'], 7 | rules: { 8 | 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off', 9 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', 10 | 'no-alert': process.env.NODE_ENV === 'production' ? 'error' : 'off', 11 | //强制使用单引号 12 | quotes: ['error', 'single'], 13 | //强制不使用分号结尾 14 | semi: ['error', 'never'] 15 | }, 16 | parserOptions: { 17 | parser: 'babel-eslint' 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /front/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | 17 | *.suo 18 | *.ntvs* 19 | *.njsproj4 20 | *.sln 21 | *.sw* 22 | 23 | package-lock.json -------------------------------------------------------------------------------- /front/.postcssrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {} 4 | } 5 | } -------------------------------------------------------------------------------- /front/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "eslintIntegration": true, 3 | "singleQuote": true, 4 | "semi": false, 5 | "printWidth": 120 6 | } 7 | -------------------------------------------------------------------------------- /front/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "vetur.format.defaultFormatter.html": "js-beautify-html", 3 | "vetur.format.defaultFormatterOptions": { 4 | "js-beautify-html": { 5 | "wrap_attributes": "force" 6 | } 7 | }, 8 | "files.associations": { 9 | "*.vue": "vue" 10 | }, 11 | "eslint.validate": [ 12 | "javascript", 13 | "javascriptreact", 14 | { 15 | "language": "vue", 16 | "autoFix": true 17 | } 18 | ], 19 | "eslint.autoFixOnSave": true 20 | } 21 | -------------------------------------------------------------------------------- /front/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['@vue/app'], 3 | plugins: ['jsx-v-model'] 4 | } 5 | -------------------------------------------------------------------------------- /front/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "proxyee-down", 3 | "version": "3.0.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "npm run serve", 7 | "serve": "vue-cli-service serve", 8 | "build": "vue-cli-service build", 9 | "lint": "vue-cli-service lint" 10 | }, 11 | "dependencies": { 12 | "axios": "^0.18.0", 13 | "iview": "^2.14.3", 14 | "numeral": "^2.0.6", 15 | "reconnecting-websocket": "^4.1.0", 16 | "vue": "^2.5.17", 17 | "vue-i18n": "^8.0.0", 18 | "vue-router": "^3.0.1", 19 | "vuex": "^3.0.1" 20 | }, 21 | "devDependencies": { 22 | "@vue/cli-plugin-babel": "^3.0.1", 23 | "@vue/cli-plugin-eslint": "^3.0.1", 24 | "@vue/cli-service": "^3.0.1", 25 | "@vue/eslint-config-prettier": "^3.0.1", 26 | "babel-plugin-jsx-v-model": "^2.0.3", 27 | "iview-loader": "^1.2.1", 28 | "less": "^3.8.1", 29 | "less-loader": "^4.1.0", 30 | "lint-staged": "^6.0.0", 31 | "vue-template-compiler": "^2.5.17" 32 | }, 33 | "browserslist": [ 34 | "> 1%", 35 | "last 2 versions", 36 | "not ie <= 8" 37 | ], 38 | "gitHooks": { 39 | "pre-commit": "lint-staged" 40 | }, 41 | "lint-staged": { 42 | "*.js": [ 43 | "vue-cli-service lint", 44 | "git add" 45 | ], 46 | "*.vue": [ 47 | "vue-cli-service lint", 48 | "git add" 49 | ] 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /front/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/proxyee-down-org/proxyee-down/a8f1709f1603ef0c92e4818261842cb4eb804f79/front/public/favicon.ico -------------------------------------------------------------------------------- /front/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Proxyee Down 10 | 15 | 16 | 17 | 18 | 21 |
22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /front/public/pay/alipay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/proxyee-down-org/proxyee-down/a8f1709f1603ef0c92e4818261842cb4eb804f79/front/public/pay/alipay.png -------------------------------------------------------------------------------- /front/public/pay/weipay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/proxyee-down-org/proxyee-down/a8f1709f1603ef0c92e4818261842cb4eb804f79/front/public/pay/weipay.png -------------------------------------------------------------------------------- /front/public/team_header/Black-Hole.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/proxyee-down-org/proxyee-down/a8f1709f1603ef0c92e4818261842cb4eb804f79/front/public/team_header/Black-Hole.png -------------------------------------------------------------------------------- /front/public/team_header/NISAL.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/proxyee-down-org/proxyee-down/a8f1709f1603ef0c92e4818261842cb4eb804f79/front/public/team_header/NISAL.png -------------------------------------------------------------------------------- /front/public/team_header/monkeyWie.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/proxyee-down-org/proxyee-down/a8f1709f1603ef0c92e4818261842cb4eb804f79/front/public/team_header/monkeyWie.png -------------------------------------------------------------------------------- /front/src/App.vue: -------------------------------------------------------------------------------- 1 | 46 | 47 | 98 | 99 | 113 | 114 | -------------------------------------------------------------------------------- /front/src/common/http.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import axios from 'axios' 3 | 4 | export default { 5 | build() { 6 | const client = axios.create() 7 | client.interceptors.request.use( 8 | config => { 9 | Vue.prototype.$Spin.show() 10 | return config 11 | }, 12 | error => Promise.reject(error) 13 | ) 14 | client.interceptors.response.use( 15 | response => { 16 | Vue.prototype.$Spin.hide() 17 | return response 18 | }, 19 | error => { 20 | Vue.prototype.$Spin.hide() 21 | return Promise.reject(error) 22 | } 23 | ) 24 | return client 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /front/src/common/native.js: -------------------------------------------------------------------------------- 1 | import http from './http' 2 | import axios from 'axios' 3 | 4 | const client = http.build() 5 | const clientNoSpin = axios.create() 6 | 7 | /** 8 | * 弹出原生文件选择框 9 | */ 10 | export const showFileChooser = () => { 11 | return new Promise((resolve, reject) => { 12 | client 13 | .get('/native/fileChooser') 14 | .then(response => resolve(response.data)) 15 | .catch(error => reject(error)) 16 | }) 17 | } 18 | 19 | /** 20 | * 弹出原生文件夹选择框 21 | */ 22 | export const showDirChooser = () => { 23 | return new Promise((resolve, reject) => { 24 | client 25 | .get('/native/dirChooser') 26 | .then(response => resolve(response.data)) 27 | .catch(error => reject(error)) 28 | }) 29 | } 30 | 31 | /** 32 | * 取应用初始化配置信息 33 | */ 34 | export const getInitConfig = () => { 35 | return new Promise((resolve, reject) => { 36 | client 37 | .get('/native/getInitConfig') 38 | .then(response => resolve(response.data)) 39 | .catch(error => reject(error)) 40 | }) 41 | } 42 | 43 | /** 44 | * 弹出系统资源管理器并选中指定文件 45 | * @param {string} path 文件路径 46 | */ 47 | export const showFile = path => { 48 | return new Promise((resolve, reject) => { 49 | client 50 | .post('/native/showFile', { path: path }) 51 | .then(response => resolve(response.data)) 52 | .catch(error => reject(error)) 53 | }) 54 | } 55 | 56 | /** 57 | * 检查证书是否安装 58 | */ 59 | export const checkCert = () => { 60 | return new Promise((resolve, reject) => { 61 | clientNoSpin 62 | .get('/native/checkCert') 63 | .then(response => resolve(response.data.status)) 64 | .catch(error => reject(error)) 65 | }) 66 | } 67 | 68 | /** 69 | * 安装证书 70 | */ 71 | export const installCert = () => { 72 | return new Promise((resolve, reject) => { 73 | client 74 | .get('/native/installCert') 75 | .then(response => resolve(response.data.status)) 76 | .catch(error => reject(error)) 77 | }) 78 | } 79 | 80 | /** 81 | * 取设置的代理模式 82 | */ 83 | export const getProxyMode = () => { 84 | return new Promise((resolve, reject) => { 85 | clientNoSpin 86 | .get('/native/getProxyMode') 87 | .then(response => resolve(response.data.mode)) 88 | .catch(error => reject(error)) 89 | }) 90 | } 91 | 92 | /** 93 | * 修改代理模式 94 | * @param {number} mode 0.不接管系统代理 1.接管系统代理 95 | */ 96 | export const changeProxyMode = mode => { 97 | return new Promise((resolve, reject) => { 98 | client 99 | .post('/native/changeProxyMode', { mode: mode }) 100 | .then(response => resolve(response.data)) 101 | .catch(error => reject(error)) 102 | }) 103 | } 104 | 105 | /** 106 | * 取本地已安装的扩展列表 107 | */ 108 | export const getExtensions = () => { 109 | return new Promise((resolve, reject) => { 110 | clientNoSpin 111 | .get('/native/getExtensions') 112 | .then(response => resolve(response.data)) 113 | .catch(error => reject(error)) 114 | }) 115 | } 116 | 117 | /** 118 | * 安装指定扩展 119 | * @param {object} data 扩展相关信息 120 | */ 121 | export const installExtension = data => { 122 | return new Promise((resolve, reject) => { 123 | clientNoSpin 124 | .post('/native/installExtension', data) 125 | .then(response => resolve(response.data)) 126 | .catch(error => reject(error)) 127 | }) 128 | } 129 | 130 | /** 131 | * 更新指定扩展 132 | * @param {object} data 扩展相关信息 133 | */ 134 | export const updateExtension = data => { 135 | return new Promise((resolve, reject) => { 136 | clientNoSpin 137 | .post('/native/updateExtension', data) 138 | .then(response => resolve(response.data)) 139 | .catch(error => reject(error)) 140 | }) 141 | } 142 | 143 | /** 144 | * 安装本地扩展 145 | * @param {string} path 扩展所在目录 146 | */ 147 | export const installLocalExtension = path => { 148 | return new Promise((resolve, reject) => { 149 | clientNoSpin 150 | .post('/native/installLocalExtension', { path: path }) 151 | .then(response => resolve(response.data.data)) 152 | .catch(error => reject(error)) 153 | }) 154 | } 155 | 156 | /** 157 | * 卸载扩展 158 | * @param {string} path 扩展所在目录 159 | * @param {boolean} isLocal 是否本地加载的扩展 160 | */ 161 | export const uninstallExtension = (path, local) => { 162 | return new Promise((resolve, reject) => { 163 | client 164 | .post('/native/uninstallExtension', { path, local }) 165 | .then(response => resolve(response.data)) 166 | .catch(error => reject(error)) 167 | }) 168 | } 169 | 170 | /** 171 | * 启用或禁用扩展 172 | * @param {object} data 173 | */ 174 | export const toggleExtension = data => { 175 | return new Promise((resolve, reject) => { 176 | client 177 | .post('/native/toggleExtension', data) 178 | .then(response => resolve(response.data)) 179 | .catch(error => reject(error)) 180 | }) 181 | } 182 | 183 | /** 184 | * 保存指定扩展的设置 185 | * @param {String} path 扩展路径 186 | * @param {object} setting 扩展设置信息 187 | */ 188 | export const updateExtensionSetting = (path, setting) => { 189 | return new Promise((resolve, reject) => { 190 | client 191 | .post('/native/updateExtensionSetting', { path, setting }) 192 | .then(response => resolve(response.data)) 193 | .catch(error => reject(error)) 194 | }) 195 | } 196 | 197 | /** 198 | * 打开浏览器并访问指定url 199 | * @param {object} data 200 | */ 201 | export const openUrl = url => { 202 | if (window.navigator.userAgent.indexOf('JavaFX') !== -1) { 203 | clientNoSpin.post('/native/openUrl', { url: encodeURIComponent(url) }) 204 | } else { 205 | window.open(url) 206 | } 207 | } 208 | 209 | /** 210 | * 在解析任务时触发 211 | * @param {object} request 212 | */ 213 | export const onResolve = request => { 214 | return new Promise((resolve, reject) => { 215 | clientNoSpin 216 | .post('/native/onResolve', request) 217 | .then(response => resolve(response.data)) 218 | .catch(error => reject(error)) 219 | }) 220 | } 221 | 222 | /** 223 | * 更新软件 224 | * @param {string} path 更新包下载地址 225 | */ 226 | export const doUpdate = path => { 227 | return new Promise((resolve, reject) => { 228 | clientNoSpin 229 | .post('/native/doUpdate', { path: path }) 230 | .then(response => resolve(response.data)) 231 | .catch(error => reject(error)) 232 | }) 233 | } 234 | 235 | /** 236 | * 更新软件进度获取 237 | */ 238 | export const getUpdateProgress = () => { 239 | return new Promise((resolve, reject) => { 240 | clientNoSpin 241 | .get('/native/getUpdateProgress') 242 | .then(response => resolve(response.data)) 243 | .catch(error => reject(error)) 244 | }) 245 | } 246 | 247 | /** 248 | * 重启软件 249 | */ 250 | export const doRestart = () => { 251 | return new Promise((resolve, reject) => { 252 | client 253 | .get('/native/doRestart') 254 | .then(response => resolve(response.data)) 255 | .catch(error => reject(error)) 256 | }) 257 | } 258 | 259 | /** 260 | * 取软件设置信息 261 | */ 262 | export const getConfig = () => { 263 | return new Promise((resolve, reject) => { 264 | clientNoSpin 265 | .get('/native/getConfig') 266 | .then(response => resolve(response.data)) 267 | .catch(error => reject(error)) 268 | }) 269 | } 270 | 271 | /** 272 | * 保存软件设置 273 | * @param {object} config 274 | */ 275 | export const setConfig = config => { 276 | return new Promise((resolve, reject) => { 277 | clientNoSpin 278 | .put('/native/setConfig', config) 279 | .then(response => resolve(response)) 280 | .catch(error => reject(error)) 281 | }) 282 | } 283 | 284 | /** 285 | * 复制数据到系统剪贴板 286 | * @param {object} data 287 | */ 288 | export const copy = data => { 289 | return new Promise((resolve, reject) => { 290 | clientNoSpin 291 | .put('/native/copy', data) 292 | .then(response => resolve(response)) 293 | .catch(error => reject(error)) 294 | }) 295 | } 296 | -------------------------------------------------------------------------------- /front/src/components/ExtensionSetting.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 41 | 42 | -------------------------------------------------------------------------------- /front/src/components/FileChoose/index.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 53 | 54 | 67 | -------------------------------------------------------------------------------- /front/src/components/Table/index.vue: -------------------------------------------------------------------------------- 1 | 109 | 110 | 202 | 203 | 298 | 299 | 308 | 309 | -------------------------------------------------------------------------------- /front/src/components/Task/Create.vue: -------------------------------------------------------------------------------- 1 | 67 | 68 | -------------------------------------------------------------------------------- /front/src/components/Task/Resolve.vue: -------------------------------------------------------------------------------- 1 | 63 | 64 | 157 | 158 | 178 | -------------------------------------------------------------------------------- /front/src/i18n/en-US.js: -------------------------------------------------------------------------------- 1 | export default { 2 | nav: { 3 | tasks: 'Tasks', 4 | extension: 'Extensions', 5 | setting: 'Settings', 6 | about: 'About', 7 | support: 'Support Us' 8 | }, 9 | tip: { 10 | tip: 'Hint', 11 | ok: 'OK', 12 | cancel: 'Cancel', 13 | notNull: 'Cannot be empty', 14 | fmtErr: 'Incorrect format', 15 | choose: 'Choose', 16 | save: 'Save', 17 | refresh: 'Refresh', 18 | copySucc: 'Copied successfully', 19 | copyFail: 'Copy failed', 20 | saveSucc: 'Save successfully', 21 | saveFail: 'Save failed' 22 | }, 23 | tasks: { 24 | createTask: 'New Task', 25 | continueDownloading: 'Resume', 26 | pauseDownloads: 'Pause', 27 | deleteTask: 'Delete Task', 28 | deleteTaskTip: 'Delete both task and file', 29 | revealInFolder: 'Reveal in download folder', 30 | method: 'Method', 31 | url: 'URL', 32 | fileName: 'Name', 33 | fileSize: 'Size', 34 | connections: 'Connections', 35 | filePath: 'Path', 36 | status: 'Status', 37 | operate: 'Actions', 38 | downloadAddress: 'Address', 39 | wait: 'Waiting', 40 | unknowLeft: 'Unknown', 41 | downloadSpeed: 'Speed', 42 | createTime: 'Created', 43 | taskProgress: 'Progress', 44 | statusPause: 'Pause', 45 | statusFail: 'Failed', 46 | statusDone: 'Done', 47 | option: 'Options', 48 | head: 'Header', 49 | body: 'Body', 50 | detail: 'Details', 51 | checkSameTask: 'The same task already exists. Refresh the task?', 52 | sameTaskList: 'Task List', 53 | sameTaskPlaceholder: 'Please select the task to refresh', 54 | running: 'Downloading', 55 | waiting: 'Waiting', 56 | done: 'Done' 57 | }, 58 | extension: { 59 | conditions: 'Notes', 60 | conditionsContent: 61 | '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.', 62 | install: 'Install', 63 | globalProxy: 'Global Proxy', 64 | proxyTip: 'View instructions', 65 | copyPac: 'Copy PAC URL', 66 | title: 'Title', 67 | description: 'Description', 68 | currVersion: 'Current Version', 69 | newVersion: 'Latest Version', 70 | installStatus: 'Status', 71 | installStatusTrue: 'ON', 72 | installStatusFalse: 'OFF', 73 | action: 'Actions', 74 | actionUpdate: 'Update', 75 | actionInstall: 'Install', 76 | uninstall: 'Uninstall', 77 | uninstallTip: 'Do you want to uninstall this extension?', 78 | actionDetail: 'Details', 79 | switch: 'ON/OFF', 80 | downloadingTip: 'Downloading...[servers(', 81 | downloadOk: 'Downloaded successfully', 82 | downloadErr: 'Download failed', 83 | downloadErrTip: 'Automatically switch servers', 84 | extCenter: 'Extension center', 85 | installLocalExt: 'Install local extension', 86 | installOk: 'Installed successfully', 87 | installErr: 'Installation failed, please check the manifest.json file', 88 | setting: 'Setting' 89 | }, 90 | setting: { 91 | downSetting: 'Download settings', 92 | path: 'Path', 93 | pathTip: 'Default download path', 94 | connections: 'Connections', 95 | connectionsTip: 'Default download connections', 96 | taskLimit: 'Simultaneous download tasks', 97 | taskSpeedLimit: 'Single task speed limit', 98 | globalSpeedLimit: 'Global speed limit', 99 | speedLimitTip: '0 for unlimited', 100 | appSetting: 'System settings', 101 | language: 'Language', 102 | uiMode: 'UI mode', 103 | uiModeWindows: 'Windows', 104 | uiModeBrowser: 'Browser', 105 | autoOpen: 'Popup at startup', 106 | checkUpdate: 'Check for update', 107 | checkUpdateWeek: 'Every week', 108 | checkUpdateStartup: 'Every startup', 109 | checkUpdateNever: 'Never', 110 | secondProxy: { 111 | secondProxy: 'Second proxy', 112 | tip: 'Configure the second (pre-proxy) proxy server for the downloader', 113 | type: 'Type', 114 | host: 'Host', 115 | port: 'Port', 116 | user: 'Username', 117 | pwd: 'password' 118 | } 119 | }, 120 | about: { 121 | project: { 122 | title: 'Project', 123 | content: 124 | '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.', 125 | githubAddress: 'Project homepage: ', 126 | official: 'Official website: ', 127 | community: 'Official community: ', 128 | tutorial: 'Tutorials: ', 129 | feedback: 'Feedback: ', 130 | currentVersion: 'Current Version: ', 131 | checkUpdate: 'Check update:', 132 | noNewVersion: 'Already the latest version' 133 | }, 134 | team: { 135 | title: 'Team' 136 | } 137 | }, 138 | update: { 139 | checkNew: 'New version available', 140 | version: 'Version', 141 | changeLog: 'Changelog', 142 | update: 'Update', 143 | done: 'Update completed', 144 | restart: 'Restart Proxyee Down?', 145 | error: 'Update failed, please check the network or manually download the update package' 146 | }, 147 | alert: { 148 | refused: 'Program exception: Connection refused', 149 | timeout: 'Program exception: Connection timeout', 150 | error: 'Program error', 151 | notFound: 'Task not found', 152 | '/tasks': { 153 | post: { 154 | 4000: 'Params parse error', 155 | 4001: 'Request is empty', 156 | 4002: 'Request URL is empty', 157 | 4003: 'File save path is empty', 158 | 4004: 'Failed to create folder', 159 | 4005: 'No write permission', 160 | 4006: 'Not enough disk space', 161 | 4007: 'File already exists' 162 | } 163 | } 164 | }, 165 | '/util/resolve': { 166 | put: { 167 | 4000: 'Params parse error', 168 | 4001: 'Request URL is empty', 169 | 4002: 'Response status code exception', 170 | 4003: 'Request timeout' 171 | } 172 | }, 173 | '/config': { 174 | put: { 175 | 4000: 'Params parse error' 176 | } 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /front/src/i18n/zh-CN.js: -------------------------------------------------------------------------------- 1 | export default { 2 | nav: { 3 | tasks: '任务管理', 4 | extension: '扩展管理', 5 | setting: '软件设置', 6 | about: '关于项目', 7 | support: '支持我们' 8 | }, 9 | tip: { 10 | tip: '提示', 11 | ok: '确定', 12 | cancel: '取消', 13 | notNull: '不能为空', 14 | fmtErr: '格式不正确', 15 | choose: '选择', 16 | save: '保存', 17 | refresh: '刷新', 18 | copySucc: '复制成功', 19 | copyFail: '复制失败', 20 | saveSucc: '保存成功', 21 | saveFail: '保存失败' 22 | }, 23 | tasks: { 24 | createTask: '创建任务', 25 | continueDownloading: '继续下载', 26 | pauseDownloads: '暂停下载', 27 | deleteTask: '删除任务', 28 | deleteTaskTip: '是否删除任务和文件?', 29 | revealInFolder: '打开下载目录', 30 | method: '方法', 31 | url: '链接', 32 | fileName: '文件名', 33 | fileSize: '大小', 34 | connections: '连接数', 35 | filePath: '路径', 36 | status: '状态', 37 | operate: '操作', 38 | downloadAddress: '下载地址', 39 | downloadSpeed: '下载速度', 40 | createTime: '开始时间', 41 | taskProgress: '任务进度', 42 | wait: '待下载', 43 | unknowLeft: '未知', 44 | statusPause: '暂停', 45 | statusFail: '失败', 46 | statusDone: '完成', 47 | option: '附加', 48 | head: '请求头', 49 | body: '请求体', 50 | detail: '下载详情', 51 | checkSameTask: '检测到可能相同的下载任务,是否选择任务进行刷新?', 52 | sameTaskList: '任务列表', 53 | sameTaskPlaceholder: '请选择要刷新的任务', 54 | running: '进行中', 55 | waiting: '等待中', 56 | done: '已完成' 57 | }, 58 | extension: { 59 | conditions: '使用须知', 60 | conditionsContent: 61 | '首次使用扩展模块时,必须安装由Proxyee Down随机生成的一个CA证书,点击下面的安装按钮并按系统的引导进行确认安装。(注意:程序会在安装前检测操作系统中是否有安装过证书,当检测到有安装的情况会提示删除对应的旧CA证书)', 62 | install: '安装', 63 | globalProxy: '全局代理', 64 | proxyTip: '点击查看说明', 65 | copyPac: '复制PAC链接', 66 | title: '名称', 67 | description: '描述', 68 | currVersion: '当前版本', 69 | newVersion: '最新版本', 70 | installStatus: '状态', 71 | installStatusTrue: '已安装', 72 | installStatusFalse: '未安装', 73 | action: '操作', 74 | actionUpdate: '更新', 75 | actionInstall: '安装', 76 | uninstall: '卸载', 77 | uninstallTip: '确定卸载此扩展吗?', 78 | actionDetail: '详情', 79 | switch: '开关', 80 | downloadingTip: '下载中...[服务器(', 81 | downloadOk: '下载成功', 82 | downloadErr: '下载失败', 83 | downloadErrTip: '自动切换服务器', 84 | extCenter: '扩展中心', 85 | installLocalExt: '加载本地扩展', 86 | installOk: '加载成功', 87 | installErr: '加载失败,请检查manifest.json文件', 88 | setting: '设置' 89 | }, 90 | setting: { 91 | downSetting: '下载设置', 92 | path: '路径', 93 | pathTip: '默认下载路径', 94 | connections: '连接数', 95 | connectionsTip: '默认连接数', 96 | taskLimit: '同时下载任务数', 97 | taskSpeedLimit: '单任务限速', 98 | globalSpeedLimit: '全局限速', 99 | speedLimitTip: '0为不限速', 100 | appSetting: '系统设置', 101 | language: '语言', 102 | uiMode: 'UI模式', 103 | uiModeWindows: '窗口', 104 | uiModeBrowser: '浏览器', 105 | autoOpen: '启动弹窗', 106 | checkUpdate: '检查更新', 107 | checkUpdateWeek: '每周', 108 | checkUpdateStartup: '每次启动', 109 | checkUpdateNever: '从不', 110 | secondProxy: { 111 | secondProxy: '二级代理', 112 | tip: '配置下载器的二级(前置)代理服务器', 113 | type: '类型', 114 | host: '服务器', 115 | port: '端口', 116 | user: '用户名', 117 | pwd: '密码' 118 | } 119 | }, 120 | about: { 121 | project: { 122 | title: '项目', 123 | content: 'Proxyee Down是一款开源的免费软件,基于本软件的高速下载内核和扩展,可以方便并快速的下载所需资源。', 124 | githubAddress: '项目主页:', 125 | official: '官方网站:', 126 | community: '官方社区:', 127 | tutorial: '使用教程:', 128 | feedback: '问题反馈:', 129 | currentVersion: '当前版本:', 130 | checkUpdate: '检查更新:', 131 | noNewVersion: '已经是最新版本' 132 | }, 133 | team: { 134 | title: '团队' 135 | } 136 | }, 137 | update: { 138 | checkNew: '检测到新版本', 139 | version: '版本号', 140 | changeLog: '更新内容', 141 | update: '更新', 142 | done: '更新完毕', 143 | restart: '是否重新启动?', 144 | error: '更新失败,请检查网络或手动下载更新包' 145 | }, 146 | alert: { 147 | refused: '程序异常,拒绝访问', 148 | timeout: '程序异常,连接超时', 149 | error: '程序出错', 150 | notFound: '任务不存在', 151 | '/tasks': { 152 | post: { 153 | 4000: '参数解析错误', 154 | 4001: '请求对象不能为空', 155 | 4002: '请求地址不能为空', 156 | 4003: '文件保存路径不能为空', 157 | 4004: '创建文件夹失败', 158 | 4005: '无写入权限', 159 | 4006: '磁盘空间不足', 160 | 4007: '文件已存在' 161 | } 162 | }, 163 | '/util/resolve': { 164 | put: { 165 | 4000: '参数解析错误', 166 | 4001: '请求地址不能为空', 167 | 4002: '响应状态码异常', 168 | 4003: '请求超时' 169 | } 170 | }, 171 | '/config': { 172 | put: { 173 | 4000: '参数解析错误' 174 | } 175 | } 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /front/src/i18n/zh-TW.js: -------------------------------------------------------------------------------- 1 | export default { 2 | nav: { 3 | tasks: '任務管理', 4 | extension: '擴充管理', 5 | setting: '軟體設定', 6 | about: '關於專案', 7 | support: '支持我們' 8 | }, 9 | tip: { 10 | tip: '提示', 11 | ok: '確定', 12 | cancel: '取消', 13 | notNull: '不能為空', 14 | fmtErr: '格式不正確', 15 | choose: '選擇', 16 | save: '儲存', 17 | refresh: '刷新', 18 | copySucc: '複製成功', 19 | copyFail: '複製失敗', 20 | saveSucc: '保存成功', 21 | saveFail: '保存失敗' 22 | }, 23 | tasks: { 24 | createTask: '建立任務', 25 | continueDownloading: '繼續下載', 26 | pauseDownloads: '暫停下載', 27 | deleteTask: '刪除任務', 28 | deleteTaskTip: '是否刪除任務和檔案?', 29 | revealInFolder: '打開下載目錄', 30 | method: '方法', 31 | url: '連結', 32 | fileName: '名稱', 33 | fileSize: '大小', 34 | connections: '連線數', 35 | filePath: '路徑', 36 | status: '狀態', 37 | operate: '操作', 38 | downloadAddress: '下載位址', 39 | downloadSpeed: '下載速度', 40 | createTime: '開始時間', 41 | taskProgress: '任務進度', 42 | wait: '待下載', 43 | unknowLeft: '不詳', 44 | statusPause: '暫停', 45 | statusFail: '失敗', 46 | statusDone: '完成', 47 | option: '附加', 48 | head: '要求標頭', 49 | body: '要求主體', 50 | detail: '下載細節', 51 | checkSameTask: '偵測到可能相同的下載任務,是否選擇任務進行更新?', 52 | sameTaskList: '任務清單', 53 | sameTaskPlaceholder: '請選擇要更新的任務', 54 | running: '進行中', 55 | waiting: '等待中', 56 | done: '已完成' 57 | }, 58 | extension: { 59 | conditions: '使用須知', 60 | conditionsContent: 61 | '首次使用擴充模組時,必須安裝由 Proxyee Down 隨機產生的一個 CA 憑證,點選下方的安裝按鈕並依系統的引導進行確認安裝。(注意:程式會在安裝前偵測作業系統中是否有安裝過憑證,當偵測到有安裝的情況會提示刪除對應的舊 CA 憑證)', 62 | install: '安裝', 63 | globalProxy: '全域代理', 64 | proxyTip: '點選檢視說明', 65 | copyPac: '複製 PAC 連結', 66 | title: '名稱', 67 | description: '描述', 68 | currVersion: '目前版本', 69 | newVersion: '最新版本', 70 | installStatus: '狀態', 71 | installStatusTrue: '已安装', 72 | installStatusFalse: '未安装', 73 | action: '操作', 74 | actionUpdate: '更新', 75 | actionInstall: '安裝', 76 | actionDetail: '細節', 77 | uninstall: '卸載', 78 | uninstallTip: '確定卸載此擴展嗎?', 79 | switch: '開關', 80 | downloadingTip: '下載中...[伺服器(', 81 | downloadOk: '下載成功', 82 | downloadErr: '下載失敗', 83 | downloadErrTip: '自動切換伺服器', 84 | extCenter: '擴充中心', 85 | installLocalExt: '加載本地擴充', 86 | installOk: '加載成功', 87 | installErr: '加載失敗,請檢查manifest.json文件', 88 | setting: '預設' 89 | }, 90 | setting: { 91 | downSetting: '下載設定', 92 | path: '路徑', 93 | pathTip: '預設下載路徑', 94 | connections: '連線數', 95 | connectionsTip: '預設連線數', 96 | taskLimit: '同時下載任務數', 97 | taskSpeedLimit: '單任務限速', 98 | globalSpeedLimit: '全域限速', 99 | speedLimitTip: '0為不限速', 100 | appSetting: '系統設定', 101 | language: '語言', 102 | uiMode: 'UI 模式', 103 | uiModeWindows: '視窗', 104 | uiModeBrowser: '瀏覽器', 105 | autoOpen: '啟動彈窗', 106 | checkUpdate: '檢查更新', 107 | checkUpdateWeek: '每週', 108 | checkUpdateStartup: '每次啟動', 109 | checkUpdateNever: '從不', 110 | secondProxy: { 111 | secondProxy: '二級代理', 112 | tip: '配置下載器的二級(前置)代理服務器', 113 | type: '類型', 114 | host: '服務器', 115 | port: '端口', 116 | user: '用戶名', 117 | pwd: '密碼' 118 | } 119 | }, 120 | about: { 121 | project: { 122 | title: '項目', 123 | content: 'Proxyee Down 是一款開源的免費軟體,基於本軟體的高速下載核心和擴充套件,可以方便並快速的下載所需資源。', 124 | githubAddress: '項目首頁:', 125 | official: '官方網站:', 126 | community: '官方社區:', 127 | tutorial: '使用教學:', 128 | feedback: '問題回報:', 129 | currentVersion: '目前版本:', 130 | checkUpdate: '檢查更新:', 131 | noNewVersion: '已經是最新版本' 132 | }, 133 | team: { 134 | title: '團隊' 135 | } 136 | }, 137 | update: { 138 | checkNew: '偵測到新版本', 139 | version: '版本號', 140 | changeLog: '更新內容', 141 | update: '更新', 142 | done: '更新完畢', 143 | restart: '是否重新啟動?', 144 | error: '更新失敗,請檢查網絡或手動下載更新包' 145 | }, 146 | alert: { 147 | refused: '程式異常,拒絕存取', 148 | timeout: '程式異常,連線逾時', 149 | error: '程式出錯', 150 | notFound: '任務不存在', 151 | '/tasks': { 152 | post: { 153 | 4000: '參數解析錯誤', 154 | 4001: '要求對象不能為空', 155 | 4002: '要求位址不能為空', 156 | 4003: '檔案儲存路徑不能為空', 157 | 4004: '建立資料夾失敗', 158 | 4005: '無寫入權限', 159 | 4006: '磁碟空間不足', 160 | 4007: '檔案已存在' 161 | } 162 | }, 163 | '/util/resolve': { 164 | put: { 165 | 4000: '參數解析錯誤', 166 | 4001: '要求位址不能為空', 167 | 4002: '回應狀態碼異常', 168 | 4003: '請求超時' 169 | } 170 | }, 171 | '/config': { 172 | put: { 173 | 4000: '參數解析錯誤' 174 | } 175 | } 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /front/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | import router from './router' 4 | import store from './store' 5 | import iView from 'iview' 6 | import axios from 'axios' 7 | import VueI18n from 'vue-i18n' 8 | import numeral from 'numeral' 9 | 10 | import 'iview/dist/styles/iview.css' 11 | import en_US from 'iview/dist/locale/en-US' 12 | import zh_CN from 'iview/dist/locale/zh-CN' 13 | import zh_TW from 'iview/dist/locale/zh-TW' 14 | import http from './common/http' 15 | import { getInitConfig } from './common/native' 16 | 17 | Vue.use(VueI18n) 18 | Vue.use(iView) 19 | 20 | Vue.config.productionTip = false 21 | 22 | // Setting i18n 23 | const i18n = new VueI18n({ 24 | locale: 'zh-CN', 25 | messages: { 26 | 'en-US': Object.assign(require('./i18n/en-US').default, en_US), 27 | 'zh-CN': Object.assign(require('./i18n/zh-CN').default, zh_CN), 28 | 'zh-TW': Object.assign(require('./i18n/zh-TW').default, zh_TW) 29 | } 30 | }) 31 | 32 | Vue.prototype.$noSpinHttp = axios.create() 33 | Vue.prototype.$http = http.build() 34 | Vue.prototype.$http.interceptors.response.use( 35 | response => { 36 | return response 37 | }, 38 | error => { 39 | if (!error.response) { 40 | Vue.prototype.$Message.error(i18n.t('alert.refused')) 41 | } else if (error.response.status == 400) { 42 | let i18nKey = 43 | 'alert["' + 44 | new URL(error.config.url).pathname + 45 | '"]' + 46 | '.' + 47 | error.config.method + 48 | '.' + 49 | error.response.data.code 50 | Vue.prototype.$Message.error(i18n.t(i18nKey)) 51 | } else if (error.response.status == 404) { 52 | Vue.prototype.$Message.error(i18n.t('alert.notFound')) 53 | } else if (error.response.status == 504) { 54 | Vue.prototype.$Message.error(i18n.t('alert.timeout')) 55 | } else { 56 | Vue.prototype.$Message.error(i18n.t('alert.error')) 57 | } 58 | return Promise.reject(error) 59 | } 60 | ) 61 | 62 | //去除字节大小格式化后的i字符 63 | const format = numeral.prototype.constructor.fn.format 64 | numeral.prototype.constructor.fn.format = function(fmt) { 65 | let result = format.call(this, fmt) 66 | if (/^.*ib$/.test(fmt)) { 67 | result = result.replace('i', '') 68 | } 69 | return result 70 | } 71 | Vue.prototype.$numeral = numeral 72 | 73 | Date.prototype.format = function(fmt) { 74 | var o = { 75 | 'M+': this.getMonth() + 1, // Month 76 | 'd+': this.getDate(), // Day 77 | 'h+': this.getHours(), // Hour 78 | 'm+': this.getMinutes(), // Minute 79 | 's+': this.getSeconds(), // Second 80 | 'q+': Math.floor((this.getMonth() + 3) / 3), // Quarter 81 | S: this.getMilliseconds() // Millisecond 82 | } 83 | if (/(y+)/.test(fmt)) { 84 | fmt = fmt.replace(RegExp.$1, (this.getFullYear() + '').substr(4 - RegExp.$1.length)) 85 | } 86 | for (var k in o) { 87 | if (new RegExp('(' + k + ')').test(fmt)) { 88 | fmt = fmt.replace(RegExp.$1, RegExp.$1.length === 1 ? o[k] : ('00' + o[k]).substr(('' + o[k]).length)) 89 | } 90 | } 91 | return fmt 92 | } 93 | 94 | Promise.prototype.finally = function(callback) { 95 | let P = this.constructor 96 | return this.then( 97 | value => P.resolve(callback()).then(() => value), 98 | reason => 99 | P.resolve(callback()).then(() => { 100 | throw reason 101 | }) 102 | ) 103 | } 104 | 105 | // Change the page according to the routing changes title 106 | router.beforeEach((to, from, next) => { 107 | if (to.meta.title) { 108 | document.title = `Proxyee Down-${to.meta.title}` 109 | } 110 | next() 111 | }) 112 | 113 | // Get client configuration information 114 | getInitConfig() 115 | .then(result => { 116 | Vue.prototype.$config = result 117 | // Set default language 118 | i18n.locale = result.locale 119 | }) 120 | .catch(() => { 121 | Vue.prototype.$config = {} 122 | }) 123 | .finally(() => { 124 | new Vue({ 125 | router, 126 | store, 127 | i18n, 128 | data() { 129 | return { 130 | badges: { tasks: 0, extension: 0, setting: 0, about: 0, support: 0 } 131 | } 132 | }, 133 | render: h => h(App) 134 | }).$mount('#app') 135 | }) 136 | -------------------------------------------------------------------------------- /front/src/router.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | import Tasks from './views/Tasks.vue' 4 | import Extension from './views/Extension.vue' 5 | import Setting from './views/Setting.vue' 6 | import About from './views/About.vue' 7 | import Support from './views/Support.vue' 8 | 9 | Vue.use(Router) 10 | 11 | export default new Router({ 12 | routes: [ 13 | { 14 | path: '/', 15 | redirect: '/tasks' 16 | }, 17 | { 18 | path: '/tasks', 19 | name: 'tasks', 20 | component: Tasks 21 | }, 22 | { 23 | path: '/extension', 24 | name: 'extension', 25 | component: Extension 26 | }, 27 | { 28 | path: '/setting', 29 | name: 'setting', 30 | component: Setting 31 | }, 32 | { 33 | path: '/about', 34 | name: 'About', 35 | component: About 36 | }, 37 | { 38 | path: '/support', 39 | name: 'Support', 40 | component: Support 41 | } 42 | ] 43 | }) 44 | -------------------------------------------------------------------------------- /front/src/store.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | 4 | Vue.use(Vuex) 5 | 6 | export default new Vuex.Store({ 7 | state: {}, 8 | mutations: {}, 9 | actions: {} 10 | }) 11 | -------------------------------------------------------------------------------- /front/src/views/About.vue: -------------------------------------------------------------------------------- 1 | 123 | 219 | 272 | 273 | -------------------------------------------------------------------------------- /front/src/views/Support.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 49 | 50 | 72 | -------------------------------------------------------------------------------- /front/vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | productionSourceMap: true, // Production environment does not generate source-map 3 | css: { 4 | sourceMap: false // CSS does not generate source-map 5 | }, 6 | outputDir: '../main/src/main/resources/http', 7 | devServer: { 8 | proxy: { 9 | '/native': { 10 | target: 'http://127.0.0.1:7478', 11 | changeOrigin: true 12 | }, 13 | '/pac': { 14 | target: 'http://127.0.0.1:7478', 15 | changeOrigin: true 16 | }, 17 | '/ws': { 18 | target: 'http://127.0.0.1:7478', 19 | ws: true, 20 | } 21 | } 22 | }, 23 | chainWebpack: config => { 24 | config.module 25 | .rule('vue') 26 | .test(/\.vue$/) 27 | .use('iview-loader') 28 | .loader('iview-loader') 29 | .options({ 30 | prefix: true 31 | }) 32 | } 33 | } -------------------------------------------------------------------------------- /main/.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | !.mvn/wrapper/maven-wrapper.jar 3 | src/main/resources/http 4 | 5 | ### STS ### 6 | .apt_generated 7 | .classpath 8 | .factorypath 9 | .project 10 | .settings 11 | .springBeans 12 | 13 | ### IntelliJ IDEA ### 14 | .idea 15 | *.iws 16 | *.iml 17 | *.ipr -------------------------------------------------------------------------------- /main/pom.xml: -------------------------------------------------------------------------------- 1 | 4 | 4.0.0 5 | 6 | org.pdown.gui 7 | proxyee-down 8 | 3.0 9 | 10 | 11 | org.pdown.gui 12 | main 13 | jar 14 | 15 | 16 | 17 | snapshots-repo 18 | https://oss.sonatype.org/content/repositories/snapshots 19 | 20 | false 21 | 22 | 23 | true 24 | 25 | 26 | 27 | 28 | 29 | 30 | dev 31 | 32 | dev 33 | 34 | 35 | true 36 | 37 | 38 | 39 | prd 40 | 41 | prd 42 | 43 | 44 | 45 | 46 | 47 | 48 | org.pdown 49 | rest 50 | 1.0.2-SNAPSHOT 51 | 52 | 53 | com.github.monkeywie 54 | proxyee 55 | 1.0.4 56 | 57 | 58 | net.java.dev.jna 59 | jna 60 | 4.5.1 61 | 62 | 63 | org.yaml 64 | snakeyaml 65 | 1.19 66 | 67 | 68 | 69 | 70 | 71 | 72 | true 73 | src/main/resources 74 | 75 | application.yml 76 | application-dev.yml 77 | application-prd.yml 78 | logback-dev.xml 79 | logback-prd.xml 80 | 81 | 82 | 83 | true 84 | src/main/resources 85 | 86 | logback-${environment}.xml 87 | application-${environment}.yml 88 | application.yml 89 | 90 | 91 | 92 | proxyee-down-main 93 | 94 | 95 | org.apache.maven.plugins 96 | maven-resources-plugin 97 | 98 | 99 | bin 100 | css 101 | js 102 | eot 103 | woff 104 | ttf 105 | ico 106 | html 107 | svg 108 | 109 | 110 | 111 | 112 | org.springframework.boot 113 | spring-boot-maven-plugin 114 | 2.0.2.RELEASE 115 | 116 | org.pdown.gui.DownApplication 117 | 118 | 119 | 120 | 121 | repackage 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | -------------------------------------------------------------------------------- /main/src/main/java/org/pdown/gui/com/Browser.java: -------------------------------------------------------------------------------- 1 | package org.pdown.gui.com; 2 | 3 | import javafx.geometry.HPos; 4 | import javafx.geometry.VPos; 5 | import javafx.scene.control.ContextMenu; 6 | import javafx.scene.input.Clipboard; 7 | import javafx.scene.input.ClipboardContent; 8 | import javafx.scene.input.DataFormat; 9 | import javafx.scene.input.MouseButton; 10 | import javafx.scene.layout.Region; 11 | import javafx.scene.web.WebEngine; 12 | import javafx.scene.web.WebView; 13 | import org.pdown.gui.util.I18nUtil; 14 | 15 | public class Browser extends Region { 16 | 17 | final WebView webView = new WebView(); 18 | final WebEngine webEngine = webView.getEngine(); 19 | private javafx.scene.control.MenuItem copy; 20 | private javafx.scene.control.MenuItem paste; 21 | 22 | public Browser() { 23 | getChildren().add(webView); 24 | webView.setContextMenuEnabled(false); 25 | //自定义webview右键菜单 26 | final Clipboard clipboard = Clipboard.getSystemClipboard(); 27 | ContextMenu contextMenu = new ContextMenu(); 28 | copy = new javafx.scene.control.MenuItem(); 29 | copy.setOnAction(e -> { 30 | ClipboardContent content = new ClipboardContent(); 31 | Object selection = webView.getEngine().executeScript("window.getSelection().toString()"); 32 | if (selection != null) { 33 | content.putString(selection.toString()); 34 | clipboard.setContent(content); 35 | } 36 | }); 37 | paste = new javafx.scene.control.MenuItem(); 38 | paste.setOnAction(e -> { 39 | Object content = clipboard.getContent(DataFormat.PLAIN_TEXT); 40 | if (content != null) { 41 | webView.getEngine().executeScript("if(document.activeElement.selectionStart>=0){" 42 | + "var value = document.activeElement.value;" 43 | + "if(!value){" 44 | + " document.activeElement.value='" + content + "';" 45 | + "}else{" 46 | + " document.activeElement.value=value.substring(0,document.activeElement.selectionStart)+'" + content + "'+value.substring(document.activeElement.selectionEnd);" 47 | + "}" 48 | + "var event = document.createEvent('Event');" 49 | + "event.initEvent('input', true, true);" 50 | + "document.activeElement.dispatchEvent(event);" 51 | + "}"); 52 | } 53 | }); 54 | refreshText(); 55 | contextMenu.getItems().addAll(copy, paste); 56 | webView.setOnMousePressed(e -> { 57 | if (e.getButton() == MouseButton.SECONDARY) { 58 | contextMenu.show(webView, e.getScreenX(), e.getScreenY()); 59 | } else { 60 | contextMenu.hide(); 61 | } 62 | }); 63 | } 64 | 65 | @Override 66 | protected void layoutChildren() { 67 | double w = getWidth(); 68 | double h = getHeight(); 69 | layoutInArea(webView, 0, 0, w, h, 0, HPos.CENTER, VPos.CENTER); 70 | } 71 | 72 | public void load(String url) { 73 | webEngine.load(url); 74 | } 75 | 76 | public boolean isLoad() { 77 | return webEngine.getLocation() != null; 78 | } 79 | 80 | public void refreshText() { 81 | copy.setText(I18nUtil.getMessage("gui.menu.copy")); 82 | paste.setText(I18nUtil.getMessage("gui.menu.paste")); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /main/src/main/java/org/pdown/gui/com/CheckboxMenuItemGroup.java: -------------------------------------------------------------------------------- 1 | package org.pdown.gui.com; 2 | 3 | import java.awt.CheckboxMenuItem; 4 | import java.awt.event.ItemEvent; 5 | import java.awt.event.ItemListener; 6 | import java.util.HashSet; 7 | import java.util.Set; 8 | 9 | public class CheckboxMenuItemGroup implements ItemListener { 10 | 11 | private Set items = new HashSet<>(); 12 | private ItemListener itemListener; 13 | 14 | public void add(CheckboxMenuItem cbmi) { 15 | cbmi.addItemListener(this); 16 | cbmi.setState(false); 17 | items.add(cbmi); 18 | } 19 | 20 | public void addActionListener(ItemListener itemListener) { 21 | this.itemListener = itemListener; 22 | } 23 | 24 | @Override 25 | public void itemStateChanged(ItemEvent e) { 26 | CheckboxMenuItem checkedItem = ((CheckboxMenuItem) e.getSource()); 27 | if (e.getStateChange() == ItemEvent.SELECTED) { 28 | String selectedItemName = checkedItem.getName(); 29 | for (CheckboxMenuItem item : items) { 30 | if (!item.getName().equals(selectedItemName)) { 31 | item.setState(false); 32 | } 33 | } 34 | if (itemListener != null) { 35 | itemListener.itemStateChanged(e); 36 | } 37 | } else { 38 | checkedItem.setState(true); 39 | } 40 | } 41 | 42 | public void selectItem(CheckboxMenuItem itemToSelect) { 43 | for (CheckboxMenuItem item : items) { 44 | item.setState(item == itemToSelect); 45 | } 46 | } 47 | 48 | public CheckboxMenuItem getSelectedItem() { 49 | for (CheckboxMenuItem item : items) { 50 | if (item.getState()) { 51 | return item; 52 | } 53 | } 54 | return null; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /main/src/main/java/org/pdown/gui/com/Components.java: -------------------------------------------------------------------------------- 1 | package org.pdown.gui.com; 2 | 3 | import java.io.File; 4 | import javafx.geometry.Insets; 5 | import javafx.scene.Group; 6 | import javafx.scene.Scene; 7 | import javafx.scene.control.Alert; 8 | import javafx.scene.control.Alert.AlertType; 9 | import javafx.scene.control.ButtonBase; 10 | import javafx.scene.control.ButtonType; 11 | import javafx.scene.control.DialogPane; 12 | import javafx.stage.DirectoryChooser; 13 | import javafx.stage.FileChooser; 14 | import javafx.stage.Modality; 15 | import javafx.stage.Stage; 16 | import javafx.stage.StageStyle; 17 | 18 | public class Components { 19 | 20 | /** 21 | * 弹出提示窗,窗口置顶 22 | */ 23 | public static void alert(String msg) { 24 | Alert alert = new Alert(AlertType.INFORMATION); 25 | // alert.setTitle("提示"); 26 | alert.setHeaderText(null); 27 | alert.setContentText(msg); 28 | 29 | DialogPane root = alert.getDialogPane(); 30 | Stage dialogStage = new Stage(); 31 | 32 | for (ButtonType buttonType : root.getButtonTypes()) { 33 | ButtonBase button = (ButtonBase) root.lookupButton(buttonType); 34 | button.setOnAction(evt -> dialogStage.close()); 35 | } 36 | 37 | root.getScene().setRoot(new Group()); 38 | root.setPadding(new Insets(10, 0, 10, 0)); 39 | 40 | Scene scene = new Scene(root); 41 | dialogStage.setScene(scene); 42 | dialogStage.initModality(Modality.APPLICATION_MODAL); 43 | dialogStage.setAlwaysOnTop(true); 44 | dialogStage.setResizable(false); 45 | dialogStage.showAndWait(); 46 | } 47 | 48 | /** 49 | * 弹出文件选择框 50 | */ 51 | public static File fileChooser() { 52 | Stage stage = buildBackgroundTopStage(); 53 | FileChooser chooser = new FileChooser(); 54 | chooser.setTitle("选择文件"); 55 | File file = chooser.showOpenDialog(stage); 56 | stage.close(); 57 | return file; 58 | } 59 | 60 | /** 61 | * 弹出文件夹选择框 62 | */ 63 | public static File dirChooser() { 64 | Stage stage = buildBackgroundTopStage(); 65 | DirectoryChooser chooser = new DirectoryChooser(); 66 | chooser.setTitle("选择文件夹"); 67 | File file = chooser.showDialog(stage); 68 | stage.close(); 69 | return file; 70 | } 71 | 72 | private static Stage buildBackgroundTopStage() { 73 | Stage stage = new Stage(); 74 | stage.setAlwaysOnTop(true); 75 | stage.setWidth(1); 76 | stage.setHeight(1); 77 | stage.initStyle(StageStyle.UNDECORATED); 78 | stage.show(); 79 | return stage; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /main/src/main/java/org/pdown/gui/content/PDownConfigContent.java: -------------------------------------------------------------------------------- 1 | package org.pdown.gui.content; 2 | 3 | import com.fasterxml.jackson.core.type.TypeReference; 4 | import java.io.File; 5 | import java.nio.file.FileSystem; 6 | import java.nio.file.FileSystems; 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | import java.util.Locale; 10 | import org.pdown.gui.entity.PDownConfigInfo; 11 | import org.pdown.rest.base.content.PersistenceContent; 12 | import org.pdown.rest.util.PathUtil; 13 | 14 | public class PDownConfigContent extends PersistenceContent { 15 | 16 | private static final PDownConfigContent INSTANCE = new PDownConfigContent(); 17 | 18 | public static PDownConfigContent getInstance() { 19 | return INSTANCE; 20 | } 21 | 22 | @Override 23 | protected TypeReference type() { 24 | return new TypeReference() { 25 | }; 26 | } 27 | 28 | @Override 29 | protected String savePath() { 30 | return PathUtil.ROOT_PATH + File.separator + "pdown.cfg"; 31 | } 32 | 33 | @Override 34 | protected PDownConfigInfo defaultValue() { 35 | PDownConfigInfo pDownConfigInfo = new PDownConfigInfo(); 36 | //取系统默认语言 37 | Locale defaultLocale = Locale.getDefault(); 38 | pDownConfigInfo.setLocale(defaultLocale.getLanguage() + "-" + defaultLocale.getCountry()); 39 | //插件文件服务器 40 | List extFileServers = new ArrayList<>(); 41 | extFileServers.add("https://github.com/proxyee-down-org/proxyee-down-extension/raw/master"); 42 | extFileServers.add("http://static.pdown.org/extensions"); 43 | pDownConfigInfo.setExtFileServers(extFileServers); 44 | return pDownConfigInfo; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /main/src/main/java/org/pdown/gui/entity/PDownConfigInfo.java: -------------------------------------------------------------------------------- 1 | package org.pdown.gui.entity; 2 | 3 | import java.io.Serializable; 4 | import java.util.List; 5 | import org.pdown.core.proxy.ProxyConfig; 6 | 7 | public class PDownConfigInfo implements Serializable { 8 | 9 | private static final long serialVersionUID = 250452934883002540L; 10 | //客户端语言 11 | private String locale; 12 | //UI模式 0.浏览器模式 1.GUI模式 13 | private int uiMode = 1; 14 | //代理模式 0.不接管系统代理 1.由pdown接管系统代理 15 | private int proxyMode; 16 | //插件文件服务器(用于下载插件相关文件) 17 | private List extFileServers; 18 | //检测更新频率 0.从不 1.一周检查一次 2.每次打开检查 19 | private int updateCheckRate = 2; 20 | //启动时是否自动打开窗口 21 | private boolean autoOpen = true; 22 | //最后一次检查更新时间 23 | private long lastUpdateCheck; 24 | //前置代理 25 | private ProxyConfig proxyConfig; 26 | 27 | public String getLocale() { 28 | return locale; 29 | } 30 | 31 | public PDownConfigInfo setLocale(String locale) { 32 | this.locale = locale; 33 | return this; 34 | } 35 | 36 | public int getUiMode() { 37 | return uiMode; 38 | } 39 | 40 | public PDownConfigInfo setUiMode(int uiMode) { 41 | this.uiMode = uiMode; 42 | return this; 43 | } 44 | 45 | public int getProxyMode() { 46 | return proxyMode; 47 | } 48 | 49 | public PDownConfigInfo setProxyMode(int proxyMode) { 50 | this.proxyMode = proxyMode; 51 | return this; 52 | } 53 | 54 | public List getExtFileServers() { 55 | return extFileServers; 56 | } 57 | 58 | public PDownConfigInfo setExtFileServers(List extFileServers) { 59 | this.extFileServers = extFileServers; 60 | return this; 61 | } 62 | 63 | public int getUpdateCheckRate() { 64 | return updateCheckRate; 65 | } 66 | 67 | public PDownConfigInfo setUpdateCheckRate(int updateCheckRate) { 68 | this.updateCheckRate = updateCheckRate; 69 | return this; 70 | } 71 | 72 | public long getLastUpdateCheck() { 73 | return lastUpdateCheck; 74 | } 75 | 76 | public PDownConfigInfo setLastUpdateCheck(long lastUpdateCheck) { 77 | this.lastUpdateCheck = lastUpdateCheck; 78 | return this; 79 | } 80 | 81 | public ProxyConfig getProxyConfig() { 82 | return proxyConfig; 83 | } 84 | 85 | public PDownConfigInfo setProxyConfig(ProxyConfig proxyConfig) { 86 | this.proxyConfig = proxyConfig; 87 | return this; 88 | } 89 | 90 | public boolean isAutoOpen() { 91 | return autoOpen; 92 | } 93 | 94 | public PDownConfigInfo setAutoOpen(boolean autoOpen) { 95 | this.autoOpen = autoOpen; 96 | return this; 97 | } 98 | 99 | public static com.github.monkeywie.proxyee.proxy.ProxyConfig convert(ProxyConfig proxyConfig) { 100 | if (proxyConfig == null) { 101 | return null; 102 | } 103 | return new com.github.monkeywie.proxyee.proxy.ProxyConfig( 104 | com.github.monkeywie.proxyee.proxy.ProxyType.valueOf(proxyConfig.getProxyType().name()), 105 | proxyConfig.getHost(), 106 | proxyConfig.getPort(), 107 | proxyConfig.getUser(), 108 | proxyConfig.getPwd()); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /main/src/main/java/org/pdown/gui/extension/ContentScript.java: -------------------------------------------------------------------------------- 1 | package org.pdown.gui.extension; 2 | 3 | import java.util.Arrays; 4 | 5 | public class ContentScript { 6 | 7 | private String[] matches; 8 | private String[] scripts; 9 | 10 | public String[] getMatches() { 11 | return matches; 12 | } 13 | 14 | public ContentScript setMatches(String[] matches) { 15 | this.matches = matches; 16 | return this; 17 | } 18 | 19 | public String[] getScripts() { 20 | return scripts; 21 | } 22 | 23 | public ContentScript setScripts(String[] scripts) { 24 | this.scripts = scripts; 25 | return this; 26 | } 27 | 28 | public boolean isMatch(String url) { 29 | if (matches != null 30 | && Arrays.stream(matches).anyMatch(m -> url.matches(m))) { 31 | return true; 32 | } 33 | return false; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /main/src/main/java/org/pdown/gui/extension/ExtensionConfig.java: -------------------------------------------------------------------------------- 1 | package org.pdown.gui.extension; 2 | 3 | import java.util.List; 4 | 5 | public class ExtensionConfig { 6 | 7 | //本地加载的扩展 8 | private List localExtensions; 9 | 10 | public List getLocalExtensions() { 11 | return localExtensions; 12 | } 13 | 14 | public void setLocalExtensions(List localExtensions) { 15 | this.localExtensions = localExtensions; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /main/src/main/java/org/pdown/gui/extension/ExtensionContent.java: -------------------------------------------------------------------------------- 1 | package org.pdown.gui.extension; 2 | 3 | import com.fasterxml.jackson.databind.DeserializationFeature; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import java.io.File; 6 | import java.io.FileInputStream; 7 | import java.io.IOException; 8 | import java.util.ArrayList; 9 | import java.util.HashMap; 10 | import java.util.HashSet; 11 | import java.util.List; 12 | import java.util.Set; 13 | import org.pdown.core.util.FileUtil; 14 | import org.pdown.rest.util.ContentUtil; 15 | import org.pdown.rest.util.PathUtil; 16 | 17 | public class ExtensionContent { 18 | 19 | public static final String EXT_DIR = PathUtil.ROOT_PATH + File.separator + "extensions"; 20 | public static final String EXT_DIR_CONFIG = EXT_DIR + File.separator + "ext.cfg"; 21 | private static final String EXT_MANIFEST = "manifest.json"; 22 | 23 | private static List EXTENSION_INFO_LIST; 24 | //代理服务器域名通配符列表 25 | private static Set PROXY_WILDCARDS; 26 | //需要嗅探下载的url正则表达式列表 27 | private static Set SNIFF_REGEXS; 28 | //配置 29 | private static ExtensionConfig CONFIG; 30 | 31 | public static void load() throws IOException { 32 | File file = new File(EXT_DIR); 33 | if (EXTENSION_INFO_LIST == null) { 34 | EXTENSION_INFO_LIST = new ArrayList<>(); 35 | } else { 36 | EXTENSION_INFO_LIST.clear(); 37 | } 38 | if (file.exists() && file.isDirectory()) { 39 | //加载所有已安装的扩展 40 | for (File extendDir : file.listFiles()) { 41 | if (extendDir.isDirectory()) { 42 | //读取manifest.json 43 | ExtensionInfo extensionInfo = parseExtensionDir(extendDir); 44 | if (extensionInfo != null) { 45 | EXTENSION_INFO_LIST.add(extensionInfo); 46 | } 47 | } 48 | } 49 | } 50 | //加载本地安装的扩展 51 | try { 52 | CONFIG = ContentUtil.get(EXT_DIR_CONFIG, ExtensionConfig.class); 53 | } catch (Exception e) { 54 | } 55 | if (CONFIG == null) { 56 | CONFIG = new ExtensionConfig(); 57 | } else if (CONFIG.getLocalExtensions() != null) { 58 | for (String localExtendDir : CONFIG.getLocalExtensions()) { 59 | File extendDir = new File(localExtendDir); 60 | if (extendDir.isDirectory()) { 61 | //读取manifest.json 62 | ExtensionInfo extensionInfo = parseExtensionDir(extendDir, true); 63 | if (extensionInfo != null) { 64 | EXTENSION_INFO_LIST.add(extensionInfo); 65 | } 66 | } 67 | } 68 | } 69 | refresh(); 70 | } 71 | 72 | public static ExtensionConfig getConfig() { 73 | return CONFIG; 74 | } 75 | 76 | public synchronized static void saveConfig() throws IOException { 77 | ContentUtil.save(CONFIG, EXT_DIR_CONFIG); 78 | } 79 | 80 | public synchronized static ExtensionInfo refresh(String path, boolean isLocal) throws IOException { 81 | ExtensionInfo loadExt = parseExtensionDir(new File((isLocal ? "" : EXT_DIR) + path), isLocal); 82 | if (loadExt != null && EXTENSION_INFO_LIST != null && path != null) { 83 | boolean match = false; 84 | for (int i = 0; i < EXTENSION_INFO_LIST.size(); i++) { 85 | ExtensionInfo extensionInfo = EXTENSION_INFO_LIST.get(i); 86 | if (loadExt.getMeta().getPath().equals(extensionInfo.getMeta().getPath()) 87 | && loadExt.getMeta().isLocal() == extensionInfo.getMeta().isLocal()) { 88 | match = true; 89 | EXTENSION_INFO_LIST.set(i, loadExt); 90 | break; 91 | } 92 | } 93 | if (!match) { 94 | EXTENSION_INFO_LIST.add(loadExt); 95 | if (isLocal) { 96 | //保存文件 97 | if (CONFIG.getLocalExtensions() == null) { 98 | CONFIG.setLocalExtensions(new ArrayList<>()); 99 | } 100 | CONFIG.getLocalExtensions().add(path); 101 | ExtensionContent.saveConfig(); 102 | } 103 | } 104 | refresh(); 105 | } 106 | return loadExt; 107 | } 108 | 109 | public synchronized static ExtensionInfo refresh(String path) throws IOException { 110 | return refresh(path, false); 111 | } 112 | 113 | public synchronized static void remove(String path, boolean isLocal) throws IOException { 114 | if (EXTENSION_INFO_LIST != null && path != null) { 115 | for (int i = 0; i < EXTENSION_INFO_LIST.size(); i++) { 116 | ExtensionInfo extensionInfo = EXTENSION_INFO_LIST.get(i); 117 | if (path.equals(extensionInfo.getMeta().getPath()) 118 | && extensionInfo.getMeta().isLocal() == isLocal) { 119 | EXTENSION_INFO_LIST.remove(i); 120 | if (!extensionInfo.getMeta().isLocal()) { 121 | FileUtil.deleteIfExists(extensionInfo.getMeta().getFullPath()); 122 | } else { 123 | //保存文件 124 | if (CONFIG.getLocalExtensions() != null) { 125 | CONFIG.getLocalExtensions().remove(extensionInfo.getMeta().getFullPath()); 126 | ExtensionContent.saveConfig(); 127 | } 128 | } 129 | break; 130 | } 131 | } 132 | refresh(); 133 | } 134 | } 135 | 136 | public synchronized static void refresh() { 137 | if (PROXY_WILDCARDS == null) { 138 | PROXY_WILDCARDS = new HashSet<>(); 139 | } else { 140 | PROXY_WILDCARDS.clear(); 141 | } 142 | if (SNIFF_REGEXS == null) { 143 | SNIFF_REGEXS = new HashSet<>(); 144 | } else { 145 | SNIFF_REGEXS.clear(); 146 | } 147 | if (EXTENSION_INFO_LIST != null) { 148 | for (ExtensionInfo extensionInfo : EXTENSION_INFO_LIST) { 149 | if (extensionInfo.getMeta().isEnabled()) { 150 | //读取需要代理的域名匹配符 151 | if (extensionInfo.getProxyWildcards() != null) { 152 | for (String wildcard : extensionInfo.getProxyWildcards()) { 153 | PROXY_WILDCARDS.add(wildcard.trim()); 154 | } 155 | } 156 | //读取需要嗅探下载的url正则表达式 157 | if (extensionInfo.getSniffRegexs() != null) { 158 | for (String regex : extensionInfo.getSniffRegexs()) { 159 | SNIFF_REGEXS.add(regex.trim()); 160 | } 161 | } 162 | } 163 | } 164 | } 165 | } 166 | 167 | private static ExtensionInfo parseExtensionDir(File extendDir, boolean isLocal) { 168 | ExtensionInfo extensionInfo = null; 169 | ObjectMapper objectMapper = new ObjectMapper(); 170 | objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); 171 | try { 172 | extensionInfo = objectMapper.readValue(new FileInputStream(extendDir + File.separator + EXT_MANIFEST), ExtensionInfo.class); 173 | } catch (IOException e) { 174 | } 175 | if (extensionInfo != null) { 176 | Meta meta = Meta.load(extendDir.getPath()); 177 | meta.setLocal(isLocal); 178 | //如果没有设置则生成默认设置信息 179 | if (extensionInfo.getSettings() != null 180 | && extensionInfo.getSettings().size() > 0) { 181 | if (meta.getSettings() == null) { 182 | meta.setSettings(new HashMap<>()); 183 | } 184 | if (meta.getSettings().size() == 0) { 185 | for (Setting setting : extensionInfo.getSettings()) { 186 | meta.getSettings().put(setting.getName(), setting.getValue()); 187 | } 188 | } 189 | } 190 | extensionInfo.setMeta(meta); 191 | } 192 | return extensionInfo; 193 | } 194 | 195 | private static ExtensionInfo parseExtensionDir(File extendDir) { 196 | return parseExtensionDir(extendDir, false); 197 | } 198 | 199 | public static List get() { 200 | return EXTENSION_INFO_LIST; 201 | } 202 | 203 | public static Set getProxyWildCards() { 204 | return PROXY_WILDCARDS; 205 | } 206 | 207 | public static Set getSniffRegexs() { 208 | return SNIFF_REGEXS; 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /main/src/main/java/org/pdown/gui/extension/ExtensionInfo.java: -------------------------------------------------------------------------------- 1 | package org.pdown.gui.extension; 2 | 3 | import java.util.List; 4 | 5 | public class ExtensionInfo { 6 | 7 | private String title; //扩展名称 8 | private double version; //扩展版本号 9 | private String homepage; //扩展主页 10 | private String description; //扩展描述 11 | private List proxyWildcards; //扩展生效配置的域名通配符列表 12 | private List sniffRegexs; //扩展嗅探下载的url正则表达式列表 13 | private List contentScripts; 14 | private HookScript hookScript; //下载状态变更时触发的钩子函数脚本 15 | private List settings; //扩展设置选项 16 | private Meta meta; 17 | 18 | public String getTitle() { 19 | return title; 20 | } 21 | 22 | public ExtensionInfo setTitle(String title) { 23 | this.title = title; 24 | return this; 25 | } 26 | 27 | public double getVersion() { 28 | return version; 29 | } 30 | 31 | public ExtensionInfo setVersion(double version) { 32 | this.version = version; 33 | return this; 34 | } 35 | 36 | public String getHomepage() { 37 | return homepage; 38 | } 39 | 40 | public ExtensionInfo setHomepage(String homepage) { 41 | this.homepage = homepage; 42 | return this; 43 | } 44 | 45 | public String getDescription() { 46 | return description; 47 | } 48 | 49 | public ExtensionInfo setDescription(String description) { 50 | this.description = description; 51 | return this; 52 | } 53 | 54 | public List getProxyWildcards() { 55 | return proxyWildcards; 56 | } 57 | 58 | public ExtensionInfo setProxyWildcards(List proxyWildcards) { 59 | this.proxyWildcards = proxyWildcards; 60 | return this; 61 | } 62 | 63 | public List getSniffRegexs() { 64 | return sniffRegexs; 65 | } 66 | 67 | public ExtensionInfo setSniffRegexs(List sniffRegexs) { 68 | this.sniffRegexs = sniffRegexs; 69 | return this; 70 | } 71 | 72 | public List getContentScripts() { 73 | return contentScripts; 74 | } 75 | 76 | public ExtensionInfo setContentScripts(List contentScripts) { 77 | this.contentScripts = contentScripts; 78 | return this; 79 | } 80 | 81 | public HookScript getHookScript() { 82 | return hookScript; 83 | } 84 | 85 | public ExtensionInfo setHookScript(HookScript hookScript) { 86 | this.hookScript = hookScript; 87 | return this; 88 | } 89 | 90 | public List getSettings() { 91 | return settings; 92 | } 93 | 94 | public ExtensionInfo setSettings(List settings) { 95 | this.settings = settings; 96 | return this; 97 | } 98 | 99 | public Meta getMeta() { 100 | return meta; 101 | } 102 | 103 | public ExtensionInfo setMeta(Meta meta) { 104 | this.meta = meta; 105 | return this; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /main/src/main/java/org/pdown/gui/extension/HookScript.java: -------------------------------------------------------------------------------- 1 | package org.pdown.gui.extension; 2 | 3 | import java.util.Arrays; 4 | 5 | public class HookScript { 6 | 7 | public static final String EVENT_RESOLVE = "resolve"; 8 | public static final String EVENT_START = "start"; 9 | public static final String EVENT_RESUME = "resume"; 10 | public static final String EVENT_PAUSE = "pause"; 11 | public static final String EVENT_ERROR = "error"; 12 | public static final String EVENT_DONE = "done"; 13 | public static final String EVENT_DELETE = "delete"; 14 | 15 | private Event[] events; 16 | private String script; 17 | 18 | public Event[] getEvents() { 19 | return events; 20 | } 21 | 22 | public HookScript setEvents(Event[] events) { 23 | this.events = events; 24 | return this; 25 | } 26 | 27 | public String getScript() { 28 | return script; 29 | } 30 | 31 | public HookScript setScript(String script) { 32 | this.script = script; 33 | return this; 34 | } 35 | 36 | /** 37 | * 判断扩展是否有注册钩子函数 38 | */ 39 | public Event hasEvent(String event, String url) { 40 | String matchUrl = url != null ? url.replaceAll("^(?i)(https?://)", "") : ""; 41 | if (events != null) { 42 | return Arrays.stream(events) 43 | .filter(e -> event.equalsIgnoreCase(e.getOn()) && (e.getMatches() == null || (Arrays.stream(e.getMatches()).anyMatch(m -> matchUrl.matches(m))))) 44 | .findFirst() 45 | .orElse(null); 46 | } 47 | return null; 48 | } 49 | 50 | public static class Event { 51 | 52 | private String on; 53 | private String[] matches; 54 | private String method; 55 | 56 | public String getOn() { 57 | return on; 58 | } 59 | 60 | public Event setOn(String on) { 61 | this.on = on; 62 | return this; 63 | } 64 | 65 | public String[] getMatches() { 66 | return matches; 67 | } 68 | 69 | public Event setMatches(String[] matches) { 70 | this.matches = matches; 71 | return this; 72 | } 73 | 74 | public String getMethod() { 75 | return method; 76 | } 77 | 78 | public Event setMethod(String method) { 79 | this.method = method; 80 | return this; 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /main/src/main/java/org/pdown/gui/extension/Meta.java: -------------------------------------------------------------------------------- 1 | package org.pdown.gui.extension; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | import java.util.Map; 6 | import org.pdown.rest.util.ContentUtil; 7 | 8 | public class Meta { 9 | 10 | public transient static final String CONFIG_FILE = ".ext_data/.config.dat"; 11 | 12 | private transient String path; 13 | private transient String fullPath; 14 | private boolean enabled = true; 15 | private boolean local = true; 16 | private Map settings; 17 | private Map data; 18 | 19 | public String getPath() { 20 | return path; 21 | } 22 | 23 | public Meta setPath(String path) { 24 | this.path = path; 25 | return this; 26 | } 27 | 28 | public String getFullPath() { 29 | return fullPath; 30 | } 31 | 32 | public Meta setFullPath(String fullPath) { 33 | this.fullPath = fullPath; 34 | return this; 35 | } 36 | 37 | public boolean isEnabled() { 38 | return enabled; 39 | } 40 | 41 | public Meta setEnabled(boolean enabled) { 42 | this.enabled = enabled; 43 | return this; 44 | } 45 | 46 | public Map getSettings() { 47 | return settings; 48 | } 49 | 50 | public Meta setSettings(Map settings) { 51 | this.settings = settings; 52 | return this; 53 | } 54 | 55 | public Map getData() { 56 | return data; 57 | } 58 | 59 | public Meta setData(Map data) { 60 | this.data = data; 61 | return this; 62 | } 63 | 64 | public boolean isLocal() { 65 | return local; 66 | } 67 | 68 | public void setLocal(boolean local) { 69 | this.local = local; 70 | } 71 | 72 | public void save() { 73 | try { 74 | ContentUtil.save(this, getFullPath() + File.separator + CONFIG_FILE, true); 75 | } catch (IOException e) { 76 | } 77 | } 78 | 79 | public static Meta load(String path) { 80 | Meta meta = null; 81 | try { 82 | meta = ContentUtil.get(path + File.separator + CONFIG_FILE, Meta.class); 83 | } catch (IOException e) { 84 | } 85 | if (meta == null) { 86 | meta = new Meta(); 87 | } 88 | meta.setPath("/" + new File(path).getName()); 89 | meta.setFullPath(path); 90 | return meta; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /main/src/main/java/org/pdown/gui/extension/Setting.java: -------------------------------------------------------------------------------- 1 | package org.pdown.gui.extension; 2 | 3 | import java.util.Map; 4 | 5 | public class Setting { 6 | 7 | private String name; 8 | private String title; 9 | private String type; 10 | private Object value; 11 | private String description; 12 | private boolean isMultiple; 13 | private Map options; 14 | 15 | public String getName() { 16 | return name; 17 | } 18 | 19 | public Setting setName(String name) { 20 | this.name = name; 21 | return this; 22 | } 23 | 24 | public String getTitle() { 25 | return title; 26 | } 27 | 28 | public Setting setTitle(String title) { 29 | this.title = title; 30 | return this; 31 | } 32 | 33 | public String getType() { 34 | return type; 35 | } 36 | 37 | public Setting setType(String type) { 38 | this.type = type; 39 | return this; 40 | } 41 | 42 | public Object getValue() { 43 | return value; 44 | } 45 | 46 | public Setting setValue(Object value) { 47 | this.value = value; 48 | return this; 49 | } 50 | 51 | public String getDescription() { 52 | return description; 53 | } 54 | 55 | public Setting setDescription(String description) { 56 | this.description = description; 57 | return this; 58 | } 59 | 60 | public boolean isMultiple() { 61 | return isMultiple; 62 | } 63 | 64 | public Setting setMultiple(boolean multiple) { 65 | isMultiple = multiple; 66 | return this; 67 | } 68 | 69 | public Map getOptions() { 70 | return options; 71 | } 72 | 73 | public Setting setOptions(Map options) { 74 | this.options = options; 75 | return this; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /main/src/main/java/org/pdown/gui/extension/jsruntime/JavascriptEngine.java: -------------------------------------------------------------------------------- 1 | package org.pdown.gui.extension.jsruntime; 2 | 3 | import com.fasterxml.jackson.core.JsonProcessingException; 4 | import javax.script.Invocable; 5 | import javax.script.ScriptContext; 6 | import javax.script.ScriptEngine; 7 | import javax.script.ScriptEngineManager; 8 | import javax.script.ScriptException; 9 | import javax.script.SimpleScriptContext; 10 | import jdk.nashorn.api.scripting.ClassFilter; 11 | import jdk.nashorn.api.scripting.NashornScriptEngineFactory; 12 | import org.pdown.gui.extension.jsruntime.polyfill.Window; 13 | import org.pdown.rest.form.HttpRequestForm; 14 | 15 | public class JavascriptEngine { 16 | 17 | public static ScriptEngine buildEngine() throws ScriptException, NoSuchMethodException { 18 | NashornScriptEngineFactory factory = new NashornScriptEngineFactory(); 19 | ScriptEngine engine = factory.getScriptEngine(new SafeClassFilter()); 20 | Window window = new Window(); 21 | Object global = engine.eval("this"); 22 | Object jsObject = engine.eval("Object"); 23 | Invocable invocable = (Invocable) engine; 24 | invocable.invokeMethod(jsObject, "bindProperties", global, window); 25 | engine.eval("var window = this"); 26 | return engine; 27 | } 28 | 29 | /** 30 | * 禁止任何显式调用java代码 31 | */ 32 | private static class SafeClassFilter implements ClassFilter { 33 | 34 | @Override 35 | public boolean exposeToScripts(String s) { 36 | return false; 37 | } 38 | } 39 | 40 | public static void main(String[] args) throws ScriptException, NoSuchMethodException, JsonProcessingException, InterruptedException { 41 | ScriptEngine engine = buildEngine(); 42 | Invocable invocable = (Invocable) engine; 43 | engine.eval("load('E:/study/extensions/bilibili-helper/dist/hook.js')"); 44 | HttpRequestForm requestForm = new HttpRequestForm(); 45 | requestForm.setUrl("https://www.bilibili.com/video/av34765642"); 46 | Object result = invocable.invokeFunction("error"); 47 | ScriptContext ctx = new SimpleScriptContext(); 48 | ctx.setAttribute("result", result, ScriptContext.ENGINE_SCOPE); 49 | System.out.println(engine.eval("!!result&&typeof result=='object'&&typeof result.then=='function'", ctx)); 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /main/src/main/java/org/pdown/gui/extension/jsruntime/polyfill/Window.java: -------------------------------------------------------------------------------- 1 | package org.pdown.gui.extension.jsruntime.polyfill; 2 | 3 | import java.util.concurrent.TimeUnit; 4 | import java.util.concurrent.atomic.AtomicLong; 5 | import java.util.function.Function; 6 | import jdk.internal.dynalink.beans.StaticClass; 7 | import org.pdown.gui.extension.jsruntime.polyfill.property.Console; 8 | import org.pdown.gui.extension.jsruntime.polyfill.property.Document; 9 | 10 | public class Window { 11 | 12 | private static final String TIMEOUT_THREAD_NAME = "babel4j-timeout-"; 13 | private static final String INTERVAL_THREAD_NAME = "babel4j-interval-"; 14 | private static final AtomicLong THREAD_ID = new AtomicLong(0); 15 | 16 | public Console console = new Console(); 17 | public Document document = new Document(); 18 | public StaticClass XMLHttpRequest = StaticClass.forClass(org.pdown.gui.extension.jsruntime.polyfill.property.XMLHttpRequest.class); 19 | 20 | public long setTimeout(Function function, long timeout) { 21 | Long id = THREAD_ID.addAndGet(1); 22 | new Thread(() -> { 23 | try { 24 | TimeUnit.MILLISECONDS.sleep(timeout); 25 | function.apply(null); 26 | } catch (InterruptedException e) { 27 | } 28 | }, TIMEOUT_THREAD_NAME + id).start(); 29 | return id; 30 | } 31 | 32 | public void clearTimeout(Long id) { 33 | Thread temp = Thread.getAllStackTraces().keySet().stream() 34 | .filter(thread -> (TIMEOUT_THREAD_NAME + id).equals(thread.getName())) 35 | .findFirst() 36 | .orElse(null); 37 | if (temp != null) { 38 | temp.interrupt(); 39 | } 40 | } 41 | 42 | public long setInterval(Function function, long timeout) { 43 | Long id = THREAD_ID.addAndGet(1); 44 | new Thread(() -> { 45 | try { 46 | while (true) { 47 | TimeUnit.MILLISECONDS.sleep(timeout); 48 | function.apply(null); 49 | } 50 | } catch (InterruptedException e) { 51 | } 52 | }, INTERVAL_THREAD_NAME + id).start(); 53 | return id; 54 | } 55 | 56 | public void clearInterval(Long id) { 57 | Thread temp = Thread.getAllStackTraces().keySet().stream() 58 | .filter(thread -> (INTERVAL_THREAD_NAME + id).equals(thread.getName())) 59 | .findFirst() 60 | .orElse(null); 61 | if (temp != null) { 62 | temp.interrupt(); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /main/src/main/java/org/pdown/gui/extension/jsruntime/polyfill/property/Console.java: -------------------------------------------------------------------------------- 1 | package org.pdown.gui.extension.jsruntime.polyfill.property; 2 | 3 | public class Console { 4 | 5 | public void log(Object object) { 6 | System.out.println(object); 7 | } 8 | 9 | public void debug(Object object) { 10 | System.out.println(object); 11 | } 12 | 13 | public void error(Object object) { 14 | if (object instanceof Throwable) { 15 | Throwable throwable = (Throwable) object; 16 | throwable.printStackTrace(); 17 | } else { 18 | System.out.println(object); 19 | } 20 | } 21 | 22 | public void error(Object msg, Object throwable) { 23 | if (throwable instanceof Throwable) { 24 | ((Throwable) throwable).printStackTrace(); 25 | } else { 26 | System.out.println(msg); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /main/src/main/java/org/pdown/gui/extension/jsruntime/polyfill/property/Document.java: -------------------------------------------------------------------------------- 1 | package org.pdown.gui.extension.jsruntime.polyfill.property; 2 | 3 | public class Document { 4 | 5 | private String cookie; 6 | 7 | public String getCookie() { 8 | return cookie; 9 | } 10 | 11 | public void setCookie(String cookie) { 12 | this.cookie = cookie; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /main/src/main/java/org/pdown/gui/extension/jsruntime/polyfill/property/XMLHttpRequest.java: -------------------------------------------------------------------------------- 1 | package org.pdown.gui.extension.jsruntime.polyfill.property; 2 | 3 | import java.io.BufferedReader; 4 | import java.io.IOException; 5 | import java.io.InputStream; 6 | import java.io.InputStreamReader; 7 | import java.io.OutputStream; 8 | import java.net.HttpURLConnection; 9 | import java.net.InetSocketAddress; 10 | import java.net.Proxy; 11 | import java.net.Proxy.Type; 12 | import java.net.URL; 13 | import java.nio.charset.Charset; 14 | import java.util.LinkedHashMap; 15 | import java.util.Map; 16 | import java.util.function.Function; 17 | import java.util.regex.Matcher; 18 | import java.util.regex.Pattern; 19 | import java.util.stream.Collectors; 20 | import java.util.zip.GZIPInputStream; 21 | import org.springframework.util.StringUtils; 22 | 23 | public class XMLHttpRequest { 24 | 25 | private String method; 26 | private String url; 27 | 28 | public Function onreadystatechange; 29 | public int readyState = 0; 30 | public int status = 0; 31 | public String responseText; 32 | 33 | private Map customRequestHeads = new LinkedHashMap<>(); 34 | private Map responseHeads = new LinkedHashMap<>(); 35 | 36 | public void setRequestHeader(String header, String value) { 37 | customRequestHeads.put(header, value); 38 | } 39 | 40 | public String getResponseHeader(String header) { 41 | return responseHeads.get(header.toLowerCase()); 42 | } 43 | 44 | public void open(String method, String url) { 45 | this.method = method; 46 | this.url = url; 47 | } 48 | 49 | public void open(String method, String url, boolean async) { 50 | this.open(method, url); 51 | } 52 | 53 | 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"; 54 | 55 | public void send(String data) throws IOException { 56 | URL u = new URL(url); 57 | HttpURLConnection connection = (HttpURLConnection) u.openConnection(); 58 | readystatechange(1); 59 | connection.setRequestMethod(method.toUpperCase()); 60 | connection.setRequestProperty("User-Agent", DEFAULT_UA); 61 | customRequestHeads.entrySet().stream().forEach(entry -> connection.setRequestProperty(entry.getKey(), entry.getValue())); 62 | connection.setDoOutput(true); 63 | if (data != null && data.trim().length() > 0) { 64 | try ( 65 | OutputStream outputStream = connection.getOutputStream() 66 | ) { 67 | outputStream.write(data.getBytes(Charset.forName("UTF-8"))); 68 | } 69 | } 70 | int code = connection.getResponseCode(); 71 | connection.getHeaderFields().entrySet().forEach(entry -> { 72 | if (entry.getKey() != null) { 73 | responseHeads.put(entry.getKey().toLowerCase(), entry.getValue().stream().collect(Collectors.joining("; "))); 74 | } 75 | } 76 | ); 77 | readystatechange(2, code); 78 | String charset = "UTF-8"; 79 | String contentType = connection.getContentType(); 80 | if (!StringUtils.isEmpty(contentType)) { 81 | Pattern pattern = Pattern.compile("charset=(.*)$", Pattern.CASE_INSENSITIVE); 82 | Matcher matcher = pattern.matcher(contentType); 83 | if (matcher.find()) { 84 | charset = matcher.group(1); 85 | } 86 | } 87 | InputStream inputStream = code != 200 ? connection.getErrorStream() : connection.getInputStream(); 88 | if (responseHeads.entrySet().stream().anyMatch(entry -> "Content-Encoding".equalsIgnoreCase(entry.getKey()) && entry.getValue().matches("^.*(?i)(gzip).*$"))) { 89 | inputStream = new GZIPInputStream(inputStream); 90 | } 91 | try ( 92 | BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, charset)) 93 | ) { 94 | readystatechange(3, code); 95 | responseText = reader.lines().collect(Collectors.joining("\n")); 96 | readystatechange(4, code); 97 | } 98 | } 99 | 100 | public static void main(String[] args) throws IOException { 101 | URL u = new URL("http://www.baidu.com"); 102 | Proxy proxy = new Proxy(Type.SOCKS, new InetSocketAddress("127.0.0.1", 1088)); 103 | HttpURLConnection connection = (HttpURLConnection) u.openConnection(proxy); 104 | System.out.println(connection.getResponseCode()); 105 | } 106 | 107 | public void send() throws IOException { 108 | send(null); 109 | } 110 | 111 | private void readystatechange(int readyState, int status) { 112 | this.readyState = readyState; 113 | this.status = status; 114 | if (onreadystatechange != null) { 115 | onreadystatechange.apply(null); 116 | } 117 | } 118 | 119 | private void readystatechange(int readyState) { 120 | readystatechange(readyState, status); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /main/src/main/java/org/pdown/gui/extension/mitm/intercept/AjaxIntercept.java: -------------------------------------------------------------------------------- 1 | package org.pdown.gui.extension.mitm.intercept; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import com.github.monkeywie.proxyee.intercept.HttpProxyIntercept; 5 | import com.github.monkeywie.proxyee.intercept.HttpProxyInterceptPipeline; 6 | import io.netty.channel.Channel; 7 | import io.netty.handler.codec.http.*; 8 | import io.netty.util.AsciiString; 9 | import org.springframework.util.StringUtils; 10 | 11 | import java.io.ByteArrayOutputStream; 12 | import java.io.IOException; 13 | import java.io.InputStream; 14 | import java.io.OutputStream; 15 | import java.net.HttpURLConnection; 16 | import java.net.URL; 17 | import java.net.URLDecoder; 18 | import java.util.Map; 19 | 20 | /** 21 | * 通过代理服务器代理ajax请求,避免浏览器CORS问题 22 | */ 23 | public class AjaxIntercept extends HttpProxyIntercept { 24 | 25 | private static final String PROXY_SEND_KEY = "X-Proxy-Send"; 26 | 27 | private boolean proxyFlag; 28 | 29 | @Override 30 | public void beforeRequest(Channel clientChannel, HttpRequest httpRequest, HttpProxyInterceptPipeline pipeline) throws Exception { 31 | proxyFlag = httpRequest.headers().contains(PROXY_SEND_KEY); 32 | super.beforeRequest(clientChannel, httpRequest, pipeline); 33 | } 34 | 35 | @Override 36 | public void afterResponse(Channel clientChannel, Channel proxyChannel, HttpResponse httpResponse, HttpProxyInterceptPipeline pipeline) throws Exception { 37 | if (proxyFlag) { 38 | httpResponse.setStatus(HttpResponseStatus.OK); 39 | proxyChannel.close(); 40 | ObjectMapper objectMapper = new ObjectMapper(); 41 | LastHttpContent content = new DefaultLastHttpContent(); 42 | String proxyRequestRaw = URLDecoder.decode(pipeline.getHttpRequest().headers().get(PROXY_SEND_KEY), "utf-8"); 43 | try { 44 | ProxyRequest proxyRequest = objectMapper.readValue(proxyRequestRaw, ProxyRequest.class); 45 | ProxyResponse proxyResponse = doRequest(proxyRequest); 46 | httpResponse.setStatus(HttpResponseStatus.valueOf(proxyResponse.getStatus())); 47 | content.content().writeBytes(proxyResponse.getData()); 48 | } catch (IOException e) { 49 | e.printStackTrace(); 50 | httpResponse.setStatus(HttpResponseStatus.SERVICE_UNAVAILABLE); 51 | } 52 | httpResponse.headers().remove(HttpHeaderNames.CONTENT_ENCODING); 53 | httpResponse.headers().remove(HttpHeaderNames.TRANSFER_ENCODING); 54 | httpResponse.headers().set(HttpHeaderNames.CONTENT_LENGTH, content.content().readableBytes()); 55 | httpResponse.headers().set(HttpHeaderNames.CONTENT_TYPE, AsciiString.cached("application/json; charset=utf-8")); 56 | super.afterResponse(clientChannel, proxyChannel, httpResponse, pipeline); 57 | clientChannel.writeAndFlush(content); 58 | } else { 59 | super.afterResponse(clientChannel, proxyChannel, httpResponse, pipeline); 60 | } 61 | } 62 | 63 | @Override 64 | public void afterResponse(Channel clientChannel, Channel proxyChannel, HttpContent httpContent, HttpProxyInterceptPipeline pipeline) throws Exception { 65 | if (!proxyFlag) { 66 | super.afterResponse(clientChannel, proxyChannel, httpContent, pipeline); 67 | } 68 | } 69 | 70 | private ProxyResponse doRequest(ProxyRequest proxyRequest) throws IOException { 71 | URL url = new URL(proxyRequest.getUrl()); 72 | HttpURLConnection connection = (HttpURLConnection) url.openConnection(); 73 | connection.setRequestMethod(proxyRequest.getMethod().toUpperCase()); 74 | connection.setRequestProperty("Content-Type", "application/json; charset=utf-8"); 75 | if (proxyRequest.getHeads() != null) { 76 | for (Map.Entry entry : proxyRequest.getHeads().entrySet()) { 77 | connection.setRequestProperty(entry.getKey(), entry.getValue()); 78 | } 79 | } 80 | connection.setDoInput(true); 81 | if (!StringUtils.isEmpty(proxyRequest.getRawData())) { 82 | connection.setDoOutput(true); 83 | try ( 84 | OutputStream output = connection.getOutputStream() 85 | ) { 86 | output.write(proxyRequest.getRawData().getBytes()); 87 | output.flush(); 88 | } 89 | } else if (proxyRequest.getData() != null) { 90 | connection.setDoOutput(true); 91 | try ( 92 | OutputStream output = connection.getOutputStream() 93 | ) { 94 | ObjectMapper objectMapper = new ObjectMapper(); 95 | output.write(objectMapper.writeValueAsBytes(proxyRequest.getData())); 96 | output.flush(); 97 | } 98 | } 99 | ProxyResponse proxyResponse = new ProxyResponse(); 100 | proxyResponse.setStatus(connection.getResponseCode()); 101 | try ( 102 | ByteArrayOutputStream output = new ByteArrayOutputStream(); 103 | InputStream input = connection.getResponseCode() == 200 ? connection.getInputStream() : connection.getErrorStream() 104 | ) { 105 | byte[] bts = new byte[8192]; 106 | int len; 107 | while ((len = input.read(bts)) != -1) { 108 | output.write(bts, 0, len); 109 | } 110 | proxyResponse.setData(output.toByteArray()); 111 | return proxyResponse; 112 | } 113 | } 114 | 115 | static class ProxyRequest { 116 | 117 | private String method; 118 | private String url; 119 | private Map heads; 120 | private Map data; 121 | private String rawData; 122 | 123 | 124 | public String getMethod() { 125 | return method; 126 | } 127 | 128 | public void setMethod(String method) { 129 | this.method = method; 130 | } 131 | 132 | public String getUrl() { 133 | return url; 134 | } 135 | 136 | public void setUrl(String url) { 137 | this.url = url; 138 | } 139 | 140 | public Map getHeads() { 141 | return heads; 142 | } 143 | 144 | public void setHeads(Map heads) { 145 | this.heads = heads; 146 | } 147 | 148 | public Map getData() { 149 | return data; 150 | } 151 | 152 | public void setData(Map data) { 153 | this.data = data; 154 | } 155 | 156 | public String getRawData() { 157 | return rawData; 158 | } 159 | 160 | public void setRawData(String rawData) { 161 | this.rawData = rawData; 162 | } 163 | } 164 | 165 | static class ProxyResponse { 166 | 167 | private int status; 168 | private byte[] data; 169 | 170 | public int getStatus() { 171 | return status; 172 | } 173 | 174 | public void setStatus(int status) { 175 | this.status = status; 176 | } 177 | 178 | public byte[] getData() { 179 | return data; 180 | } 181 | 182 | public void setData(byte[] data) { 183 | this.data = data; 184 | } 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /main/src/main/java/org/pdown/gui/extension/mitm/intercept/CookieIntercept.java: -------------------------------------------------------------------------------- 1 | package org.pdown.gui.extension.mitm.intercept; 2 | 3 | import com.github.monkeywie.proxyee.intercept.HttpProxyIntercept; 4 | import com.github.monkeywie.proxyee.intercept.HttpProxyInterceptPipeline; 5 | import io.netty.channel.Channel; 6 | import io.netty.handler.codec.http.DefaultHttpHeaders; 7 | import io.netty.handler.codec.http.DefaultHttpResponse; 8 | import io.netty.handler.codec.http.DefaultLastHttpContent; 9 | import io.netty.handler.codec.http.HttpHeaderNames; 10 | import io.netty.handler.codec.http.HttpRequest; 11 | import io.netty.handler.codec.http.HttpResponse; 12 | import io.netty.handler.codec.http.HttpResponseStatus; 13 | import io.netty.handler.codec.http.HttpVersion; 14 | import io.netty.util.AsciiString; 15 | import io.netty.util.internal.StringUtil; 16 | import java.net.URL; 17 | 18 | /** 19 | * 嗅探目标网站cookie,支持HTTP only 20 | */ 21 | public class CookieIntercept extends HttpProxyIntercept { 22 | 23 | @Override 24 | public void beforeRequest(Channel clientChannel, HttpRequest httpRequest, HttpProxyInterceptPipeline pipeline) throws Exception { 25 | String acceptValue = httpRequest.headers().get(HttpHeaderNames.ACCEPT); 26 | if (acceptValue != null && acceptValue.contains("application/x-sniff-cookie")) { 27 | HttpResponse httpResponse = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, new DefaultHttpHeaders()); 28 | httpResponse.headers().set(HttpHeaderNames.CONTENT_LENGTH, 0); 29 | //https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Access-Control-Expose-Headers 30 | AsciiString customHeadKey = AsciiString.cached("X-Sniff-Cookie"); 31 | String cookie = pipeline.getHttpRequest().headers().get(HttpHeaderNames.COOKIE); 32 | httpResponse.headers().set(customHeadKey, cookie == null ? "" : cookie); 33 | httpResponse.headers().set(HttpHeaderNames.ACCESS_CONTROL_EXPOSE_HEADERS, customHeadKey); 34 | String origin = httpRequest.headers().get(HttpHeaderNames.ORIGIN); 35 | if (StringUtil.isNullOrEmpty(origin)) { 36 | String referer = httpRequest.headers().get(HttpHeaderNames.REFERER); 37 | URL url = new URL(referer); 38 | origin = url.getHost(); 39 | } 40 | httpResponse.headers().set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_ORIGIN, origin); 41 | httpResponse.headers().set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_CREDENTIALS, true); 42 | clientChannel.writeAndFlush(httpResponse); 43 | clientChannel.writeAndFlush(new DefaultLastHttpContent()); 44 | clientChannel.close(); 45 | } else { 46 | super.beforeRequest(clientChannel, httpRequest, pipeline); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /main/src/main/java/org/pdown/gui/extension/mitm/intercept/ScriptIntercept.java: -------------------------------------------------------------------------------- 1 | package org.pdown.gui.extension.mitm.intercept; 2 | 3 | import com.github.monkeywie.proxyee.intercept.HttpProxyInterceptPipeline; 4 | import com.github.monkeywie.proxyee.intercept.common.FullResponseIntercept; 5 | import com.github.monkeywie.proxyee.util.ByteUtil; 6 | import io.netty.handler.codec.http.FullHttpResponse; 7 | import io.netty.handler.codec.http.HttpRequest; 8 | import io.netty.handler.codec.http.HttpResponse; 9 | import java.io.File; 10 | import java.io.IOException; 11 | import java.nio.charset.Charset; 12 | import java.nio.file.Files; 13 | import java.util.List; 14 | import org.pdown.core.util.HttpDownUtil; 15 | import org.pdown.gui.extension.ContentScript; 16 | import org.pdown.gui.extension.ExtensionContent; 17 | import org.pdown.gui.extension.ExtensionInfo; 18 | import org.pdown.gui.extension.util.ExtensionUtil; 19 | 20 | public class ScriptIntercept extends FullResponseIntercept { 21 | 22 | @Override 23 | public boolean match(HttpRequest httpRequest, HttpResponse httpResponse, HttpProxyInterceptPipeline pipeline) { 24 | return isHtml(httpRequest, httpResponse); 25 | } 26 | 27 | private static final String INSERT_TOKEN = ""; 28 | private static final String INIT_TEMPLATE = ";(function (pdown) {\n" 29 | + " ${content}\n" 30 | + "})(${runtime})"; 31 | 32 | private String readInsertTemplate(ExtensionInfo extensionInfo) { 33 | String js = ExtensionUtil.readRuntimeTemplate(extensionInfo); 34 | js = INIT_TEMPLATE.replace("${runtime}", js); 35 | js = ""; 36 | return js; 37 | } 38 | 39 | @Override 40 | public void handelResponse(HttpRequest httpRequest, FullHttpResponse httpResponse, HttpProxyInterceptPipeline pipeline) { 41 | List extensionInfoList = ExtensionContent.get(); 42 | if (isEmpty(extensionInfoList)) { 43 | return; 44 | } 45 | for (ExtensionInfo extensionInfo : extensionInfoList) { 46 | if (isEmpty(extensionInfo.getContentScripts())) { 47 | continue; 48 | } 49 | for (ContentScript contentScript : extensionInfo.getContentScripts()) { 50 | //扩展注入正则表达式与当前访问的url匹配则注入脚本 51 | String url = HttpDownUtil.getUrl(httpRequest); 52 | if (contentScript.isMatch(url)) { 53 | String apiTemplate = readInsertTemplate(extensionInfo); 54 | StringBuilder scriptsBuilder = new StringBuilder(); 55 | for (String script : contentScript.getScripts()) { 56 | File scriptFile = new File(extensionInfo.getMeta().getFullPath() + File.separator + script); 57 | if (scriptFile.exists() && scriptFile.isFile()) { 58 | try { 59 | scriptsBuilder.append(new String(Files.readAllBytes(scriptFile.toPath()), "UTF-8")); 60 | } catch (IOException e) { 61 | } 62 | } 63 | } 64 | apiTemplate = apiTemplate.replace("${content}", scriptsBuilder.toString()); 65 | int index = ByteUtil.findText(httpResponse.content(), INSERT_TOKEN); 66 | ByteUtil.insertText(httpResponse.content(), index == -1 ? 0 : index - INSERT_TOKEN.length(), apiTemplate, Charset.forName("UTF-8")); 67 | } 68 | } 69 | } 70 | } 71 | 72 | private boolean isEmpty(List list) { 73 | return list == null || list.size() == 0; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /main/src/main/java/org/pdown/gui/extension/mitm/intercept/SniffIntercept.java: -------------------------------------------------------------------------------- 1 | package org.pdown.gui.extension.mitm.intercept; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import com.github.monkeywie.proxyee.intercept.HttpProxyIntercept; 5 | import com.github.monkeywie.proxyee.intercept.HttpProxyInterceptPipeline; 6 | import com.github.monkeywie.proxyee.util.HttpUtil; 7 | import io.netty.buffer.ByteBuf; 8 | import io.netty.buffer.PooledByteBufAllocator; 9 | import io.netty.channel.Channel; 10 | import io.netty.channel.nio.NioEventLoopGroup; 11 | import io.netty.handler.codec.http.DefaultLastHttpContent; 12 | import io.netty.handler.codec.http.HttpContent; 13 | import io.netty.handler.codec.http.HttpHeaderNames; 14 | import io.netty.handler.codec.http.HttpHeaderValues; 15 | import io.netty.handler.codec.http.HttpHeaders; 16 | import io.netty.handler.codec.http.HttpRequest; 17 | import io.netty.handler.codec.http.HttpResponse; 18 | import io.netty.handler.codec.http.LastHttpContent; 19 | import io.netty.util.ReferenceCountUtil; 20 | import java.net.URLEncoder; 21 | import java.util.Arrays; 22 | import java.util.Set; 23 | import org.pdown.core.entity.HttpRequestInfo; 24 | import org.pdown.core.entity.HttpResponseInfo; 25 | import org.pdown.core.util.HttpDownUtil; 26 | import org.pdown.core.util.ProtoUtil.RequestProto; 27 | import org.pdown.gui.DownApplication; 28 | import org.pdown.gui.extension.ExtensionContent; 29 | import org.pdown.rest.form.HttpRequestForm; 30 | import org.slf4j.Logger; 31 | import org.slf4j.LoggerFactory; 32 | 33 | public class SniffIntercept extends HttpProxyIntercept { 34 | 35 | private final static Logger LOGGER = LoggerFactory.getLogger(SniffIntercept.class); 36 | 37 | private boolean matchFlag = false; 38 | 39 | private ByteBuf content; 40 | private boolean downFlag = false; 41 | 42 | @Override 43 | public void beforeRequest(Channel clientChannel, HttpRequest httpRequest, 44 | HttpProxyInterceptPipeline pipeline) throws Exception { 45 | Set sniffRegexs = ExtensionContent.getSniffRegexs(); 46 | if (sniffRegexs == null) { 47 | matchFlag = false; 48 | } else { 49 | matchFlag = sniffRegexs.stream().anyMatch(regex -> HttpUtil.checkUrl(httpRequest, regex)); 50 | } 51 | if (!matchFlag) { 52 | super.beforeRequest(clientChannel, httpRequest, pipeline); 53 | return; 54 | } 55 | String contentLength = httpRequest.headers().get(HttpHeaderNames.CONTENT_LENGTH); 56 | //缓存request content 57 | if (contentLength != null) { 58 | content = PooledByteBufAllocator.DEFAULT.buffer(); 59 | } 60 | pipeline.beforeRequest(clientChannel, HttpRequestInfo.adapter(httpRequest)); 61 | } 62 | 63 | @Override 64 | public void beforeRequest(Channel clientChannel, HttpContent httpContent, 65 | HttpProxyInterceptPipeline pipeline) throws Exception { 66 | if (!matchFlag) { 67 | super.beforeRequest(clientChannel, httpContent, pipeline); 68 | return; 69 | } 70 | if (content != null) { 71 | ByteBuf temp = httpContent.content().slice(); 72 | content.writeBytes(temp); 73 | if (httpContent instanceof LastHttpContent) { 74 | try { 75 | byte[] contentBts = new byte[content.readableBytes()]; 76 | content.readBytes(contentBts); 77 | ((HttpRequestInfo) pipeline.getHttpRequest()).setContent(contentBts); 78 | } finally { 79 | ReferenceCountUtil.release(content); 80 | } 81 | } 82 | } 83 | pipeline.beforeRequest(clientChannel, httpContent); 84 | } 85 | 86 | @Override 87 | public void afterResponse(Channel clientChannel, Channel proxyChannel, HttpResponse httpResponse, 88 | HttpProxyInterceptPipeline pipeline) throws Exception { 89 | if (!matchFlag) { 90 | super.afterResponse(clientChannel, proxyChannel, httpResponse, pipeline); 91 | return; 92 | } 93 | if ((httpResponse.status().code() + "").indexOf("20") == 0) { //响应码为20x 94 | HttpHeaders httpResHeaders = httpResponse.headers(); 95 | String accept = pipeline.getHttpRequest().headers().get(HttpHeaderNames.ACCEPT); 96 | String contentType = httpResHeaders.get(HttpHeaderNames.CONTENT_TYPE); 97 | //有两种情况进行下载 1.url后缀为.xxx 2.带有CONTENT_DISPOSITION:ATTACHMENT响应头 98 | String disposition = httpResHeaders.get(HttpHeaderNames.CONTENT_DISPOSITION); 99 | if (accept != null 100 | && accept.matches("^.*text/html.*$") 101 | && ((disposition != null 102 | && disposition.contains(HttpHeaderValues.ATTACHMENT) 103 | && disposition.contains(HttpHeaderValues.FILENAME)) 104 | || isDownContentType(contentType))) { 105 | downFlag = true; 106 | } 107 | 108 | HttpRequestInfo httpRequestInfo = (HttpRequestInfo) pipeline.getHttpRequest(); 109 | if (downFlag) { //如果是下载 110 | LOGGER.debug("=====================下载===========================\n" + 111 | pipeline.getHttpRequest().toString() + "\n" + 112 | "------------------------------------------------" + 113 | httpResponse.toString() + "\n" + 114 | "================================================"); 115 | proxyChannel.close();//关闭嗅探下载连接 116 | httpRequestInfo.setRequestProto(new RequestProto(pipeline.getRequestProto().getHost(), pipeline.getRequestProto().getPort(), pipeline.getRequestProto().getSsl())); 117 | HttpRequestForm requestForm = HttpRequestForm.parse(httpRequestInfo); 118 | HttpResponseInfo responseInfo = HttpDownUtil.getHttpResponseInfo(httpRequestInfo, null, null, (NioEventLoopGroup) clientChannel.eventLoop().parent()); 119 | httpResponse.headers().clear(); 120 | httpResponse.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html"); 121 | httpResponse.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE); 122 | String js = ""; 123 | HttpContent httpContent = new DefaultLastHttpContent(); 124 | httpContent.content().writeBytes(js.getBytes()); 125 | httpResponse.headers().set(HttpHeaderNames.CONTENT_LENGTH, httpContent.content().readableBytes()); 126 | clientChannel.writeAndFlush(httpResponse); 127 | clientChannel.writeAndFlush(httpContent); 128 | clientChannel.close(); 129 | ObjectMapper objectMapper = new ObjectMapper(); 130 | String requestParam = URLEncoder.encode(objectMapper.writeValueAsString(requestForm), "utf-8"); 131 | String responseParam = URLEncoder.encode(objectMapper.writeValueAsString(responseInfo), "utf-8"); 132 | String uri = "/#/tasks?request=" + requestParam + "&response=" + responseParam; 133 | DownApplication.INSTANCE.loadUri(uri, false); 134 | return; 135 | } else { 136 | if (httpRequestInfo.content() != null) { 137 | httpRequestInfo.setContent(null); 138 | } 139 | } 140 | } 141 | super.afterResponse(clientChannel, proxyChannel, httpResponse, pipeline); 142 | } 143 | 144 | @Override 145 | public void afterResponse(Channel clientChannel, Channel proxyChannel, HttpContent httpContent, 146 | HttpProxyInterceptPipeline pipeline) throws Exception { 147 | if (!matchFlag) { 148 | super.afterResponse(clientChannel, proxyChannel, httpContent, pipeline); 149 | return; 150 | } 151 | if (downFlag) { 152 | httpContent.release(); 153 | } else { 154 | pipeline.afterResponse(clientChannel, proxyChannel, httpContent); 155 | } 156 | } 157 | 158 | //https://chromium.googlesource.com/chromium/src/+/master/net/base/mime_util.cc 159 | private static final String[] CONTENT_TYPES = { 160 | "application/javascript", 161 | "application/x-javascript", 162 | "application/wasm", 163 | "application/x-chrome-extension", 164 | "application/xhtml+xml", 165 | "application/font-woff", 166 | "application/json", 167 | "application/x-shockwave-flash", 168 | "audio/mpeg", 169 | "audio/flac", 170 | "audio/mp3", 171 | "audio/ogg", 172 | "audio/wav", 173 | "audio/webm", 174 | "audio/x-m4a", 175 | "image/gif", 176 | "image/jpeg", 177 | "image/png", 178 | "image/apng", 179 | "image/webp", 180 | "image/x-icon", 181 | "image/bmp", 182 | "image/jpeg", 183 | "image/svg+xml", 184 | "image/tiff", 185 | "image/vnd.microsoft.icon", 186 | "image/x-png", 187 | "image/x-xbitmap", 188 | "video/webm", 189 | "video/ogg", 190 | "video/mp4", 191 | "video/mpeg", 192 | "text/css", 193 | "text/html", 194 | "text/xml", 195 | "text/calendar", 196 | "text/html", 197 | "text/plain", 198 | "text/x-sh", 199 | "text/xml", 200 | "multipart/related", 201 | "message/rfc822", 202 | }; 203 | 204 | private boolean isDownContentType(String contentType) { 205 | if (contentType != null) { 206 | String contentTypeFinal = contentType.split(";")[0].trim().toLowerCase(); 207 | return Arrays.stream(CONTENT_TYPES).noneMatch(type -> contentTypeFinal.equals(type)); 208 | } 209 | return true; 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /main/src/main/java/org/pdown/gui/extension/mitm/server/PDownProxyServer.java: -------------------------------------------------------------------------------- 1 | package org.pdown.gui.extension.mitm.server; 2 | 3 | import com.github.monkeywie.proxyee.exception.HttpProxyExceptionHandle; 4 | import com.github.monkeywie.proxyee.intercept.HttpProxyInterceptInitializer; 5 | import com.github.monkeywie.proxyee.intercept.HttpProxyInterceptPipeline; 6 | import com.github.monkeywie.proxyee.server.HttpProxyServer; 7 | import com.github.monkeywie.proxyee.server.HttpProxyServerConfig; 8 | import io.netty.channel.Channel; 9 | import org.pdown.gui.content.PDownConfigContent; 10 | import org.pdown.gui.entity.PDownConfigInfo; 11 | import org.pdown.gui.extension.mitm.intercept.AjaxIntercept; 12 | import org.pdown.gui.extension.mitm.intercept.CookieIntercept; 13 | import org.pdown.gui.extension.mitm.intercept.ScriptIntercept; 14 | import org.pdown.gui.extension.mitm.intercept.SniffIntercept; 15 | import org.pdown.gui.extension.mitm.ssl.PDownCACertFactory; 16 | import org.slf4j.Logger; 17 | import org.slf4j.LoggerFactory; 18 | 19 | public class PDownProxyServer { 20 | 21 | private static final Logger LOGGER = LoggerFactory.getLogger(PDownProxyServer.class); 22 | 23 | private static volatile HttpProxyServer httpProxyServer; 24 | public static volatile boolean isStart = false; 25 | 26 | public static void start(int port) { 27 | HttpProxyServerConfig config = new HttpProxyServerConfig(); 28 | //处理ssl 29 | config.setHandleSsl(true); 30 | //线程池数量都设置为1 31 | config.setBossGroupThreads(1); 32 | config.setWorkerGroupThreads(1); 33 | config.setProxyGroupThreads(1); 34 | httpProxyServer = new HttpProxyServer() 35 | .serverConfig(config) 36 | .proxyConfig(PDownConfigInfo.convert(PDownConfigContent.getInstance().get().getProxyConfig())) 37 | .caCertFactory(new PDownCACertFactory()) 38 | .proxyInterceptInitializer(new HttpProxyInterceptInitializer() { 39 | @Override 40 | public void init(HttpProxyInterceptPipeline pipeline) { 41 | pipeline.addLast(new CookieIntercept()); 42 | pipeline.addLast(new AjaxIntercept()); 43 | pipeline.addLast(new ScriptIntercept()); 44 | pipeline.addLast(new SniffIntercept()); 45 | } 46 | }) 47 | .httpProxyExceptionHandle(new HttpProxyExceptionHandle() { 48 | @Override 49 | public void beforeCatch(Channel clientChannel, Throwable cause) throws Exception { 50 | LOGGER.warn("beforeCatch", cause); 51 | } 52 | 53 | @Override 54 | public void afterCatch(Channel clientChannel, Channel proxyChannel, Throwable cause) throws Exception { 55 | LOGGER.warn("afterCatch", cause); 56 | } 57 | }); 58 | isStart = true; 59 | httpProxyServer.start(port); 60 | } 61 | 62 | public static void close() { 63 | if (httpProxyServer != null) { 64 | httpProxyServer.close(); 65 | } 66 | isStart = false; 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /main/src/main/java/org/pdown/gui/extension/mitm/ssl/PDownCACertFactory.java: -------------------------------------------------------------------------------- 1 | package org.pdown.gui.extension.mitm.ssl; 2 | 3 | import com.github.monkeywie.proxyee.crt.CertUtil; 4 | import com.github.monkeywie.proxyee.server.HttpProxyCACertFactory; 5 | import java.io.File; 6 | import java.security.PrivateKey; 7 | import java.security.cert.X509Certificate; 8 | import org.pdown.rest.util.PathUtil; 9 | 10 | public class PDownCACertFactory implements HttpProxyCACertFactory { 11 | 12 | private static final String SSL_PATH = PathUtil.ROOT_PATH + File.separator + "ssl" + File.separator; 13 | 14 | @Override 15 | public X509Certificate getCACert() throws Exception { 16 | return CertUtil.loadCert(SSL_PATH + "ca.crt"); 17 | } 18 | 19 | @Override 20 | public PrivateKey getCAPriKey() throws Exception { 21 | return CertUtil.loadPriKey(SSL_PATH + ".ca_pri.der"); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /main/src/main/java/org/pdown/gui/extension/mitm/util/ExtensionCertUtil.java: -------------------------------------------------------------------------------- 1 | package org.pdown.gui.extension.mitm.util; 2 | 3 | import com.github.monkeywie.proxyee.crt.CertUtil; 4 | import java.io.File; 5 | import java.io.IOException; 6 | import java.net.URLEncoder; 7 | import java.nio.file.Files; 8 | import java.nio.file.Paths; 9 | import java.security.KeyPair; 10 | import java.security.MessageDigest; 11 | import java.security.cert.X509Certificate; 12 | import java.util.Date; 13 | import java.util.concurrent.TimeUnit; 14 | import java.util.regex.Matcher; 15 | import java.util.regex.Pattern; 16 | import org.pdown.core.util.FileUtil; 17 | import org.pdown.core.util.OsUtil; 18 | import org.pdown.gui.DownApplication; 19 | import org.pdown.gui.util.ExecUtil; 20 | import sun.security.x509.X500Name; 21 | 22 | /** 23 | * 用于处理系统的证书安装、查询和卸载 24 | */ 25 | public class ExtensionCertUtil { 26 | 27 | 28 | /** 29 | * 在指定目录生成一个ca证书和私钥 30 | */ 31 | public static void buildCert(String path, String subjectName) throws Exception { 32 | //生成ca证书和私钥 33 | KeyPair keyPair = CertUtil.genKeyPair(); 34 | File priKeyFile = FileUtil.createFile(path + File.separator + ".ca_pri.der", true); 35 | File caCertFile = FileUtil.createFile(path + File.separator + "ca.crt", false); 36 | Files.write(Paths.get(priKeyFile.toURI()), keyPair.getPrivate().getEncoded()); 37 | Files.write(Paths.get(caCertFile.toURI()), 38 | CertUtil.genCACert( 39 | "C=CN, ST=GD, L=SZ, O=lee, OU=study, CN=" + subjectName, 40 | new Date(), 41 | new Date(System.currentTimeMillis() + TimeUnit.DAYS.toMillis(3650)), 42 | keyPair) 43 | .getEncoded()); 44 | } 45 | 46 | /** 47 | * 安装证书 48 | */ 49 | public static void installCert(File file) throws IOException { 50 | String path = file.getPath(); 51 | if (OsUtil.isWindows()) { 52 | ExecUtil.execBlock("certutil", 53 | "-addstore", 54 | "-user", 55 | "root", 56 | path); 57 | } else if (OsUtil.isMac()) { 58 | ExecUtil.httpGet("http://127.0.0.1:" + DownApplication.macToolPort + "/cert/install?path=" + URLEncoder.encode(path, "utf-8")); 59 | } 60 | } 61 | 62 | /** 63 | * 通过证书subjectName和sha1,判断系统是否已安装该证书 64 | */ 65 | public static boolean isInstalledCert(File file) throws Exception { 66 | if (!file.exists()) { 67 | return false; 68 | } 69 | if (OsUtil.isUnix()) { 70 | return true; 71 | } 72 | X509Certificate cert = CertUtil.loadCert(file.toURI()); 73 | String subjectName = ((X500Name) cert.getSubjectDN()).getCommonName(); 74 | String sha1 = getCertSHA1(cert); 75 | return findCertList(subjectName).toUpperCase().replaceAll("\\s", "").indexOf(":" + sha1.toUpperCase()) != -1; 76 | } 77 | 78 | /** 79 | * 通过证书subjectName,判断系统是否已安装此subjectName的证书 80 | */ 81 | public static boolean existsCert(String subjectName) throws IOException { 82 | if (OsUtil.isWindows() && findCertList(subjectName).toUpperCase().indexOf("=====") != -1) { 83 | return true; 84 | } else if (OsUtil.isMac() && findCertList(subjectName).toUpperCase().indexOf("BEGIN CERTIFICATE") != -1) { 85 | return true; 86 | } 87 | return false; 88 | } 89 | 90 | /** 91 | * 通过证书name,卸载证书 92 | */ 93 | public static void uninstallCert(String subjectName) throws IOException { 94 | if (OsUtil.isWindows()) { 95 | Pattern pattern = Pattern.compile("(?i)\\(sha1\\):\\s(.*)\r?\n"); 96 | String certList = findCertList(subjectName); 97 | Matcher matcher = pattern.matcher(certList); 98 | while (matcher.find()) { 99 | String hash = matcher.group(1).replaceAll("\\s", ""); 100 | ExecUtil.execBlock("certutil", 101 | "-delstore", 102 | "-user", 103 | "root", 104 | hash); 105 | } 106 | } else if (OsUtil.isMac()) { 107 | String certList = findCertList(subjectName); 108 | Pattern pattern = Pattern.compile("(?i)SHA-1 hash:\\s(.*)\r?\n"); 109 | Matcher matcher = pattern.matcher(certList); 110 | while (matcher.find()) { 111 | String hash = matcher.group(1); 112 | ExecUtil.httpGet("http://127.0.0.1:" + DownApplication.macToolPort + "/cert/uninstall" 113 | + "?hash=" + hash); 114 | } 115 | } 116 | } 117 | 118 | //查询证书列表 119 | private static String findCertList(String subjectName) throws IOException { 120 | if (OsUtil.isWindows()) { 121 | return ExecUtil.exec("certutil ", 122 | "-store", 123 | "-user", 124 | "root", 125 | subjectName); 126 | } else if (OsUtil.isMac()) { 127 | return ExecUtil.exec("security", 128 | "find-certificate", 129 | "-a", 130 | "-c", 131 | subjectName, 132 | "-p", 133 | "-Z", 134 | "/Library/Keychains/System.keychain"); 135 | } 136 | return null; 137 | } 138 | 139 | private static String getCertSHA1(X509Certificate certificate) throws Exception { 140 | MessageDigest md = MessageDigest.getInstance("SHA-1"); 141 | byte[] der = certificate.getEncoded(); 142 | md.update(der); 143 | return btsToHex(md.digest()); 144 | } 145 | 146 | private static String btsToHex(byte[] bts) { 147 | StringBuilder str = new StringBuilder(); 148 | for (byte b : bts) { 149 | str.append(String.format("%2s", Integer.toHexString(b & 0xFF)).replace(" ", "0")); 150 | } 151 | return str.toString(); 152 | } 153 | 154 | public static void main(String[] args) throws Exception { 155 | String subjectName = "ProxyeeDown CA"; 156 | String path = "f:/test/"; 157 | File certFile = new File(path + "ca.crt"); 158 | //证书还未安装 159 | if (!isInstalledCert(certFile)) { 160 | if (existsCert(subjectName)) { 161 | //存在无用证书需要卸载 162 | uninstallCert(subjectName); 163 | } 164 | //生成新的证书 165 | buildCert(path, subjectName); 166 | //安装 167 | installCert(certFile); 168 | } 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /main/src/main/java/org/pdown/gui/extension/mitm/util/ExtensionProxyUtil.java: -------------------------------------------------------------------------------- 1 | package org.pdown.gui.extension.mitm.util; 2 | 3 | import com.sun.jna.Pointer; 4 | import java.io.IOException; 5 | import java.net.InetAddress; 6 | import java.net.NetworkInterface; 7 | import java.net.Socket; 8 | import java.net.SocketException; 9 | import java.util.ArrayList; 10 | import java.util.Arrays; 11 | import java.util.Enumeration; 12 | import java.util.HashMap; 13 | import java.util.List; 14 | import java.util.Map; 15 | import java.util.Map.Entry; 16 | import java.util.regex.Matcher; 17 | import java.util.regex.Pattern; 18 | import org.pdown.core.util.OsUtil; 19 | import org.pdown.gui.DownApplication; 20 | import org.pdown.gui.extension.mitm.util.WinInet.INTERNET_PER_CONN_OPTION; 21 | import org.pdown.gui.extension.mitm.util.WinInet.INTERNET_PER_CONN_OPTION.ByReference; 22 | import org.pdown.gui.extension.mitm.util.WinInet.INTERNET_PER_CONN_OPTION_LIST; 23 | import org.pdown.gui.util.ExecUtil; 24 | 25 | /** 26 | * 系统的代理切换工具类 27 | */ 28 | public class ExtensionProxyUtil { 29 | 30 | /** 31 | * 设置PAC代理 32 | */ 33 | public static void enabledPACProxy(String url) throws IOException { 34 | if (OsUtil.isWindows()) { 35 | String interName = getRemoteInterface(); 36 | INTERNET_PER_CONN_OPTION_LIST list = buildOptionList(interName, 2); 37 | INTERNET_PER_CONN_OPTION[] pOptions = (INTERNET_PER_CONN_OPTION[]) list.pOptions 38 | .toArray(list.dwOptionCount); 39 | // Set flags. 40 | pOptions[0].dwOption = WinInet.INTERNET_PER_CONN_FLAGS; 41 | pOptions[0].Value.dwValue = WinInet.PROXY_TYPE_AUTO_PROXY_URL; 42 | pOptions[0].Value.setType(int.class); 43 | 44 | // Set flags. 45 | pOptions[1].dwOption = WinInet.INTERNET_PER_CONN_AUTOCONFIG_URL; 46 | pOptions[1].Value.pszValue = url; 47 | pOptions[1].Value.setType(String.class); 48 | 49 | refreshOptions(list); 50 | } else if (OsUtil.isMac()) { 51 | String networkService = disabledProxy(); 52 | ExecUtil.httpGet("http://127.0.0.1:" + DownApplication.macToolPort + "/proxy/enabledPAC" 53 | + "?ns=" + networkService 54 | + "&url=" + url); 55 | } 56 | } 57 | 58 | /** 59 | * 启用http代理 60 | */ 61 | public static void enabledHTTPProxy(String host, int port) throws IOException { 62 | if (OsUtil.isWindows()) { 63 | String interName = getRemoteInterface(); 64 | INTERNET_PER_CONN_OPTION_LIST list = buildOptionList(interName, 2); 65 | INTERNET_PER_CONN_OPTION[] pOptions = (INTERNET_PER_CONN_OPTION[]) list.pOptions 66 | .toArray(list.dwOptionCount); 67 | 68 | // Set flags. 69 | pOptions[0].dwOption = WinInet.INTERNET_PER_CONN_FLAGS; 70 | pOptions[0].Value.dwValue = WinInet.PROXY_TYPE_PROXY; 71 | pOptions[0].Value.setType(int.class); 72 | 73 | // Set proxy name. 74 | pOptions[1].dwOption = WinInet.INTERNET_PER_CONN_PROXY_SERVER; 75 | pOptions[1].Value.pszValue = host + ":" + port; 76 | pOptions[1].Value.setType(String.class); 77 | 78 | refreshOptions(list); 79 | } else if (OsUtil.isMac()) { 80 | String networkService = disabledProxy(); 81 | ExecUtil.httpGet("http://127.0.0.1:" + DownApplication.macToolPort + "/proxy/enabledHTTP" 82 | + "?ns=" + networkService 83 | + "&host=" + host 84 | + "&port=" + port); 85 | } 86 | } 87 | 88 | /** 89 | * 禁用代理 90 | */ 91 | public static String disabledProxy() throws IOException { 92 | if (OsUtil.isWindows()) { 93 | String interName = getRemoteInterface(); 94 | INTERNET_PER_CONN_OPTION_LIST list = buildOptionList(interName, 1); 95 | INTERNET_PER_CONN_OPTION[] pOptions = (INTERNET_PER_CONN_OPTION[]) list.pOptions 96 | .toArray(list.dwOptionCount); 97 | // Set flags. 98 | pOptions[0].dwOption = WinInet.INTERNET_PER_CONN_FLAGS; 99 | pOptions[0].Value.dwValue = WinInet.PROXY_TYPE_DIRECT; 100 | pOptions[0].Value.setType(int.class); 101 | 102 | refreshOptions(list); 103 | } else if (OsUtil.isMac()) { 104 | String networkService = getRemoteInterface(); 105 | ExecUtil.httpGet("http://127.0.0.1:" + DownApplication.macToolPort + "/proxy/disabled" 106 | + "?ns=" + networkService); 107 | return networkService; 108 | } 109 | return null; 110 | } 111 | 112 | /** 113 | * 获取访问外网使用的网卡 114 | */ 115 | private static String getRemoteInterface() throws IOException { 116 | Map> interfacesInfo = getInterfacesInfo(); 117 | Socket socket = new Socket("www.baidu.com", 80); 118 | for (Entry> entry : interfacesInfo.entrySet()) { 119 | if (entry.getValue().contains(socket.getLocalAddress().getHostAddress())) { 120 | String remoteInterface = entry.getKey(); 121 | if (OsUtil.isWindows()) { 122 | try { 123 | String result = ExecUtil.exec("rasdial"); 124 | if (result != null && Arrays.stream(result.split("\r\n")).anyMatch(line -> line.equals(remoteInterface))) { 125 | return remoteInterface; 126 | } 127 | } catch (IOException e) { 128 | return null; 129 | } 130 | } else if (OsUtil.isMac()) { 131 | String result = ExecUtil.exec("networksetup", "-listnetworkserviceorder"); 132 | Pattern pattern = Pattern.compile("\\(Hardware\\sPort:\\s(.*),\\sDevice:\\s(.*)\\)"); 133 | Matcher matcher = pattern.matcher(result); 134 | while (matcher.find()) { 135 | if (matcher.group(2).equalsIgnoreCase(remoteInterface)) { 136 | return matcher.group(1); 137 | } 138 | } 139 | } 140 | } 141 | } 142 | return null; 143 | } 144 | 145 | /** 146 | * 获取本机所有网卡 147 | */ 148 | public static Map> getInterfacesInfo() throws SocketException { 149 | Map> interfacesInfo = new HashMap<>(); 150 | Enumeration interfaces = NetworkInterface.getNetworkInterfaces(); 151 | while (interfaces.hasMoreElements()) { 152 | NetworkInterface networkInterface = interfaces.nextElement(); 153 | Enumeration addresses = networkInterface.getInetAddresses(); 154 | while (addresses.hasMoreElements()) { 155 | InetAddress nextElement = addresses.nextElement(); 156 | String name = networkInterface.getDisplayName(); 157 | List ipList = interfacesInfo.get(name); 158 | if (ipList == null) { 159 | ipList = new ArrayList<>(); 160 | interfacesInfo.put(name, ipList); 161 | } 162 | ipList.add(nextElement.getHostAddress()); 163 | } 164 | } 165 | return interfacesInfo; 166 | } 167 | 168 | public static void main(String[] args) throws Exception { 169 | System.out.println(getRemoteInterface()); 170 | } 171 | 172 | private static INTERNET_PER_CONN_OPTION_LIST buildOptionList(String connectionName, int size) { 173 | INTERNET_PER_CONN_OPTION_LIST list = new INTERNET_PER_CONN_OPTION_LIST(); 174 | // Fill the list structure. 175 | list.dwSize = list.size(); 176 | 177 | // NULL == LAN, otherwise connectoid name. 178 | list.pszConnection = connectionName; 179 | 180 | // Set three options. 181 | list.dwOptionCount = size; 182 | list.pOptions = new ByReference(); 183 | 184 | // Ensure that the memory was allocated. 185 | if (null == list.pOptions) { 186 | // Return FALSE if the memory wasn't allocated. 187 | return null; 188 | } 189 | return list; 190 | } 191 | 192 | private static boolean refreshOptions(INTERNET_PER_CONN_OPTION_LIST list) { 193 | if (!WinInet.INSTANCE 194 | .InternetSetOption(Pointer.NULL, WinInet.INTERNET_OPTION_PER_CONNECTION_OPTION, list, 195 | list.size())) { 196 | return false; 197 | } 198 | 199 | if (!WinInet.INSTANCE 200 | .InternetSetOption(Pointer.NULL, WinInet.INTERNET_OPTION_PROXY_SETTINGS_CHANGED, 201 | Pointer.NULL, 0)) { 202 | return false; 203 | } 204 | 205 | // Refresh Internet Options 206 | if (!WinInet.INSTANCE 207 | .InternetSetOption(Pointer.NULL, WinInet.INTERNET_OPTION_REFRESH, Pointer.NULL, 0)) { 208 | return false; 209 | } 210 | return true; 211 | } 212 | } 213 | 214 | -------------------------------------------------------------------------------- /main/src/main/java/org/pdown/gui/http/EmbedHttpServer.java: -------------------------------------------------------------------------------- 1 | package org.pdown.gui.http; 2 | 3 | import io.netty.bootstrap.ServerBootstrap; 4 | import io.netty.channel.Channel; 5 | import io.netty.channel.ChannelFuture; 6 | import io.netty.channel.ChannelHandlerContext; 7 | import io.netty.channel.ChannelInitializer; 8 | import io.netty.channel.SimpleChannelInboundHandler; 9 | import io.netty.channel.nio.NioEventLoopGroup; 10 | import io.netty.channel.socket.nio.NioServerSocketChannel; 11 | import io.netty.handler.codec.http.FullHttpRequest; 12 | import io.netty.handler.codec.http.FullHttpResponse; 13 | import io.netty.handler.codec.http.HttpHeaderNames; 14 | import io.netty.handler.codec.http.HttpHeaderValues; 15 | import io.netty.handler.codec.http.HttpObjectAggregator; 16 | import io.netty.handler.codec.http.HttpResponseStatus; 17 | import io.netty.handler.codec.http.HttpServerCodec; 18 | import io.netty.util.concurrent.GenericFutureListener; 19 | import java.lang.reflect.Method; 20 | import java.net.URI; 21 | import java.util.ArrayList; 22 | import java.util.HashMap; 23 | import java.util.List; 24 | import java.util.Map; 25 | import org.pdown.gui.http.controller.DefaultController; 26 | import org.pdown.gui.http.controller.NativeController; 27 | import org.pdown.gui.http.util.HttpHandlerUtil; 28 | import org.slf4j.Logger; 29 | import org.slf4j.LoggerFactory; 30 | import org.springframework.web.bind.annotation.RequestMapping; 31 | 32 | public class EmbedHttpServer { 33 | 34 | private static final Logger LOGGER = LoggerFactory.getLogger(EmbedHttpServer.class); 35 | 36 | private int port; 37 | private DefaultController defaultController; 38 | private List controllerList; 39 | 40 | public EmbedHttpServer(int port) { 41 | this.port = port; 42 | this.defaultController = new DefaultController(); 43 | this.controllerList = new ArrayList<>(); 44 | } 45 | 46 | //根据请求uri找到对应的处理类方法执行 47 | public FullHttpResponse invoke(String uri, Channel channel, FullHttpRequest request) 48 | throws Exception { 49 | if (controllerList != null) { 50 | for (Object obj : controllerList) { 51 | Class clazz = obj.getClass(); 52 | RequestMapping mapping = clazz.getAnnotation(RequestMapping.class); 53 | if (mapping != null) { 54 | String mappingUri = fixUri(mapping.value()[0]); 55 | for (Method actionMethod : clazz.getMethods()) { 56 | RequestMapping subMapping = actionMethod.getAnnotation(RequestMapping.class); 57 | if (subMapping != null) { 58 | String subMappingUri = fixUri(subMapping.value()[0]); 59 | if (uri.equalsIgnoreCase(mappingUri + subMappingUri)) { 60 | return (FullHttpResponse) actionMethod.invoke(obj, channel, request); 61 | } 62 | } 63 | } 64 | } 65 | } 66 | } 67 | return defaultController.handle(channel, request); 68 | } 69 | 70 | private String fixUri(String uri) { 71 | StringBuilder builder = new StringBuilder(uri); 72 | if (builder.indexOf("/") != 0) { 73 | builder.insert(0, "/"); 74 | } 75 | if (builder.lastIndexOf("/") == builder.length() - 1) { 76 | builder.delete(builder.length() - 1, builder.length()); 77 | } 78 | return builder.toString(); 79 | } 80 | 81 | public void start() { 82 | start(null); 83 | } 84 | 85 | public void start(GenericFutureListener startedListener) { 86 | NioEventLoopGroup bossGroup = new NioEventLoopGroup(2); 87 | NioEventLoopGroup workGroup = new NioEventLoopGroup(2); 88 | try { 89 | ServerBootstrap bootstrap = new ServerBootstrap().group(bossGroup, workGroup) 90 | .channel(NioServerSocketChannel.class) 91 | .childHandler(new ChannelInitializer() { 92 | @Override 93 | protected void initChannel(Channel ch) throws Exception { 94 | ch.pipeline().addLast("httpCodec", new HttpServerCodec()); 95 | ch.pipeline().addLast(new HttpObjectAggregator(4194304)); 96 | ch.pipeline() 97 | .addLast("serverHandle", new SimpleChannelInboundHandler() { 98 | 99 | @Override 100 | protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) 101 | throws Exception { 102 | URI uri = new URI(request.uri()); 103 | FullHttpResponse httpResponse = invoke(uri.getPath(), ctx.channel(), request); 104 | if (httpResponse != null) { 105 | httpResponse.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE); 106 | httpResponse.headers().set(HttpHeaderNames.CONTENT_LENGTH, httpResponse.content().readableBytes()); 107 | ch.writeAndFlush(httpResponse); 108 | } 109 | } 110 | 111 | @Override 112 | public void channelUnregistered(ChannelHandlerContext ctx) { 113 | ctx.channel().close(); 114 | } 115 | 116 | @Override 117 | public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { 118 | LOGGER.error("native request error", cause.getCause() == null ? cause : cause.getCause()); 119 | Map data = new HashMap<>(); 120 | data.put("error", cause.getCause().toString()); 121 | FullHttpResponse httpResponse = HttpHandlerUtil.buildJson(data); 122 | httpResponse.setStatus(HttpResponseStatus.INTERNAL_SERVER_ERROR); 123 | ctx.channel().writeAndFlush(httpResponse); 124 | } 125 | }); 126 | } 127 | }); 128 | ChannelFuture f = bootstrap.bind("127.0.0.1", port).sync(); 129 | if (startedListener != null) { 130 | f.addListener(startedListener); 131 | } 132 | f.channel().closeFuture().sync(); 133 | } catch (Exception e) { 134 | e.printStackTrace(); 135 | } finally { 136 | bossGroup.shutdownGracefully(); 137 | workGroup.shutdownGracefully(); 138 | } 139 | } 140 | 141 | public EmbedHttpServer addController(Object obj) { 142 | this.controllerList.add(obj); 143 | return this; 144 | } 145 | 146 | public static void main(String[] args) { 147 | 148 | new EmbedHttpServer(8998) 149 | .addController(new NativeController()) 150 | .start(); 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /main/src/main/java/org/pdown/gui/http/controller/ApiController.java: -------------------------------------------------------------------------------- 1 | package org.pdown.gui.http.controller; 2 | 3 | import io.netty.channel.Channel; 4 | import io.netty.handler.codec.http.DefaultFullHttpResponse; 5 | import io.netty.handler.codec.http.FullHttpRequest; 6 | import io.netty.handler.codec.http.FullHttpResponse; 7 | import io.netty.handler.codec.http.HttpResponseStatus; 8 | import io.netty.handler.codec.http.HttpVersion; 9 | import java.io.IOException; 10 | import java.util.HashMap; 11 | import java.util.Map; 12 | import javafx.application.Platform; 13 | import org.pdown.gui.DownApplication; 14 | import org.springframework.web.bind.annotation.RequestMapping; 15 | 16 | @RequestMapping("api") 17 | public class ApiController { 18 | 19 | @RequestMapping("createTask") 20 | public FullHttpResponse createTask(Channel channel, FullHttpRequest request) throws Exception { 21 | Map map = getQueryParams(request); 22 | DownApplication.INSTANCE.loadUri("/#/tasks?request=" + map.get("request") + 23 | "&response=" + map.get("response") + 24 | "&config=" + map.get("config") + 25 | "&data=" + map.get("data"), false); 26 | FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK); 27 | response.headers().set("Access-Control-Allow-Origin", "*"); 28 | return response; 29 | } 30 | 31 | private Map getQueryParams(FullHttpRequest request) throws IOException { 32 | Map map = new HashMap<>(); 33 | String uri = request.uri(); 34 | int index = uri.lastIndexOf("?"); 35 | if (index != -1 && index != uri.length() - 1) { 36 | String[] params = uri.substring(index + 1).split("&"); 37 | for (String param : params) { 38 | String[] kv = param.split("="); 39 | if (kv.length == 2) { 40 | map.put(kv[0], kv[1]); 41 | } 42 | } 43 | } 44 | return map; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /main/src/main/java/org/pdown/gui/http/controller/DefaultController.java: -------------------------------------------------------------------------------- 1 | package org.pdown.gui.http.controller; 2 | 3 | import io.netty.channel.Channel; 4 | import io.netty.handler.codec.http.DefaultFullHttpResponse; 5 | import io.netty.handler.codec.http.FullHttpRequest; 6 | import io.netty.handler.codec.http.FullHttpResponse; 7 | import io.netty.handler.codec.http.HttpHeaderNames; 8 | import io.netty.handler.codec.http.HttpHeaderValues; 9 | import io.netty.handler.codec.http.HttpResponseStatus; 10 | import io.netty.handler.codec.http.HttpVersion; 11 | import io.netty.util.AsciiString; 12 | import java.io.InputStream; 13 | import java.net.URI; 14 | import org.springframework.web.bind.annotation.RequestMapping; 15 | 16 | @RequestMapping("/") 17 | public class DefaultController { 18 | 19 | public FullHttpResponse handle(Channel channel, FullHttpRequest request) throws Exception { 20 | URI uri = new URI(request.uri()); 21 | String path = uri.getPath(); 22 | if ("/".equals(path)) { 23 | path = "/index.html"; 24 | } 25 | InputStream inputStream = Thread.currentThread().getContextClassLoader() 26 | .getResourceAsStream("http" + path); 27 | FullHttpResponse httpResponse; 28 | if (inputStream != null) { 29 | String mime = path.substring(path.lastIndexOf(".") + 1); 30 | httpResponse = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK); 31 | buildHead(httpResponse, mime); 32 | try { 33 | byte[] bts = new byte[8192]; 34 | int len; 35 | while ((len = inputStream.read(bts)) != -1) { 36 | httpResponse.content().writeBytes(bts, 0, len); 37 | } 38 | } finally { 39 | inputStream.close(); 40 | } 41 | } else { 42 | httpResponse = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, 43 | HttpResponseStatus.NOT_FOUND); 44 | buildHead(httpResponse, null); 45 | } 46 | return httpResponse; 47 | } 48 | 49 | private void buildHead(FullHttpResponse httpResponse, String mime) { 50 | if (mime != null) { 51 | AsciiString contentType; 52 | switch (mime) { 53 | case "txt": 54 | case "text": 55 | contentType = AsciiString.cached("text/plain; charset=utf-8"); 56 | break; 57 | case "html": 58 | case "htm": 59 | contentType = AsciiString.cached("text/html; charset=utf-8"); 60 | break; 61 | case "css": 62 | contentType = AsciiString.cached("text/css; charset=utf-8"); 63 | break; 64 | case "js": 65 | contentType = AsciiString.cached("application/javascript; charset=utf-8"); 66 | break; 67 | case "png": 68 | contentType = AsciiString.cached("image/png"); 69 | break; 70 | case "jpg": 71 | case "jpeg": 72 | contentType = AsciiString.cached("image/jpeg"); 73 | break; 74 | case "bmp": 75 | contentType = AsciiString.cached("application/x-bmp"); 76 | break; 77 | case "gif": 78 | contentType = AsciiString.cached("image/gif"); 79 | break; 80 | case "ico": 81 | contentType = AsciiString.cached("image/x-icon"); 82 | break; 83 | case "ttf": 84 | contentType = AsciiString.cached("font/ttf; charset=utf-8"); 85 | break; 86 | case "woff": 87 | contentType = AsciiString.cached("application/font-woff; charset=utf-8"); 88 | break; 89 | default: 90 | contentType = HttpHeaderValues.APPLICATION_OCTET_STREAM; 91 | } 92 | httpResponse.headers().set(HttpHeaderNames.CONTENT_TYPE, contentType); 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /main/src/main/java/org/pdown/gui/http/controller/PacController.java: -------------------------------------------------------------------------------- 1 | package org.pdown.gui.http.controller; 2 | 3 | import io.netty.channel.Channel; 4 | import io.netty.handler.codec.http.FullHttpRequest; 5 | import io.netty.handler.codec.http.FullHttpResponse; 6 | import io.netty.handler.codec.http.HttpHeaderNames; 7 | import io.netty.handler.codec.http.HttpHeaderValues; 8 | import java.util.Set; 9 | import org.pdown.gui.DownApplication; 10 | import org.pdown.gui.extension.ExtensionContent; 11 | import org.pdown.gui.http.util.HttpHandlerUtil; 12 | import org.springframework.web.bind.annotation.RequestMapping; 13 | 14 | @RequestMapping("pac") 15 | public class PacController { 16 | 17 | private static final String PAC_TEMPLATE = "function FindProxyForURL(url, host) {" 18 | + " if (isInNet(host, '127.0.0.1', '255.0.0.255')" 19 | + " || isInNet(dnsResolve(host), '127.0.0.1', '255.0.0.255')) {" 20 | + " return 'DIRECT';" 21 | + " }" 22 | + " var domains = [{domains}];" 23 | + " var match = false;" 24 | + " for (var i = 0; i < domains.length; i++) {" 25 | + " if (shExpMatch(host, domains[i])) {" 26 | + " match = true;" 27 | + " break;" 28 | + " }" 29 | + " }" 30 | + " return match ? 'PROXY 127.0.0.1:{port};DIRECT' : 'DIRECT';" 31 | + "}"; 32 | 33 | @RequestMapping("pdown.pac") 34 | public FullHttpResponse build(Channel channel, FullHttpRequest request) throws Exception { 35 | Set domains = ExtensionContent.getProxyWildCards(); 36 | String pacContent = PAC_TEMPLATE.replace("{port}", DownApplication.INSTANCE.PROXY_PORT + ""); 37 | if (domains != null && domains.size() > 0) { 38 | StringBuilder domainsBuilder = new StringBuilder(); 39 | for (String domain : domains) { 40 | if (domainsBuilder.length() != 0) { 41 | domainsBuilder.append(","); 42 | } 43 | domainsBuilder.append("'" + domain + "'"); 44 | } 45 | pacContent = pacContent.replace("{domains}", domainsBuilder.toString()); 46 | } else { 47 | pacContent = pacContent.replace("{domains}", ""); 48 | } 49 | FullHttpResponse httpResponse = HttpHandlerUtil.buildContent(pacContent, "application/x-ns-proxy-autoconfig"); 50 | httpResponse.headers().set(HttpHeaderNames.CACHE_CONTROL, HttpHeaderValues.NO_CACHE); 51 | httpResponse.headers().set(HttpHeaderNames.PRAGMA, HttpHeaderValues.NO_CACHE); 52 | httpResponse.headers().set(HttpHeaderNames.EXPIRES, 0); 53 | return httpResponse; 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /main/src/main/java/org/pdown/gui/http/util/HttpHandlerUtil.java: -------------------------------------------------------------------------------- 1 | package org.pdown.gui.http.util; 2 | 3 | import com.fasterxml.jackson.annotation.JsonInclude.Include; 4 | import com.fasterxml.jackson.core.JsonProcessingException; 5 | import com.fasterxml.jackson.databind.ObjectMapper; 6 | import io.netty.channel.Channel; 7 | import io.netty.handler.codec.http.DefaultFullHttpResponse; 8 | import io.netty.handler.codec.http.FullHttpResponse; 9 | import io.netty.handler.codec.http.HttpHeaderNames; 10 | import io.netty.handler.codec.http.HttpResponseStatus; 11 | import io.netty.handler.codec.http.HttpVersion; 12 | import io.netty.util.AsciiString; 13 | import java.nio.charset.Charset; 14 | 15 | public class HttpHandlerUtil { 16 | 17 | public static void writeJson(Channel channel, Object obj) { 18 | channel.writeAndFlush(buildJson(obj)); 19 | } 20 | 21 | public static FullHttpResponse buildJson(Object obj) { 22 | return buildJson(obj, null); 23 | } 24 | 25 | public static FullHttpResponse buildJson(Object obj, Include include) { 26 | FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK); 27 | response.headers().set(HttpHeaderNames.CONTENT_TYPE, AsciiString.cached("application/json; charset=utf-8")); 28 | if (obj != null) { 29 | try { 30 | ObjectMapper objectMapper = new ObjectMapper(); 31 | if (include != null) { 32 | objectMapper.setSerializationInclusion(include); 33 | } 34 | String content = objectMapper.writeValueAsString(obj); 35 | response.content().writeBytes(content.getBytes(Charset.forName("utf-8"))); 36 | } catch (JsonProcessingException e) { 37 | response.setStatus(HttpResponseStatus.SERVICE_UNAVAILABLE); 38 | } 39 | } 40 | response.headers().set(HttpHeaderNames.CONTENT_LENGTH, response.content().readableBytes()); 41 | return response; 42 | } 43 | 44 | public static FullHttpResponse buildContent(String content, String contentType) { 45 | FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK); 46 | response.headers().set(HttpHeaderNames.CONTENT_TYPE, AsciiString.cached(contentType)); 47 | if (content != null) { 48 | response.content().writeBytes(content.getBytes(Charset.forName("utf-8"))); 49 | } 50 | response.headers().set(HttpHeaderNames.CONTENT_LENGTH, response.content().readableBytes()); 51 | return response; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /main/src/main/java/org/pdown/gui/rest/HttpDownAppCallback.java: -------------------------------------------------------------------------------- 1 | package org.pdown.gui.rest; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import java.util.HashMap; 5 | import java.util.List; 6 | import java.util.Map; 7 | import org.pdown.core.boot.HttpDownBootstrap; 8 | import org.pdown.core.entity.HttpResponseInfo; 9 | import org.pdown.core.util.HttpDownUtil; 10 | import org.pdown.gui.extension.ExtensionContent; 11 | import org.pdown.gui.extension.ExtensionInfo; 12 | import org.pdown.gui.extension.HookScript; 13 | import org.pdown.gui.extension.HookScript.Event; 14 | import org.pdown.gui.extension.util.ExtensionUtil; 15 | import org.pdown.rest.content.HttpDownContent; 16 | import org.pdown.rest.controller.HttpDownRestCallback; 17 | import org.pdown.rest.entity.DownInfo; 18 | import org.pdown.rest.form.HttpRequestForm; 19 | import org.pdown.rest.form.TaskForm; 20 | import org.slf4j.Logger; 21 | import org.slf4j.LoggerFactory; 22 | import org.springframework.beans.BeanUtils; 23 | import org.springframework.util.StringUtils; 24 | 25 | public class HttpDownAppCallback extends HttpDownRestCallback { 26 | 27 | private static final Logger LOGGER = LoggerFactory.getLogger(HttpDownAppCallback.class); 28 | 29 | @Override 30 | public void onStart(HttpDownBootstrap httpDownBootstrap) { 31 | super.onStart(httpDownBootstrap); 32 | commonHook(httpDownBootstrap, HookScript.EVENT_START, false); 33 | } 34 | 35 | @Override 36 | public void onResume(HttpDownBootstrap httpDownBootstrap) { 37 | super.onResume(httpDownBootstrap); 38 | commonHook(httpDownBootstrap, HookScript.EVENT_RESUME, false); 39 | } 40 | 41 | @Override 42 | public void onPause(HttpDownBootstrap httpDownBootstrap) { 43 | super.onPause(httpDownBootstrap); 44 | commonHook(httpDownBootstrap, HookScript.EVENT_PAUSE, false); 45 | } 46 | 47 | @Override 48 | public void onError(HttpDownBootstrap httpDownBootstrap) { 49 | super.onError(httpDownBootstrap); 50 | commonHook(httpDownBootstrap, HookScript.EVENT_ERROR, true); 51 | } 52 | 53 | @Override 54 | public void onDone(HttpDownBootstrap httpDownBootstrap) { 55 | super.onDone(httpDownBootstrap); 56 | commonHook(httpDownBootstrap, HookScript.EVENT_DONE, true); 57 | } 58 | 59 | private void commonHook(HttpDownBootstrap httpDownBootstrap, String event, boolean async) { 60 | DownInfo downInfo = findDownInfo(httpDownBootstrap); 61 | Map taskInfo = buildTaskInfo(downInfo); 62 | if (taskInfo != null) { 63 | //遍历扩展模块是否有对应的处理 64 | List extensionInfos = ExtensionContent.get(); 65 | for (ExtensionInfo extensionInfo : extensionInfos) { 66 | if (extensionInfo.getMeta().isEnabled()) { 67 | if (extensionInfo.getHookScript() != null 68 | && !StringUtils.isEmpty(extensionInfo.getHookScript().getScript())) { 69 | Event e = extensionInfo.getHookScript().hasEvent(event, HttpDownUtil.getUrl(httpDownBootstrap.getRequest())); 70 | if (e != null) { 71 | try { 72 | //执行钩子函数 73 | Object result = ExtensionUtil.invoke(extensionInfo, e, taskInfo, async); 74 | if (result != null) { 75 | ObjectMapper objectMapper = new ObjectMapper(); 76 | String temp = objectMapper.writeValueAsString(result); 77 | TaskForm taskForm = objectMapper.readValue(temp, TaskForm.class); 78 | if (taskForm.getRequest() != null) { 79 | httpDownBootstrap.setRequest( 80 | HttpDownUtil.buildRequest(taskForm.getRequest().getMethod(), 81 | taskForm.getRequest().getUrl(), 82 | taskForm.getRequest().getHeads(), 83 | taskForm.getRequest().getBody()) 84 | ); 85 | } 86 | if (taskForm.getResponse() != null) { 87 | httpDownBootstrap.setResponse(taskForm.getResponse()); 88 | } 89 | if (taskForm.getData() != null) { 90 | downInfo.setData(taskForm.getData()); 91 | } 92 | HttpDownContent.getInstance().save(); 93 | } 94 | } catch (Exception ex) { 95 | LOGGER.error("An hook exception occurred while " + event + "()", ex); 96 | } 97 | } 98 | } 99 | } 100 | } 101 | } 102 | } 103 | 104 | private Map buildTaskInfo(DownInfo downInfo) { 105 | if (downInfo != null) { 106 | Map taskForm = new HashMap<>(); 107 | taskForm.put("id", downInfo.getId()); 108 | taskForm.put("data", clone(downInfo.getData(), new HashMap())); 109 | taskForm.put("request", HttpRequestForm.parse(downInfo.getBootstrap().getRequest())); 110 | taskForm.put("response", clone(downInfo.getBootstrap().getResponse(), new HttpResponseInfo())); 111 | return taskForm; 112 | } 113 | return null; 114 | } 115 | 116 | private Object clone(Object source, Object target) { 117 | if (source != null && target != null) { 118 | BeanUtils.copyProperties(source, target); 119 | return target; 120 | } 121 | return null; 122 | } 123 | 124 | } 125 | -------------------------------------------------------------------------------- /main/src/main/java/org/pdown/gui/update/CheckUpdate.java: -------------------------------------------------------------------------------- 1 | package org.pdown.gui.update; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import java.net.HttpURLConnection; 5 | import java.net.URL; 6 | import org.pdown.gui.util.ConfigUtil; 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | 10 | public class CheckUpdate { 11 | 12 | private static final Logger LOGGER = LoggerFactory.getLogger(CheckUpdate.class); 13 | 14 | public static VersionInfo doCheck() { 15 | String adminServer = ConfigUtil.getString("adminServer"); 16 | double currVersion = Double.parseDouble(ConfigUtil.getString("version")); 17 | try { 18 | URL url = new URL(adminServer + "checkUpdate"); 19 | HttpURLConnection connection = (HttpURLConnection) url.openConnection(); 20 | if (connection.getResponseCode() == 200) { 21 | ObjectMapper objectMapper = new ObjectMapper(); 22 | VersionInfo versionInfo = objectMapper.readValue(connection.getInputStream(), VersionInfo.class); 23 | if (versionInfo.getVersion() > currVersion) { 24 | return versionInfo; 25 | } 26 | } 27 | } catch (Exception e) { 28 | LOGGER.warn("Check update error", e); 29 | } 30 | return null; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /main/src/main/java/org/pdown/gui/update/VersionInfo.java: -------------------------------------------------------------------------------- 1 | package org.pdown.gui.update; 2 | 3 | public class VersionInfo { 4 | private double version; 5 | private String path; 6 | 7 | public double getVersion() { 8 | return version; 9 | } 10 | 11 | public VersionInfo setVersion(double version) { 12 | this.version = version; 13 | return this; 14 | } 15 | 16 | public String getPath() { 17 | return path; 18 | } 19 | 20 | public VersionInfo setPath(String path) { 21 | this.path = path; 22 | return this; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /main/src/main/java/org/pdown/gui/util/AppUtil.java: -------------------------------------------------------------------------------- 1 | package org.pdown.gui.util; 2 | 3 | import java.io.File; 4 | import java.io.FileOutputStream; 5 | import java.io.IOException; 6 | import java.io.InputStream; 7 | import java.net.Authenticator; 8 | import java.net.HttpURLConnection; 9 | import java.net.InetSocketAddress; 10 | import java.net.PasswordAuthentication; 11 | import java.net.Proxy; 12 | import java.net.Proxy.Type; 13 | import java.net.URL; 14 | import org.pdown.core.boot.HttpDownBootstrap; 15 | import org.pdown.core.boot.URLHttpDownBootstrapBuilder; 16 | import org.pdown.core.dispatch.HttpDownCallback; 17 | import org.pdown.core.entity.HttpDownConfigInfo; 18 | import org.pdown.core.entity.HttpResponseInfo; 19 | import org.pdown.core.proxy.ProxyConfig; 20 | import org.pdown.core.proxy.ProxyType; 21 | import org.pdown.core.util.FileUtil; 22 | import org.pdown.core.util.OsUtil; 23 | import org.pdown.gui.DownApplication; 24 | import org.pdown.gui.content.PDownConfigContent; 25 | import org.pdown.gui.extension.mitm.server.PDownProxyServer; 26 | import org.pdown.gui.extension.mitm.util.ExtensionCertUtil; 27 | import org.pdown.gui.extension.mitm.util.ExtensionProxyUtil; 28 | import org.pdown.rest.util.PathUtil; 29 | import org.springframework.util.StringUtils; 30 | 31 | public class AppUtil { 32 | 33 | public static final String SUBJECT = "ProxyeeDown CA"; 34 | public static final String SSL_PATH = PathUtil.ROOT_PATH + File.separator + "ssl" + File.separator; 35 | public static final String CERT_PATH = SSL_PATH + "ca.crt"; 36 | public static final String PRIVATE_PATH = SSL_PATH + ".ca_pri.der"; 37 | 38 | /** 39 | * 证书和私钥文件都存在并且检测到系统安装了这个证书 40 | */ 41 | public static boolean checkIsInstalledCert() throws Exception { 42 | return FileUtil.exists(CERT_PATH) 43 | && FileUtil.exists(PRIVATE_PATH) 44 | && ExtensionCertUtil.isInstalledCert(new File(CERT_PATH)); 45 | } 46 | 47 | /** 48 | * 刷新系统PAC代理 49 | */ 50 | public static void refreshPAC() throws IOException { 51 | if (PDownConfigContent.getInstance().get().getProxyMode() == 1) { 52 | ExtensionProxyUtil.enabledPACProxy("http://127.0.0.1:" + DownApplication.INSTANCE.API_PORT + "/pac/pdown.pac?t=" + System.currentTimeMillis()); 53 | } 54 | } 55 | 56 | /** 57 | * 运行代理服务器 58 | */ 59 | public static void startProxyServer() throws IOException { 60 | DownApplication.INSTANCE.PROXY_PORT = OsUtil.getFreePort(9999); 61 | PDownProxyServer.start(DownApplication.INSTANCE.PROXY_PORT); 62 | } 63 | 64 | /** 65 | * 下载http资源 66 | */ 67 | public static void download(String url, String path) throws IOException { 68 | URL u = new URL(url); 69 | HttpURLConnection connection; 70 | ProxyConfig proxyConfig = PDownConfigContent.getInstance().get().getProxyConfig(); 71 | if (proxyConfig != null) { 72 | Type type; 73 | if (proxyConfig.getProxyType() == ProxyType.HTTP) { 74 | type = Type.HTTP; 75 | } else { 76 | type = Type.SOCKS; 77 | } 78 | if (!StringUtils.isEmpty(proxyConfig.getUser())) { 79 | Authenticator authenticator = new Authenticator() { 80 | public PasswordAuthentication getPasswordAuthentication() { 81 | return new PasswordAuthentication(proxyConfig.getUser(), 82 | proxyConfig.getPwd() == null ? null : proxyConfig.getPwd().toCharArray()); 83 | } 84 | }; 85 | Authenticator.setDefault(authenticator); 86 | } 87 | Proxy proxy = new Proxy(type, new InetSocketAddress(proxyConfig.getHost(), proxyConfig.getPort())); 88 | connection = (HttpURLConnection) u.openConnection(proxy); 89 | } else { 90 | connection = (HttpURLConnection) u.openConnection(); 91 | } 92 | connection.setConnectTimeout(30000); 93 | connection.setReadTimeout(0); 94 | File file = new File(path); 95 | if (!file.exists() || file.isDirectory()) { 96 | FileUtil.createFileSmart(file.getPath()); 97 | } 98 | try ( 99 | InputStream input = connection.getInputStream(); 100 | FileOutputStream output = new FileOutputStream(file) 101 | ) { 102 | byte[] bts = new byte[8192]; 103 | int len; 104 | while ((len = input.read(bts)) != -1) { 105 | output.write(bts, 0, len); 106 | } 107 | } 108 | } 109 | 110 | /** 111 | * 使用pdown-core多连接下载http资源 112 | */ 113 | public static HttpDownBootstrap fastDownload(String url, File file, HttpDownCallback callback) throws IOException { 114 | HttpDownBootstrap httpDownBootstrap = new URLHttpDownBootstrapBuilder(url, null, null) 115 | .callback(callback) 116 | .downConfig(new HttpDownConfigInfo().setFilePath(file.getParent()).setConnections(64)) 117 | .response(new HttpResponseInfo().setFileName(file.getName())) 118 | .proxyConfig(PDownConfigContent.getInstance().get().getProxyConfig()) 119 | .build(); 120 | httpDownBootstrap.start(); 121 | return httpDownBootstrap; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /main/src/main/java/org/pdown/gui/util/ConfigUtil.java: -------------------------------------------------------------------------------- 1 | package org.pdown.gui.util; 2 | 3 | import java.util.Map; 4 | import java.util.Map.Entry; 5 | import org.yaml.snakeyaml.Yaml; 6 | 7 | public class ConfigUtil { 8 | 9 | private static Map map = null; 10 | 11 | static { 12 | Yaml yaml = new Yaml(); 13 | map = yaml.load(Thread.currentThread().getContextClassLoader().getResourceAsStream("application.yml")); 14 | String active = getString("spring.profiles.active"); 15 | merge(map, yaml.load(Thread.currentThread().getContextClassLoader().getResourceAsStream("application-" + active + ".yml"))); 16 | } 17 | 18 | public static String getString(String key) { 19 | return getString(map, key); 20 | } 21 | 22 | public static boolean getBoolean(String key) { 23 | return getBoolean(map, key); 24 | } 25 | 26 | public static int getInt(String key) { 27 | return getInt(map, key); 28 | } 29 | 30 | static Object get(Map map, String key) { 31 | String[] keyArray = key.split("\\."); 32 | if (keyArray.length == 1) { 33 | return map.get(key); 34 | } else { 35 | for (int i = 0; i < keyArray.length - 1; i++) { 36 | map = (Map) get(map, keyArray[i]); 37 | } 38 | return map.get(keyArray[keyArray.length - 1]); 39 | } 40 | } 41 | 42 | private static void merge(Map map1, Map map2) { 43 | for (Entry entry : map2.entrySet()) { 44 | if (map1.containsKey(entry.getKey())) { 45 | if (entry.getValue() instanceof Map && map2.get(entry.getKey()) instanceof Map) { 46 | merge(map1, (Map) map2.get(entry.getKey())); 47 | } else { 48 | map1.put(entry.getKey(), map2.get(entry.getKey())); 49 | } 50 | } else { 51 | map1.put(entry.getKey(), map2.get(entry.getKey())); 52 | } 53 | } 54 | } 55 | 56 | private static String getString(Map map, String key) { 57 | return String.valueOf(get(map, key)); 58 | } 59 | 60 | private static boolean getBoolean(Map map, String key) { 61 | return Boolean.valueOf(get(map, key).toString()); 62 | } 63 | 64 | private static int getInt(Map map, String key) { 65 | return Integer.valueOf(get(map, key).toString()); 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /main/src/main/java/org/pdown/gui/util/ExecUtil.java: -------------------------------------------------------------------------------- 1 | package org.pdown.gui.util; 2 | 3 | import java.io.BufferedReader; 4 | import java.io.IOException; 5 | import java.io.InputStream; 6 | import java.io.InputStreamReader; 7 | import java.net.HttpURLConnection; 8 | import java.net.MalformedURLException; 9 | import java.net.URL; 10 | import java.nio.charset.Charset; 11 | import org.pdown.core.util.OsUtil; 12 | 13 | public class ExecUtil { 14 | 15 | /** 16 | * 执行shell并返回标准输出文本内容 17 | */ 18 | public static String exec(String... shell) throws IOException { 19 | Process process = Runtime.getRuntime().exec(shell); 20 | StringBuilder sb = new StringBuilder(); 21 | Charset charset = OsUtil.isWindows() ? Charset.forName("GBK") : Charset.defaultCharset(); 22 | try ( 23 | BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream(), charset)) 24 | ) { 25 | String line; 26 | while ((line = reader.readLine()) != null) { 27 | sb.append(line + System.lineSeparator()); 28 | } 29 | } finally { 30 | process.destroy(); 31 | } 32 | return sb.toString(); 33 | } 34 | 35 | /** 36 | * 同步执行shell,阻塞当前线程 37 | */ 38 | public static void execBlock(String... shell) throws IOException { 39 | Process process = Runtime.getRuntime().exec(shell); 40 | try ( 41 | InputStream inputStream = process.getInputStream() 42 | ) { 43 | byte[] bytes = new byte[8192]; 44 | while ((inputStream.read(bytes)) != -1) { 45 | //Do nothing 46 | } 47 | } finally { 48 | process.destroy(); 49 | } 50 | } 51 | 52 | /** 53 | * 以管理员权限,同步执行shell,阻塞当前线程 54 | */ 55 | public static void execBlockWithAdmin(String shell) throws IOException { 56 | //osascript -e "do shell script \"shell\" with administrator privileges" 57 | Process process = Runtime.getRuntime().exec(new String[]{ 58 | "osascript", 59 | "-e", 60 | "do shell script \"" + 61 | shell + 62 | "\"" + 63 | "with administrator privileges" 64 | }); 65 | try ( 66 | InputStream inputStream = process.getInputStream() 67 | ) { 68 | byte[] bytes = new byte[8192]; 69 | while ((inputStream.read(bytes)) != -1) { 70 | //Do nothing 71 | } 72 | } finally { 73 | process.destroy(); 74 | } 75 | } 76 | 77 | public static void httpGet(String url) throws IOException { 78 | URL u = new URL(url); 79 | HttpURLConnection connection = (HttpURLConnection) u.openConnection(); 80 | if (connection.getResponseCode() != 200) { 81 | throw new RuntimeException("http get error:" + url); 82 | } 83 | } 84 | 85 | 86 | public static void main(String[] args) throws Exception { 87 | httpGet("http://www.baidu.com"); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /main/src/main/java/org/pdown/gui/util/I18nUtil.java: -------------------------------------------------------------------------------- 1 | package org.pdown.gui.util; 2 | 3 | import java.io.File; 4 | import java.io.FileInputStream; 5 | import java.io.IOException; 6 | import java.net.JarURLConnection; 7 | import java.net.URL; 8 | import java.net.URLConnection; 9 | import java.text.MessageFormat; 10 | import java.util.Enumeration; 11 | import java.util.HashMap; 12 | import java.util.Map; 13 | import java.util.jar.JarEntry; 14 | import java.util.jar.JarFile; 15 | import org.pdown.gui.content.PDownConfigContent; 16 | import org.yaml.snakeyaml.Yaml; 17 | 18 | public class I18nUtil { 19 | 20 | private static final String DEFAULT_LOCALE = "zh-CN"; 21 | private static Map> map; 22 | 23 | static { 24 | Yaml yaml = new Yaml(); 25 | map = new HashMap<>(); 26 | try { 27 | ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); 28 | URL url = classLoader.getResource("i18n"); 29 | URLConnection connection = url.openConnection(); 30 | if (connection instanceof JarURLConnection) { 31 | JarURLConnection jarURLConnection = (JarURLConnection) connection; 32 | JarFile jarFile = jarURLConnection.getJarFile(); 33 | Enumeration entries = jarFile.entries(); 34 | while (entries.hasMoreElements()) { 35 | JarEntry entry = entries.nextElement(); 36 | if (entry.getName().matches("^i18n/[^/]+\\.yml$")) { 37 | map.put(takeLocale(entry.getName()), yaml.load(jarFile.getInputStream(entry))); 38 | } 39 | } 40 | jarFile.close(); 41 | } else { 42 | File dir = new File(url.getPath()); 43 | for (File message : dir.listFiles()) { 44 | map.put(takeLocale(message.getName()), yaml.load(new FileInputStream(message))); 45 | } 46 | } 47 | } catch (IOException e) { 48 | } 49 | } 50 | 51 | private static String takeLocale(String name) { 52 | return name.substring(name.lastIndexOf("_") + 1, name.length() - 4); 53 | } 54 | 55 | public static String getMessage(String key, Object... args) { 56 | String locale = null; 57 | if (PDownConfigContent.getInstance().get() != null) { 58 | locale = PDownConfigContent.getInstance().get().getLocale(); 59 | } 60 | if (locale == null || !map.containsKey(locale)) { 61 | locale = DEFAULT_LOCALE; 62 | } 63 | if (map == null || map.size() == 0) { 64 | return key; 65 | } 66 | Map localeMap = map.get(locale); 67 | if (localeMap == null) { 68 | return key; 69 | } 70 | return MessageFormat.format(ConfigUtil.get(localeMap, key).toString(), args); 71 | } 72 | 73 | public static void main(String[] args) { 74 | PDownConfigContent.getInstance().load(); 75 | System.out.println(getMessage("gui.alert.startError", "test")); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /main/src/main/resources/application-dev.yml: -------------------------------------------------------------------------------- 1 | logging: 2 | config: classpath:logback-dev.xml 3 | front: 4 | port: 8080 5 | api: 6 | port: 7478 7 | #adminServer: http://127.0.0.1:9494/ 8 | adminServer: http://api.pdown.org/ -------------------------------------------------------------------------------- /main/src/main/resources/application-prd.yml: -------------------------------------------------------------------------------- 1 | logging: 2 | config: classpath:logback-prd.xml 3 | front: 4 | port: -1 5 | api: 6 | port: 7478 7 | adminServer: http://api.pdown.org/ -------------------------------------------------------------------------------- /main/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | version: 3.41 2 | spring: 3 | profiles: 4 | active: @environment@ 5 | messages: 6 | basename: i18n/messages -------------------------------------------------------------------------------- /main/src/main/resources/banner.txt: -------------------------------------------------------------------------------- 1 | _ 2 | | | 3 | _ __ _ __ ___ __ __ _ _ ___ ___ ______ __| | ___ __ __ _ __ 4 | | '_ \ | '__| / _ \ \ \/ /| | | | / _ \ / _ \|______| / _` | / _ \ \ \ /\ / /| '_ \ 5 | | |_) || | | (_) | > < | |_| || __/| __/ | (_| || (_) | \ V V / | | | | 6 | | .__/ |_| \___/ /_/\_\ \__, | \___| \___| \__,_| \___/ \_/\_/ |_| |_| 7 | | | __/ | 8 | |_| |___/ -------------------------------------------------------------------------------- /main/src/main/resources/i18n/messages_en-US.yml: -------------------------------------------------------------------------------- 1 | gui: 2 | warning: Warning 3 | alert: 4 | startError: Startup error:{0} 5 | restPortBusy: Port 26339 is busy, please do not run multiple instances of the app. 6 | noFreePort: The system cannot allocate the TCP port 7 | menu: 8 | copy: Copy 9 | paste: Paste 10 | tray: 11 | show: Show 12 | set: Settings 13 | about: About 14 | support: Donate 15 | exit: Exit 16 | -------------------------------------------------------------------------------- /main/src/main/resources/i18n/messages_zh-CN.yml: -------------------------------------------------------------------------------- 1 | gui: 2 | warning: 警告 3 | alert: 4 | startError: 启动失败:{0} 5 | restPortBusy: 端口26339被占用,请勿重复启动软件 6 | noFreePort: 系统无法分配TCP端口 7 | menu: 8 | copy: 复制 9 | paste: 粘贴 10 | tray: 11 | show: 显示 12 | set: 设置 13 | about: 关于 14 | support: 打赏 15 | exit: 退出 -------------------------------------------------------------------------------- /main/src/main/resources/i18n/messages_zh-TW.yml: -------------------------------------------------------------------------------- 1 | gui: 2 | warning: 警告 3 | alert: 4 | startError: 啟動失敗:{0} 5 | restPortBusy: 連接埠 26339 被占用,請勿重複啟動軟體 6 | noFreePort: 系統無法指派 TCP 連接埠 7 | menu: 8 | copy: 複製 9 | paste: 貼上 10 | tray: 11 | show: 顯示 12 | set: 設定 13 | about: 關於 14 | support: 打賞 15 | exit: 結束 -------------------------------------------------------------------------------- /main/src/main/resources/linux/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/proxyee-down-org/proxyee-down/a8f1709f1603ef0c92e4818261842cb4eb804f79/main/src/main/resources/linux/logo.png -------------------------------------------------------------------------------- /main/src/main/resources/logback-dev.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /main/src/main/resources/logback-prd.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n 7 | 8 | 9 | 10 | 11 | ${ROOT_PATH}/log/pdown-error.%d{yyyy-MM-dd}.log 12 | 7 13 | 14 | 15 | %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{80} - %msg%n 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /main/src/main/resources/mac/dock_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/proxyee-down-org/proxyee-down/a8f1709f1603ef0c92e4818261842cb4eb804f79/main/src/main/resources/mac/dock_logo.png -------------------------------------------------------------------------------- /main/src/main/resources/mac/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/proxyee-down-org/proxyee-down/a8f1709f1603ef0c92e4818261842cb4eb804f79/main/src/main/resources/mac/logo.png -------------------------------------------------------------------------------- /main/src/main/resources/mac/mitm-tool.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/proxyee-down-org/proxyee-down/a8f1709f1603ef0c92e4818261842cb4eb804f79/main/src/main/resources/mac/mitm-tool.bin -------------------------------------------------------------------------------- /main/src/main/resources/windows/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/proxyee-down-org/proxyee-down/a8f1709f1603ef0c92e4818261842cb4eb804f79/main/src/main/resources/windows/logo.png -------------------------------------------------------------------------------- /main/src/main/resources/windows/logo_xp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/proxyee-down-org/proxyee-down/a8f1709f1603ef0c92e4818261842cb4eb804f79/main/src/main/resources/windows/logo_xp.png -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 4 | 4.0.0 5 | org.pdown.gui 6 | proxyee-down 7 | 3.0 8 | pom 9 | 10 | 11 | main 12 | runner 13 | 14 | proxyee-down 15 | https://github.com/proxyee-down-org/proxyee-down 16 | 17 | 18 | UTF-8 19 | 1.8 20 | 1.8 21 | 22 | 23 | -------------------------------------------------------------------------------- /runner/.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | !.mvn/wrapper/maven-wrapper.jar 3 | 4 | ### STS ### 5 | .apt_generated 6 | .classpath 7 | .factorypath 8 | .project 9 | .settings 10 | .springBeans 11 | 12 | ### IntelliJ IDEA ### 13 | .idea 14 | *.iws 15 | *.iml 16 | *.ipr -------------------------------------------------------------------------------- /runner/pom.xml: -------------------------------------------------------------------------------- 1 | 4 | 4.0.0 5 | 6 | org.pdown.gui 7 | proxyee-down 8 | 3.0 9 | 10 | 11 | org.pdown.gui 12 | runner 13 | jar 14 | 15 | 16 | 17 | oss 18 | https://oss.sonatype.org/content/repositories/snapshots 19 | 20 | true 21 | 22 | 23 | 24 | 25 | 26 | proxyee-down-runner 27 | 28 | 29 | maven-assembly-plugin 30 | 31 | false 32 | 33 | jar-with-dependencies 34 | 35 | 36 | 37 | org.pdown.gui.Runner 38 | 39 | 40 | 41 | 42 | 43 | make-assembly 44 | package 45 | 46 | assembly 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /runner/src/main/deploy/package/linux/Proxyee Down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/proxyee-down-org/proxyee-down/a8f1709f1603ef0c92e4818261842cb4eb804f79/runner/src/main/deploy/package/linux/Proxyee Down.png -------------------------------------------------------------------------------- /runner/src/main/deploy/package/macosx/Proxyee Down.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/proxyee-down-org/proxyee-down/a8f1709f1603ef0c92e4818261842cb4eb804f79/runner/src/main/deploy/package/macosx/Proxyee Down.icns -------------------------------------------------------------------------------- /runner/src/main/deploy/package/windows/Proxyee Down.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/proxyee-down-org/proxyee-down/a8f1709f1603ef0c92e4818261842cb4eb804f79/runner/src/main/deploy/package/windows/Proxyee Down.ico -------------------------------------------------------------------------------- /runner/src/main/java/org/pdown/gui/Runner.java: -------------------------------------------------------------------------------- 1 | package org.pdown.gui; 2 | 3 | import java.io.BufferedReader; 4 | import java.io.BufferedWriter; 5 | import java.io.File; 6 | import java.io.FileOutputStream; 7 | import java.io.IOException; 8 | import java.io.InputStreamReader; 9 | import java.io.OutputStreamWriter; 10 | import java.nio.file.Files; 11 | import java.nio.file.Paths; 12 | import java.util.ArrayList; 13 | import java.util.Arrays; 14 | import java.util.Collections; 15 | import java.util.List; 16 | import javax.swing.JOptionPane; 17 | 18 | public class Runner { 19 | 20 | private static final String JAVA_CMD_PATH = System.getProperty("java.home") + File.separator + "bin" + File.separator + "java"; 21 | private static final String MAIN_JAR_PATH = "main/proxyee-down-main.jar"; 22 | private static final String MAIN_JAR_BAK_PATH = MAIN_JAR_PATH + ".bak"; 23 | private static final String VM_OPTIONS_PATH = "main/run.cfg"; 24 | 25 | private static List VM_OPTIONS; 26 | 27 | public static void main(String[] args) throws IOException { 28 | VM_OPTIONS = parseVmOptions(); 29 | fork(); 30 | } 31 | 32 | private static List parseVmOptions() { 33 | File file = Paths.get(VM_OPTIONS_PATH).toFile(); 34 | if (!file.exists()) { 35 | try { 36 | file.createNewFile(); 37 | try ( 38 | BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file))) 39 | ) { 40 | writer.write("-Xms128m"); 41 | writer.newLine(); 42 | writer.write("-Xmx384m"); 43 | } catch (Exception e) { 44 | e.printStackTrace(); 45 | } 46 | } catch (Exception e) { 47 | e.printStackTrace(); 48 | } 49 | } 50 | try { 51 | return Files.readAllLines(Paths.get(VM_OPTIONS_PATH)); 52 | } catch (IOException e) { 53 | } 54 | return null; 55 | } 56 | 57 | private static void fork() { 58 | File bakFile = new File(MAIN_JAR_BAK_PATH); 59 | if (bakFile.exists()) { 60 | //更新后删除旧版本 61 | for (int i = 0; i < 30; i++) { 62 | if (new File(MAIN_JAR_PATH).delete()) { 63 | bakFile.renameTo(new File(MAIN_JAR_PATH)); 64 | break; 65 | } else { 66 | try { 67 | Thread.sleep(1000); 68 | } catch (InterruptedException e) { 69 | } 70 | } 71 | } 72 | } 73 | try { 74 | List execParams = new ArrayList<>(); 75 | execParams.add(JAVA_CMD_PATH); 76 | execParams.add("-jar"); 77 | if (VM_OPTIONS == null) { 78 | execParams.add("-Xms128m"); 79 | execParams.add("-Xmx384m"); 80 | } else { 81 | for (String option : VM_OPTIONS) { 82 | execParams.add(option); 83 | } 84 | } 85 | execParams.add(MAIN_JAR_PATH); 86 | String[] execArray = new String[execParams.size()]; 87 | execParams.toArray(execArray); 88 | Process process = Runtime.getRuntime().exec(execArray); 89 | BufferedReader br = new BufferedReader(new InputStreamReader(process.getInputStream())); 90 | String line; 91 | boolean isClose = false; 92 | while ((line = br.readLine()) != null) { 93 | System.out.println(line); 94 | if ("proxyee-down-exit".equals(line)) { 95 | isClose = true; 96 | break; 97 | } 98 | } 99 | if (isClose) { 100 | process.destroy(); 101 | fork(); 102 | } 103 | } catch (Throwable throwable) { 104 | alert(throwable.getMessage()); 105 | System.exit(1); 106 | } 107 | } 108 | 109 | private static void alert(String msg) { 110 | JOptionPane.showMessageDialog(null, msg, "title", JOptionPane.ERROR_MESSAGE); 111 | } 112 | } 113 | --------------------------------------------------------------------------------