1

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

 1 year ago
source link: https://www.v2ex.com/t/934674
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

V2EX  ›  Go 编程语言

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

  dzdh · 4 小时 7 分钟前 · 585 次点击

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

18 条回复    2023-04-23 13:40:30 +08:00
Nazz

Nazz      4 小时 1 分钟前   ❤️ 1

需要库 /框架的支持, 试试 gnet, netpoll
0o0O0o0O0o

0o0O0o0O0o      3 小时 59 分钟前 via iPhone

把 ja3 ( tls )的逻辑放进 http handle ,我觉得已经是高度定制了,建议自己实现这样的 http 库。
Nazz

Nazz      3 小时 54 分钟前   ❤️ 1

nbio 也是支持的 OnOpen, SetSession 的, 比其他异步框架更易用一些
sofukwird

sofukwird      3 小时 2 分钟前 via Android

放 header 里,自定义 header 字段 X-Custom-Field ,每次进来都初始化默认值,避免客户端注入 header
dzdh

dzdh      2 小时 50 分钟前

@sofukwird verifypeer 操作不了 request
aladdinding

aladdinding      2 小时 49 分钟前   ❤️ 1

重写 conn 的 read 方法就行了,可以看下 cmux 这个库
lesismal

lesismal      2 小时 48 分钟前   ❤️ 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

lesismal      2 小时 39 分钟前   ❤️ 1

哦对了,基于标准库的代码部分,我忘记处理连接关闭时 Map.Delete 了,得看下 ConnState 之类的 Hook 能不能处理,如果不能处理,那基于标准库还不能这样简单搞、连接断开不清理就相当于泄露了导致 map 无限增长
lesismal

lesismal      2 小时 31 分钟前   ❤️ 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

dzdh      2 小时 29 分钟前

总结一下各位佬推荐的是 wrapper 思路。也未尝不可。我尝试一下。
0o0O0o0O0o

0o0O0o0O0o      2 小时 14 分钟前   ❤️ 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

0o0O0o0O0o      2 小时 13 分钟前

像这样拿 conn 乃至 tls conn 是可以的,问题就是怎么从 tls conn 读 handshake 信息,无非是再给 conn 包一层,可是浪费性能,不如修改 tls 代码。

高度定制的逻辑没必要追求用原版。
sofukwird

sofukwird      2 小时 6 分钟前 via Android

@dzdh 你可以参考 caddy 的做法,它能把证书信息写到 header 里
lesismal

lesismal      36 分钟前

@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

lesismal      33 分钟前

@lesismal #14

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

ConnContext 就可以了,更简单
0o0O0o0O0o

0o0O0o0O0o      22 分钟前 via iPhone

@lesismal

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

而且我觉得 tls 很复杂,我一直是不喜欢用 go 直接 serve tls ,我平时对付这种需求都是 docker compose 里套一个 nginx 。。。
lesismal

lesismal      1 分钟前

@0o0O0o0O0o
其实用 go 最大的好处是:可以省去 nginx 了。:joy:
lesismal

lesismal      刚刚

#17 go 性能、占用、开发效率能达到非常好的平衡。自定制起来也比 nginx lua/resty 要更有潜力。

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK