diff --git a/README.md b/README.md index de1905d..a294384 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,9 @@ //deploy your app to heroku git push heroku main ``` +or you can deploy with Heroku with `Deploy To Heroku` button below: + +[![Deploy To Heroku](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy) ### Client side ``` @@ -17,11 +20,12 @@ go run main.go -client -proxyServer yourProxyServer.herokuapp.com go run main.go -client -port 3000 ``` -Now your local server is exposed to the internet, you could visit it by http://yourProxyServer.herokuapp.com +Now your local server is exposed to the internet, you can visit it by https://yourProxyServer.herokuapp.com ## Useful docs - [Architecture of ggrok](./docs/architecture.md) +- [Introduction about ggrok](./docs/introduce.md) - [How to run this project locally](./docs/run-locally.md) - [How to debug concurrent issues](./docs/debug.md) - [Useful Heroku command](./docs/heroku-command.md) \ No newline at end of file diff --git a/core/ggrok-client.go b/core/ggrok-client.go index 7ae3d12..dcc3b20 100644 --- a/core/ggrok-client.go +++ b/core/ggrok-client.go @@ -104,7 +104,6 @@ func readWebSocketReq(c *websocket.Conn) WebSocketRequest { } // deserialize request -//TODO: change to config func socketToLocalRequest(websocketReq WebSocketRequest, port int) *http.Request { r := bufio.NewReader(bytes.NewReader([]byte(websocketReq.Req))) localRequest, err := http.ReadRequest(r) diff --git a/docs/flow.jpg b/docs/flow.jpg new file mode 100644 index 0000000..804b7d9 Binary files /dev/null and b/docs/flow.jpg differ diff --git a/docs/introduce-zh-CN.md b/docs/introduce-zh-CN.md new file mode 100644 index 0000000..356a9bc --- /dev/null +++ b/docs/introduce-zh-CN.md @@ -0,0 +1,173 @@ +# 基于Golang和WebSocket打造自已的反向代理 + +当我们在开发的时候,有时想要把自已开发的接口暴露给其他开发者或者第三方的服务,方便我们调试和排查问题,那就需要某种机制把我们本地的服务接口暴露到互联网上,本文将要介绍如何通过Golang和WebSocket来实现这一功能 + +## 为什么我们需要开发自已的代理服务 + +目前已经有许多可用的代理服务了,比如ngrok和localtunnel,但ngrok有个缺点就是提供的域名只能用几个小时,然后需要新生成新的域名,如果想要固定域名就要花钱,但我们自已实现的代理可以用一个固定的域名,如果给前端同学来调试的话,不用改来改去,很方便。 + +## ggrok简介 + +![ggrok-flow](./flow.jpg) + +ggrok是通过Golang和WebSocket实现的代理应用,你可以通过Github[仓库](https://github.com/onyas/ggrok)上的Heroku按钮非常方便的部署,然后就可以拥有一个固定的域名了。 + +## 如何实现 + +### Step1 在服务器和客户端建立WebSocket连接 + +服务端基于gorilla,监听WebSocket连接 + +```golang +func (s *Server) Register(w http.ResponseWriter, r *http.Request) { + c, err := upgrader.Upgrade(w, r, nil) + if err != nil { + log.Print("upgrade:", err) + return + } + gconn := &Connection{ + Socket: c, + mu: sync.Mutex{}, + } + connections[r.Host] = gconn + log.Println("current connections: ", connections) +} + +http.HandleFunc("/$$ggrok", s.Register) +``` + +客户端连接服务端 + +```golang +func (ggclient *GGrokClient) Proxy() { + interrupt := make(chan os.Signal, 1) + signal.Notify(interrupt, os.Interrupt) + + u := url.URL{Scheme: "ws", Host: ggclient.RemoteServer, Path: "/$$ggrok"} + log.Printf("connecting to %s", u.String()) + + c, _, err := websocket.DefaultDialer.Dial(u.String(), nil) + if err != nil { + log.Fatal("dial:", err) + } + defer c.Close() + + done := make(chan struct{}) + + for { + select { + case <-done: + return + } + } +} +``` + + +### Step2 服务端收到http请求以后转成WebSocket消息转发给客户端 + +```golang +http.HandleFunc("/", s.Proxy) + +func (s *Server) Proxy(w http.ResponseWriter, r *http.Request) { + remoteConn := connections[r.Host] + if remoteConn == nil || remoteConn.Socket == nil { + io.WriteString(w, "client not register") + return + } + + wsRequest := httpRequestToWebSocketRequest(r) + + wsRes := triggerWS(remoteConn, wsRequest) +} + +func triggerWS(remoteConn *Connection, reqRemote WebSocketRequest) WebSocketResponse { + remoteConn.mu.Lock() + defer remoteConn.mu.Unlock() + + remoteConn.Socket.WriteJSON(reqRemote) + + var wsRes WebSocketResponse + err := remoteConn.Socket.ReadJSON(&wsRes) + if err != nil { + log.Println("read remote client response error", err) + } + log.Println("remote client response: ", wsRes) + return wsRes +} + +func httpRequestToWebSocketRequest(r *http.Request) (ws WebSocketRequest) { + reqStr, err := captureRequestData(r) + if err != nil { + log.Println("captureRequestData error:", err) + } + log.Println("req serialized: ", reqStr) + + reqRemote := WebSocketRequest{Req: reqStr, URL: r.URL.String()} + return reqRemote +} +``` + +### Step3 客户端收到WebSocket消息以后转发到LocalServer,并把LocalServer的响应返回给服务端 + +```golang +go func() { + defer close(done) + for { + websocketReq := readWebSocketReq(c) + + localRequest := socketToLocalRequest(websocketReq, ggclient.ProxyLocalPort) + resp, err := (&http.Client{}).Do(localRequest) + if err != nil { + log.Println("local http request error:", err) + continue + } + + wsRes := localResponseToWebSocketResponse(resp) + + // log.Printf("client send response: %s \n", wsRes.Body) + c.WriteJSON(wsRes) + } + }() + +func socketToLocalRequest(websocketReq WebSocketRequest, port int) *http.Request { + r := bufio.NewReader(bytes.NewReader([]byte(websocketReq.Req))) + localRequest, err := http.ReadRequest(r) + if err != nil { + log.Println("deserialize request error", err) + return localRequest + } + + localRequest.RequestURI = "" + u, err := url.Parse(websocketReq.URL) + if err != nil { + log.Println("parse url error", err) + } + localRequest.URL = u + localRequest.URL.Scheme = "http" + localRequest.URL.Host = "localhost:" + strconv.Itoa(port) + return localRequest +} + +func localResponseToWebSocketResponse(resp *http.Response) WebSocketResponse { + body, err := io.ReadAll(resp.Body) + if err != nil { + log.Println("read local response error ", err) + } + resp.Body.Close() + wsRes := WebSocketResponse{Status: resp.Status, StatusCode: resp.StatusCode, + Proto: resp.Proto, Header: resp.Header, Body: body, ContentType: resp.Header.Get("Content-Type")} + return wsRes +} +``` + +### Step4 服务端收到响应以后返回给前端 + +```golang +func wsResToHttpResponse(w http.ResponseWriter, wsRes WebSocketResponse) { + copyHeader(w, wsRes) + io.Copy(w, bytes.NewReader(wsRes.Body)) +} +``` + +至此,通过ggrok我们实现了本地服务的代理,并发布到互联网上。以上是一些主要的代码,详细的可以看[github](https://github.com/onyas/ggrok)上面的代码,有问题请提issue或者pr,共同打造更健壮的开源系统。 \ No newline at end of file diff --git a/docs/introduce.md b/docs/introduce.md new file mode 100644 index 0000000..ed6bc62 --- /dev/null +++ b/docs/introduce.md @@ -0,0 +1,173 @@ +# Create your own reverse proxy based on Golang and WebSocket + +When we are developing, sometimes we want to expose our own developed interfaces to other developers or third-party services to facilitate our debugging and troubleshooting, so we need some mechanism to expose our local service interfaces to the Internet. This article will introduce how to realize this function through golang and WebSocket + +## Why do we need to develop our own proxy services + +Currently, many proxy services are available, such as ngrok and localtunnel. However, ngrok has a disadvantage: the domain name provided can only be used for a few hours, and then a new domain name needs to be generated. If you want a fixed domain name, you need to spend money. However, our own proxy can use a fixed domain name. If frontend developers use it, it is very convenient without changing the domain over time. + +## ggrok introduction + +![ggrok-flow](./flow.jpg) + +Ggrok is a proxy application implemented through golang and WebSocket. You can use the Heroku button on the GitHub [repo](https://github.com/onyas/ggrok) to deploy it conveniently, and then you can have a fixed domain name. + +## How to implement + +### Step1 establish a WebSocket connection between the server and the client + +The server is based on the gorilla and listens for WebSocket connections + +```golang +func (s *Server) Register(w http.ResponseWriter, r *http.Request) { + c, err := upgrader.Upgrade(w, r, nil) + if err != nil { + log.Print("upgrade:", err) + return + } + gconn := &Connection{ + Socket: c, + mu: sync.Mutex{}, + } + connections[r.Host] = gconn + log.Println("current connections: ", connections) +} + +http.HandleFunc("/$$ggrok", s.Register) +``` + +The Client connect to the Server + +```golang +func (ggclient *GGrokClient) Proxy() { + interrupt := make(chan os.Signal, 1) + signal.Notify(interrupt, os.Interrupt) + + u := url.URL{Scheme: "ws", Host: ggclient.RemoteServer, Path: "/$$ggrok"} + log.Printf("connecting to %s", u.String()) + + c, _, err := websocket.DefaultDialer.Dial(u.String(), nil) + if err != nil { + log.Fatal("dial:", err) + } + defer c.Close() + + done := make(chan struct{}) + + for { + select { + case <-done: + return + } + } +} +``` + +### Step2 After receiving the HTTP request, the server converts it into a websocket message and forwards it to the client + + +```golang +http.HandleFunc("/", s.Proxy) + +func (s *Server) Proxy(w http.ResponseWriter, r *http.Request) { + remoteConn := connections[r.Host] + if remoteConn == nil || remoteConn.Socket == nil { + io.WriteString(w, "client not register") + return + } + + wsRequest := httpRequestToWebSocketRequest(r) + + wsRes := triggerWS(remoteConn, wsRequest) +} + +func triggerWS(remoteConn *Connection, reqRemote WebSocketRequest) WebSocketResponse { + remoteConn.mu.Lock() + defer remoteConn.mu.Unlock() + + remoteConn.Socket.WriteJSON(reqRemote) + + var wsRes WebSocketResponse + err := remoteConn.Socket.ReadJSON(&wsRes) + if err != nil { + log.Println("read remote client response error", err) + } + log.Println("remote client response: ", wsRes) + return wsRes +} + +func httpRequestToWebSocketRequest(r *http.Request) (ws WebSocketRequest) { + reqStr, err := captureRequestData(r) + if err != nil { + log.Println("captureRequestData error:", err) + } + log.Println("req serialized: ", reqStr) + + reqRemote := WebSocketRequest{Req: reqStr, URL: r.URL.String()} + return reqRemote +} +``` + +### Step3 After receiving the websocket message, the client forwards it to the localserver and returns the response of the localserver to the server + +```golang +go func() { + defer close(done) + for { + websocketReq := readWebSocketReq(c) + + localRequest := socketToLocalRequest(websocketReq, ggclient.ProxyLocalPort) + resp, err := (&http.Client{}).Do(localRequest) + if err != nil { + log.Println("local http request error:", err) + continue + } + + wsRes := localResponseToWebSocketResponse(resp) + + // log.Printf("client send response: %s \n", wsRes.Body) + c.WriteJSON(wsRes) + } + }() + +func socketToLocalRequest(websocketReq WebSocketRequest, port int) *http.Request { + r := bufio.NewReader(bytes.NewReader([]byte(websocketReq.Req))) + localRequest, err := http.ReadRequest(r) + if err != nil { + log.Println("deserialize request error", err) + return localRequest + } + + localRequest.RequestURI = "" + u, err := url.Parse(websocketReq.URL) + if err != nil { + log.Println("parse url error", err) + } + localRequest.URL = u + localRequest.URL.Scheme = "http" + localRequest.URL.Host = "localhost:" + strconv.Itoa(port) + return localRequest +} + +func localResponseToWebSocketResponse(resp *http.Response) WebSocketResponse { + body, err := io.ReadAll(resp.Body) + if err != nil { + log.Println("read local response error ", err) + } + resp.Body.Close() + wsRes := WebSocketResponse{Status: resp.Status, StatusCode: resp.StatusCode, + Proto: resp.Proto, Header: resp.Header, Body: body, ContentType: resp.Header.Get("Content-Type")} + return wsRes +} +``` + +### Step 4 After receiving the response, the server returns it to the http response + +```golang +func wsResToHttpResponse(w http.ResponseWriter, wsRes WebSocketResponse) { + copyHeader(w, wsRes) + io.Copy(w, bytes.NewReader(wsRes.Body)) +} +``` + +So far, though ggrok, we have implemented the local service proxy and published it on the Internet. The above are some main codes. See [GitHub](https://github.com/onyas/ggrok) for details. Please create issue or PR if you have any problems, and jointly create a more robust open source system. \ No newline at end of file