Skip to content

projectdiscovery之httpx源码学习

字数
2041 字
阅读时间
10 分钟
更新日期
3/12/2021

httpx is a fast and multi-purpose HTTP toolkit allows to run multiple probers using retryablehttp library, it is designed to maintain the result reliability with increased threads.

httpx是一个多功能的http请求工具包,通常可以用它来批量探测资产的一些基本信息。

源码地址:https://github.com/projectdiscovery/httpx

支持的探针有

image-20210310095545598

通常情况下,实现一个类似的工具是很容易的,如果用python来写

python
import requests
url = "https://x.hacking8.com"
r = requests.get(url)
resp = r.text
status_code = r.status_code

就可以获得大部分探针的信息,但要做成一个工程化的软件,就要考虑更多问题。

如何处理go的并发,如何加快请求速度,如何进行原生http请求以及如何工程化的组织go的源码框架,看了httpx的源码,这些都挺有收获的,所以写一篇源码记录。

基础结构

源码目录

.
├── cmd
│   └── httpx # 程序编译目录
├── common # 公共函数
│   ├── customheader # 自定义header的一些处理
│   ├── customports # 自定义端口的处理
│   ├── fileutil # 文件处理
│   ├── httputilz # http处理
│   ├── httpx # 一些探针
│   ├── iputil # ip处理
│   ├── slice # go切片处理
│   └── stringz # string函数
├── internal # 运行处理主目录
│   └── runner
├── scripts # 不重要
└── static # 不重要

进入到cmd/httpx/httpx.go

go
package main

import (
    "github.com/projectdiscovery/gologger"
    "github.com/projectdiscovery/httpx/internal/runner"
)

func main() {
    // 解析参数
    options := runner.ParseOptions()

    // 启动,初始化工作
    run, err := runner.New(options)
    if err != nil {
        gologger.Fatalf("Could not create runner: %s\n", err)
    }

    // 运行
    run.RunEnumeration()

    // 关闭
    run.Close()
}

就是它编译成命令行的主文件,整个程序的流程一目了然言简意赅。

Go的并发处理

在http的请求中,最好对go协程的并发做一些限制,类似于线程池的概念。

httpx使用了https://github.com/remeh/sizedwaitgroup的库

image-20210312103337622

image-20210312103353709

使用也很简单,初始化之后,在要使用协程之前wg.Add(),使用协程后wg.Done()代表结束即可。

以上只是对协程数做了限制,如果还想对每秒并行的协程数量 做限制,可以参考projectdiscover另一个项目nuclei,它在使用了sizedwaitgroup的基础上,还使用了go.uber.org/ratelimit

使用更加简单

go
package main

import (
    "fmt"
    "time"

    "go.uber.org/ratelimit"
)

func main() {
    rl := ratelimit.New(1) // per second

    prev := time.Now()
    for i := 0; i < 10; i++ {
        now := rl.Take() // 重点是它
        if i > 0 {
            fmt.Println(i, now.Sub(prev))
        }
        prev = now
    }

}

初始化后在协程中调用Take()方法即可。

探针

httpx支持了很多基于http的基础探针,看几个有意思的。

探针的实现大部分在common/httpx目录。

Cdn check

common/httpx/cdn.go

在以前看naabu时也看到过 projectdiscover之naabu 端口扫描器源码学习

顾名思义,跟踪一下,发现cdn检查调用的是github.com/projectdiscovery/cdncheck中的项目。

通过接口获取一些CDN的ip段,判断ip是否在这些ip段中

CSP

common/httpx/csp.go

主要是获取header中的响应头

go
// CSPHeaders is an incomplete list of most common CSP headers
var CSPHeaders []string = []string{
    "Content-Security-Policy",               // standard
    "Content-Security-Policy-Report-Only",   // standard
    "X-Content-Security-Policy-Report-Only", // non - standard
    "X-Webkit-Csp-Report-Only",              // non - standard
}

HTTP2.0

httpx支持http2.0的探针,但不太明白实际中这个作用是什么

go
httpx.client2 = &http.Client{
        Transport: &http2.Transport{
            TLSClientConfig: &tls.Config{
                InsecureSkipVerify: true,
            },
            AllowHTTP: true,
        },
        Timeout: httpx.Options.Timeout,
    }

Pipeline 探针

http 1.1支持管道传输,例如在一个数据包中发送这样的包

GET / HTTP/1.1
HOST: 127.0.0.1

GET / HTTP/1.1
HOST: 127.0.0.1

GET / HTTP/1.1
HOST: 127.0.0.1

检测最后的返回是否合法。

go
package httpx

import (
    "crypto/tls"
    "fmt"
    "net"
    "strings"
    "time"
)

// SupportPipeline checks if the target host supports HTTP1.1 pipelining by sending x probes
// and reading back responses expecting at least 2 with HTTP/1.1 or HTTP/1.0
func (h *HTTPX) SupportPipeline(protocol, method, host string, port int) bool {
    addr := host
    if port == 0 {
        port = 80
        if protocol == "https" {
            port = 443
        }
    }
    if port > 0 {
        addr = fmt.Sprintf("%s:%d", host, port)
    }
    // dummy method while awaiting for full rawhttp implementation
    dummyReq := fmt.Sprintf("%s / HTTP/1.1\nHost: %s\n\n", method, addr)
    conn, err := pipelineDial(protocol, addr)
    if err != nil {
        return false
    }
    // send some probes
    nprobes := 10
    for i := 0; i < nprobes; i++ {
        if _, err = conn.Write([]byte(dummyReq)); err != nil {
            return false
        }
    }
    gotReplies := 0
    reply := make([]byte, 1024)
    for i := 0; i < nprobes; i++ {
        err := conn.SetReadDeadline(time.Now().Add(1 * time.Second))
        if err != nil {
            return false
        }

        if _, err := conn.Read(reply); err != nil {
            break
        }

        // The check is very naive, but it works most of the times
        for _, s := range strings.Split(string(reply), "\n\n") {
            if strings.Contains(s, "HTTP/1.1") || strings.Contains(s, "HTTP/1.0") {
                gotReplies++
            }
        }
    }

    // expect at least 2 replies
    return gotReplies >= 2
}

func pipelineDial(protocol, addr string) (net.Conn, error) {
    // http
    if protocol == "http" {
        return net.Dial("tcp", addr)
    }

    // https
    return tls.Dial("tcp", addr, &tls.Config{InsecureSkipVerify: true})
}

Title

httpx里面的title单独对中文编码做了优化

image-20210312113849879

但是不太完美,只对返回头做了判断,也有从内容meta中定义编码的,也有gbk编码的,有兴趣的老哥可以去提哥pr。

HTTP 证书信息

go内置的网络库已经把tls信息获取并解析了,直接获取即可。

go
package httpx

import (
    "net/http"
)

// TLSData contains the relevant Transport Layer Security information
type TLSData struct {
    DNSNames         []string `json:"dns_names,omitempty"`
    Emails           []string `json:"emails,omitempty"`
    CommonName       []string `json:"common_name,omitempty"`
    Organization     []string `json:"organization,omitempty"`
    IssuerCommonName []string `json:"issuer_common_name,omitempty"`
    IssuerOrg        []string `json:"issuer_organization,omitempty"`
}

// TLSGrab fills the TLSData
func (h *HTTPX) TLSGrab(r *http.Response) *TLSData {
    if r.TLS != nil {
        var tlsdata TLSData
        for _, certificate := range r.TLS.PeerCertificates {
            tlsdata.DNSNames = append(tlsdata.DNSNames, certificate.DNSNames...)
            tlsdata.Emails = append(tlsdata.Emails, certificate.EmailAddresses...)
            tlsdata.CommonName = append(tlsdata.CommonName, certificate.Subject.CommonName)
            tlsdata.Organization = append(tlsdata.Organization, certificate.Subject.Organization...)
            tlsdata.IssuerOrg = append(tlsdata.IssuerOrg, certificate.Issuer.Organization...)
            tlsdata.IssuerCommonName = append(tlsdata.IssuerCommonName, certificate.Issuer.CommonName)
        }
        return &tlsdata
    }
    return nil
}

检测virtualhost

检测是否是虚拟主机,方法很简单,将host换成一个随机值,和原来的返回进行比对,不一样则为虚拟主机。

当然比对的方法有很多,状态码,长度,单词数,行数,相似度等等。

go
package httpx

import (
    "fmt"

    "github.com/hbakhtiyor/strsim"
    retryablehttp "github.com/projectdiscovery/retryablehttp-go"
    "github.com/rs/xid"
)

const simMultiplier = 100

// IsVirtualHost checks if the target endpoint is a virtual host
func (h *HTTPX) IsVirtualHost(req *retryablehttp.Request) (bool, error) {
    httpresp1, err := h.Do(req)
    if err != nil {
        return false, err
    }

    // request a non-existing endpoint
    req.Host = fmt.Sprintf("%s.%s", xid.New().String(), req.Host)

    httpresp2, err := h.Do(req)
    if err != nil {
        return false, err
    }

    // Status Code
    if !h.Options.VHostIgnoreStatusCode && httpresp1.StatusCode != httpresp2.StatusCode {
        return true, nil
    }

    // Content - Bytes Length
    if !h.Options.VHostIgnoreContentLength && httpresp1.ContentLength != httpresp2.ContentLength {
        return true, nil
    }

    // Content - Number of words (space separated)
    if !h.Options.VHostIgnoreNumberOfWords && httpresp1.Words != httpresp2.Words {
        return true, nil
    }

    // Content - Number of lines (newline separated)
    if !h.Options.VHostIgnoreNumberOfLines && httpresp1.Lines != httpresp2.Lines {
        return true, nil
    }

    // Similarity Ratio - if similarity is under threshold we consider it a valid vHost
    if int(strsim.Compare(httpresp1.Raw, httpresp2.Raw)*simMultiplier) <= h.Options.VHostSimilarityRatio {
        return true, nil
    }

    return false, nil
}

不为人知的优化

autofdmax

go
// automatic fd max increase if running as root
    _ "github.com/projectdiscovery/fdmax/autofdmax

之前看naabu时也说过哦

大多数类UNIX操作系统(包括Linux和macOS)在每个进程和每个用户的基础上提供了系统资源的限制和控制(如线程,文件和网络连接)的方法。 这些“ulimits”阻止单个用户使用太多系统资源。

这个库的作用是自动将限制调到最大。

dns缓存

httpx调用了github.com/projectdiscovery/fastdialer/fastdialer

会缓存dns记录(文件式),加快请求速度

http/https的识别转换

httpx默认会请求http/https两种协议,默认先请求https,如果失败了会再请求http,这样就能识别出使用了http还是https了。

internal/runner/runner.go

image-20210312142919960

可以学习的

结合GitHub action的自动编译发布

实现打个tag,github就自动帮你编译发布,在开源软件中还是比较方便的

新建.github/workflows目录,新建release.yml

yaml
name: Release
on:
  create:
    tags:
      - v*

jobs: 
  release: 
    runs-on: ubuntu-latest
    steps: 
      - 
        name: "Check out code"
        uses: actions/checkout@v2
        with: 
          fetch-depth: 0
      - 
        name: "Set up Go"
        uses: actions/setup-go@v2
        with: 
          go-version: 1.14
      - 
        env: 
          GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
        name: "Create release on GitHub"
        uses: goreleaser/goreleaser-action@v2
        with: 
          args: "release --rm-dist"
          version: latest

编译分为三步,第一步签出代码,第二部安装go环境,第三部使用goreleaser/goreleaser-action编译上传。

说下第三步,在根目录新建.goreleaser.yml文件,文件内容用于配制编译以及上传选项,httpx的配置如下

yaml
builds:
    - binary: httpx
      main: cmd/httpx/httpx.go
      goos:
        - linux
        - windows
        - darwin
      goarch:
        - amd64
        - 386
        - arm
        - arm64

archives:
    - id: tgz
      format: tar.gz
      replacements:
          darwin: macOS
      format_overrides:
          - goos: windows
            format: zip

具体字段的介绍可查看:https://goreleaser.com/intro/

最后

projectdiscover为一些项目造了很多go的基础轮子,用起来很方便,这真的是tql。

撰写

布局切换

调整 VitePress 的布局样式,以适配不同的阅读习惯和屏幕环境。

全部展开
使侧边栏和内容区域占据整个屏幕的全部宽度。
全部展开,但侧边栏宽度可调
侧边栏宽度可调,但内容区域宽度不变,调整后的侧边栏将可以占据整个屏幕的最大宽度。
全部展开,且侧边栏和内容区域宽度均可调
侧边栏宽度可调,但内容区域宽度不变,调整后的侧边栏将可以占据整个屏幕的最大宽度。
原始宽度
原始的 VitePress 默认布局宽度

页面最大宽度

调整 VitePress 布局中页面的宽度,以适配不同的阅读习惯和屏幕环境。

调整页面最大宽度
一个可调整的滑块,用于选择和自定义页面最大宽度。

内容最大宽度

调整 VitePress 布局中内容区域的宽度,以适配不同的阅读习惯和屏幕环境。

调整内容最大宽度
一个可调整的滑块,用于选择和自定义内容最大宽度。

聚光灯

支持在正文中高亮当前鼠标悬停的行和元素,以优化阅读和专注困难的用户的阅读体验。

ON开启
开启聚光灯。
OFF关闭
关闭聚光灯。

聚光灯样式

调整聚光灯的样式。

置于底部
在当前鼠标悬停的元素下方添加一个纯色背景以突出显示当前鼠标悬停的位置。
置于侧边
在当前鼠标悬停的元素旁边添加一条固定的纯色线以突出显示当前鼠标悬停的位置。