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

2023-04-23 09:32:50 +08:00
 dzdh

比如客户端是 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 值呢?

1676 次点击
所在节点    Go 编程语言
19 条回复
Nazz
2023-04-23 09:38:43 +08:00
需要库 /框架的支持, 试试 gnet, netpoll
0o0O0o0O0o
2023-04-23 09:41:32 +08:00
把 ja3 ( tls )的逻辑放进 http handle ,我觉得已经是高度定制了,建议自己实现这样的 http 库。
Nazz
2023-04-23 09:46:12 +08:00
nbio 也是支持的 OnOpen, SetSession 的, 比其他异步框架更易用一些
sofukwird
2023-04-23 10:38:13 +08:00
放 header 里,自定义 header 字段 X-Custom-Field ,每次进来都初始化默认值,避免客户端注入 header
dzdh
2023-04-23 10:50:38 +08:00
@sofukwird verifypeer 操作不了 request
aladdinding
2023-04-23 10:51:20 +08:00
重写 conn 的 read 方法就行了,可以看下 cmux 这个库
lesismal
2023-04-23 10:52:00 +08:00
如果只是要知道 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
2023-04-23 11:01:06 +08:00
哦对了,基于标准库的代码部分,我忘记处理连接关闭时 Map.Delete 了,得看下 ConnState 之类的 Hook 能不能处理,如果不能处理,那基于标准库还不能这样简单搞、连接断开不清理就相当于泄露了导致 map 无限增长
lesismal
2023-04-23 11:08:47 +08:00
#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
2023-04-23 11:11:05 +08:00
总结一下各位佬推荐的是 wrapper 思路。也未尝不可。我尝试一下。
0o0O0o0O0o
2023-04-23 11:25:50 +08:00
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
2023-04-23 11:27:35 +08:00
像这样拿 conn 乃至 tls conn 是可以的,问题就是怎么从 tls conn 读 handshake 信息,无非是再给 conn 包一层,可是浪费性能,不如修改 tls 代码。

高度定制的逻辑没必要追求用原版。
sofukwird
2023-04-23 11:34:23 +08:00
@dzdh 你可以参考 caddy 的做法,它能把证书信息写到 header 里
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
2023-04-23 13:06:40 +08:00
@lesismal #14

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

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

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

而且我觉得 tls 很复杂,我一直是不喜欢用 go 直接 serve tls ,我平时对付这种需求都是 docker compose 里套一个 nginx 。。。
lesismal
2023-04-23 13:38:42 +08:00
@0o0O0o0O0o
其实用 go 最大的好处是:可以省去 nginx 了。:joy:
lesismal
2023-04-23 13:40:30 +08:00
#17 go 性能、占用、开发效率能达到非常好的平衡。自定制起来也比 nginx lua/resty 要更有潜力。
lesismal
2023-04-23 13:42:45 +08:00
@0o0O0o0O0o 我们的一些新项目,没有历史包袱,就没有部署 nginx 了。但很多团队里,nginx 这些似乎成了一种技术惯性,不管有没有必要、都部署,整个社区需要很长时间去摆脱这个惯性

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/934674

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX