├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README-en.md ├── README.md ├── assest.json ├── build.sh ├── conf ├── pproxy.conf ├── req_rewrite_8080.js └── users ├── file └── index.htm ├── fmt.sh ├── go.mod ├── go.sum ├── main.go ├── res ├── conf │ └── demo.conf ├── css │ ├── flat.css │ └── style.css ├── img │ ├── favicon.ico │ ├── folder.png │ ├── list-add.png │ ├── list.png │ ├── logo.png │ ├── text.png │ └── view.png ├── js │ ├── base64.js │ ├── default.js │ ├── jquery.js │ ├── session.js │ └── socket.io.js ├── private │ ├── client_cert.pem │ └── server_key.pem ├── sjs │ └── req_rewrite.js ├── tpl │ ├── about.html │ ├── config.html │ ├── config │ │ ├── req_demo.html │ │ └── req_form.html │ ├── error.html │ ├── file.html │ ├── file_edit.html │ ├── file_new.html │ ├── layout.html │ ├── login.html │ ├── network.html │ ├── replay.html │ ├── replay_direct.html │ └── useage.html └── version ├── script ├── create_dest_zip.sh ├── pproxy_control.sh └── windows_run.bat └── serve ├── assest.go ├── auth.go ├── broadcast.go ├── certs.go ├── config.go ├── init.go ├── kvStore.go ├── proxy.go ├── reqCtx.go ├── req_modifer.go ├── req_replay.go ├── req_rewrite.go ├── serve.go ├── sessions.go ├── util.go ├── web.go ├── web_file.go ├── wsClient.go └── wsServer.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | test/ 22 | 23 | *.exe 24 | *.test 25 | data/ 26 | dest/ 27 | hosts_* 28 | conf/* 29 | file/ 30 | .*.html 31 | .DS_Store 32 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2016-07-27 2 | 1. 升级依赖 3 | 4 | 2016-07-27 5 | 1. version 0.5.2 6 | 2. 修复文件上传页面错误 7 | 3. 更新lib otto ,boltdb 为最新版本 8 | 9 | 2016-03-25 10 | 1.版本号升级为0.5.1 11 | 2.改进request动态修改引擎并发异常问题 12 | 13 | 2014-12-27 14 | 1.版本号升级为0.4.7 15 | 2.添加adminPort配置项,以独立的端口提供给管理界面 16 | 17 | 2014-12-17 18 | 1.静态资源使用goassest方式而不使用之前的读取zip的方式 19 | 20 | 2014-11-08 21 | 1.重构代理处理逻辑,改进Upgrade代理协议 22 | 2.会话详情页面展现请求时间 23 | 3.upgrade结束的时候也记录一条response 以方便查看何时断开连接 24 | 25 | 2014-09-27 26 | 1.http session list support local filter 27 | 28 | 29 | 2014-09-14 30 | 1.downgrade the socket.io lib 31 | 32 | 2014-08-14 33 | 1.emit data with base64encode 34 | 2.fix some url has no schema 35 | 36 | 2014-08-10 37 | 1.websocket proxy support 38 | 39 | 2014-08-06 40 | 1.update socket.io 41 | 42 | 2014-07-19 43 | 1.修复监听端口为80时不能查看会话列表的问题 44 | 2.完善帮助说明 45 | 46 | 2014-07-15 47 | 1.get和post参数支持重写 48 | 2.重写请求出现错误自己返回502错误 49 | 50 | 2014-07-12 51 | 1.认证机制升级,新认证机制:一个ip第一次访问的时候会要求登录,若没有输入登录信息也跳过。 52 | 2.管理员用户(登录后)在session filter 输入user:any 可以查看到所有的会话信息 53 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 du 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README-en.md: -------------------------------------------------------------------------------- 1 | pproxy 0.5.2 2 | ====== 3 | HTTP protocol analysis tool. 4 | 5 | 6 | 7 | features: 8 |
  9 | 1.url redirect
 10 |    redirect *http://www.baidu.com/s?wd=pproxy* to  *http://m.baidu.com/s?wd=pproxy*
 11 |    redirect  *ws://www.test.com/a* to  *ws://www.example.com/b*
 12 |    
 13 | 2.form dynamic modification  
 14 |    get、post and header all can modify  
 15 |    
 16 | 3.hosts
 17 |   www.baidu.com to 127.0.0.1  
 18 |   or www.baidu.com:81 to 192.168.1.2:8080 ,and only  takes effect on port 81  
 19 |   
 20 | 4.view request and response detail
 21 |    form params,header and all response and easy to share
 22 |    
 23 | 5.auth sup
 24 |    http Basic or only try basic auth at first request
 25 |    
 26 | 6.replay
 27 |    can modify the get、post、header params and replay the request
 28 | 
 29 | 7.parent proxy
 30 |   
 31 | 
32 | 33 | use javascript code as config to modify the request params: 34 | ``` 35 | if(req.host=="www.baidu.com"){ 36 | req.host="www.163.com" 37 | req.host_addr="127.0.0.0:81" // send req to 127.0.0.1:81 38 | } 39 | ``` 40 | or: 41 | ``` 42 | if(req.host.indexOf("baidu.com")>-1){ 43 | req.host_addr="127.0.0.0:81" 44 | } 45 | ``` 46 | 47 | request params dump: 48 | ``` 49 | #url : http://www.example.com/album/list?cid=126 50 | #request has these attrs: 51 | schema : http 52 | host : www.example.com 53 | port : 80 54 | path : /album/list 55 | get: {cid:[123]} 56 | post: {} 57 | username : 58 | password : 59 | method: GET 60 | form_get : {add:function(k,v){},set:function(k,v){},get:function(k){},len:function(){}} 61 | form_post : {add:function(k,v){},set:function(k,v){},get:function(k){},len:function(){}} 62 | 63 | host_addr: #modify hosts eg:127.0.0.1:3218 64 | 65 | #note get and post value is array 66 | #form_get: helper function for get params 67 | #form_post: helper function for post params 68 | ``` 69 | 70 | 71 | hosts config demo: 72 | ``` 73 | www.baidu.com 127.0.0.1 74 | www.baidu.com:81 10.0.2.2:8080 75 | ``` 76 | 77 | disable req_rewrite.js 78 | first line ```//ignore``` 79 | 80 | 81 | req_rewrite.js支持不同用户设置不同的规则。默认使用当前验证使用用户名的规则,若无则使用默认的。 82 | 83 | configs: 84 | ``` 85 | conf/ 86 | ├── pproxy.conf #server config 87 | ├── hosts_8080 #hosts for 8080 88 | ├── req_rewrite_8080.js #8080端口server的url重写规则 89 | ├── hosts_8081 90 | ├── req_rewrite_8081.js 91 | └── users #全局帐号配置文件 92 | ``` 93 | 94 | users配置: 95 | ``` 96 | #帐号 admin,密码 是 psw,是管理员帐号 97 | name:admin psw:psw is_admin:admin 98 | 99 | #密码也可以存储为md5值,使用 psw_md5:32位md密码 100 | name:admin_sec psw_md5:7bb483729b5a8e26f73e1831cde5b842 is_admin:admin 101 | ``` 102 | 可以在线修改配置时必须使用管理员帐号登录 103 | 104 | 配置文件示例: 105 | ``` 106 | 107 | port : 8080 108 | 109 | title : demo 110 | notice :notice notice 111 | 112 | #数据存放目录,相对于当前配置的路径 113 | dataDir : ../data/ 114 | 115 | #数据存放天数,0为永久存储(目前只在重启的时候会进行数据清理) 116 | dataStoreDay : 15 117 | 118 | #代理服务认证方式 119 | #options:{none : 无认证,basic:http basic ,basic_try:尝试httpBasic认证 ,basic_any:任意帐号} 120 | authType : none 121 | 122 | #那些request和response数据进行存储 123 | #options:{ all : 所有 only_broadcast : 发送到session list的才存储} 124 | responseSave : only_broadcast 125 | 126 | #session列表查看数据 127 | # options :{ all:所有人可见 ip_or_user : 输入正确的ip或者user后可见} 128 | sessionView : all 129 | 130 | #父级代理 131 | #eg http://10.10.2.2:3128 or http://name:psw@10.10.2.2:3128 132 | # http://pass:pass@10.10.2.2:3128 the user and psw will pass through to the parent proxy 133 | parentProxy: 134 | ``` 135 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | pproxy 0.5.2 2 | ====== 3 | ## intro 4 | HTTP 协议抓包代理程序, HTTP 协议调试工具。 5 | 采用 Go 编写,采用 BS 模式(s-代理程序,b-会话查看、配置管理等功能) 6 | 7 | 0.4.2版本已经支持websocket代理,以及重定向(和普通http请求一样使用) 8 | 9 | 0.5 版本是对底层存储进行了替换,并且尝试支持https抓包 10 | 11 | ## install 12 | 13 | 已经安装go的用户直接安装: 14 | >go install github.com/hidu/pproxy@master 15 | 16 | ## 功能特性 17 |
 18 | 1.url重定向
 19 |    如把 http://www.baidu.com/s?wd=pproxy 修改为 http://m.baidu.com/s?wd=pproxy
 20 |    或者把 ws://www.test.com/a 重定向到 ws://www.example.com/b
 21 |    
 22 | 2.form表单动态修改  
 23 |    get、post可以动态修改(增删改)  
 24 |    
 25 | 3.hosts文件支持
 26 |   相当于 修改host或者dns 如  
 27 |   将www.baidu.com 请求全部发往127.0.0.1  
 28 |   将www.baidu.com:81 请求全部发往192.168.1.2:8080  
 29 |   
 30 | 4.可查看request 和response详情
 31 |    form表单参数,header等都可以很方便的看到
 32 |    
 33 | 5.登录认证支持
 34 |    支持httpBasic认证
 35 |    
 36 | 6.replay功能
 37 |    可以修改request的参数(get、post、header)
 38 | 
 39 | 7.父级代理
 40 |   
 41 | 
42 | 43 | ## 配置 44 | 45 | ### rewrite req 46 | 使用javascript来配置重定向功能,如 47 | ``` 48 | if(req.host=="www.baidu.com"){ 49 | req.host="www.163.com" 50 | req.host_addr="127.0.0.0:81" // send req to 127.0.0.1:81 51 | } 52 | ``` 53 | 当然也可以这样: 54 | ``` 55 | if(req.host.indexOf("baidu.com")>-1){ 56 | req.host_addr="127.0.0.0:81" 57 | } 58 | ``` 59 | 60 | ### req变量示例 61 | ``` 62 | #url : http://www.example.com/album/list?cid=126 63 | #req对象有如下一下属性: 64 | schema : http 65 | host : www.example.com 66 | port : 80 67 | path : /album/list 68 | get: {cid:[123]} 69 | post: {} 70 | username : 71 | password : 72 | method: GET 73 | form_get : {add:function(k,v){},set:function(k,v){},get:function(k){},len:function(){}} 74 | form_post : {add:function(k,v){},set:function(k,v){},get:function(k){},len:function(){}} 75 | 76 | host_addr: #修改该请求的host是使用,如 127.0.0.1:3218 77 | 78 | #注意 get 和post的值是数组,如上cid参数 79 | #form_get 用于更方便的操作 get参数对象 80 | #form_post 用于更方便的操作 post参数对象 81 | ``` 82 | 83 | ### hosts 84 | 增强的hosts文件使用: 85 | ``` 86 | www.baidu.com 127.0.0.1 87 | www.baidu.com:81 10.0.2.2:8080 88 | ``` 89 | 90 | ### other 91 | 忽略禁用req_rewrite.js 92 | 在js文件的第一行内容写入 ```//ignore``` 93 | 94 | req_rewrite.js支持不同用户设置不同的规则。默认使用当前验证使用用户名的规则,若无则使用默认的。 95 | 96 | ### 配置文件结构: 97 | ``` 98 | conf/ 99 | ├── pproxy.conf #server的配置 100 | ├── hosts_8080 #8080端口server的hosts规则 101 | ├── req_rewrite_8080.js #8080端口server的url重写规则 102 | ├── hosts_8081 103 | ├── req_rewrite_8081.js 104 | └── users #全局帐号配置文件 105 | ``` 106 | 107 | ### users配置: 108 | ``` 109 | #帐号 admin,密码 是 psw,是管理员帐号 110 | name:admin psw:psw is_admin:admin 111 | 112 | #密码也可以存储为md5值,使用 psw_md5:32位md密码 113 | name:admin_sec psw_md5:7bb483729b5a8e26f73e1831cde5b842 is_admin:admin 114 | ``` 115 | 可以在线修改配置时必须使用管理员帐号登录 116 | 117 | ### 配置文件示例(pproxy.conf): 118 | ``` 119 | #提供代理服务的端口 120 | port : 8080 121 | 122 | #管理界面的端口,为0表示和代理服务使用相同的端口,eg:8081 123 | adminPort : 0 124 | 125 | title : demo 126 | notice :notice notice 127 | 128 | #数据存放目录,相对于当前配置的路径 129 | dataDir : ../data/ 130 | 131 | #数据存放天数,0为永久存储(目前只在重启的时候会进行数据清理) 132 | dataStoreDay : 15 133 | 134 | #代理服务认证方式 135 | #options:{none : 无认证,basic:http basic ,basic_try:尝试httpBasic认证 ,basic_any:任意帐号} 136 | authType : none 137 | 138 | #那些request和response数据进行存储 139 | #options:{ all : 所有 only_broadcast : 发送到session list的才存储} 140 | responseSave : only_broadcast 141 | 142 | #session列表查看数据 143 | # options :{ all:所有人可见 ip_or_user : 输入正确的ip或者user后可见} 144 | sessionView : all 145 | 146 | #父级代理 147 | #eg http://10.10.2.2:3128 or http://name:psw@10.10.2.2:3128 148 | # http://pass:pass@10.10.2.2:3128 the user and psw will pass through to the parent proxy 149 | parentProxy: 150 | 151 | 152 | #是否使用中间人方式对https进行抓包,若启用的话 需要客户端按照证书-/res/private/client_cert.pem 153 | #pproxy内置默认证书存放在/res/private目录中 154 | #options:{on:启用 off:禁用} 155 | ssl : on 156 | 157 | #ssl 服务端秘钥文件地址,为空则使用默认内置的 /res/private/server_key.pem 158 | ssl_server_key: 159 | #ssl 公钥地址 ,为空则使用默认内置的 /res/private/client_cert.pem 160 | ssl_client_cert : 161 | ``` 162 | 163 | ## (管理)web查看界面 164 | 方式1: 直接访问 http://serverHost:port 165 | 方式2: 直接访问 http://serverHost:adminPort 166 | 方式3: 浏览器设置http代理 serverHost:port,访问 http://pproxy.man 或者 http://pproxy.com 167 | 168 | # 其他 169 | ## 如何自己修改源码中的静态资源? 170 | 该项目的静态资源(res目录中的所有内容)都编译到go文件中去了,可以处理即可: 171 | 1. 安装goassest工具: 172 | ``` 173 | go get -u github.com/hidu/goassest 174 | ``` 175 | 2.到pproxy代码根目录下运行命令: 176 | ``` 177 | goassest 178 | ``` 179 | 180 | 调试过程中可以添加参数 `-assest_direct` 可以让静态资源实时生效而不需要重新编译静态资源: 181 | ``` 182 | go run proxy_main.go -assest_direct 183 | ``` 184 | -------------------------------------------------------------------------------- /assest.json: -------------------------------------------------------------------------------- 1 | { 2 | "src":"res", 3 | "dest":"serve/assest.go", 4 | "package":"serve" 5 | } 6 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # build for window: build.sh windows 3 | # default linux 4 | #./gox -build-toolchain 5 | 6 | set -e 7 | cd $(dirname $0) 8 | 9 | #export GOPATH=`readlink -f Godeps/_workspace`:$GOPATH 10 | 11 | export GO15VENDOREXPERIMENT=1 12 | 13 | go install -------------------------------------------------------------------------------- /conf/pproxy.conf: -------------------------------------------------------------------------------- 1 | ########################################################## 2 | # pproxy demo conf # 3 | ########################################################## 4 | 5 | #提供代理服务的端口 6 | port : 8080 7 | 8 | #管理界面的端口,为0表示和代理服务使用相同的端口 9 | adminPort : 0 10 | 11 | title : hello pproxy 12 | notice : notice notice 13 | 14 | #数据存放目录,相对于当前配置的路径 15 | dataDir : ../data/ 16 | 17 | #静态文件存放目录 18 | fileDir: ../file/ 19 | 20 | 21 | #数据存放天数,0为永久存储 22 | dataStoreDay : 15 23 | 24 | #代理服务认证方式 25 | #options:{none : 无认证,basic:http basic ,basic_try:尝试httpBasic认证 ,basic_any:任意帐号} 26 | authType : none 27 | 28 | #那些request和response数据进行存储 29 | #options:{ all : 所有 only_broadcast : 发送到session list的才存储} 30 | responseSave : only_broadcast 31 | 32 | #session列表查看数据 33 | # options :{ all:所有人可见 ip_or_user : 输入正确的ip或者user后可见} 34 | sessionView : all 35 | 36 | #eg http://10.10.2.2:3128 or http://name:psw@10.10.2.2:3128 37 | # http://name:psw@10.10.2.2:3128 the user and psw will pass through to the parent proxy 38 | parentProxy: 39 | 40 | #是否使用中间人方式对https进行抓包,若启用的话 需要客户端按照证书-/res/private/client_cert.pem 41 | #pproxy内置默认证书存放在/res/private目录中 42 | #options:{on:启用 off:禁用} 43 | ssl : off 44 | 45 | #ssl 服务端秘钥文件地址,为空则使用默认内置的 /res/private/server_key.pem 46 | ssl_server_key: 47 | #ssl 公钥地址 ,为空则使用默认内置的 /res/private/client_cert.pem 48 | ssl_client_cert : 49 | 50 | #是否开启动态修改请求的功能 {on:开启,off:禁止} 51 | modifyRequest:on -------------------------------------------------------------------------------- /conf/req_rewrite_8080.js: -------------------------------------------------------------------------------- 1 | if (req.host == "news.163.com") { 2 | req.host = "news.baidu.com" 3 | req.host_addr = "127.0.0.1:80" 4 | req.path = "/h/g.php" 5 | form_get.add("ga", "aaa") 6 | form_post.set("a", "ddd") 7 | req.post["d"] = "ddd" 8 | req.post["c"] = 123 9 | } 10 | if (req.host == "news.baidu.com" && req.schema=="ws") { 11 | req.host_addr="127.0.0.1:23456" 12 | } 13 | if(req.host=="www.hao123.com"){ 14 | req.url="http://127.0.0.1/" 15 | } 16 | 17 | if(req.host=="www.oschina.net"){ 18 | //use_file("index.htm") 19 | use_file("http://127.0.0.1:8080/f/index.htm") 20 | } -------------------------------------------------------------------------------- /conf/users: -------------------------------------------------------------------------------- 1 | name:admin psw:psw is_admin:admin 2 | name:abc psw:abc 3 | name:a psw:a -------------------------------------------------------------------------------- /file/index.htm: -------------------------------------------------------------------------------- 1 | hello pproxy < " -------------------------------------------------------------------------------- /fmt.sh: -------------------------------------------------------------------------------- 1 | #/bin/bash 2 | cd $(dirname $0) 3 | cd serve 4 | #gofmt -tabs=false -w=true -tabwidth=4 . 5 | gofmt -w=true -s=true . 6 | 7 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/hidu/pproxy 2 | 3 | go 1.23 4 | 5 | require ( 6 | github.com/Unknwon/goconfig v1.0.0 7 | github.com/boltdb/bolt v1.3.1 8 | github.com/elazarl/goproxy v0.0.0-20241219141958-0cbc93263399 9 | github.com/googollee/go-socket.io v0.9.1 10 | github.com/hidu/goutils v0.0.2 11 | github.com/robertkrimen/otto v0.5.1 12 | ) 13 | 14 | require ( 15 | github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e // indirect 16 | github.com/smartystreets/assertions v0.0.0-20190215210624-980c5ac6f3ac // indirect 17 | github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337 // indirect 18 | golang.org/x/net v0.33.0 // indirect 19 | golang.org/x/sys v0.28.0 // indirect 20 | golang.org/x/text v0.21.0 // indirect 21 | gopkg.in/sourcemap.v1 v1.0.5 // indirect 22 | ) 23 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/Unknwon/goconfig v1.0.0 h1:9IAu/BYbSLQi8puFjUQApZTxIHqSwrj5d8vpP8vTq4A= 2 | github.com/Unknwon/goconfig v1.0.0/go.mod h1:wngxua9XCNjvHjDiTiV26DaKDT+0c63QR6H5hjVUUxw= 3 | github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= 4 | github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= 5 | github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4= 6 | github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps= 7 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 9 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/elazarl/goproxy v0.0.0-20241219141958-0cbc93263399 h1:WBf0ImRm78m/cgOOob9PUyQdYYod2luQA6fs+OqAjbw= 11 | github.com/elazarl/goproxy v0.0.0-20241219141958-0cbc93263399/go.mod h1:3tPvP6c6GrQS4u4TJEbOoqGC2wDNMLra3t6AXWwtL0M= 12 | github.com/elazarl/goproxy/ext v0.0.0-20241217120900-7711dfa3811c h1:R+i10jtNSzKJKqEZAYJnR9M8y14k0zrNHqD1xkv/A2M= 13 | github.com/elazarl/goproxy/ext v0.0.0-20241217120900-7711dfa3811c/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8= 14 | github.com/googollee/go-socket.io v0.9.1 h1:KYsu63c3H5SaeQ3MDlHSTE/LJnwok2SH1M5wy4ZaYD0= 15 | github.com/googollee/go-socket.io v0.9.1/go.mod h1:Q0CvnKmaZNgDXIi85at4eLadAOS1hWDLaDATQpuH3i4= 16 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 17 | github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e h1:JKmoR8x90Iww1ks85zJ1lfDGgIiMDuIptTOhJq+zKyg= 18 | github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 19 | github.com/hidu/goutils v0.0.2 h1:ZjwXZhuWXZjzQ4dHoFo2iN6oDXH1TCudQZ0V2vdAUfQ= 20 | github.com/hidu/goutils v0.0.2/go.mod h1:m13DejGt6FVHM+taWpMHpavxBRZnnQBZeDJyB/YsyRI= 21 | github.com/howeyc/fsnotify v0.9.0/go.mod h1:41HzSPxBGeFRQKEEwgh49TRw/nKBsYZ2cF1OzPjSJsA= 22 | github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= 23 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 24 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 25 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 26 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 27 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 28 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 29 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 30 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 31 | github.com/robertkrimen/otto v0.5.1 h1:avDI4ToRk8k1hppLdYFTuuzND41n37vPGJU7547dGf0= 32 | github.com/robertkrimen/otto v0.5.1/go.mod h1:bS433I4Q9p+E5pZLu7r17vP6FkE6/wLxBdmKjoqJXF8= 33 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= 34 | github.com/smartystreets/assertions v0.0.0-20190215210624-980c5ac6f3ac h1:wbW+Bybf9pXxnCFAOWZTqkRjAc7rAIwo2e1ArUhiHxg= 35 | github.com/smartystreets/assertions v0.0.0-20190215210624-980c5ac6f3ac/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= 36 | github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337 h1:WN9BUFbdyOsSH/XohnWpXOlq9NBD5sGAB2FciQMUEe8= 37 | github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= 38 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 39 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 40 | github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= 41 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 42 | github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= 43 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= 44 | github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= 45 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 46 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 47 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 48 | golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= 49 | golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= 50 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 51 | golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= 52 | golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 53 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 54 | golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= 55 | golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= 56 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 57 | gopkg.in/sourcemap.v1 v1.0.5 h1:inv58fC9f9J3TK2Y2R1NPntXEn3/wjWHkonhIUODNTI= 58 | gopkg.in/sourcemap.v1 v1.0.5/go.mod h1:2RlvNNSMglmRrcvhfuzp4hQHwOtjxlbjX7UPY/GXb78= 59 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 60 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 61 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | "os" 8 | 9 | "github.com/hidu/pproxy/serve" 10 | ) 11 | 12 | var configPath = flag.String("conf", "./conf/pproxy.conf", "pproxy's config file") 13 | var port = flag.Int("port", 0, "proxy port") 14 | var vv = flag.Bool("vv", false, "debug,log request with more detail") 15 | var showConf = flag.Bool("demo_conf", false, "show default conf") 16 | 17 | var version = flag.Bool("v", false, "show version") 18 | 19 | func init() { 20 | df := flag.Usage 21 | 22 | flag.Usage = func() { 23 | df() 24 | fmt.Fprintln(os.Stderr, "\n HTTP protocol analysis tool\n https://github.com/hidu/pproxy/\n") 25 | } 26 | } 27 | 28 | func main() { 29 | flag.Parse() 30 | 31 | if *showConf { 32 | demoConf := serve.GetDemoConf() 33 | fmt.Println(demoConf) 34 | os.Exit(0) 35 | } 36 | 37 | if *version { 38 | fmt.Println("pproxy version:", serve.GetVersion()) 39 | os.Exit(0) 40 | } 41 | 42 | log.SetFlags(log.Lshortfile | log.LstdFlags | log.Ldate) 43 | ser, err := serve.NewProxyServe(*configPath, *port) 44 | if err != nil { 45 | fmt.Println("start pproxy failed", err) 46 | os.Exit(2) 47 | } 48 | ser.Debug = *vv 49 | ser.Start() 50 | } 51 | -------------------------------------------------------------------------------- /res/conf/demo.conf: -------------------------------------------------------------------------------- 1 | ########################################################## 2 | # pproxy demo conf # 3 | ########################################################## 4 | 5 | #提供代理服务的端口 6 | port : 8080 7 | 8 | #管理界面的端口,为0表示和代理服务使用相同的端口 9 | adminPort : 0 10 | 11 | title : hello pproxy 12 | notice : notice notice 13 | 14 | #数据存放目录,相对于当前配置的路径 15 | dataDir : ../data/ 16 | 17 | #静态文件存放目录 18 | fileDir: ../file/ 19 | 20 | 21 | #数据存放天数,0为永久存储(目前只在重启的时候会进行数据清理) 22 | dataStoreDay : 15 23 | 24 | #代理服务认证方式 25 | #options:{none : 无认证,basic:http basic ,basic_try:尝试httpBasic认证 ,basic_any:任意帐号} 26 | authType : none 27 | 28 | #那些request和response数据进行存储 29 | #options:{ all : 所有 only_broadcast : 发送到session list的才存储} 30 | responseSave : only_broadcast 31 | 32 | #session列表查看数据 33 | # options :{ all:所有人可见 ip_or_user : 输入正确的ip或者user后可见} 34 | sessionView : all 35 | 36 | #eg http://10.10.2.2:3128 or http://name:psw@10.10.2.2:3128 37 | # http://name:psw@10.10.2.2:3128 the user and psw will pass through to the parent proxy 38 | parentProxy: -------------------------------------------------------------------------------- /res/css/flat.css: -------------------------------------------------------------------------------- 1 | .dropdown-arrow-inverse { 2 | border-bottom-color: #34495e !important; 3 | border-top-color: #34495e !important; 4 | } 5 | a { 6 | color: #428bca; 7 | text-decoration: none; 8 | -webkit-transition: 0.25s; 9 | transition: 0.25s; 10 | } 11 | a:hover, 12 | a:focus { 13 | color: #428bca; 14 | text-decoration: none; 15 | } 16 | a:focus { 17 | outline: none; 18 | } 19 | .img-rounded { 20 | border-radius: 6px; 21 | } 22 | .img-thumbnail { 23 | padding: 4px; 24 | line-height: 1.72222; 25 | background-color: #ffffff; 26 | border: 2px solid #bdc3c7; 27 | border-radius: 6px; 28 | -webkit-transition: all 0.25s ease-in-out; 29 | transition: all 0.25s ease-in-out; 30 | display: inline-block; 31 | max-width: 100%; 32 | height: auto; 33 | } 34 | .img-comment { 35 | font-size: 15px; 36 | line-height: 1.2; 37 | font-style: italic; 38 | margin: 24px 0; 39 | } 40 | h1, 41 | h2, 42 | h3, 43 | h4, 44 | h5, 45 | h6, 46 | .h1, 47 | .h2, 48 | .h3, 49 | .h4, 50 | .h5, 51 | .h6 { 52 | font-family: inherit; 53 | font-weight: 700; 54 | line-height: 1.1; 55 | color: inherit; 56 | } 57 | h1 small, 58 | h2 small, 59 | h3 small, 60 | h4 small, 61 | h5 small, 62 | h6 small, 63 | .h1 small, 64 | .h2 small, 65 | .h3 small, 66 | .h4 small, 67 | .h5 small, 68 | .h6 small { 69 | color: #e7e9ec; 70 | } 71 | h1, 72 | h2, 73 | h3 { 74 | margin-top: 30px; 75 | margin-bottom: 15px; 76 | } 77 | h4, 78 | h5, 79 | h6 { 80 | margin-top: 15px; 81 | margin-bottom: 15px; 82 | } 83 | h6 { 84 | font-weight: normal; 85 | } 86 | h1, 87 | .h1 { 88 | font-size: 61px; 89 | } 90 | h2, 91 | .h2 { 92 | font-size: 53px; 93 | } 94 | h3, 95 | .h3 { 96 | font-size: 40px; 97 | } 98 | h4, 99 | .h4 { 100 | font-size: 29px; 101 | } 102 | h5, 103 | .h5 { 104 | font-size: 28px; 105 | } 106 | h6, 107 | .h6 { 108 | font-size: 24px; 109 | } 110 | p { 111 | font-size: 18px; 112 | line-height: 1.72222; 113 | margin: 0 0 15px; 114 | } 115 | .lead { 116 | margin-bottom: 30px; 117 | font-size: 28px; 118 | line-height: 1.46428571; 119 | font-weight: 300; 120 | } 121 | @media (min-width: 768px) { 122 | .lead { 123 | font-size: 30.006px; 124 | } 125 | } 126 | small, 127 | .small { 128 | font-size: 83%; 129 | line-height: 2.067; 130 | } 131 | .text-muted { 132 | color: #bdc3c7; 133 | } 134 | .text-inverse { 135 | color: #ffffff; 136 | } 137 | .text-primary { 138 | color: #428bca; 139 | } 140 | a.text-primary:hover { 141 | color: #15967d; 142 | } 143 | .text-warning { 144 | color: #f1c40f; 145 | } 146 | a.text-warning:hover { 147 | color: #c19d0c; 148 | } 149 | .text-danger { 150 | color: #e74c3c; 151 | } 152 | a.text-danger:hover { 153 | color: #b93d30; 154 | } 155 | .text-success { 156 | color: #2ecc71; 157 | } 158 | a.text-success:hover { 159 | color: #25a35a; 160 | } 161 | .text-info { 162 | color: #3498db; 163 | } 164 | a.text-info:hover { 165 | color: #2a7aaf; 166 | } 167 | .bg-primary { 168 | color: #ffffff; 169 | background-color: #34495e; 170 | } 171 | a.bg-primary:hover { 172 | background-color: #222f3d; 173 | } 174 | .bg-success { 175 | background-color: #dff0d8; 176 | } 177 | a.bg-success:hover { 178 | background-color: #c1e2b3; 179 | } 180 | .bg-info { 181 | background-color: #d9edf7; 182 | } 183 | a.bg-info:hover { 184 | background-color: #afd9ee; 185 | } 186 | .bg-warning { 187 | background-color: #fcf8e3; 188 | } 189 | a.bg-warning:hover { 190 | background-color: #f7ecb5; 191 | } 192 | .bg-danger { 193 | background-color: #f2dede; 194 | } 195 | a.bg-danger:hover { 196 | background-color: #e4b9b9; 197 | } 198 | .page-header { 199 | padding-bottom: 14px; 200 | margin: 60px 0 30px; 201 | border-bottom: 1px solid #e7e9ec; 202 | } 203 | ul, 204 | ol { 205 | margin-bottom: 15px; 206 | } 207 | dl { 208 | margin-bottom: 30px; 209 | } 210 | dt, 211 | dd { 212 | line-height: 1.72222; 213 | } 214 | @media (min-width: 768px) { 215 | .dl-horizontal dt { 216 | width: 160px; 217 | } 218 | .dl-horizontal dd { 219 | margin-left: 180px; 220 | } 221 | } 222 | abbr[title], 223 | abbr[data-original-title] { 224 | border-bottom: 1px dotted #bdc3c7; 225 | } 226 | blockquote { 227 | border-left: 3px solid #e7e9ec; 228 | padding: 0 0 0 16px; 229 | margin: 0 0 30px; 230 | } 231 | blockquote p { 232 | font-size: 20px; 233 | line-height: 1.55; 234 | font-weight: normal; 235 | margin-bottom: .4em; 236 | } 237 | blockquote small, 238 | blockquote .small { 239 | font-size: 18px; 240 | line-height: 1.72222; 241 | font-style: italic; 242 | color: inherit; 243 | } 244 | blockquote small:before, 245 | blockquote .small:before { 246 | content: ""; 247 | } 248 | blockquote.pull-right { 249 | padding-right: 16px; 250 | padding-left: 0; 251 | border-right: 3px solid #e7e9ec; 252 | border-left: 0; 253 | } 254 | blockquote.pull-right small:after { 255 | content: ""; 256 | } 257 | address { 258 | margin-bottom: 30px; 259 | line-height: 1.72222; 260 | } 261 | code, 262 | kbd, 263 | pre, 264 | samp { 265 | font-family: Monaco, Menlo, Consolas, "Courier New", monospace; 266 | } 267 | code { 268 | padding: 2px 6px; 269 | font-size: 85%; 270 | color: #c7254e; 271 | background-color: #f9f2f4; 272 | border-radius: 4px; 273 | } 274 | kbd { 275 | padding: 2px 6px; 276 | font-size: 85%; 277 | color: #ffffff; 278 | background-color: #34495e; 279 | border-radius: 4px; 280 | box-shadow: none; 281 | } 282 | pre { 283 | padding: 8px; 284 | margin: 0 0 15px; 285 | font-size: 13px; 286 | line-height: 1.72222; 287 | color: inherit; 288 | background-color: #ffffff; 289 | border: 2px solid #e7e9ec; 290 | border-radius: 6px; 291 | white-space: pre; 292 | } 293 | .pre-scrollable { 294 | max-height: 340px; 295 | } 296 | .btn { 297 | border: none; 298 | font-size: 14px; 299 | font-weight: normal; 300 | cursor: pointer; 301 | line-height: 1.4; 302 | border-radius: 4px; 303 | padding: 6px 12px; 304 | -webkit-font-smoothing: subpixel-antialiased; 305 | -webkit-transition: border .25s linear, color .25s linear, background-color .25s linear; 306 | transition: border .25s linear, color .25s linear, background-color .25s linear; 307 | } 308 | .btn:hover, 309 | .btn:focus { 310 | outline: none; 311 | color: #ffffff; 312 | } 313 | .btn:active, 314 | .btn.active { 315 | outline: none; 316 | -webkit-box-shadow: none; 317 | box-shadow: none; 318 | } 319 | .btn.disabled, 320 | .btn[disabled], 321 | fieldset[disabled] .btn { 322 | background-color: #bdc3c7; 323 | color: rgba(255, 255, 255, 0.75); 324 | opacity: 0.7; 325 | filter: alpha(opacity=70); 326 | } 327 | .btn > [class^="fui-"] { 328 | margin: 0 1px; 329 | position: relative; 330 | line-height: 1; 331 | top: 1px; 332 | } 333 | .btn-xs.btn > [class^="fui-"] { 334 | font-size: 11px; 335 | top: 0; 336 | } 337 | .btn-hg.btn > [class^="fui-"] { 338 | top: 2px; 339 | } 340 | .btn-default { 341 | color: #ffffff; 342 | background-color: #bdc3c7; 343 | } 344 | .btn-default:hover, 345 | .btn-default:focus, 346 | .btn-default:active, 347 | .btn-default.active, 348 | .open .dropdown-toggle.btn-default { 349 | color: #ffffff; 350 | background-color: #cacfd2; 351 | border-color: #cacfd2; 352 | } 353 | .btn-default:active, 354 | .btn-default.active, 355 | .open .dropdown-toggle.btn-default { 356 | background: #a1a6a9; 357 | border-color: #a1a6a9; 358 | } 359 | .btn-default.disabled, 360 | .btn-default[disabled], 361 | fieldset[disabled] .btn-default, 362 | .btn-default.disabled:hover, 363 | .btn-default[disabled]:hover, 364 | fieldset[disabled] .btn-default:hover, 365 | .btn-default.disabled:focus, 366 | .btn-default[disabled]:focus, 367 | fieldset[disabled] .btn-default:focus, 368 | .btn-default.disabled:active, 369 | .btn-default[disabled]:active, 370 | fieldset[disabled] .btn-default:active, 371 | .btn-default.disabled.active, 372 | .btn-default[disabled].active, 373 | fieldset[disabled] .btn-default.active { 374 | background-color: #bdc3c7; 375 | border-color: #bdc3c7; 376 | } 377 | .btn-primary { 378 | color: #ffffff; 379 | background-color: #428bca; 380 | } 381 | .btn-primary:hover, 382 | .btn-primary:focus, 383 | .btn-primary:active, 384 | .btn-primary.active, 385 | .open .dropdown-toggle.btn-primary { 386 | color: #ffffff; 387 | background-color: #3276b1; 388 | border-color: #3276b1; 389 | } 390 | .btn-primary:active, 391 | .btn-primary.active, 392 | .open .dropdown-toggle.btn-primary { 393 | background: #428bca; 394 | border-color: #428bca; 395 | } 396 | .btn-primary.disabled, 397 | .btn-primary[disabled], 398 | fieldset[disabled] .btn-primary, 399 | .btn-primary.disabled:hover, 400 | .btn-primary[disabled]:hover, 401 | fieldset[disabled] .btn-primary:hover, 402 | .btn-primary.disabled:focus, 403 | .btn-primary[disabled]:focus, 404 | fieldset[disabled] .btn-primary:focus, 405 | .btn-primary.disabled:active, 406 | .btn-primary[disabled]:active, 407 | fieldset[disabled] .btn-primary:active, 408 | .btn-primary.disabled.active, 409 | .btn-primary[disabled].active, 410 | fieldset[disabled] .btn-primary.active { 411 | background-color: #428bca; 412 | border-color: #428bca; 413 | } 414 | .btn-info { 415 | color: #ffffff; 416 | background-color: #3498db; 417 | } 418 | .btn-info:hover, 419 | .btn-info:focus, 420 | .btn-info:active, 421 | .btn-info.active, 422 | .open .dropdown-toggle.btn-info { 423 | color: #ffffff; 424 | background-color: #5dade2; 425 | border-color: #5dade2; 426 | } 427 | .btn-info:active, 428 | .btn-info.active, 429 | .open .dropdown-toggle.btn-info { 430 | background: #2c81ba; 431 | border-color: #2c81ba; 432 | } 433 | .btn-info.disabled, 434 | .btn-info[disabled], 435 | fieldset[disabled] .btn-info, 436 | .btn-info.disabled:hover, 437 | .btn-info[disabled]:hover, 438 | fieldset[disabled] .btn-info:hover, 439 | .btn-info.disabled:focus, 440 | .btn-info[disabled]:focus, 441 | fieldset[disabled] .btn-info:focus, 442 | .btn-info.disabled:active, 443 | .btn-info[disabled]:active, 444 | fieldset[disabled] .btn-info:active, 445 | .btn-info.disabled.active, 446 | .btn-info[disabled].active, 447 | fieldset[disabled] .btn-info.active { 448 | background-color: #3498db; 449 | border-color: #3498db; 450 | } 451 | .btn-danger { 452 | color: #ffffff; 453 | background-color: #e74c3c; 454 | } 455 | .btn-danger:hover, 456 | .btn-danger:focus, 457 | .btn-danger:active, 458 | .btn-danger.active, 459 | .open .dropdown-toggle.btn-danger { 460 | color: #ffffff; 461 | background-color: #ec7063; 462 | border-color: #ec7063; 463 | } 464 | .btn-danger:active, 465 | .btn-danger.active, 466 | .open .dropdown-toggle.btn-danger { 467 | background: #c44133; 468 | border-color: #c44133; 469 | } 470 | .btn-danger.disabled, 471 | .btn-danger[disabled], 472 | fieldset[disabled] .btn-danger, 473 | .btn-danger.disabled:hover, 474 | .btn-danger[disabled]:hover, 475 | fieldset[disabled] .btn-danger:hover, 476 | .btn-danger.disabled:focus, 477 | .btn-danger[disabled]:focus, 478 | fieldset[disabled] .btn-danger:focus, 479 | .btn-danger.disabled:active, 480 | .btn-danger[disabled]:active, 481 | fieldset[disabled] .btn-danger:active, 482 | .btn-danger.disabled.active, 483 | .btn-danger[disabled].active, 484 | fieldset[disabled] .btn-danger.active { 485 | background-color: #e74c3c; 486 | border-color: #e74c3c; 487 | } 488 | .btn-success { 489 | color: #ffffff; 490 | background-color: #2ecc71; 491 | } 492 | .btn-success:hover, 493 | .btn-success:focus, 494 | .btn-success:active, 495 | .btn-success.active, 496 | .open .dropdown-toggle.btn-success { 497 | color: #ffffff; 498 | background-color: #58d68d; 499 | border-color: #58d68d; 500 | } 501 | .btn-success:active, 502 | .btn-success.active, 503 | .open .dropdown-toggle.btn-success { 504 | background: #27ad60; 505 | border-color: #27ad60; 506 | } 507 | .btn-success.disabled, 508 | .btn-success[disabled], 509 | fieldset[disabled] .btn-success, 510 | .btn-success.disabled:hover, 511 | .btn-success[disabled]:hover, 512 | fieldset[disabled] .btn-success:hover, 513 | .btn-success.disabled:focus, 514 | .btn-success[disabled]:focus, 515 | fieldset[disabled] .btn-success:focus, 516 | .btn-success.disabled:active, 517 | .btn-success[disabled]:active, 518 | fieldset[disabled] .btn-success:active, 519 | .btn-success.disabled.active, 520 | .btn-success[disabled].active, 521 | fieldset[disabled] .btn-success.active { 522 | background-color: #2ecc71; 523 | border-color: #2ecc71; 524 | } 525 | .btn-warning { 526 | color: #ffffff; 527 | background-color: #f1c40f; 528 | } 529 | .btn-warning:hover, 530 | .btn-warning:focus, 531 | .btn-warning:active, 532 | .btn-warning.active, 533 | .open .dropdown-toggle.btn-warning { 534 | color: #ffffff; 535 | background-color: #f4d313; 536 | border-color: #f4d313; 537 | } 538 | .btn-warning:active, 539 | .btn-warning.active, 540 | .open .dropdown-toggle.btn-warning { 541 | background: #cda70d; 542 | border-color: #cda70d; 543 | } 544 | .btn-warning.disabled, 545 | .btn-warning[disabled], 546 | fieldset[disabled] .btn-warning, 547 | .btn-warning.disabled:hover, 548 | .btn-warning[disabled]:hover, 549 | fieldset[disabled] .btn-warning:hover, 550 | .btn-warning.disabled:focus, 551 | .btn-warning[disabled]:focus, 552 | fieldset[disabled] .btn-warning:focus, 553 | .btn-warning.disabled:active, 554 | .btn-warning[disabled]:active, 555 | fieldset[disabled] .btn-warning:active, 556 | .btn-warning.disabled.active, 557 | .btn-warning[disabled].active, 558 | fieldset[disabled] .btn-warning.active { 559 | background-color: #f1c40f; 560 | border-color: #f1c40f; 561 | } 562 | .btn-inverse { 563 | color: #ffffff; 564 | background-color: #34495e; 565 | } 566 | .btn-inverse:hover, 567 | .btn-inverse:focus, 568 | .btn-inverse:active, 569 | .btn-inverse.active, 570 | .open .dropdown-toggle.btn-inverse { 571 | color: #ffffff; 572 | background-color: #415b76; 573 | border-color: #415b76; 574 | } 575 | .btn-inverse:active, 576 | .btn-inverse.active, 577 | .open .dropdown-toggle.btn-inverse { 578 | background: #2c3e50; 579 | border-color: #2c3e50; 580 | } 581 | .btn-inverse.disabled, 582 | .btn-inverse[disabled], 583 | fieldset[disabled] .btn-inverse, 584 | .btn-inverse.disabled:hover, 585 | .btn-inverse[disabled]:hover, 586 | fieldset[disabled] .btn-inverse:hover, 587 | .btn-inverse.disabled:focus, 588 | .btn-inverse[disabled]:focus, 589 | fieldset[disabled] .btn-inverse:focus, 590 | .btn-inverse.disabled:active, 591 | .btn-inverse[disabled]:active, 592 | fieldset[disabled] .btn-inverse:active, 593 | .btn-inverse.disabled.active, 594 | .btn-inverse[disabled].active, 595 | fieldset[disabled] .btn-inverse.active { 596 | background-color: #34495e; 597 | border-color: #34495e; 598 | } 599 | .btn-embossed { 600 | -webkit-box-shadow: inset 0 -2px 0 rgba(0, 0, 0, 0.15); 601 | box-shadow: inset 0 -2px 0 rgba(0, 0, 0, 0.15); 602 | } 603 | .btn-embossed.active, 604 | .btn-embossed:active { 605 | -webkit-box-shadow: inset 0 2px 0 rgba(0, 0, 0, 0.15); 606 | box-shadow: inset 0 2px 0 rgba(0, 0, 0, 0.15); 607 | } 608 | .btn-wide { 609 | min-width: 140px; 610 | padding-left: 30px; 611 | padding-right: 30px; 612 | } 613 | .btn-link { 614 | color: #428bca; 615 | } 616 | .btn-link:hover, 617 | .btn-link:focus { 618 | color: #428bca; 619 | text-decoration: underline; 620 | background-color: transparent; 621 | } 622 | .btn-link[disabled]:hover, 623 | fieldset[disabled] .btn-link:hover, 624 | .btn-link[disabled]:focus, 625 | fieldset[disabled] .btn-link:focus { 626 | color: #bdc3c7; 627 | text-decoration: none; 628 | } 629 | .btn-hg { 630 | padding: 13px 20px; 631 | font-size: 22px; 632 | line-height: 1.227; 633 | border-radius: 6px; 634 | } 635 | .btn-lg { 636 | padding: 10px 19px; 637 | font-size: 17px; 638 | line-height: 1.471; 639 | border-radius: 6px; 640 | } 641 | .btn-sm { 642 | padding: 9px 13px; 643 | font-size: 13px; 644 | line-height: 1.385; 645 | border-radius: 4px; 646 | } 647 | .btn-xs { 648 | padding: 6px 9px; 649 | font-size: 12px; 650 | line-height: 1.083; 651 | border-radius: 3px; 652 | } 653 | .btn-tip { 654 | font-weight: 300; 655 | padding-left: 10px; 656 | font-size: 92%; 657 | } 658 | .btn-block { 659 | white-space: normal; 660 | } 661 | .caret { 662 | border-width: 8px 6px; 663 | border-bottom-color: #34495e; 664 | border-top-color: #34495e; 665 | border-style: solid; 666 | border-bottom-style: none; 667 | -webkit-transition: 0.25s; 668 | transition: 0.25s; 669 | -webkit-transform: scale(1.001, ); 670 | -ms-transform: scale(1.001, ); 671 | transform: scale(1.001, ); 672 | } 673 | .dropup .caret, 674 | .dropup .btn-lg .caret, 675 | .navbar-fixed-bottom .dropdown .caret { 676 | border-bottom-width: 8px; 677 | } 678 | .btn-lg .caret { 679 | border-top-width: 8px; 680 | border-right-width: 6px; 681 | border-left-width: 6px; 682 | } 683 | .select { 684 | display: inline-block; 685 | margin-bottom: 10px; 686 | } 687 | [class*="span"] > .select[class*="span"] { 688 | margin-left: 0; 689 | } 690 | .select[class*="span"] .btn { 691 | width: 100%; 692 | } 693 | .select.select-block { 694 | display: block; 695 | float: none; 696 | margin-left: 0; 697 | width: auto; 698 | } 699 | .select.select-block:before, 700 | .select.select-block:after { 701 | content: " "; 702 | /* 1 */ 703 | display: table; 704 | /* 2 */ 705 | } 706 | .select.select-block:after { 707 | clear: both; 708 | } 709 | .select.select-block .btn { 710 | width: 100%; 711 | } 712 | .select.select-block .dropdown-menu { 713 | width: 100%; 714 | } 715 | .select .btn { 716 | width: 220px; 717 | } 718 | .select .btn.btn-hg .filter-option { 719 | left: 20px; 720 | right: 40px; 721 | top: 13px; 722 | } 723 | .select .btn.btn-hg .caret { 724 | right: 20px; 725 | } 726 | .select .btn.btn-lg .filter-option { 727 | left: 18px; 728 | right: 38px; 729 | } 730 | .select .btn.btn-sm .filter-option { 731 | left: 13px; 732 | right: 33px; 733 | } 734 | .select .btn.btn-sm .caret { 735 | right: 13px; 736 | } 737 | .select .btn.btn-xs .filter-option { 738 | left: 13px; 739 | right: 33px; 740 | top: 5px; 741 | } 742 | .select .btn.btn-xs .caret { 743 | right: 13px; 744 | } 745 | .select .btn .filter-option { 746 | height: 26px; 747 | left: 13px; 748 | overflow: hidden; 749 | position: absolute; 750 | right: 33px; 751 | text-align: left; 752 | top: 10px; 753 | } 754 | .select .btn .caret { 755 | position: absolute; 756 | right: 16px; 757 | top: 50%; 758 | margin-top: -3px; 759 | } 760 | .select .btn .dropdown-toggle { 761 | border-radius: 6px; 762 | } 763 | .select .btn .dropdown-menu { 764 | min-width: 100%; 765 | } 766 | .select .btn .dropdown-menu dt { 767 | cursor: default; 768 | display: block; 769 | padding: 3px 20px; 770 | } 771 | .select .btn .dropdown-menu li:not(.disabled) > a:hover small { 772 | color: rgba(255, 255, 255, 0.004); 773 | } 774 | .select .btn .dropdown-menu li > a { 775 | min-height: 20px; 776 | } 777 | .select .btn .dropdown-menu li > a.opt { 778 | padding-left: 35px; 779 | } 780 | .select .btn .dropdown-menu li small { 781 | padding-left: .5em; 782 | } 783 | .select .btn .dropdown-menu li > dt small { 784 | font-weight: normal; 785 | } 786 | .select .btn > .disabled, 787 | .select .btn .dropdown-menu li.disabled > a { 788 | cursor: default; 789 | } 790 | .select .caret { 791 | border-bottom-color: #ffffff; 792 | border-top-color: #ffffff; 793 | } 794 | 795 | textarea { 796 | font-size: 20px; 797 | line-height: 24px; 798 | padding: 5px 11px; 799 | } 800 | input[type="search"] { 801 | -webkit-appearance: none !important; 802 | } 803 | label { 804 | font-weight: normal; 805 | font-size: 15px; 806 | line-height: 2.4; 807 | } 808 | .form-control:-moz-placeholder { 809 | color: #b2bcc5; 810 | } 811 | .form-control::-moz-placeholder { 812 | color: #b2bcc5; 813 | opacity: 1; 814 | } 815 | .form-control:-ms-input-placeholder { 816 | color: #b2bcc5; 817 | } 818 | .form-control::-webkit-input-placeholder { 819 | color: #b2bcc5; 820 | } 821 | .form-control.placeholder { 822 | color: #b2bcc5; 823 | } 824 | .form-control { 825 | border: 2px solid #bdc3c7; 826 | color: #34495e; 827 | font-family: "Lato", Helvetica, Arial, sans-serif; 828 | font-size: 15px; 829 | line-height: 1.467; 830 | padding: 8px 12px; 831 | height: 42px; 832 | -webkit-appearance: none; 833 | border-radius: 6px; 834 | -webkit-box-shadow: none; 835 | box-shadow: none; 836 | -webkit-transition: border .25s linear, color .25s linear, background-color .25s linear; 837 | transition: border .25s linear, color .25s linear, background-color .25s linear; 838 | } 839 | .form-group.focus .form-control, 840 | .form-control:focus { 841 | border-color: #428bca; 842 | outline: 0; 843 | -webkit-box-shadow: none; 844 | box-shadow: none; 845 | } 846 | .form-control[disabled], 847 | .form-control[readonly], 848 | fieldset[disabled] .form-control { 849 | background-color: #f4f6f6; 850 | border-color: #d5dbdb; 851 | color: #d5dbdb; 852 | cursor: default; 853 | opacity: 0.7; 854 | filter: alpha(opacity=70); 855 | } 856 | .form-control.flat { 857 | border-color: transparent; 858 | } 859 | .form-control.flat:hover { 860 | border-color: #bdc3c7; 861 | } 862 | .form-control.flat:focus { 863 | border-color: #428bca; 864 | } 865 | .input-sm { 866 | height: 35px; 867 | padding: 6px 10px; 868 | font-size: 13px; 869 | line-height: 1.462; 870 | border-radius: 6px; 871 | } 872 | select.input-sm { 873 | height: 35px; 874 | line-height: 35px; 875 | } 876 | textarea.input-sm, 877 | select[multiple].input-sm { 878 | height: auto; 879 | } 880 | .input-lg { 881 | height: 45px; 882 | padding: 10px 15px; 883 | font-size: 17px; 884 | line-height: 1.235; 885 | border-radius: 6px; 886 | } 887 | select.input-lg { 888 | height: 45px; 889 | line-height: 45px; 890 | } 891 | textarea.input-lg, 892 | select[multiple].input-lg { 893 | height: auto; 894 | } 895 | .input-hg { 896 | height: 53px; 897 | padding: 10px 16px; 898 | font-size: 22px; 899 | line-height: 1.318; 900 | border-radius: 6px; 901 | } 902 | select.input-hg { 903 | height: 53px; 904 | line-height: 53px; 905 | } 906 | textarea.input-hg, 907 | select[multiple].input-hg { 908 | height: auto; 909 | } 910 | .has-warning .help-block, 911 | .has-warning .control-label, 912 | .has-warning .radio, 913 | .has-warning .checkbox, 914 | .has-warning .radio-inline, 915 | .has-warning .checkbox-inline { 916 | color: #f1c40f; 917 | } 918 | .has-warning .form-control { 919 | color: #f1c40f; 920 | border-color: #f1c40f; 921 | -webkit-box-shadow: none; 922 | box-shadow: none; 923 | } 924 | .has-warning .form-control:-moz-placeholder { 925 | color: #f1c40f; 926 | } 927 | .has-warning .form-control::-moz-placeholder { 928 | color: #f1c40f; 929 | opacity: 1; 930 | } 931 | .has-warning .form-control:-ms-input-placeholder { 932 | color: #f1c40f; 933 | } 934 | .has-warning .form-control::-webkit-input-placeholder { 935 | color: #f1c40f; 936 | } 937 | .has-warning .form-control.placeholder { 938 | color: #f1c40f; 939 | } 940 | .has-warning .form-control:focus { 941 | border-color: #f1c40f; 942 | -webkit-box-shadow: none; 943 | box-shadow: none; 944 | } 945 | .has-warning .input-group-addon { 946 | color: #f1c40f; 947 | border-color: #f1c40f; 948 | background-color: #ffffff; 949 | } 950 | .has-error .help-block, 951 | .has-error .control-label, 952 | .has-error .radio, 953 | .has-error .checkbox, 954 | .has-error .radio-inline, 955 | .has-error .checkbox-inline { 956 | color: #e74c3c; 957 | } 958 | .has-error .form-control { 959 | color: #e74c3c; 960 | border-color: #e74c3c; 961 | -webkit-box-shadow: none; 962 | box-shadow: none; 963 | } 964 | .has-error .form-control:-moz-placeholder { 965 | color: #e74c3c; 966 | } 967 | .has-error .form-control::-moz-placeholder { 968 | color: #e74c3c; 969 | opacity: 1; 970 | } 971 | .has-error .form-control:-ms-input-placeholder { 972 | color: #e74c3c; 973 | } 974 | .has-error .form-control::-webkit-input-placeholder { 975 | color: #e74c3c; 976 | } 977 | .has-error .form-control.placeholder { 978 | color: #e74c3c; 979 | } 980 | .has-error .form-control:focus { 981 | border-color: #e74c3c; 982 | -webkit-box-shadow: none; 983 | box-shadow: none; 984 | } 985 | .has-error .input-group-addon { 986 | color: #e74c3c; 987 | border-color: #e74c3c; 988 | background-color: #ffffff; 989 | } 990 | .has-success .help-block, 991 | .has-success .control-label, 992 | .has-success .radio, 993 | .has-success .checkbox, 994 | .has-success .radio-inline, 995 | .has-success .checkbox-inline { 996 | color: #2ecc71; 997 | } 998 | .has-success .form-control { 999 | color: #2ecc71; 1000 | border-color: #2ecc71; 1001 | -webkit-box-shadow: none; 1002 | box-shadow: none; 1003 | } 1004 | .has-success .form-control:-moz-placeholder { 1005 | color: #2ecc71; 1006 | } 1007 | .has-success .form-control::-moz-placeholder { 1008 | color: #2ecc71; 1009 | opacity: 1; 1010 | } 1011 | .has-success .form-control:-ms-input-placeholder { 1012 | color: #2ecc71; 1013 | } 1014 | .has-success .form-control::-webkit-input-placeholder { 1015 | color: #2ecc71; 1016 | } 1017 | .has-success .form-control.placeholder { 1018 | color: #2ecc71; 1019 | } 1020 | .has-success .form-control:focus { 1021 | border-color: #2ecc71; 1022 | -webkit-box-shadow: none; 1023 | box-shadow: none; 1024 | } 1025 | .has-success .input-group-addon { 1026 | color: #2ecc71; 1027 | border-color: #2ecc71; 1028 | background-color: #ffffff; 1029 | } 1030 | .help-block { 1031 | font-size: 15px; 1032 | margin-bottom: 5px; 1033 | color: inherit; 1034 | } 1035 | .form-group { 1036 | position: relative; 1037 | margin-bottom: 20px; 1038 | } 1039 | .form-horizontal .control-label, 1040 | .form-horizontal .radio, 1041 | .form-horizontal .checkbox, 1042 | .form-horizontal .radio-inline, 1043 | .form-horizontal .checkbox-inline { 1044 | margin-top: 0; 1045 | margin-bottom: 0; 1046 | padding-top: 6px; 1047 | } 1048 | .form-horizontal .form-group { 1049 | margin-left: -15px; 1050 | margin-right: -15px; 1051 | } 1052 | .form-horizontal .form-group:before, 1053 | .form-horizontal .form-group:after { 1054 | content: " "; 1055 | /* 1 */ 1056 | display: table; 1057 | /* 2 */ 1058 | } 1059 | .form-horizontal .form-group:after { 1060 | clear: both; 1061 | } 1062 | .form-horizontal .form-control-static { 1063 | padding-top: 6px; 1064 | } 1065 | .form-group { 1066 | position: relative; 1067 | } 1068 | .form-control + .input-icon { 1069 | position: absolute; 1070 | top: 2px; 1071 | right: 2px; 1072 | line-height: 37px; 1073 | vertical-align: middle; 1074 | font-size: 20px; 1075 | color: #b2bcc5; 1076 | background-color: #ffffff; 1077 | padding: 0 12px 0 0; 1078 | border-radius: 6px; 1079 | } 1080 | .input-hg + .input-icon { 1081 | line-height: 49px; 1082 | padding: 0 16px 0 0; 1083 | } 1084 | .input-lg + .input-icon { 1085 | line-height: 41px; 1086 | padding: 0 15px 0 0; 1087 | } 1088 | .input-sm + .input-icon { 1089 | font-size: 18px; 1090 | line-height: 30px; 1091 | padding: 0 10px 0 0; 1092 | } 1093 | .has-success .input-icon { 1094 | color: #2ecc71; 1095 | } 1096 | .has-warning .input-icon { 1097 | color: #f1c40f; 1098 | } 1099 | .has-error .input-icon { 1100 | color: #e74c3c; 1101 | } 1102 | .form-control[disabled] + .input-icon, 1103 | .form-control[readonly] + .input-icon, 1104 | fieldset[disabled] .form-control + .input-icon, 1105 | .form-control.disabled + .input-icon { 1106 | color: #d5dbdb; 1107 | background-color: transparent; 1108 | opacity: 0.7; 1109 | filter: alpha(opacity=70); 1110 | } 1111 | .input-group-hg > .form-control, 1112 | .input-group-hg > .input-group-addon, 1113 | .input-group-hg > .input-group-btn > .btn { 1114 | height: 53px; 1115 | padding: 10px 16px; 1116 | font-size: 22px; 1117 | line-height: 1.318; 1118 | border-radius: 6px; 1119 | } 1120 | select.input-group-hg > .form-control, 1121 | select.input-group-hg > .input-group-addon, 1122 | select.input-group-hg > .input-group-btn > .btn { 1123 | height: 53px; 1124 | line-height: 53px; 1125 | } 1126 | textarea.input-group-hg > .form-control, 1127 | textarea.input-group-hg > .input-group-addon, 1128 | textarea.input-group-hg > .input-group-btn > .btn, 1129 | select[multiple].input-group-hg > .form-control, 1130 | select[multiple].input-group-hg > .input-group-addon, 1131 | select[multiple].input-group-hg > .input-group-btn > .btn { 1132 | height: auto; 1133 | } 1134 | .input-group-lg > .form-control, 1135 | .input-group-lg > .input-group-addon, 1136 | .input-group-lg > .input-group-btn > .btn { 1137 | height: 45px; 1138 | padding: 10px 15px; 1139 | font-size: 17px; 1140 | line-height: 1.235; 1141 | border-radius: 6px; 1142 | } 1143 | select.input-group-lg > .form-control, 1144 | select.input-group-lg > .input-group-addon, 1145 | select.input-group-lg > .input-group-btn > .btn { 1146 | height: 45px; 1147 | line-height: 45px; 1148 | } 1149 | textarea.input-group-lg > .form-control, 1150 | textarea.input-group-lg > .input-group-addon, 1151 | textarea.input-group-lg > .input-group-btn > .btn, 1152 | select[multiple].input-group-lg > .form-control, 1153 | select[multiple].input-group-lg > .input-group-addon, 1154 | select[multiple].input-group-lg > .input-group-btn > .btn { 1155 | height: auto; 1156 | } 1157 | .input-group-sm > .form-control, 1158 | .input-group-sm > .input-group-addon, 1159 | .input-group-sm > .input-group-btn > .btn { 1160 | height: 35px; 1161 | padding: 6px 10px; 1162 | font-size: 13px; 1163 | line-height: 1.462; 1164 | border-radius: 6px; 1165 | } 1166 | select.input-group-sm > .form-control, 1167 | select.input-group-sm > .input-group-addon, 1168 | select.input-group-sm > .input-group-btn > .btn { 1169 | height: 35px; 1170 | line-height: 35px; 1171 | } 1172 | textarea.input-group-sm > .form-control, 1173 | textarea.input-group-sm > .input-group-addon, 1174 | textarea.input-group-sm > .input-group-btn > .btn, 1175 | select[multiple].input-group-sm > .form-control, 1176 | select[multiple].input-group-sm > .input-group-addon, 1177 | select[multiple].input-group-sm > .input-group-btn > .btn { 1178 | height: auto; 1179 | } 1180 | .input-group-addon { 1181 | padding: 10px 12px; 1182 | font-size: 15px; 1183 | color: #ffffff; 1184 | text-align: center; 1185 | background-color: #bdc3c7; 1186 | border: 1px solid #bdc3c7; 1187 | border-radius: 6px; 1188 | -webkit-transition: border .25s linear, color .25s linear, background-color .25s linear; 1189 | transition: border .25s linear, color .25s linear, background-color .25s linear; 1190 | } 1191 | .input-group-hg .input-group-addon, 1192 | .input-group-lg .input-group-addon, 1193 | .input-group-sm .input-group-addon { 1194 | line-height: 1; 1195 | } 1196 | .input-group .form-control:first-child, 1197 | .input-group-addon:first-child, 1198 | .input-group-btn:first-child > .btn, 1199 | .input-group-btn:first-child > .dropdown-toggle, 1200 | .input-group-btn:last-child > .btn:not(:last-child):not(.dropdown-toggle) { 1201 | border-bottom-right-radius: 0; 1202 | border-top-right-radius: 0; 1203 | } 1204 | .input-group .form-control:last-child, 1205 | .input-group-addon:last-child, 1206 | .input-group-btn:last-child > .btn, 1207 | .input-group-btn:last-child > .dropdown-toggle, 1208 | .input-group-btn:first-child > .btn:not(:first-child) { 1209 | border-bottom-left-radius: 0; 1210 | border-top-left-radius: 0; 1211 | } 1212 | .form-group.focus .input-group-addon, 1213 | .input-group.focus .input-group-addon { 1214 | background-color: #428bca; 1215 | border-color: #428bca; 1216 | } 1217 | .form-group.focus .input-group-btn > .btn-default + .btn-default, 1218 | .input-group.focus .input-group-btn > .btn-default + .btn-default { 1219 | border-left-color: #428bca; 1220 | } 1221 | .form-group.focus .input-group-btn .btn, 1222 | .input-group.focus .input-group-btn .btn { 1223 | border-color: #428bca; 1224 | background-color: #ffffff; 1225 | color: #428bca; 1226 | } 1227 | .form-group.focus .input-group-btn .btn-default, 1228 | .input-group.focus .input-group-btn .btn-default { 1229 | color: #ffffff; 1230 | background-color: #428bca; 1231 | } 1232 | .form-group.focus .input-group-btn .btn-default:hover, 1233 | .input-group.focus .input-group-btn .btn-default:hover, 1234 | .form-group.focus .input-group-btn .btn-default:focus, 1235 | .input-group.focus .input-group-btn .btn-default:focus, 1236 | .form-group.focus .input-group-btn .btn-default:active, 1237 | .input-group.focus .input-group-btn .btn-default:active, 1238 | .form-group.focus .input-group-btn .btn-default.active, 1239 | .input-group.focus .input-group-btn .btn-default.active, 1240 | .open .dropdown-toggle.form-group.focus .input-group-btn .btn-default, 1241 | .open .dropdown-toggle.input-group.focus .input-group-btn .btn-default { 1242 | color: #ffffff; 1243 | background-color: #3276b1; 1244 | border-color: #3276b1; 1245 | } 1246 | .form-group.focus .input-group-btn .btn-default:active, 1247 | .input-group.focus .input-group-btn .btn-default:active, 1248 | .form-group.focus .input-group-btn .btn-default.active, 1249 | .input-group.focus .input-group-btn .btn-default.active, 1250 | .open .dropdown-toggle.form-group.focus .input-group-btn .btn-default, 1251 | .open .dropdown-toggle.input-group.focus .input-group-btn .btn-default { 1252 | background: #428bca; 1253 | border-color: #428bca; 1254 | } 1255 | .form-group.focus .input-group-btn .btn-default.disabled, 1256 | .input-group.focus .input-group-btn .btn-default.disabled, 1257 | .form-group.focus .input-group-btn .btn-default[disabled], 1258 | .input-group.focus .input-group-btn .btn-default[disabled], 1259 | fieldset[disabled] .form-group.focus .input-group-btn .btn-default, 1260 | fieldset[disabled] .input-group.focus .input-group-btn .btn-default, 1261 | .form-group.focus .input-group-btn .btn-default.disabled:hover, 1262 | .input-group.focus .input-group-btn .btn-default.disabled:hover, 1263 | .form-group.focus .input-group-btn .btn-default[disabled]:hover, 1264 | .input-group.focus .input-group-btn .btn-default[disabled]:hover, 1265 | fieldset[disabled] .form-group.focus .input-group-btn .btn-default:hover, 1266 | fieldset[disabled] .input-group.focus .input-group-btn .btn-default:hover, 1267 | .form-group.focus .input-group-btn .btn-default.disabled:focus, 1268 | .input-group.focus .input-group-btn .btn-default.disabled:focus, 1269 | .form-group.focus .input-group-btn .btn-default[disabled]:focus, 1270 | .input-group.focus .input-group-btn .btn-default[disabled]:focus, 1271 | fieldset[disabled] .form-group.focus .input-group-btn .btn-default:focus, 1272 | fieldset[disabled] .input-group.focus .input-group-btn .btn-default:focus, 1273 | .form-group.focus .input-group-btn .btn-default.disabled:active, 1274 | .input-group.focus .input-group-btn .btn-default.disabled:active, 1275 | .form-group.focus .input-group-btn .btn-default[disabled]:active, 1276 | .input-group.focus .input-group-btn .btn-default[disabled]:active, 1277 | fieldset[disabled] .form-group.focus .input-group-btn .btn-default:active, 1278 | fieldset[disabled] .input-group.focus .input-group-btn .btn-default:active, 1279 | .form-group.focus .input-group-btn .btn-default.disabled.active, 1280 | .input-group.focus .input-group-btn .btn-default.disabled.active, 1281 | .form-group.focus .input-group-btn .btn-default[disabled].active, 1282 | .input-group.focus .input-group-btn .btn-default[disabled].active, 1283 | fieldset[disabled] .form-group.focus .input-group-btn .btn-default.active, 1284 | fieldset[disabled] .input-group.focus .input-group-btn .btn-default.active { 1285 | background-color: #428bca; 1286 | border-color: #428bca; 1287 | } 1288 | .input-group-btn .btn { 1289 | background-color: #ffffff; 1290 | border: 2px solid #bdc3c7; 1291 | color: #bdc3c7; 1292 | line-height: 18px; 1293 | } 1294 | .input-group-btn .btn-default { 1295 | color: #ffffff; 1296 | background-color: #bdc3c7; 1297 | } 1298 | .input-group-btn .btn-default:hover, 1299 | .input-group-btn .btn-default:focus, 1300 | .input-group-btn .btn-default:active, 1301 | .input-group-btn .btn-default.active, 1302 | .open .dropdown-toggle.input-group-btn .btn-default { 1303 | color: #ffffff; 1304 | background-color: #cacfd2; 1305 | border-color: #cacfd2; 1306 | } 1307 | .input-group-btn .btn-default:active, 1308 | .input-group-btn .btn-default.active, 1309 | .open .dropdown-toggle.input-group-btn .btn-default { 1310 | background: #a1a6a9; 1311 | border-color: #a1a6a9; 1312 | } 1313 | .input-group-btn .btn-default.disabled, 1314 | .input-group-btn .btn-default[disabled], 1315 | fieldset[disabled] .input-group-btn .btn-default, 1316 | .input-group-btn .btn-default.disabled:hover, 1317 | .input-group-btn .btn-default[disabled]:hover, 1318 | fieldset[disabled] .input-group-btn .btn-default:hover, 1319 | .input-group-btn .btn-default.disabled:focus, 1320 | .input-group-btn .btn-default[disabled]:focus, 1321 | fieldset[disabled] .input-group-btn .btn-default:focus, 1322 | .input-group-btn .btn-default.disabled:active, 1323 | .input-group-btn .btn-default[disabled]:active, 1324 | fieldset[disabled] .input-group-btn .btn-default:active, 1325 | .input-group-btn .btn-default.disabled.active, 1326 | .input-group-btn .btn-default[disabled].active, 1327 | fieldset[disabled] .input-group-btn .btn-default.active { 1328 | background-color: #bdc3c7; 1329 | border-color: #bdc3c7; 1330 | } 1331 | .input-group-hg .input-group-btn .btn { 1332 | line-height: 31px; 1333 | } 1334 | .input-group-lg .input-group-btn .btn { 1335 | line-height: 21px; 1336 | } 1337 | .input-group-sm .input-group-btn .btn { 1338 | line-height: 19px; 1339 | } 1340 | .input-group-btn:first-child > .btn { 1341 | border-right-width: 0; 1342 | margin-right: -2px; 1343 | } 1344 | .input-group-btn:last-child > .btn { 1345 | border-left-width: 0; 1346 | margin-left: -2px; 1347 | } 1348 | .input-group-btn > .btn-default + .btn-default { 1349 | border-left: 2px solid #bdc3c7; 1350 | } 1351 | .input-group-btn > .btn:first-child + .btn .caret { 1352 | margin-left: 0; 1353 | } 1354 | .input-group-rounded .input-group-btn + .form-control, 1355 | .input-group-rounded .input-group-btn:last-child .btn { 1356 | border-bottom-right-radius: 20px; 1357 | border-top-right-radius: 20px; 1358 | } 1359 | .input-group-hg.input-group-rounded .input-group-btn + .form-control, 1360 | .input-group-hg.input-group-rounded .input-group-btn:last-child .btn { 1361 | border-bottom-right-radius: 27px; 1362 | border-top-right-radius: 27px; 1363 | } 1364 | .input-group-lg.input-group-rounded .input-group-btn + .form-control, 1365 | .input-group-lg.input-group-rounded .input-group-btn:last-child .btn { 1366 | border-bottom-right-radius: 25px; 1367 | border-top-right-radius: 25px; 1368 | } 1369 | .input-group-rounded .form-control:first-child, 1370 | .input-group-rounded .input-group-btn:first-child .btn { 1371 | border-bottom-left-radius: 20px; 1372 | border-top-left-radius: 20px; 1373 | } 1374 | .input-group-hg.input-group-rounded .form-control:first-child, 1375 | .input-group-hg.input-group-rounded .input-group-btn:first-child .btn { 1376 | border-bottom-left-radius: 27px; 1377 | border-top-left-radius: 27px; 1378 | } 1379 | .input-group-lg.input-group-rounded .form-control:first-child, 1380 | .input-group-lg.input-group-rounded .input-group-btn:first-child .btn { 1381 | border-bottom-left-radius: 25px; 1382 | border-top-left-radius: 25px; 1383 | } 1384 | .input-group-rounded .input-group-btn + .form-control { 1385 | padding-left: 0; 1386 | } 1387 | .checkbox, 1388 | .radio { 1389 | margin-bottom: 12px; 1390 | padding-left: 32px; 1391 | position: relative; 1392 | -webkit-transition: color 0.25s linear; 1393 | transition: color 0.25s linear; 1394 | font-size: 14px; 1395 | line-height: 1.5; 1396 | } 1397 | .checkbox input, 1398 | .radio input { 1399 | outline: none !important; 1400 | display: none; 1401 | } 1402 | .checkbox .icons, 1403 | .radio .icons { 1404 | color: #bdc3c7; 1405 | display: block; 1406 | height: 20px; 1407 | left: 0; 1408 | position: absolute; 1409 | top: 0; 1410 | width: 20px; 1411 | text-align: center; 1412 | line-height: 21px; 1413 | font-size: 20px; 1414 | cursor: pointer; 1415 | -webkit-transition: color 0.25s linear; 1416 | transition: color 0.25s linear; 1417 | } 1418 | .checkbox .icons .first-icon, 1419 | .radio .icons .first-icon, 1420 | .checkbox .icons .second-icon, 1421 | .radio .icons .second-icon { 1422 | display: inline-table; 1423 | position: absolute; 1424 | left: 0; 1425 | top: 0; 1426 | background-color: transparent; 1427 | margin: 0; 1428 | opacity: 1; 1429 | filter: alpha(opacity=100); 1430 | } 1431 | .checkbox .icons .second-icon, 1432 | .radio .icons .second-icon { 1433 | opacity: 0; 1434 | filter: alpha(opacity=0); 1435 | } 1436 | .checkbox:hover, 1437 | .radio:hover { 1438 | -webkit-transition: color 0.25s linear; 1439 | transition: color 0.25s linear; 1440 | } 1441 | .checkbox:hover .first-icon, 1442 | .radio:hover .first-icon { 1443 | opacity: 0; 1444 | filter: alpha(opacity=0); 1445 | } 1446 | .checkbox:hover .second-icon, 1447 | .radio:hover .second-icon { 1448 | opacity: 1; 1449 | filter: alpha(opacity=100); 1450 | } 1451 | .checkbox.checked, 1452 | .radio.checked { 1453 | color: #428bca; 1454 | } 1455 | .checkbox.checked .first-icon, 1456 | .radio.checked .first-icon { 1457 | opacity: 0; 1458 | filter: alpha(opacity=0); 1459 | } 1460 | .checkbox.checked .second-icon, 1461 | .radio.checked .second-icon { 1462 | opacity: 1; 1463 | filter: alpha(opacity=100); 1464 | color: #428bca; 1465 | -webkit-transition: color 0.25s linear; 1466 | transition: color 0.25s linear; 1467 | } 1468 | .checkbox.disabled, 1469 | .radio.disabled { 1470 | cursor: default; 1471 | color: #e6e8ea; 1472 | } 1473 | .checkbox.disabled .icons, 1474 | .radio.disabled .icons { 1475 | color: #e6e8ea; 1476 | } 1477 | .checkbox.disabled .first-icon, 1478 | .radio.disabled .first-icon { 1479 | opacity: 1; 1480 | filter: alpha(opacity=100); 1481 | } 1482 | .checkbox.disabled .second-icon, 1483 | .radio.disabled .second-icon { 1484 | opacity: 0; 1485 | filter: alpha(opacity=0); 1486 | } 1487 | .checkbox.disabled.checked .icons, 1488 | .radio.disabled.checked .icons { 1489 | color: #e6e8ea; 1490 | } 1491 | .checkbox.disabled.checked .first-icon, 1492 | .radio.disabled.checked .first-icon { 1493 | opacity: 0; 1494 | filter: alpha(opacity=0); 1495 | } 1496 | .checkbox.disabled.checked .second-icon, 1497 | .radio.disabled.checked .second-icon { 1498 | opacity: 1; 1499 | filter: alpha(opacity=100); 1500 | color: #e6e8ea; 1501 | } 1502 | .checkbox.primary .icons, 1503 | .radio.primary .icons { 1504 | color: #34495e; 1505 | } 1506 | .checkbox.primary.checked, 1507 | .radio.primary.checked { 1508 | color: #428bca; 1509 | } 1510 | .checkbox.primary.checked .icons, 1511 | .radio.primary.checked .icons { 1512 | color: #428bca; 1513 | } 1514 | .checkbox.primary.disabled, 1515 | .radio.primary.disabled { 1516 | cursor: default; 1517 | color: #bdc3c7; 1518 | } 1519 | .checkbox.primary.disabled .icons, 1520 | .radio.primary.disabled .icons { 1521 | color: #bdc3c7; 1522 | } 1523 | .checkbox.primary.disabled.checked .icons, 1524 | .radio.primary.disabled.checked .icons { 1525 | color: #bdc3c7; 1526 | } 1527 | .radio + .radio, 1528 | .checkbox + .checkbox { 1529 | margin-top: 10px; 1530 | } 1531 | .tooltip { 1532 | font-size: 14px; 1533 | line-height: 1.286; 1534 | } 1535 | .tooltip.in { 1536 | opacity: 1; 1537 | } 1538 | .tooltip.top { 1539 | padding-bottom: 9px; 1540 | } 1541 | .tooltip.top .tooltip-arrow { 1542 | border-top-color: #34495e; 1543 | border-width: 9px 9px 0; 1544 | bottom: 0; 1545 | margin-left: -9px; 1546 | } 1547 | .tooltip.right .tooltip-arrow { 1548 | border-right-color: #34495e; 1549 | border-width: 9px 9px 9px 0; 1550 | margin-top: -9px; 1551 | left: -3px; 1552 | } 1553 | .tooltip.bottom { 1554 | padding-top: 8px; 1555 | } 1556 | .tooltip.bottom .tooltip-arrow { 1557 | border-bottom-color: #34495e; 1558 | border-width: 0 9px 9px; 1559 | margin-left: -9px; 1560 | top: -1px; 1561 | } 1562 | .tooltip.left .tooltip-arrow { 1563 | border-left-color: #34495e; 1564 | border-width: 9px 0 9px 9px; 1565 | margin-top: -9px; 1566 | right: -3px; 1567 | } 1568 | .tooltip-inner { 1569 | background-color: #34495e; 1570 | line-height: 1.286; 1571 | padding: 12px 12px; 1572 | text-align: center; 1573 | width: 183px; 1574 | border-radius: 6px; 1575 | } 1576 | @media only screen and (-webkit-min-device-pixel-ratio: 2), only screen and (-webkit-min-device-pixel-ratio: 1.5), only screen and (-moz-min-device-pixel-ratio: 2), only screen and (-o-min-device-pixel-ratio: 3/2), only screen and (-o-min-device-pixel-ratio: 2/1), only screen and (-moz-min-device-pixel-ratio: 1.5), only screen and (min-device-pixel-ratio: 1.5), only screen and (min-device-pixel-ratio: 2) { 1577 | .login { 1578 | background-image: url(../images/login/imac-2x.png); 1579 | } 1580 | } 1581 | -------------------------------------------------------------------------------- /res/css/style.css: -------------------------------------------------------------------------------- 1 | @CHARSET "UTF-8"; 2 | .clear { 3 | clear: both; 4 | } 5 | html,body{margin: 0;padding: 0} 6 | #nav { 7 | filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=90); 8 | opacity: 0.9; 9 | background: #FFF; 10 | padding: 0; 11 | margin:0; 12 | background: #446CB3; 13 | color:white; 14 | } 15 | #menu{ width: 80%; 16 | margin: 0 auto 17 | } 18 | #nav ul { 19 | margin: 0px auto; 20 | padding: 0px; 21 | list-style-type: none; 22 | } 23 | #nav ul li { 24 | float: left; 25 | padding: 10px 10px; 26 | font-size: 14px; 27 | border-right: 1px solid rgba(0, 0, 0, 0.1); 28 | box-shadow: 1px 0 0 rgba(255, 255, 255, 0.11); 29 | font-weight: bold; 30 | text-align: center; 31 | } 32 | .border_first{ 33 | border-left: 1px solid rgba(0, 0, 0, 0.1); 34 | box-shadow: 1px 0 0 rgba(255, 255, 255, 0.11); 35 | } 36 | 37 | #nav ul#left_submenu li:HOVER{ 38 | background: white; 39 | } 40 | #nav ul#left_submenu li:HOVER a{ 41 | color:#446CB3 42 | } 43 | #nav ul li a{display: inline-block;width:50px; color:white;} 44 | 45 | #network-tb td{ 46 | vertical-align: top 47 | } 48 | 49 | #aside { 50 | width:33%; 51 | min-width: 450px; 52 | } 53 | .w120{width: 120px} 54 | .w140{width: 140px} 55 | .w150{width: 150px} 56 | 57 | #connect_status{padding:6px;color: white} 58 | 59 | table {border-collapse: collapse;border-spacing: 0;} 60 | .tb_1{border:1px solid #cccccc;table-layout:fixed;word-break:break-all;width: 100%;background:#ffffff;margin-bottom:5px} 61 | .tb_1 caption{text-align: center;background: #F0F4F6;font-weight: bold;padding-top: 5px;height: 25px;border:1px solid #cccccc;border-bottom:none} 62 | .tb_1 a{margin:0 3px 0 3px} 63 | .tb_1 tr th,.tb_1 tr td{padding: 3px;border:1px solid #cccccc;line-height:20px} 64 | .tb_1 thead tr th{font-weight:bold;text-align: center;background:#e3eaee} 65 | .tb_1 tbody tr th{text-align: right;background:#f0f4f6;padding-right:5px} 66 | .tb_1 tfoot{color:#cccccc} 67 | .td_c td{text-align: center} 68 | .td_r td{text-align: right} 69 | .t_c{text-align: center !important;} 70 | .t_r{text-align: right !important;} 71 | .t_l{text-align: left !important;} 72 | 73 | .panel_1{background: #e5e2eb;padding:4px} 74 | #network_filter_form fieldset{padding-right:3px} 75 | 76 | #tb_network{font-size: 11px;margin-top: -8px;} 77 | #tb_network td{overflow: auto;cursor: pointer;} 78 | #right_content{margin-top:5px} 79 | #right_content table th,#right_content table td{font-size:11px;vertical-align: top} 80 | #right_content pre{border:none} 81 | 82 | #div_tb_network_list {overflow:auto;max-height: 1000px;min-height:450px} 83 | tr.selected{background: #ccffee;} 84 | .right{float: right} 85 | .left{float:left} 86 | .c{clear: both} 87 | .hide{display:none} 88 | #network_list_div tbody th{text-align: right;} 89 | .td_ul{list-style: none inside;padding: 0;margin: 0} 90 | .td_ul li{list-style: none inside;} 91 | 92 | td.td_has_sub{padding: 0 !important;} 93 | td.td_has_sub .tb_1{margin: 0;border: 0;} 94 | 95 | #bd0{margin:0 auto;width:80%} 96 | .form-control{height: 26px;padding:0} 97 | fieldset { 98 | border-radius:10px; 99 | border: 1px solid gray 100 | } 101 | tr.replay{ 102 | color:blue; 103 | } 104 | 105 | .res_td_body{ 106 | max-height: 120px; 107 | overflow: scroll; 108 | } 109 | 110 | .div_b_i{ 111 | display: inline-block; 112 | } 113 | 114 | .login-screen { 115 | min-height: 400px; 116 | width:300px; 117 | margin: 0 auto; 118 | text-align: center; 119 | } 120 | 121 | #bd{ 122 | margin: 0 auto; 123 | width:80%; 124 | padding-top: 30px; 125 | } 126 | 127 | .form_0 table{width: 100%} 128 | .form_0 th{text-align: right;width: 180px;vertical-align:top;} 129 | .form_0 input[type=text]{width: 100%;margin-bottom:2px} 130 | .form_0 td.last{width: 90px;padding-left: 10px} 131 | .oneline{height: 20px;overflow: hidden;} 132 | 133 | img {margin-bottom: -3px} -------------------------------------------------------------------------------- /res/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hidu/pproxy/cc3978cfd1b726740b9d6925c7cd2a9cdf43c998/res/img/favicon.ico -------------------------------------------------------------------------------- /res/img/folder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hidu/pproxy/cc3978cfd1b726740b9d6925c7cd2a9cdf43c998/res/img/folder.png -------------------------------------------------------------------------------- /res/img/list-add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hidu/pproxy/cc3978cfd1b726740b9d6925c7cd2a9cdf43c998/res/img/list-add.png -------------------------------------------------------------------------------- /res/img/list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hidu/pproxy/cc3978cfd1b726740b9d6925c7cd2a9cdf43c998/res/img/list.png -------------------------------------------------------------------------------- /res/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hidu/pproxy/cc3978cfd1b726740b9d6925c7cd2a9cdf43c998/res/img/logo.png -------------------------------------------------------------------------------- /res/img/text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hidu/pproxy/cc3978cfd1b726740b9d6925c7cd2a9cdf43c998/res/img/text.png -------------------------------------------------------------------------------- /res/img/view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hidu/pproxy/cc3978cfd1b726740b9d6925c7cd2a9cdf43c998/res/img/view.png -------------------------------------------------------------------------------- /res/js/base64.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Base64 encode / decode 4 | * http://www.webtoolkit.info/ 5 | * 6 | **/ 7 | var Base64 = { 8 | 9 | // private property 10 | _keyStr : "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=", 11 | 12 | // public method for encoding 13 | encode : function (input) { 14 | var output = ""; 15 | var chr1, chr2, chr3, enc1, enc2, enc3, enc4; 16 | var i = 0; 17 | 18 | input = Base64._utf8_encode(input); 19 | 20 | while (i < input.length) { 21 | 22 | chr1 = input.charCodeAt(i++); 23 | chr2 = input.charCodeAt(i++); 24 | chr3 = input.charCodeAt(i++); 25 | 26 | enc1 = chr1 >> 2; 27 | enc2 = ((chr1 & 3) << 4) | (chr2 >> 4); 28 | enc3 = ((chr2 & 15) << 2) | (chr3 >> 6); 29 | enc4 = chr3 & 63; 30 | 31 | if (isNaN(chr2)) { 32 | enc3 = enc4 = 64; 33 | } else if (isNaN(chr3)) { 34 | enc4 = 64; 35 | } 36 | 37 | output = output + 38 | this._keyStr.charAt(enc1) + this._keyStr.charAt(enc2) + 39 | this._keyStr.charAt(enc3) + this._keyStr.charAt(enc4); 40 | 41 | } 42 | 43 | return output; 44 | }, 45 | 46 | // public method for decoding 47 | decode : function (input) { 48 | var output = ""; 49 | var chr1, chr2, chr3; 50 | var enc1, enc2, enc3, enc4; 51 | var i = 0; 52 | 53 | input = input.replace(/[^A-Za-z0-9\+\/\=]/g, ""); 54 | 55 | while (i < input.length) { 56 | 57 | enc1 = this._keyStr.indexOf(input.charAt(i++)); 58 | enc2 = this._keyStr.indexOf(input.charAt(i++)); 59 | enc3 = this._keyStr.indexOf(input.charAt(i++)); 60 | enc4 = this._keyStr.indexOf(input.charAt(i++)); 61 | 62 | chr1 = (enc1 << 2) | (enc2 >> 4); 63 | chr2 = ((enc2 & 15) << 4) | (enc3 >> 2); 64 | chr3 = ((enc3 & 3) << 6) | enc4; 65 | 66 | output = output + String.fromCharCode(chr1); 67 | 68 | if (enc3 != 64) { 69 | output = output + String.fromCharCode(chr2); 70 | } 71 | if (enc4 != 64) { 72 | output = output + String.fromCharCode(chr3); 73 | } 74 | 75 | } 76 | 77 | output = Base64._utf8_decode(output); 78 | 79 | return output; 80 | 81 | }, 82 | 83 | // private method for UTF-8 encoding 84 | _utf8_encode : function (string) { 85 | string = string.replace(/\r\n/g,"\n"); 86 | var utftext = ""; 87 | 88 | for (var n = 0; n < string.length; n++) { 89 | 90 | var c = string.charCodeAt(n); 91 | 92 | if (c < 128) { 93 | utftext += String.fromCharCode(c); 94 | } 95 | else if((c > 127) && (c < 2048)) { 96 | utftext += String.fromCharCode((c >> 6) | 192); 97 | utftext += String.fromCharCode((c & 63) | 128); 98 | } 99 | else { 100 | utftext += String.fromCharCode((c >> 12) | 224); 101 | utftext += String.fromCharCode(((c >> 6) & 63) | 128); 102 | utftext += String.fromCharCode((c & 63) | 128); 103 | } 104 | 105 | } 106 | 107 | return utftext; 108 | }, 109 | 110 | // private method for UTF-8 decoding 111 | _utf8_decode : function (utftext) { 112 | var string = ""; 113 | var i = 0; 114 | var c = c1 = c2 = 0; 115 | 116 | while ( i < utftext.length ) { 117 | 118 | c = utftext.charCodeAt(i); 119 | 120 | if (c < 128) { 121 | string += String.fromCharCode(c); 122 | i++; 123 | } 124 | else if((c > 191) && (c < 224)) { 125 | c2 = utftext.charCodeAt(i+1); 126 | string += String.fromCharCode(((c & 31) << 6) | (c2 & 63)); 127 | i += 2; 128 | } 129 | else { 130 | c2 = utftext.charCodeAt(i+1); 131 | c3 = utftext.charCodeAt(i+2); 132 | string += String.fromCharCode(((c & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63)); 133 | i += 3; 134 | } 135 | 136 | } 137 | 138 | return string; 139 | } 140 | 141 | } -------------------------------------------------------------------------------- /res/js/default.js: -------------------------------------------------------------------------------- 1 | function pproxy_tab_sup(target){ 2 | $(target).find("textarea").bind('keydown', function(e) { 3 | if (e.keyCode == 9 ) { 4 | e.preventDefault(); 5 | this.setRangeText('\t'); 6 | this.selectionEnd = ++this.selectionStart; 7 | } 8 | }); 9 | } -------------------------------------------------------------------------------- /res/js/session.js: -------------------------------------------------------------------------------- 1 | var socket = io.connect(); 2 | var connectNum=0; 3 | var pproxy_colors=["#FFFFFF","#CCFFFF","#FFCCCC","#99CCCC","996699","#CC9999","#0099CC","#FFFF66","#336633","#99CC00"] 4 | 5 | var ip_colors={} 6 | var pproxy_session_max_len=0; 7 | var pproxy_table_max_row=2000; 8 | 9 | 10 | var pproxy_req_list=[]; 11 | 12 | if(window.localStorage){ 13 | pproxy_req_list=$.parseJSON(window.localStorage["reqs"]||"[]") 14 | pproxy_session_max_len=1000; 15 | } 16 | 17 | function pproxy_show_reqs_from_local(){ 18 | for(var i=0;i0){ 25 | window.localStorage["reqs"]=JSON.stringify(pproxy_req_list); 26 | } 27 | } 28 | 29 | function pproxy_save_req_local(dataStr64){ 30 | if(pproxy_session_max_len<1){ 31 | return; 32 | } 33 | var n=pproxy_req_list.push(dataStr64) 34 | if(n>pproxy_session_max_len){ 35 | pproxy_req_list.shift(); 36 | } 37 | } 38 | 39 | function pporxy_session_clean(){ 40 | pproxy_req_list=[]; 41 | $('#tb_network tbody').empty(); 42 | } 43 | 44 | function pproxy_log(msg){ 45 | $("#log_div").append("
"+(new Date().toString())+":"+msg+"
"); 46 | } 47 | 48 | 49 | function pproxy_net_local_filter(host,path){ 50 | if(!_pproxy_filter(host,$("#net_local_host").val())){ 51 | return false; 52 | } 53 | if(!_pproxy_filter(path,$("#net_local_path").val())){ 54 | return false; 55 | } 56 | return true; 57 | } 58 | 59 | function _pproxy_filter(kw,kwstr){ 60 | if(kwstr==""){ 61 | return true; 62 | } 63 | var kws=(kwstr+"").split("|"); 64 | kw+=""; 65 | var tmp=[] 66 | for(var i in kws){ 67 | var _kw=$.trim(kws[i]+""); 68 | if(_kw!=""){ 69 | tmp.push(_kw); 70 | } 71 | } 72 | if(tmp.length==0){ 73 | return true; 74 | } 75 | for (var i in tmp){ 76 | if(kw.indexOf(tmp[i])!=-1){ 77 | return true; 78 | } 79 | } 80 | return false; 81 | } 82 | 83 | socket.on('connect', function() { 84 | connectNum++ 85 | if(connectNum>1){ 86 | pproxy_log("ws error.connectNum="+connectNum); 87 | socket.emit("disconnect"); 88 | return; 89 | } 90 | $("#connect_status").html("online") 91 | $("#network_filter_form").change(); 92 | }); 93 | 94 | socket.on("disconnect", function() { 95 | connectNum--; 96 | $("#connect_status").html("offline"); 97 | }); 98 | 99 | socket.on("hello",function(data){ 100 | console && console.log(new Date().toString(),data); 101 | }); 102 | 103 | function pproxy_getColor(addr){ 104 | var info=addr.split(":"); 105 | if(info.length!=2){ 106 | return "#FFFFFF"; 107 | } 108 | var ip=info[0]; 109 | var color=ip_colors[ip]; 110 | if(!color){ 111 | var l=0; 112 | for(var _t in ip_colors){ 113 | l++; 114 | } 115 | color=pproxy_colors[l% pproxy_colors.length]; 116 | ip_colors[ip]=color; 117 | } 118 | return color 119 | } 120 | 121 | 122 | function pproxy_show_req(data) { 123 | console && console.log("pproxy_show_req:",data) 124 | // var dataStr=Base64.decode(dataStr64+""); 125 | // var data={}; 126 | // try{ 127 | // data=$.parseJSON(dataStr) 128 | // }catch(e){ 129 | // console && console.log("parseJSON_error-pproxy_show_req",e,"datastr:",dataStr) 130 | // return 131 | // } 132 | 133 | // console && console.log("req", data) 134 | var html="0){ 144 | html+="class='"+cls.join(" ")+"' "; 145 | } 146 | html+=" data-ip='"+data["client_ip"]+"' data-host='"+h(data["host"])+"' data-path='"+h(data["path"])+"'>" 147 | + "" + data["sid"] + "" 148 | + "
" + data["host"] + "
" + 149 | "
" +data["method"]+" "+ h(data["path"])+ "
" + 150 | ""; 151 | var tb=$("#tb_network tbody"); 152 | tb.prepend(html); 153 | 154 | if(Math.random()*100>95 && tb.find("tr").size()>pproxy_table_max_row*1.5){ 155 | tb.find("tr:gt("+pproxy_table_max_row+")").each(function(){ 156 | $(this).remove(); 157 | }); 158 | } 159 | } 160 | 161 | 162 | socket.on("req",function(data){ 163 | console && console.log("on.req","data:",data); 164 | pproxy_save_req_local(data); 165 | pproxy_show_req(data); 166 | }); 167 | 168 | 169 | socket.on("user_num", function(data) { 170 | $("#user_num_online").html(data); 171 | }); 172 | 173 | socket.on("res", 174 | function(data) { 175 | console && console.log("on.res","data:",data) 176 | // var dataStr=Base64.decode(dataStr64+""); 177 | // try{ 178 | // var data=$.parseJSON(dataStr) 179 | // }catch(e){ 180 | // console && console.log("parseJSON_error on.res:",e) 181 | // return 182 | // } 183 | // console && console.log(data) 184 | var req = data["req"]; 185 | var res = data["res"]; 186 | var html=""; 187 | if(req){ 188 | var re_do_str=req["schema"]=="http"?(" 
replay"):""; 189 | 190 | html += "
"; 191 | html += "" 192 | if (req["url_origin"]!=req["url"]) { 193 | html += ""; 194 | } 195 | if (req["msg"]) { 196 | html += ""; 197 | } 198 | html += "" + 199 | ""; 201 | html += pproxy_tr_sub_table(req["form_get"], "get_params"); 202 | html += pproxy_tr_sub_table(req["form_post"], "post_params"); 203 | if (req["dump"]) { 204 | html += ""; 206 | } 207 | html += "
Request"+re_do_str+pproxy_timeformat(req["now"])+"
url" + h(req["url"]) + "  view
origin" + h(req["url_origin"]) + "
msg" + h(req["msg"])+"
proxy_urerremote_addr :  " +req["client_ip"] + "   docid :  "+ req["id"] + 200 | "
req_dump" + h(Base64.decode(req["dump"])).replace(/\n/g, "
") 205 | + "
"; 208 | }else{ 209 | html="



request not exists!
"; 210 | } 211 | var res_link = ""; 212 | 213 | var hideBigBody=false; 214 | 215 | if (res) { 216 | res_link = "view"; 217 | html += "
" 218 | if (res["msg"]) { 219 | html += ""; 220 | } 221 | 222 | if (res["dump"]) { 223 | html += ""; 225 | } 226 | var body_str = Base64.decode(res["body"]) 227 | var isImg = res["header"]["Content-Type"] != undefined 228 | && res["header"]["Content-Type"][0].indexOf("image") > -1; 229 | 230 | var isStatusOk = res["status"] == 200; 231 | 232 | var bd_json = pproxy_parseAsjson(body_str); 233 | 234 | if (bd_json) { 235 | hideBigBody=true; 236 | html += ""; 237 | } 238 | if (isImg) { 239 | hideBigBody=true; 240 | html += ""; 242 | } 243 | if (!isImg || res["body"].length < 1000 || !isStatusOk) { 244 | html += "" + 251 | ""; 254 | } 255 | } 256 | 257 | html += "
Response " + res_link +pproxy_timeformat(res["now"])+ "
msg" + h(res["msg"])+"
res_dump" + h(Base64.decode(res["dump"])).replace(/\n/g, "
") 224 | + "
body_json" + bd_json + "
body_img
body"; 245 | if(res["body"].length>400){ 246 | html+=""; 247 | }else{ 248 | hideBigBody=false; 249 | } 250 | html+= "" + 252 | "
" + h(body_str).replace(/\n/g, "
") + 253 | "
"; 258 | $("#right_content").empty().html(html).hide().slideDown("fast"); 259 | }) 260 | 261 | function pproxy_res_td_body_toggle(){ 262 | $("#res_td_body").toggleClass("res_td_body"); 263 | return false; 264 | } 265 | 266 | function pproxy_timeformat(sec){ 267 | if(!sec ||sec<1000){ 268 | return ""; 269 | } 270 | var numFill=function(num,len){ 271 | num=num+""; 272 | var l=len-num.length; 273 | for(;l>0;l--){ 274 | num="0"+num; 275 | } 276 | return num; 277 | } 278 | var d=new Date() 279 | d.setTime(sec*1000) 280 | return " "+d.getFullYear()+"-"+numFill(d.getMonth()+1,2)+"-"+numFill(d.getDate(),2)+" " 281 | +numFill(d.getHours(),2)+":"+numFill(d.getMinutes(),2)+":"+numFill(d.getSeconds(),2)+""; 282 | } 283 | 284 | 285 | function pproxy_parseAsjson(str) { 286 | try { 287 | str=str+""; 288 | if(str[0]!="{" && str[0]!="["){ 289 | return false; 290 | } 291 | var jsonObj = JSON.parse(str); 292 | if (jsonObj) { 293 | var json_str = JSON.stringify(jsonObj, null, 4); 294 | return "
" + json_str + "
"; 295 | } 296 | } catch (e) { 297 | console.log("pproxy_parseAsjson_error",e); 298 | } 299 | return false; 300 | } 301 | 302 | function pproxy_tr_sub_table(obj, name) { 303 | if (!obj) { 304 | return ""; 305 | } 306 | var html = "" + name + ""; 307 | var i = 0; 308 | var max_key_len=0; 309 | for ( var k in obj) { 310 | max_key_len=Math.max(max_key_len,(k+"").length); 311 | } 312 | for ( var k in obj) { 313 | html += ""; 325 | i++; 326 | } 327 | if (i < 1) { 328 | return ""; 329 | } 330 | html += "
" + k + "
    "; 314 | for ( var i in obj[k]) { 315 | html += "
  • "; 316 | var json_str = pproxy_parseAsjson(obj[k][i]); 317 | if (json_str) { 318 | html += json_str; 319 | } else { 320 | html += h(obj[k][i]); 321 | } 322 | html += "
  • "; 323 | } 324 | html += "
" 331 | return html; 332 | } 333 | 334 | function pproxy_show_response(docid){ 335 | console && console.log("get_response start,docid=", docid); 336 | var loading_msg="loading...docid=" + docid; 337 | var isValidId=(docid+"").length>2; 338 | if(!isValidId){ 339 | loading_msg="https request:no data"; 340 | }else{ 341 | loading_msg+=" reload"; 342 | } 343 | $("#right_content").empty().html("
"+loading_msg+"
"); 344 | if(!isValidId){ 345 | return; 346 | } 347 | socket.emit("get_response", docid); 348 | console.log && console.log("emit get_response,docid=",docid); 349 | } 350 | 351 | function get_response(tr, docid) { 352 | pproxy_show_response(docid); 353 | $(tr).parent("tbody").find("tr").removeClass("selected"); 354 | $(tr).addClass("selected"); 355 | location.hash="req_"+docid; 356 | } 357 | 358 | function bytesToString(bytes) { 359 | var result = ""; 360 | for (var i = 0; i < bytes.length; i++) { 361 | result += String.fromCharCode(parseInt(bytes[i], 2)); 362 | } 363 | return result; 364 | } 365 | 366 | function h(html) { 367 | if(html==""){ 368 | return " "; 369 | } 370 | html = (html+"").replace(/&/g, '&') 371 | .replace(//g, '>') 373 | .replace(/'/g, '´') 374 | .replace(/"/g, '"') 375 | .replace(/\|/g, '¦'); 376 | return html; 377 | } 378 | 379 | $().ready(function() { 380 | $("#network_filter_form input:text").each(function(){ 381 | pproxy_local_save(this,$(this).attr("name")); 382 | }); 383 | var filter_form=$("#network_filter_form"); 384 | filter_form.change(function() { 385 | var form_data = $(this).serialize(); 386 | socket.emit("client_filter", form_data); 387 | }); 388 | 389 | setTimeout(function(){filter_form.change();},600); 390 | setTimeout(function(){filter_form.change();},3000); 391 | 392 | filter_form.find("input:text").keyup(function(){ 393 | filter_form.change(); 394 | }); 395 | 396 | if(location.hash.match(/req_\d+/)){ 397 | var docid=location.hash.substr(5); 398 | setTimeout((function(id){ 399 | return function(){ 400 | pproxy_show_response(id); 401 | } 402 | })(docid),500); 403 | } 404 | 405 | setTimeout(pproxy_show_reqs_from_local,0); 406 | 407 | 408 | $("#net_local_host,#net_local_path").bind("keyup change",function(){ 409 | $('#tb_network tbody tr').each(function(){ 410 | if(pproxy_net_local_filter($(this).data("host"),$(this).data("path"))){ 411 | $(this).removeClass("hide"); 412 | }else{ 413 | $(this).addClass("hide"); 414 | } 415 | }); 416 | }); 417 | }); 418 | 419 | function pproxy_local_save(target,id){ 420 | if(!window.localStorage){ 421 | return; 422 | } 423 | $(target).val(window.localStorage[id]||"").change(function(){ 424 | window.localStorage[id]=$(this).val(); 425 | }); 426 | } -------------------------------------------------------------------------------- /res/private/client_cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDnzCCAoegAwIBAgIJAKBuAUr+T7ouMA0GCSqGSIb3DQEBBQUAMGYxCzAJBgNV 3 | BAYTAlhYMRUwEwYDVQQHDAxEZWZhdWx0IENpdHkxHzAdBgNVBAoMFmdpdGh1Yi5j 4 | b20vaGlkdS9wcHJveHkxHzAdBgNVBAMMFmdpdGh1Yi5jb20vaGlkdS9wcHJveHkw 5 | HhcNMTUwNTAzMTUxMDM3WhcNMjUwNDMwMTUxMDM3WjBmMQswCQYDVQQGEwJYWDEV 6 | MBMGA1UEBwwMRGVmYXVsdCBDaXR5MR8wHQYDVQQKDBZnaXRodWIuY29tL2hpZHUv 7 | cHByb3h5MR8wHQYDVQQDDBZnaXRodWIuY29tL2hpZHUvcHByb3h5MIIBIjANBgkq 8 | hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0JsXsBi/sfgCRqfeSeff1almRidf0wZn 9 | jt/2htKmjzluJ/f0+irK20AOI4aCtLg+zKopJ8aekwqeo2vm8k9Ht8xWcX6g7I6Y 10 | ZNEDVjrFPlcZd+HFfZ2ItH70YAJQInMHqEWrrNCNR/ndHnlQg+HUrA+od3HjiegQ 11 | u/OmVVPaSGiytAFc9QzqCuYPtKOZvCn6KBPX3mXbrBnWf0ASpz7ndRPRfYP0MU0u 12 | t+AfNcj4/lDvSDOKFVbUSe6Prx4cv4HkcQuPngzqHO4el6ABf0ZWJjBwvZFWW14X 13 | EJ1Jx2ODJmY+Aq3/yq9Z9MjnuVBs7QjoHIqUMuzWeC7MSBZUC5x5SQIDAQABo1Aw 14 | TjAdBgNVHQ4EFgQUXHNOovxL+NAVXguLVvH+c6Up+YUwHwYDVR0jBBgwFoAUXHNO 15 | ovxL+NAVXguLVvH+c6Up+YUwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQUFAAOC 16 | AQEAm1KOjuRKJRjUgSckYf1CpGrrSK+YRycspPfTg8EZ10TER9ojGj8xF5QYWg50 17 | DYgFqx5bWLmytIzf+Ob1Ai6w19i+LvuKo2+vAjBm8QfVrqr/wdzDIhBCj+viY/ib 18 | 7OGTEjAPMk149cy/mu+8ta63dGrX15bIBLfS42ntnTlfNVi2UdtSt0HbkBhzu8lo 19 | wG3HTwWbFLy8TH/mhTuaFE9CqLzYUA+Lyk2/JDKnhHRpkDPgjiZNidQfDP80IMN/ 20 | CAlc/f1kBV1FHOUSQgzaiSKoi3ypsV+fnLNENz+cjBEEgIi3B/daRH+tVqQvh/8R 21 | LvYTFF4bAcdouYi0y2leXfHhpw== 22 | -----END CERTIFICATE----- 23 | -------------------------------------------------------------------------------- /res/private/server_key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpAIBAAKCAQEA0JsXsBi/sfgCRqfeSeff1almRidf0wZnjt/2htKmjzluJ/f0 3 | +irK20AOI4aCtLg+zKopJ8aekwqeo2vm8k9Ht8xWcX6g7I6YZNEDVjrFPlcZd+HF 4 | fZ2ItH70YAJQInMHqEWrrNCNR/ndHnlQg+HUrA+od3HjiegQu/OmVVPaSGiytAFc 5 | 9QzqCuYPtKOZvCn6KBPX3mXbrBnWf0ASpz7ndRPRfYP0MU0ut+AfNcj4/lDvSDOK 6 | FVbUSe6Prx4cv4HkcQuPngzqHO4el6ABf0ZWJjBwvZFWW14XEJ1Jx2ODJmY+Aq3/ 7 | yq9Z9MjnuVBs7QjoHIqUMuzWeC7MSBZUC5x5SQIDAQABAoIBAQC8EngCze1WOLFk 8 | nkgs/Z6ydW295hXgna+UApuy5gxAqJiF9GmrehU2IsQch1MkN9B2mRtNvyaMj1CD 9 | Ke8nmw6fyNxOqsnPPKhsjHyjq4zVLZXKnYR+Qh9UC/mq7artxCOtNFMZFVWrBLy0 10 | ks9id6JUFjHerpFkbhNYQM0/tL/h8tEoO4kanUbxq5un8F1Cl2NxRYCgIWvOG5NO 11 | UZVPtj6oyHUy4OJ33gNkEJ14HYD200F0iw8rhiug7ZKaXd8SSx4yEvOUCSQ+6pV6 12 | FJOQQLK8gn2V8YKLQNUbdtp4JoQSeaILf4KuWQ423UIW/WXIIoil222hCNnDNRaI 13 | WW5Pie25AoGBAPev8f6WY3sxpXHpNMrXKj7C0TVf+JQbESS3jMpNm+1iE2uAx99e 14 | 6pXMbWH/7v8WjdLeIMGFNpsI2NCzbQ+cgfrH0SBNiPzelppGZDriqGuy2ezQ3Ypu 15 | pMMr5KyCP+BCoM5PKZQT2qWqiy1tB5mPPMUs45IJbG95G35vU3MmCeP7AoGBANeb 16 | XwGFImCDFzC53j08GZ+3MhNjLs+6HD75OAdLOmSwJH+NiDal5S2xzDXw+aSwYYv5 17 | Be8al5Vp+ulZJ2M3qmjlzXp4n8IxooftieDHCoyBILqrmoSHF0aVCyblsecdxQSl 18 | EL8VgDTFcngwZtJaxZc7Xqe2GmzamW1h330TmhCLAoGAV4Wup12Q7ZlPcv8LDpoV 19 | bXP95TRybDNcTXMmpt3huXIslpI9DmtFzYUdKcH8O9tGZjrjrD5cW1A2/RhJ83hE 20 | Xc950EZVn7Uv1ngFNuGczeG3K/1qK16JjgXWmja0R5SDqiNC9/ZEDsJCx9x9EQAS 21 | Y0JHb/Uwgftzgavo+wl3+T0CgYAr/eu4p62H+7dznbkWzXh8+ighhI88m0DAKKGh 22 | +1uCx93qmLo+TEMiu7BrISwOyl5c7Qak7swXFHS5wBMlT2pZ1OnEH3CZcv8ytOj5 23 | ECO6324KKJFykQ3SvP51hVBzU8OrWvK7ymtKWS8uDtIsAZFndhmuJp3lsAS2KM4s 24 | +x7oWQKBgQCUIcsrr9P5KCs8LTivHSZbw2bxBSAK/FLnRb/Z3iE0F0Cg7sfoqTBc 25 | mvT0W1Cn0FaXmt3IPk+hgdkKRYS/nocKdH1dAKOiI4sod69D0Ysrp8wRyNyoQRwr 26 | 1OX1MV1VdU5DsC+wIE72U4p6ctAUaAaM4PH5NlHqLrDyHBVvK254Dg== 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /res/sjs/req_rewrite.js: -------------------------------------------------------------------------------- 1 | 2 | function pproxy_rewrite(req){ 3 | req.get=pproxy_params_copy(req.origin.get||{}); 4 | req.post=pproxy_params_copy(req.origin.post||{}); 5 | 6 | 7 | for(var k in req.origin.header){ 8 | req[k]=req.origin.header[k]+""; 9 | } 10 | 11 | var form_get=pproxy_obj_helper(req.get); 12 | var form_post=pproxy_obj_helper(req.post); 13 | 14 | var use_file=function(filePath){ 15 | filePath=filePath+"" 16 | req.url=filePath.substr(0,7)=="http://"?filePath:("http://PPROXY_HOST"+"/f/"+filePath) 17 | } 18 | 19 | CUSTOM_JS 20 | return req; 21 | } 22 | //clone the get and post params 23 | function pproxy_params_copy(obj){ 24 | var newObj=new Object(); 25 | for(var k in obj){ 26 | var arr=new Array(); 27 | for(var i in obj[k]){ 28 | arr[i]=obj[k][i]+""; 29 | } 30 | newObj[k]=arr; 31 | } 32 | return newObj 33 | } 34 | 35 | function pproxy_obj_helper(values){ 36 | return { 37 | get:function(name){ 38 | return values[name] 39 | }, 40 | set:function(name,val){ 41 | values[name]=[val] 42 | }, 43 | add:function(name,val){ 44 | val=val+"" 45 | if(typeof values[name]=="undefined"){ 46 | values[name]=[val] 47 | return 48 | } 49 | if(typeof values[name]!="Object"){ 50 | values[name]=[values[name]+""] 51 | } 52 | values[name].push(val) 53 | }, 54 | del:function(name){ 55 | delete values[name] 56 | }, 57 | len:function(name){ 58 | if(name==undefined){ 59 | return values.length 60 | }else{ 61 | return (values[name]||[]).length 62 | } 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /res/tpl/about.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

about

4 |

HTTP protocol analysis tool.

5 |

Based on BS architecture,written by golang.

6 |

Project : https://github.com/hidu/pproxy

7 |

Current Version: {{.version}}

8 |

9 |
10 | 11 |
12 |
13 |
14 | 15 | 20 | -------------------------------------------------------------------------------- /res/tpl/config.html: -------------------------------------------------------------------------------- 1 | 2 | 5 |
6 |
7 | 8 | 9 | 10 | 11 | 12 |
{%my_include "config/req_form.html" %}
{%my_include "config/req_demo.html" %}
{%my_include "config/hosts_form.html" %}
{%my_include "config/hosts_demo.html" %}
13 | 14 | 15 |
16 | 17 | 20 | -------------------------------------------------------------------------------- /res/tpl/config/req_demo.html: -------------------------------------------------------------------------------- 1 |
2 | note: 3 |
    4 |
  1. put //ignore at the first line to ignore the js
  2. 5 |
  3. user's js is default,or the global
  4. 6 | 7 | {{if not .isLogin}} 8 |
  5. login to edit your custom profile
  6. 9 | {{end}} 10 | 11 |
12 |
13 | req demo:show/hide 14 | 42 |

-------------------------------------------------------------------------------- /res/tpl/config/req_form.html: -------------------------------------------------------------------------------- 1 | req rewrite 2 | {{range $i,$js:=.jss}} 3 |
4 |
5 | {{$js.title}} 6 | function rewrite(req){ 7 | 8 | return req;
9 | } 10 | 11 | 12 |
13 | 14 | {{if $.isLogin}} 15 | 16 | {{else}} 17 |
login for submit
18 | {{end}} 19 |
20 | 21 |
    22 | {{range $j,$use_file:=$js.use_file}} 23 |
  1. 24 | {{$use_file.name}} 25 |
  2. 26 | {{end}} 27 |
28 | 29 |
30 |
31 | {{end}} -------------------------------------------------------------------------------- /res/tpl/error.html: -------------------------------------------------------------------------------- 1 |
2 | Error: 3 | {{.error}} 4 |
-------------------------------------------------------------------------------- /res/tpl/file.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 10 | 11 | 12 | 13 | 19 | 20 | 21 | 22 | 23 | {{range $k,$file:=.files}} 24 | 25 | 26 | 38 | 39 | 40 | {{end}} 41 | 42 |
4 |
5 |   6 | /f/{{.currentDir}}   7 | 8 |
9 |
no 14 | {{if .isSubDir}} 15 | ../ 16 | {{end}} 17 | Name 18 | Size
{{$k}} 27 | 28 | {{if $file.IsDir}} {{else}}{{end}} 29 | {{$file.Name}} 30 | 31 |   32 | {{if $file.IsDir}} 33 | 34 | {{else}} 35 | 36 | {{end}} 37 | {{$file.Size}}
43 |
-------------------------------------------------------------------------------- /res/tpl/file_edit.html: -------------------------------------------------------------------------------- 1 |
2 |
edit file
3 |
4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |    12 | go File List 13 |    14 | view 15 | 16 |
17 | 18 |
19 |
20 | js req write:
21 | use_file("{{.file.Name}}")
22 | 
23 |
24 |
25 | 26 | 27 |
28 | 29 | 32 | 33 | -------------------------------------------------------------------------------- /res/tpl/file_new.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
new file
4 |
5 | 6 | 7 | 8 | 9 | 10 | {{.dir}}/ 11 | 12 |
13 |
14 | 15 |
16 | 17 |
18 |
19 | 20 | 21 |
22 | 23 | 26 | 27 | -------------------------------------------------------------------------------- /res/tpl/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | {{.subTitle}}{{.title}} | pproxy {{.version}} 10 | 11 | 12 | 13 | 14 | 15 | 16 | 44 |
45 |
46 | {{.body}} 47 |
48 |
49 |
50 | 51 | -------------------------------------------------------------------------------- /res/tpl/login.html: -------------------------------------------------------------------------------- 1 |
2 |




3 |
4 |
5 |
6 | Admin Login 7 | 17 |
18 |
19 |
20 | 21 |
22 | -------------------------------------------------------------------------------- /res/tpl/network.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 62 | 65 | 66 |
8 |
9 |
10 |
11 |
12 | session filter 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 |
client: user:
exclude: 23 | 24 | 25 | 26 |
url:
35 |
36 |
37 |
38 | 39 |
40 |
#
41 | host 42 |
43 | path 44 |
45 | 46 |
47 |
48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 |
58 |
59 |
60 | 61 |
63 |
64 |
67 | -------------------------------------------------------------------------------- /res/tpl/replay.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
replay
5 |
6 | basic 7 | 8 | 9 | 10 | 11 |
action url:
method:
Host:
12 | 13 | 14 |
15 | 16 |
17 | header 18 | (only use these replay with pproxy)   19 | show/hide 20 | 21 | 22 | {{range $k,$v:=.req.header}} 23 | 24 | 25 | 30 | 36 | 37 | {{end}} 38 | 39 |
40 | 41 |
42 | get_params 43 | +param 44 | 45 | 46 | {{range $k,$v:=.req.form_get}} 47 | 52 | 53 | 59 | 60 | 61 | {{end}} 62 | 63 |
{{$k}}: 48 | {{range $index,$_v:=$v}} 49 |
50 | {{end}} 51 |
54 |
55 | +  56 | X 57 |
58 |
64 |
65 | 66 | {{if eq .req.method "POST"}} 67 |
68 | post_params 69 | +param 70 | 71 | 72 | {{range $k,$v:=.req.form_post}} 73 | 78 | 84 | 85 | {{end}} 86 |
{{$k}}: 74 | {{range $index,$_v:=$v}} 75 |
76 | {{end}} 77 |
79 |
80 | +  81 | X 82 |
83 |
87 |
88 | {{end}} 89 |
90 |
91 | 92 | 93 | 94 | 95 |
96 |
97 |
98 |
99 | 100 |
101 | 102 | 138 | -------------------------------------------------------------------------------- /res/tpl/replay_direct.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | 6 | 7 | {{range $k,$v:=.form.get}} 8 | {{range $_k,$_v:=$v}} 9 | 10 | {{end}} 11 | {{end}} 12 | 13 | {{range $k,$v:=.form.post}} 14 | {{range $_k,$_v:=$v}} 15 | 16 | {{end}} 17 | {{end}} 18 |
{{.form.basic.method}}  {{.form.basic.action_url}}
{{$k}}:
{{$k}}:
19 | 20 |
21 | 22 | 25 | 26 |
-------------------------------------------------------------------------------- /res/tpl/useage.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Useage

