目录

GO语言开发实践-https 请求绑定指定的 ip

golang 对访问某域名的 https 请求绑定指定的 ip (类似于 curl –resolve)。

动机

我们需要自己实现一个拨测的工具,具体功能是从我们的管理后台获取域名列表,然后根据域名去做该域名的所有 ip 地址解析(有些域名不止一个 A 记录),拿到该域名的所有 ip 后,对该域名下特定的 ip 进行拨测。

看了一些开源的 request 库,好像都没有我们想要的这个功能,于是我们打算用 golang 的标准库 “net/http” 包来实现,golang “net/http” 包很强大,默认的行为是对该域名进行解析,得到该域名的所有 ip ,然后依次对这些 ip 进行探测,首次成功或者失败,即退出程序,所以,我们还需要对它进行二次封装才能符合我们的要求。

方法一

这个方法比较简单,直接重构 http 客户端的 DefaultTransport 下的 DialContext 方法。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
package main

import (
    "context"
    "fmt"
    "log"
    "net"
    "net/http"
    "strings"

    "time"
)

func HttpsHostBindIp(ip, domain string) {
    //异常处理
    defer func() {
        //捕获异常
        err := recover()
        if err != nil {
            //存在异常,抛出异常
            fmt.Printf("http 请求 serviceUrl 一次出错:%v", err)
        }
    }()

    dialer := &net.Dialer{
        Timeout:   30 * time.Second,
        KeepAlive: 30 * time.Second,
        // DualStack: true, // this is deprecated as of go 1.16
    }
    // create your own transport, there's an example on godoc.
    http.DefaultTransport.(*http.Transport).DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
        // 3. 获取 addr 的端口,有可能我们第一次访问它会重定向,端口变了
        // 所以这里的逻辑是防止它重定向的时候端口变了,这里 ip 还是绑定的
        // 之前端口,会导致错误 400 
        portSlice := strings.Split(addr, ":")
        port := portSlice[1]
        addr = ip + ":" + port
        return dialer.DialContext(ctx, network, addr)
    }

    resp, err := http.Get(domain)
    if err != nil {
        fmt.Println(err)
    }

    log.Println(resp.StatusCode, resp.Header, err)
}

func main() {
    HttpsHostBindIp("14.17.92.71", "http://www.bilibili.com")
}

方法二

这种写法就比较灵活,可以定制 http 客户端的其他参数,实现效果也是一样的。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
package main

import (
    "bytes"
    "context"
    "errors"
    "fmt"
    "net"
    "net/http"
    "net/url"
    "strings"
    "time"
)

func HttpsHostBindIp(ipAddr, serviceUrl string) (*http.Response, error) {
    //异常处理
    defer func() {
        //捕获异常
        err := recover()
        if err != nil {
            //存在异常,抛出异常
            fmt.Printf("http 请求 serviceUrl 一次出错:%v", err)
        }
    }()

    urlObj, err := url.Parse(serviceUrl)
    if err != nil {
        fmt.Printf("url.Parse(serviceUrl) Error: %v", err)
    }

    request, _ := http.NewRequest("GET", urlObj.String(), bytes.NewBuffer([]byte{}))

    defer request.Body.Close() //
    var response *http.Response

    // 一、 绑定固定的IP
    // 1. 先准备一个Dialer对象
    dialer := &net.Dialer{
        Timeout:   7 * time.Second,
        KeepAlive: 30 * time.Second,
    }
    transport := &http.Transport{
        // 2. 插入特别的改写条件,然后继续利用原先的DialContext逻辑
        DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
            // 3. 获取 addr 的端口,有可能我们第一次访问它会重定向,端口变了
            // 所以这里的逻辑是防止它重定向的时候端口变了,这里 ip 还是绑定的
            // 之前端口,会导致错误 400 
            portSlice := strings.Split(addr, ":")
            port := portSlice[1]
            addr = ipAddr + ":" + port
            return dialer.DialContext(ctx, network, addr)
        },
    }

    // 客户端重定向
    client := &http.Client{
        CheckRedirect: func(req *http.Request, via []*http.Request) error {
            // 重定向超过10次就跳出
            if len(via) >= 10 {
                return errors.New("stopped after 10 redirects")
            }
            return nil
        },
        Transport: transport,
    }

    response, err = client.Do(request)
    if err != nil {
        fmt.Printf("client.Do(request) Error:%v\n", err)
    }

    return response, err
}

func main() {
    r, e := HttpTestOnceHostBindIp("14.17.92.71", "http://www.bilibili.com")
    if e != nil {
        fmt.Println(e)
        return
    }

    log.Println(r.StatusCode, r.Header, err)
}

总结

我们想通过代码来实现类似 curl http://www.example.com --resolve www.example.com:80:127.0.0.1 的效果,一开始也没什么头绪,不过后面参考网上其他网友的帖子,加上自己的一些理解,于是也是实现了自己工具的功能。

参考

golang 强制对特定 ip 的 http 请求(类似于 curl –resolve)

使用GO 程序指定IP地址访问 http/https 地址 类似curl –resolve XXXIP:PortYYY