[Golang] 自动健康上报

Charles http.Client mailx crontab

  • Charles
  • Golang 1.14
  • CentOS 7
  • 健康上报APP

1. Charles

1.1 基本配置

本身的功能还是很多的,由于健康信息上报在手机APP中,无法正常通过浏览器来分析请求信息,所以此处用Charles进行抓包。

Charles为收费软件,自行决定下载源,安装完成后:

  • 系统代理端口:菜单栏Proxy - Proxy Settings - Proxies - HTTP Proxy设置为8888(默认值);
  • 确保手机和电脑在同一局域网下;
  • 手机修改Wi-Fi配置,一般在“无线网络选项”或“高级选项”中,可以设置HTTP代理,将其设置为PC_ADDR: 8888,此时手机的所有流量都会通过电脑的8888端口,由Charles截获;
  • 包乱码问题:
    • 以上仅针对http请求,而我们需要截获的APP请求是通过HTTPS进行的,因此要求电脑端和手机端都有相同的证书,便于伪装(我猜的)
    • 菜单栏:Help - SSL Proxing - Install Charles Root Certificate
    • 手机打开网址http://www.charlesproxy.com/getssl,获取电脑上的证书
    • 菜单栏:Proxy - SSL Proxying Settings - SSL Proxying,Location Include中添加*:443,即匹配任意站点的HTTPS请求

1.2 抓包

完成配置之后,抓包是很简单的,手机打开相应页面,电脑截获请求,由于这个APP用的是WebView,甚至可以直接将请求的网址提取出来,利用Chrome浏览器进行分析。

根据相关信息找到服务器的地址,在uc/wap/路径中,找到了目标login,我们关注的主要在Contents中,上下两部分分别是Request与Response。

至此,手机可以丢到一旁了。关于Charles使用的详细信息,可以参考这篇文章.

1.3 分析

在信息上报的流程中,通常我们需要完成两步操作“登陆-提交信息”,尽管我们知道提及信息对应的路径ncov/wap/default/save,但是直接访问这个地址会被重定向到登陆页面,因此我们需要登陆获取Cookies。

  • 伪造请求:请求中需要包括与Request中相同的Headers,使我们的data可以顺利的被服务器认可、读取并返回需要的response。

  • 读取返回:response通常为返回的HTML或者关于操作成功/失败的status,按照Response - Header中可能存在的Content-Encoding类型对返回值进行解码,并通过正则表达式提取出文本中我们需要的内容。

  • 绕过合法性校验:通过分析网页的源码,针对所处地理位置、身体状况、是否已提交等信息的校验几乎都是在前端完成的,这也为我们之后的工作提供了便利。

  • 定位信息:定位是通过请求百度地图的API完成的,源码中就包含相应的token。但使用API定位收到的限制比较大:1.最终部署服务器的位置;2.大公司API可能存在的一些安全限制。不过网页js中就包含了“上次提交”的信息oldInfo,可以直接针对上次信息作出提取,修改几个值就可以作为新的信息进行提交了。


2. 伪装请求

基于上述的分析,我们可以将程序分成三个步骤:

  1. 首先向POST uc/wap/login/check发送信息,成功登陆并获取cookies;
  2. GET /ncov/wap/default/index向信息上报页面请求信息,并且对所需信息进行匹配;
  3. 之后绕过信息上报的页面,直接将包装好的信息通过POST /ncov/wap/default/index/save

2.1 Golang的几种请求方式

2.1.1 GET

1
2
3
func Get(url string) (resp *Response, err error) {
return DefaultClient.Get(url)
}

Get请求可以直接用http.Get(url string)方法进行,如下:

1
2
3
4
5
6
7
8
9
10
11
12
func httpGet() {
resp, err := http.Get("https://example.com")
if err != nil {
// handle error
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)   //请求数据进行读取
if err != nil {
// handle error
}
fmt.Println(string(body))
}

2.1.2 POST

http.Post()接收string类型的url与请求头bodyType,最后一个参数接收io.Reader类型的主体内容,可以通过strings.NewReader()进行转换

1
2
3
func Post(url string, bodyType string, body io.Reader) (resp *Response, err error) {
return DefaultClient.Post(url, bodyType, body)
}

例:

1
2
3
4
5
6
7
8
9
10
11
12
13
func httpPost() {

resp, err := http.Post("https://example.com","application/x-www-form-urlencoded",strings.NewReader("name=abc"))
if err != nil {
fmt.Println(err)
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
// handle error
}
fmt.Println(string(body))
}

http.PostForm()支持表单的请求:

1
2
3
4
5
6
7
func httpPostForm() {
form := url.Value{}
form.Add("name", "张三")
form.Add("password", "zhangsanNB")
resp, err := http.PostForm("https://example.com", form)
...
}

2.1.3 复杂请求

对于一些复杂的请求,可以用http.NewRequest()&http.Client{}进行构建。

事实上,http.Get() http.Post() http.PostForm()都是对http.Client{}的封装,在执行请求的时候,都是用了client.Do()方法,因此要设置更详细的请求信息,可以直接首先对http.Client{}进行初始化:

1
2
3
4
5
jar, _ := cookiejar.New(nil)
client := &http.Client{
Jar: jar,
Timeout: 10 * time.Second,
}

cookiejar为保存cookie的容器,Timeout用于设置超时时间。对于client我们需要详细设置其Headers

1
2
3
4
5
6
7
8
9
10
11
12
13
14
req.Header.Set("Host", "example.com")
req.Header.Set("Connection", "keep-alive")
req.Header.Set("Content-Control", "max-age=0")
req.Header.Set("DNT", "1")
req.Header.Set("Upgrade-Insecure-Requests", "1")
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36")
req.Header.Set("Sec-Fetch-Dest", "document")
req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9")
req.Header.Set("Sec-Fetch-Site", "same-origin")
req.Header.Set("Sec-Fetch-Mode", "navigate")
req.Header.Set("Sec-Fetch-User", "?1")
req.Header.Set("Referer", "https://example.com/uc/wap/login?redirect=https%3A%2F%2Fexample.com%2Fncov%2Fwap%2Fdefault%2Findex")
req.Header.Set("Accept-Encoding", "gzip, deflate, br")
req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7")

这一步的内容取决于你捕获的包的请求头。之后我们需要自己定义一个请求http.NewRequest()

1
2
3
4
5
6
7
8
9
10
11
12
13
// GET
resp, err := client.Get("https://example.com")
if err != nil {
logs.Warn(err.Error())
}
defer resp.Body.Close()

// POST
req, _ := http.Newrequest("POST", "https://example.com/", "data")
resp, _ := client.Do(req) //篇幅有限,err省略
// or
// resp, _ := client.Post("https://example.com/", "data", nil)
defer resp.Body.Close()

注意及时关闭resp.Body,防止资源占用导致的阻塞。

2.2 内容提取

通过http.Client.Get("https://example.com/ncov/wap/default/index")可以获取页面信息,当然由于在headers中设定了Accept-Encoding: gzip,此处需要对response.Body进行解码操作,否则看到的是一堆乱码:

1
2
3
4
5
6
7
8
9
10
11
//解决Content乱码问题
var reader io.ReadCloser
switch response.Header.Get("Content-Encoding") {
case "gzip":
reader, err = gzip.NewReader(response.Body)
defer reader.Close()
default:
reader = response.Body
}
byte, err := ioutil.ReadAll(reader)
str := string(byte)

截取内容中的一段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# ----HTML ABOVE------ #
var vm = new Vue({
el: '.form-detail2',
data: {
realname: "张三",
number: 'SZ111111111',
date: '2020-03-25',
info: $.extend({
...
}, def, {
szgj: '',
szcs: ''
}),
oldInfo: {"id":2368048,"uid":236939,"date":"20200324",...,"sflznjcjwfh":"0"},
tipMsg: '',
ajaxLock: false,
showFxyy: false,
sfzgn: 1,
hasFlag: '1',
}
}

简单粗暴一点,直接用正则表达式匹配我们需要的信息oldInfo realname number

1
2
3
4
5
6
7
8
9
func GetInfo(str string) (string, error) {
matched, err := regexp.MatchString(".*oldInfo:.*", str)
infoRegexp := regexp.MustCompile(`.*oldInfo:\s(\{.*\})`)
params := infoRegexp.FindStringSubmatch(str)
if matched {
return params[1], nil
}
return "", err
}

2.3 数据序列化json

显然oldInfo为一段json文本,通过GetInfo(str string) (string, err)我们得到了一段string,将其转为map[string]interface{}便于对其中的值进行修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
str := ReadString(username)
var data map[string]interface{}
// var info map[string]string
// 由于部分值并不是string类型,所以用interface{}进行读取
if err := json.Unmarshal([]byte(str), &data); err != nil {
return err
}
// some procedures ...
byte, _ := json.Marshal(data)
str := string(byte)
// 将数据持久化
f, _ := os.Open(path)
l, _ := os.WriteString(str) // l为写入的字节数
f.Close()

这里有个不好处理的地方:由于返回的json文件中,存在整型和字符串类型的value,而在将数据封装到请求包的过程中,需要将json存到url.Values{}中,这是一个map[string][]string类型的对象,为了避免麻烦,在上述过程中,直接讲部分整型通过strconv.Itoa(int)转成了字符类型。

3. 部署

为了实现整个上报过程的自动化,需要将程序放在服务器上,并定期运行。

3.1 构建多平台可执行程序

针对不同平台使用env GOOS=target-OS GOARCH=target-architecture go build path -o build-name,常用的target-OStarget-architecture如下:
| GOOS - Target Operating System | GOARCH - Target Platform |
| :——————————: | :————————: |
| android | arm |
| darwin | amd64 |
| darwin | arm |
| darwin | arm64 |
| linux | amd64 |
| windows | amd64 |

当然,你也可以使用自动交叉编译的脚本来完成这个工作。

3.2 邮件提醒

目前主流邮箱都可以开启pop3或smtp的服务,用于在第三方上收发邮件,诸如qq、outlook、163(垃圾,别用)、gmail等都可以在设置界面找到相应的选项,开启后,服务商都会给你一段授权码(不是密码),用于身份识别。具体步骤如下:

  • 开启smtp授权

  • 服务器上mkdir /root/.certs

  • 配置证书:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    echo -n | openssl s_client -connect smtp.qq.com:465 | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' > ~/.certs/qq.crt

    certutil -A -n "GeoTrust Global CA" -t "C,," -d ~/.certs -i ~/.certs/qq.crt

    certutil -A -n "GeoTrust SSL CA" -t "C,," -d ~/.certs -i ~/.certs/qq.crt

    certutil -L -d /root/.certs

    certutil -A -n "GeoTrust SSL CA - G3" -t "Pu,Pu,Pu" -d ~/.certs/ -i ~/.certs/qq.crt
  • 安装mailx

    1
    2
    yum -y install sendmail
    yum -y install mailx
  • 配置发件人信息:vim /etc/mail.rc

    1
    2
    3
    4
    5
    set from=example@qq.com
    set smtp=smtp.qq.com
    set smtp-auth-user=example@qq.com
    set smtp-auth-password=AUTHORITY_CODE
    set smtp-auth=login
  • 发送邮件:cat response.log | mail -s 'title' yourmail@gmail.commail -s 'title' yourmail@gmail.com < test.txt

3.3 定时执行任务

  • 安装crontab
1
2
yum install -y vixie-cron
yum install -y crontabs
  • 配置
1
service crond start
  • 定时运行
1
crontab -e

出现一个文本文件,按照以下格式:

1
2
*   *   *   *   *  command
分 时 日 月 周

设置为:

1
30 6 * * * {$PATH}/report.sh

4. 总结

  • 程序结构混杂;
  • golang http包源码可以看一下;
  • 源码.