4 |

1.Client(eg:phone)

5 |
 6 | set wifi http proxy:
 7 | proxy host : {{.pproxy_host}}
 8 | proxy port : {{.pproxy_port}}
 9 | 
10 | 
11 | android users can use proxyDroid to manager proxys 12 | 13 |

2.Server:user interface

14 |
15 | visit Session List Page to view all the http request through this proxy.
16 | in the session filter form, all text input can use| to enter multiple conditions.
17 | eg user:,it mean user is a or b.
18 | you can use replay to replay a request.
19 | 
20 | 21 |

3.Modify Requests

22 |
you can modify http request.GET、POST paramas and http headers.
23 |
pproxy use javascript to achieve it:
24 |
25 | function rewrite(req){
26 |     //you code start
27 |     if(req.host=="www.baidu.com"){
28 | 	   req.host="www.163.com"
29 | 	   req.host_addr="127.0.0.0:81" // send req to 127.0.0.1:81
30 | 	   form_get.add("a","a")
31 | 	   //form_post.set("d","a")
32 | 	}
33 | 	
34 |     // you code end
35 |     return req
36 | }
37 | 
38 |
req object has these attributes(url=http://www.example.com/album/list?cid=126):
39 |
40 | schema : http
41 | host : www.example.com
42 | port : 80
43 | path : /album/list
44 | get: {cid:[123]}
45 | post: {}
46 | username : 
47 | password : 
48 | method: GET
49 | form_get  : {add:function(k,v){},set:function(k,v){},get:function(k){},len:function(){}} 
50 | form_post : {add:function(k,v){},set:function(k,v){},get:function(k){},len:function(){}}
51 | 
52 | host_addr: #the real host you wish,eg 127.0.0.1:3218
53 | 
54 | 55 |

4.Modify Hosts

56 |
57 | #all port
58 | news.baidu.com 127.0.0.1
59 | 
60 | #only 81 port match
61 | news.baidu.com:81 127.0.0.1:82
62 | 
63 | news.163.com 127.0.0.1:8080
64 | 
65 |
-------------------------------------------------------------------------------- /res/version: -------------------------------------------------------------------------------- 1 | 0.5.2 -------------------------------------------------------------------------------- /script/create_dest_zip.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "bye bye" 4 | exit 5 | 6 | cd $(dirname $0) 7 | cd ../ 8 | 9 | if [ -z "$1" ];then 10 | gox -arch amd64 -os linux 11 | bash build.sh windows 12 | fi 13 | 14 | version=$(cat res/version) 15 | 16 | cd dest 17 | ################################################ 18 | 19 | if [ -d conf ];then 20 | rm -rf conf 21 | fi 22 | 23 | rm -rf data/* 24 | mkdir conf 25 | cp ../res/conf/demo.conf conf/pproxy.conf 26 | echo -e "name:admin psw:psw is_admin:admin">conf/users 27 | cp ../conf/req_rewrite_8080.js conf/ 28 | echo -e "news.baidu.com 127.0.0.1\nnews.163.com 127.0.0.1:81">conf/hosts_8080 29 | 30 | t=$(date +"%Y%m%d%H") 31 | 32 | rm pproxy_*.tar.gz pproxy_*.zip 33 | 34 | ################################################ 35 | target_linux="pproxy_${version}_linux_$t.tar.gz" 36 | 37 | 38 | mkdir -p linux/data 39 | mkdir -p linux/file/ 40 | 41 | cp pproxy ../script/pproxy_control.sh linux/ 42 | cp -r conf linux/conf 43 | 44 | 45 | dir_new="pproxy_${version}" 46 | if [ -d $dir_new ];then 47 | rm -rf $dir_new 48 | fi 49 | 50 | mv linux $dir_new 51 | tar -czvf $target_linux $dir_new 52 | 53 | rm -rf $dir_new 54 | 55 | 56 | ################################################ 57 | target_windows="pproxy_${version}_windows_$t.zip" 58 | 59 | 60 | mkdir -p windows/data 61 | mkdir -p windows/file/ 62 | 63 | cp pproxy.exe windows 64 | cp ../script/windows_run.bat windows/start.bat 65 | cp -r conf windows/conf 66 | 67 | 68 | mv windows $dir_new 69 | zip -r $target_windows $dir_new 70 | 71 | rm -rf $dir_new conf 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /script/pproxy_control.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | CUR_DIR=$(dirname $0) 4 | 5 | BIN_NAME="./pproxy" 6 | DEFAULT_CONF="./conf/pproxy.conf" 7 | INTRO="get more info from github.com/hidu/pproxy" 8 | 9 | 10 | CONF_FILE=$2 11 | 12 | if [ -z "$CONF_FILE" ];then 13 | cd $CUR_DIR 14 | CONF_FILE="$DEFAULT_CONF" 15 | fi 16 | 17 | CONF_PATH=$(readlink -f "$CONF_FILE") 18 | 19 | cd $CUR_DIR 20 | 21 | BIN_PATH=$(readlink -f $BIN_NAME) 22 | 23 | if [ ! -f "$CONF_PATH" ];then 24 | echo "conf file[${CONF_PATH}] not exists!" 25 | exit 2 26 | fi 27 | 28 | 29 | RUN_CMD="$BIN_PATH -conf $CONF_PATH" 30 | 31 | function start(){ 32 | nohup $RUN_CMD>/dev/null 2>&1 & 33 | status=$? 34 | if [ "$status" == "0" ];then 35 | echo "start suc! pid="$! 36 | else 37 | echo "start failed!" 38 | exit 2 39 | fi 40 | } 41 | 42 | function stop(){ 43 | list=$(ps aux|grep "$RUN_CMD"|grep -v grep) 44 | if [ -z "${list}" ];then 45 | echo "no process to kill" 46 | else 47 | pid=$( echo "$list"|awk '{print $2}') 48 | kill $pid 49 | if [ "$?"=="0" ];then 50 | echo "stop suc! pid=${pid}" 51 | else 52 | echo "stop failed! pid=${pid}" 53 | exit 3 54 | fi 55 | fi 56 | } 57 | 58 | function restart(){ 59 | stop 60 | start 61 | } 62 | 63 | function useage(){ 64 | echo "pproxy useage:" 65 | echo $0 "start|stop|restart" [conf_path] 66 | echo -e "$INTRO" 67 | } 68 | 69 | if [ $# -lt 1 ]; then 70 | useage 71 | exit 1 72 | fi 73 | 74 | case "$1" in 75 | start) 76 | start 77 | ;; 78 | stop) 79 | stop 80 | ;; 81 | restart) 82 | restart 83 | ;; 84 | esac -------------------------------------------------------------------------------- /script/windows_run.bat: -------------------------------------------------------------------------------- 1 | echo "start pproxy,don't close it" 2 | @echo off 3 | pproxy -conf=./conf/pproxy.conf -------------------------------------------------------------------------------- /serve/auth.go: -------------------------------------------------------------------------------- 1 | package serve 2 | 3 | import ( 4 | "encoding/base64" 5 | "net/http" 6 | "strings" 7 | 8 | "github.com/hidu/goutils/str_util" 9 | ) 10 | 11 | var proxyAuthorizatonHeader = "Proxy-Authorization" 12 | 13 | func getAuthorInfo(req *http.Request) *User { 14 | defaultInfo := new(User) 15 | authheader := strings.SplitN(req.Header.Get(proxyAuthorizatonHeader), " ", 2) 16 | if len(authheader) != 2 || authheader[0] != "Basic" { 17 | return defaultInfo 18 | } 19 | userpassraw, err := base64.StdEncoding.DecodeString(authheader[1]) 20 | if err != nil { 21 | return defaultInfo 22 | } 23 | userpass := strings.SplitN(string(userpassraw), ":", 2) 24 | if len(userpass) != 2 { 25 | return defaultInfo 26 | } 27 | return &User{Name: userpass[0], PswMd5: str_util.StrMd5(userpass[1]), Psw: userpass[1]} 28 | } 29 | 30 | func (ser *ProxyServe) checkUserLogin(userInfo *User) bool { 31 | if userInfo == nil || ser.Users == nil { 32 | return false 33 | } 34 | 35 | if userInfo.SkipCheckPsw { 36 | return true 37 | } 38 | 39 | if user, has := ser.Users[userInfo.Name]; has { 40 | return user.PswMd5 == userInfo.PswMd5 41 | } 42 | return false 43 | } 44 | 45 | // (ser.conf.AuthType == AuthType_Basic && !ser.CheckUserLogin(reqCtx.User)) 46 | func (ser *ProxyServe) checkHTTPAuth(reqCtx *requestCtx) bool { 47 | switch ser.conf.AuthType { 48 | case authTypeNO: 49 | return true 50 | case authTypeBasic: 51 | return ser.checkUserLogin(reqCtx.User) 52 | case authTypeBasicWithAny: 53 | return reqCtx.User.Name != "" 54 | case authTypeBasicTry: 55 | if reqCtx.ClientSession.RequestNum == 1 { 56 | return reqCtx.User.Name != "" 57 | } 58 | return true 59 | default: 60 | return false 61 | } 62 | return false 63 | } 64 | -------------------------------------------------------------------------------- /serve/broadcast.go: -------------------------------------------------------------------------------- 1 | package serve 2 | 3 | import ( 4 | "strconv" 5 | ) 6 | 7 | // broadcastReq broadcast request to user's browser 8 | func (ser *ProxyServe) broadcastReq(reqCtx *requestCtx) bool { 9 | req := reqCtx.Req 10 | data := make(map[string]any) 11 | data["docid"] = strconv.Itoa(reqCtx.Docid) 12 | data["sid"] = reqCtx.SessionID % 10000 13 | data["host"] = req.Host 14 | data["client_ip"] = req.RemoteAddr 15 | urlPath := req.URL.Path 16 | if req.URL.RawQuery != "" { 17 | urlPath += "?" + req.URL.RawQuery 18 | } 19 | data["path"] = urlPath 20 | data["url"] = req.URL.String() 21 | if req.Method == "CONNECT" && !ser.conf.SslOn { 22 | data["path"] = "https req,unknow path" 23 | } 24 | data["method"] = req.Method 25 | data["replay"] = reqCtx.IsRePlay 26 | 27 | hasSend := ser.wsSer.broadcastReq(req, reqCtx, data) 28 | return hasSend 29 | } 30 | -------------------------------------------------------------------------------- /serve/certs.go: -------------------------------------------------------------------------------- 1 | package serve 2 | 3 | import ( 4 | "crypto/tls" 5 | "crypto/x509" 6 | "log" 7 | "os" 8 | ) 9 | 10 | func newCaCert(caCert []byte, caKey []byte) (tls.Certificate, error) { 11 | ca, err := tls.X509KeyPair(caCert, caKey) 12 | if err != nil { 13 | log.Println("NewCaCert error:", err) 14 | return ca, err 15 | } 16 | if ca.Leaf, err = x509.ParseCertificate(ca.Certificate[0]); err != nil { 17 | log.Println("NewCaCert error:", err) 18 | return ca, err 19 | } 20 | log.Println("NewCaCert Ok") 21 | return ca, nil 22 | } 23 | 24 | // getSslCert get user's caCert or use the default buildin 25 | func getSslCert(caCertPath string, caKeyPath string) (ca tls.Certificate, err error) { 26 | if caCertPath == "" { 27 | caCert := Assest.GetContent("/res/private/client_cert.pem") 28 | caKey := Assest.GetContent("/res/private/server_key.pem") 29 | return newCaCert([]byte(caCert), []byte(caKey)) 30 | } 31 | cert, err := os.ReadFile(caCertPath) 32 | if err != nil { 33 | return ca, err 34 | } 35 | key, err := os.ReadFile(caKeyPath) 36 | if err != nil { 37 | return ca, err 38 | } 39 | return newCaCert(cert, key) 40 | } 41 | -------------------------------------------------------------------------------- /serve/config.go: -------------------------------------------------------------------------------- 1 | package serve 2 | 3 | import ( 4 | "crypto/tls" 5 | "errors" 6 | "fmt" 7 | "log" 8 | "net/http" 9 | "net/url" 10 | "strings" 11 | 12 | "github.com/Unknwon/goconfig" 13 | "github.com/hidu/goutils/fs" 14 | "github.com/hidu/goutils/str_util" 15 | ) 16 | 17 | // Config pproxy's config 18 | type Config struct { 19 | Port int 20 | AdminPort int 21 | Title string 22 | Notice string 23 | AuthType int 24 | DataDir string 25 | FileDir string 26 | ResponseSave int 27 | SessionView int 28 | DataStoreDay float64 29 | ParentProxy *url.URL 30 | 31 | SslOn bool 32 | 33 | SslCert tls.Certificate 34 | 35 | ModifyRequest bool 36 | } 37 | 38 | const ( 39 | authTypeNO = 0 40 | authTypeBasic = 1 41 | authTypeBasicWithAny = 2 42 | authTypeBasicTry = 3 43 | 44 | responseSaveAll = 0 45 | responseSaveHasBroad = 1 // has show 46 | 47 | sessionViewALL = 0 48 | sessionViewIPOrUser = 1 49 | ) 50 | 51 | // User user struct 52 | type User struct { 53 | Name string 54 | Psw string 55 | PswMd5 string 56 | IsAdmin bool 57 | SkipCheckPsw bool 58 | } 59 | 60 | // String string format 61 | func (u *User) String() string { 62 | return fmt.Sprintf("Name:%s,Psw:%s,isAdmin:%v,SkipCheckPsw:%v", u.Name, u.Psw, u.IsAdmin, u.SkipCheckPsw) 63 | } 64 | 65 | // ConfigString one line in file 66 | func (u *User) ConfigString() string { 67 | return fmt.Sprintf("name:%s\tpsw:%s\tis_admin:%v\tpsw_md5:%s", u.Name, u.Psw, u.IsAdmin, u.PswMd5) 68 | } 69 | 70 | const ( 71 | contentEncoding = "Content-Encoding" 72 | ) 73 | 74 | // "0:no auth | 1:basic auth | 2:basic auth with any name" 75 | 76 | // GetVersion get current version 77 | func GetVersion() string { 78 | return Assest.GetContent("res/version") 79 | } 80 | 81 | // GetDemoConf get the demo config 82 | func GetDemoConf() string { 83 | return strings.TrimSpace(Assest.GetContent("res/conf/demo.conf")) 84 | } 85 | 86 | func (u *User) isPswEq(psw string) bool { 87 | return u.PswMd5 == str_util.StrMd5(psw) 88 | } 89 | 90 | // LoadConfig load the pproxy's config 91 | func LoadConfig(confPath string) (*Config, error) { 92 | gconf, err := goconfig.LoadConfigFile(confPath) 93 | if err != nil { 94 | log.Println("load config", confPath, "failed,err:", err) 95 | return nil, err 96 | } 97 | config := new(Config) 98 | config.Port = gconf.MustInt(goconfig.DEFAULT_SECTION, "port", 8080) 99 | config.AdminPort = gconf.MustInt(goconfig.DEFAULT_SECTION, "adminPort", 0) 100 | 101 | if config.AdminPort == 0 { 102 | config.AdminPort = config.Port 103 | } 104 | 105 | config.DataStoreDay = gconf.MustFloat64(goconfig.DEFAULT_SECTION, "dataStoreDay", 0) 106 | if config.DataStoreDay < 0 { 107 | log.Println("wrong DataStoreDay,skip") 108 | config.DataStoreDay = 0 109 | } 110 | 111 | config.Title = gconf.MustValue(goconfig.DEFAULT_SECTION, "title") 112 | config.Notice = gconf.MustValue(goconfig.DEFAULT_SECTION, "notice") 113 | config.DataDir = gconf.MustValue(goconfig.DEFAULT_SECTION, "dataDir", "../data/") 114 | 115 | config.FileDir = gconf.MustValue(goconfig.DEFAULT_SECTION, "fileDir", "../file/") 116 | 117 | _authType := strings.ToLower(gconf.MustValue(goconfig.DEFAULT_SECTION, "authType", "none")) 118 | authTypes := map[string]int{"none": 0, "basic": 1, "basic_any": 2, "basic_try": 3, "try_basic": 3} 119 | 120 | hasError := false 121 | if authType, has := authTypes[_authType]; has { 122 | config.AuthType = authType 123 | } else { 124 | hasError = true 125 | log.Println("conf error,unknow value authType:", _authType) 126 | } 127 | 128 | _responseSave := strings.ToLower(gconf.MustValue(goconfig.DEFAULT_SECTION, "responseSave", "all")) 129 | responseSaveMap := map[string]int{"all": 0, "only_broadcast": 1} 130 | 131 | if responseSave, has := responseSaveMap[_responseSave]; has { 132 | config.ResponseSave = responseSave 133 | } else { 134 | hasError = true 135 | log.Println("conf error,unknow value responseSave:", _authType) 136 | } 137 | 138 | _sessionView := strings.ToLower(gconf.MustValue(goconfig.DEFAULT_SECTION, "sessionView", "all")) 139 | sessionViewMap := map[string]int{"all": 0, "ip_or_user": 1} 140 | 141 | if sessionView, has := sessionViewMap[_sessionView]; has { 142 | config.SessionView = sessionView 143 | } else { 144 | hasError = true 145 | log.Println("conf error,unknow value responseSave:", _authType) 146 | } 147 | 148 | parentProxy := gconf.MustValue(goconfig.DEFAULT_SECTION, "parentProxy", "") 149 | if parentProxy != "" { 150 | _urlObj, err := url.Parse(parentProxy) 151 | if err != nil || _urlObj.Scheme != "http" { 152 | hasError = true 153 | log.Println("parentProxy wrong,must http proxy") 154 | } else { 155 | config.ParentProxy = _urlObj 156 | } 157 | } 158 | config.SslOn = gconf.MustValue(goconfig.DEFAULT_SECTION, "ssl", "off") == "on" 159 | if config.SslOn { 160 | _sslClientCert := gconf.MustValue(goconfig.DEFAULT_SECTION, "ssl_client_cert", "") 161 | _sslServerKey := gconf.MustValue(goconfig.DEFAULT_SECTION, "ssl_server_key", "") 162 | cert, err := getSslCert(_sslClientCert, _sslServerKey) 163 | if err != nil { 164 | hasError = true 165 | log.Println("ssl ca config error:", err) 166 | } else { 167 | config.SslCert = cert 168 | } 169 | } 170 | 171 | config.ModifyRequest = gconf.MustValue(goconfig.DEFAULT_SECTION, "modifyRequest", "on") == "on" 172 | 173 | if hasError { 174 | return config, errors.New("config error") 175 | } 176 | 177 | return config, nil 178 | } 179 | 180 | type configHosts map[string]string 181 | 182 | // loadHosts 读取host配置文件 183 | func loadHosts(confPath string) (hosts configHosts, err error) { 184 | hosts = make(configHosts) 185 | if !fs.FileExists(confPath) { 186 | return 187 | } 188 | hostsByte, err := fs.FileGetContents(confPath) 189 | if err != nil { 190 | log.Println("load hosts_file failed:", confPath, err) 191 | return nil, err 192 | } 193 | hostsArr := str_util.LoadText2Slice(string(hostsByte)) 194 | for _, v := range hostsArr { 195 | if len(v) != 2 { 196 | log.Println("hosts file line wrong,ignore,", v) 197 | continue 198 | } 199 | hosts[v[0]] = v[1] 200 | } 201 | return 202 | } 203 | 204 | func loadUsers(confPath string) (users map[string]*User, err error) { 205 | users = make(map[string]*User) 206 | if !fs.FileExists(confPath) { 207 | return 208 | } 209 | userInfoByte, err := fs.FileGetContents(confPath) 210 | if err != nil { 211 | log.Println("load user file failed:", confPath, err) 212 | return 213 | } 214 | lines := str_util.LoadText2SliceMap(string(userInfoByte)) 215 | for _, line := range lines { 216 | name, has := line["name"] 217 | if !has || name == "" { 218 | continue 219 | } 220 | if _, has := users[name]; has { 221 | log.Println("dup name in users:", name, line) 222 | continue 223 | } 224 | 225 | user := new(User) 226 | user.Name = name 227 | if val, has := line["is_admin"]; has && (val == "admin" || val == "true") { 228 | user.IsAdmin = true 229 | } 230 | if val, has := line["psw_md5"]; has { 231 | user.PswMd5 = val 232 | } 233 | 234 | if user.PswMd5 == "" { 235 | if val, has := line["psw"]; has { 236 | user.Psw = val 237 | user.PswMd5 = str_util.StrMd5(val) 238 | } 239 | } 240 | users[user.Name] = user 241 | } 242 | return 243 | } 244 | 245 | func (config *Config) getTransport() *http.Transport { 246 | if config.ParentProxy == nil { 247 | return nil 248 | } 249 | tr := &http.Transport{ 250 | Proxy: func(req *http.Request) (*url.URL, error) { 251 | if config.ParentProxy.User.Username() == "pass" { 252 | user := getAuthorInfo(req) 253 | urlTmp, err := url.Parse(config.ParentProxy.String()) 254 | if err != nil { 255 | return nil, err 256 | } 257 | urlTmp.User = url.UserPassword(user.Name, user.Psw) 258 | return urlTmp, nil 259 | } 260 | return config.ParentProxy, nil 261 | }, 262 | } 263 | return tr 264 | } 265 | -------------------------------------------------------------------------------- /serve/init.go: -------------------------------------------------------------------------------- 1 | package serve 2 | 3 | // 系统版本 4 | var PproxyVersion string 5 | 6 | func init() { 7 | PproxyVersion = GetVersion() 8 | } 9 | -------------------------------------------------------------------------------- /serve/kvStore.go: -------------------------------------------------------------------------------- 1 | package serve 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/boltdb/bolt" 7 | "github.com/hidu/goutils/time_util" 8 | ) 9 | 10 | type KV_TBALE_NAME_TYPE string 11 | 12 | const ( 13 | KV_TABLE_REQ KV_TBALE_NAME_TYPE = "req" 14 | KV_TABLE_RES KV_TBALE_NAME_TYPE = "res" 15 | ) 16 | 17 | type kvStore struct { 18 | dbPath string 19 | db *bolt.DB 20 | tables map[KV_TBALE_NAME_TYPE]*kvStoreTable 21 | } 22 | 23 | type kvStoreTable struct { 24 | name KV_TBALE_NAME_TYPE 25 | kv *kvStore 26 | } 27 | 28 | type StoreType struct { 29 | Now int64 `json:"now"` 30 | Data KvType `json:"data"` 31 | } 32 | 33 | func newStoreType(data map[string]any) *StoreType { 34 | return &StoreType{Now: time.Now().Unix(), Data: data} 35 | } 36 | 37 | func newKvStore(dbPath string) (kv *kvStore, err error) { 38 | kv = &kvStore{ 39 | dbPath: dbPath, 40 | } 41 | kv.db, err = bolt.Open(kv.dbPath, 0600, nil) 42 | if err != nil { 43 | return 44 | } 45 | kv.tables = make(map[KV_TBALE_NAME_TYPE]*kvStoreTable) 46 | 47 | kv.initTable(KV_TABLE_REQ) 48 | kv.initTable(KV_TABLE_RES) 49 | 50 | return 51 | } 52 | 53 | func (kv *kvStore) initTable(name KV_TBALE_NAME_TYPE) { 54 | kv.tables[name] = newkvStoreTable(name, kv) 55 | } 56 | 57 | func (kv *kvStore) GetkvStoreTable(name KV_TBALE_NAME_TYPE) (tb *kvStoreTable) { 58 | if tb, has := kv.tables[name]; has { 59 | return tb 60 | } 61 | return nil 62 | } 63 | 64 | func (kv *kvStore) Gc(max_life int64) { 65 | for _, tb := range kv.tables { 66 | tb.Gc(max_life) 67 | } 68 | } 69 | 70 | func (kv *kvStore) StartGcTimer(sec int64, max_life int64) { 71 | if max_life < 1 { 72 | return 73 | } 74 | time_util.SetInterval(func() { 75 | kv.Gc(max_life) 76 | }, sec) 77 | } 78 | 79 | func newkvStoreTable(name KV_TBALE_NAME_TYPE, kv *kvStore) *kvStoreTable { 80 | tb := &kvStoreTable{ 81 | name: name, 82 | kv: kv, 83 | } 84 | tb.kv.db.Update(func(tx *bolt.Tx) error { 85 | _, err := tx.CreateBucketIfNotExists([]byte(name)) 86 | return err 87 | }) 88 | return tb 89 | } 90 | 91 | func (tb *kvStoreTable) Save(key []byte, val *StoreType) error { 92 | err := tb.kv.db.Update(func(tx *bolt.Tx) error { 93 | bk, _ := tx.CreateBucketIfNotExists([]byte(tb.name)) 94 | return bk.Put(key, dataEncode(val)) 95 | }) 96 | return err 97 | } 98 | 99 | func (tb *kvStoreTable) Get(key []byte) (val *StoreType, err error) { 100 | err = tb.kv.db.View(func(tx *bolt.Tx) error { 101 | bk := tx.Bucket([]byte(tb.name)) 102 | bs := bk.Get(key) 103 | if len(bs) > 0 { 104 | return dataDecode(bs, &val) 105 | } 106 | return nil 107 | }) 108 | return 109 | } 110 | 111 | func (tb *kvStoreTable) Del(key []byte) (err error) { 112 | err = tb.kv.db.Update(func(tx *bolt.Tx) error { 113 | bk := tx.Bucket([]byte(tb.name)) 114 | return bk.Delete(key) 115 | }) 116 | return 117 | } 118 | 119 | func (tb *kvStoreTable) Gc(gc_life int64) { 120 | if gc_life < 1 { 121 | return 122 | } 123 | max_time := time.Now().Unix() - gc_life 124 | var val *StoreType 125 | tb.kv.db.View(func(tx *bolt.Tx) error { 126 | bk := tx.Bucket([]byte(tb.name)) 127 | bk.ForEach(func(k, v []byte) error { 128 | dataDecode(v, &val) 129 | if val != nil && val.Now < max_time { 130 | tb.Del(k) 131 | } 132 | return nil 133 | }) 134 | return nil 135 | }) 136 | } 137 | -------------------------------------------------------------------------------- /serve/proxy.go: -------------------------------------------------------------------------------- 1 | package serve 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "net" 7 | "net/http" 8 | "net/http/httputil" 9 | "strconv" 10 | "sync" 11 | "time" 12 | 13 | "github.com/elazarl/goproxy" 14 | ) 15 | 16 | type HttpProxy struct { 17 | GoProxy *goproxy.ProxyHttpServer 18 | ser *ProxyServe 19 | ctxs map[string]*requestCtx 20 | mu sync.RWMutex 21 | goproxyMitmConnect *goproxy.ConnectAction 22 | } 23 | 24 | func NewHttpProxy(ser *ProxyServe) *HttpProxy { 25 | proxy := new(HttpProxy) 26 | proxy.ser = ser 27 | proxy.GoProxy = goproxy.NewProxyHttpServer() 28 | tr := ser.conf.getTransport() 29 | if tr != nil { 30 | proxy.GoProxy.Tr = tr 31 | } 32 | proxy.ctxs = make(map[string]*requestCtx) 33 | if proxy.ser.conf.SslOn { 34 | proxy.goproxyMitmConnect = &goproxy.ConnectAction{ 35 | Action: goproxy.ConnectMitm, 36 | TLSConfig: goproxy.TLSConfigFromCA(&proxy.ser.conf.SslCert), 37 | } 38 | proxy.GoProxy.OnRequest().HandleConnectFunc(proxy.httpsHandle) 39 | } 40 | proxy.GoProxy.OnRequest().DoFunc(my_requestHanderFunc) 41 | proxy.GoProxy.OnResponse().DoFunc(proxy.onResponse) 42 | return proxy 43 | } 44 | 45 | func my_requestHanderFunc(r *http.Request, ctx *goproxy.ProxyCtx) (*http.Request, *http.Response) { 46 | log.Println("trace_my_requestHanderFunc call url:", r.URL.String()) 47 | return r, nil 48 | } 49 | 50 | const PROXY_CTX_NAME = "X-PPROXY-CTX-ID" 51 | 52 | func (proxy *HttpProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) { 53 | // fmt.Println("call url:",req.URL.String()) 54 | proxy.GoProxy.ServeHTTP(rw, req) 55 | } 56 | 57 | func (proxy *HttpProxy) httpsHandle(host string, ctx *goproxy.ProxyCtx) (*goproxy.ConnectAction, string) { 58 | log.Println("https conn", host, ctx.Req.URL.String()) 59 | return proxy.goproxyMitmConnect, host 60 | } 61 | 62 | func (proxy *HttpProxy) RoundTrip(ctx *requestCtx) { 63 | sid := strconv.FormatInt(ctx.SessionID, 10) 64 | ctx.Req.Header.Set(PROXY_CTX_NAME, sid) 65 | func() { 66 | proxy.mu.Lock() 67 | defer proxy.mu.Unlock() 68 | proxy.ctxs[sid] = ctx 69 | }() 70 | 71 | defer func() { 72 | proxy.mu.Lock() 73 | defer proxy.mu.Unlock() 74 | if _, has := proxy.ctxs[sid]; has { 75 | delete(proxy.ctxs, sid) 76 | } 77 | }() 78 | 79 | if ctx.Req.Header.Get("Upgrade") != "" { 80 | proxy.roundTripUpgrade(ctx) 81 | return 82 | } 83 | proxy.ServeHTTP(ctx.Rw, ctx.Req) 84 | } 85 | 86 | func (proxy *HttpProxy) getReqCtx(req *http.Request) *requestCtx { 87 | sid := req.Header.Get(PROXY_CTX_NAME) 88 | if sid == "" { 89 | return nil 90 | } 91 | proxy.mu.RLock() 92 | defer proxy.mu.RUnlock() 93 | if ctx, has := proxy.ctxs[sid]; has { 94 | return ctx 95 | } 96 | return nil 97 | } 98 | 99 | func (proxy *HttpProxy) onResponse(resp *http.Response, ctx *goproxy.ProxyCtx) *http.Response { 100 | if resp == nil || resp.Request == nil { 101 | return resp 102 | } 103 | reqCtx := proxy.getReqCtx(resp.Request) 104 | 105 | if reqCtx != nil { 106 | reqCtx.saveResponse(resp) 107 | } 108 | return resp 109 | } 110 | 111 | func (proxy *HttpProxy) roundTripUpgrade(ctx *requestCtx) (err error) { 112 | // save it,so we know it has been closed 113 | defer func() { 114 | resp := &http.Response{ 115 | Request: ctx.Req, 116 | Header: make(http.Header), 117 | Body: nil, 118 | } 119 | ctx.saveResponse(resp) 120 | }() 121 | 122 | reqDump, err := httputil.DumpRequest(ctx.Req, false) 123 | if err != nil { 124 | ctx.Msg = "dump req failed:" + err.Error() 125 | return 126 | } 127 | ctx.SetTimePoint("startDial") 128 | dia, err := net.Dial("tcp", ctx.DestAddr()) 129 | if err != nil { 130 | ctx.Msg = "dia connect " + ctx.DestAddr() + " failed!" + err.Error() 131 | return 132 | } 133 | defer dia.Close() 134 | _, err = dia.Write(reqDump) 135 | if err != nil { 136 | return 137 | } 138 | 139 | hijack, _ := ctx.Rw.(http.Hijacker) 140 | conn, _, _ := hijack.Hijack() 141 | 142 | errc := make(chan error, 2) 143 | 144 | cp := func(dst io.Writer, src io.Reader) { 145 | _, err := io.Copy(dst, src) 146 | errc <- err 147 | 148 | time.AfterFunc(3*time.Second, func() { 149 | dia.Close() 150 | conn.Close() 151 | }) 152 | } 153 | 154 | go cp(dia, conn) 155 | go cp(conn, dia) 156 | <-errc 157 | return 158 | } 159 | -------------------------------------------------------------------------------- /serve/reqCtx.go: -------------------------------------------------------------------------------- 1 | package serve 2 | 3 | import ( 4 | "encoding/base64" 5 | "errors" 6 | "fmt" 7 | "log" 8 | "net/http" 9 | "net/http/httputil" 10 | "net/url" 11 | "strconv" 12 | "strings" 13 | "time" 14 | ) 15 | 16 | // requestCtx 一次http请求的数据结构 17 | type requestCtx struct { 18 | RemoteAddr string 19 | Req *http.Request 20 | Rw http.ResponseWriter 21 | 22 | Host string // eg www.baidu.com 23 | Port int // eg 80 24 | 25 | User *User 26 | Docid int 27 | IsRePlay bool 28 | 29 | SessionID int64 30 | HasBroadcast bool 31 | FormPost *url.Values 32 | ClientSession *clientSession 33 | 34 | OriginURL string 35 | logData map[any]any 36 | Msg string 37 | 38 | ser *ProxyServe 39 | startTime time.Time 40 | timeDurations map[string]time.Duration 41 | hasPrint bool 42 | } 43 | 44 | // NewRequestCtx 构建一个新的请求 45 | func NewRequestCtx(ser *ProxyServe, rw http.ResponseWriter, req *http.Request) *requestCtx { 46 | ctx := &requestCtx{} 47 | ctx.Req = req 48 | ctx.ser = ser 49 | ctx.Rw = rw 50 | ctx.SessionID = ser.reqNum 51 | 52 | ctx.logData = make(map[any]any) 53 | ctx.timeDurations = make(map[string]time.Duration) 54 | 55 | ctx.FormPost = &url.Values{} 56 | ctx.init() 57 | ctx.startTime = time.Now() 58 | return ctx 59 | } 60 | 61 | func (ctx *requestCtx) init() { 62 | if ctx.Req == nil { 63 | return 64 | } 65 | fixRequest(ctx.Req) 66 | req := ctx.Req 67 | ctx.Host, ctx.Port, _ = getHostPortFromReq(req) 68 | 69 | ctx.User = getAuthorInfo(req) 70 | ctx.FormPost = getPostData(req) 71 | 72 | ctx.OriginURL = req.URL.String() 73 | ctx.IsRePlay = len(req.Header.Get(REPLAY_FLAG)) > 0 74 | ctx.SetLog("url", req.URL.String()) 75 | 76 | ctx.RemoteAddr = req.RemoteAddr 77 | 78 | if _replayAddr := req.Header.Get(REPLAY_REMOTEADDR); _replayAddr != "" { 79 | ctx.RemoteAddr = _replayAddr 80 | } 81 | if _replayUser := req.Header.Get(REPLAY_USER_NAME); _replayUser != "" { 82 | ctx.User = &User{Name: _replayUser, SkipCheckPsw: true} 83 | } 84 | ctx.Docid = ctx.getNewDocid() 85 | ctx.ser.regirestReq(ctx) 86 | } 87 | 88 | func fixRequest(req *http.Request) { 89 | if req.Method != "CONNECT" && !req.URL.IsAbs() { 90 | urlOrigin := req.URL.String() 91 | urlStr := "http://" + req.Host + req.URL.Path 92 | if req.URL.RawQuery != "" { 93 | urlStr += "?" + req.URL.RawQuery 94 | } 95 | var err error 96 | req.URL, err = url.Parse(urlStr) 97 | if err != nil { 98 | log.Println("fix url failed,originUrl:", urlOrigin, "err:", err) 99 | return 100 | } 101 | } 102 | } 103 | 104 | func (ctx *requestCtx) IsLocalRequest() bool { 105 | isLocalReq := ctx.Port == ctx.ser.conf.Port 106 | if isLocalReq { 107 | isLocalReq = IsLocalIP(ctx.Host) 108 | } 109 | return isLocalReq 110 | } 111 | 112 | func (ctx *requestCtx) GetIp() string { 113 | hostInfo := strings.Split(ctx.RemoteAddr, ":") 114 | return hostInfo[0] 115 | } 116 | 117 | func (ctx *requestCtx) PrintLog() { 118 | reqID := 0 119 | if ctx.ClientSession != nil { 120 | reqID = ctx.ClientSession.RequestNum 121 | } 122 | log.Println( 123 | "session_id:", ctx.SessionID, 124 | "remote:", ctx.RemoteAddr, 125 | "reqId:", reqID, 126 | "docid:", ctx.Docid, 127 | "uname:", ctx.User.Name, 128 | "broadcast:", ctx.HasBroadcast, 129 | "startTime:", ctx.startTime.Unix(), 130 | "timeUsed:", fmt.Sprintf("%.3fs", time.Since(ctx.startTime).Seconds()), 131 | "data:", ctx.logData, 132 | "times:", ctx.timeDurations, 133 | ) 134 | } 135 | 136 | func (ctx *requestCtx) RoundTrip() { 137 | defer func() { 138 | ctx.hasPrint = true 139 | ctx.SetLog("logType", "defer") 140 | ctx.PrintLog() 141 | }() 142 | 143 | time.AfterFunc(10*time.Second, func() { 144 | if !ctx.hasPrint { 145 | ctx.SetLog("logType", "timeout10") 146 | ctx.PrintLog() 147 | } 148 | }) 149 | 150 | removeHeader(ctx.Req) 151 | rewriteCode := ctx.ser.reqRewrite(ctx) 152 | 153 | ctx.HasBroadcast = ctx.ser.broadcastReq(ctx) 154 | 155 | // reqDump, _ := httputil.DumpRequest(ctx.Req, true) 156 | // fmt.Println("req dump3:\n",string(reqDump)) 157 | 158 | ctx.SetLog("js_rewrite_code", rewriteCode) 159 | 160 | ctx.saveRequestData() 161 | // 异步的会导致req.body dump不了,先暂时这样,对接口会有一些影响 162 | // time.AfterFunc(1*time.Second, ctx.saveRequestData) 163 | 164 | if rewriteCode != 200 && rewriteCode != 304 { 165 | ctx.badGateway(errors.New("rewrite failed")) 166 | return 167 | } 168 | ctx.ser.proxy.RoundTrip(ctx) 169 | } 170 | 171 | func (ctx *requestCtx) badGateway(err error) { 172 | ctx.SetLog("errMsg", fmt.Sprintf("%s", err)) 173 | ctx.Rw.WriteHeader(http.StatusBadGateway) 174 | ctx.Rw.Write([]byte("pproxy error")) 175 | } 176 | 177 | func (ctx *requestCtx) DestAddr() string { 178 | return fmt.Sprintf("%s:%d", ctx.Host, ctx.Port) 179 | } 180 | 181 | func (ctx *requestCtx) saveRequestData() { 182 | if ctx.ser.conf.ResponseSave == responseSaveAll || 183 | (ctx.ser.conf.ResponseSave == responseSaveHasBroad && ctx.HasBroadcast) { 184 | logdata := KvType{} 185 | logdata["host"] = ctx.Req.Host 186 | logdata["schema"] = ctx.Req.URL.Scheme 187 | logdata["header"] = map[string][]string(ctx.Req.Header) 188 | logdata["url"] = ctx.Req.URL.String() 189 | logdata["url_origin"] = ctx.OriginURL 190 | logdata["path"] = ctx.Req.URL.Path 191 | // logdata["cookies"] = ctx.Req.Cookies() 192 | // logdata["now"] = time.Now().Unix() 193 | logdata["user"] = ctx.User.Name 194 | logdata["client_ip"] = ctx.RemoteAddr 195 | logdata["method"] = ctx.Req.Method 196 | logdata["form_get"] = ctx.Req.URL.Query() 197 | logdata["replay"] = ctx.IsRePlay 198 | logdata["msg"] = ctx.Msg 199 | logdata["id"] = strconv.Itoa(ctx.Docid) 200 | 201 | // 当无普通form post表单数据的时候,比如可能body也有数据 202 | // 比如request 的content-type=application/json 203 | dumpBody := len(*ctx.FormPost) == 0 204 | // fmt.Println("dumpBody",dumpBody) 205 | // dumpBody=false 206 | reqDump, errDump := httputil.DumpRequest(ctx.Req, dumpBody) 207 | if errDump != nil { 208 | ctx.SetLog("dumpMsg", fmt.Sprintf("dump request failed,%s", errDump.Error())) 209 | reqDump = []byte(fmt.Sprintf("dump failed,%s", errDump.Error())) 210 | } 211 | logdata["dump"] = base64.StdEncoding.EncodeToString(reqDump) 212 | // buf := forgetRead(&ctx.Req.Body) 213 | // logdata["dump"] = base64.StdEncoding.EncodeToString(buf.Bytes()) 214 | 215 | logdata["form_post"] = ctx.FormPost 216 | 217 | tb := ctx.ser.mydb.GetkvStoreTable(KV_TABLE_REQ) 218 | data := newStoreType(logdata) 219 | err := tb.Save(IntToBytes(ctx.Docid), data) 220 | if err != nil { 221 | log.Println("save req failed:", err) 222 | } 223 | } 224 | } 225 | 226 | func (ctx *requestCtx) saveResponse(res *http.Response) { 227 | if ctx.Docid < 1 || res == nil { 228 | return 229 | } 230 | data := KvType{} 231 | data["now"] = time.Now().Unix() 232 | data["header"] = map[string][]string(res.Header) 233 | data["status"] = res.StatusCode 234 | data["content_length"] = res.ContentLength 235 | data["msg"] = ctx.Msg 236 | data["id"] = strconv.Itoa(ctx.Docid) 237 | 238 | resDump, dumpErr := httputil.DumpResponse(res, false) 239 | if dumpErr != nil { 240 | log.Println("dump res err", dumpErr) 241 | resDump = []byte("dump res failed") 242 | } 243 | data["dump"] = base64.StdEncoding.EncodeToString(resDump) 244 | // data["cookies"]=res.Cookies() 245 | 246 | body := []byte("pproxy skip") 247 | if res.Body != nil && res.ContentLength <= ctx.ser.MaxResSaveLength { 248 | buf := forgetRead(&res.Body) 249 | if res.Header.Get(contentEncoding) == "gzip" { 250 | body = []byte(gzipDocode(buf)) 251 | } else { 252 | body = buf.Bytes() 253 | } 254 | l := int64(len(body)) 255 | if l > ctx.ser.MaxResSaveLength { 256 | body = []byte(fmt.Sprintf("pproxy skip,body too large,[len=%d]", l)) 257 | } 258 | } 259 | data["body"] = base64.StdEncoding.EncodeToString(body) 260 | 261 | tb := ctx.ser.mydb.GetkvStoreTable(KV_TABLE_RES) 262 | storeData := newStoreType(data) 263 | err := tb.Save(IntToBytes(ctx.Docid), storeData) 264 | 265 | log.Println("save_res", ctx.SessionID, "docid=", ctx.Docid, "body_len=", len(data["body"].(string)), err) 266 | } 267 | 268 | func (ctx *requestCtx) SetLog(k, v any) { 269 | ctx.logData[k] = v 270 | } 271 | 272 | func (ctx *requestCtx) SetTimePoint(key string) { 273 | ctx.timeDurations[key] = time.Since(ctx.startTime) 274 | } 275 | 276 | func (ctx *requestCtx) getNewDocid() int { 277 | idStr := fmt.Sprintf("%s%d", time.Now().Format("200601021504"), ctx.ser.reqNum) 278 | id, err := parseDocID(idStr) 279 | if err == nil { 280 | return id 281 | } 282 | log.Println("GetNewDocid failed", idStr, err) 283 | return int(time.Now().UnixNano() + ctx.ser.reqNum) 284 | } 285 | -------------------------------------------------------------------------------- /serve/req_modifer.go: -------------------------------------------------------------------------------- 1 | package serve 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "log" 7 | "os" 8 | "strings" 9 | "sync" 10 | 11 | "github.com/hidu/goutils/fs" 12 | "github.com/robertkrimen/otto" 13 | ) 14 | 15 | var rewriteJsTpl = Assest.GetContent("res/sjs/req_rewrite.js") 16 | 17 | /* 18 | * request动态修改引擎 19 | * 使用javascript 来对请求进行修改 20 | */ 21 | type requestModifier struct { 22 | mu sync.RWMutex 23 | jsVm *otto.Otto 24 | jsFns map[string]*otto.Value 25 | canMod bool 26 | ser *ProxyServe 27 | } 28 | 29 | func NewRequestModifier(ser *ProxyServe) *requestModifier { 30 | reqMod := &requestModifier{ 31 | jsVm: otto.New(), 32 | jsFns: make(map[string]*otto.Value), 33 | ser: ser, 34 | } 35 | return reqMod 36 | } 37 | 38 | func (reqMod *requestModifier) getJsPath(name string) string { 39 | baseName := fmt.Sprintf("%s/req_rewrite_%d", reqMod.ser.configDir, reqMod.ser.conf.Port) 40 | if name == "" { 41 | return fmt.Sprintf("%s.js", baseName) 42 | } 43 | return fmt.Sprintf("%s_%s.js", baseName, name) 44 | } 45 | 46 | func (reqMod *requestModifier) tryLoadJs(name string) (err error) { 47 | jsContent, err := reqMod.getJsContent(name) 48 | if jsContent != "" && err == nil { 49 | err = reqMod.parseJs(jsContent, name, false) 50 | if err != nil { 51 | log.Println("load rewrite js failed:", err) 52 | return err 53 | } 54 | log.Println("load rewrite js[", name, "] suc") 55 | } 56 | return nil 57 | } 58 | 59 | func (reqMod *requestModifier) loadAllJs() error { 60 | if !reqMod.ser.conf.ModifyRequest { 61 | log.Println("ignore requestModifier loadAllJs") 62 | return nil 63 | } 64 | names := []string{""} 65 | for _, user := range reqMod.ser.Users { 66 | names = append(names, user.Name) 67 | } 68 | for _, name := range names { 69 | err := reqMod.tryLoadJs(name) 70 | if err != nil { 71 | return err 72 | } 73 | } 74 | return nil 75 | } 76 | 77 | func (reqMod *requestModifier) getJsContent(name string) (content string, err error) { 78 | jsPath := reqMod.getJsPath(name) 79 | if fs.FileExists(jsPath) { 80 | script, err := os.ReadFile(jsPath) 81 | if err == nil { 82 | return string(script), nil 83 | } 84 | return "", err 85 | } 86 | return "", nil 87 | } 88 | 89 | func (reqMod *requestModifier) CanMod() bool { 90 | return reqMod.canMod 91 | } 92 | 93 | func (reqMod *requestModifier) parseJs(jsStr string, name string, save2File bool) error { 94 | jsStr = strings.TrimSpace(jsStr) 95 | rewriteJs := strings.Replace(rewriteJsTpl, "CUSTOM_JS", jsStr, 1) 96 | rewriteJs = strings.Replace(rewriteJs, "PPROXY_HOST", fmt.Sprintf("127.0.0.1:%d", reqMod.ser.conf.Port), 1) 97 | 98 | reqMod.mu.Lock() 99 | defer reqMod.mu.Unlock() 100 | if reqMod.ser.Debug { 101 | log.Println("jsvm_execute:", rewriteJs) 102 | } 103 | reqMod.jsVm.Run(rewriteJs) 104 | jsFn, err := reqMod.jsVm.Get("pproxy_rewrite") 105 | if err != nil { 106 | log.Println("rewrite js init error:", err) 107 | return err 108 | } 109 | 110 | if strings.HasPrefix(jsStr, "//ignore") { 111 | if _, has := reqMod.jsFns[name]; has { 112 | delete(reqMod.jsFns, name) 113 | } 114 | log.Println("req_mod [", name, "] ignore") 115 | } else { 116 | reqMod.jsFns[name] = &jsFn 117 | log.Println("req_mod [", name, "] register suc") 118 | } 119 | reqMod.canMod = true 120 | if save2File { 121 | jsPath := reqMod.getJsPath(name) 122 | err = fs.FilePutContents(jsPath, []byte(jsStr)) 123 | log.Println("save rewritejs ", jsPath, err) 124 | } 125 | return err 126 | } 127 | 128 | func (reqMod *requestModifier) getJsFnByName(name string) (*otto.Value, error) { 129 | names := []string{name, ""} 130 | for _, name := range names { 131 | if jsFn, has := reqMod.jsFns[name]; has { 132 | return jsFn, nil 133 | } 134 | } 135 | return nil, errors.New("no rewrite rules") 136 | } 137 | 138 | func (reqMod *requestModifier) rewrite(data map[string]any, name string) (map[string]any, error) { 139 | reqMod.mu.Lock() 140 | defer reqMod.mu.Unlock() 141 | 142 | reqJsObj, _ := reqMod.jsVm.Object(`req={}`) 143 | reqJsObj.Set("origin", data) 144 | 145 | jsFn, err := reqMod.getJsFnByName(name) 146 | 147 | if err != nil { 148 | return nil, err 149 | } 150 | 151 | defer func() { 152 | if caught := recover(); caught != nil { 153 | log.Println("fatal:requestModifer recover:", caught) 154 | } 155 | }() 156 | 157 | js_ret, err_js := (*jsFn).Call(*jsFn, reqJsObj) 158 | 159 | if err_js != nil { 160 | log.Println("parse js error:", err_js) 161 | return nil, err_js 162 | } 163 | if !js_ret.IsObject() { 164 | log.Println("wrong req_rewirte return value,not object:", js_ret) 165 | return nil, fmt.Errorf("wrong req_rewirte return value,not object.%t", js_ret) 166 | } 167 | obj, export_err := js_ret.Export() 168 | 169 | if export_err != nil { 170 | return nil, export_err 171 | } 172 | reqObjNew := obj.(map[string]any) 173 | return reqObjNew, nil 174 | } 175 | -------------------------------------------------------------------------------- /serve/req_replay.go: -------------------------------------------------------------------------------- 1 | package serve 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "net/url" 7 | "strings" 8 | ) 9 | 10 | const ( 11 | REPLAY_FLAG = "Proxy-pproxy_replay" 12 | REPLAY_REMOTEADDR = "Proxy-pproxy_remoteaddr" 13 | REPLAY_USER_NAME = "Proxy-pproxy_user" 14 | ) 15 | 16 | func (ctx *webRequestCtx) handleReplay() { 17 | if ctx.req.Method == "POST" { 18 | ctx.reqReplayPost() 19 | return 20 | } 21 | docidStr := strings.TrimSpace(ctx.req.FormValue("id")) 22 | if docidStr == "" { 23 | ctx.w.WriteHeader(http.StatusBadRequest) 24 | ctx.w.Write([]byte("empty id param")) 25 | return 26 | } 27 | docid, errInt := parseDocID(docidStr) 28 | if errInt != nil { 29 | ctx.w.WriteHeader(http.StatusInternalServerError) 30 | fmt.Fprintf(ctx.w, "param id[%s] error:\n%s", docidStr, errInt) 31 | return 32 | } 33 | reqDoc, _ := ctx.ser.getRequestByDocid(docid) 34 | if reqDoc == nil { 35 | ctx.w.WriteHeader(http.StatusNotFound) 36 | ctx.w.Write([]byte("request doc not found!")) 37 | return 38 | } 39 | _url := fmt.Sprintf("%s", reqDoc.Data["url"]) 40 | u, err := url.Parse(_url) 41 | if err != nil { 42 | ctx.w.WriteHeader(http.StatusInternalServerError) 43 | fmt.Fprintf(ctx.w, "parse url[%s] error\n%s", _url, err) 44 | return 45 | } 46 | u.RawQuery = "" 47 | 48 | ctx.values["req"] = reqDoc 49 | ctx.values["action_url"] = u.String() 50 | 51 | ctx.render("replay.html", true) 52 | } 53 | 54 | var replaySkipHeaders = map[string]int{"Content-Length": 1} 55 | 56 | func (ctx *webRequestCtx) reqReplayPost() { 57 | replay := ctx.req.FormValue("replay") 58 | basic := make(map[string]string) 59 | basic["action_url"] = strings.TrimSpace(ctx.req.FormValue("basic_action_url")) 60 | method := strings.TrimSpace(strings.ToUpper(ctx.req.FormValue("basic_method"))) 61 | basic["method"] = method 62 | 63 | host := strings.TrimSpace(ctx.req.FormValue("basic_host")) 64 | 65 | basicRemoteAddr := ctx.req.FormValue("basic_RemoteAddr") 66 | basicUser := ctx.req.FormValue("basic_user") 67 | 68 | header := getFormValuesWithPrefix(ctx.req.Form, "header_") 69 | get := getFormValuesWithPrefix(ctx.req.Form, "get_") 70 | post := getFormValuesWithPrefix(ctx.req.Form, "post_") 71 | 72 | formData := make(map[string]any) 73 | formData["basic"] = basic 74 | 75 | formData["header"] = header 76 | formData["get"] = get 77 | formData["post"] = post 78 | 79 | ctx.values["form"] = formData 80 | if replay == "direct" { 81 | ctx.render("replay_direct.html", true) 82 | return 83 | } 84 | reqBd := "" 85 | _url := basic["action_url"] 86 | 87 | if len(get) > 0 { 88 | formValues := make(url.Values) 89 | for k, v := range get { 90 | for _, _v := range v { 91 | formValues.Add(k, _v) 92 | } 93 | } 94 | if strings.Contains(_url, "?") { 95 | _url += "&" 96 | } else { 97 | _url += "?" 98 | } 99 | _url += formValues.Encode() 100 | } 101 | 102 | if len(post) > 0 { 103 | formValues := make(url.Values) 104 | for k, v := range post { 105 | for _, _v := range v { 106 | formValues.Add(k, _v) 107 | } 108 | } 109 | reqBd = formValues.Encode() 110 | } 111 | 112 | replayReq, err := http.NewRequest(method, _url, strings.NewReader(reqBd)) 113 | if err != nil { 114 | ctx.w.Write([]byte("build request failed\n" + err.Error())) 115 | return 116 | } 117 | if host != "" { 118 | replayReq.Host = host 119 | } 120 | replayReq.Header.Set(REPLAY_FLAG, "replay") 121 | 122 | replayReq.Header.Set(REPLAY_REMOTEADDR, basicRemoteAddr) 123 | replayReq.Header.Set(REPLAY_USER_NAME, basicUser) 124 | 125 | for k, v := range header { 126 | if _, has := replaySkipHeaders[k]; has { 127 | continue 128 | } 129 | replayReq.Header.Set(k, strings.Join(v, ";")) 130 | } 131 | ctx.ser.ServeHTTPProxy(ctx.w, replayReq) 132 | } 133 | -------------------------------------------------------------------------------- /serve/req_rewrite.go: -------------------------------------------------------------------------------- 1 | package serve 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "log" 8 | "net/http" 9 | "net/url" 10 | "strconv" 11 | "strings" 12 | ) 13 | 14 | func (ser *ProxyServe) reqRewriteByjs(reqCtx *requestCtx) int { 15 | modifer := ser.reqMod 16 | if !modifer.CanMod() { 17 | return 304 18 | } 19 | req := reqCtx.Req 20 | schema := req.URL.Scheme 21 | originURL := req.URL.String() 22 | originGetQuery := req.URL.Query() 23 | // /================================================================ 24 | headerKv := make(map[string]string) 25 | headerKv["method"] = req.Method 26 | headerKv["schema"] = schema 27 | headerKv["path"] = req.URL.Path 28 | 29 | _host, portInt, _ := getHostPortFromReq(req) 30 | 31 | headerKv["host"] = _host 32 | headerKv["port"] = strconv.Itoa(portInt) 33 | 34 | username := "" 35 | psw := "" 36 | if req.URL.User != nil { 37 | username = req.URL.User.Username() 38 | psw, _ = req.URL.User.Password() 39 | } 40 | headerKv["username"] = username 41 | 42 | headerKv["proxy_user"] = reqCtx.User.Name 43 | 44 | headerKv["password"] = psw 45 | 46 | // =================================================================== 47 | rewriteData := make(map[string]any) 48 | rewriteData["header"] = headerKv 49 | rewriteData["get"] = originGetQuery 50 | rewriteData["post"] = *reqCtx.FormPost 51 | 52 | _buf := forgetRead(&reqCtx.Req.Body) 53 | var rawBody string 54 | //  暂时只考虑gip的,其他的压缩就不支持了 55 | if req.Header.Get(contentEncoding) == "gzip" { 56 | rawBody = gzipDocode(_buf) 57 | } else { 58 | rawBody = _buf.String() 59 | } 60 | rewriteData["body"] = rawBody 61 | 62 | reqObjNew, rErr := modifer.rewrite(rewriteData, reqCtx.User.Name) 63 | if rErr != nil { 64 | log.Println("rewrite failed:", rErr) 65 | } 66 | 67 | headerKvNew := make(map[string]string) 68 | isHeaderChange := false 69 | 70 | skipHeader := false 71 | var err error 72 | 73 | urlStrNew := getMapValStr(reqObjNew, "url") 74 | 75 | if urlStrNew != "" { 76 | req.URL, err = url.Parse(urlStrNew) 77 | if err != nil || req.URL.Scheme != "http" { 78 | log.Println("new url wrong!url is:", urlStrNew, err) 79 | return 500 80 | } 81 | req.Host = req.URL.Host 82 | skipHeader = true 83 | } 84 | 85 | if !skipHeader { 86 | for k, v := range headerKv { 87 | _newVal := getMapValStr(reqObjNew, k) 88 | headerKvNew[k] = _newVal 89 | if _newVal != v { 90 | isHeaderChange = true 91 | } 92 | } 93 | } 94 | // ------------------------------------------------------- 95 | var getNew url.Values 96 | 97 | isGetChange := false 98 | 99 | if _get, has := reqObjNew["get"]; has { 100 | getNew = _reqMapToURLValue(_get) 101 | isGetChange = checkURLValuesChange(originGetQuery, getNew) 102 | } 103 | // ------------------------------------------------------- 104 | var postNew url.Values 105 | isPostChange := false 106 | 107 | if schema == "http" { 108 | if _post, has := reqObjNew["post"]; has { 109 | postNew = _reqMapToURLValue(_post) 110 | isPostChange = checkURLValuesChange(*reqCtx.FormPost, postNew) 111 | } 112 | } 113 | isBodyChange := false 114 | bodyNew := "" 115 | if _bodyNew, has := reqObjNew["body"]; has { 116 | bodyNew = _bodyNew.(string) 117 | isBodyChange = rawBody != bodyNew 118 | } 119 | 120 | hostAddr := getMapValStr(reqObjNew, "hostAddr") 121 | isHostAddrChange := hostAddr != "" 122 | 123 | if ser.Debug { 124 | fmt.Println("rewriteChange:", "is_get_change:", isGetChange, "new_get:", getNew, 125 | "isPostChange:", isPostChange, "new_post:", postNew, 126 | "isHostAddrChange:", isHostAddrChange, "newHostAddr:", hostAddr, 127 | "isBodyChange:", isBodyChange, 128 | ) 129 | } 130 | 131 | // /=============================================================================== 132 | if !isHeaderChange && !isGetChange && !isPostChange && !isHostAddrChange && !isBodyChange { 133 | return 304 134 | } 135 | // /=============================================================================== 136 | 137 | var urlBase string 138 | 139 | if isHeaderChange { 140 | // schema := headerKvNew["schema"] 141 | urlBase = schema + "://" 142 | 143 | if headerKvNew["username"] != "" { 144 | urlBase += fmt.Sprintf("%s:%s@", headerKvNew["username"], headerKvNew["password"]) 145 | } 146 | urlBase += headerKvNew["host"] 147 | if headerKvNew["port"] != "" && headerKvNew["port"] != "80" { 148 | urlBase += ":" + headerKvNew["port"] 149 | } 150 | urlBase += headerKvNew["path"] 151 | } else { 152 | if req.URL.RawQuery == "" { 153 | urlBase = originURL 154 | } else { 155 | urlBase = originURL[:len(originURL)-len(req.URL.RawQuery)-1] 156 | } 157 | } 158 | 159 | if isGetChange { 160 | urlBase += "?" + getNew.Encode() 161 | } else { 162 | urlBase += "?" + req.URL.RawQuery 163 | } 164 | 165 | if isHeaderChange || isGetChange { 166 | var urlErr error 167 | req.URL, urlErr = url.Parse(urlBase) 168 | if ser.Debug { 169 | log.Println("DEBUG req_rewrite,url_new:", urlBase, "req_new:", req.URL) 170 | } 171 | if urlErr != nil { 172 | return 502 173 | } 174 | 175 | req.Host = req.URL.Host 176 | } 177 | 178 | // //////////////////////////////////////////////////////////////////////////// 179 | 180 | if isPostChange || isBodyChange { 181 | buf := bytes.NewBuffer([]byte{}) 182 | var bodyData string 183 | if isPostChange { 184 | bodyData = postNew.Encode() 185 | } else if isBodyChange { 186 | bodyData = bodyNew 187 | } 188 | req.Header.Del("Content-Length") 189 | if req.Header.Get(contentEncoding) == "gzip" { 190 | tmp := gzipEncode([]byte(bodyData)).Bytes() 191 | buf.Write(tmp) 192 | } else { 193 | buf.WriteString(bodyData) 194 | } 195 | req.ContentLength = int64(buf.Len()) 196 | req.Body = io.NopCloser(buf).(io.ReadCloser) 197 | } 198 | 199 | // ////////////////////////////////////////////////////////////////////////// 200 | 201 | if isHostAddrChange { 202 | req.URL.Host = hostAddr 203 | if ser.Debug { 204 | log.Println("rewrite host addr:", req.URL.Host, "==>", hostAddr) 205 | } 206 | } 207 | return 200 208 | } 209 | 210 | func (ser *ProxyServe) reqRewrite(reqCtx *requestCtx) int { 211 | if !ser.conf.ModifyRequest { 212 | return 304 213 | } 214 | if reqCtx.Req.Method == "CONNECT" { 215 | return 304 216 | } 217 | originHost := reqCtx.Req.Host + "#" + reqCtx.Req.URL.Host 218 | statusCode1 := ser.reqRewriteByjs(reqCtx) 219 | newHost := reqCtx.Req.Host + "#" + reqCtx.Req.URL.Host 220 | 221 | if ser.Debug { 222 | log.Println("rewrte_debug:\n", "originHost:", originHost, "\nnewHost:", newHost, "\n") 223 | } 224 | 225 | statusCode2 := 304 226 | if originHost == newHost { 227 | statusCode2 = ser.reqRewriteByHosts(reqCtx.Req) 228 | } 229 | if statusCode1 == 200 || statusCode2 == 200 { 230 | return 200 231 | } 232 | if statusCode1 >= 500 || statusCode2 >= 500 { 233 | return 502 234 | } 235 | return 304 236 | } 237 | 238 | func (ser *ProxyServe) reqRewriteByHosts(req *http.Request) int { 239 | if ser.hosts == nil { 240 | return 304 241 | } 242 | if host, has := ser.hosts[req.URL.Host]; has { 243 | log.Println("rewrite host:", req.URL.Host, "==>", host) 244 | req.URL.Host = host 245 | return 200 246 | } 247 | hostInfo := strings.Split(req.URL.Host, ":") 248 | if len(hostInfo) == 1 { 249 | if req.URL.Scheme == "http" { 250 | hostInfo = append(hostInfo, "80") 251 | } 252 | } 253 | reqHost := strings.Join(hostInfo, ":") 254 | if host, has := ser.hosts[reqHost]; has { 255 | log.Println("rewrite host:", req.Host, "==>", host) 256 | req.URL.Host = host 257 | return 200 258 | } 259 | 260 | if host, has := ser.hosts[hostInfo[0]]; has { 261 | log.Println("rewrite host:", req.Host, "==>", host) 262 | req.URL.Host = host 263 | if !strings.Contains(host, ":") { 264 | req.URL.Host += ":" + hostInfo[1] 265 | } 266 | return 200 267 | } 268 | return 304 269 | } 270 | 271 | func _reqMapToURLValue(values any) url.Values { 272 | uValues := make(url.Values) 273 | if values == nil { 274 | return uValues 275 | } 276 | vs := values.(map[string]any) 277 | 278 | for k, arr := range vs { 279 | switch value := arr.(type) { 280 | case []any: 281 | for _, v := range value { 282 | uValues.Add(k, fmt.Sprintf("%v", v)) 283 | } 284 | case any: 285 | uValues.Set(k, fmt.Sprintf("%v", value)) 286 | default: 287 | log.Println("unkonw type:", value) 288 | } 289 | } 290 | return uValues 291 | } 292 | -------------------------------------------------------------------------------- /serve/serve.go: -------------------------------------------------------------------------------- 1 | package serve 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "math/rand" 7 | "net/http" 8 | "net/http/httputil" 9 | "os" 10 | "path/filepath" 11 | "sync" 12 | "sync/atomic" 13 | "time" 14 | 15 | "github.com/hidu/goutils/fs" 16 | "github.com/hidu/goutils/time_util" 17 | ) 18 | 19 | type ProxyServe struct { 20 | mydb *kvStore 21 | proxy *HttpProxy 22 | 23 | wsSer *wsServer 24 | 25 | startTime time.Time 26 | 27 | MaxResSaveLength int64 28 | 29 | mu sync.RWMutex 30 | 31 | Debug bool 32 | 33 | conf *Config 34 | configDir string 35 | hosts configHosts 36 | 37 | Users map[string]*User 38 | ProxyClients map[string]*clientSession 39 | reqNum int64 40 | 41 | reqMod *requestModifier 42 | } 43 | 44 | type KvType map[string]any 45 | 46 | func (ser *ProxyServe) ServeHTTP(w http.ResponseWriter, req *http.Request) { 47 | atomic.AddInt64(&ser.reqNum, 1) 48 | 49 | // reqDump, _ := httputil.DumpRequest(req, true) 50 | // fmt.Println("req dump:\n",string(reqDump)) 51 | 52 | ctx := NewRequestCtx(ser, w, req) 53 | if ctx.Host == "p.info" || ctx.Host == "pproxy.info" { 54 | ser.handleUserInfo(w, req) 55 | return 56 | } 57 | 58 | if ctx.Host == "pproxy.man" || ctx.Host == "pproxy.com" || ctx.IsLocalRequest() { 59 | ser.handleLocalReq(w, req) 60 | } else { 61 | if ser.Debug { 62 | reqDumpDebug, _ := httputil.DumpRequest(req, req.Method == "GET") 63 | log.Println("DEBUG req BEFORE:\nurl_full:", req.URL.String(), "\nschema:", req.URL.Scheme, "\n", string(reqDumpDebug), "\n\n") 64 | } 65 | if !ser.checkHTTPAuth(ctx) { 66 | ctx.SetLog("msg", "login required") 67 | ctx.Rw.Header().Set("Proxy-Authenticate", "Basic realm=auth required") 68 | ctx.Rw.WriteHeader(http.StatusProxyAuthRequired) 69 | ctx.Rw.Write([]byte("auth required")) 70 | return 71 | } 72 | ctx.RoundTrip() 73 | } 74 | } 75 | 76 | // for replay 77 | func (ser *ProxyServe) ServeHTTPProxy(w http.ResponseWriter, req *http.Request) { 78 | atomic.AddInt64(&ser.reqNum, 1) 79 | ctx := NewRequestCtx(ser, w, req) 80 | ctx.RoundTrip() 81 | } 82 | 83 | func (ser *ProxyServe) Start() { 84 | addr := fmt.Sprintf("%s:%d", "", ser.conf.Port) 85 | fmt.Println("proxy listen at ", addr) 86 | defer log.Println("pproxy exit") 87 | 88 | ser.wsInit() 89 | 90 | wg := new(sync.WaitGroup) 91 | wg.Add(1) 92 | go func() { 93 | err := http.ListenAndServe(addr, ser) 94 | log.Println(err) 95 | fmt.Println(err) 96 | wg.Done() 97 | }() 98 | 99 | wg.Add(1) 100 | go func() { 101 | ser.startAdmin() 102 | wg.Done() 103 | }() 104 | 105 | wg.Wait() 106 | } 107 | 108 | func (ser *ProxyServe) startAdmin() { 109 | if ser.conf.Port == ser.conf.AdminPort { 110 | return 111 | } 112 | addr := fmt.Sprintf(":%d", ser.conf.AdminPort) 113 | fmt.Println("admin http service listen at ", addr) 114 | httpSer := http.NewServeMux() 115 | httpSer.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) { 116 | ser.handleLocalReq(w, req) 117 | }) 118 | http.ListenAndServe(addr, httpSer) 119 | } 120 | 121 | func (ser *ProxyServe) getResponseByDocid(docid int) (resData *StoreType, err error) { 122 | tb := ser.mydb.GetkvStoreTable(KV_TABLE_RES) 123 | return tb.Get(IntToBytes(docid)) 124 | } 125 | 126 | func (ser *ProxyServe) getRequestByDocid(docid int) (reqData *StoreType, err error) { 127 | tb := ser.mydb.GetkvStoreTable(KV_TABLE_REQ) 128 | return tb.Get(IntToBytes(docid)) 129 | } 130 | 131 | func (ser *ProxyServe) getHostsFilePath() string { 132 | return fmt.Sprintf("%s/hosts_%d", ser.configDir, ser.conf.Port) 133 | } 134 | 135 | func (ser *ProxyServe) loadHosts() { 136 | ser.mu.Lock() 137 | defer ser.mu.Unlock() 138 | hostsPath := ser.getHostsFilePath() 139 | log.Println("load hosts:", hostsPath) 140 | ser.hosts, _ = loadHosts(hostsPath) 141 | } 142 | 143 | func NewProxyServe(confPath string, port int) (*ProxyServe, error) { 144 | conf, err := LoadConfig(confPath) 145 | if err != nil { 146 | log.Println("load config faield", err) 147 | return nil, err 148 | } 149 | if port > 0 && port < 65535 { 150 | conf.Port = port 151 | } 152 | 153 | absPath, err := filepath.Abs(confPath) 154 | if err != nil { 155 | log.Println("get config path failed", confPath) 156 | return nil, err 157 | } 158 | GetVersion() 159 | os.Chdir(filepath.Dir(absPath)) 160 | setupLog(conf.DataDir, conf.Port) 161 | 162 | proxy := new(ProxyServe) 163 | proxy.configDir = filepath.Dir(absPath) 164 | proxy.Users, _ = loadUsers(proxy.configDir + "/users") 165 | 166 | conf.FileDir, _ = filepath.Abs(conf.FileDir) 167 | 168 | proxy.conf = conf 169 | 170 | proxy.reqMod = NewRequestModifier(proxy) 171 | err = proxy.reqMod.loadAllJs() 172 | if err != nil { 173 | return nil, err 174 | } 175 | 176 | proxy.loadHosts() 177 | 178 | dbPath := fmt.Sprintf("%s/%d.db", conf.DataDir, conf.Port) 179 | 180 | // proxy.mydb = NewTieDb(fmt.Sprintf("%s/%d/", conf.DataDir, conf.Port), conf.DataStoreDay) 181 | proxy.mydb, err = newKvStore(dbPath) 182 | if err != nil { 183 | log.Fatalln("init db failed", err) 184 | } 185 | 186 | proxy.startTime = time.Now() 187 | proxy.MaxResSaveLength = 2 * 1024 * 1024 188 | 189 | rand.Seed(time.Now().UnixNano()) 190 | 191 | proxy.ProxyClients = make(map[string]*clientSession) 192 | proxy.proxy = NewHttpProxy(proxy) 193 | 194 | time_util.SetInterval(func() { 195 | proxy.cleanExpiredSession() 196 | }, 60) 197 | proxy.mydb.StartGcTimer(60, int64(conf.DataStoreDay*86400)) 198 | return proxy, nil 199 | } 200 | 201 | func setupLog(dataDir string, port int) { 202 | logPath := fmt.Sprintf("%s/%d.log", dataDir, port) 203 | 204 | logFile, err := os.OpenFile(logPath, os.O_CREATE|os.O_RDWR|os.O_APPEND, 0644) 205 | if err != nil { 206 | log.Println("create log file failed [", logPath, "]", err) 207 | os.Exit(2) 208 | } 209 | log.SetOutput(logFile) 210 | 211 | time_util.SetInterval(func() { 212 | if !fs.FileExists(logPath) { 213 | logFile.Close() 214 | logFile, _ = os.OpenFile(logPath, os.O_CREATE|os.O_RDWR|os.O_APPEND, 0644) 215 | log.SetOutput(logFile) 216 | } 217 | }, 30) 218 | } 219 | -------------------------------------------------------------------------------- /serve/sessions.go: -------------------------------------------------------------------------------- 1 | package serve 2 | 3 | import ( 4 | "log" 5 | "time" 6 | ) 7 | 8 | type clientSession struct { 9 | Ip string 10 | Port string 11 | RequestNum int 12 | FirstRequestTime time.Time 13 | LastRequestTime time.Time 14 | User *User 15 | } 16 | 17 | func (ser *ProxyServe) regirestReq(reqCtx *requestCtx) { 18 | ip := reqCtx.GetIp() 19 | now := time.Now() 20 | ser.mu.Lock() 21 | defer ser.mu.Unlock() 22 | var session *clientSession 23 | client, has := ser.ProxyClients[ip] 24 | if has { 25 | session = client 26 | } else { 27 | session = &clientSession{ 28 | Ip: ip, 29 | RequestNum: 0, 30 | FirstRequestTime: now, 31 | LastRequestTime: now, 32 | } 33 | } 34 | if reqCtx.User.Name == "" && session.User != nil { 35 | reqCtx.User = session.User 36 | } else if reqCtx.User.Name != "" { 37 | session.User = reqCtx.User 38 | } 39 | 40 | session.LastRequestTime = now 41 | session.RequestNum++ 42 | if ser.Debug { 43 | log.Println("session_debug:", session) 44 | } 45 | ser.ProxyClients[ip] = session 46 | 47 | reqCtx.ClientSession = session 48 | 49 | if !has { 50 | ser.wsSer.broadProxyClientNum() 51 | } 52 | } 53 | 54 | func (ser *ProxyServe) cleanExpiredSession() { 55 | ser.mu.Lock() 56 | defer ser.mu.Unlock() 57 | now := time.Now() 58 | deleteIps := []string{} 59 | for ip, session := range ser.ProxyClients { 60 | t := now.Sub(session.LastRequestTime) 61 | if t.Minutes() > 10 { 62 | deleteIps = append(deleteIps, ip) 63 | } 64 | } 65 | for _, ip := range deleteIps { 66 | delete(ser.ProxyClients, ip) 67 | log.Println("session expired:ip=", ip) 68 | } 69 | ser.wsSer.broadProxyClientNum() 70 | } 71 | -------------------------------------------------------------------------------- /serve/util.go: -------------------------------------------------------------------------------- 1 | package serve 2 | 3 | import ( 4 | "bytes" 5 | "compress/gzip" 6 | // "encoding/base64" 7 | "encoding/binary" 8 | "encoding/json" 9 | "errors" 10 | "fmt" 11 | "io" 12 | "log" 13 | "net" 14 | "net/http" 15 | "net/url" 16 | "strconv" 17 | "strings" 18 | 19 | // "gopkg.in/vmihailenco/msgpack.v2" 20 | ) 21 | 22 | // Int64ToBytes int64转换为byte 23 | func Int64ToBytes(i int64) []byte { 24 | var buf = make([]byte, 8) 25 | binary.BigEndian.PutUint64(buf, uint64(i)) 26 | return buf 27 | } 28 | 29 | // IntToBytes int转换为byte 30 | func IntToBytes(i int) []byte { 31 | var buf = make([]byte, 8) 32 | binary.BigEndian.PutUint64(buf, uint64(i)) 33 | return buf 34 | } 35 | 36 | // IsLocalIP 判断一个host是否本地ip 37 | func IsLocalIP(host string) bool { 38 | ips, _ := net.LookupIP(host) 39 | for _, ip := range ips { 40 | if ip.IsLoopback() { 41 | return true 42 | } 43 | } 44 | if addrs, err := net.InterfaceAddrs(); err == nil { 45 | for _, addr := range addrs { 46 | _, ipG, err := net.ParseCIDR(addr.String()) 47 | if err == nil { 48 | for _, ip := range ips { 49 | if ipG.Contains(ip) { 50 | return true 51 | } 52 | } 53 | } 54 | } 55 | } 56 | return false 57 | } 58 | 59 | func forgetRead(reader *io.ReadCloser) *bytes.Buffer { 60 | buf := bytes.NewBuffer([]byte{}) 61 | io.Copy(buf, *reader) 62 | *reader = io.NopCloser(buf).(io.ReadCloser) 63 | return bytes.NewBuffer(buf.Bytes()) 64 | } 65 | 66 | func dataEncode(data any) []byte { 67 | bf, err := json.Marshal(data) 68 | if err != nil { 69 | log.Println("data_encode_err", err) 70 | return bf 71 | } 72 | return bf 73 | } 74 | 75 | func dataDecode(dataInput []byte, out any) error { 76 | if len(dataInput) == 0 { 77 | return errors.New("empty dataInput") 78 | } 79 | err := json.Unmarshal(dataInput, &out) 80 | if err != nil { 81 | log.Println("json_decode_err:", err, "dataInput:", string(dataInput)) 82 | return err 83 | } 84 | return err 85 | } 86 | 87 | func getMapValStr(m map[string]any, k string) string { 88 | if val, has := m[k]; has { 89 | return fmt.Sprintf("%s", val) 90 | } 91 | return "" 92 | } 93 | 94 | func gzipDocode(buf *bytes.Buffer) string { 95 | if buf.Len() < 1 { 96 | return "" 97 | } 98 | gr, err := gzip.NewReader(buf) 99 | defer gr.Close() 100 | if err == nil { 101 | bdBt, _ := io.ReadAll(gr) 102 | return string(bdBt) 103 | } 104 | log.Println("unzip body failed", err) 105 | return "" 106 | } 107 | 108 | func gzipEncode(data []byte) *bytes.Buffer { 109 | buf := bytes.NewBuffer([]byte{}) 110 | gw := gzip.NewWriter(buf) 111 | defer gw.Close() 112 | gw.Write(data) 113 | return buf 114 | } 115 | 116 | func parseURLInputAsSlice(input string) []string { 117 | arr := strings.Split(input, "|") 118 | var result []string 119 | for _, val := range arr { 120 | val = strings.TrimSpace(val) 121 | if val != "" { 122 | result = append(result, val) 123 | } 124 | } 125 | return result 126 | } 127 | 128 | func getFormValuesWithPrefix(values url.Values, prefix string) map[string][]string { 129 | result := make(map[string][]string) 130 | for k, v := range values { 131 | if strings.HasPrefix(k, prefix) { 132 | k1 := strings.TrimPrefix(k, prefix) 133 | result[k1] = v 134 | } 135 | } 136 | return result 137 | } 138 | 139 | func getTextAreaHeightByString(mystr string, minHeight int) int { 140 | height := (len(strings.Split(mystr, "\n")) + 1) * 25 141 | if height < minHeight { 142 | height = minHeight 143 | } 144 | return height 145 | } 146 | 147 | func getHostPortFromReq(req *http.Request) (host string, port int, err error) { 148 | host, port, err = parseHostPort(req.Host) 149 | if err == nil && port == 0 { 150 | switch req.URL.Scheme { 151 | case "http": 152 | port = 80 153 | break 154 | case "https": 155 | port = 443 156 | break 157 | default: 158 | break 159 | } 160 | } 161 | return 162 | } 163 | 164 | func parseHostPort(hostPortstr string) (host string, port int, err error) { 165 | var portStr string 166 | if !strings.Contains(hostPortstr, ":") { 167 | hostPortstr += ":0" 168 | } 169 | host, portStr, err = net.SplitHostPort(hostPortstr) 170 | if err != nil { 171 | return 172 | } 173 | port, err = strconv.Atoi(portStr) 174 | if err != nil { 175 | return 176 | } 177 | return 178 | } 179 | 180 | func checkURLValuesChange(first url.Values, second url.Values) (change bool) { 181 | for k, v := range first { 182 | secV, has := second[k] 183 | if !has { 184 | return true 185 | } 186 | if len(v) != len(secV) || fmt.Sprintf("%v", v) != fmt.Sprintf("%v", secV) { 187 | return true 188 | } 189 | } 190 | for k, v := range second { 191 | firstV, has := first[k] 192 | if !has { 193 | return true 194 | } 195 | if len(v) != len(firstV) || fmt.Sprintf("%v", v) != fmt.Sprintf("%v", firstV) { 196 | return true 197 | } 198 | } 199 | return false 200 | } 201 | 202 | func parseDocID(strid string) (docid int, err error) { 203 | docid64, parseErr := strconv.ParseUint(strid, 10, 64) 204 | if parseErr == nil { 205 | return int(docid64), nil 206 | } 207 | return 0, parseErr 208 | } 209 | 210 | func removeHeader(req *http.Request) { 211 | for k := range req.Header { 212 | if len(k) > 5 && k[:6] == "Proxy-" { 213 | req.Header.Del(k) 214 | } 215 | } 216 | } 217 | 218 | func getPostData(req *http.Request) (post *url.Values) { 219 | post = new(url.Values) 220 | if strings.Contains(req.Header.Get("Content-Type"), "x-www-form-urlencoded") { 221 | buf := forgetRead(&req.Body) 222 | var bodyStr string 223 | if req.Header.Get(contentEncoding) == "gzip" { 224 | bodyStr = gzipDocode(buf) 225 | } else { 226 | bodyStr = buf.String() 227 | } 228 | var err error 229 | *post, err = url.ParseQuery(bodyStr) 230 | if err != nil { 231 | log.Println("parse post err", err, "url=", req.URL.String()) 232 | } 233 | } 234 | return post 235 | } 236 | 237 | func headerEncode(data []byte) []byte { 238 | t := bytes.Replace(data, []byte("\r"), []byte("\\r"), -1) 239 | t = bytes.Replace(t, []byte("\n"), []byte("\\n"), -1) 240 | return t 241 | } 242 | -------------------------------------------------------------------------------- /serve/web.go: -------------------------------------------------------------------------------- 1 | package serve 2 | 3 | import ( 4 | "bytes" 5 | "encoding/base64" 6 | "fmt" 7 | "html" 8 | "log" 9 | "net" 10 | "net/http" 11 | "net/url" 12 | "regexp" 13 | "strconv" 14 | "strings" 15 | "text/template" 16 | "time" 17 | 18 | "github.com/hidu/goutils/fs" 19 | "github.com/hidu/goutils/html_util" 20 | "github.com/hidu/goutils/object" 21 | ) 22 | 23 | type webRequestCtx struct { 24 | values map[string]any 25 | user *User 26 | isLogin bool 27 | isAdmin bool 28 | req *http.Request 29 | w http.ResponseWriter 30 | ser *ProxyServe 31 | } 32 | 33 | var cookieName = "pproxy" 34 | 35 | func (ser *ProxyServe) handleLocalReq(w http.ResponseWriter, req *http.Request) { 36 | accessLogStr := "web_access " + req.Method + " " + req.URL.String() + " " + req.RemoteAddr + " refer:" + req.Referer() 37 | defer (func() { 38 | log.Println(accessLogStr) 39 | })() 40 | 41 | if strings.HasPrefix(req.URL.Path, "/socket.io/") { 42 | ser.wsSer.server.ServeHTTP(w, req) 43 | return 44 | } 45 | 46 | if strings.HasPrefix(req.URL.Path, "/f/") { 47 | req.URL.Path = req.URL.Path[3:] 48 | http.FileServer(http.Dir(ser.conf.FileDir)).ServeHTTP(w, req) 49 | return 50 | } 51 | 52 | if strings.HasPrefix(req.URL.Path, "/res/") { 53 | Assest.HTTPHandler("/").ServeHTTP(w, req) 54 | return 55 | } 56 | 57 | values := make(map[string]any) 58 | values["title"] = ser.conf.Title 59 | values["subTitle"] = "" 60 | values["version"] = PproxyVersion 61 | values["notice"] = ser.conf.Notice 62 | values["port"] = strconv.Itoa(ser.conf.Port) 63 | values["userOnlineTotal"] = len(ser.ProxyClients) 64 | _host, _, _ := getHostPortFromReq(req) 65 | values["pproxy_host"] = _host 66 | values["pproxy_port"] = ser.conf.Port 67 | 68 | ctx := &webRequestCtx{ 69 | values: values, 70 | w: w, 71 | req: req, 72 | ser: ser, 73 | } 74 | ctx.checkLogin() 75 | 76 | funcMap := make(map[string]func()) 77 | funcMap["/"] = ctx.handle_index 78 | funcMap["/about"] = ctx.handle_about 79 | funcMap["/config"] = ctx.handleConfig 80 | funcMap["/useage"] = ctx.handle_useage 81 | funcMap["/replay"] = ctx.handleReplay 82 | funcMap["/login"] = ctx.handle_login 83 | funcMap["/logout"] = ctx.handle_logout 84 | funcMap["/response"] = ctx.handle_response 85 | funcMap["/file"] = ctx.handle_file 86 | 87 | if fn, has := funcMap[req.URL.Path]; has { 88 | if len(req.URL.Path) > 1 { 89 | ctx.values["subTitle"] = req.URL.Path[1:] + " |" 90 | } 91 | fn() 92 | } else { 93 | ctx.showError("404") 94 | } 95 | } 96 | 97 | func (ser *ProxyServe) web_checkLogin(req *http.Request) (user *User, isLogin bool) { 98 | if req == nil { 99 | return 100 | } 101 | cookie, err := req.Cookie(cookieName) 102 | if err != nil { 103 | return 104 | } 105 | info := strings.SplitN(cookie.Value, ":", 2) 106 | if len(info) != 2 { 107 | return 108 | } 109 | if user, has := ser.Users[info[0]]; has { 110 | if user.PswMd5 == info[1] { 111 | return user, true 112 | } 113 | } 114 | return 115 | } 116 | 117 | func (ctx *webRequestCtx) checkLogin() { 118 | user, isLogin := ctx.ser.web_checkLogin(ctx.req) 119 | if isLogin { 120 | ctx.user = user 121 | ctx.isLogin = true 122 | ctx.isAdmin = user.IsAdmin 123 | } 124 | ctx.values["isLogin"] = ctx.isLogin 125 | ctx.values["user"] = ctx.user 126 | ctx.values["isAdmin"] = ctx.isAdmin 127 | } 128 | 129 | func (ctx *webRequestCtx) handle_index() { 130 | ctx.render("network.html", true) 131 | } 132 | 133 | func (ctx *webRequestCtx) handle_useage() { 134 | ctx.render("useage.html", true) 135 | } 136 | 137 | func (ctx *webRequestCtx) getRewriteJsInfo(name string, title string) map[string]any { 138 | info := make(map[string]any) 139 | jsStr, _ := ctx.ser.reqMod.getJsContent(name) 140 | 141 | re := regexp.MustCompile(`use_file\(["'](.+)["']\)`) 142 | matches := re.FindAllStringSubmatch(jsStr, -1) 143 | 144 | // fmt.Println(matches) 145 | 146 | var useFile []map[string]any 147 | tmpNames := make(map[string]int) 148 | 149 | for _, subMatch := range matches { 150 | if len(subMatch) != 2 { 151 | continue 152 | } 153 | use := make(map[string]any) 154 | fileName := strings.TrimSpace(subMatch[1]) 155 | use["name"] = subMatch[0] 156 | use["file"] = fileName 157 | 158 | if _, has := tmpNames[fileName]; has { 159 | continue 160 | } 161 | tmpNames[fileName] = 1 162 | 163 | isURL := strings.HasPrefix(fileName, "http://") 164 | use["isUrl"] = isURL 165 | if isURL { 166 | use["url"] = subMatch[1] 167 | } else { 168 | webFile, err := newWebFileInfo(ctx.ser.conf.FileDir, fileName) 169 | if err != nil { 170 | continue 171 | } 172 | use["url"] = webFile.link() 173 | defer webFile.Close() 174 | } 175 | useFile = append(useFile, use) 176 | } 177 | 178 | info["name"] = name 179 | info["use_file"] = useFile 180 | info["title"] = title 181 | info["rewriteJs"] = html.EscapeString(jsStr) 182 | info["jsHeight"] = getTextAreaHeightByString(jsStr, 100) 183 | return info 184 | } 185 | 186 | func (ctx *webRequestCtx) handleConfig() { 187 | if ctx.req.Method == "GET" { 188 | jsDataArr := make([]any, 0, 2) 189 | jsDataArr = append(jsDataArr, ctx.getRewriteJsInfo("", "global config")) 190 | 191 | if ctx.isLogin { 192 | jsDataArr = append(jsDataArr, ctx.getRewriteJsInfo(ctx.user.Name, ctx.user.Name+"'s config")) 193 | } 194 | 195 | ctx.values["jss"] = jsDataArr 196 | 197 | hostsByte, _ := fs.FileGetContents(ctx.ser.getHostsFilePath()) 198 | ctx.values["hosts"] = html.EscapeString(string(hostsByte)) 199 | ctx.values["hostsHeight"] = getTextAreaHeightByString("", 100) 200 | 201 | ctx.render("config.html", true) 202 | } else if ctx.req.Method == "POST" { 203 | if !ctx.isLogin { 204 | ctx.jsAlert("login first") 205 | return 206 | } 207 | do := ctx.req.PostFormValue("type") 208 | var err error 209 | if do == "js" { 210 | name := strings.TrimSpace(ctx.req.PostFormValue("name")) 211 | if !ctx.isAdmin && name != ctx.user.Name { 212 | ctx.jsAlert("you are not admin") 213 | return 214 | } 215 | jsStr := strings.TrimSpace(ctx.req.PostFormValue("js")) 216 | err = ctx.ser.reqMod.parseJs(jsStr, name, true) 217 | } else if do == "hosts" { 218 | if !ctx.isAdmin { 219 | ctx.jsAlert("you are not admin") 220 | return 221 | } 222 | hosts := strings.TrimSpace(ctx.req.PostFormValue("hosts")) 223 | log.Println("hosts_update", hosts) 224 | err = fs.FilePutContents(ctx.ser.getHostsFilePath(), []byte(hosts)) 225 | ctx.ser.loadHosts() 226 | } 227 | if err != nil { 228 | ctx.jsAlert("save failed,err:" + err.Error()) 229 | } else { 230 | ctx.w.Write([]byte("")) 231 | } 232 | } 233 | } 234 | 235 | func (ctx *webRequestCtx) handle_response() { 236 | docid, uintParseErr := parseDocID(ctx.req.FormValue("id")) 237 | if uintParseErr == nil { 238 | responseData, _ := ctx.ser.getResponseByDocid(docid) 239 | if responseData == nil { 240 | ctx.showError("response not found") 241 | } else { 242 | walker := object.NewInterfaceWalker(map[string]any(responseData.Data)) 243 | var contentType string 244 | if typeHeader, has := walker.GetStringSlice("/header/Content-Type"); has { 245 | contentType = strings.Join(typeHeader, ";") 246 | } 247 | 248 | customContentType := ctx.req.FormValue("type") 249 | // set custom content type 250 | if customContentType != "" { 251 | switch customContentType { 252 | case "json": 253 | contentType = "application/json" 254 | case "html": 255 | contentType = "text/html;charset=utf-8" 256 | default: 257 | contentType = customContentType 258 | } 259 | } 260 | if contentType != "" { 261 | ctx.w.Header().Set("Content-Type", contentType) 262 | } 263 | if statusCode, has := walker.GetInt("/status"); has { 264 | ctx.w.WriteHeader(statusCode) 265 | } 266 | if bodyStr, has := walker.GetString("/body"); has { 267 | bodyByte, err := base64.StdEncoding.DecodeString(bodyStr) 268 | if err == nil { 269 | ctx.w.Write(bodyByte) 270 | } else { 271 | log.Println("decode body failed", err) 272 | } 273 | } else { 274 | ctx.showError("response body not found") 275 | } 276 | } 277 | } else { 278 | ctx.showError("param err") 279 | } 280 | } 281 | 282 | func (ctx *webRequestCtx) jsAlert(msg string) { 283 | fmt.Fprintf(ctx.w, "", html.EscapeString(msg)) 284 | } 285 | 286 | func (ctx *webRequestCtx) jsAlertJump(msg string, urlStr string) { 287 | fmt.Fprintf(ctx.w, "", html.EscapeString(msg), urlStr) 288 | } 289 | 290 | func (ctx *webRequestCtx) handle_about() { 291 | ctx.render("about.html", true) 292 | } 293 | 294 | func (ctx *webRequestCtx) handle_logout() { 295 | cookie := &http.Cookie{Name: cookieName, Value: "", Path: "/"} 296 | http.SetCookie(ctx.w, cookie) 297 | http.Redirect(ctx.w, ctx.req, "/", 302) 298 | } 299 | 300 | func (ctx *webRequestCtx) handle_login() { 301 | if ctx.req.Method == "GET" { 302 | ctx.render("login.html", true) 303 | } else { 304 | name := strings.TrimSpace(ctx.req.FormValue("name")) 305 | psw := strings.TrimSpace(ctx.req.FormValue("psw")) 306 | if name == "" { 307 | ctx.jsAlert("empty name") 308 | return 309 | } 310 | if user, has := ctx.ser.Users[name]; has { 311 | if user.isPswEq(psw) { 312 | log.Println("login suc,name=", name) 313 | cookie := &http.Cookie{ 314 | Name: cookieName, 315 | Value: fmt.Sprintf("%s:%s", name, user.PswMd5), 316 | Path: "/", 317 | Expires: time.Now().Add(86400 * time.Second), 318 | } 319 | http.SetCookie(ctx.w, cookie) 320 | ctx.w.Write([]byte("")) 321 | } else { 322 | log.Println("login failed psw incorrect,name=", name, "psw=", psw) 323 | ctx.jsAlert("password incorrect") 324 | } 325 | return 326 | } 327 | log.Println("login failed not exists,name=", name, "psw=", psw) 328 | ctx.jsAlert("user not exists") 329 | } 330 | } 331 | 332 | func (ctx *webRequestCtx) render(name string, layout bool) { 333 | html := render_html(name, ctx.values, layout) 334 | ctx.w.Write([]byte(html)) 335 | } 336 | 337 | func (ctx *webRequestCtx) showError(msg string) { 338 | ctx.values["error"] = msg 339 | ctx.values["subTitle"] = "Error Page |" 340 | ctx.render("error.html", true) 341 | } 342 | 343 | func (ctx *webRequestCtx) showErrorOrAlert(msg string) { 344 | if ctx.req.Method == "POST" { 345 | ctx.jsAlert(msg) 346 | } else { 347 | ctx.showError(msg) 348 | } 349 | } 350 | 351 | func reader_html_include(fileName string) string { 352 | html := Assest.GetContent("/res/tpl/" + fileName) 353 | myfn := template.FuncMap{ 354 | "my_include": func(name string) string { 355 | return reader_html_include(name) 356 | }, 357 | } 358 | tpl, _ := template.New("page_include").Delims("{%", "%}").Funcs(myfn).Parse(html) 359 | var bf []byte 360 | w := bytes.NewBuffer(bf) 361 | tpl.Execute(w, make(map[string]string)) 362 | body := w.String() 363 | return body 364 | } 365 | 366 | func render_html(fileName string, values map[string]any, layout bool) string { 367 | html := reader_html_include(fileName) 368 | funcs := template.FuncMap{ 369 | "escape": func(str string) string { 370 | return url.QueryEscape(str) 371 | }, 372 | "my_include": func(fileName string) string { 373 | return "include (" + fileName + ") with Delims {%my_include %}" 374 | }, 375 | } 376 | tpl, _ := template.New("page").Funcs(funcs).Parse(html) 377 | var bf []byte 378 | w := bytes.NewBuffer(bf) 379 | tpl.Execute(w, values) 380 | body := w.String() 381 | if layout { 382 | values["body"] = body 383 | return render_html("layout.html", values, false) 384 | } 385 | return html_util.Html_reduceSpace(body) 386 | } 387 | 388 | func (ser *ProxyServe) handleUserInfo(w http.ResponseWriter, req *http.Request) { 389 | host, _, _ := net.SplitHostPort(req.RemoteAddr) 390 | data := "client ip:" + host 391 | w.Write([]byte(data)) 392 | } 393 | -------------------------------------------------------------------------------- /serve/web_file.go: -------------------------------------------------------------------------------- 1 | package serve 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "log" 7 | "net/url" 8 | "os" 9 | "path/filepath" 10 | "regexp" 11 | "strings" 12 | 13 | "github.com/hidu/goutils/fs" 14 | ) 15 | 16 | type webFileInfo struct { 17 | Name string 18 | RootDir string 19 | IsDir bool 20 | Size int64 21 | Link string 22 | fullPath string 23 | file *os.File 24 | subFileInfos []*webFileInfo 25 | } 26 | 27 | func newWebFileInfo(rootDir, name string) (*webFileInfo, error) { 28 | rootDir = filepath.Clean(rootDir + "/") 29 | fullPath := filepath.Clean(fmt.Sprintf("%s/%s", rootDir, name)) 30 | if !strings.HasPrefix(fullPath, rootDir) { 31 | return nil, fmt.Errorf("unsafe path:%s", name) 32 | } 33 | f, err := os.Open(fullPath) 34 | if err != nil { 35 | return nil, err 36 | } 37 | stat, err := f.Stat() 38 | if err != nil { 39 | return nil, err 40 | } 41 | info := &webFileInfo{ 42 | Name: strings.TrimLeft(fullPath[len(rootDir):], "/"), 43 | RootDir: rootDir, 44 | IsDir: stat.IsDir(), 45 | Size: stat.Size(), 46 | fullPath: fullPath, 47 | file: f, 48 | } 49 | info.Link = info.link() 50 | return info, nil 51 | } 52 | 53 | func (f *webFileInfo) String() string { 54 | return fmt.Sprintf("Name:%s\nRootDir:%s\nisDir:%v\nSize:%d\nfullPath:%s\n", f.Name, f.RootDir, f.IsDir, f.Size, f.fullPath) 55 | } 56 | 57 | func (f *webFileInfo) link() string { 58 | values := make(url.Values) 59 | values.Set("name", f.Name) 60 | if !f.IsDir { 61 | values.Set("op", "edit") 62 | } 63 | return "/file?" + values.Encode() 64 | } 65 | 66 | func (f *webFileInfo) getContent() string { 67 | if f.IsDir { 68 | return "" 69 | } 70 | data, err := io.ReadAll(f.file) 71 | if err != nil { 72 | log.Println("read file failed:", err) 73 | return "" 74 | } 75 | return string(data) 76 | } 77 | 78 | func (f *webFileInfo) Close() { 79 | f.file.Close() 80 | if f.subFileInfos != nil { 81 | for _, info := range f.subFileInfos { 82 | info.Close() 83 | } 84 | } 85 | } 86 | 87 | func (f *webFileInfo) subFiles() ([]*webFileInfo, error) { 88 | names, err := f.file.Readdirnames(0) 89 | if err != nil { 90 | return nil, err 91 | } 92 | fileInfos := make([]*webFileInfo, 0) 93 | 94 | for _, name := range names { 95 | info, err := newWebFileInfo(f.RootDir, fmt.Sprintf("%s/%s", f.Name, name)) 96 | if err != nil { 97 | log.Println("read file err,skip.name=", name, err) 98 | } else { 99 | fileInfos = append(fileInfos, info) 100 | } 101 | } 102 | f.subFileInfos = fileInfos 103 | return fileInfos, nil 104 | } 105 | 106 | func (ser *ProxyServe) getWebFilePath(name string) (fullPath string, nameNew string, err error) { 107 | rootDir := filepath.Clean(ser.conf.FileDir + "/") 108 | fullPath = filepath.Clean(fmt.Sprintf("%s/%s", rootDir, name)) 109 | if !strings.HasPrefix(fullPath, rootDir) { 110 | return "", "", fmt.Errorf("unsafe path:%s", name) 111 | } 112 | nameNew = fullPath[len(rootDir):] 113 | re := regexp.MustCompile(`^[\w/\-\.]*$`) 114 | if !re.MatchString(nameNew) { 115 | err = fmt.Errorf("illegal path:%s", nameNew) 116 | } 117 | return fullPath, nameNew, err 118 | } 119 | 120 | func (ctx *webRequestCtx) handle_file() { 121 | if !ctx.isLogin { 122 | ctx.showError("need login") 123 | return 124 | } 125 | 126 | opMap := make(map[string]func()) 127 | opMap["edit"] = ctx.handle_file_edit 128 | opMap["new"] = ctx.handle_file_new 129 | opMap["del"] = ctx.handle_file_del 130 | opMap["save"] = ctx.handle_file_save 131 | 132 | op := ctx.req.FormValue("op") 133 | if fn, has := opMap[op]; has { 134 | fn() 135 | return 136 | } 137 | name := ctx.req.FormValue("name") 138 | if !ctx.isAdmin && name == "" { 139 | name = ctx.user.Name 140 | dirFullPath, _, err := ctx.ser.getWebFilePath(name) 141 | if err != nil { 142 | ctx.showError("file dir wrong") 143 | return 144 | } 145 | if !fs.FileExists(dirFullPath) { 146 | os.MkdirAll(dirFullPath, os.ModePerm) 147 | } 148 | } 149 | 150 | dirInfo, err := newWebFileInfo(ctx.ser.conf.FileDir, name) 151 | if err != nil { 152 | ctx.showError("open file dir failed:" + name) 153 | return 154 | } 155 | defer dirInfo.Close() 156 | 157 | ctx.values["currentDir"] = dirInfo.Name 158 | ctx.values["isSubDir"] = dirInfo.Name != "" 159 | if !ctx.isAdmin && !strings.Contains(dirInfo.Name, "/") { 160 | ctx.values["isSubDir"] = false 161 | } 162 | files, err := dirInfo.subFiles() 163 | if err != nil { 164 | ctx.showError(err.Error()) 165 | return 166 | } 167 | ctx.values["files"] = files 168 | 169 | ctx.render("file.html", true) 170 | } 171 | 172 | func (ctx *webRequestCtx) handle_file_edit() { 173 | name := ctx.req.FormValue("name") 174 | if name == "" { 175 | ctx.showError("params wrong") 176 | return 177 | } 178 | info, err := newWebFileInfo(ctx.ser.conf.FileDir, name) 179 | if err != nil { 180 | ctx.showError("read file info failed:" + err.Error()) 181 | return 182 | } 183 | defer info.Close() 184 | if info.IsDir { 185 | ctx.showError("params wrong,only file can view") 186 | return 187 | } 188 | ctx.values["file"] = info 189 | fileContent := info.getContent() 190 | ctx.values["fileContent"] = fileContent 191 | ctx.values["fileContentRows"] = len(strings.Split(fileContent, "\n")) + 8 192 | ctx.render("file_edit.html", true) 193 | } 194 | 195 | func (ctx *webRequestCtx) handle_file_del() {} 196 | 197 | func (ctx *webRequestCtx) handle_file_new() { 198 | dirFullPath, dirNew, err := ctx.ser.getWebFilePath(ctx.req.FormValue("dir")) 199 | if err != nil { 200 | ctx.showErrorOrAlert("params err:" + err.Error()) 201 | return 202 | } 203 | ctx.values["dir"] = dirNew 204 | 205 | if ctx.req.Method == "GET" { 206 | finfo, fErr := os.Stat(dirFullPath) 207 | if fErr != nil || !finfo.IsDir() { 208 | ctx.showErrorOrAlert("open dir failed") 209 | return 210 | } 211 | ctx.render("file_new.html", true) 212 | } else if ctx.req.Method == "POST" { 213 | name := strings.TrimSpace(ctx.req.FormValue("name")) 214 | if name == "" { 215 | ctx.jsAlert("empty filename") 216 | return 217 | } 218 | fpath := ctx.req.FormValue("dir") + "/" + ctx.req.FormValue("name") 219 | 220 | fileFullPath, fileName, err := ctx.ser.getWebFilePath(fpath) 221 | 222 | if err != nil { 223 | ctx.jsAlert("wrong fileName") 224 | return 225 | } 226 | 227 | if fileName == "" || strings.HasSuffix(fileName, "/") { 228 | ctx.jsAlert("wrong file name") 229 | return 230 | } 231 | 232 | if fs.FileExists(fileFullPath) { 233 | ctx.jsAlert("file already exists") 234 | return 235 | } 236 | 237 | if !strings.HasPrefix(fileFullPath, dirFullPath) { 238 | ctx.jsAlert("file name wrong") 239 | return 240 | } 241 | 242 | if !ctx.user.IsAdmin && !strings.HasPrefix(fileName+"/", "/"+ctx.user.Name+"/") { 243 | ctx.jsAlert("file path wrong:" + fileName) 244 | return 245 | } 246 | 247 | dirName := filepath.Dir(fileFullPath) 248 | if !fs.FileExists(dirName) { 249 | os.MkdirAll(dirName, os.ModePerm) 250 | } 251 | content := ctx.req.FormValue("content") 252 | 253 | wErr := fs.FilePutContents(fileFullPath, []byte(content)) 254 | if wErr != nil { 255 | ctx.jsAlert("write file failed") 256 | return 257 | } 258 | 259 | finfo, _ := newWebFileInfo(ctx.ser.conf.FileDir, fpath) 260 | defer finfo.Close() 261 | 262 | ctx.jsAlertJump("save suc", finfo.link()) 263 | } 264 | } 265 | 266 | func (ctx *webRequestCtx) handle_file_save() { 267 | nameOrigin := ctx.req.PostFormValue("nameOrigin") 268 | name := ctx.req.PostFormValue("name") 269 | content := ctx.req.PostFormValue("content") 270 | 271 | fullPath, nameFix, err := ctx.ser.getWebFilePath(name) 272 | if err != nil { 273 | ctx.jsAlert("file path wrong:" + err.Error()) 274 | return 275 | } 276 | if name == "" || nameFix == "" || strings.HasSuffix(nameFix, "/") { 277 | ctx.jsAlert("wrong file name") 278 | return 279 | } 280 | fullPathOrigin, _, err := ctx.ser.getWebFilePath(nameOrigin) 281 | 282 | if fullPathOrigin == "" && err != nil { 283 | ctx.jsAlert("origin file path wrong:" + err.Error()) 284 | return 285 | } 286 | 287 | dirName := filepath.Dir(fullPath) 288 | if !fs.FileExists(dirName) { 289 | os.MkdirAll(dirName, os.ModePerm) 290 | } 291 | 292 | errWrite := fs.FilePutContents(fullPath, []byte(content)) 293 | if errWrite != nil { 294 | ctx.jsAlert("save failed:" + errWrite.Error()) 295 | return 296 | } 297 | if fullPath != fullPathOrigin { 298 | os.Remove(fullPathOrigin) 299 | } 300 | info, _ := newWebFileInfo(ctx.ser.conf.FileDir, name) 301 | ctx.jsAlertJump("save suc", info.link()) 302 | } 303 | -------------------------------------------------------------------------------- /serve/wsClient.go: -------------------------------------------------------------------------------- 1 | package serve 2 | 3 | import ( 4 | "net/http" 5 | "path/filepath" 6 | "strings" 7 | 8 | "github.com/googollee/go-socket.io" 9 | ) 10 | 11 | type wsClient struct { 12 | ns *socketio.NameSpace 13 | user string 14 | filterUser []string 15 | filterIP []string 16 | filterHideExt []string 17 | filterURL []string 18 | filterURLHide []string 19 | LoginUser *User 20 | } 21 | 22 | var extTypes = map[string][]string{ 23 | "js": {"js"}, 24 | "css": {"css"}, 25 | "image": {"jpg", "jpeg", "png", "gif", "bmp", "tiff", "jpe", "tif", "webp", "ico", "webp"}, 26 | } 27 | 28 | func (client *wsClient) checkFilter(req *http.Request, reqCtx *requestCtx) bool { 29 | if len(client.filterUser) > 0 { 30 | userInList := false 31 | for _, name := range client.filterUser { 32 | if name == "any" && client.LoginUser != nil && client.LoginUser.IsAdmin { 33 | userInList = true 34 | break 35 | } 36 | if name != "" && name == reqCtx.User.Name { 37 | userInList = true 38 | break 39 | } 40 | } 41 | if !userInList { 42 | return false 43 | } 44 | } 45 | 46 | if len(client.filterIP) > 0 { 47 | addrInfo := strings.Split(reqCtx.RemoteAddr, ":") 48 | ipInList := false 49 | for _, ip := range client.filterIP { 50 | if ip == "*" { 51 | ipInList = true 52 | break 53 | } 54 | 55 | if ip != "" && addrInfo[0] == ip { 56 | ipInList = true 57 | break 58 | } 59 | } 60 | if !ipInList { 61 | return false 62 | } 63 | } 64 | 65 | if len(client.filterURL) > 0 { 66 | url := req.URL.String() 67 | hasKw := false 68 | for _, subURL := range client.filterURL { 69 | if strings.Contains(url, subURL) { 70 | hasKw = true 71 | break 72 | } 73 | } 74 | if !hasKw { 75 | return false 76 | } 77 | } 78 | 79 | if len(client.filterHideExt) > 0 { 80 | ext := strings.ToLower(strings.Trim(filepath.Ext(req.URL.Path), ".")) 81 | for _, hideType := range client.filterHideExt { 82 | for _, hideExt := range extTypes[hideType] { 83 | if ext == hideExt { 84 | return false 85 | } 86 | } 87 | } 88 | } 89 | if len(client.filterURLHide) > 0 { 90 | _url := req.URL.String() 91 | for _, hideKw := range client.filterURLHide { 92 | if hideKw != "" && strings.Contains(_url, hideKw) { 93 | return false 94 | } 95 | } 96 | } 97 | return true 98 | } 99 | -------------------------------------------------------------------------------- /serve/wsServer.go: -------------------------------------------------------------------------------- 1 | package serve 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | "net/url" 8 | "sync" 9 | 10 | "github.com/googollee/go-socket.io" 11 | "github.com/hidu/goutils/time_util" 12 | ) 13 | 14 | type wsServer struct { 15 | clients map[string]*wsClient 16 | server *socketio.SocketIOServer 17 | mu sync.RWMutex 18 | proxySer *ProxyServe 19 | } 20 | 21 | func (ser *ProxyServe) wsInit() { 22 | ser.wsSer = newWsServer(ser) 23 | } 24 | 25 | func newWsServer(ser *ProxyServe) *wsServer { 26 | wsSer := &wsServer{ 27 | clients: make(map[string]*wsClient), 28 | proxySer: ser, 29 | } 30 | var err error 31 | wsSer.server = socketio.NewSocketIOServer(&socketio.Config{}) 32 | if err != nil { 33 | log.Fatal(err) 34 | } 35 | wsSer.init() 36 | return wsSer 37 | } 38 | 39 | func (wsSer *wsServer) init() { 40 | wsSer.server.On("connect", func(ns *socketio.NameSpace) { 41 | wsSer.mu.Lock() 42 | defer wsSer.mu.Unlock() 43 | wsSer.clients[ns.Id()] = &wsClient{ns: ns, user: "guest"} 44 | log.Println("ws connected", ns.Id(), "ws_client_num:", len(wsSer.clients)) 45 | }) 46 | wsSer.server.On("disconnect", func(ns *socketio.NameSpace) { 47 | wsSer.remove(ns.Id()) 48 | log.Println("ws disconnect", ns.Id(), "ws_client_num:", len(wsSer.clients)) 49 | }) 50 | wsSer.server.On("error", func(ns *socketio.NameSpace, err error) { 51 | log.Println("ws error:", err) 52 | }) 53 | wsSer.server.On("get_response", wsSer.getResponse) 54 | wsSer.server.On("client_filter", wsSer.saveFilter) 55 | 56 | time_util.SetInterval(func() { 57 | wsSer.broadcast("hello", "hello", false) 58 | }, 120) 59 | } 60 | 61 | func (wsSer *wsServer) remove(id string) { 62 | wsSer.mu.Lock() 63 | defer wsSer.mu.Unlock() 64 | if _, has := wsSer.clients[id]; has { 65 | delete(wsSer.clients, id) 66 | } 67 | } 68 | 69 | func (wsSer *wsServer) broadProxyClientNum() { 70 | wsSer.broadcast("user_num", len(wsSer.proxySer.ProxyClients), false) 71 | } 72 | 73 | /* 74 | * https://github.com/googollee/go-socket.io 75 | */ 76 | func (wsSer *wsServer) getResponse(ns *socketio.NameSpace, docidStr string) { 77 | docid, uintParseErr := parseDocID(docidStr) 78 | if uintParseErr != nil { 79 | log.Println("parse str2int failed", docidStr, uintParseErr) 80 | return 81 | } 82 | log.Println("receive docid", docid) 83 | req, _ := wsSer.proxySer.getRequestByDocid(docid) 84 | res, _ := wsSer.proxySer.getResponseByDocid(docid) 85 | if wsSer.proxySer.Debug { 86 | fmt.Println("req:\n", req, "\n==========\n") 87 | fmt.Println("res:\n", res, "\n==========\n") 88 | } 89 | // delete(req,"header") 90 | data := make(map[string]any) 91 | data["req"] = nil 92 | data["res"] = nil 93 | if req != nil { 94 | data["req"] = req.Data 95 | } 96 | if res != nil { 97 | data["res"] = res.Data 98 | } 99 | wsSer.send(ns, "res", data, true) 100 | } 101 | 102 | func (wsSer *wsServer) saveFilter(ns *socketio.NameSpace, formData string) { 103 | m, err := url.ParseQuery(formData) 104 | if err != nil { 105 | log.Println("parse filter data err", err) 106 | return 107 | } 108 | wsSer.mu.Lock() 109 | defer wsSer.mu.Unlock() 110 | if nsClient, has := wsSer.clients[ns.Id()]; has { 111 | nsClient.filterIP = parseURLInputAsSlice(m.Get("client_ip")) 112 | nsClient.filterHideExt = m["hide"] 113 | nsClient.filterURL = parseURLInputAsSlice(m.Get("url_match")) 114 | nsClient.filterURLHide = parseURLInputAsSlice(m.Get("hide_url")) 115 | nsClient.filterUser = parseURLInputAsSlice(m.Get("user")) 116 | loginUser, isLogin := wsSer.proxySer.web_checkLogin(ns.Session.Request) 117 | if isLogin { 118 | nsClient.LoginUser = loginUser 119 | } 120 | } else { 121 | log.Println("ws_saveFilter failed,ws not exists") 122 | } 123 | } 124 | 125 | var nnnn int 126 | 127 | func (wsSer *wsServer) send(ns *socketio.NameSpace, msgName string, data any, encode bool) { 128 | wsSer.mu.Lock() 129 | 130 | defer func(ns *socketio.NameSpace) { 131 | wsSer.mu.Unlock() 132 | if e := recover(); e != nil { 133 | log.Println("ws_send failed", e, ns.Session.Request.RemoteAddr, "msgName:", msgName, "client:", len(wsSer.clients)) 134 | wsSer.remove(ns.Id()) 135 | } 136 | }(ns) 137 | var err error 138 | encode = false 139 | if encode { 140 | err = ns.Emit(msgName, dataEncode(data)) 141 | } else { 142 | err = ns.Emit(msgName, data) 143 | } 144 | if err != nil { 145 | log.Println("emit_failed", msgName, err) 146 | } 147 | } 148 | 149 | func (wsSer *wsServer) broadcastReq(req *http.Request, reqCtx *requestCtx, data any) bool { 150 | wsSer.mu.RLock() 151 | defer wsSer.mu.RUnlock() 152 | 153 | hasSend := false 154 | for _, client := range wsSer.clients { 155 | if wsSer.proxySer.conf.SessionView == sessionViewIPOrUser && len(client.filterIP) == 0 && len(client.filterUser) == 0 { 156 | continue 157 | } 158 | 159 | if reqCtx.User.Name != "" && len(client.filterUser) < 1 { 160 | continue 161 | } 162 | 163 | if client.checkFilter(req, reqCtx) { 164 | go wsSer.send(client.ns, "req", data, true) 165 | hasSend = true 166 | } 167 | } 168 | return hasSend 169 | } 170 | 171 | func (wsSer *wsServer) broadcast(name string, data any, encode bool) { 172 | wsSer.mu.RLock() 173 | defer wsSer.mu.RUnlock() 174 | for _, client := range wsSer.clients { 175 | go wsSer.send(client.ns, name, data, encode) 176 | } 177 | } 178 | --------------------------------------------------------------------------------