diff --git a/README.md b/README.md index d516c90..93a7e5d 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,12 @@ `reserve proxy` extension for Hertz +## Install + +```shell +go get github.com/hertz-contrib/reverseproxy +``` + ## Quick Start ```go @@ -86,5 +92,30 @@ func main() { `ReverseProxy` provides `SetDirector`、`SetModifyResponse`、`SetErrorHandler` to modify `Request` and `Response`. +### Websocket Reverse Proxy + +Websocket reverse proxy for Hertz, inspired by [fasthttp-reverse-proxy](https://github.com/yeqown/fasthttp-reverse-proxy) + +```go +package main + +import ( + "github.com/cloudwego/hertz/pkg/app/server" + "github.com/hertz-contrib/reverseproxy" +) + +func main() { + h := server.Default() + h.GET("/backend", reverseproxy.NewWSReverseProxy("ws://example.com").ServeHTTP) + h.Spin() +} +``` + +| Configuration | Default | Description | +|----------------|---------------------------|------------------------------| +| `WithDirector` | `nil` | customize the forward header | +| `WithDialer` | `gorillaws.DefaultDialer` | for dialer customization | +| `WithUpgrader` | `hzws.HertzUpgrader` | for upgrader customization | + ### More info See [example](https://github.com/cloudwego/hertz-examples) diff --git a/go.mod b/go.mod index 0ccd1e1..8a5e219 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,9 @@ module github.com/hertz-contrib/reverseproxy go 1.16 -require github.com/cloudwego/hertz v0.6.5 +require ( + github.com/bytedance/gopkg v0.0.0-20220413063733-65bf48ffb3a7 + github.com/cloudwego/hertz v0.6.5 + github.com/gorilla/websocket v1.5.1 + github.com/hertz-contrib/websocket v0.0.1 +) diff --git a/go.sum b/go.sum index 5e23caa..efbd268 100644 --- a/go.sum +++ b/go.sum @@ -4,14 +4,17 @@ github.com/bytedance/gopkg v0.0.0-20220413063733-65bf48ffb3a7 h1:PtwsQyQJGxf8iaP github.com/bytedance/gopkg v0.0.0-20220413063733-65bf48ffb3a7/go.mod h1:2ZlV9BaUH4+NXIBF0aMdKKAnHTzqH+iMU4KUjAbL23Q= github.com/bytedance/mockey v1.2.1 h1:g84ngI88hz1DR4wZTL3yOuqlEcq67MretBfQUdXwrmw= github.com/bytedance/mockey v1.2.1/go.mod h1:+Jm/fzWZAuhEDrPXVjDf/jLM2BlLXJkwk94zf2JZ3X4= +github.com/bytedance/sonic v1.3.0/go.mod h1:V973WhNhGmvHxW6nQmsHEfHaoU9F3zTF+93rH03hcUQ= github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= github.com/bytedance/sonic v1.8.1 h1:NqAHCaGaTzro0xMmnTCLUyRlbEP6r8MCA1cJUrH3Pu4= github.com/bytedance/sonic v1.8.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= +github.com/cloudwego/hertz v0.3.0/go.mod h1:GWWYlAVkq1gDu6vJd/XNciWsP6q0d4TrEKk5fpJYF04= github.com/cloudwego/hertz v0.6.5 h1:2yZY8Tn4YIX7CF75oaGw3NymXgewPzGcxdwfVKfNBCo= github.com/cloudwego/hertz v0.6.5/go.mod h1:KhztQcZtMQ46gOjZcmCy557AKD29cbumGEV0BzwevwA= +github.com/cloudwego/netpoll v0.2.4/go.mod h1:1T2WVuQ+MQw6h6DpE45MohSvDTKdy2DlzCx2KsnPI4E= github.com/cloudwego/netpoll v0.3.2 h1:/998ICrNMVBo4mlul4j7qcIeY7QnEfuCCPPwck9S3X4= github.com/cloudwego/netpoll v0.3.2/go.mod h1:xVefXptcyheopwNDZjDPcfU6kIjZXZ4nY550k1yH9eQ= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -19,26 +22,38 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= +github.com/goccy/go-json v0.9.4/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= +github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= github.com/henrylee2cn/ameda v1.4.8/go.mod h1:liZulR8DgHxdK+MEwvZIylGnmcjzQ6N6f2PlWe7nEO4= github.com/henrylee2cn/ameda v1.4.10 h1:JdvI2Ekq7tapdPsuhrc4CaFiqw6QXFvZIULWJgQyCAk= github.com/henrylee2cn/ameda v1.4.10/go.mod h1:liZulR8DgHxdK+MEwvZIylGnmcjzQ6N6f2PlWe7nEO4= github.com/henrylee2cn/goutil v0.0.0-20210127050712-89660552f6f8 h1:yE9ULgp02BhYIrO6sdV/FPe0xQM6fNHkVQW2IAymfM0= github.com/henrylee2cn/goutil v0.0.0-20210127050712-89660552f6f8/go.mod h1:Nhe/DM3671a5udlv2AdV2ni/MZzgfv2qrPL5nIi3EGQ= +github.com/hertz-contrib/websocket v0.0.1 h1:NVtGICwqFyAXPotY/KGwYMXSi2l1S+vM6JJMqkIu0Ho= +github.com/hertz-contrib/websocket v0.0.1/go.mod h1:rBtjAV7auKVBjtKvuQX9zzR8gZ2zKPHybPodAhqdbVo= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/nyaruka/phonenumbers v1.0.55 h1:bj0nTO88Y68KeUQ/n3Lo2KgK7lM1hF7L9NFuwcCl3yg= github.com/nyaruka/phonenumbers v1.0.55/go.mod h1:sDaTZ/KPX5f8qyV9qN+hIm+4ZBARJrupC6LuhshJq1U= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/savsgio/gotils v0.0.0-20220530130905-52f3993e8d6d h1:Q+gqLBOPkFGHyCJxXMRqtUgUbTjI8/Ze8vu8GGyNFwo= +github.com/savsgio/gotils v0.0.0-20220530130905-52f3993e8d6d/go.mod h1:Gy+0tqhJvgGlqnTF8CVGP0AaGRjwBtXs/a5PA0Y3+A4= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= @@ -46,6 +61,7 @@ github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -54,26 +70,66 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/tidwall/gjson v1.9.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.12.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/gjson v1.13.0 h1:3TFY9yxOQShrvmjdM76K+jc66zJeT6D3/VFFYCGQf7M= github.com/tidwall/gjson v1.13.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/sjson v1.2.4/go.mod h1:098SZ494YoMWPmMO6ct4dcFnqxwj9r/gF0Etp19pSNM= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/arch v0.0.0-20201008161808-52c3e6f60cff/go.mod h1:flIaEI6LNU6xOCD5PaJvn9wGP0agmIOqjrtsKGRguv4= golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220110181412-a018aaa089fe/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220412211240-33da011f77ad h1:ntjMns5wyP/fN65tdBD4g8J5w8n015+iIIs9rtjXkY0= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= diff --git a/licenses/LICENSE-fasthttp-reverse-proxy b/licenses/LICENSE-fasthttp-reverse-proxy new file mode 100644 index 0000000..9650b63 --- /dev/null +++ b/licenses/LICENSE-fasthttp-reverse-proxy @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 YeQiang + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/licenses/LICENSE-gorilla-websocket b/licenses/LICENSE-gorilla-websocket new file mode 100644 index 0000000..8692af6 --- /dev/null +++ b/licenses/LICENSE-gorilla-websocket @@ -0,0 +1,27 @@ +Copyright (c) 2023 The Gorilla Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/reverse_proxy_test.go b/reverse_proxy_test.go index 9ed83d2..6699aac 100644 --- a/reverse_proxy_test.go +++ b/reverse_proxy_test.go @@ -33,11 +33,10 @@ import ( "testing" "time" - "github.com/cloudwego/hertz/pkg/common/test/assert" - "github.com/cloudwego/hertz/pkg/app" "github.com/cloudwego/hertz/pkg/app/client" "github.com/cloudwego/hertz/pkg/app/server" + "github.com/cloudwego/hertz/pkg/common/test/assert" "github.com/cloudwego/hertz/pkg/protocol" ) diff --git a/ws_reverse_proxy.go b/ws_reverse_proxy.go new file mode 100644 index 0000000..90aa925 --- /dev/null +++ b/ws_reverse_proxy.go @@ -0,0 +1,247 @@ +// Copyright 2023 CloudWeGo Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// MIT License +// +// Copyright (c) 2018 YeQiang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +// This file may have been modified by CloudWeGo authors. +// All CloudWeGo Modifications are Copyright 2023 CloudWeGo Authors. + +package reverseproxy + +import ( + "bytes" + "context" + "errors" + "io" + "net/http" + + "github.com/bytedance/gopkg/util/gopool" + + "github.com/cloudwego/hertz/pkg/app" + "github.com/cloudwego/hertz/pkg/common/hlog" + "github.com/cloudwego/hertz/pkg/protocol" + "github.com/cloudwego/hertz/pkg/protocol/consts" + "github.com/gorilla/websocket" + hzws "github.com/hertz-contrib/websocket" +) + +type WSReverseProxy struct { + target string + options *Options +} + +// NewWSReverseProxy new a proxy which will provide handler for websocket reverse proxy +func NewWSReverseProxy(target string, opts ...Option) *WSReverseProxy { + if target == "" { + panic("target string must not be empty") + } + options := newOptions(opts...) + wsrp := &WSReverseProxy{ + target: target, + options: options, + } + return wsrp +} + +// ServeHTTP provides websocket reverse proxy service +func (w *WSReverseProxy) ServeHTTP(ctx context.Context, c *app.RequestContext) { + forwardHeader := prepareForwardHeader(ctx, c) + // NOTE: customer Director will overwrite existed header if they have the same header key + if w.options.Director != nil { + w.options.Director(ctx, c, forwardHeader) + } + connBackend, respBackend, err := w.options.Dialer.Dial(w.target, forwardHeader) + if err != nil { + hlog.CtxErrorf(ctx, "can not dial to remote backend(%v): %v", w.target, err) + if respBackend != nil { + if err = wsCopyResponse(&c.Response, respBackend); err != nil { + hlog.CtxErrorf(ctx, "can not copy response: %v", err) + } + } else { + c.AbortWithMsg(err.Error(), consts.StatusServiceUnavailable) + } + return + } + if err := w.options.Upgrader.Upgrade(c, func(connClient *hzws.Conn) { + defer connClient.Close() + + var ( + errClientC = make(chan error, 1) + errBackendC = make(chan error, 1) + errMsg string + ) + + hlog.CtxDebugf(ctx, "upgrade handler working...") + + // replicateWSRespConn + // ┌─────────────────────────────────┐ + // │ errClientC │ + // ┌─────▼──────┐ ┌──────┴──────┐ + // │ connClient │ │ connBackend │ + // └─────┬──────┘ └──────▲──────┘ + // │ errBackendC │ + // └─────────────────────────────────┘ + // replicateWSReqConn + // + // ┌──────────┐ ┌────────────────┐ ┌──────────┐ + // │ ├───────────► wsreverseproxy ├─────────────► backend │ + // │ client │ │ │ │ │ + // │ ◄───────────┤ (server) ◄─────────────┤ (server) │ + // └──────────┘ └────────────────┘ └──────────┘ + + gopool.CtxGo(ctx, func() { + replicateWSRespConn(ctx, connClient, connBackend, errClientC) + }) + gopool.CtxGo(ctx, func() { + replicateWSReqConn(ctx, connBackend, connClient, errBackendC) + }) + + for { + select { + case err = <-errClientC: + errMsg = "copy websocket response err: %v" + case err = <-errBackendC: + errMsg = "copy websocket request err: %v" + } + + var ce *websocket.CloseError + var hzce *hzws.CloseError + if !errors.As(err, &ce) || !errors.As(err, &hzce) { + hlog.CtxErrorf(ctx, errMsg, err) + } + } + }); err != nil { + hlog.CtxErrorf(ctx, "can not upgrade to websocket: %v", err) + } +} + +func prepareForwardHeader(_ context.Context, c *app.RequestContext) http.Header { + forwardHeader := make(http.Header, 4) + if origin := string(c.Request.Header.Peek("Origin")); origin != "" { + forwardHeader.Add("Origin", origin) + } + if proto := string(c.Request.Header.Peek("Sec-Websocket-Protocol")); proto != "" { + forwardHeader.Add("Sec-WebSocket-Protocol", proto) + } + if cookie := string(c.Request.Header.Peek("Cookie")); cookie != "" { + forwardHeader.Add("Cookie", cookie) + } + if host := string(c.Request.Host()); host != "" { + forwardHeader.Set("Host", host) + } + clientIP := c.ClientIP() + if prior := c.Request.Header.Peek("X-Forwarded-For"); prior != nil { + clientIP = string(prior) + ", " + clientIP + } + forwardHeader.Set("X-Forwarded-For", clientIP) + forwardHeader.Set("X-Forwarded-Proto", "http") + if string(c.Request.URI().Scheme()) == "https" { + forwardHeader.Set("X-Forwarded-Proto", "https") + } + return forwardHeader +} + +func replicateWSReqConn(ctx context.Context, dst *websocket.Conn, src *hzws.Conn, errC chan error) { + for { + msgType, msg, err := src.ReadMessage() + if err != nil { + hlog.CtxErrorf(ctx, "read message failed when replicating websocket conn: msgType=%v msg=%v err=%v", msgType, msg, err) + var ce *hzws.CloseError + if errors.As(err, &ce) { + msg = hzws.FormatCloseMessage(ce.Code, ce.Text) + } else { + hlog.CtxErrorf(ctx, "read message failed when replicate websocket conn: err=%v", err) + msg = hzws.FormatCloseMessage(hzws.CloseAbnormalClosure, err.Error()) + } + errC <- err + + if err = dst.WriteMessage(websocket.CloseMessage, msg); err != nil { + hlog.CtxErrorf(ctx, "write message failed when replicate websocket conn: err=%v", err) + } + break + } + + err = dst.WriteMessage(msgType, msg) + if err != nil { + hlog.CtxErrorf(ctx, "write message failed when replicating websocket conn: msgType=%v msg=%v err=%v", msgType, msg, err) + errC <- err + break + } + } +} + +func replicateWSRespConn(ctx context.Context, dst *hzws.Conn, src *websocket.Conn, errC chan error) { + for { + msgType, msg, err := src.ReadMessage() + if err != nil { + hlog.CtxErrorf(ctx, "read message failed when replicating websocket conn: msgType=%v msg=%v err=%v", msgType, msg, err) + var ce *websocket.CloseError + if errors.As(err, &ce) { + msg = websocket.FormatCloseMessage(ce.Code, ce.Text) + } else { + hlog.CtxErrorf(ctx, "read message failed when replicate websocket conn: err=%v", err) + msg = websocket.FormatCloseMessage(websocket.CloseAbnormalClosure, err.Error()) + } + errC <- err + + if err = dst.WriteMessage(hzws.CloseMessage, msg); err != nil { + hlog.CtxErrorf(ctx, "write message failed when replicate websocket conn: err=%v", err) + } + break + } + + err = dst.WriteMessage(msgType, msg) + if err != nil { + hlog.CtxErrorf(ctx, "write message failed when replicating websocket conn: msgType=%v msg=%v err=%v", msgType, msg, err) + errC <- err + break + } + } +} + +func wsCopyResponse(dst *protocol.Response, src *http.Response) error { + for k, vs := range src.Header { + for _, v := range vs { + dst.Header.Add(k, v) + } + } + dst.SetStatusCode(src.StatusCode) + defer src.Body.Close() + buf := bytes.NewBuffer(nil) + if _, err := io.Copy(buf, src.Body); err != nil { + return err + } + dst.SetBody(buf.Bytes()) + return nil +} diff --git a/ws_reverse_proxy_option.go b/ws_reverse_proxy_option.go new file mode 100644 index 0000000..a24c6da --- /dev/null +++ b/ws_reverse_proxy_option.go @@ -0,0 +1,81 @@ +// Copyright 2023 CloudWeGo Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package reverseproxy + +import ( + "context" + "net/http" + + "github.com/cloudwego/hertz/pkg/app" + "github.com/gorilla/websocket" + hzws "github.com/hertz-contrib/websocket" +) + +type Director func(ctx context.Context, c *app.RequestContext, forwardHeader http.Header) + +type Option func(o *Options) + +type Options struct { + Director Director + Dialer *websocket.Dialer + Upgrader *hzws.HertzUpgrader +} + +var DefaultOptions = &Options{ + Director: nil, + Dialer: websocket.DefaultDialer, + Upgrader: &hzws.HertzUpgrader{ + ReadBufferSize: 1024, + WriteBufferSize: 1024, + }, +} + +func newOptions(opts ...Option) *Options { + options := &Options{ + Director: DefaultOptions.Director, + Dialer: DefaultOptions.Dialer, + Upgrader: DefaultOptions.Upgrader, + } + options.apply(opts...) + return options +} + +func (o *Options) apply(opts ...Option) { + for _, opt := range opts { + opt(o) + } +} + +// WithDialer for dialer customization +func WithDialer(dialer *websocket.Dialer) Option { + return func(o *Options) { + o.Dialer = dialer + } +} + +// WithDirector user can edit the forward header by using custom Director +// NOTE: custom Director will overwrite default forward header field if they have the same key +func WithDirector(director Director) Option { + return func(o *Options) { + o.Director = director + } +} + +// WithUpgrader for upgrader customization +func WithUpgrader(upgrader *hzws.HertzUpgrader) Option { + return func(o *Options) { + o.Upgrader = upgrader + } +} diff --git a/ws_reverse_proxy_option_test.go b/ws_reverse_proxy_option_test.go new file mode 100644 index 0000000..bdf65a6 --- /dev/null +++ b/ws_reverse_proxy_option_test.go @@ -0,0 +1,53 @@ +// Copyright 2023 CloudWeGo Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package reverseproxy + +import ( + "context" + "fmt" + "net/http" + "testing" + + "github.com/cloudwego/hertz/pkg/app" + "github.com/cloudwego/hertz/pkg/common/test/assert" + "github.com/gorilla/websocket" + hzws "github.com/hertz-contrib/websocket" +) + +func TestOptions(t *testing.T) { + director := func(ctx context.Context, c *app.RequestContext, forwardHeader http.Header) { + forwardHeader.Add("X-Test-Head", "content") + } + dialer := websocket.DefaultDialer + upgrader := &hzws.HertzUpgrader{ + ReadBufferSize: 64, + WriteBufferSize: 64, + } + options := newOptions( + WithDirector(director), + WithDialer(dialer), + WithUpgrader(upgrader), + ) + assert.DeepEqual(t, fmt.Sprintf("%p", director), fmt.Sprintf("%p", options.Director)) + assert.DeepEqual(t, fmt.Sprintf("%p", dialer), fmt.Sprintf("%p", options.Dialer)) + assert.DeepEqual(t, fmt.Sprintf("%p", upgrader), fmt.Sprintf("%p", options.Upgrader)) +} + +func TestDefaultOptions(t *testing.T) { + options := newOptions() + assert.Nil(t, options.Director) + assert.DeepEqual(t, DefaultOptions.Dialer, options.Dialer) + assert.DeepEqual(t, DefaultOptions.Upgrader, options.Upgrader) +} diff --git a/ws_reverse_proxy_test.go b/ws_reverse_proxy_test.go new file mode 100644 index 0000000..b372ea7 --- /dev/null +++ b/ws_reverse_proxy_test.go @@ -0,0 +1,124 @@ +// Copyright 2023 CloudWeGo Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package reverseproxy + +import ( + "context" + "net/http" + "testing" + "time" + + "github.com/cloudwego/hertz/pkg/app" + "github.com/cloudwego/hertz/pkg/app/server" + "github.com/cloudwego/hertz/pkg/common/hlog" + "github.com/cloudwego/hertz/pkg/common/test/assert" + "github.com/gorilla/websocket" + hzws "github.com/hertz-contrib/websocket" +) + +func BenchmarkNewWSReverseProxy(b *testing.B) { + for i := 0; i < b.N; i++ { + _ = NewWSReverseProxy("ws://localhost:8888/echo") + } +} + +var ( + proxyURL = "ws://127.0.0.1:7777" + backendURL = "ws://127.0.0.1:8888" +) + +func TestProxy(t *testing.T) { + // websocket proxy + supportedSubProtocols := []string{"test-protocol"} + upgrader := &hzws.HertzUpgrader{ + ReadBufferSize: 4096, + WriteBufferSize: 4096, + CheckOrigin: func(c *app.RequestContext) bool { + return true + }, + Subprotocols: supportedSubProtocols, + } + + proxy := NewWSReverseProxy(backendURL, WithUpgrader(upgrader)) + + // proxy server + ps := server.Default(server.WithHostPorts(":7777")) + ps.GET("/proxy", proxy.ServeHTTP) + go ps.Spin() + + time.Sleep(time.Millisecond * 100) + + go func() { + // backend server + bs := server.Default() + bs.GET("/", func(ctx context.Context, c *app.RequestContext) { + // Don't upgrade if original host header isn't preserved + host := string(c.Host()) + if host != "127.0.0.1:7777" { + hlog.Errorf("Host header set incorrectly. Expecting 127.0.0.1:7777 got %s", host) + return + } + + if err := upgrader.Upgrade(c, func(conn *hzws.Conn) { + msgType, msg, err := conn.ReadMessage() + assert.Nil(t, err) + + if err = conn.WriteMessage(msgType, msg); err != nil { + return + } + }); err != nil { + hlog.Errorf("upgrade error: %v", err) + return + } + }) + bs.Spin() + }() + + time.Sleep(time.Millisecond * 100) + + // only one is supported by the server + clientSubProtocols := []string{"test-protocol", "test-notsupported"} + h := http.Header{} + for _, subproto := range clientSubProtocols { + h.Add("Sec-WebSocket-Protocol", subproto) + } + + // client + conn, resp, err := websocket.DefaultDialer.Dial(proxyURL+"/proxy", h) + assert.Nil(t, err) + + // check if the server really accepted the correct protocol + in := func(desired string) bool { + for _, proto := range resp.Header[http.CanonicalHeaderKey("Sec-WebSocket-Protocol")] { + if desired == proto { + return true + } + } + return false + } + + assert.True(t, in("test-protocol")) + assert.False(t, in("test-notsupported")) + + // now write a message and send it to the proxy + msg := "hello world" + err = conn.WriteMessage(websocket.TextMessage, []byte(msg)) + assert.Nil(t, err) + + msgType, data, err := conn.ReadMessage() + assert.Nil(t, err) + assert.DeepEqual(t, websocket.TextMessage, msgType) + assert.DeepEqual(t, msg, string(data)) +}