V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
The Go Programming Language
http://golang.org/
Go Playground
Go Projects
Revel Web Framework
dzdh
V2EX  ›  Go 编程语言

怎么在 http sever 的链接握手时设置一些变量能在 handle 里接收到呢?

  •  
  •   dzdh · 2023-04-23 09:32:50 +08:00 · 1680 次点击
    这是一个创建于 614 天前的主题,其中的信息可能已经有所发展或是发生改变。

    比如客户端是 ipv6 ,我可能想在 server.OnConnect (假如有这么一个 callback) 时候设置一个 context.WithValue(conn.context, "is-ipv6", "true")然后在 handle 里判断 context.Value("is-ipv6") == "true" 来进行判断并处理一些逻辑

    当然只是举个栗子,并不一定真是判断是不是 ipv6

    再比如,https 我想在 HandshakeComplete 时获取一下客户端证书的内容( DN/CN 啥的),我知道可以在 handle 获取 peercertificate ,但要是想计算 JA3 指纹呢 看了看只有在 clienthello 阶段能获取到链接的一些信息,在 handle 里就没有了,关键在tls.Config.GetConfigForClient/tls.Config.VerifyPeerConnection等 callback 中,也没法设置请求上下文的值。

    然后重新总结一下问题,就是在 http/https 的连接建立时候有什么方法或者怎么实现 设置仅限当前请求有效的 context 值呢?

    19 条回复    2023-04-23 13:42:45 +08:00
    Nazz
        1
    Nazz  
       2023-04-23 09:38:43 +08:00   ❤️ 1
    需要库 /框架的支持, 试试 gnet, netpoll
    0o0O0o0O0o
        2
    0o0O0o0O0o  
       2023-04-23 09:41:32 +08:00 via iPhone
    把 ja3 ( tls )的逻辑放进 http handle ,我觉得已经是高度定制了,建议自己实现这样的 http 库。
    Nazz
        3
    Nazz  
       2023-04-23 09:46:12 +08:00   ❤️ 1
    nbio 也是支持的 OnOpen, SetSession 的, 比其他异步框架更易用一些
    sofukwird
        4
    sofukwird  
       2023-04-23 10:38:13 +08:00 via Android
    放 header 里,自定义 header 字段 X-Custom-Field ,每次进来都初始化默认值,避免客户端注入 header
    dzdh
        5
    dzdh  
    OP
       2023-04-23 10:50:38 +08:00
    @sofukwird verifypeer 操作不了 request
    aladdinding
        6
    aladdinding  
       2023-04-23 10:51:20 +08:00   ❤️ 1
    重写 conn 的 read 方法就行了,可以看下 cmux 这个库
    lesismal
        7
    lesismal  
       2023-04-23 10:52:00 +08:00   ❤️ 1
    如果只是要知道 ip 相关,使用 RemoteAddr 就可以了。

    如果是需要其他信息(但楼主不讲明是需要什么的前提下我还真想不出 conn 除了 remote addr 还需要啥信息这么有必要去特殊处理),基于标准库包装一下 Listener 自己 map 映射存上也可以,或者基于 nbio OnOpen 。

    基于标准库和 nbio 的区别是:
    基于标准库的方式在 handler 里没法拿到 Conn ,因为只有 Hijack 一种方法拿到,但拿到后标准库 http server 就不继续处理该 Conn 了、Hijack 得到的 Conn 处理权转给了用户;
    nbio 的 http handler 里除了 Hijack 的方式、也可以通过类型断言拿到 Conn 并且不涉及处理权的转移。

    完整代码:
    https://gist.github.com/lesismal/316b711a7f39cc539cebaad6c8e5b0fd

    代码只示例了 4 层的 Conn ,如果需要 tls ,另外再加点处理、OP 可以自行探索
    lesismal
        8
    lesismal  
       2023-04-23 11:01:06 +08:00   ❤️ 1
    哦对了,基于标准库的代码部分,我忘记处理连接关闭时 Map.Delete 了,得看下 ConnState 之类的 Hook 能不能处理,如果不能处理,那基于标准库还不能这样简单搞、连接断开不清理就相当于泄露了导致 map 无限增长
    lesismal
        9
    lesismal  
       2023-04-23 11:08:47 +08:00   ❤️ 1
    #8

    修改了下,用标准库的没必要自己封装 Listener ,直接 ConnState 处理就可以了,代码更新了

    ```golang
    package main

    import (
    "context"
    "fmt"
    "net"
    "net/http"
    "os"
    "os/signal"
    "sync"
    "time"

    "github.com/lesismal/nbio/nbhttp"
    )

    var connections = sync.Map{}

    func nbioOnEcho(w http.ResponseWriter, r *http.Request) {
    res, ok := w.(*nbhttp.Response)
    if ok {
    conn := res.Parser.Processor.Conn()
    yourData, ok := connections.Load(conn)
    if ok {
    fmt.Println("nbioServer onEcho:", yourData)
    } else {
    fmt.Println("nbioServer onEcho: not found connection")
    }
    }
    w.Write([]byte(time.Now().Format("20060102 15:04:05")))
    }

    func nbioServer() {
    mux := &http.ServeMux{}
    mux.HandleFunc("/", nbioOnEcho)

    engine := nbhttp.NewServer(nbhttp.Config{
    Network: "tcp",
    Addrs: []string{"localhost:8080"},
    Handler: mux,
    IOMod: nbhttp.IOModBlocking,
    })
    engine.OnOpen(func(conn net.Conn) {
    fmt.Println("nbioServer onOpen:", conn.RemoteAddr())
    yourData := "data: " + conn.RemoteAddr().String() // or other things
    connections.Store(conn, yourData)
    })
    engine.OnClose(func(conn net.Conn, err error) {
    connections.Delete(conn)
    fmt.Println("nbioServer onClose:", conn.RemoteAddr(), err)
    })

    err := engine.Start()
    if err != nil {
    fmt.Printf("nbio.Start failed: %v\n", err)
    return
    }

    interrupt := make(chan os.Signal, 1)
    signal.Notify(interrupt, os.Interrupt)
    <-interrupt

    ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
    defer cancel()
    engine.Shutdown(ctx)
    }

    func netOnEcho(w http.ResponseWriter, r *http.Request) {
    yourData, ok := connections.Load(r.RemoteAddr)
    if ok {
    fmt.Println("netServer onEcho:", yourData)
    } else {
    fmt.Println("netServer onEcho: not found connection " + r.RemoteAddr)
    }
    w.Write([]byte(time.Now().Format("20060102 15:04:05")))
    }

    func netServer() {
    mux := &http.ServeMux{}
    mux.HandleFunc("/", netOnEcho)

    server := &http.Server{
    Addr: "localhost:8080",
    Handler: mux,
    ConnState: func(conn net.Conn, state http.ConnState) {
    switch state {

    case http.StateNew:
    fmt.Println("netServer onOpen:", conn.RemoteAddr())
    remoteAddr := conn.RemoteAddr().String()
    yourData := "data: " + remoteAddr // or other things
    connections.Store(remoteAddr, yourData)
    case http.StateActive:
    case http.StateIdle:
    case http.StateHijacked:
    case http.StateClosed:
    connections.Delete(conn)
    fmt.Println("netServer onClose:", conn.RemoteAddr())
    }
    },
    }
    err := server.ListenAndServe()
    fmt.Println("server exit:", err)
    }

    func main() {
    netServer()
    // nbioServer()
    }
    ```
    dzdh
        10
    dzdh  
    OP
       2023-04-23 11:11:05 +08:00
    总结一下各位佬推荐的是 wrapper 思路。也未尝不可。我尝试一下。
    0o0O0o0O0o
        11
    0o0O0o0O0o  
       2023-04-23 11:25:50 +08:00   ❤️ 1
    package main

    import (
    "context"
    "crypto/tls"
    "fmt"
    "net"
    "net/http"
    "reflect"
    )

    type contextKey struct {
    key string
    }

    var ConnContextKey = &contextKey{"http-conn"}

    func SaveConnInContext(ctx context.Context, c net.Conn) context.Context {
    return context.WithValue(ctx, ConnContextKey, c)
    }
    func GetConn(r *http.Request) net.Conn {
    return r.Context().Value(ConnContextKey).(net.Conn)
    }

    func main() {
    http.HandleFunc("/", myHandler)

    server := http.Server{
    Addr: ":8443",
    ConnContext: SaveConnInContext,
    }
    server.ListenAndServeTLS("server.crt", "server.key")
    // server.ListenAndServe()
    }

    func myHandler(w http.ResponseWriter, r *http.Request) {
    conn := GetConn(r)

    switch c := conn.(type) {
    case *tls.Conn:
    fmt.Println(reflect.ValueOf(c).Elem().FieldByName("config"))
    }

    fmt.Fprintf(w, "%s\n", conn.RemoteAddr())
    w.WriteHeader(200)
    }
    0o0O0o0O0o
        12
    0o0O0o0O0o  
       2023-04-23 11:27:35 +08:00
    像这样拿 conn 乃至 tls conn 是可以的,问题就是怎么从 tls conn 读 handshake 信息,无非是再给 conn 包一层,可是浪费性能,不如修改 tls 代码。

    高度定制的逻辑没必要追求用原版。
    sofukwird
        13
    sofukwird  
       2023-04-23 11:34:23 +08:00 via Android
    @dzdh 你可以参考 caddy 的做法,它能把证书信息写到 header 里
    lesismal
        14
    lesismal  
       2023-04-23 13:04:14 +08:00
    @0o0O0o0O0o SaveConnInContext 这个挺好,学到了

    @dzdh 要不我把 nbio 的 tls 的 clientHello 以及 clientHello 的 marshal 暴露出来,然后就方便拿到了,我本地改了试了下是可以的了,暂时没有更新到 repo:
    https://gist.github.com/lesismal/316b711a7f39cc539cebaad6c8e5b0fd?permalink_comment_id=4545513#gistcomment-4545513



    但是 nbio 只支持 http1.x ,http2.0/3.0QUIC 功能太多、我短期内没精力去做了。

    或者 OP 也可以另一种方案,还是先自己封装下 Listener ,tls Accept 得到的 tls Conn 用 RemoteAddr string 存 map 里,handler 里取出 tls Conn ,并且反射或者自己 type 一个相同 fields 的结构体但是 ClientHello 大写导出的方式+unsafe 指针强转类型后把 clientHello 这个字段取出来用,但是这样怕 go 不同版本该结构体字段发生变化,要自己额外做一些对应 go 不同版本编译的兼容
    lesismal
        15
    lesismal  
       2023-04-23 13:06:40 +08:00
    @lesismal #14

    > 或者 OP 也可以另一种方案,还是先自己封装下 Listener ,tls Accept 得到的 tls Conn 用 RemoteAddr string 存 map 里

    ConnContext 就可以了,更简单
    0o0O0o0O0o
        16
    0o0O0o0O0o  
       2023-04-23 13:18:18 +08:00 via iPhone
    @lesismal

    虽说肯定能实现,但其实我觉得他这种需求还是丢给 nginx 去做比较好,也有现成的。

    而且我觉得 tls 很复杂,我一直是不喜欢用 go 直接 serve tls ,我平时对付这种需求都是 docker compose 里套一个 nginx 。。。
    lesismal
        17
    lesismal  
       2023-04-23 13:38:42 +08:00
    @0o0O0o0O0o
    其实用 go 最大的好处是:可以省去 nginx 了。:joy:
    lesismal
        18
    lesismal  
       2023-04-23 13:40:30 +08:00   ❤️ 1
    #17 go 性能、占用、开发效率能达到非常好的平衡。自定制起来也比 nginx lua/resty 要更有潜力。
    lesismal
        19
    lesismal  
       2023-04-23 13:42:45 +08:00
    @0o0O0o0O0o 我们的一些新项目,没有历史包袱,就没有部署 nginx 了。但很多团队里,nginx 这些似乎成了一种技术惯性,不管有没有必要、都部署,整个社区需要很长时间去摆脱这个惯性
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2560 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 24ms · UTC 15:02 · PVG 23:02 · LAX 07:02 · JFK 10:02
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.